Версия фреймворка:
5.4 4.2
Прогресс перевода
Перевод немного отстаёт от оригинала. Коммитов не переведено: 1

Расширение фреймворка

Введение

Laravel предоставляет вам множество точек для настройки поведения различных частей ядра библиотеки или даже полной замены. К примеру, хэширующие функции определены интерфейсом HasherInterface, который вы можете реализовать (implement) в зависимости от требований вашего приложения. Вы также можете расширить объект Request, добавив собственные удобные вспомогательные методы (helpers). Вы даже можете добавить новый драйвер авторизации, кэширования и сессии!

Расширение компонентов Laravel происходит двумя основными способами: привязка новой реализации через контейнер IoC или регистрация расширения через класс Manager, который реализует шаблон проектирования "Factory" ("Фабрика"). В этом разделе мы изучим различные методы расширения фреймворка и код, который для этого необходим.

Managers и Factory

Laravel содержит несколько классов Manager, которые управляют созданием компонентов, основанных на драйверах. Эти компоненты включают в себя кэш, сессии, авторизацию и очереди. Класс-управляющий ответственнен за создание конкретной реализации драйвера в зависимости от настроек приложения. Например, класс CacheManager может создавать объекты-реализации APC, Memcached, Native и различных других драйверов.

Каждый из этих управляющих классов имеет метод extend, который может использоваться для простого добавления новой реализации драйвера. Мы поговорим об этих управляющих классах ниже, с примерами о том, как добавить собственный драйвер в каждый из них.

Примечание: посветите несколько минут изучению различных классов Manager, которые поставляются с Laravel, таких как CacheManager и SessionManager. Знакомство с их кодом поможет вам лучше понять внутреннюю работу Laravel. Все классы-управляющие наследуют базовый класс Illuminate\Support\Manager, который реализует общую полезную функциональность для каждого из них.

Где писать код ?

Данная документация описывает как расширять Laravel. Где именно писать этот код - зависит от вас. Например, для расширения Cache или Auth неплохим выбором будут start-файлы фреймворка. Те же расширения, которые должны подключаться максимально рано, например Session, должны располагаться в методе registerсервис-провайдера вашего приложения.

Cache

Для расширения подсистемы кэширования мы используем метод extend класса CacheManager, который используется для привязки стороннего драйвера к управляющему классу и является общим для всех таких классов. Например, для регистрации нового драйвера кэша с именем "mongo" нужно будет сделать следующее:

Cache::extend('mongo', function($app)
{
	// Вернуть объект типа Illuminate\Cache\Repository...
});

Первый параметр, передаваемый методу extend - имя драйвера. Это имя соответствует значению параметра driver файла настроек app/config/cache.php. Второй параметр - функция-замыкание, которая должна вернуть объект типа Illuminate\Cache\Repository. Замыкание получит параметр $app - объект Illuminate\Foundation\Application, IoC-контейнер.

Для создания стороннего драйвера для кэша мы начнём с реализации интерфейса Illuminate\Cache\StoreInterface. Итак, наша реализация MongoDB будет выглядеть примерно так:

class MongoStore implements Illuminate\Cache\StoreInterface {

	public function get($key) {}
	public function put($key, $value, $minutes) {}
	public function increment($key, $value = 1) {}
	public function decrement($key, $value = 1) {}
	public function forever($key, $value) {}
	public function forget($key) {}
	public function flush() {}

}

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

use Illuminate\Cache\Repository;

Cache::extend('mongo', function($app)
{
	return new Repository(new MongoStore);
});

Как вы видите в примере выше, можно использовать базовый класс Illuminate\Cache\Repository при создании нового драйвера для кэша. Обычно не требуется создавать собственный класс хранилища.

Если вы задумались о том, куда поместить ваш новый дравер - подумайте о том, чтобы сделать отдельный модуль и распространять его через Packagist. Либо вы можете создать пространство имён Extensions в основной папке вашего приложения. Например, если оно называется Snappy, вы можете поместить драйвер кэша в app/Snappy/Extensions/MongoStore.php. Впрочем, так как Laravel не имеет жёсткой структуры папок, вы можете организовать свои файлы, как вам удобно.

Примечание: если у вас стоит вопрос о том, где должен располагаться определённый код, в первую очередь вспомните о сервис-провайдерах.

Сессии

Расширение системы сессий Laravel собственным драйвером так же просто, как и расширение драйвером кэша. Мы вновь используем метод extend для регистрации собственного кода:

Session::extend('mongo', function($app)
{
	// Вернуть объект, реализующий SessionHandlerInterface
});

Где расширять Session

