За последние годы Laravel стал одним из самых известных фреймворков, который используют разработчики для создания своих приложений. Имея такую же популярность, какую имел CodeIgniter во время своего расцвета, Laravel славится своей простотой в использовании, удобством для начинающих и верностью отраслевым стандартам.
Введение
Почему-то немногие разработчики пользуются тем преимуществом, что Laravel — компонентная система. С момента перехода к компонентам на основе Composer, Laravel 4 стал очень модульной системой, похожей по разнообразию возможностей на более взрослые фреймворки, такие как Symfony. Такая группа компонентов, называемая PHPIlluminate
, на мой взгляд, не является самим фреймворком, но представляет собой подборку библиотек, которые потенциально могут быть использованы фреймворком. Сам фреймворк Laravel представлен приложением-каркасом Laravel (который находится в репозитории на GitHub PHPlaravel/laravel
), в котором эти компоненты используются для создания реальных веб-приложений.
В этом уроке мы углубимся в группу этих компонентов, узнаем, как они работают, как они используются фреймворком, и как мы можем расширить их возможности.
Компонент Session
Компонент Laravel Session обрабатывает сессии в веб-приложении. Он использует систему на основе драйверов, которая называется Laravel Manager и выступает как фабрика и как обертка для любого драйвера, указанного в файле конфигурации. На момент написания этой статьи в компоненте Session есть драйверы для:
- file — драйвер сессии на основе файлов, в котором данные сессии хранятся в зашифрованном файле.
- cookie — драйвер сессии на основе cookie, в котором данные сессии шифруются в пользовательские cookie.
- database — данные сессии хранятся в БД, настроенной для приложения.
- apc — данные сессии хранятся в APC.
- memcached — данные сессии хранятся в Memcached.
- redis — данные сессии хранятся в Redis.
- array — данные сессии хранятся в массиве PHP. Обратите внимание на то, что драйвер сессии на основе массива не поддерживает хранение и обычно используется в консольных командах.
Сервис-провайдеры
Многие пользователи Laravel не осознают это, но большая часть того, как работает Laravel, находится внутри его сервис-провайдеров. Они по существу являются загрузочными файлами для каждого компонента и достаточно абстрагированы, поэтому пользователи могут загружать любые компоненты, любыми способами.
Вот краткое объяснение того, как это работает:
- Инициируется компонент приложения Laravel Application. Это основной драйвер всего фреймворка, отвечающий за обработку HTTP-запросов, запускающий сервис-провайдеры, а также выступающий в качестве контейнера зависимостей для фреймворка.
- После запуска сервис-провайдера вызывается его метод
PHPregister
. Это позволяет нам получать экземпляр любого компонента.- Помните, что у всех сервис-провайдеров есть доступ к основному приложению Laravel (через
PHP$this->app
), что позволяет им размещать экземпляры реализованных классов в контейнере зависимостей.
- Помните, что у всех сервис-провайдеров есть доступ к основному приложению Laravel (через
- Когда эти зависимости загружены, мы можем свободно использовать их, вызывая в контейнере, например, через систему фасадов Laravel Facade,
PHPApp::make
.
Вернемся к сессиям и взглянем на PHPSessionServiceProivider
:
/**
* Регистрация экземпляра менеджера сессий.
*
* @return void
*/
protected function registerSessionManager()
{
$this->app->bindShared('session', function($app)
{
return new SessionManager($app);
});
}
/**
* Регистрация экземпляра драйвера сессий.
*
* @return void
*/
protected function registerSessionDriver()
{
$this->app->bindShared('session.store', function($app)
{
// Сначала мы создадим менеджер сессий, который отвечает за
// создание различных драйверов сессий, когда они нужны
// экземпляру приложения, и будем работать с ними в режиме "ленивой" нагрузки.
$manager = $app['session'];
return $manager->driver();
});
}
Эти два метода вызываются функцией PHPregister()
. Первый PHPregisterSessionManager()
вызывается для начальной регистрации PHPSessionManager
. Этот класс наследует PHPManager
, который я упоминал выше. Второй PHPregisterSessionDriver()
регистрирует обработчик сессий для менеджера на основе нашей конфигурации. В конечном итоге в классе PHPIlluminate\Support\Manager
вызывается этот метод:
/**
* Создание нового экземпляра драйвера.
*
* @param string $driver
* @return mixed
*
* @throws \InvalidArgumentException
*/
protected function createDriver($driver)
{
$method = 'create'.ucfirst($driver).'Driver';
// Мы проверяем, существует ли метод-конструктор для данного драйвера. Если нет, мы
// проверяем пользовательский конструктор драйвера, который позволяет пользователям создавать
// драйверы, используя собственное замыкание.
if (isset($this->customCreators[$driver]))
{
return $this->callCustomCreator($driver);
}
elseif (method_exists($this, $method))
{
return $this->$method();
}
throw new \InvalidArgumentException("Driver [$driver] not supported.");
}
Здесь мы видим что, исходя из имени драйвера в файле конфигурации, вызывается определенный метод. Поэтому, если у нас настроено использование обработчика PHPfile
-сессий, в классе PHPSessionManager
будет вызван этот метод:
/**
* Создание экземпляра драйвера file-сессий.
*
* @return \Illuminate\Session\Store
*/
protected function createFileDriver()
{
return $this->createNativeDriver();
}
/**
* Создание экземпляра драйвера file-сессий.
*
* @return \Illuminate\Session\Store
*/
protected function createNativeDriver()
{
$path = $this->app['config']['session.files'];
return $this->buildSession(new FileSessionHandler($this->app['files'], $path));
}
Затем класс драйвера вводится в класс PHPStore
, который отвечает за вызов методов текущей сессии. Это позволяет нам отделить реализацию PHPSessionHandlerInterface
от SPL в драйверах, класс PHPStore
способствует этому.
Создание собственного обработчика сессий
Давайте создадим наш собственный обработчик для сессий MongoDB. Сначала нам надо создать PHPMongoSessionHandler
внутри свежеустановленного проекта Laravel. (Мы позаимствуем его из PHPSymfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler
):
<?php namespace Illuminate\Session;
use Mongo;
use MongoDate;
use MongoBinData;
class MongoSessionHandler implements \SessionHandlerInterface
{
/**
* Настройки Mongo
*
* @var array
*/
protected $config;
/**
* Соединение с Mongo
*
* @var \Mongo
*/
protected $connection;
/**
* Коллекция Mongo
*
* @var \MongoCollection
*/
protected $collection;
/**
* Создание нового экземпляра обработчика для Mongo.
*
* @param array $config
* - $config['host'] Mongodb-хост
* - $config['username'] Mongodb-логин
* - $config['password'] Mongodb-пароль
* - $config['database'] Mongodb-БД
* - $config['collection'] Mongodb-коллекция
* @return void
*/
public function __construct(array $config)
{
$this->config = $config;
$connection_string = 'mongodb://';
if (!empty($this->config['username']) && !empty($this->config['password'])) {
$connection_string .= "{$this->config['user']}:{$this->config['password']}@";
}
$connection_string .= "{$this->config['host']}";
$this->connection = new Mongo($connection_string);
$this->collection = $this->connection->selectCollection($this->config['database'], $this->config['collection']);
}
/**
* {@inheritDoc}
*/
public function open($savePath, $sessionName)
{
return true;
}
/**
* {@inheritDoc}
*/
public function close()
{
return true;
}
/**
* {@inheritDoc}
*/
public function read($sessionId)
{
$session_data = $this->collection->findOne(array(
'_id' => $sessionId,
));
if (is_null($session_data)) {
return '';
} else {
return $session_data['session_data']->bin;
}
}
/**
* {@inheritDoc}
*/
public function write($sessionId, $data)
{
$this->collection->update(
array(
'_id' => $sessionId
),
array(
'$set' => array(
'session_data' => new MongoBinData($data, MongoBinData::BYTE_ARRAY),
'timestamp' => new MongoDate(),
)
),
array(
'upsert' => true,
'multiple' => false
)
);
}
/**
* {@inheritDoc}
*/
public function destroy($sessionId)
{
$this->collection->remove(array(
'_id' => $sessionId
));
return true;
}
/**
* {@inheritDoc}
*/
public function gc($lifetime)
{
$time = new MongoDate(time() - $lifetime);
$this->collection->remove(array(
'timestamp' => array('$lt' => $time),
));
return true;
}
}
Вам надо сохранить его в папке PHPvendor/laravel/framework/src/Illuminate/Session
. Для этого примера мы поместим его сюда, но правильнее размещать этот файл в пространстве имен его собственной библиотеки.
Далее нам надо убедиться в том, что класс PHPManager
может вызвать этот драйвер. Это можно сделать с помощью метода PHPManager::extend
. Откройте vendor/laravel/framework/src/Illuminate/Session/SessionServiceProvider.php и добавьте следующий код. Правильнее было бы наследовать сервис-провайдер, но это выходит за рамки данного урока.
/**
* Установка обратного вызова драйвера Mongo
*
* @return void
*/
public function setupMongoDriver()
{
$manager = $this->app['session'];
$manager->extend('mongo', function($app) {
return new MongoSessionHandler(array(
'host' => $app['config']->get('session.mongo.host'),
'username' => $app['config']->get('session.mongo.username'),
'password' => $app['config']->get('session.mongo.password'),
'database' => $app['config']->get('session.mongo.database'),
'collection' => $app['config']->get('session.mongo.collection')
));
});
}
Не забудьте обновить метод PHPregister()
для вызова этого метода:
/**
* Регистрация сервис-провайдера.
*
* @return void
*/
public function register()
{
$this->setupDefaultDriver();
$this->registerSessionManager();
$this->setupMongoDriver();
$this->registerSessionDriver();
}
Далее нам надо задать конфигурацию Mongo DB. Откройте app/config/session.php и задайте следующие настройки:
/**
* Настройки Mongo DB
*/
'mongo' => array(
'host' => '127.0.0.1',
'username' => '',
'password' => '',
'database' => 'laravel',
'collection' => 'laravel_session_collection'
)
Пока мы в этом файле, нам также надо обновить настройку driver вверху:
'driver' => 'mongo'
Теперь попробуем зайти на главную страницу (обычно это localhost/somefolder/public). Если эта страница загружается без отображения страницы WHOOPS, тогда примите мои поздравления, мы успешно создали совершенно новый драйвер сессий! Протестируем его, задавая какие-либо данные о сессии через PHPSession::set()
, и затем получая их обратно через PHPSession::get()
.
Компонент Auth
Компонент Laravel Auth выполняет проверку подлинности пользователя для фреймворка, а также управляет паролями. Компонент Laravel создает абстрактную интерпретацию типичной системы управления пользователями, которая используется в большинстве веб-приложений, и помогает программисту легко внедрить систему регистрации и входа. Как и компонент Session он использует Laravel Manager. В настоящее время компонент Auth имеет драйверы для:
- eloquent — для использования встроенной в Laravel ORM, которая называется Eloquent. Он также использует готовый класс User.php из папки models.
- database — для использования подключения к БД, которое настроено по умолчанию. Он использует класс
PHPGenericUser
для доступа к данным пользователя.
Поскольку этот компонент следует той же реализации, что и PHPSession
, сервис-провайдер очень похож на тот, который мы видели вначале:
/**
* Регистрация сервис-провайдера.
*
* @return void
*/
public function register()
{
$this->app->bindShared('auth', function($app)
{
// Когда разработчиком запрашивается служба аутентификации
// мы задаем значение переменной в приложении для индикации этого. Это дает нам
// знать, что нам надо будет задать некоторые очередные cookies в последующем событии.
$app['auth.loaded'] = true;
return new AuthManager($app);
});
}
Здесь мы видим, что создается класс PHPAuthManager
, который служит оберткой для используемого нами драйвера, а также выступает в качестве фабрики для него. Внутри PHPAuthManager
он снова создает соответствующий драйвер, обернутый вокруг класса PHPGuard
, который действует так же, как класс PHPStore
из PHPSession
.
Создание собственного обработчика аутентификации
Так же как раньше, давайте начнем с создания PHPMongoUserProvider
:
<?php namespace Illuminate\Auth;
use Mongo;
use Illuminate\Hashing\HasherInterface;
class MongoUserProvider implements UserProviderInterface {
/**
* Экземпляр mongo
*
* @param \Mongo
*/
protected $connection;
/**
* Экземпляр подключения mongo
*
* @param \MongoConnection
*/
protected $collection;
/**
* Массив конфигурации Mongo
*
* @var array
*/
protected $config;
/**
* Создание нового user-провайдера Mongo.
*
* @param array $config
* - $config['host'] Mongodb-хост
* - $config['username'] Mongodb-логин
* - $config['password'] Mongodb-пароль
* - $config['database'] Mongodb-БД
* - $config['collection'] Mongodb-коллекция
* @return void
*/
public function __construct(array $config)
{
$this->config = $config;
$connection_string = 'mongodb://';
if (!empty($this->config['username']) && !empty($this->config['password'])) {
$connection_string .= "{$this->config['user']}:{$this->config['password']}@";
}
$connection_string .= "{$this->config['host']}";
$this->connection = new Mongo($connection_string);
$this->collection = $this->connection->selectCollection($this->config['database'], $this->config['collection']);
}
/**
* Получение пользователя по его уникальному идентификатору.
*
* @param mixed $identifier
* @return \Illuminate\Auth\UserInterface|null
*/
public function retrieveById($identifier)
{
$user_data = $this->collection->findOne(array(
'_id' => $identifier,
));
if (!is_null($user_data)) {
return new GenericUser((array) $user_data);
}
}
/**
* Получение пользователя по введенным данным авторизации.
*
* @param array $credentials
* @return \Illuminate\Auth\UserInterface|null
*/
public function retrieveByCredentials(array $credentials)
{
// Попытка найти пользователя сначала независимо от пароля
// Мы сделаем это в методе validateCredentials
if (isset($credentials['password'])) {
unset($credentials['password']);
}
$user_data = $this->collection->findOne($credentials);
if (!is_null($user_data)) {
return new GenericUser((array) $user_data);
}
}
/**
* Подтверждение пользователя на основе введенных данных авторизации.
*
* @param \Illuminate\Auth\UserInterface $user
* @param array $credentials
* @return bool
*/
public function validateCredentials(UserInterface $user, array $credentials)
{
if (!isset($credentials['password'])) {
return false;
}
return ($credentials['password'] === $user->getAuthPassword());
}
}
Важно заметить, что я не проверяю хешированный пароль, это сделано для упрощения примера, чтобы облегчить создание тестовых данных и дальнейшую проверку. В рабочем коде вам надо убедиться, что пароль хешируется. Замечательный пример того, как это сделать, есть в классе PHPIlluminate\Auth\DatabaseUserProvider
.
После этого нам надо зарегистрировать наш обратный вызов драйвера в PHPAuthManager
. Для этого нам необходимо обновить метод PHPregister
сервис-провайдера:
/**
* Регистрация сервис-провайдера.
*
* @return void
*/
public function register()
{
$this->app->bindShared('auth', function($app)
{
// Когда разработчиком запрашивается служба аутентификации
// мы задаем значение переменной в приложении для индикации этого. Это дает нам
// знать, что нам надо будет задать некоторые очередные cookies в последующем событии.
$app['auth.loaded'] = true;
$auth_manager = new AuthManager($app);
$auth_manager->extend('mongo', function($app) {
return new MongoUserProvider(
array(
'host' => $app['config']->get('auth.mongo.host'),
'username' => $app['config']->get('auth.mongo.username'),
'password' => $app['config']->get('auth.mongo.password'),
'database' => $app['config']->get('auth.mongo.database'),
'collection' => $app['config']->get('auth.mongo.collection')
)
);
});
return $auth_manager;
});
}
Наконец, нам также надо обновить файл конфигурации auth.php для использования драйвера Mongo, а также предоставить ему соответствующие значения конфигурации Mongo:
'driver' => 'mongo',
...
...
...
/**
* Mongo DB settings
*/
'mongo' => array(
'host' => '127.0.0.1',
'username' => '',
'password' => '',
'database' => 'laravel',
'collection' => 'laravel_auth_collection'
)
Проверка будет немного сложнее, для этого используйте командную строку Mongo DB, чтобы вставить нового пользователя в коллекцию:
shell% mongo > use laravel_auth switched to db laravel_auth > db.laravel_auth_collection.insert({id: 1, email:"nikko@nikkobautista.com", password:"test_password"}) > db.laravel_auth_collection.find() > { "_id" : ObjectId("530c609f2caac8c3a8e4814f"), "id" 1, "email" : "nikko@emailtest.com", "password" : "test_password" }
Теперь протестируем его, попробовав вызвать метод PHPAuth::validate
:
var_dump(Auth::validate(array('email' => 'nikko@emailtest.com', 'password' => 'test_password')));
Должно получиться bool(true). Если получилось, значит мы успешно создали свой собственный драйвер Auth!
Компонент Cache
Компонент Laravel Cache обрабатывает механизмы кэширования для использования в фреймворке. Как и оба рассмотренных компонента, он также использует Laravel Manager (заметили шаблон?). Компонент Cache имеет драйверы для:
- apc
- memcached
- redis
- file — кэш на основе файлов. Данные хранятся в папке app/storage/cache.
- database — кэш на основе БД. Данные хранятся в строках в БД. Схема БД описана в документации Laravel.
- array — данные кэшируются в массив. Помните, что кэш в массиве не сохраняется и зачищается при каждой загрузке страницы.
Поскольку используется та же реализация, что и в рассмотренных выше компонентах, вы можете с уверенностью предположить, что и сервис-провайдер очень похож:
/**
* Регистрация сервис-провайдера.
*
* @return void
*/
public function register()
{
$this->app->bindShared('cache', function($app)
{
return new CacheManager($app);
});
$this->app->bindShared('cache.store', function($app)
{
return $app['cache']->driver();
});
$this->app->bindShared('memcached.connector', function()
{
return new MemcachedConnector;
});
$this->registerCommands();
}
Здесь метод PHPregister()
создает PHPCacheManager
, который снова выступает в качестве обертки и фабрики для драйвера. В менеджере он оборачивает драйвер вокруг класса PHPRepository
, подобно классам PHPStore
и PHPGuard
.
Создание собственного обработчика кэша
Создайте PHPMongoStore
, который должен наследовать PHPIlluminate\Cache\StoreInterface
:
<?php namespace Illuminate\Cache;
use Mongo;
class MongoStore implements StoreInterface
{
/**
* Экземпляр mongo
*
* @param \Mongo
*/
protected $connection;
/**
* Экземпляр подключения mongo
*
* @param \MongoConnection
*/
protected $collection;
/**
* Массив конфигурации Mongo
*
* @var array
*/
protected $config;
/**
* Создание нового хранилища кэша Mongo.
*
* @param array $config
* - $config['host'] Mongodb-хост
* - $config['username'] Mongodb-логин
* - $config['password'] Mongodb-пароль
* - $config['database'] Mongodb-БД
* - $config['collection'] Mongodb-коллекция
* @return void
*/
public function __construct(array $config)
{
$this->config = $config;
$connection_string = 'mongodb://';
if (!empty($this->config['username']) && !empty($this->config['password'])) {
$connection_string .= "{$this->config['user']}:{$this->config['password']}@";
}
$connection_string .= "{$this->config['host']}";
$this->connection = new Mongo($connection_string);
$this->collection = $this->connection->selectCollection($this->config['database'], $this->config['collection']);
}
/**
* Извлечение элемента из кэша по ключу.
*
* @param string $key
* @return mixed
*/
public function get($key)
{
$cache_data = $this->getObject($key);
if (!$cache_data) {
return null;
}
return unserialize($cache_data['cache_data']);
}
/**
* Возвращение целого объекта, а не только cache_data
*
* @param string $key
* @return array|null
*/
protected function getObject($key)
{
$cache_data = $this->collection->findOne(array(
'key' => $key,
));
if (is_null($cache_data)) {
return null;
}
if (isset($cache_data['expire']) && time() >= $cache_data['expire']) {
$this->forget($key);
return null;
}
return $cache_data;
}
/**
* Сохранение элемента в кэше на заданное количество минут.
*
* @param string $key
* @param mixed $value
* @param int $minutes
* @return void
*/
public function put($key, $value, $minutes)
{
$expiry = $this->expiration($minutes);
$this->collection->update(
array(
'key' => $key
),
array(
'$set' => array(
'cache_data' => serialize($value),
'expiry' => $expiry,
'ttl' => ($minutes * 60)
)
),
array(
'upsert' => true,
'multiple' => false
)
);
}
/**
* Инкрементирование значения элемента в кэше.
*
* @param string $key
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
public function increment($key, $value = 1)
{
$cache_data = $this->getObject($key);
if (!$cache_data) {
$new_data = array(
'cache_data' => serialize($value),
'expiry' => $this->expiration(0),
'ttl' => $this->expiration(0)
);
} else {
$new_data = array(
'cache_data' => serialize(unserialize($cache_data['cache_data']) + $value),
'expiry' => $this->expiration((int) ($cache_data['ttl']/60)),
'ttl' => $cache_data['ttl']
);
}
$this->collection->update(
array(
'key' => $key
),
array(
'$set' => $new_data
),
array(
'upsert' => true,
'multiple' => false
)
);
}
/**
* Декрементирование значения элемента в кэше.
*
* @param string $key
* @param mixed $value
* @return void
*
* @throws \LogicException
*/
public function decrement($key, $value = 1)
{
$cache_data = $this->getObject($key);
if (!$cache_data) {
$new_data = array(
'cache_data' => serialize((0 - $value)),
'expiry' => $this->expiration(0),
'ttl' => $this->expiration(0)
);
} else {
$new_data = array(
'cache_data' => serialize(unserialize($cache_data['cache_data']) - $value),
'expiry' => $this->expiration((int) ($cache_data['ttl']/60)),
'ttl' => $cache_data['ttl']
);
}
$this->collection->update(
array(
'key' => $key
),
array(
'$set' => $new_data
),
array(
'upsert' => true,
'multiple' => false
)
);
}
/**
* Сохранение элемента в кэше на неопределенное время.
*
* @param string $key
* @param mixed $value
* @return void
*/
public function forever($key, $value)
{
return $this->put($key, $value, 0);
}
/**
* Удаление элемента из кэша.
*
* @param string $key
* @return void
*/
public function forget($key)
{
$this->collection->remove(array(
'key' => $key
));
}
/**
* Удаление всех элементов из кэша.
*
* @return void
*/
public function flush()
{
$this->collection->remove();
}
/**
* Получение срока хранения на основе введенных минут.
*
* @param int $minutes
* @return int
*/
protected function expiration($minutes)
{
if ($minutes === 0) return 9999999999;
return time() + ($minutes * 60);
}
/**
* Получение префикса ключа кэша.
*
* @return string
*/
public function getPrefix()
{
return '';
}
}
Нам также надо снова добавить обратный вызов Mongo в менеджер:
/**
* Регистрация сервис-провайдера.
*
* @return void
*/
public function register()
{
$this->app->bindShared('cache', function($app)
{
$cache_manager = new CacheManager($app);
$cache_manager->extend('mongo', function($app) {
return new MongoStore(
array(
'host' => $app['config']->get('cache.mongo.host'),
'username' => $app['config']->get('cache.mongo.username'),
'password' => $app['config']->get('cache.mongo.password'),
'database' => $app['config']->get('cache.mongo.database'),
'collection' => $app['config']->get('cache.mongo.collection')
)
);
});
return $cache_manager;
});
$this->app->bindShared('cache.store', function($app)
{
return $app['cache']->driver();
});
$this->app->bindShared('memcached.connector', function()
{
return new MemcachedConnector;
});
$this->registerCommands();
}
Наконец, нам надо обновить файл конфигурации cache.php:
'driver' => 'mongo',
// ...
/**
* Mongo DB settings
*/
'mongo' => array(
'host' => '127.0.0.1',
'username' => '',
'password' => '',
'database' => 'laravel',
'collection' => 'laravel_cache_collection'
)
Теперь попробуем использовать методы PHPCache::put()
и PHPCache::get()
. Если все сделано правильно, то мы сможем использовать MongoDB для кэширования данных!
Заключение
- Систему на основе компонентов Laravel
PHPIlluminate
, используемую в фреймворке Laravel. - Сервис-провайдеры Laravel, и немного о том, как они работают.
- Систему Laravel Manager, которая выступает в качестве обертки и фабрики для драйверов.
- Компоненты Session, Auth и Cache, и как создавать драйверы для каждого из них.
- Библиотеки Store, Guard и Repository, которые используют эти драйверы.
Надеюсь, это поможет программистам создавать их собственные драйверы и расширять имеющуюся функциональность фреймворка Laravel.