AR focuses on data. Moreover, Eloquent makes this data public. Objects designed around an Eloquent model assume public access to those pieces of data, so encapsulation is harder and cohesion is blurred. Tracking where properties are accessed or modified is harder, even with advanced IDEs, because of magic properties and weak type hinting.
In the context of a Laravel project, data gets validated as arrays (most likely user input extracted from the Request) and, later on, added to an Eloquent model through generic methods:
According to its author, "Laravel has no opinion on where or how you do validation." By default, all your models will inherit generic methods to be constructed or updated:
Model::create(array $attributes);
Model::update($id, array $attributes);
$model->fill(array $attributes);
new Model(array $attributes);
This behavior moves the responsibility of validation and protecting invariants outside of the model. Depending on the architecture of the project, this may be a service layer, a command or an http layer (controller, form request). Having this responsibility outside of the object makes for weak objects, whose state can't be trusted to be valid.
Intercepting this behavior is very difficult, as it involves either overriding multiple methods of the Model class, some of which the Model itself assumes to be safe to call (empty constructors, for example).
While it is entirely possible to do performance optimizations in the AR pattern, Eloquent lacks work in this area. It has no IdentityMap to prevent hitting the database for the same record, does not handle join queries into joined models (with possible column collisions if done wrong!) and has removed it's small cache implementation since version 5.0.
The only methods that allows for query optimization are the with / load
relation eager-loading methods.
Implementing any sort of cache means intercepting internal ORM calls, which by the level of coupling between Model, Query\Builder and Relations, would need to be done at the ORM level. Overriding methods at the Model level cannot accomplish any of this.
Eloquent models inherit a large, generic API that assumes all models will need. This can be a problem when working in large teams, because knowledge of how things should be done is passed through convention instead of enforced by code. For example, a Model::all()
method call could be a self-destruct button in a rather large table. Preventing calls from an already existing public method is more difficult than preventing the creation of a yet-to-be-added method.
While this is true for Doctrine as well (generic repositories also have self-destruct findAll()
methods), Eloquent using inheritance makes this worst: it gets all of these methods closer to the consumer, which is a negative point in the case of unwanted API, and it statically couples to it, so you can't hide it behind an injected dependency.
DataMapper assumes that the object's state can be modeled in a relational manner, but most of the times we end up adapting our modeling decisions to this restriction. This topic is older than Doctrine itself, and while DMs have evolved through the years, it's still a very important constraint.
While database access and usage is no simple task, the mapping layer adds an extra level of complexity to it, one that ActiveRecord explicitly avoids. Reconsitution of objects from the database is dealt by the ORM, and that assumes an internal structure of the mapped objects, which also limits design. Hooking to those processes is possible, but demonstrates how much more complex it is than just overriding a method.
Anemic Domain Modeling is modeling objects with public setters and getters and no real behavior outside of transporting data around. This incurs in the cost of domain modeling, without the benefits of actually adding behavior related to the domain, as described by Fowler in his bliki.
While this is not intrinsic of the DM pattern, it does have something to do with it. In an empty AR model, behavior is always present: AR gives you database access for all your models. But if you design an Entity that does not know about database and does not have any relevant behavior, then you are arguably worst than with AR.
@nilportugues
Caching: Not a single model here is doing any caching. The caching implementation showed here is a Repository, and I didn't add a level of indirection to a "database repository" just to keep the example simple.
Testing: I understand that some people are more extreme than others on unit vs integration testing. I will compromise with both: Given a very, very fast DB engine, if you can keep your complete test suite running on a manageable time, then I wouldn't mind hitting the database once in a few tests.
Now, the definition of manageable may vary: For me, a test suite should be fully executed faster than what it takes my brain to lose focus on the task at hand. That is probably somewhere below 30 seconds, ideally below 5 seconds. Suites that take more than 10 seconds will make me run only the current test while testing, then the whole suite when finished. Suites that take longer than that will probably be skipped for "small changes", checked only on CI instead.
Performance: Do you have a reproducible way to verify Doctrine vs Eloquent memory usage? I'd love to see that.
Serialization has nothing to do with performance. If your serializer of choice doesn't deal with cyclic references, PR the fix.
Leakage: I've tried my best to limit this to the Model / Entity and its direct consumer. In different architectures the consumer will take a different role, and that's ok, it's up to the application to understand how much indirection it needs. In both Eloquent and Doctrine you can either use from Controllers or use from a Command handler, App service layer, repository, etc.
If you need to model both a Model class and a Model PoPo, then you're going into a lot of trouble to deal with the tool. I'd rather choose a tool that doesn't force me to duplicate all my models when I don't want that sort of leakage. Which is why I firmly agree with what was said in the Laravel Podcast: if you're using Eloquent, embrace it. Don't try to go around it with that kind of practices, you'll end up doing the same method call, but with layers and layers of indirection.
And you were the first one here to bring "the drama" up. This gist is not drama, I'm trying to share something useful to everyone, either Laravel adopters that are thinking about using Doctrine, or Doctrine users that are starting to work in Laravel. Don't make this a drama because we have different opinions.