Расширение системы сессий должно быть зарегистрировано раньше, чем расширения других систем. Эта система стартует очень рано, до запуска старт-файлов. Поэтому вы должны регистрировать расширение этой системы в методе register сервис-провайдера вашего приложения, и ваш сервис-провайдер должен находиться ниже дефолтного Illuminate\Session\SessionServiceProvider в массиве providers файла app\config\app.php.

Написание расширения

Заметьте, что наш драйвер сессии должен реализовывать интерфейс SessionHandlerInterface. Он включен в ядро PHP 5.4+. Если вы используете PHP 5.3, то Laravel создаст его для вас, что позволит поддерживать совместимость будущих версий. Этот интерфейс содержит несколько простых методов, которые нам нужно написать. Заглушка драйвера MongoDB выглядит так:

class MongoHandler implements SessionHandlerInterface {

	public function open($savePath, $sessionName) {}
	public function close() {}
	public function read($sessionId) {}
	public function write($sessionId, $data) {}
	public function destroy($sessionId) {}
	public function gc($lifetime) {}

}	

Эти методы не так легки в понимании, как методы драйвера кэша (StoreInterface), поэтому давайте пробежимся по каждому из них подробнее:

  • Метод open обычно используется при открытии системы сессий, основанной на файлах. Laravel поставляется с драйвером native, который использует стандартное файловое хранилище PHP, вам почти никогда не понадобится добавлять что-либо в этот метод. Вы можете всегда оставить его пустым. Фактически, это просто тяжелое наследие плохого дизайна PHP, из-за которого мы должны написать этот метод (мы обсудим это ниже).
  • Метод close, аналогично методу open, обычно также игнорируется. Для большей части драйверов он не требуется.
  • Метод read должен вернуть строку - данные сессии, связанные с переданным $sessionId. Нет необходимости сериализовать объекты или делать какие-то другие преобразования при чтении или записи данных сессии в вашем драйвере - Laravel делает это автоматически.
  • Метод write должен связать строку $data с данными сессии с переданным идентификатором $sessionId, сохранив её в каком-либо постоянном хранилище, таком как MongoDB, Dynamo и др.
  • Метод destroy должен удалить все данные, связанные с переданным $sessionId, из постоянного хранилища.
  • Метод gc должен удалить все данные, которые старее переданного $lifetime (unixtime секунд). Для самоочищающихся систем вроде Memcached и Redis этот метод может быть пустым.

Как только интерфейс SessionHandlerInterface реализован, мы готовы к тому, чтобы зарегистрировать новый драйвер в управляющем классе Session:

Session::extend('mongo', function($app)
{
	return new MongoHandler;
});

Как только драйвер сессии зарегистрирован мы можем использовать его имя mongo в нашем файле настроек app/config/session.php.

Подсказка: если вы написали новый драйвер сессии, поделитесь им на Packagist!

Аутентификация

Механизм аутентификации может быть расширен тем же способом, что и кэш и сессии. Мы используем метод extend, с которым вы уже знакомы:

Auth::extend('riak', function($app)
{
	// Вернуть объект, реализующий Illuminate\Auth\UserProviderInterface
});

Реализация UserProviderInterface ответственна только за то, чтобы получать нужный объект UserInterface из постоянного хранилища, такого как MySQL, Riak и др. Эти два интерфейса позволяют работать механизму авторизации Laravel вне зависимости от того, как хранятся пользовательские данные и какой класс используется для их представления.

Давайте посмотрим на определение UserProviderInterface:

interface UserProviderInterface {

	public function retrieveById($identifier);
	public function retrieveByToken($identifier, $token);
	public function updateRememberToken(UserInterface $user, $token);
	public function retrieveByCredentials(array $credentials);
	public function validateCredentials(UserInterface $user, array $credentials);

}

Метод retrieveById обычно получает числовой ключ, идентифицирующий пользователя - такой, как первичный ключ в MySQL. Метод должен возвращать объект UserInterface, соответствующий переданному ID.

Метод retrieveByToken получает пользователя по его уникальному идентификатору (токену). Этот идентификатор хранится в БД в поле remember_token. Как и предыдущий метод, этот метод должен возвращать объект, который является реализацией UserInterface.

Метод updateRememberToken обновляет поле remember_token. Новый $token может быть строкой, полученной от куки remember_me, или null, если пользователь разлогинился.

Метод retrieveByCredentials получает массив данных, ктоорые были переданы методу Auth::attempt при попытке входа в систему. Этот метод должен запросить своё постоянное хранилище на наличие пользователя с совпадающими данными. Обычно этот метод выполнит SQL-запрос с проверкой на $credentails['username']. Этот метод не должен производить сравнение паролей или выполнять вход.

