My comments seemed to have caused a few people in the Laravel community to flip out (and I'm not referring to Nikko, despite the title tribute). Apparently it's become out of fashion to have strong opinions with sound technical reasoning. That said, the comments were a pretty crappy medium, and I've learned a bit about Laravel since then, so allow me to express myself better here.
Let's begin with the basics of why accessor methods are good practice both inside and outside of the context of any particular framework. We'll address Laravel's particulars in the explanations.
- Namespacing
- Domain Model Authority
- Centralization of Logic
- Clearer Overloading
Namespacing
Many ORMs promote the idea that a column name or column name mapping should be expressed as a property. For example $model->column_name should essentially get the data stored in the column directly or indirectly associated with 'column_name'. While this is perhaps more intuitive, it violates a few key concepts of what models and ORMs do.
Firstly, the ORM and its models are not simply a database API abstraction mechanism. This is evidenced by the fact that we state quite clearly that "business logic belongs in the model." But what does that really mean? In short, it means that our models are actually supposed to represent something more than pure data. They represent decisions about that data.
More thorough and robust patterns and implementations of those patterns, such as data mappers, will often make a clear distinction between what is called a 'domain model' vs. simply what might be considered a 'data model.'
"Ok," you say, "but why shouldn't a column name be a first class citizen and have it's own property associated with it?" Well that's fine... if you never actually intend to have very robust, reusable, or distributed models.
Accessor methods promote the separation between what is a property of the domain model and what is a property of the data model. That is to say, I may very well have a property on my domain model which alters the way my business logic works. What happens when the most suitable name for this happens to be the same as one of my column names? "Change the name!"... right?
Well sure, that's fine if I'm the only person who ever needs to be concerned with the distinction. But what happens if I release some of these models and related database for public consumption? What happens when someone wants to extend it and add a new column to the data model? What happens when the column they want to add has the same name as one of my domain model properties? Or if they want to add a domain model property with the same name as one of my columns?
Nikko's argument (based on one of his others) to this is probably simply that such a conflict is an edge case, or that if the name of the domain model property and the column are truly best suited by exactly the same term, they should probably affect one another anyhow. Nikko is probably right in 95% of circumstances, but what about the 5% where he's not? Why make it an issue to begin with when there's an easy solution?
It's very important to note at this point even Laravel's own domain model properties are public. So tough luck if you have a data model column named 'original' which the domain model uses to identify "the original state of the data model" and which you might want to use to signify a first draft or something similar.
Here's some other column names you can't use because Laravel thought it expedient to mix access to domain model and data model properties:
- 'attributes'
- 'relationships'
- 'exists'
- 'includes'
Although you may wonder why you would have column names such as these, that's not really the point. Again, the 95/5 argument is not very powerful. What this means is that any expansion of the base domain model, or your particular domain model if you're not careful is necessarily a limitation of the data model.
Domain Model Authority
A simple model concept:
class User extends ActiveRecord
{
// This is where our domain model interfaces with our
// data model. All our data model properties are stored
// here. Let's pretend we don't know what their names
// or keys are.
private $data = array();
// This is where we define properties for the domain model
// which might affect how business logic operates.
public $property1 = TRUE;
protected $property2 = NULL;
private $property3 = 'prefix';
// Now our methods, i.e. custom business logic...
public function specialOperation()
{
...
}
}
Now, if you're not familiar with how most ActiveRecord patterns handle the magic of mapping various properties or methods in PHP you should probably research these. Suffice to say the major problem that exists with these methods is that they fall into a pretty common battle with key language concepts. Namely, these methods get called when properties or methods are "not accessible." That is a very different distinction from "not defined."
This means that essentially we negate public, protected, and in some cases private distinction unless we implement our own checks and be very careful about the logic in these methods. If we added the following methods to our example class above, we can quickly demonstrate this.
public function __get($property)
{
return $this->$property;
}
public function __set($property, $value)
{
$this->$property = $value;
}
Essentially the above code just mooted the distinction between public and
protected (and actually in our particular case private too, but that becomes less clear when you have a base class implementing this that your models extend). If, for example, I call $user->property2 = 'whatever' from a totally unrelated class, PHP says "property2 is protected, are you in the same line of inheritance? no, sorry that's inaccessible to you, so we'll pass it to __set()." Now, since our __set() method is implemented on the class itself and it just blindly assigns the value... it doesn't have any access issues and the value is assigned.
Now, let's examine what Laravel does for its __set() method.
public function __set($key, $value)
{
$this->{"set_{$key}"}($value);
}
This is a little bit different from how I felt it was being explained in the Nettuts thread, however, it quickly unravels to being just as bad. Essentially unless you overload set_<property> this essentially gets passed to the __call() magic method... it's relevant code looks like this:
if (starts_with($method, 'get_'))
{
return $this->get_attribute(substr($method, 4));
}
elseif (starts_with($method, 'set_'))
{
$this->set_attribute(substr($method, 4), $parameters[0]);
}
Lastly, set_attribute() essentially just does the assignment in the
equivalent to our above domain model's $data array. Does this sound confusing and convoluted? It is!
$user->attributes['column_name'] = 'value';
The above code allows you to completely bypass all the overload
logic in any set_<column name> completely, but that's "OK" (not really), cause you could equally just call set_attribute() directly since it is public as well.
This rapidly destroys the authority of the domain model <-> data model paradigm. If I want to modify, transform, validate, whatever a particular piece of data upon actually setting it, there is almost no way for my model to actually ensure this happens, because someone can always bypass it with ease, from anywhere.
Centralization of Logic
Using the above code examples and points, I would like to emphasize yet again that there is no way to easily force setting and getting through any particular business logic. Similarly to this, there is no clear area in which this logic should even be placed. While Laravel provides the get_<column_name> and set_<column_name> "filters" for this the inversion and utter confusion of the Eloquent class's access modifiers introduces a large surface area for where and how to implement your logic.
Accessor methods on the other hand force users to call a particular method to get or set values on the data model. This intuitively identifies a single place to add any requisite logic that should affect the setting or getting of these values.
Clearer Overloading
Because of the centralized nature of accessor methods, overloading is much more natural. You don't have to separately document how to access your data model properites and how to overload their access behavior. Instead you need only to say "to get the value of a column use the get_<column_name>() method, to set it use set_column_name()," and at that point anyone who knows PHP knows they can easily overload these methods for custom logic.
Conclusion
Laravel is an interesting framework and there are many things I like about it, however, no matter how much I like something, holding my tongue on valid criticisms seems counter productive for not only the things I criticize, but the PHP community in general. Was Nettuts+ the best medium? No, this is clearly a better medium, however, I think it's important to raise these issues in areas where the larger community aggregates. It is part of the learning process, and for Laravel is part of the improvement process.
The faster paced nature of the comment thread on Nettuts caused some confusing wording on both sides (I think). This article represents a much clearer criticism (I hope) which articulates the points I was trying to make but also represents some additional time getting questions answered in their IRC channel to better clarify whether or not these criticisms were accurate.
I'm willing to accept that I may still be overlooking something here, and I welcome any comments from Laravel users or developers who feel they can put these criticisms to rest. Laravel and the PHP community will be better for it.
comments powered by Disqus