(continued)
Taking User as an example of the above
class User
{
protected static $config;
protected $storageHandler;
protected $dirty;
protected $username;
public function __construct($storageHandler)
{
$this->storageHandler = $storageHandler;
static::$config = [
'pimary' => 'id',
/* I'd rather stick to only true/false to designate required / non-required */
'data' => [
'username' => true,
'email' => true,
'age' => false,
],
'constraints' => [
'username' => 'is_string',
'email' => function( $v ){ return filter_var( $v,FILTER_VALIDATE_EMAIL ); },
'age' => 'is_int',
],
];
}
# anything relating to User that will be stored in the db goes through this function
public function setVar($name, $value)
{
if (!isset(static::$config['data'][$name]))
throw new Exception('unknown variable');
# stops assignment of '' to username…
if (empty($value) && static::$config['data'][$name])
throw new Exception($name . ' is a required field');
# …which would otherwise be allowed by this check
$validator = static::$config['constraints'][$name];
if (!$validator($value))
throw new Exception('validation error');
# if all is ok
if ($this->$name != $value) {
$this->$name = $value;
# track changes…
$this->dirty[$name] = $value;
}
}
# … so that we only need to update on dirty
public function store() {
$k = static::$config['primary'];
# Insert case, check that all required fields have been set
if (empty($this->$k)) {
foreach (static::$config['data'] as $field => $required)
if ($required && empty($this->$field))
throw new Exception('missing required field');
# assemble all data needing to be stored
$this->id = $this->storageHandler->store($data, 'id', null);
}
# Update case - as long as we do not allow clearing required fields
# we do not need to recheck that we have all fields, or keep re-setting them
# to the same value over and over again, which means this would suffice
elseif (count($this->dirty)) {
$this->storageHandler->store($this->dirty, 'id', $this->id);
}
}
}
$u = new User(null);
Question: Should store() etc be in a base class or implemented as behaviour? If you do not implement the config variables, the functionality provided by store(), setVar(), the config validator would all have to be implemented as class/variable-specific, and you'd find them as functions of Person, User, SuperUser, with children possibly redefining its parent's functions. However, if as above, you consider the code as being generic and separate from the data it validates and manipulates, while taking meta-data descriptors on how to handle your data, the code should be considered as behaviour instead, and implemented by composition.
We probably all agree that specifying setName in Person and redefining it in a child class User, if necessary, makes sense. Doing things the other way, would it make sense to move setVar into a separate entity and use composition? I'd say - yes. If ever you come upon a situation where you need to slightly modify how validation (or whatever else) is done, and you have one base class managing everything, I shudder to think about how much code would be affected by a slight change in setVar() which all/most classes inherit… What happens if something goes slightly wrong with, say password validation? And as soon as you're afraid of changing code, something is very very wrong.
If you instead use composition, you may instead either create a new setter/validation handler - from scratch or by extending the base handler. Then you plug the newly created or modified version into whatever class needed the change, thus achieving isolation in the exact same way as if that particular class had implemented changes to a particular setWhatever() function it needed. And once the new handler is up and running, if you wish to replace the old one in more/all places, you can do so one place at the time. Possibly still eliminating the need for the initial handler unless both are needed, while not risk breaking untested code.
Which one to choose then? As long as you don't take the generic functions with meta data input and put that in a base class, I'd say whichever way you find to be easier to maintain. But no matter how you turn the problem around, you will have to somehow validate all input, check that all required fields are present etc.
Also note that when autogenerating your classes, you may create your template along the lines of
public function __construct()
{
static::$config['constraints'] = [
'name' => 'varchar(15)',
'birthdate' => 'date',
'balls' => 'tinyint unsigned',
];
}
These "data type strings" will mean nothing as far as actual code goes. But when you look at the code, you have all the information at hand to start implementing your real validators.
sneakyimp;11034447 wrote:
I can then use these objects in my other model classes generally without having to worry about the database much -- unless of course I need to JOIN a few tables.
Once again, using composition, this is simple. Plug in a new storage handler which deals with these joins as needed.
There are 3 basic cases that you possibly need to cover: instance-to-row, collection and hierarchical. Wether you do this with a generic storage handler or not will depend on wether you need it to be generic or not. Instance to row is easy to implement and will be used by a lot of classes. And you may choose to implement it straight away.
But perhaps the only entity involving multiple tables on fetch and update is Order (the order and the items ordered). In that case, you implement the particulars either directly in Order::store(), Order::fetch(), or in a non-generic OrderStorage class used by Order. If the ordinary Storage::store will be using 3 parameters for updates ($map - key/value pair of data, $keyname - used for WHERE constraint, $keyvalue), the OrderStorage might take separate arrays for the Order data and for OrderItems data. Or they come nested in $map. Still, you will require a different algorithm to push this to storage.
Later down the road, you might wish to implement ImageGallery for your users, and realize that they share storage behaviour with Order - one entity contains a collection of other entities. Assuming the ordinary fetch and store operations are the same, you would simply generalize the rules at this point, stick it in CollectionStorage and pass an instance of this to Order instead of the old OrderStorage instances.
And in general, "worry about it the second time around" is usually a good way to go. That is, start by one single particular implementation without worrying about generalizing it. The second time you implement a similar thing , generalize what you have, move it to a separate entity and use that from both particular cases. This way you will not spend too much time thinking about what may or may not be down the road, while having real cases to look at when trying to build the general common functionality.
Once a third similar case pops up, you may wish to either add something to the general code, or slightly change something to accomodate all three cases.
As long as you keep "decent structure" to begin with, doing this is quite easy and you spend the effort on generlization when it is needed. But you should indeed do it the second time around, else you will end up with the same code repeated 10 times over…
If you find it hard to generalize what you have so far, you most likely need to refactor your present code first to separate responsibilities and achieve a decent structure to begin with. But in doing so, you learn both what a good structure is as well as the cost for not adhering to it.
While theoretical input may be invaluable help in reaching new insights, repeatedly having to sort out the messes I create for myself by "recrusive coding" (code -> snag -> refactor -> snag -> refactor) is by far the best teacher in my experience.