Метод validateCredentials должен сравнить переданный объект пользователя $user с данными для входа $credentials для того, чтобы его авторизовать. К примеру, этот метод может сравнивать строку $user->getAuthPassword() с результатом вызова Hash::make а строке $credentials['password'].

Теперь, когда мы узнали о каждом методе интерфейса UserProviderInterfaceдавайте посмотрим на интерфейс UserInterface. Как вы помните, поставщик должен вернуть реализацию этого интерфейса из своих методов retrieveById и retrieveByCredentials.

interface UserInterface {

	public function getAuthIdentifier();
	public function getAuthPassword();

}

Это простой интерфейс. Метод getAuthIdentifier должен просто вернуть "первичный ключ" пользователя. Если используется хранилище MySQL, то это будет автоматическое числовое поле - первичный ключ. Метод getAuthPassword должен вернуть хэшированный пароль. Этот интерфейс позволяет системе авторизации работать с любым классом пользователя, вне зависимости от используемой ORM или хранилища данных. Изначально Laravel содержит класс User в папке app/models который реализует этот интерфейс, поэтому мы можете обратиться к этому классу, чтобы увидеть пример реализации.

Наконец, как только мы написали класс-реализацию UserProviderInterface, у нас готово для регистрации расширения в фасаде Auth:

Auth::extend('riak', function($app)
{
	return new RiakUserProvider($app['riak.connection']);
});

Когда вы зарегистрировали драйвер методом extend вы можете активировать него в вашем файле настроек app/config/auth.php.

Расширения посредством IoC

Почти каждый сервис-провайдер Laravel получает свои объекты из контейнера IoC. Вы можете увидеть список сервис-провайдеров в вашем приложения в файле app/config/app.php. Вам стоит пробежаться по коду каждого из поставщиков в свободное время - сделав это вы получите намного более чёткое представление о том, какую именно функционалньость каждый из них добавляет к фреймворку, а также какие ключи используются для регистрации различных услуг в контейнере IoC.

Например, HashServiceProvider использует ключ hash для получения экземпляра Illuminate\Hashing\BcryptHasher из контейнера IoC. Вы можете легко расширить и перекрыть этот класс в вашем приложении, перекрыв эту привязку. Например:

class SnappyHashProvider extends Illuminate\Hashing\HashServiceProvider {

	public function boot()
	{
		App::bindShared('hash', function()
		{
			return new Snappy\Hashing\ScryptHasher;
		});

		parent::boot();
	}

}

Заметьте, что этот класс расширяет HashServiceProvider, а не базовый класс ServiceProvider. Как только вы расширили этот сервис-провайдер, измените HashServiceProvider в файле настроек app/config/app.php на имя вашего нового сервис-провайдера.

Это общий подход к расширению любого класса ядра, который привязан к IoC-контейнеру. Фактически каждый класс так или иначе привязан к нему, и с помощью вышеописанного метода может быть перекрыт. Опять же, посмотрев код сервис-провайдеров фреймворка, вы познакомитесь с тем, где различные классы привязываются к IoC-контейнеру и какие ключи для этого используются. Это отличный способ понять глубинную работу Laravel.

Расширение Request

Класс Request это основополагающая часть фреймворка и создаётся в самом начале обработки запроса - gj'njve его расширение немного отличается от описанного выше.

Для начала расширьте класс, как это обычно делается:

<?php namespace QuickBill\Extensions;

class Request extends \Illuminate\Http\Request {

	// Здесь ваши собственные полезные методы

}

Как только класс расширен, откройте файл bootstrap/start.php. Это один из старт-файлов, подключаемых в самом начале обработки запроса в вашем приложении. Заметьте, что самое первое, что здесь происходит - создание объекта $app:

$app = new \Illuminate\Foundation\Application;

Когда объект приложения создан, он создаст новый объект Illuminate\Http\Request и привяжет его к IoC-контейнеру с ключом request. Итак, нам нужен способ для указания нашего собственного класса, который должен использоваться в виде объекта запроса по умолчанию, верно? К счастью, метод объекта приложения requestClass выполняет как раз эту задачу! Итак, мы можем добавить эти строки в начало вашего файла bootstrap/start.php:

use Illuminate\Foundation\Application;

Application::requestClass('QuickBill\Extensions\Request');

Как только вы указали сторонний класс запроса, Laravel будет использовать его каждый раз при создании объекта Request, позволяя вам всегда под рукой собственную реализацию, даже в юнит-тестах.