Categories


Archives


Recent Posts


Categories


Laravel’s MacroableTrait

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.

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

Another quick primer this week, but this time it’s Laravel specific.

The idea of a “macro” has a long history in computer science and programming. This long history means it’s one of those words with numerous different overloaded meanings. The first time I encountered something called a macro it was in mid-to-late-1990s visual basic/vb-script, where a macro is a function (or subroutine) with no parameters. The C programming language has macros, but in C they’re a small programming language within a programming language that lets you change the contents of your program before sending it to the compiler. Even Microsoft Office has macros, which are small recordable programs that allow you to automate tasks.

While each of these things are different, they all share the common theme of performing a programmatic task, but with limited access to the full features of the real programming environment.

With that context, we’re going to take a look at Laravel’s implementation of the macro concept.

What Does “Macroable” Mean in Laravel

To start, let’s add the following to our app/routes.php file.

#File: app/routes.php
class Hello{}

Route::get('testbed', function(){

    $hello = new Hello;

    $hello->sayHi();

});

In real life we’d never define a class in the app/routes.php file, but it’s convenient for demo/learning purposes.

If we load the testbed route in a browser

http://laravel.example.com/testbed

We’ll get the following PHP error

Call to undefined method Hello::sayHi()

This is unsurprising, as we never defined a sayHi method for the Hello class.

Next, let’s add the MacroableTrait to our class

#File: app/routes.php   
class Hello
{
    use Illuminate\Support\Traits\MacroableTrait;
}

Here we’ve used the full trait name, namespace and all. If you’re not familiar with traits, checkout last week’s primer. Traits follow the same namespace rules as classes, and using a trait will invoke the PHP autoloader. If we load the page with the above in place, you’ll still see the same error

Call to undefined method Hello::sayHi()

However, now try adding the following code to your route

#File: app/routes.php    
class Hello
{
    use Illuminate\Support\Traits\MacroableTrait;
}
Route::get('testbed', function(){
    // include '/tmp/test.php';

    $hello = new Hello;
    Hello::macro('sayHi', function(){
        echo "Hello","\n<br>\n";
    });
    $hello->sayHi();
    Hello::sayHi();
});

Here we’ve called the static method macro on Hello. While Hello doesn’t define a static method named macro, it does inherit one from the Illuminate\Support\Traits\MacroableTrait trait.

If you reload the page with the above in place, you’ll see the following.

Hello 
Hello 

This is what the MacroableTrait does — it allows you to add a method (or, a “macro”) to a PHP class programmatically. By calling the macro method, we’ve effectively added the sayHi method to our Hello class, and all objects instantiated from that class. The above example is a little silly (again, for pedagogical reasons) — in real life you’ll see the MacroableTrait used in some global bootstrap-ish code to make a method available to other programmers. An example you might be familiar with from Laravel 4.2 is the Form helper macros.

How MacroableTrait Works

The MacroableTrait is simpler than you might think. You can find its definition in the following file

vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php

The most obvious place to start your investigation is the macro method we called earlier

#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php
public static function macro($name, callable $macro)
{
    static::$macros[$name] = $macro;
}

This method stashed the anonymous PHP function in a static array property, indexed by $name. If you scroll down a bit in the file, you’ll see our old friends __call and __callStatic. The __call method

#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php
public function __call($method, $parameters)
{
    return static::__callStatic($method, $parameters);
}

Simply passes the method call on to the __callStatic method. The __callStatic method

#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php    
public static function __callStatic($method, $parameters)
{
    if (static::hasMacro($method))
    {
        return call_user_func_array(static::$macros[$method], $parameters);
    }

    throw new \BadMethodCallException("Method {$method} does not exist.");
}

will fetch the anonymous function or PHP callback added with the call to macro, and then call the anonymous function/callback using PHP’s call_use_func_array method.

If you didn’t follow that, let’s trace things again, this time with our example code. First, when we called

#File: app/routes.php      
Route::get('testbed', function(){    
    //...
    Hello::macro('sayHi', function(){
        echo "Hello","\n<br>\n";
    });
    //...
});    

we were telling our class to store the anonymous function inside of static::$macros['sayHi'].

Next, here’s where we called sayHi (both statically and via an instance method)

#File: app/routes.php       
Route::get('testbed', function(){        
    //...
    $hello->sayHi();
    Hello::sayHi();
    //...
});    

Since sayHi isn’t defined on the class Hello, PHP ends up calling __call (for the -> invokation) and __callStatic (for the :: invocation), per the rules of PHP Magic Methods. The __call method simply passes the method call onto __callStatic. This means __callStatic actually executes for both instance and static method calls.

If we put on some x-ray variable specs, the method call looks something like this

#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php        
public static function __callStatic('sayHi', $parameters)
{
    if (static::hasMacro('sayHi'))
    {
        return call_user_func_array(static::$macros['sayHi'], $parameters);
    }

    throw new \BadMethodCallException("Method {'sayHi'} does not exist.");
}

Or, expanding that out even further, like this

#File: vendor/laravel/framework/src/Illuminate/Support/Traits/MacroableTrait.php    
public static function __callStatic('sayHi', $parameters)
{
    if (static::hasMacro('sayHi'))
    {
        return call_user_func_array(function(){
            echo "Hello","\n<br>\n";
        }, $parameters);
    }

    throw new \BadMethodCallException("Method {'sayHi'} does not exist.");
}    

By leveraging PHP’s magic methods and static functions, the MacroableTrait trait gives you the ability to dynamically add methods to objects at runtime — a feature usually reserved for “more advanced” dynamic languages like ruby/python.

Gotchas

While powerful, keep in mind macros (by design) have no knowledge of the other properties and methods of a class/object. Since you’re using an anonymous function (or PHP callback) to add your method, this function/callback won’t have access to the usual variables like $this, self, or static. This means the methods you add via a MacroableTrait will never be more than stateless helper methods. If you need access to an object or class’s state, then a macro is the wrong tool for your job.

The other thing you should keep in mind when considering the MacroableTrait is whether you want to add another item to the already busy “static method namespace” in your Laravel program. We’ll talk more about this next time, when we dive deeper into the various different things a static method call might do in Laravel.

Originally published October 26, 2014
Series Navigation<< PHP TraitsLaravel Service Manager Indirection >>