Статьи

Создайте свой собственный PHP Framework с компонентами Symfony

Вы, вероятно, встречали Symfony в своей карьере PHP — или хотя бы слышали об этом. Что вы можете не знать, так это то, что Symfony по своей сути состоит из отдельных библиотек, называемых компонентами , которые можно повторно использовать в любом приложении PHP.

Например, популярный PHP-фреймворк Laravel был разработан с использованием нескольких компонентов Symfony, которые мы также будем использовать в этом руководстве. Следующая версия популярного CMS Drupal также строится поверх некоторых основных компонентов Symfony.

Мы увидим, как создать минимальный PHP-фреймворк, используя эти компоненты, и как они могут взаимодействовать, чтобы создать базовую структуру для любого веб-приложения.

Примечание. Этот учебник не охватывает все компоненты Symfony и все функции каждого из них. Мы увидим только основные вещи, которые нам нужны для построения минимальной функциональной структуры. Если вы хотите углубиться в компоненты Symfony, я рекомендую вам прочитать их отличную документацию .

Создание проекта

Мы начнем с нуля с простого файла index.php в корне каталога нашего проекта и будем использовать Composer для установки зависимостей.

На данный момент наш файл будет содержать только этот простой фрагмент кода:

 switch($_SERVER['PATH_INFO']) { case '/': echo 'This is the home page'; break; case '/about': echo 'This is the about page'; break; default: echo 'Not found!'; } 

Этот код просто отображает запрошенный URL (содержащийся в $_SERVER['PATH_INFO'] ) в правильную команду echo . Это очень, очень примитивный маршрутизатор.

Компонент HttpFoundation

HttpFoundation действует как уровень верхнего уровня для работы с потоком HTTP. Его наиболее важными точками входа являются два класса Request и Response .

Request позволяет нам обрабатывать информацию HTTP-запроса, такую ​​как запрошенный URI или заголовки клиента, абстрагируя глобальные PHP-значения по умолчанию ( $_GET , $_POST и т. Д.). Response используется для отправки обратно HTTP-заголовков и данных ответа клиенту, вместо использования header или echo как мы это делаем в «классическом» PHP.

Установите его, используя composer:

 php composer.phar require symfony/http-foundation 2.5.* 

Это поместит библиотеку в каталог vendor . Теперь поместите в файл index.php следующее:

 // Initializes the autoloader generated by composer $loader = require 'vendor/autoload.php'; $loader->register(); use Symfony\Component\HttpFoundation\Request; $request = Request::createFromGlobals(); switch($request->getPathInfo()) { case '/': echo 'This is the home page'; break; case '/about': echo 'This is the about page'; break; default: echo 'Not found!'; } 

То, что мы сделали здесь, довольно просто:

  • Создайте экземпляр Request используя статический метод createFromGlobals . Вместо создания пустого объекта этот метод заполняет объект Request используя информацию о текущем запросе.
  • Проверьте значение, возвращаемое методом getPathInfo .

Мы также можем заменить различные команды echo , используя экземпляр Response для хранения нашего контента, и send его клиенту (который в основном выводит заголовки ответа и контент).

 $loader = require 'vendor/autoload.php'; $loader->register(); use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; $request = Request::createFromGlobals(); $response = new Response(); switch ($request->getPathInfo()) { case '/': $response->setContent('This is the website home'); break; case '/about': $response->setContent('This is the about page'); break; default: $response->setContent('Not found !'); $response->setStatusCode(Response::HTTP_NOT_FOUND); } $response->send(); 

Используйте HttpKernel, чтобы обернуть ядро ​​фреймворка

 php composer.phar require symfony/http-kernel 2.5.* 

На данный момент, как бы просто она ни была, логика фреймворка все еще находится в нашем фронт-контроллере, файле index.php . Если бы мы хотели добавить больше кода, было бы лучше поместить его в другой класс, который станет «ядром» нашей платформы.

Компонент HttpKernel был задуман с этой целью. Он предназначен для работы с HttpFoundation для преобразования экземпляра Request в Response и предоставляет нам несколько классов для достижения этой цели. На данный момент мы будем использовать HttpKernelInterface интерфейс HttpKernelInterface . Этот интерфейс определяет только один метод: handle .

Этот метод принимает экземпляр Request в качестве аргумента и должен возвращать Response . Таким образом, каждый класс, реализующий этот интерфейс, может обрабатывать Request и возвращать соответствующий объект Response .

