Laravel

Extending base classes and helpers using macro classes

Introduction

Laravel 4.2 introduced macros. We can use them to create custom methods for(some of the) Laravel classes and, therefore, extend them in almost any way we want. Let's assume that we are creating an API server and we have some controllers which often (or even at all times) return responses of a very specific structure. For example:

public function index(): Response
{
    ...
    return response([
        status => 200,
        message => null,
        data => [
            ...
        ],
    ]);
}

It's obvious that returning responses in such manner, in every method of every controller, will sooner or later become a nightmare. Especially if we wanted to change the structure of the response, we would have to change it everywhere. Luckily, this is where macros come to the rescue. Some Laravel classes (those with the Macroable trait) provide the macro() method which allows to extend these classes with custom methods. Such macros should be registered inside the boot() method of a ServiceProvider. This way they can be accessed anywhere in the code. In fact, it is best to create a separate ServiceProvider for a class we want to extend (and register it in the config file). In our case, we could create a ResponseMacrosServiceProvider with the following code:

namespace App\Services;

use Illuminate\Http\Response;
use Illuminate\Support\ServiceProvider;
use ReflectionException;

class ResponseMacrosServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     * @throws ReflectionException
     */
    public function boot(): void
    {
        Response::macro(structured, function (array $data, ?string $message = null, ?int $status = 200) {
            return response([
                status => $status,
                message => $message,
                data => $data,
            ]);
        });
    }
}

Then, our controllers methods could simply invoke our macro:

public function index(): Response
{
    ...
    return Response::structured([
        ...
    ]);
}

because at this point Laravel will know that structured() is a macro. We define the function parameters by ourselves, therefore we can freely extend the class. Other arugments may be omitted as we made them optional with default values. This way we don't need to waste our time rewriting or copying the response snippet. Moreover, should any change of the response structure occur, we only need to update a single file instead of all controllers.

Macro classes

Another great convenience are the macro classes. In a way, it is a generalization and structuring of macros in cases when there is too many of them and the code becomes to hard to read and maintain. Let's consider a situation in which we want to add some more macros for the Response class. For example:

namespace App\Services;

use Illuminate\Http\Response;
use Illuminate\Support\ServiceProvider;
use ReflectionException;

class ResponseMacrosServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap services.
     *
     * @return void
     * @throws ReflectionException
     */
    public function boot(): void
    {
        ...
        Response::macro(data, function (array $data) {
            return Response::structured($data);
        });

        Response::macro(message, function (string $message) {
            return Response::structured([], $message);
        });

        Response::macro(empty, function () {
            return Response::structured([]);
        });
    }
}

This example is pretty simple and there is still not that much code but it's easy to think of a situation where we want to add macros that do not share any logic and therefore produce a large amount of lines of code. Obviously, we would very much like our boot() method to only invoke some methods instead of defining them. This can be handled by "delegating" the problem, which in this case would mean to extract macros definitions into separate private methods. For example:

/**
 * Registers data macro for Response class.
 *
 * @return void
 */
private function registerDataMacro(): void
{
    Response::macro(data, function (array $data) {
        return Response::structured($data);
    });
}

This way, the boot() method contains calls to the private methods and only registers our macros. This solution is far from ideal, though, as it still puts a huge unwanted load on our ServiceProvider which should not contain logic by itself. Much better approach is to use a macro class.

Macro classes (or mixins) are just separate classes, stored for example inside app/Mixins/ directory, that include methods which return Closures. These closures are the macros that will be appended to the Macroable class. In our case, we could create a app/Mixins/ResponseMixin class as follows:

namespace App\Mixins;

use Closure;
use Illuminate\Http\Response;

class ResponseMixin
{
    /**
     * Returns a structured response with data and message.
     *
     * @return Closure
     */
    public function structured(): Closure
    {
        return function (array $data = [], ?string $message = null, ?int $status = 200)
        {
            return response([
                status => $status,
                message => $message,
                data => $data,
            ]);
        };
    }

    /**
     * Returns only a message with empty data.
     *
     * @return Closure
     */
    public function message(): Closure
    {
        return function (string $message)
        {
            return Response::structured([], $message);
        };
    }

    /**
     * Returns only data with `null` message.
     *
     * @return Closure
     */
    public function data(): Closure
    {
        return function (array $data)
        {
            return Response::structured($data, null);
        };
    }
}

Just by reading the class, we instantly know what is happening and what it does. We cleverly separated all the macros logic from the ServiceProvider which led to better insight, understanding and maintainability of our code. Moreover, all macros have found their own place in our project! Now we only need to register our mixin. To do this, we need to go back to the boot() method of our ResponseMacrosServiceProvider class and call the mixin() method of the Response class (remembering to load the necesarry namespaces):

/**
 * Bootstrap services.
 *
 * @return void
 * @throws ReflectionException
 */
public function boot(): void
{
    Response::mixin(new ResponseMixin());
}

That's it. Laravel will automatically scan through all public and protected methods and load them as macros. Now our class is going to be a mixin of the Response class which will enable our custom methods.

Helpers

It is worth to warn you not to get into the problem of extending helper functions. I assume you do know that most of the Laravel classes have their corresponding helper functions. For example, the Response class has a response() helper function, the Request class has a request() helper function and so forth and so on. These helpers are global which means you do not have to include any namespaces to use them, so some (including me) may consider it a facilitation. Helpers can also be extended but not by using their corresponding classes but their factories. For example, if we want to extend the response() helper we need to use the ResponseFactory class:

/**
 * Bootstrap services.
 *
 * @return void
 * @throws ReflectionException
 */
public function boot(): void
{
    ResponseFactory::mixin(new ResponseMixin());
}

Then, in our case, we could use the helper to return the response:

...
return response()->structured([
    ...
]);

This holds for all other helpers and their base classes factories. You better remeber this if you suffer from excessive use of the helpers (such as I do).

Afterword

There are two issues that show up when using Laravel macros.

Firstly, there's always this guy, who says, that macros are a really "dirty" way of extending classes and the use of some magic accessors shows contempt to polymorphism and inheritance... and I couldn't agree more. In an ideal world, we should be given a class that we can freely extend and then inject as if it was the original class. Unfortunately, it cannot always be done this easily. In Laravel, many classes are invoked in very different contexts and places in the code and it becomes impossible to inject our own code there without modifying the package (do not ever do this!). Macros are just perfect in such cases, especially when stored and maintained in such easy way.

Secondly, most IDEs will probably have a hard time dealing with loading custom macros. I personally use PhpStorm along with the laravel-ide-helper which analyzes the sources and generates a single PHP file. This file is not included anywhere in the code, but is visible for PhpStorm which then does the indexing and knows everything about my macros. Though, if you're not offended by the lack of support for the custom macros by your IDE, that's good for you.

  • admin
  • , updated

Leave a Reply

Your email address will not be published. Required fields are marked *