Categories


Archives


Recent Posts


Categories


Eloquent ORM Static Meta-Programming

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

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 eached. 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

  1. Instantiate a new instance of that model
  2. 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.

Originally published November 15, 2014
Series Navigation<< Laravel Service Manager Indirection

Copyright © Alana Storm 1975 – 2023 All Rights Reserved

Originally Posted: 15th November 2014

email hidden; JavaScript is required