Давайте создадим класс Core нашего фреймворка, который реализует HttpKernelInterface . Теперь создайте файл Core.php каталоге lib/Framework :

 <?php namespace Framework; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; class Core implements HttpKernelInterface { public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) { switch ($request->getPathInfo()) { case '/': $response = new Response('This is the website home'); break; case '/about': $response = new Response('This is the about page'); break; default: $response = new Response('Not found !', Response::HTTP_NOT_FOUND); } return $response; } } 

Примечание. Метод handle принимает еще два необязательных аргумента: тип запроса и логическое значение, указывающее, должно ли ядро ​​выдавать исключение в случае ошибки. Мы не будем использовать их в этом руководстве, но нам нужно реализовать точно такой же метод, который определен HttpKernelInterface , иначе PHP выдаст ошибку.

Единственное, что мы здесь сделали, — переместили существующий код в метод handle . Теперь мы можем избавиться от этого кода в index.php и использовать вместо этого наш недавно созданный класс:

 require 'lib/Framework/Core.php'; $request = Request::createFromGlobals(); // Our framework is now handling itself the request $app = new Framework\Core(); $response = $app->handle($request); $response->send(); 

Лучшая система маршрутизации

У нашего класса все еще есть проблема: он содержит логику маршрутизации нашего приложения. Если бы мы хотели добавить больше URL-адресов, чтобы соответствовать, мы должны были бы изменить код внутри нашей платформы — что явно не очень хорошая идея. Более того, это будет означать добавление блока наблюдений для каждого нового маршрута. Нет, мы определенно не хотим идти по этой грязной дороге.

Решение состоит в том, чтобы добавить систему маршрутизации в нашу инфраструктуру. Мы можем сделать это, создав метод map который связывает URI с обратным вызовом PHP, который будет выполнен, если будет найден правильный URI:

 class Core implements HttpKernelInterface { protected $routes = array(); public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) { $path = $request->getPathInfo(); // Does this URL match a route? if (array_key_exists($path, $this->routes)) { // execute the callback $controller = $routes[$path]; $response = $controller(); } else { // no route matched, this is a not found. $response = new Response('Not found!', Response::HTTP_NOT_FOUND); } return $response; } // Associates an URL with a callback function public function map($path, $controller) { $this->routes[$path] = $controller; } } 

Теперь маршруты приложений могут быть установлены непосредственно во фронт-контроллере:

 $app->map('/', function () { return new Response('This is the home page'); }); $app->map('/about', function () { return new Response('This is the about page'); }); $response = $app->handle($request); 

Эта крошечная система маршрутизации работает хорошо, но у нее есть серьезные недостатки: что если мы хотим сопоставить динамические URL-адреса, которые содержат параметры? Мы могли бы представить URL-адрес, например posts/:id где :id — это переменный параметр, который можно сопоставить с идентификатором записи в базе данных.

Нам нужна более гибкая и мощная система: поэтому мы будем использовать компонент Symfony Routing .

 php composer.phar require symfony/routing 2.5.* 

Использование компонента Routing позволяет нам загружать объекты Route в UrlMatcher , который сопоставит запрошенный URI с соответствующим маршрутом. Этот объект Route может содержать любые атрибуты, которые могут помочь нам выполнить правильную часть приложения. В нашем случае такой объект будет содержать обратный вызов PHP для выполнения, если маршрут совпадает. Кроме того, любые динамические параметры, содержащиеся в URL, будут присутствовать в атрибутах маршрута.

Чтобы реализовать это, нам нужно сделать следующие изменения:

  • Замените массив RouteCollection экземпляром RouteCollection для хранения наших маршрутов.
  • Измените метод map чтобы он регистрировал экземпляр Route в этой коллекции.
  • Создайте экземпляр UrlMatcher и сообщите ему, как сопоставить его маршруты с запрошенным URI, предоставив ему контекст с использованием экземпляра RequestContext .
 use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Exception\ResourceNotFoundException; class Core implements HttpKernelInterface { /** @var RouteCollection */ protected $routes; public function __construct() { $this->routes = new RouteCollection(); } public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) { // create a context using the current request $context = new RequestContext(); $context->fromRequest($request); $matcher = new UrlMatcher($this->routes, $context); try { $attributes = $matcher->match($request->getPathInfo()); $controller = $attributes['controller']; $response = $controller(); } catch (ResourceNotFoundException $e) { $response = new Response('Not found!', Response::HTTP_NOT_FOUND); } return $response; } public function map($path, $controller) { $this->routes->add($path, new Route( $path, array('controller' => $controller) )); } } 

Метод match пытается сопоставить URL с известным шаблоном маршрута и в случае успеха возвращает соответствующие атрибуты маршрута. В противном случае он создает исключение ResourceNotFoundException которое мы можем перехватить для отображения страницы 404.

Теперь мы можем воспользоваться компонентом маршрутизации для получения любых параметров URL. После избавления от атрибута controller мы можем вызвать нашу функцию обратного вызова, передав в качестве аргументов другие параметры (используя функцию call_user_func_array ):

 try { $attributes = $matcher->match($request->getPathInfo()); $controller = $attributes['controller']; unset($attributes['controller']); $response = call_user_func_array($controller, $attributes); } catch (ResourceNotFoundException $e) { $response = new Response('Not found!', Response::HTTP_NOT_FOUND); } return $response; } с try { $attributes = $matcher->match($request->getPathInfo()); $controller = $attributes['controller']; unset($attributes['controller']); $response = call_user_func_array($controller, $attributes); } catch (ResourceNotFoundException $e) { $response = new Response('Not found!', Response::HTTP_NOT_FOUND); } return $response; } 

