Oh Dear is the all-in-one monitoring tool for your entire website. We monitor uptime, SSL certificates, broken links, scheduled tasks and more. You'll get a notifications for us when something's wrong. All that paired with a developer friendly API and kick-ass documentation. O, and you'll also be able to create a public status page under a minute. Start monitoring using our free trial now.

A trait to dynamically add methods to a class

Original – by Freek Van der Herten – 4 minute read

We recently released our newest package called macroable. It contains a trait that, when applied to class, can dynamically add methods to that class. This trait is basically a stand alone version of the macroable trait in Laravel. In this post I'd like to show you how you can use it, how it works behind the scenes and explain why we created it.

How it can be used

Here's a simple example of how the trait can be used. In this example we are going to add concatenate method to an anonymous class. Of course you can use the trait on regular classes as well.

$myClass = new class() {
    use Spatie\Macroable\Macroable;
};

$myClass::macro('concatenate', function(... $strings) {
   return implode('-', $strings);
};

$myClass->concatenate('one', 'two', 'three'); // returns 'one-two-three'

Callables passed to the macro function will be bound to the class the trait is applied upon. Sounds scientific, here's an example to make that clear:

$macroableClass = new class() {

    protected $name = 'myName';

    use Spatie\Macroable\Macroable;
};

$macroableClass::macro('getName', function() {
   return $this->name;
};

$macroableClass->getName(); // returns 'myName'

You can also add multiple methods in one go my using a mixin class. A mixin class contains methods that return callables. Each method from the mixin will be registered on the macroable class.

$mixin = new class() {
    public function mixinMethod()
    {
       return function() {
          return 'mixinMethod';
       };
    }

    public function anotherMixinMethod()
    {
       return function() {
          return 'anotherMixinMethod';
       };
    }

$macroableClass->mixin($mixin);

$macroableClass->mixinMethod() // returns 'mixinMethod';

$macroableClass->anotherMixinMethod() // returns 'anotherMixinMethod';

How it works behind the scenes

The implementation of macroable is really simple. The name and the callable passed to macro is simply stored in an array.

//inside the trait 

protected static $macros = [];

/**
 * Register a custom macro.
 *
 * @param  string $name
 * @param  object|callable  $macro
 */
public static function macro(string $name, $macro)
{
    static::$macros[$name] = $macro;
}

Whenever you call a function upon the class that does not exists the trait will try to find a macro with the name of the called function.

public function __call($method, $parameters)
{
    if (! static::hasMacro($method)) {
        throw new BadMethodCallException("Method {$method} does not exist.");
    }
    $macro = static::$macros[$method];
    if ($macro instanceof Closure) {
        return call_user_func_array($macro->bindTo($this, static::class), $parameters);
    }
    return call_user_func_array($macro, $parameters);
}

Even though there's a little bit more to it, that's basically the gist of it. An important bit to remember is that you can't use this trait on a class that implements __call itself.

Why we created the macroable package

You might wonder why you ever would need to use macroable in your project when you can just simply add methods to classes. Well, this macroable trait isn't really meant to be used in regular projects. It's more targeted at package and framework developers. By applying the trait to certain classes in a package users of the package can add methods to the classes of the package without changing the source code.

In Laravel there are plenty of classes that are macroable. Take the Collection class for instance. Because it is macroable you can add your own custom methods to it without them having to be PRed (and accepted) to the framework. My team even released a package that contains a bunch of collection macros that are quite handy, but maybe not general enough to be added to framework. Want to know more about how macros can be used in Laravel? Then read this excellent piece by Caleb Porzio which contains a list of macroable things in Laravel.

Besides Laravel specific ones, my team releases a lot of framework agnostic packages as well. We extracted Laravels macroable trait to it's own stand alone package because we wanted to add some macroable magic to some of our packages. At the moment of writing the macroable trait is used in our url package and our ssl certificate package. Pretty sure there will be other packages in the future where we will use the trait.

In closing

If you're a package creator and find yourself closing PRs that add methods that you don't deem useful enough to warrant a place in your package, consider adding the macroable trait. Check out the macroable package on GitHub to know more. Also take a look at the Laravel, PHP and JavaScript packages our team previously released. Might be something useful for you in there too.

Stay up to date with all things Laravel, PHP, and JavaScript.

You can follow me on these platforms:

On all these platforms, regularly share programming tips, and what I myself have learned in ongoing projects.

Every month I send out a newsletter containing lots of interesting stuff for the modern PHP developer.

Expect quick tips & tricks, interesting tutorials, opinions and packages. Because I work with Laravel every day there is an emphasis on that framework.

Rest assured that I will only use your email address to send you the newsletter and will not use it for any other purposes.

Comments

What are your thoughts on "A trait to dynamically add methods to a class"?

Comments powered by Laravel Comments
Want to join the conversation? Log in or create an account to post a comment.