Pull to refresh

Создаем собственный фреймворк на основе Symfony2. (Часть 4)

Reading time5 min
Views6.3K
Original author: Fabien Potencier


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

<?php
 
// example.com/web/front.php
 
require_once __DIR__.'/../src/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
 
$request = Request::createFromGlobals();
 
$map = array(
    '/hello' => 'hello',
    '/bye'   => 'bye',
);
 
$path = $request->getPathInfo();
if (isset($map[$path])) {
    ob_start();
    extract($request->query->all(), EXTR_SKIP);
    include sprintf(__DIR__.'/../src/pages/%s.php', $map[$path]);
    $response = new Response(ob_get_clean());
} else {
    $response = new Response('Not Found', 404);
}
 
$response->send();


Так как мы импортировали переменные из массива запросов в текущую таблицу символов, упростим шаблон hello.php следующим образом:

<!-- example.com/src/pages/hello.php -->
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> 


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

# Before
/hello?name=Fabien
 
# After
/hello/Fabien


Для поддержки этой функции, мы будем использовать компонент Symfony2 – маршрутизацию (Symfony2 «Routing»component). Как всегда, добавьте компонент в composer.json и запустить команду php composer.phar update для его установки:

{
    "require": {
        "symfony/class-loader": "2.1.*",
        "symfony/http-foundation": "2.1.*",
        "symfony/routing": "2.1.*"
    }
}


С этого момента, мы будем использовать сгенерированный Composer’ом автозагрузчик вместо нашего autoload.php. Удалите файл autoload.php и измените ссылку на него в front.php:

<?php
 
// example.com/web/front.php
 
require_once __DIR__.'/../vendor/.composer/autoload.php';
 
// ...


Компонент маршрутизации использует экземпляр класса «RouteCollection», вместо массива соответствий (карты ссылок).

use Symfony\Component\Routing\RouteCollection;
 
$routes = new RouteCollection();


Давайте добавим маршрут для ссылки вида /hello/SOMETHING и еще один для простой /bye:

use Symfony\Component\Routing\Route;

$routes->add('hello', new Route('/hello/{name}', array('name' => 'World')));
$routes->add('bye', new Route('/bye'));


Каждый элемент коллекции определяется именем (hello) и экземпляром класса «Route», который в свою очередь определяется паттерном роута (/hello/{name}) и массивом дефолтных атрибутов (array('name' => 'World')).
В официальной документации по данному компоненту вы найдете информацию об остальных его функциях, таких как генерация URL, требования к атрибутам, соблюдение HTTP методов, загрузчик YAML или XML файлов, экспорт rewrite rules в PHP или Apache для повышения производительности, и многое другое.
Основываясь на информации, находящейся в экземпляре класса «RouteCollection», экземпляр класса «UrlMatcher» свяжет пути:

use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Matcher\UrlMatcher;
 
$context = new RequestContext();
$context->fromRequest($request);
$matcher = new UrlMatcher($routes, $context);
 
$attributes = $matcher->match($request->getPathInfo());


Метод match() разделяет запрашиваемый путь на массив (обратите внимание, что совпадающие пути автоматически сохраняются под специальным ключом _route ):

print_r($matcher->match('/bye'));
array (
  '_route' => 'bye',
);
 
print_r($matcher->match('/hello/Fabien'));
array (
  'name' => 'Fabien',
  '_route' => 'hello',
);
 
print_r($matcher->match('/hello'));
array (
  'name' => 'World',
  '_route' => 'hello',
);


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

$matcher->match('/not-found');
 
// throws a Symfony\Component\Routing\Exception\ResourceNotFoundException


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

<?php
 
// example.com/web/front.php
 
require_once __DIR__.'/../vendor/.composer/autoload.php';
 
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing;
 
$request = Request::createFromGlobals();
$routes = include __DIR__.'/../src/app.php';
 
$context = new Routing\RequestContext();
$context->fromRequest($request);
$matcher = new Routing\Matcher\UrlMatcher($routes, $context);
 
try {
    extract($matcher->match($request->getPathInfo()), EXTR_SKIP);
    ob_start();
    include sprintf(__DIR__.'/../src/pages/%s.php', $_route);
 
    $response = new Response(ob_get_clean());
} catch (Routing\Exception\ResourceNotFoundException $e) {
    $response = new Response('Not Found', 404);
} catch (Exception $e) {
    $response = new Response('An error occurred', 500);
}
 
$response->send();


Новые изменения в коде:
• Имена путей используются для названий шаблонов
• Ошибка 500 обрабатывается корректно
• Параметры запроса отделены для сохранения простоты шаблонов
<!-- example.com/src/pages/hello.php -->
 
Hello <?php echo htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?>

• Конфигурация маршрутов была перемещена в отдельный файл:

<?php
 
// example.com/src/app.php
 
use Symfony\Component\Routing;
 
$routes = new Routing\RouteCollection();
$routes->add('hello', new Routing\Route('/hello/{name}', array('name' => 'World')));
$routes->add('bye', new Routing\Route('/bye'));
 
return $routes;


Мы полностью отделили конфигурацию (все, характеристики нашего приложения — в app.php) и фреймворк (остальной код на котором основано наше приложение – в front.php).
Добавив около 30 строк кода, мы получили новый фреймворк – более производительный, более гибкий, нежели предыдущий. Наслаждайтесь!
У использования компонента маршрутизации есть еще одно большое преимущество: способность генерировать URL-адреса на основе Route определений. При одновременном использовании URL matching и URL generation в вашем коде, при изменении вида URL не должно быть никаких последствий. Хотите узнать, как использовать генератор? Безумно просто:

use Symfony\Component\Routing;
 
$generator = new Routing\Generator\UrlGenerator($routes, $context);
 
echo $generator->generate('hello', array('name' => 'Fabien'));
// outputs /hello/Fabien


Код абсолютно понятен, так же мы можем генерировать и абсолютные пути добавив третий параметр true:

echo $generator->generate('hello', array('name' => 'Fabien'), true);
// outputs something like http://example.com/somewhere/hello/Fabien


Обеспокоены производительностью? Используя ваши определения маршрутов, создадим оптимизированный класс совпадений URL, который заменит дефолтный «UrlMatcher»:

$dumper = new Routing\Matcher\Dumper\PhpMatcherDumper($routes);
 
echo $dumper->dump();


Хотите еще большей производительности? Экспортируйте ваши маршруты как mod_rewrite правила для Apache:

$dumper = new Routing\Matcher\Dumper\ApacheMatcherDumper($routes);
 
echo $dumper->dump();


Мы с пользователем fozzy условились переводить данный цикл вместе.
Tags:
Hubs:
Total votes 14: ↑9 and ↓5+4
Comments3

Articles