Микрофреймворк Slim
KOD25 апреля 2019 года свет увидела новая мажорная alpha
-версия микрофреймворка Slim, а 18 мая она выросла до beta
.
Как создать приложение?
<?php use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Slim\Factory\AppFactory; require 'vendor/autoload.php'; $app = AppFactory::create(); $app->get('/hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response) { $name = $request->getAttribute('name'); $response->getBody()->write("Hello, $name"); return $response; }); $app->run();
Верните мне 404 ошибку!
Если мы попробуем открыть несуществующую страницу, получим код ответа 500
, а не 404
. Чтобы ошибки обрабатывались корректно, нужно подключить \Slim\Middleware\ErrorMiddleware
<?php use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Slim\Factory\AppFactory; use Slim\Middleware\ErrorMiddleware; require 'vendor/autoload.php'; $app = AppFactory::create(); $middleware = new ErrorMiddleware( $app->getCallableResolver(), $app->getResponseFactory(), false, false, false ); $app->add($middleware); $app->get('/hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response) { $name = $request->getAttribute('name'); $response->getBody()->write("Hello, $name"); return $response; }); $app->run();
Middleware
Промежуточное ПО должно быть реализацией PSR-15. В качестве исключения, можно передавать функции, но сигнатура должна соответствовать методу process()
интерфейса \Psr\Http\Server\MiddlewareInterface
<?php use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Server\RequestHandlerInterface; use Slim\Factory\AppFactory; require 'vendor/autoload.php'; $app = AppFactory::create(); $app->add(function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { $response = $handler->handle($request); return $response->withHeader('Content-Type', 'application/json'); }); // ... Описание роутов и прочее $app->run();
Сигнатура ($request, $response, $next)
больше не поддерживается
Создание приложения на Slim-4
Чтобы более подробно рассмотреть фреймворк, напишем небольшое приложение.
Приложение будет иметь следующие роуты:
/hello/{name}
— страница приветствия;/
— редирект на страницу/hello/world
- Остальные роуты будут возвращать кастомизированную страницу с 404 ошибкой.
Логика будет в контроллерах, рендерить страницу будем через шаблонизатор Twig
Бонусом добавим консольное приложение на базе компонента Symfony Console с командой, отображающей список роутов
Шаг 0. Установка зависимостей
Нам понадобится:
- микрофреймворк, slim/slim;
- реализация интерфейса контейнера (PSR-11) psr/container;
- реализация интерфейсов http-сообщений (PSR-7) psr/http-message;
- реализация интерфейсов фабрик http-сообщений (PSR-17) psr/http-factory;
- шаблонизатор twig/twig;
- консольное приложение symfony/console.
В качестве контенера зависимостей я выбрал ultra-lite/container, как легкий, лаконичный и соответствующий стандарту.
PSR-7 и PSR-17 разработчики Slim предоставляют в одном пакете slim/psr7. Им и воспользуемся
Предполагается, что пакетный менеджер Composer уже установлен.
Создаём папку под проект (в качестве примера будет использоваться /path/to/project
) и переходим в неё.
Добавим в проект файл composer.json
со следующим содержимым:
{ "require": { "php": ">=7.1", "slim/slim": "4.0.0-beta", "slim/psr7": "~0.3", "ultra-lite/container": "^6.2", "symfony/console": "^4.2", "twig/twig": "^2.10" }, "autoload": { "psr-4": { "App\\": "app" } } }
и выполним команду
composer install
Теперь у нас есть все необходимые пакеты и настроен автозагрузчик классов.
Если работаем с git, добавим файл .gitignore
и внесем туда директорию vendor
(и диреткорию своей IDE при необходимости)
/.idea/* /vendor/*
Для комфортной разработки самое время подружить контейнер и IDE.
В корне проекта создадим файл .phpstorm.meta.php
и напишем там такой код:
<?php // .phpstorm.meta.php namespace PHPSTORM_META { override( \Psr\Container\ContainerInterface::get(0), map([ '' => '@', ]) ); }
Этот код подскажет IDE, что у объекта, реализующего интерфейс \Psr\Container\ContainerInterface
, метод get()
вернёт объект класса или реализацию интерфейса, имя которого передано в параметре.
Шаг 1. Каркас приложения
Добавим каталоги:
app
— код приложения. К нему мы подключим наше пространство имен для автозагрузчика классов;bin
— директория для консольной утилиты;config
— здесь будут файлы конфигурации приложения;public
— директория, открытая в веб (точка входа приложения, стили, js, картинки и т.д.);template
— директория шаблонов;var
— директория для различных файлов. Логи, кэш, локальное хранилище и т.д.
И файлы:
config/app.ini
— основной конфиг приложения;config/app.local.ini
— конфиг для окруженияlocal
;app/Support/CommandMap.php
— маппинг команд консольного приложения для ленивой загрузки.app/Support/Config.php
— Класс конфигурации (Чтобы IDE знала, какие конфиги у нас имеются).app/Support/NotFoundHandler.php
— Класс обработчика 404й ошибки.app/Support/ServiceProviderInterface.php
— Интерфейс сервис-провайдера.app/Provider/AppProvider.php
— Основной провайдер приложения.bootstrap.php
— сборка контейнера;bin/console
— точка входа консольного приложения;public/index.php
— точка входа веб приложения.
Проверка.
Запустим консольное приложение:
./bin/console
В ответ должно отобразиться окно приветсвия компонета symfony/console
с двумя доступными командами — help
и list
.
Console Tool Usage: command [options] [arguments] Options: -h, --help Display this help message -q, --quiet Do not output any message -V, --version Display this application version --ansi Force ANSI output --no-ansi Disable ANSI output -n, --no-interaction Do not ask any interactive question -v|vv|vvv, --verbose Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug Available commands: help Displays help for a command list Lists commands
Теперь запустим веб-сервер.
php -S localhost:8080 -t public public/index.php
И откроем любой урл на localhost:8080.
Все запросы должны возвращать ответ с кодом 404
и пустым телом.
Это происходит, потому что у нас не указан ни один маршрут.
Нам осталось подключить рендер, написать шаблоны, контроллеры и задать маршруты.
Шаг 2. Рендер
Добавим шаблон template/layout.twig
. Это базовый шаблон для всех страниц
Добавим шаблон template/layout.twig
. Это базовый шаблон для всех страниц
template/layout.twig
{# template/layout.twig #} <!DOCTYPE html> <html lang="en"> <head> <title>{% block title %}Slim demo{% endblock %}</title> </head> <body> {% block content %}{% endblock %} </body> </html>
Добавим шаблон страницы приветствия template/hello.twig
template/hello.twig
{# template/hello.twig #} {% extends 'layout.twig' %} {% block title %}Slim demo::hello, {{ name }}{% endblock %} {% block content %} <h1>Welcome!</h1> <p>Hello, {{ name }}!</p> {% endblock %}
И шаблон страницы ошибки template/err404.twig
template/err404.twig
{# template/err404.twig #} {% extends 'layout.twig' %} {% block title %}Slim demo::not found{% endblock %} {% block content %} <h1>Error!</h1> <p>Page not found =(</p> {% endblock %}
Добавим провайдер рендеринга app/Provider/RenderProvider.php
app/Provider/RenderProvider.php
<?php // app/Provider/RenderProvider.php namespace App\Provider; use App\Support\Config; use App\Support\ServiceProviderInterface; use Psr\Container\ContainerInterface; use Twig\Environment; use Twig\Loader\FilesystemLoader; use UltraLite\Container\Container; class RenderProvider implements ServiceProviderInterface { public function register(Container $container) { $container->set(Environment::class, function (ContainerInterface $container) { $config = $container->get(Config::class); $loader = new FilesystemLoader($config->get('templates.dir')); $cache = $config->get('templates.cache'); $options = [ 'cache' => empty($cache) ? false : $cache, ]; $twig = new Environment($loader, $options); return $twig; }); } }
Включим провайдер в бутстрап
bootstrap.php
<?php // bootstrap.php // ... use App\Provider\RenderProvider; // ... $providers = [ // ... RenderProvider::class, // ... ]; // ...
Добавим рендер в обработчик 404 ошибки
app/Support/NotFoundHandler.php (DIFF)
--- a/app/Support/NotFoundHandler.php +++ b/app/Support/NotFoundHandler.php @@ -8,15 +8,22 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\Interfaces\ErrorHandlerInterface; use Throwable; +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Error\RuntimeError; +use Twig\Error\SyntaxError; class NotFoundHandler implements ErrorHandlerInterface { private $factory; - public function __construct(ResponseFactoryInterface $factory) + private $render; + + public function __construct(ResponseFactoryInterface $factory, Environment $render) { $this->factory = $factory; + $this->render = $render; } /** @@ -26,10 +33,14 @@ class NotFoundHandler implements ErrorHandlerInterface * @param bool $logErrors * @param bool $logErrorDetails * @return ResponseInterface + * @throws LoaderError + * @throws RuntimeError + * @throws SyntaxError */ public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails): ResponseInterface { $response = $this->factory->createResponse(404); + $response->getBody()->write($this->render->render('err404.twig')); return $response; } }
app/Provider/AppProvider.php (DIFF)
--- a/app/Provider/AppProvider.php +++ b/app/Provider/AppProvider.php @@ -19,6 +19,7 @@ use Slim\Middleware\RoutingMiddleware; use Slim\Psr7\Factory\ResponseFactory; use Slim\Routing\RouteCollector; use Slim\Routing\RouteResolver; +use Twig\Environment; use UltraLite\Container\Container; class AppProvider implements ServiceProviderInterface @@ -99,7 +100,7 @@ class AppProvider implements ServiceProviderInterface * Регистрируем обработчика ошибки 404 */ $container->set(NotFoundHandler::class, function (ContainerInterface $container) { - return new NotFoundHandler($container->get(ResponseFactoryInterface::class)); + return new NotFoundHandler($container->get(ResponseFactoryInterface::class), $container->get(Environment::class)); }); /*
Теперь наша 404 ошибка приобрела тело.
Шаг 3. Контроллеры
Теперь можно браться за контроллеры
У нас их будет 2:
app/Controller/HomeController.php
— главная страницаapp/Controller/HelloController.php
— страница приветствия
Контроллеру главной страницы из зависимостей необходим роутер (для построения URL редиректа), а контроллеру страницы приветствия — рендер (для рендегинга html)
app/Controller/HomeController.php
<?php // app/Controller/HomeController.php namespace App\Controller; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\Interfaces\RouteParserInterface; class HomeController { /** * @var RouteParserInterface */ private $router; public function __construct(RouteParserInterface $router) { $this->router = $router; } public function index(ServerRequestInterface $request, ResponseInterface $response) { $uri = $this->router->fullUrlFor($request->getUri(), 'hello', ['name' => 'world']); return $response ->withStatus(301) ->withHeader('location', $uri); } }
app/Controller/HelloController.php
<?php // app/Controller/HelloController.php namespace App\Controller; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Twig\Environment as Render; use Twig\Error\LoaderError; use Twig\Error\RuntimeError; use Twig\Error\SyntaxError; class HelloController { /** * @var Render */ private $render; public function __construct(Render $render) { $this->render = $render; } /** * @param ServerRequestInterface $request * @param ResponseInterface $response * @return ResponseInterface * @throws LoaderError * @throws RuntimeError * @throws SyntaxError */ public function show(ServerRequestInterface $request, ResponseInterface $response) { $response->getBody()->write($this->render->render('hello.twig', ['name' => $request->getAttribute('name')])); return $response; } }
Добавим провайдер, регистрирующий контроллеры
app/Provider/WebProvider.php
<?php // app/Provider/WebProvider.php namespace App\Provider; use App\Controller\HelloController; use App\Controller\HomeController; use App\Support\ServiceProviderInterface; use Psr\Container\ContainerInterface; use Slim\Interfaces\RouteCollectorInterface; use Slim\Interfaces\RouteCollectorProxyInterface; use Twig\Environment; use UltraLite\Container\Container; class WebProvider implements ServiceProviderInterface { public function register(Container $container) { /* * Зарегистрируем контроллеры */ $container->set(HomeController::class, function (ContainerInterface $container) { return new HomeController($container->get(RouteCollectorInterface::class)->getRouteParser()); }); $container->set(HelloController::class, function (ContainerInterface $container) { return new HelloController($container->get(Environment::class)); }); /* * Зарегистрируем маршруты */ $router = $container->get(RouteCollectorInterface::class); $router->group('/', function(RouteCollectorProxyInterface $router) { $router->get('', HomeController::class . ':index')->setName('index'); $router->get('hello/{name}', HelloController::class . ':show')->setName('hello'); }); } }
Не забудем добавить провайдер в бутстрап
bootstrap.php
<?php // bootstrap.php // ... use App\Provider\WebProvider; // ... $providers = [ // ... WebProvider::class, // ... ]; // ...
Мы можем запустить веб-сервер (если останавливали)...
php -S localhost:8080 -t public public/index.php
… открыть в браузере http://localhost:8080 и увидеть, что браузер нас перенаправил на http://localhost:8080/hello/world
Мы видим теперь приветствие world'а.
Мы можем открыть http://localhost:8080/hello/ivan и бразуер поприветствует ivan'а.
Несуществующая страница, например, http://localhost:8080/helo/world отображает наш кастомный текст и отдаёт 404 статус.
Шаг 4. Консольная команда
Напишем команду route:list
app/Command/RouteListCommand.php
<?php // app/Command/RouteListCommand.php namespace App\Command; use Slim\Interfaces\RouteCollectorInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; class RouteListCommand extends Command { /* * Имя вынесено в константу, чтобы было меньше ошибок при маппинге команд */ const NAME = 'route:list'; /** * @var RouteCollectorInterface */ private $router; public function __construct(RouteCollectorInterface $router) { $this->router = $router; parent::__construct(); } protected function configure() { $this->setName(self::NAME) ->setDescription('List of routes.') ->setHelp('List of routes.') ; } protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $io->title('Routes'); $rows = []; $routes = $this->router->getRoutes(); if (!$routes) { $io->text('Routes list is empty'); return 0; } foreach ($routes as $route) { $rows[] = [ 'path' => $route->getPattern(), 'methods' => implode(', ', $route->getMethods()), 'name' => $route->getName(), 'handler' => $route->getCallable(), ]; } $io->table( ['Route', 'Methods', 'Name', 'Handler'], $rows ); return 0; } }
Теперь нужен провайдер, который зарегистрирует команду в контейнере и добавит её в маппинг
app/Provider/CommandProvider.php
<?php // app/Provider/CommandProvider.php namespace App\Provider; use App\Command\RouteListCommand; use App\Support\CommandMap; use App\Support\ServiceProviderInterface; use Psr\Container\ContainerInterface; use Slim\Interfaces\RouteCollectorInterface; use UltraLite\Container\Container; class CommandProvider implements ServiceProviderInterface { public function register(Container $container) { /* * Добавим команду списка маршрутов в контейнер */ $container->set(RouteListCommand::class, function (ContainerInterface $container) { return new RouteListCommand($container->get(RouteCollectorInterface::class)); }); /* * Добавим команду списка маршрутов в маппинг команд */ $container->get(CommandMap::class)->set(RouteListCommand::NAME, RouteListCommand::class); } }
Помним про бутстрап
bootstrap.php
<?php // bootstrap.php // ... use App\Provider\CommandProvider; // ... $providers = [ // ... CommandProvider::class, // ... ]; // ...
Теперь мы можем ввести команду...
./bin/console route:list
… и увидеть список роутов:
Routes ====== --------------- --------- ------- ------------------------------------- Route Methods Name Handler --------------- --------- ------- ------------------------------------- / GET index App\Controller\HomeController:index /hello/{name} GET hello App\Controller\HelloController:show --------------- --------- ------- -------------------------------------
Заключение
Slim — это не обязательно вся логика приложения в файле routes.php
(как во многочисленных примерах), на нём можно писать качественные и поддерживаемые приложения. Главное — не испугаться в начале пути, когда контейнер пуст, а зависимотсти нужны.