So far in this series we’ve stayed pretty focused on core Laravel concepts and their underlying implementation. However, this week we’re going to go slightly farther afield and discuss some of the static method meta-programming used by Laravel’s “Eloquent” ORM.
What’s an ORM
ORM stands for Object Relational Mapper. In less fancy terms, ORMs are systems that hide the underlying storage technology for your application’s data, and present client-programmers with simpler objects to manipulate.
As a web developer, the most common ORM pattern you’ll see is ActiveRecord. Most frameworks that ship with an ORM use one that implements and/or is inspired by ActiveRecord, and Laravel’s Eloquent ORM is no exception.
This article isn’t a full ORM, ActiveRecord, or Eloquent tutorial. If you’re not familiar with the concepts you’ll probably be OK — just think of an ORM as that thing we do instead of writing raw SQL.
Eloquent Parts
First, here’s a lightning round primer on the trinity of objects that you’ll use most commonly with Eloquent.
First, there’s your standard model object. A model represents the data for a single object or item. All models in Laravel inherit from the Illuminate\Database\Eloquent\Model
object. There’s also an Eloquent
class_alias
setup in a stock Laravel system
#File: app/config/app.php
'aliases' => array(
//...
'Eloquent' => 'Illuminate\Database\Eloquent\Model',
This allows users to use the global shortcut Eloquent
when defining their models.
#File: app/models/SportsBallPlayer.php
class SportsBallPlayer extends Eloquent
{
}
This is another area where Laravel shields users from PHP’s underlying namespace system.
To fetch models, Laravel has a query builder object (class name: Illuminate\Database\Query\Builder
). You use a query builder object to fetch specific models from your system. In pseudo-code that might look like this
//pseduo code to simplify things, we'll explain more below
$player = new SportsBallPlayer;
$query_builder = new \Illuminate\Database\Query\Builder;
$query_builder->setModel($player);
$results = $query_builder
->where('height_in_inches','>','72')
->get();
You’re probably wondering what’s in the $results
variable above. That brings us to the third, and final Laravel ORM object we’ll talk about today: The collection object (class name: Illuminate\Database\Eloquent\Collection
). The collection object is an array-like PHP object that contains a collection of models returned by the query builder. The most common way you’ll use this object is via a for each
statement
foreach($results as $model)
{
var_dump($model->toArray());
}
If you’re new to PHP you may be surprised that data structures other than the built-in array
type can be for each
ed. While it’s beyond the scope of this article, this ability comes by way of the IteratorAggregate
interface. Magento developers should be familiar with the concept, but take note that Laravel doesn’t have typed collections.
The __call
and __callStatic
Methods
With that quick primer out of the way, we’re set to explore Eloquent’s use of __callStatic
. That is, using our model above, what happens if we say
SportsBallPlayer::callTheThing(...);
and the SportsBallPlayer
class doesn’t have a static callTheThing
method defined. If you’ve been following along, you’ll know we want to jump right to the base model class’s __callStatic
method
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
public static function __callStatic($method, $parameters)
{
$instance = new static;
return call_user_func_array(array($instance, $method), $parameters);
}
As __callStatic
methods go, this is pretty simple. Remember, the static
keyword, as used here, is how a static method can refer to its calling class. In our case, the following lines are equivalent
$instance = new static;
$instance = new SportsBallPlayer
This means calling an undefined static method on an Eloquent model will
- Instantiate a new instance of that model
- Pass the static call on as an instance method call
Put in code, that means this
SportsBallPlayer::all(...);
is equal to this
$model = new SportsBallPlayer;
$model->all(...);
That’s pretty simple, and a clever way to save developers from needing to instantiate a model when they want to work with it.
However, let’s go back to our original example
SportsBallPlayer::callTheThing(...);
//or
$model = new SportsBallPlayer;
$model->callTheThing(...);
There’s no instance method named callTheThing
. As you’ve no doubt already guessed, Eloquent models have a __call
method defined as well, which will catch calls to undefined methods
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
public function __call($method, $parameters)
{
if (in_array($method, array('increment', 'decrement')))
{
return call_user_func_array(array($this, $method), $parameters);
}
$query = $this->newQuery();
return call_user_func_array(array($query, $method), $parameters);
}
We’re going to ignore that first conditional, and concentrate on the last two lines
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$query = $this->newQuery();
return call_user_func_array(array($query, $method), $parameters);
When you call an undefined method on an Eloquent model, Laravel will pass that method call on to an automatically instantiated query builder object. The newQuery
method above returns a prepared query builder object, ready to query for your model. So, in simplified terms, that means our example
SportsBallPlayer::callTheThing(...);
//or
$model = new SportsBallPlayer;
$model->callTheThing(...);
actually expands out into something like
$model = new SportsBallPlayer;
$query = new Illuminate\Database\Query\Builder;
$query->setModel($model);
$query->callTheThing(...);
We say “simplified” because the instantiation of a query builder object is a complicated thing. It’s beyond the scope of this article, but if you’re interested it’s worth tracing out exactly what the newQuery
method does.
Why do this?
In our callTheThing
example, the end result here is the same. Since the query builder object doesn’t have a callTheTing
method defined, PHP dies with a fatal error. That said, let’s consider a more common example.
SportsBallPlayer::where('height_in_inches','>','72')
There’s no where
method defined on an Eloquent
model. Instead, by passing the undefined method calls onto the query builder object, Laravel provides an instant shortcut to querying for a specific type of object. Even better, most of the query builder methods return themselves, which means method chaining is possible
SportsBallPlayer::where('height_in_inches','>',72)
->where('that_coach','=','Quite a Character')
->where('scoredowns', '=', 10);
Through a clever bit of meta-programming, Laravel maintains separation between the model logic and the querying logic, while still offering a simplified format that any PHP programmer can get started with. I know some programmers chafe at the marketing angle in many of Laravel’s system names, but this really is an eloquent pattern compared to many other ORMs.
Tradeoffs
There are of course, tradeoffs. The first I want to talk about is the similarity between these static method calls and Laravel facade calls. Consider the following
Auth::isLoggedIn();
Is this calling isLoggedIn
on an Auth
facade? Or is there an Eloquent model named Auth
with an isLoggedIn
method? Or (not likely, but still possible), does the query builder object have an isLoggedIn
method for some reason? I know it’s a common refrain in this series, but Eloquent’s use of the static method calling surface area to implement meta-programming features means code is less readable until you have some level of expertise in the system. It’s one more thing you need to check when you’re debugging someone else’s code.
There’s also some confusion that’s confined to Eloquent. Consider the following
$model_first = SportsBallPlayer::whereRaw('1=1')->first();
$model_first = SportsBallPlayer::all()->first();
These two calls look the same, don’t they? In the simple case, they are. However, consider this
$model_first = SportsBallPlayer::whereRaw('1=1')->first(['name']);
var_dump(
$model_first->toArray()
);
$model_second = SportsBallPlayer::all()->first(['name']);
var_dump(
$model_second->toArray()
);
Again, identical looking calls. The first works as expected, restricting the columns requested from the database
array (size=1)
'name' => string 'Gibson' (length=6)
However, the second call ends up throwing an exception
Argument 1 passed to Illuminate\Support\Collection::first() must be an instance of Closure, array given, called in /path/to/laravel/app/routes.php on line 257 and defined …
What gives? If we apply what we’ve learned, we know Eloquent routes the call to whereRaw
through a query builder object. The query builder object implements a return $this
for method chaining
#File: framework/src/Illuminate/Database/Query/Builder.php
public function whereRaw($sql, array $bindings = array(), $boolean = 'and')
{
//...
return $this;
}
Which means the call to first
is also made on the query builder object
#File: framework/src/Illuminate/Database/Query/Builder.php
public function first($columns = array('*'))
{
$results = $this->take(1)->get($columns);
return count($results) > 0 ? reset($results) : null;
}
If we consider the second call, it’d be easy to jump to the same conclusion
$model = SportsBallPlayer::all()->first();
However, Laravel does not route the call to all
through a query builder object. Why not? Because an all
method is actually defined on the base Eloquent model
#File: framework/src/Illuminate/Database/Eloquent/Model.php
public static function all($columns = array('*'))
{
$instance = new static;
return $instance->newQuery()->get($columns);
}
This means all
returns a collection object, and it is the collection object’s first
method that’s really called.
#File: framework/src/Illuminate/Support/Collection.php
public function first(Closure $callback = null, $default = null)
{
if (is_null($callback))
{
return count($this->items) > 0 ? reset($this->items) : null;
}
else
{
return array_first($this->items, $callback, $default);
}
}
The problem here is two fold — first, without intimate knowledge of the base classes it’s impossible to tell which methods are passed along to a query builder object, and which are not. The second aspect is both the collection object and the query builder object have identical method names. This can lead to a number of confusing scenarios when querying for Eloquent models, with your only solution being rote memorization or relying on third party extensions for your IDE’s auto-complete.
These are, of course, the perils of meta-programming. The Eloquent querying model is an improvement — but at the cost of some confusion and learning curve. We’ll have more to say about this next time in a our final wrap-up article for this series.