Теперь мы можем легко обрабатывать динамические URL, как это:

 $app->map('/hello/{name}', function ($name) { return new Response('Hello '.$name); }); 

Обратите внимание, что это очень похоже на то, что делает среда полного стека Symfony: мы вводим параметры URL в правильный контроллер.

Зацепление в рамки

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

Все это возможно благодаря компоненту EventDispatcher , который позволяет различным компонентам приложения взаимодействовать, реализуя шаблон Observer .

 php composer.phar require symfony/event-dispatcher 2.5 

В основе этого лежит класс EventDispatcher, который регистрирует прослушиватели определенного события. Когда диспетчер уведомляется о событии, вызываются все известные слушатели этого события. Слушателем может быть любая допустимая функция или метод, вызываемый PHP.

Мы можем реализовать это в нашей среде, добавив dispatcher свойств, который будет содержать экземпляр EventDispatcher , и метод on , чтобы связать событие с обратным вызовом PHP. Мы будем использовать диспетчер для регистрации обратного вызова и для запуска события позже в рамках.

 use Symfony\Component\Routing\Matcher\UrlMatcher; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\Exception\ResourceNotFoundException; use Symfony\Component\EventDispatcher\EventDispatcher class Core implements HttpKernelInterface { /** @var RouteCollection */ protected $routes; public function __construct() { $this->routes = new RouteCollection(); $this->dispatcher = new EventDispatcher(); } // ... public function on($event, $callback) { $this->dispatcher->addListener($event, $callback); } } 

Теперь мы можем регистрировать слушателей, которые являются простыми обратными вызовами PHP. Давайте напишем метод fire который скажет нашему диспетчеру уведомить всех слушателей, которых он знает, когда происходит какое-то событие.

 public function fire($event) { return $this->dispatcher->dispatch($event); } 

Менее чем в десяти строках кода мы просто добавили в нашу среду хорошую систему прослушивания событий благодаря компоненту EventDispatcher.

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

Давайте напишем класс RequestEvent , который будет немедленно запущен при обработке запроса RequestEvent . Конечно, это событие должно иметь доступ к текущему запросу, используя атрибут, содержащий экземпляр Request .

 namespace Framework\Event; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\EventDispatcher\Event; class RequestEvent extends Event { protected $request; public function setRequest(Request $request) { $this->request = $request; } public function getRequest() { return $this->request; } } 

Теперь мы можем обновить код в методе handle чтобы RequestEvent событие RequestEvent диспетчеру при каждом получении запроса.

 public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = true) { $event = new RequestEvent(); $event->setRequest($request); $this->dispatcher->dispatch('request', $event); // ... } 

Таким образом, все вызываемые слушатели смогут получить доступ к объекту RequestEvent а также к текущему Request . На данный момент мы не написали такого слушателя, но мы могли легко представить себе тот, который проверял бы, имеет ли запрошенный URL-адрес ограниченный доступ, прежде чем что-либо еще произойдет.

 $app->on('request', function (RequestEvent $event) { // let's assume a proper check here if ('admin' == $event->getRequest()->getPathInfo()) { echo 'Access Denied!'; exit; } }); 

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

Вывод

Прочитав это руководство, вы увидели, что компоненты Symfony являются отличными автономными библиотеками. Более того, они могут взаимодействовать друг с другом, создавая структуру, соответствующую вашим потребностям. Есть много других, которые действительно интересны, например, компонент DependencyInjection или компонент Security .

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