In this short write-up, I will briefly summarize the types of modeling I have seen and attempted over the years, highlighting some of the advantages and drawbacks of each, ending with a conclusion explaining which one I prefer and why.
Bare models without any run-time annotations, e.g.:
class User
{
/** @var string */
public $email;
}
Controllers and views need to be fully hard-coded and tailored to specific input-types, etc. - this is the simplest approach, but not the most elegant.
Every model has a type-object with synchronous property-names providing meta information about the properties, e.g.:
class User
{
/** @var string */
public $email;
}
class UserType
{
/** @var EmailInput */
public $email;
public function __construct()
{
$this->email = new EmailInput('email');
$this->email->required = true;
$this->email->unique = true;
}
}
This requires more up-front model work, but the pay-off is the ability to reuse
information provided by the type-object, e.g. for cross-cutting concerns dependent
on the same information - for example, the $unique
property could be used to
drive both validation and schema generation, etc.
It requires no framework, however, so it is still simple.
Meta-data is embedded inside the source-code in the form of annotations, e.g.:
class User
{
/**
* @var string
* @input(type="text")
*/
public $first_name;
}
This approach appears on the surface to be simpler than the type-objects approach. However, parsing annotations requires a complex framework that performs parsing, validation, caching, etc. and is actually far from simple.
This involves building a run-time meta-model driving a code-generation framework, and could look something like this:
$class = new MetaClass('User');
$class->addProperty(new EmailProperty('email'));
$code_generator->run($class);
The code generator would emit a model, a meta-model, and possibly other things like factory-classes etc.
Assuming passive (design-time) code-generation, as opposed to active (run-time) code-generation, this does provide static coupling at design-time, but perhaps not to the full extend - for example, refactorings (such as changing a property name) would involve a code-generation step, so even though you have static coupling, you can't really have automated refactorings.
It goes without saying, this approach is by no means simple - it involves building a large complicated code-generation engine, a lot of careful design, optimizations, etc.
This is the approach used by e.g. the M# Language, in which a declarative specification is written in a custom language, consumed by a parser and used to drive a code-generator.
The premise of this is brevity, but obviously this comes at the cost of very high complexity, since a custom language requires custom IDE support, as well as all of the complexity and drawbacks of other code-generation approaches.
Similar to approach number 2 ("Bare Models plus type-objects") but with the actual model being generated by letting a code-generator consume the type-object, just like any other service.
In some ways, this approach is both the best and the worst of all worlds - you get to describe and compose the model by implementing your type-objects using all the features of the host language, you avoid having to repeat anything, but it comes with the same problems (e.g. refactoring of the generated model) and at least some of the complexity of the other code-generation systems.
My favorite is number 2: Bare Models plus type-objects, because:
-
Avoids code-generation.
-
Static coupling and automated refactoring everywhere.
-
The non-static property-name as a string is encapsulated once in the type-object and never needs to be repeated in an unsafe manner.
The bare models plus type objects approach keeps the overall complexity low, keeps transparency high, avoids introduction of design-time steps and completely obviates the need for any framework at the model-level - your models, and type-objects, are just code.
Granted, developers will need to learn and understand why every model is implemented as two classes, but this is a small price to pay - and it inspires learning and independent thinking.
I should write more about type-objects, which almost nobody is doing - it has been by far the most productive and satisfying approach I've used...