I'm trying to make open source Cache library. The purpose of library is to provide the way of storing the variable (can be object, can be be array, can be anything) to files and then retrieved it back on call. (usually those variable value is result of massive database queries and calculations).
The basic aim of the project is to practice the Object Oriented Design Principle Called Solid.
If any one can indicate where i'm violating solid principle and how to fix it
I totally understand stackoverflow is not a code writing service but hey i'm making this library open source, so it would benefit our community.
So here is my file structure.
I'm new to UML so please ignore if any errors found
here is the classes implementation.
Cache
namespace library\pingle\cache;
use library\pingle\cache\config\CacheConfigurator;
use library\pingle\cache\file\FileHandler;
/**
* #property CacheReader $cache_reader
* #property CacheWriter $cache_write
*/
Class Cache {
private $config;
private $file_hander;
private $cache_reader;
private $cache_write;
private $cache_directory;
private $cache_kept_days;
private $cache_file_prams;
private $function_name;
private $file_path;
function __construct(CacheConfigurator $config) {
$this->file_hander = new FileHandler();
$this->config = $config;
list($this->cache_directory, $this->function_name, $this->cache_kept_days, $this->cache_file_prams) = $this->config->getConfig();
$this->file_path = $this->generateFileName($this->cache_file_prams);
}
public function read() {
if (is_null($this->cache_reader)) {
$this->cache_reader = new CacheReader($this->file_hander);
}
return $this->cache_reader->readCache($this->file_path);
}
public function write($data) {
if (is_null($this->cache_write)) {
$this->cache_write = new CacheWriter($this->file_hander);
}
if (!$this->file_hander->checkDirectory($this->cache_directory . "/" . $this->function_name)) {
$this->file_hander->createDirectory($this->cache_directory . "/" . $this->function_name);
}
$this->cache_write->writeCache($this->file_path, $data);
}
public function check() {
if ($this->file_hander->checkFileExits($this->file_path)) {
if (time() - filemtime($this->file_path) >= 60 * 60 * 24 * $this->cache_kept_days) {
return false;
}
return true;
} else {
return false;
}
}
private function generateFileName(Array $nameprams) {
$this->file_name = "";
$file = "CC";
foreach ($nameprams as $key => $value) {
$file .= "-$key|$value-";
}
$file .= ".bak";
return $this->cache_directory . "/" . $this->function_name . "/" . $file;
}
}
AbstractCache
<?php
namespace library\pingle\cache;
use library\pingle\cache\file\FileHandler;
abstract Class AbstractCache {
protected $file_handler;
public function __construct(FileHandler $file_handler) {
$this->file_handler = $file_handler;
}
protected function checkDirectory($path) {
//check directory exists
$dircheck = $this->file_handler->checkDirectory(dirname($path));
if ($dircheck) {
//check directory permission
if ($this->file_handler->checkPermission(dirname($path))) {
return true;
} else {
throw new \Exception("Directory ($path) Permission Error.");
}
} else {
throw new \Exception("Directory ($path) not found.");
}
}
}
CacheReader
<?php
namespace library\pingle\cache;
use library\pingle\cache\file\FileHandler;
/**
* #property FileHandler $file_handler
*/
Class CacheReader extends AbstractCache {
public function __construct(FileHandler $file_handler) {
parent::__construct($file_handler);
}
public function readCache($path) {
if ($this->checkDirectory($path)) {
//delete the file if it exits
if ($this->file_handler->checkFileExits($path)) {
return $this->file_handler->readFile($path);
} else {
throw new \Exception("File ($path) not found");
}
}
}
}
CacheWriter
<?php
namespace library\pingle\cache;
use library\pingle\cache\file\FileHandler;
/**
* #property FileHandler $file_handler
*/
Class CacheWriter extends AbstractCache {
public function __construct(FileHandler $file_handler) {
parent::__construct($file_handler);
}
function writeCache($path, $data) {
if ($this->checkDirectory($path)) {
//delete the file if it exits
if ($this->file_handler->checkFileExits($path)) {
$this->file_handler->deleteFile($path);
}
//write cache
$this->file_handler->writeFile($path, $data);
}
}
}
FileHandler
<?php
namespace library\pingle\cache\file;
Class FileHandler {
public function writeFile($path, $data) {
$content = serialize($data);
file_put_contents($path, $content);
}
public function createDirectory($path) {
mkdir($path);
}
public function deleteFile($path) {
unlink($path);
}
public function checkDirectory($path) {
if (file_exists($path)) {
return true;
} else {
return false;
}
}
public function checkPermission($path) {
if (is_writable($path)) {
return true;
} else {
return false;
}
}
public function checkFileExits($path) {
if (is_file($path)) {
return true;
}
return false;
}
public function readFile($path) {
return unserialize(file_get_contents($path));
}
public function checkFileCreated($path, $format = "Y-m-d") {
return date($format, filemtime($path));
}
}
CacheConfigurator
<?php
namespace library\pingle\cache\config;
/**
* #property PramsFormatter $prams_formatter
*/
class CacheConfigurator {
private $prams_formatter;
private $cache_directory;
private $cache_kept_days;
private $cache_file_prams;
private $function_name;
function __construct($file_prams) {
$this->cache_file_prams = $file_prams;
$this->cache_directory = ""; //Def Path
}
public function setCacheDirectory($cache_directory) {
$this->cache_directory = $cache_directory;
return $this;
}
public function setFunction($function) {
$this->function_name = $function;
return $this;
}
public function setCacheKeptDays($cache_kept_days) {
$this->cache_kept_days = $cache_kept_days;
return $this;
}
public function getConfig() {
$this->prams_formatter = new PramsFormatter($this->cache_file_prams);
$this->cache_file_prams = $this->prams_formatter->getFormattedPrams();
$this->function_name = $this->prams_formatter->cleanValue($this->function_name);
return array($this->cache_directory, $this->function_name, $this->cache_kept_days, $this->cache_file_prams);
}
}
PramsFormatter
<?php
namespace library\pingle\cache\config;
class PramsFormatter {
private $cache_file_prams;
public function __construct(Array $prams) {
$this->cache_file_prams = $prams;
$this->formatPrams();
}
public function formatPrams() {
if (is_array($this->cache_file_prams)) {
foreach ($this->cache_file_prams as $k => $value) {
$this->cache_file_prams[$k] = $this->cleanValue($value);
}
}
}
public function cleanValue($value) {
if (is_array($value)) {
throw new \Exception("Array as paramter value is not accepted");
} else {
return str_replace(array(" ", " ", ".", "/", "\\"), "-", $value);
}
}
public function getFormattedPrams() {
return $this->cache_file_prams;
}
}
Usage
$cache_config = new CacheConfigurator(array('carrier_id' => $invoicedata['carrier_id'], 'month' => $month, 'year' => $year));
$cache_config->setFunction('Inter-department Calls');
$cache_config->setCacheKeptDays(30);
$cache_config->setCacheDirectory("bin/cache");
$cache = new Cache($cache_config);
if ($cache->check()) {
$node = $cache->read();
} else {
//calculate node
$cache->write($node);
}
Git Repository with Improve Design
https://github.com/FaizRasool/EPC
Very good question, but an entire book could probably be written on this, which makes it quite hard to answer.
I'd start with this simple question: what better describes caching in the choices below?
Caching is a mechanism that allows to store the result of a function to a file for a number of days in order to provide fast access to it.
Caching is a mechanism that allows to retain the result of an operation for as long as the associated retention policy is satisfied in order to provide fast access to it.
None of the definitions are perfect and that's not the point, but what I wanted to emphasis is that #1 explains caching with very specific infrastructure details while #2 defines the mechanism in a more abstract way.
Hopefully, you will now have realized one of the biggest flaws of your design IMO. The various abstractions are wrong:
The abstraction of the storage mechanism is dependent of a specific infrastructure detail: the entire API revolves around files. What if I wanted an in-memory cache?
The data retention policy algorithm is very specific: data will be retained only for a specific number of days. What if I wanted to express the cache in minutes where the counter resets evertime the data is accessed?
One advice I'd give you is to always challenge your abstractions and make sure that they are not too specific so that your code can be extensible and reusable, but not too wide either. Paying attention to the language of the problem domain greatly helps with that.
There's obviously much more than this that could be said and sometimes being technology-dependent is the right choice, but I think my answer will help...
Related
I'm implementing a search functionality and based on the query parameter i use a different class to search.
class Search {
public function getResults()
{
if (request('type') == 'thread') {
$results = app(SearchThreads::class)->query();
} elseif (request('type') == 'profile_post') {
$results = app(SearchProfilePosts::class)->query();
} elseif (request()->missing('type')) {
$results = app(SearchAllPosts::class)->query();
}
}
Now when i want to search threads i have the following code.
class SearchThreads{
public function query()
{
$searchQuery = request('q');
$onlyTitle = request()->boolean('only_title');
if (isset($searchQuery)) {
if ($onlyTitle) {
$query = Thread::search($searchQuery);
} else {
$query = Threads::search($searchQuery);
}
} else {
if ($onlyTitle) {
$query = Activity::ofThreads();
} else {
$query = Activity::ofThreadsAndReplies();
}
}
}
}
To explain the code.
If the user enters a search word ( $searchQuery) then use Algolia to search, otherwise make a database query directly.
If the user enters a search word
Use the Thread index if the user has checked the onlyTitle checkbox
Use the Threads index if the user hasn't checked the onlyTitle checkbox
If the user doesn't enter a search word
Get all the threads if the user has checked the onlyTitle checkbox
Get all the threads and replies if the user hasn't checked the onlyTitle checkbox
Is there a pattern to simplify the nested if statements or should i just create a separate class for the cases where
a user has entered a search word
a user hasn't entered a search word
And inside each of those classes to check if the user has checked the onlyTitle checkbox
I would refactor this code to this:
Leave the request parameter to unify the search methods in an interface.
interface SearchInterface
{
public function search(\Illuminate\Http\Request $request);
}
class Search {
protected $strategy;
public function __construct($search)
{
$this->strategy = $search;
}
public function getResults(\Illuminate\Http\Request $request)
{
return $this->strategy->search($request);
}
}
class SearchFactory
{
private \Illuminate\Contracts\Container\Container $container;
public function __construct(\Illuminate\Contracts\Container\Container $container)
{
$this->container = $container;
}
public function algoliaFromRequest(\Illuminate\Http\Request $request): Search
{
$type = $request['type'];
$onlyTitle = $request->boolean('only_title');
if ($type === 'thread' && !$onlyTitle) {
return $this->container->get(Threads::class);
}
if ($type === 'profile_post' && !$onlyTitle) {
return $this->container->get(ProfilePosts::class);
}
if (empty($type) && !$onlyTitle) {
return $this->container->get(AllPosts::class);
}
if ($onlyTitle) {
return $this->container->get(Thread::class);
}
throw new UnexpectedValueException();
}
public function fromRequest(\Illuminate\Http\Request $request): Search
{
if ($request->missing('q')) {
return $this->databaseFromRequest($request);
}
return $this->algoliaFromRequest($request);
}
public function databaseFromRequest(\Illuminate\Http\Request $request): Search
{
$type = $request['type'];
$onlyTitle = $request->boolean('only_title');
if ($type === 'thread' && !$onlyTitle) {
return $this->container->get(DatabaseSearchThreads::class);
}
if ($type === 'profile_post' && !$onlyTitle) {
return $this->container->get(DatabaseSearchProfilePosts::class);
}
if ($type === 'thread' && $onlyTitle) {
return $this->container->get(DatabaseSearchThread::class);
}
if ($request->missing('type')) {
return $this->container->get(DatabaseSearchAllPosts::class);
}
throw new InvalidArgumentException();
}
}
final class SearchController
{
private SearchFactory $factory;
public function __construct(SearchFactory $factory)
{
$this->factory = $factory;
}
public function listResults(\Illuminate\Http\Request $request)
{
return $this->factory->fromRequest($request)->getResults($request);
}
}
The takeaway from this is it is very important to not involve the request in the constructors. This way you can create instances without a request in the application lifecycle. This is good for caching, testability and modularity. I also don't like the app and request methods as they pull variables out of thin air, reducing testability and performance.
class Search
{
public function __construct(){
$this->strategy = app(SearchFactory::class)->create();
}
public function getResults()
{
return $this->strategy->search();
}
}
class SearchFactory
{
public function create()
{
if (request()->missing('q')) {
return app(DatabaseSearch::class);
} else {
return app(AlgoliaSearch::class);
}
}
}
class AlgoliaSearch implements SearchInterface
{
public function __construct()
{
$this->strategy = app(AlgoliaSearchFactory::class)->create();
}
public function search()
{
$this->strategy->search();
}
}
class AlgoliaSearchFactory
{
public function create()
{
if (request('type') == 'thread') {
return app(Threads::class);
} elseif (request('type') == 'profile_post') {
return app(ProfilePosts::class);
} elseif (request()->missing('type')) {
return app(AllPosts::class);
} elseif (request()->boolean('only_title')) {
return app(Thread::class);
}
}
}
Where the classes created in the AlgoliaSearchFactory are algolia aggregators so the search method can be called on any of those classes.
Would something like this make it cleaner or even worse ?
Right now i have strategies that have strategies which sounds too much to me.
I have tried to implement a good solution for you, but I had to make some assumptions about the code.
I decoupled the request from the constructor logic and gave the search interface a request parameter. This makes the intention clearer than just pulling the Request from thin air with the request function.
final class SearchFactory
{
private ContainerInterface $container;
/**
* I am not a big fan of using the container to locate the dependencies.
* If possible I would implement the construction logic inside the methods.
* The only object you would then pass into the constructor are basic building blocks,
* independent from the HTTP request (e.g. PDO, AlgoliaClient etc.)
*/
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
private function databaseSearch(): DatabaseSearch
{
return // databaseSearch construction logic
}
public function thread(): AlgoliaSearch
{
return // thread construction logic
}
public function threads(): AlgoliaSearch
{
return // threads construction logic
}
public function profilePost(): AlgoliaSearch
{
return // thread construction logic
}
public function onlyTitle(): AlgoliaSearch
{
return // thread construction logic
}
public function fromRequest(Request $request): SearchInterface
{
if ($request->missing('q')) {
return $this->databaseSearch();
}
// Fancy solution to reduce if statements in exchange for legibility :)
// Note: this is only a viable solution if you have done correct http validation IMO
$camelCaseType = Str::camel($request->get('type'));
if (!method_exists($this, $camelCaseType)) {
// Throw a relevent error here
}
return $this->$camelCaseType();
}
}
// According to the code you provided, algoliasearch seems an unnecessary wrapper class, which receives a search interface, just to call another search interface. If this is the only reason for its existence, I would remove it
final class AlgoliaSearch implements SearchInterface {
private SearchInterface $search;
public function __construct(SearchInterface $search) {
$this->search = $search;
}
public function search(Request $request): SearchInterface {
return $this->search->search($request);
}
}
I am also not sure about the point of the Search class. If it only effectively renames the search methods to getResults, I am not sure what the point is. Which is why I omitted it.
I had to write all this to make the problem understandable.
The SearchFactory takes all the required parameters and based on these parameters, it calls either AlgoliaSearchFactory or DatabaseSearchFactory to produce the final object that will be returned.
class SearchFactory
{
protected $type;
protected $searchQuery;
protected $onlyTitle;
protected $algoliaSearchFactory;
protected $databaseSearchFactory;
public function __construct(
$type,
$searchQuery,
$onlyTitle,
DatabaseSearchFactory $databaseSearchFactory,
AlgoliaSearchFactory $algoliaSearchFactory
) {
$this->type = $type;
$this->searchQuery = $searchQuery;
$this->onlyTitle = $onlyTitle;
$this->databaseSearchFactory = $databaseSearchFactory;
$this->algoliaSearchFactory = $algoliaSearchFactory;
}
public function create()
{
if (isset($this->searchQuery)) {
return $this->algoliaSearchFactory->create($this->type, $this->onlyTitle);
} else {
return $this->databaseSearchFactory->create($this->type, $this->onlyTitle);
}
}
}
The DatabaseSearchFactory based on the $type and the onlyTitle parameters that are passed from the SearchFactory returns an object which is the final object that needs to be used in order to get the results.
class DatabaseSearchFactory
{
public function create($type, $onlyTitle)
{
if ($type == 'thread' && !$onlyTitle) {
return app(DatabaseSearchThreads::class);
} elseif ($type == 'profile_post' && !$onlyTitle) {
return app(DatabaseSearchProfilePosts::class);
} elseif ($type == 'thread' && $onlyTitle) {
return app(DatabaseSearchThread::class);
} elseif (is_null($type)) {
return app(DatabaseSearchAllPosts::class);
}
}
}
Same logic with DatabaseSearchFactory
class AlgoliaSearchFactory
{
public function create($type, $onlyTitle)
{
if ($type == 'thread' && !$onlyTitle) {
return app(Threads::class);
} elseif ($type == 'profile_post' && !$onlyTitle) {
return app(ProfilePosts::class);
} elseif (empty($type) && !$onlyTitle) {
return app(AllPosts::class);
} elseif ($onlyTitle) {
return app(Thread::class);
}
}
}
The objects that are created by AlgoliaSearchFactory have a method search which needs a $searchQuery value
interface AlgoliaSearchInterface
{
public function search($searchQuery);
}
The objects that are created by DatabaseSearchFactory have a search method that doesn't need any parameters.
interface DatabaseSearchInterface
{
public function search();
}
The class Search now takes as a parameter the final object that is produced by SearchFactory which can either implement AlgoliaSearchInterface or DatabaseSearchInterface that's why I haven't type hinted
The getResults method now has to find out the type of the search variable ( which interface it implements ) in order to either pass the $searchQuery as a parameter or not.
And that is how a controller can use the Search class to get the results.
class Search
{
protected $strategy;
public function __construct($search)
{
$this->strategy = $search;
}
public function getResults()
{
if(isset(request('q')))
{
$results = $this->strategy->search(request('q'));
}
else
{
$results = $this->strategy->search();
}
}
}
class SearchController(Search $search)
{
$results = $search->getResults();
}
According to all of #Transitive suggestions this is what I came up with. The only thing that I cannot solve is how to call search in the getResults method without having an if statement.
I am new to PHP & Codeigniter but it was needed some kind of implementation in PHP.
Following are dirty methods are provided in rails framework by default, here person is model object representing row inside persons table.
person.name = 'Bob'
person.changed? # => true
person.name_changed? # => true
person.name_changed?(from: nil, to: "Bob") # => true
person.name_was # => nil
person.name_change # => [nil, "Bob"]
person.name = 'Bill'
person.name_change # => [nil, "Bill"]
I am interested in to & from specially, Please suggest whether it is possible with any way.
If you would consider Laravel's elquent framework you have a great deal of that functionality already.
Laravel Eloquent update just if changes have been made
It holds an array of the "original" values in the Model, and if any of them have been changed it will commit them to the database.
They also come with a lot of events you can plug into(beforeSave, afterSave, beforeCreate, afterCreate, validation rules, etc...) and they can be extended easily. It might be the closest compatible thing I can imagine you're looking for.
This is however not codeigniter, it relies on a different framework. If you're not dead set on codeigniter you might consider switching to a framework like Laravel or OctoberCMS depending on your needs.
Edit because you're stuck with codeigniter
You might wish to use a library like this one: https://github.com/yidas/codeigniter-model
It is then very easy to extend with some custom caching mechanisms.
The code below is something you could use as a basis for your own model implementation.
It has a very rudementary logic basis, but allows you to check on the dirty status and roll back any changes made to the model.
Note this this is very rudementary, and might even contain a few errors because I have not run this code. it's more a proof of concept to help you create a model that suits your needs.
class User extends CI_Model
{
public $table = 'users';
public $primaryKey = 'id';
public $attributes;
public $original;
public $dirty = [];
public $exists = false;
function __construct()
{
parent::Model();
}
public static function find($model_id)
{
$static = new static;
$query = $static->db->query("SELECT * FROM ' . $static->getTable() . ' WHERE ' . $this->getKeyName() . ' = ?", [$model_id]);
if($result->num_rows()) {
$static->fill($query->row_array());
$static->exists = true;
}
else {
return null;
}
return $static;
}
public function getKeyName()
{
return $this->primaryKey;
}
public function getKey()
{
return $this->getAttribute($this->getKeyName());
}
public function getTable()
{
return $this->table;
}
public function fill($attributes)
{
if(is_null($this->original)) {
$this->original = $attributes;
$this->dirty = $attributes;
}
else {
foreach($attributes as $key => $value) {
if($this->original[$key] != $value) {
$this->dirty[$key] = $value;
}
}
}
$this->attributes = $attributes;
}
public function reset()
{
$this->dirty = [];
$this->attributes = $this->original;
}
public function getAttribute($attribute_name)
{
return array_key_exists($attribute_name, $this->attributes) ? $this->attributes[$attribute_name] : null;
}
public function __get($key)
{
return $this->getAttribute($key);
}
public function __set($key, $value)
{
$this->setAttribute($key, $value);
}
public function setAttribute($key, $value)
{
if(array_key_exists($key, $this->original)) {
if($this->original[$key] !== $value) {
$this->dirty[$key] = $value;
}
}
else {
$this->original[$key] = $value;
$this->dirty[$key] = $value;
}
$this->attributes[$key] = $value;
}
public function getDirty()
{
return $this->dirty;
}
public function isDirty()
{
return (bool)count($this->dirty);
}
public function save()
{
if($this->isDirty()) {
if($this->exists)
{
$this->db->where($this->getKeyName(), $this->getKey());
$this->db->update($this->getTable(), $this->getDirty());
$this->dirty = [];
$this->original = $this->attributes;
}
else
{
$this->db->insert($this->getTable(), $this->getDirty());
$this->dirty = [];
$this->original = $this->attributes;
$this->attributes[$this->getKeyName()] = $this->db->insert_id();
$this->original[$this->getKeyName()] = $this->getKey();
$this->exists = true;
}
}
}
}
if($user = User::find(1)) {
$user->name = "Johnny Bravo";
$user->save();
}
i'am not very well OO, and search a few web site to write this class
now my problem is how to use this class just like the Laravel 5.5 build in Storage class
i want use like this
MyStorage::disk('dropbox')
->addDirFilter('(school|travel)')
->addDirFilter('\d{4}-\d{2}-\d{2}')
->addFileFilter('.*\.jpg')
->getMatch();
here is MyStorage class
<?php
namespace App\MySupport;
class MyStorage
{
private $flag;
private $disk;
private $matches;
public function __construct()
{
$this->flag = false;
$this->disk = '';
$this->matches = [];
}
public function disk($disk)
{
$this->disk = $disk;
return $this;
}
public function addDirFilter($filter)
{
if (! $this->flag)
{
$this->flag = true;
$subDirs[] = \Storage::disk($this->disk)->directories();
}
else
{
foreach ($this->matches as $dir)
{
$subDirs[] = \Storage::disk($this->disk)->directories($dir);
}
}
$this->findMatch($subDirs, $filter);
return $this;
}
public function addFileFilter($filter)
{
if (! $this->flag)
{
$this->flag = true;
$subFiles[] = \Storage::disk($this->disk)->files();
}
else
{
foreach ($this->matches as $dir)
{
$subFiles[] = \Storage::disk($this->disk)->files($dir);
}
}
$this->findMatch($subFiles, $filter);
return $this;
}
public function findMatch($subItems, $filter)
{
// set empty before update
$this->matches = [];
// if call this method by addDirFilter() , $subItem contain the Dir path, eg, DirA/DirB
// if call this method by addFileFilter() , $subItem contain the File path, eg, DirX/File.txt
foreach (array_collapse($subItems) as $subItem)
{
// get the last str, eg, DirB OR File.txt
$lastStr = #end(explode('/', $subItem));
if ( preg_match('/^' . $filter . '$/u', $lastStr) )
{
// update new matches
$this->matches[] = $subItem;
}
}
}
public function getMatch()
{
return $this->matches;
}
}
Usage
<?php
namespace App\Http\Controllers;
use App\MySupport\MyStorage;
class TestController extends Controller
{
public function storageTest()
{
$MyStorage = new MyStorage();
// for example in my dropbox disk have
//
// school/2018-01-01/emails.txt
// school/2018-02-02/Peter.jpg
// travel/2017-06-06/TW.jpg
$folders = $MyStorage->disk('dropbox')
->addDirFilter('school')
->addDirFilter('\d{4}-\d{2}-\d{2}')
->getMatch();
// $folders result is:
// school/2018-01-01
// school/2018-02-02
$files = $MyStorage->disk('dropbox')
->addDirFilter('(school|travel)')
->addDirFilter('\d{4}-\d{2}-\d{2}')
->addFileFilter('.*\.jpg')
->getMatch();
// $files result is:
// school/2018-02-02/Peter.jpg
// travel/2017-06-06/TW.jpg
}
}
i tested seems all fine
can anyone point me to the direction, how to use MyStorage class just like Laravel 5.5 build in Storage class, thanks
You should use static function.
Try replace your function as code below.
public static function disk($disk)
{
$instance = new MyStorage();
$instance->disk = $disk;
return $instance;
Furthermore, I would suggest you learn singleton design pattern
I understand that one can use interfaces to mandate the definition of a function, but I cannot find something that enables one to mandate function calls, such that e.g. if I create a class being a member of another class (via extends, etc), with a function, for that class to automatically ensure that mandatory functions are called in part with that function.
I mean, to clarify further:
class domain {
function isEmpty($input) {
//apply conditional logic and results
}
}
class test extends domain {
function addTestToDBTable($test) {
/**
* try to add but this class automatically makes it so that all rules of
* class domain must be passed before it can run
* - so essentially, I am no longer required to call those tests for each and
* every method
**/
}
}
Apologies if this appears incoherent by any means. Sure, it seems lazy but I want to be able to force context without having to concern abou
Update:
Okay, to clarify further: in PHP, if I extend and declare a __construct() for a child class, that child class will override the parent __construct(). I do not want this, I want the parent construct to remain and mandate whatever as it pleases just as the child class may do so also.
I guess it can be done in two different ways.
Aspect Oriented Programming
Have a look here https://github.com/AOP-PHP/AOP
Generate or write Proxy classes
A really simple example could be:
<?php
class A {
public function callMe() {
echo __METHOD__ . "\n";
}
}
class B extends A {
// prevents instantiation
public function __construct() {
}
public function shouldCallMe() {
echo __METHOD__ . "\n";
}
public static function newInstance() {
return new ABProxy();
}
}
class ABProxy {
private $b;
public function __construct() {
$this->b = new B();
}
public function __call($method, $args) {
$this->b->callMe();
return call_user_func_array(array($this->b, $method), $args);
}
}
// make the call
$b = B::newInstance();
$b->shouldCallMe();
// Outputs
// ------------------
// A::callMe
// B::shouldCallMe
Hopes this helps a bit.
Sounds like you want a Decorator.
See This answer for a detailed explanation on how to do it. Note that it does not require a class extension.
I would use a domain-validating decorator with some doc-block metaprogramming magic. But this is really a job for an entire library, which no doubt exists.
fiddle
<?php
class FooDomain {
public static function is_not_empty($input) {
return !empty($input);
}
}
class Foo {
/**
* #domain FooDomain::is_not_empty my_string
*/
public function print_string($my_string) {
echo $my_string . PHP_EOL;
}
}
$foo = new DomainValidator(new Foo());
$foo->print_string('Hello, world!');
try {
$foo->print_string(''); // throws a DomainException
} catch (\DomainException $e) {
echo 'Could not print an empty string...' . PHP_EOL;
}
// ---
class DomainValidator {
const DOMAIN_TAG = '#domain';
private $object;
public function __construct($object) {
$this->object = $object;
}
public function __call($function, $arguments) {
if (!$this->verify_domain($function, $arguments)) {
throw new \DomainException('Bad domain!');
}
return call_user_func_array(
array($this->object, $function),
$arguments
);
}
public function __get($name) {
return $this->object->name;
}
public function __set($name, $value) {
$this->object->name = $value;
}
private function verify_domain($function, $arguments) {
// Get reference to method
$method = new \ReflectionMethod($this->object, $function);
$domains = $this->get_domains($method->getDocComment());
$arguments = $this->parse_arguments(
$method->getParameters(),
$arguments
);
foreach ($domains as $domain) {
if (!call_user_func(
$domain['name'],
$arguments[$domain['parameter']]
)) {
return false;
}
}
return true;
}
private function get_domains($doc_block) {
$lines = explode("\n", $doc_block);
$domains = array();
$domain_tag = DomainValidator::DOMAIN_TAG . ' ';
foreach ($lines as $line) {
$has_domain = stristr($line, $domain_tag) !== false;
if ($has_domain) {
$domain_info = explode($domain_tag, $line);
$domain_info = explode(' ', $domain_info[1]);
$domains[] = array(
'name' => $domain_info[0],
'parameter' => $domain_info[1],
);
}
}
return $domains;
}
private function parse_arguments($parameters, $values) {
$ret = array();
for ($i = 0, $size = sizeof($values); $i < $size; $i++) {
$ret[$parameters[$i]->name] = $values[$i];
}
return $ret;
}
}
Output:
Hello, world!
Could not print an empty string...
I have problem in using ExceptionMatcher...My example spec:
class DescribeBall extends \PHPSpec\Context {
private $_ball = null;
function before() {
$this->_ball = $this->spec(new Ball);
}
function itShouldHaveStatusRolledOnRoll() {
$this->_ball->roll();
$this->_ball->getStatus()->should->be('Rolled');
}
function itShouldThrowException() {
$this->_ball->getException()->should->throwException('Exception','Error');
}
}
My example class
class Ball {
private $status = null;
public function roll() {
$this->status = 'Rolled';
}
public function getStatus() {
return $this->status;
}
public function getException() {
throw new Exception('Error');
}
}
Anyone used this matcher with success?
$this->_ball->getException()->should->throwException('Exception','Error');
Thanks to my colleagues:
"The last time I looked at it, it used closures (unless Marcello changed it meanwhile) it should still work like this":
function itShouldThrowException() {
$ball = $this->_ball;
$this->spec(function() use ($ball) {
$ball->getException();
})->should->throwException('Exception','Error');
}