Laravel

Klasy makr i rozszerzanie klas bazowych oraz helperów

Wstęp

Laravel 4.2 wprowadził tak zwane makra. Możemy za ich pomocą tworzyć własne metody dla (niektórych) klas Laravela i w ten sposób właściwie dowolnie je rozszerzać. Załóżmy, że w swoim projekcie serwera API mamy kontrolery, które często (lub nawet zawsze) zwracają odpowiedzi o pewnej konkretnej strukturze, na przykład:

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

Oczywistym jest, że zwracanie odpowiedzi w taki sposób, w każdej metodzie każdego kontrolera, prędzej czy później stanie się koszmarem. Zwłaszcza kiedy okaże się, że strukturę trzeba zmienić - wówczas trzeba będzie ją zmienić wszędzie. Na szczęście w sukurs idą nam makra! Część klas Laravela udostępnia metodę macro(), która pozwala rozszerzyć te klasy o własne metody. Takie makra należy tworzyć w ciele metody boot() w dowolnej klasie typu ServiceProvider, dzięki czemu będą zawsze widoczne i dostępne z dowolnego miejsca w kodzie. W praktyce poleca się stworzyć osobny ServiceProvider dla wybranej klasy, którą chcemy rozszerzyć (należy również pamiętać, aby go uaktywnić w pliku konfiguracyjnym). Dla powyższego przykładu moglibyśmy stworzyć klasę ResponseMacrosServiceProvider o następującej treści:

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,
            ]);
        });
    }
}

Wówczas, wewnątrz metod kontrolerów moglibyśmy po prostu wykonywać poniższą linijkę:

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

ponieważ na tym etapie Laravel będzie wiedział, że structured() to stworzone makro. Argumenty tego makra określamy sami, dlatego możemy w ten sposób dowolnie rozszerzyć klasę. Pozostałe parametry możemy podawać tylko w razie potrzeby, ponieważ zdefiniowaliśmy dla nich domyślne wartości przy definicji makra. Dzięki temu zyskaliśmy sporo czasu i miejsca w kodzie, a także pewność, że w przypadku zmiany struktury wystarczy zaktualizować jeden ServiceProvider zamiast wszystkich kontrolerów.

Klasy makr

Kolejnym udogodnieniem, związanym z makrami, są klasy makr. Jest to pewne uogólnienie i ustrukturyzowanie makr, w przypadku, gdy jest zwyczajnie zbyt wiele, aby trzymać je jako definicje metod w klasie ServiceProvider. Rozważmy podobną do poprzedniej sytuację, w której chcemy dopisać jeszcze kilka makr do klasy Response. Na przykład:

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([]);
        });
    }
}

Podany przykład jest dosyć prosty i widać, że kolejne makra wykorzystują to pierwotnie utworzone, dlatego kodu nie ma jeszcze aż tak dużo. Łatwo jednak wyobrazić sobie sytuację, w której chcemy dopisać makra, które nie mają ze sobą wiele wspólnego i produkują mnóstwo linii kodu. Oczywiście najbardziej zależałoby nam, żeby takie metody jak boot() w klasie ServiceProvider zawierały jedynie wywołania metod innych klas, a nie jakieś definicje i obliczenia rozwleczone na kilkaset linii. Można sobie z tym poradzić przez tak zwane oddelegowanie problemu, co w tym przypadku oznaczałoby utworzenie prywatnych metod w klasie ResponseMacrosServiceProvider odpowiadających makrom i przeniesienie tam definicji makr. Na przykład:

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

Dzięki temu w metodzie boot() wystarczy wywołać odpowiednie metody prywatne, aby zarejestrować wybrane makra. To jednak nie jest idealne rozwiązanie, ponieważ obciąża klasę ResponseMacrosServiceProvider, która jako ServiceProvider nie powinna zawierać logiki sama w sobie. Dużo lepszym sposobem jest zastosowanie klasy makr. Klasy makr (lub mixiny) to po prostu osobne klasy przechowywane na przykład w katalogu app/Mixins/, które zawierają metody zwracające funkcje/domknięcia (ang. Closure). Klasa taka posiada tyle metod, ile makr będzie dodawać do wybranej klasy Laravela. Klasę makr dla naszego przykładu moglibyśmy zapisać jako app/Mixins/ResponseMixin i mogłaby wyglądać następująco:

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);
        };
    }
}

