Laravel: Request-Response

31.10.2020 в 15:32
19396
+6

В этой статье мы рассмотрим внутренности Laravel. Попробуем разобраться, как устроен популярный фреймворк, из чего состоит и какие подходы использует. Чем отличается от того же Symfony, например. Конечно, чтобы использовать фреймворк, совсем необязательно знать, как он работает, однако это полезно, в первую очередь, потому, что так вы лучше понимаете инструмент, знаете, как его дебажить, легче определяете источник возникших багов и быстрее находите решения нестандартных проблем, выходящих за рамки документации.

Обзор будет состоять из нескольких частей: для начала мы рассмотрим полный цикл работы фреймворка, потом посмотрим на различные компоненты и — для лучшего понимания — напишем свои, если придется, а также обсудим, почему профессиональное php сообщество не очень любит Laravel (к слову, есть за что, но об этом позже).

Bootstrap приложения

Как это всегда бывает, наш веб-сервер перенаправляет все запросы на public/index.php

public/index.php

В нем Laravel подключает файл app.php из папки bootstrap в корне проекта, в котором создается контейнер внедрения зависимостей и биндятся (связываются интерфейсы с реализациями) некоторые важные зависимости: Kernel для консоли и веба и обработчик ошибок. Тут стоит немного остановиться и рассказать про контейнер зависимостей. Дело в том, что в Laravel он не только инжектит, но является самим приложением, которое тесно связано со всеми процессами, происходящими во фреймворке: определение роута, создание контроллера и так далее. Это первый минус в копилку Laravel, так как если вы хоть раз задумывались о том, чтобы поставить компонент фреймворка отдельно, вы неизбежно почти всегда потянете контейнер.

Давайте посмотрим на файл bootstrap/app.php, а потом вернемся обратно:

bootstrap/app.php

Обратите внимание на первые строки, в них мы связываем интерфейс Kernel с классом App\Http\Kernel, именно в этом классе вы чаще всего могли видеть и определять миддлвары, однако возможности его чуть шире, но об этом ниже. Также вы могли видеть, что он наследуется от Illuminate\Foundation\Http\Kernel из вендора. В нем расположен весь код по работе с запросом и ответом, однако фреймворк намеренно использует Kernel из папки приложения, чтобы у вас была возможность переопределить некоторые методы, убрать или добавить миддлвары и/или бутстрапперы.

Возвращаемся к index.php:

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
    $request = Illuminate\Http\Request::capture()
);

$response->send();

$kernel->terminate($request, $response);

Контейнер создал нам Kernel (App\Http\Kernel) и вызвал handle для текущего запроса.

public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Throwable $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new RequestHandled($request, $response)
        );

        return $response;
    }

Как мы видим, все наше приложение обернуто в try .. catch, что позволяет обрабатывать конкретные типы исключений по отдельности и не показывать пользователю на проде полную информацию о случившейся ошибке. Вся работа происходит в методе sendRequestThroughRouter():

protected function sendRequestThroughRouter($request)
{
     $this->app->instance('request', $request);

     Facade::clearResolvedInstance('request');

     $this->bootstrap();

     return (new Pipeline($this->app))
                ->send($request)
                 ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                 ->then($this->dispatchToRouter());
 }

Поскольку Laravel дает возможность не только инжектить Request в методы контроллера, но и использовать его в качестве фасада и функции, необходимо, чтобы актуальный Request был доступен из контейнера, так как оба — фасад и функция — достают его именно из него. Дальше мы очищаем старые инстансы реквеста с помощью Facade::clearResolvedInstance, так как они хранятся в статическом свойстве $resolvedInstance с более продолжительным временем жизни.

Метод bootstrap, идущий следом, вызывает так называемые бутстрапперы - обработчики, которые будут вызваны прежде, чем запрос будет передан контроллеру: это загрузка и кэширование переменных окружения и конфигурации, установка параметров, таких как определение обработчика ошибок, обработчика исключений, настройка ini для показа или нет ошибок в зависимости от окружения, запуск провайдеров, которые регистрируют зависимости в контейнере, и регистрация фасадов. Список стандартных бутстрапперов можно увидеть тут же, в файле Illuminate\Foundation\Http\Kernel:

бутстрапперы

По умолчанию фреймворк не предоставляет возможности добавить свой бутстраппер, однако поскольку Kernel из приложения наследуется от Illuminate\Foundation\Http\Kernel, где определено свойство $bootstrappers с изображения выше, вы можете определить это свойство в App\Http\Kernel, перетащив туда стандартные бутстрапперы и добавив свои. Чтобы ваш бутстраппер заработал, необходимо в нем определить следующий метод:

public function bootstrap(Application $app)
{
}

Так как в Laravel зачастую используется утиная типизация, имплементить какой-либо интерфейс или класс не нужно.

Среди стандартных бутстрапперов есть такие два: RegisterProviders и BootProviders. Они вызывают сервис-провайдеры из config/app:providers, которые предоставляют возможность вам и фреймворку настроить необходимые зависимости для работы приложения: соединение с базой данных, конфигурация файловой системы, сервисы аутентификации и многое другое. Это можно назвать сердцем фреймворка, так как без зависимостей ваше приложение будет мало что уметь.

Из интересных сервис-провайдеров обратите внимание на DatabaseServiceProvider, в нем ваша модель "наполняется" соединением с базой данных. Это к слову о магии, которую вы могли не понимать.

Давайте вернемся к методу sendRequestThroughRouter. После бутстраппа приложения происходит следующее:

return (new Pipeline($this->app))
          ->send($request)
          ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
          ->then($this->dispatchToRouter());

Компонент Pipeline предоставляет возможность запускать ряд обработчиков словно по трубе — один за другим. Такими обработчиками в Laravel являются, как вы уже, наверно, догадались, миддлвары. В данный момент запускаются глобальные миддлвары для всего приложения, а в конце выполняется callback-функция, которую возвращает метод dispatchToRouter. Компонент Pipeline устроен несложно: с помощью функции array_reduce по очереди выполняются одна миддлвара за другой, передавая результат обработки предыдущей миддлвары следующей.

Наверно, создавая собственные миддлвары, вы по умолчанию определяли в нем метод handle, однако вы могли бы определить вместо него магический метод __invoke с той же сигнатурой и все работало бы так же. Чтобы убедиться в этом, можете посмотреть код класса Pipeline на этой строке illuminate/pipeline

По умолчанию используется метод handle. Вы так же можете это изменить, если переопределите в Kernel метод sendRequestThroughRouter. Например, так:

namespace App\Http;

use Illuminate\Foundation\Http\Kernel as HttpKernel;

class Kernel extends HttpKernel
{
    ...

    protected function sendRequestThroughRouter($request)
    {
        $this->app->instance('request', $request);

        Facade::clearResolvedInstance('request');

        $this->bootstrap();

        return (new Pipeline($this->app))
                    ->send($request)
                    ->via('process')
                    ->through($this->app->shouldSkipMiddleware() ? [] : $this->middleware)
                    ->then($this->dispatchToRouter());
    }
}

Теперь все ваши миддлвары должны будут иметь или метод process, или __invoke.

Есть еще одна особенность миддлвар, о которой вы могли не знать, и связана она с тем, что миддлвары могут запускаться как до контроллера, так и после. Ничего специально для этого делать не нужно, достаточно запомнить разницу:

// До контроллера
public function handle(Request $request, \Closure $next)
{
    // do something

    return $next($request);
}

// После контроллера
public function handle(Request $request, \Closure $next)
{
    $response = $next($request);

    // do something

    return $response;
}

Как видно из примера, в первом случае мы возвращаем результат вызова callback-функции $next от Request, а во втором — используем этот результат (результатом будет вызов всех миддлвар и контроллера) и модифицируем ответ пользователю. Также одна миддлвара может быть выполнена как до контроллера, так и после. Примером такой миддлвары является StartSession. Обратите внимание на этот код: тут мы запускаем сессию до вызова контроллера, а после работы контроллера добавляем куки в ответ, сохраняем сессию и возвращаем ответ.

Роутинг

После того, как все глобальные миддлвары были запущены, выполняется callback-функция из метода dispatchToRouter: Kernel.php

Обратите внимание на следующую строку:

$this->app->instance('request', $request);

Поскольку миддлвары могли модифицировать Request, нам необходимо положить его в контейнер заново, чтобы вы могли использовать актуальный контекст запроса, когда будете получать его из метода контроллера, с помощью фасада или функции request().

Далее мы передаем обработку роутеру:

return $this->router->dispatch($request);

Роутер в свою очередь пытается найти роут и запустить его Router.php

Вы помните, что для отдельного роута тоже есть миддлвары, они запускаются тут. Если вам понадобится пропустить миддлвары для роута, вы можете определить в контейнере переменную middleware.disable со значением true. Нужно это в основном бывает только в тестах. Например, вы можете вызвать в тесте метод withoutMiddleware и ничего не передать, тогда отключатся все миддлвары. Этот метод доступен после наследования от TestCase.

И вновь мы снова видим Pipeline, на этот раз он перебирает миддлвары для конкретного роута и в конце вызывает $route->run():

public function run()
    {
        $this->container = $this->container ?: new Container;

        try {
            if ($this->isControllerAction()) {
                return $this->runController();
            }

            return $this->runCallable();
        } catch (HttpResponseException $e) {
            return $e->getResponse();
        }
    } 

Вы же не забыли, что мы до сих пор находимся в callback-функции, которую чуть ранее передали сюда? Напоминаю на всякий случай, чтобы вы понимали контекст.

Тут фреймворк пытается вызвать обработчик роута как callable или как контроллер. В обоих случаях ему необходимо определить входящие зависимости: если это callable —зависимости этой функции, если контроллер с методом — зависимости конструктора контроллера и вызываемого для роута метода.

В Route и ControllerDispatcher подключается трейт RouteDependencyResolverTrait, который с помощью рефлексии и контейнера внедрения зависимостей создает и возвращает входящие аргументы обработчика. Про контейнер внедрения зависимостей поговорим в следующей части обзора, а также напишем свой вариант.

После того, как контроллер отработал, мы возвращаем Response. Несмотря на то, что из контроллера вы можете возвращать не только объект Response, как это сделано в Symfony, Laravel все равно трансформирует ваш ответ в этот объект.

На всех этапах перемещения запроса от пользователю к ядру Laravel бросает различные события: RouteMatched, RequestHandled, события о том, что конкретный сервис-провайдер начал выполняться и так далее. Это дает разработчикам дополнительные точки расширения, однако это не так популярно, как в Symfony, где события являются основным функционалом для общения различных компонентов. Тем не менее, это не так удобно, как миддлвары, которых в Symfony нет, за что Laravel можно только похвалить.

Response

В index.php после обработки запроса вызывается $response->send(). Этот метод отправляет заголовки через функцию php header и распечатывает контент с помощью echo. В самом конце у Kernel вызывается метод terminate, который, во-первых, вызывает terminable middleware (подробнее читайте в документации), а во-вторых, выполняет terminatingCallbacks, куда вы можете добавить джобу следующим образом:

use Illuminate\Bus\Dispatcher;

final class SomeCommandHandler
{
    private Dispatcher $dispatcher;

    public function __construct(Dispatcher $dispatcher)
    {
       $this->dispatcher = $dispatcher;
    }

    public function handle(SomeCommand $command)
    {
       // do something here

       $this->dispatcher->dispatchAfterResponse(new AfterResponseJob(...));
    }
}

Теперь после того, как мы вернули ответ пользователю, приложение выполнит эти джобы.

Помимо успешного ответа, у нас есть исключения, их надо как-то обработать и показывать пользователю. Если вы не словили исключение, его словит метод Kernel:handle. Давайте вспомним код из начала статьи:

public function handle($request)
    {
        try {
            $request->enableHttpMethodParameterOverride();

            $response = $this->sendRequestThroughRouter($request);
        } catch (Throwable $e) {
            $this->reportException($e);

            $response = $this->renderException($request, $e);
        }

        $this->app['events']->dispatch(
            new RequestHandled($request, $response)
        );

        return $response;
    }

Если мы попадем в catch, фреймворк вызовет специальный объект ExceptionHandler, где вы можете обработать исключения в зависимости от типа в два этапа: зарепортить, то есть сообщить о нем в sentry, telegram, slack, куда-либо еще или ничего не делать, и отрендерить, то есть вернуть Response, который увидит пользователь, или, опять же, ничего не делать, тогда пользователь увидит неприятную или приятную ошибку в зависимости от того, как вы настроили веб-сервер.

Есть еще один способ, чтобы зарепортить и отрендерить исключение, — вы можете реализовать у исключения методы report и render, подробнее смотрите в документации.

Итак, в первой части обзора мы постарались разобрать основной процесс работы Laravel. В последующих частях мы начнем более детально разбирать каждый из компонентов: контейнер внедрения зависимостей, миддлвары, event-dispatcher и так далее.

loader
31.10.2020 в 15:32
19396
+6
Логические задачи с собеседований