При наследовании Illuminate-класса PHPRequest
и класса PHPResponse
возникает некоторая путаница.
Эти два класса работают несколько иначе, чем обычные классы из-за их важности в обработке HTTP-запросов. Я расскажу, как их наследовать, а потом покажу метод, который показывает, как композиция может быть лучше наследования.
Наследование класса PHPResponse
Сначала я опишу наследование класса PHPResponse
, так как оно проще.
Обычно в Illuminate-библиотеках есть класс, который содержит основную массу функциональных возможностей этой библиотеки. Этот класс часто имеет соответствующий фасад, который предоставляет разработчикам «простой» доступ к его методам. Поэтому мы можем просто вызывать PHPAuth::guest()
или PHPRedirect::route()
.
Illuminate-класс Response — PHPIlluminate\Http\Response
. Но его фасад PHPIlluminate\Support\Facades\Response
отличается от остальных. Вместо наследования PHPFacade
он представляет собой собственный маленький одинокий класс.
То есть это не «лицо» PHPIlluminate\Http\Response
, а скорее он просто использует класс PHPResponse
:
# Файл Illuminate/Support/Facades/Response.php
class Response {
/**
* Вернуть новый объект ответа приложения.
*
* @param string $content
* @param int $status
* @param array $headers
* @return Symfony\Component\HttpFoundation\Response
*/
public static function make($content = '', $status = 200, array $headers = array())
{
return new \Illuminate\Http\Response($content, $status, $headers);
}
Примечание: фасад класса Response в Laravel не использует PHPIlluminate\Http\Response
для всех своих ответов. Он использует классы Symfony для ответов JSON, Stream и Download.
Итак, как нам наследовать этот класс? К счастью, это не так сложно! Оказывается, нам вообще не надо иметь дело с фасадами, что делает всё ещё проще.
Во-первых, зададим для класса наследование «фасада» PHPResponse
:
<?php namespace Fideloper\Example\Facades;
use Illuminate\Support\Facades\Response as BaseResponse;
class Response extends BaseResponse {
public static function doSomething()
{
return new \Symfony\Component\HttpFoundation\JsonResponse(['message' => 'yay!']);
}
}
Во-вторых, заменим фасад Response в Laravel на свой.
Учитывая, что ваша автозагрузка будет обрабатывать этот класс (см. мою статью Установка библиотеки приложения Laravel), ваш следующий шаг — просто заменить фасад PHPResponse
Laravel вашим собственным.
Зайдите в app/config/app.php и добавьте ваш класс PHPResponse
вместо того, что указан в Laravel по умолчанию:
# Файл: app/config/app.php
'aliases' => array(
... прочие алиасы ...
'Redis' => 'Illuminate\Support\Facades\Redis',
'Request' => 'Illuminate\Support\Facades\Request',
//'Response' => 'Illuminate\Support\Facades\Response',
'Response' => 'Fideloper\Example\Facades\Response',
... больше алиасов ...
)
И вуаля! Вы можете использовать PHPResponse::DoSomething()
как пожелаете!
Наследование класса PHPRequest
С классом PHPRequest
сложнее. Его наследование не так изящно и просто. Сложность в том, что он используется в самом начале выполнения Laravel. Давайте посмотрим.
// Комментарии удалены:
require __DIR__.'/../bootstrap/autoload.php';
$app = require_once __DIR__.'/../bootstrap/start.php';
$app->run();
$app->shutdown();
Обратите внимание, что вторым вызывается файл bootstrap/start.php. Давайте посмотрим на этот файл:
// Комментарии удалены:
$app = new Illuminate\Foundation\Application;
$env = $app->detectEnvironment(array(
'local' => array('your-machine-name'),
));
... Больше кода ...
Сначала создается класс приложения PHPApplication
. Затем в Illuminate\Foundation\Application происходит следующее:
public function __construct(Request $request = null)
{
$this['request'] = $this->createRequest($request);
... Больше кода ...
}
protected function createRequest(Request $request = null)
{
return $request ?: Request::createFromGlobals();
}
Таким образом, в классе PHPApplication
первым происходит создание (и «заполнение») объекта PHPRequest
. Мы видим, что объект PHPRequest
создается в самом начале обработки запроса клиента. Он не только создается, но и заполняется информацией HTTP-запросов (заголовками, параметрами и т.д.).
Это происходит до того, как вызываются какие-либо события (PHPEvents
), и регистрируются какие-либо сервис-провайдеры или псевдонимы (фасады). Это означает, что если мы перепишем классы ядра Laravel с помощью обычных инструментов (обычно через сервис-провайдер), то мы просто заменим объект приложения PHPRequest
на пустой.
Я допустил эту ошибку — я использовал сервис-провайдер и просто заменил старый объект PHPRequest
своим собственным. Я даже вызывал PHPRequest::createFromGlobals()
, чтобы мой объект мог так же получить информацию о запросе. Однако это не работало. В конечном счёте объект запроса создавался дважды (один в классе PHPApplication
, другой в моём сервис-провайдере). Когда запрос создается в первый раз, то он, по-видимому, сбрасывает часть информации о самом запросе (в целях безопасности?).
Поэтому один из вариантов для нас — отредактировать PHPbootstrap/start.php
и передать наш собственный класс PHPRequest
.
Во-первых, создадим класс PHPRequest
:
<?php namespace Fideloper\Example\Http;
class Request extends \Illuminate\Http\Request {
public function doSomething()
{
echo 'Делаем что-либо.';
{
}
Во-вторых, внедрим наш PHPRequest
в PHPApplication
при его создании.
Вы могли заметить, что метод PHPApplication::__construct
может принять PHPRequest
в качестве параметра. Мы это и используем. Откройте bootstrap/start.php.
$app = new Illuminate\Foundation\Application;
На это (используйте любое подходящее имя класса):
// Обратате внимание на вызов createFromGlobals()
$request = Fideloper\Example\Http\Request::createFromGlobals();
$app = new Illuminate\Foundation\Application( $request );
Недостаток такого подхода в том, что он не удобен для пакетов. Вы не сможете устанавливать пакеты, которые будут наследовать класс PHPRequest
, не требуя при этом от разработчика редактирования bootstrap/start.php. Может это и небольшое неудобство, но PHP (к счастью) движется в сторону устанавливаемых пакетов, и это может стать препятствием на этом пути.
Композиция вместо наследования
Мартин Фаулер как-то здорово сказал: «выбирайте композицию (composition) вместо наследования». Это значит, что лучше использовать классы библиотек внутри ваших собственных классов вместо наследования и расширения классов этих библиотек.
Если говорить о приведенных выше примерах, то вместо наследования PHPRequest
и PHPResponse
подумайте о создании нового класса и использования базовых PHPRequest
и PHPResponse
внутри (с помощью внедрения зависимостей или используя старое доброе ключевое слово new).
Преимущество такого способа в отсутствии описанных выше проблем, при этом вы можете абстрагировать вашу дополнительную функциональность таким образом, чтобы не привязывать её непосредственно к Laravel или другому фреймворку.
Ниже приведен пример кода. Я создаю интерфейс для моих новых классов PHPRequest
и PHPResponse
, а затем создаю специфическую для Laravel реализацию каждого из них. Позднее вы можете создать реализации для других фреймворков, если захотите, и ваш пакет станет независимым от фреймворков!
# Файл: ResponseInterface.php
<?php namespace Fideloper\Example\Http;
interface ResponseInterface {
public function doSomething();
}
# Файл: LaravelResponse.php
<?php namespace Fideloper\Example\Http;
class LaravelResponse implements ResponseInterface {
public function doSomething()
{
return \Response::make('Делаем что-то');
}
}
# Файл: RequestInterface.php
<?php namespace Fideloper\Example\Http;
interface RequestInterface {
public function doSomething();
}
# Файл: LaravelRequest.php - использует DI
<?php namespace Fideloper\Example\Http;
use Symfony\Component\HttpFoundation\Request;
class LaravelRequest implements RequestInterface {
public function __construct(Request $request)
{
$this->request = $request;
}
public function doSomething()
{
$somethingHeader = $this->request->header('нечто');
return $somethingHeader;
}
}
Чтобы сделать еще один шаг вперед вы можете создать фасады для этих классов, как описано в этой статье.
Как уже упоминалось, приведенный код использует классы PHPRequest
/PHPResponse
Laravel, а не наследует их.
Использование интерфейсов может сделать их независимыми от фреймворка, позволяя вам реализовать класс для любой ситуации (использовать в других фреймворках или библиотеках). Если вы придерживаетесь только Laravel, то можете вообще не использовать интерфейсы.