{{TOC}} За последние годы ((http://laravel.com/==Laravel)) стал одним из самых известных фреймворков, который используют разработчики для создания своих приложений. Имея такую же популярность, какую имел ((http://ellislab.com/codeigniter==CodeIgniter)) во время своего расцвета, Laravel славится своей простотой в использовании, удобством для начинающих и верностью отраслевым стандартам. ==Введение== Почему-то немногие разработчики пользуются тем преимуществом, что Laravel - компонентная система. С момента перехода к компонентам на основе Composer, Laravel 4 стал очень модульной системой, похожей по разнообразию возможностей на более взрослые фреймворки, такие как Symfony. Такая группа компонентов, называемая %%Illuminate%%, на мой взгляд, не является самим фреймворком, но представляет собой подборку библиотек, которые потенциально могут быть использованы фреймворком. Сам фреймворк Laravel представлен приложением-каркасом Laravel (который находится в ((https://github.com/laravel/laravel==репозитории на GitHub)) %%laravel/laravel%%), в котором эти компоненты используются для создания реальных веб-приложений. В этом уроке мы углубимся в группу этих компонентов, узнаем, как они работают, как они используются фреймворком, и как мы можем расширить их возможности. ==Компонент Session== Компонент Laravel Session обрабатывает сессии в веб-приложении. Он использует систему на основе драйверов, которая называется Laravel Manager и выступает как фабрика и как обертка для любого драйвера, указанного в файле конфигурации. На момент написания этой статьи в компоненте Session есть драйверы для: * %%(t)file%% - драйвер сессии на основе файлов, в котором данные сессии хранятся в зашифрованном файле. * %%(t)cookie%% - драйвер сессии на основе cookie, в котором данные сессии шифруются в пользовательские cookie. * %%(t)database%% - данные сессии хранятся в БД, настроенной для приложения. * %%(t)apc%% - данные сессии хранятся в APC. * %%(t)memcached%% - данные сессии хранятся в Memcached. * %%(t)redis%% - данные сессии хранятся в Redis. * %%(t)array%% - данные сессии хранятся в массиве PHP. Обратите внимание на то, что драйвер сессии на основе массива не поддерживает хранение и обычно используется в консольных командах. ===Сервис-провайдеры=== Многие пользователи Laravel не осознают это, но большая часть того, как работает Laravel, находится внутри его сервис-провайдеров. Они по существу являются загрузочными файлами для каждого компонента и достаточно абстрагированы, поэтому пользователи могут загружать любые компоненты, любыми способами. Вот краткое объяснение того, как это работает: 1. Инициируется компонент приложения Laravel Application. Это основной драйвер всего фреймворка, отвечающий за обработку HTTP-запросов, запускающий сервис-провайдеры, а также выступающий в качестве контейнера зависимостей для фреймворка. 2. После запуска сервис-провайдера вызывается его метод %%register%%. Это позволяет нам получать экземпляр любого компонента. * Помните, что у всех сервис-провайдеров есть доступ к основному приложению Laravel (через %%$this->app%%), что позволяет им размещать экземпляры реализованных классов в контейнере зависимостей. 3. Когда эти зависимости загружены, мы можем свободно использовать их, вызывая в контейнере, например, через систему фасадов Laravel Facade, %%App::make%%. Вернемся к сессиям и взглянем на %%SessionServiceProivider%%: %% /** * Регистрация экземпляра менеджера сессий. * * @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(); }); } %% Эти два метода вызываются функцией %%register()%%. Первый %%registerSessionManager()%% вызывается для начальной регистрации %%SessionManager%%. Этот класс наследует %%Manager%%, который я упоминал выше. Второй %%registerSessionDriver()%% регистрирует обработчик сессий для менеджера на основе нашей конфигурации. В конечном итоге в классе %%Illuminate\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."); } %% Здесь мы видим что, исходя из имени драйвера в файле конфигурации, вызывается определенный метод. Поэтому, если у нас настроено использование обработчика %%file%%-сессий, в классе %%SessionManager%% будет вызван этот метод: %% /** * Создание экземпляра драйвера 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)); } %% Затем класс драйвера вводится в класс %%Store%%, который отвечает за вызов методов текущей сессии. Это позволяет нам отделить реализацию %%SessionHandlerInterface%% от SPL в драйверах, класс %%Store%% способствует этому. ===Создание собственного обработчика сессий=== Давайте создадим наш собственный обработчик для сессий MongoDB. Сначала нам надо создать %%MongoSessionHandler%% внутри свежеустановленного проекта Laravel. (Мы позаимствуем его из %%Symfony\Component\HttpFoundation\Session\Storage\Handler\MongoDbSessionHandler%%): %% 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; } } %% Вам надо сохранить его в папке %%vendor/laravel/framework/src/Illuminate/Session%%. Для этого примера мы поместим его сюда, но правильнее размещать этот файл в пространстве имен его собственной библиотеки. Далее нам надо убедиться в том, что класс %%Manager%% может вызвать этот драйвер. Это можно сделать с помощью метода %%Manager::extend%%. Откройте %%(t)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') )); }); } %% Не забудьте обновить метод %%register()%% для вызова этого метода: %% /** * Регистрация сервис-провайдера. * * @return void */ public function register() { $this->setupDefaultDriver(); $this->registerSessionManager(); $this->setupMongoDriver(); $this->registerSessionDriver(); } %% Далее нам надо задать конфигурацию Mongo DB. Откройте %%(t)app/config/session.php%% и задайте следующие настройки: %% /** * Настройки Mongo DB */ 'mongo' => array( 'host' => '127.0.0.1', 'username' => '', 'password' => '', 'database' => 'laravel', 'collection' => 'laravel_session_collection' ) %% Пока мы в этом файле, нам также надо обновить настройку %%(t)driver%% вверху: %% 'driver' => 'mongo' %% Теперь попробуем зайти на главную страницу (обычно это %%(t)localhost/somefolder/public%%). Если эта страница загружается без отображения страницы %%(t)WHOOPS%%, тогда примите мои поздравления, мы успешно создали совершенно новый драйвер сессий! Протестируем его, задавая какие-либо данные о сессии через %%Session::set()%%, и затем получая их обратно через %%Session::get()%%. ==Компонент Auth== Компонент Laravel Auth выполняет проверку подлинности пользователя для фреймворка, а также управляет паролями. Компонент Laravel создает абстрактную интерпретацию типичной системы управления пользователями, которая используется в большинстве веб-приложений, и помогает программисту легко внедрить систему регистрации и входа. Как и компонент Session он использует Laravel Manager. В настоящее время компонент Auth имеет драйверы для: * %%(t)eloquent%% - для использования встроенной в Laravel ORM, которая называется %%(t)Eloquent%%. Он также использует готовый класс %%(t)User.php%% из папки %%(t)models%%. * %%(t)database%% - для использования подключения к БД, которое настроено по умолчанию. Он использует класс %%GenericUser%% для доступа к данным пользователя. Поскольку этот компонент следует той же реализации, что и %%Session%%, сервис-провайдер очень похож на тот, который мы видели вначале: %% /** * Регистрация сервис-провайдера. * * @return void */ public function register() { $this->app->bindShared('auth', function($app) { // Когда разработчиком запрашивается служба аутентификации // мы задаем значение переменной в приложении для индикации этого. Это дает нам // знать, что нам надо будет задать некоторые очередные cookies в последующем событии. $app['auth.loaded'] = true; return new AuthManager($app); }); } %% Здесь мы видим, что создается класс %%AuthManager%%, который служит оберткой для используемого нами драйвера, а также выступает в качестве фабрики для него. Внутри %%AuthManager%% он снова создает соответствующий драйвер, обернутый вокруг класса %%Guard%%, который действует так же, как класс %%Store%% из %%Session%%. ===Создание собственного обработчика аутентификации=== Так же как раньше, давайте начнем с создания %%MongoUserProvider%%: %% 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()); } } %% Важно заметить, что я не проверяю хешированный пароль, это сделано для упрощения примера, чтобы облегчить создание тестовых данных и дальнейшую проверку. В рабочем коде вам надо убедиться, что пароль хешируется. Замечательный пример того, как это сделать, есть в классе %%Illuminate\Auth\DatabaseUserProvider%%. После этого нам надо зарегистрировать наш обратный вызов драйвера в %%AuthManager%%. Для этого нам необходимо обновить метод %%register%% сервис-провайдера: %% /** * Регистрация сервис-провайдера. * * @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; }); } %% Наконец, нам также надо обновить файл конфигурации %%(t)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" } %% Теперь протестируем его, попробовав вызвать метод %%Auth::validate%%: %% var_dump(Auth::validate(array('email' => 'nikko@emailtest.com', 'password' => 'test_password'))); %% Должно получиться %%(t)bool(true)%%. Если получилось, значит мы успешно создали свой собственный драйвер Auth! ==Компонент Cache== Компонент Laravel Cache обрабатывает механизмы кэширования для использования в фреймворке. Как и оба рассмотренных компонента, он также использует Laravel Manager (заметили шаблон?). Компонент Cache имеет драйверы для: * %%(t)apc%% * %%(t)memcached%% * %%(t)redis%% * %%(t)file%% - кэш на основе файлов. Данные хранятся в папке %%(t)app/storage/cache%%. * %%(t)database%% - кэш на основе БД. Данные хранятся в строках в БД. Схема БД описана в ((док4:cache==документации Laravel)). * %%(t)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(); } %% Здесь метод %%register()%% создает %%CacheManager%%, который снова выступает в качестве обертки и фабрики для драйвера. В менеджере он оборачивает драйвер вокруг класса %%Repository%%, подобно классам %%Store%% и %%Guard%%. ===Создание собственного обработчика кэша=== Создайте %%MongoStore%%, который должен наследовать %%Illuminate\Cache\StoreInterface%%: %% 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(); } %% Наконец, нам надо обновить файл конфигурации %%(t)cache.php%%: %% 'driver' => 'mongo', // ... /** * Mongo DB settings */ 'mongo' => array( 'host' => '127.0.0.1', 'username' => '', 'password' => '', 'database' => 'laravel', 'collection' => 'laravel_cache_collection' ) %% Теперь попробуем использовать методы %%Cache::put()%% и %%Cache::get()%%. Если все сделано правильно, то мы сможем использовать MongoDB для кэширования данных! ==Заключение== В этом уроке мы изучили: * Систему на основе компонентов Laravel %%Illuminate%%, используемую в фреймворке Laravel. * Сервис-провайдеры Laravel, и немного о том, как они работают. * Систему Laravel Manager, которая выступает в качестве обертки и фабрики для драйверов. * Компоненты Session, Auth и Cache, и как создавать драйверы для каждого из них. * Библиотеки Store, Guard и Repository, которые используют эти драйверы. Надеюсь, это поможет программистам создавать их собственные драйверы и расширять имеющуюся функциональность фреймворка Laravel.