Już na pierwszy rzut oka widać co się dzieje i do czego taka klasa służy. W sprytny sposób oddzieliliśmy tym samym całą logikę tych metod od samej klasy ServiceProvider, dzięki czemu kod jest bardziej przejrzysty, zrozumiały i łatwiejszy w utrzymaniu. W dodatku wszystkie makra znalazły dla siebie własne miejsce w projekcie! Teraz wystarczy wczytać tę klasę. W tym celu wracamy do metody boot() naszego ResponseMacrosServiceProvider i piszemy tylko jedną linijkę (pamiętając od wczytaniu naszej klasy do namespace'a):

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

To wszystko. Laravel automatycznie przeskanuje wszystkie metody public oraz protected i wczyta je jako makra. Teraz nasza klasa będzie uwzględniona jako mixin dla klasy Response, co pozwoli nam korzystać ze zdefiniowanych przez siebie metod.

Helpery

Tutaj wspomnę tylko bardzo krótko o tym, żeby nie nadziać się na problem z rozszerzaniem metod helperów. Wiadome jest zapewne, że dla większości użytkowych klas Laravela istnieją odpowiadające im helpery. Na przykład dla klasy Response istnieje helper response(), a dla klasy Request istnieje helper request() i tak dalej, które pozwalają szybciej i bez wczytywania namespace'ów dobrać się do metod wymaganej klasy. Te helpery również podlegają rozszerzeniom, ale nie poprzez odpowiadające im klasy, a poprzez fabryki tych klas. Jeżeli zatem chcemy rozszerzyć helper response() musimy skorzystać z klasy ResponseFactory:

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

Wówczas, dla naszego przykładu, będziemy mogli skorzystać z helpera w taki sposób:

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

To samo tyczy się pozostałych helperów i fabryk ich klas bazowych. Warto o tym pamiętać, jeśli ktoś nadmiernie (tak jak ja) i z zamiłowaniem korzysta z helperów.

Słowem zakończenia

Są jeszcze dwie kwestie, o których warto wspomnieć przy okazji makr.

Po pierwsze oczywiście znajdzie się ktoś, kto powie, że makra to bardzo brudny sposób na rozszerzanie klas i nie po to bozia dała dziedziczenie i polimorfizm, żebyśmy paskudzili nasz kod jakimiś magicznymi akcesorami. I zgadzam się w stu procentach z taką osobą. W idealnym świecie powinniśmy dostać klasę, którą możemy dowolnie rozszerzyć i wstrzyknąć jak gdyby była klasą bazową. Niestety nie w każdej sytuacji da się to osiągnąć. W przypadku Laravela niektóre klasy są często wywoływane w różnych kontekstach i nie da się ich wstrzyknąć wszędzie tam, gdzie by się chciało. Makra w takim przypadku są idealnym rozwiązaniem, zwłaszcza gdy można je tak wygodnie zorganizować i przechowywać.

Druga sprawa jest taka, że prawdopodobnie nie wszystkie IDE poradzą sobie z rozpoznaniem i wykryciem makr. Osobiście programuję w PhpStormie i korzystam z pakietu laravel-ide-helper, który pozwala na wygenerowanie plików do analizy przez IDE. Te wygenerowane pliki są widoczne przez PhpStorma, ale nie są nigdzie importowane. Zawierają, w formie klas PHP, wszystkie rozszerzenia stworzone przez programistę, dzięki czemu IDE widzi wszystkie te makra (i wiele więcej) i jest w stanie doskonale podpowiadać dalsze akcje tak, jak gdyby te makra faktycznie stanowiły część frameworku. Jeżeli jednak komuś nie przeszkadza brak podpowiedzi makr w innych IDE, to tym lepiej dla niego.

  • admin
  • , zaktualizowano

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *