25 апреля 2019 года свет увидела новая мажорная alpha-версия микрофреймворка Slim, а 18 мая она выросла до beta. Предлагаю по этому поводу ознакомиться с новой версией.
Под катом:
- О новшествах фреймворка
- Написание простого приложения на Slim-4
- О дружбе Slim и PhpStorm
Новое в Slim 4
Основные нововведения по сравнению с версией 3:
- Минимальная версия PHP — 7.1;
- Поддержка PSR-15 (Middleware);
- Удалена реализация http-сообщений. Устанавливаем любую PSR-7 совместимую библиотеку и пользуемся;
- Удалена зависимость Pimple. Устанавливаем свой любимый PSR-11 совместимый контейнер и пользуемся;
- Возможность использования своего роутера (Раньше не было возможности отказаться от FastRoute);
- Изменена реализация обработки ошибок;
- Изменена реализация вывода ответа;
- Добавлена фабрика для создания экземпляра приложения;
- Удалены настройки;
- Slim больше не устанавливает
default_mimetypeв пустую строку, поэтому нужно установить его самостоятельно вphp.iniили в вашем приложении, используяini_set('default_mimetype', ''); - Обработчик запроса приложения теперь принимает только объект запроса (в старой версии принимал объекты запроса и ответа).
Как теперь создать приложение?
В третьей версии создание приложения выглядело примерно так:
<?php use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use Slim\App; require 'vendor/autoload.php'; $settings = [ 'addContentLengthHeader' => false, ]; $app = new App(['settings' => $settings]); $app->get('/hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response, array $args) { $name = $args['name']; $response->getBody()->write("Hello, $name"); return $response; }); $app->run();
Теперь конструктор приложения принимает следующие параметры:
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
| $responseFactory | Psr\Http\Message\ResponseFactoryInterface |
да | PSR-17 совместимая фабрика серверного http-запроса |
| $container | \Psr\Container\ContainerInterface |
нет | Контейнер зависимостей |
| $callableResolver | \Slim\Interfaces\CallableResolverInterface |
нет | Обработчик вызываемых методов |
| $routeCollector | \Slim\Interfaces\RouteCollectorInterface |
нет | Роутер |
| $routeResolver | \Slim\Interfaces\RouteResolverInterface |
нет | Обработчик результатов роутинга |
Так же теперь можно воспользоваться статическим методом create фабрики приложения \Slim\Factory\AppFactory.
Этот метод принимает на вход такие же параметры, только все они опциональные.
<?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) больше не поддерживается
Как жить без настроек?
Без настроек жить можно. Предоставленные инструменты нам в этом помогут.
httpVersion и responseChunkSize
Настройка httpVersion отвечала за вывод версии протокола в ответе.
Настройка responseChunkSize определяла размер каждого чанка, читаемого из тела ответа при отправке в браузер.
Сейчас эти функции можно возложить на эмиттер ответа.
Пишем эмиттер
<?php // /src/ResponseEmitter.php namespace App; use Psr\Http\Message\ResponseInterface; use Slim\ResponseEmitter as SlimResponseEmitter; class ResponseEmitter extends SlimResponseEmitter { private $protocolVersion; public function __construct(string $protocolVersion = '1.1', int $responseChunkSize = 4096) { $this->protocolVersion = $protocolVersion; parent::__construct($responseChunkSize); } public function emit(ResponseInterface $response) : void{ parent::emit($response->withProtocolVersion($this->protocolVersion)); } }
Подключаем к приложению
<?php use App\ResponseEmitter; use Slim\Factory\AppFactory; require 'vendor/autoload.php'; $app = AppFactory::create(); $serverRequestFactory = \Slim\Factory\ServerRequestCreatorFactory::create(); $request = $serverRequestFactory->createServerRequestFromGlobals(); // ... Описание роутов и прочее $response = $app->handle($request); $emitter = new ResponseEmitter('2.0', 4096); $emitter->emit($response);
outputBuffering
Данная настройка позволяла включать/выключать буфферизацию вывода. Значения настройки:
false— буфферизация выключена (все вызовы операторовecho,printигнорируются).'append'— все вызовы операторовecho,printдобавляются после тела ответа'prepend'— все вызовы операторовecho,printдобавляются перед телом ответа
Разработчики фреймворка предлагают заменить эту опцию промежуточным ПО \Slim\Middleware\OutputBufferingMiddleware, в конструктор которого передается PSR-17 совместимая фабрика потока и режим, который может быть равен append или prepend
<?php use Slim\Factory\AppFactory; use Slim\Factory\Psr17\SlimPsr17Factory; use Slim\Middleware\OutputBufferingMiddleware; require 'vendor/autoload.php'; $app = AppFactory::create(); $middleware = new OutputBufferingMiddleware(SlimPsr17Factory::getStreamFactory(), OutputBufferingMiddleware::APPEND); $app->add($middleware); // ... Описание роутов и прочее $app->run();
determineRouteBeforeAppMiddleware
Эта настройка позволяла получить текущий маршрут из объекта запроса в промежуточном ПО
На замену предоставляется \Slim\Middleware\RoutingMiddleware
<?php use Slim\Factory\AppFactory; use Slim\Middleware\RoutingMiddleware; require 'vendor/autoload.php'; $app = AppFactory::create(); $middleware = new RoutingMiddleware($app->getRouteResolver()); $app->add($middleware); // ... Описание роутов и прочее $app->run();
displayErrorDetails
Настройка позволяла выводить подробности ошибок. При отладке это неплохо упрощает жизнь.
Помните \Slim\Middleware\ErrorMiddleware? Оно и здесь нас выручит!
<?php use Slim\Factory\AppFactory; use Slim\Middleware\ErrorMiddleware; require 'vendor/autoload.php'; $app = AppFactory::create(); $middleware = new ErrorMiddleware( $app->getCallableResolver(), $app->getResponseFactory(), true, // Этот параметр отвечает за подробный вывод ошибок false, // Логирование ошибок false // Логирование подробностей ошибок ); $app->add($middleware); // ... Описание роутов и прочее $app->run();
addContentLengthHeader
Данная настройка позволяла включать/отключать автодобавление заголовка Content-Length со значением объема данных в теле ответа
Заменяет опцию промежуточное ПО \Slim\Middleware\ContentLengthMiddleware
<?php use Slim\Factory\AppFactory; use Slim\Middleware\ContentLengthMiddleware; require 'vendor/autoload.php'; $app = AppFactory::create(); $middleware = new ContentLengthMiddleware(); $app->add($middleware); // ... Описание роутов и прочее $app->run();
routerCacheFile
Теперь вы можете напрямую установить файл кеша роутера
<?php use Slim\Factory\AppFactory; require 'vendor/autoload.php'; $app = AppFactory::create(); $app->getRouteCollector()->setCacheFile('/path/to/cache/router.php'); // ... Описание роутов и прочее $app->run();
Создание приложения на 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 и горжусь этим. Для комфортной разработки самое время подружить контейнер и 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— точка входа веб приложения.
; режим отладки slim.debug=Off ; директория шаблонов templates.dir=template ; кэш шаблонов templates.cache=var/cache/template
; В этом файле мы только переопределим некоторые параметры. Остальные значения подставятся из основного конфига ; В локальном окружении полезно видеть подробности ошибок slim.debug=On ; кэш шаблонов для разработки не ��ужен templates.cache=
Ах да, ещё неплохо исключить конфиги окружения из репозитория. Ведь там могут быть явки/пароли. Кэш тоже исключим.
/.idea/* /config/* /vendor/* /var/cache/* !/config/app.ini !/var/cache/.gitkeep
<?php // app/Support/CommandMap.php namespace App\Support; class CommandMap { /** * Маппинг команд. Имя команды => Ключ в контейнере * @var string[] */ private $map = []; public function set(string $name, string $value) { $this->map[$name] = $value; } /** * @return string[] */ public function getMap() { return $this->map; } }
<?php // app/Support/Config.php namespace App\Support; class Config { /** * @var string[] */ private $config = []; public function __construct(string $dir, string $env, string $root) { if (!is_dir($dir)) return; /* * Парсим основной конфиг */ $config = (array)parse_ini_file($dir . DIRECTORY_SEPARATOR . 'app.ini', false); /* * Переопределяем параметры из конфига окружения */ $environmentConfigFile = $dir . DIRECTORY_SEPARATOR . 'app.' . $env . '.ini'; if (is_readable($environmentConfigFile)) { $config = array_replace_recursive($config, (array)parse_ini_file($environmentConfigFile, false)); } /* * Указываем, какие параметры конфига являются путями */ $dirs = ['templates.dir', 'templates.cache']; foreach ($config as $name=>$value) { $this->config[$name] = $value; } /* * Устанавливаем абсолютные пути в конфигурации */ foreach ($dirs as $parameter) { $value = $config[$parameter]; if (mb_strpos($value, '/') === 0) { continue; } if (empty($value)) { $this->config[$parameter] = null; continue; } $this->config[$parameter] = $root . DIRECTORY_SEPARATOR . $value; } } public function get(string $name) { return array_key_exists($name, $this->config) ? $this->config[$name] : null; } }
<?php // app/Support/NotFoundHandler.php namespace App\Support; use Psr\Http\Message\ResponseFactoryInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use Slim\Interfaces\ErrorHandlerInterface; use Throwable; class NotFoundHandler implements ErrorHandlerInterface { private $factory; public function __construct(ResponseFactoryInterface $factory) { $this->factory = $factory; } /** * @param ServerRequestInterface $request * @param Throwable $exception * @param bool $displayErrorDetails * @param bool $logErrors * @param bool $logErrorDetails * @return ResponseInterface */ public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails): ResponseInterface { $response = $this->factory->createResponse(404); return $response; } }
Теперь можно научить PhpStorm понимать, какие у конфига есть ключи и какого они типа
<?php // .phpstorm.meta.php namespace PHPSTORM_META { override( \Psr\Container\ContainerInterface::get(0), map([ '' => '@', ]) ); override( \App\Support\Config::get(0), map([ 'slim.debug' => 'bool', 'templates.dir' => 'string|false', 'templates.cache' => 'string|false', ]) ); }
<?php // app/Support/ServiceProviderInterface.php namespace App\Support; use UltraLite\Container\Container; interface ServiceProviderInterface { public function register(Container $container); }
<?php // app/Provider/AppProvider.php namespace App\Provider; use App\Support\CommandMap; use App\Support\Config; use App\Support\NotFoundHandler; use App\Support\ServiceProviderInterface; use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; use Slim\CallableResolver; use Slim\Exception\HttpNotFoundException; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\RouteCollectorInterface; use Slim\Interfaces\RouteResolverInterface; use Slim\Middleware\ErrorMiddleware; use Slim\Middleware\RoutingMiddleware; use Slim\Psr7\Factory\ResponseFactory; use Slim\Routing\RouteCollector; use Slim\Routing\RouteResolver; use UltraLite\Container\Container; class AppProvider implements ServiceProviderInterface { public function register(Container $container) { /* * Регистрируем маппинг консольных команд */ $container->set(CommandMap::class, function () { return new CommandMap(); }); /* * Регистрируем фабрику http-запроса */ $container->set(ResponseFactory::class, function () { return new ResponseFactory(); }); /* * Связываем интерфейс фабрики http-запроса с реализацией */ $container->set(ResponseFactoryInterface::class, function (ContainerInterface $container) { return $container->get(ResponseFactory::class); }); /* * Регистрируем обработчик вызываемых методов */ $container->set(CallableResolver::class, function (ContainerInterface $container) { return new CallableResolver($container); }); /* * Связываем интерфейс обработчика вызываемых методов с реализацией */ $container->set(CallableResolverInterface::class, function (ContainerInterface $container) { return $container->get(CallableResolver::class); }); /* * Регистрируем роутер */ $container->set(RouteCollector::class, function (ContainerInterface $container) { $router = new RouteCollector( $container->get(ResponseFactoryInterface::class), $container->get(CallableResolverInterface::class), $container ); return $router; }); /* * Связываем интерфес роутера с реализацией */ $container->set(RouteCollectorInterface::class, function (ContainerInterface $container) { return $container->get(RouteCollector::class); }); /* * Регистрируем обработчик результатов роутера */ $container->set(RouteResolver::class, function (ContainerInterface $container) { return new RouteResolver($container->get(RouteCollectorInterface::class)); }); /* * Связываем интерфес обработчика результатов роутера с реализацией */ $container->set(RouteResolverInterface::class, function (ContainerInterface $container) { return $container->get(RouteResolver::class); }); /* * Регистрируем обработчика ошибки 404 */ $container->set(NotFoundHandler::class, function (ContainerInterface $container) { return new NotFoundHandler($container->get(ResponseFactoryInterface::class)); }); /* * Регистрируем middleware обработки ошибок */ $container->set(ErrorMiddleware::class, function (ContainerInterface $container) { $middleware = new ErrorMiddleware( $container->get(CallableResolverInterface::class), $container->get(ResponseFactoryInterface::class), $container->get(Config::class)->get('slim.debug'), true, true); $middleware->setErrorHandler(HttpNotFoundException::class, $container->get(NotFoundHandler::class)); return $middleware; }); /* * Регистрируем middleware роутера */ $container->set(RoutingMiddleware::class, function (ContainerInterface $container) { return new RoutingMiddleware($container->get(RouteResolverInterface::class)); }); } }
Мы вынесли роутинг в контейнер для того, чтобы можно было с ним работать без инициализации объекта \Slim\App.
<?php // bootstrap.php require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php'; use App\Support\ServiceProviderInterface; use App\Provider\AppProvider; use App\Support\Config; use UltraLite\Container\Container; /* * Определяем окружение */ $env = getenv('APP_ENV'); if (!$env) $env = 'local'; /* * Строим конфиг */ $config = new Config(__DIR__ . DIRECTORY_SEPARATOR . 'config', $env, __DIR__); /* * Определяем сервис-провайдеры */ $providers = [ AppProvider::class, ]; /* * Создаем экземпляр контейнера */ $container = new Container([ Config::class => function () use ($config) { return $config;}, ]); /* * Регистрируем сервисы */ foreach ($providers as $className) { if (!class_exists($className)) { /** @noinspection PhpUnhandledExceptionInspection */ throw new Exception('Provider ' . $className . ' not found'); } $provider = new $className; if (!$provider instanceof ServiceProviderInterface) { /** @noinspection PhpUnhandledExceptionInspection */ throw new Exception($className . ' has not provider'); } $provider->register($container); } /* * Возвращаем контейнер */ return $container;
#!/usr/bin/env php <?php // bin/console use App\Support\CommandMap; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; use Symfony\Component\Console\Input\ArgvInput; use Symfony\Component\Console\Output\ConsoleOutput; /** @var ContainerInterface $container */ $container = require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'bootstrap.php'; $loader = new ContainerCommandLoader($container, $container->get(CommandMap::class)->getMap()); $app = new Application(); $app->setCommandLoader($loader); /** @noinspection PhpUnhandledExceptionInspection */ $app->run(new ArgvInput(), new ConsoleOutput());
Желательно дать этому файлу права на выполнение
chmod +x ./bin/console
<?php // public/index.php use Psr\Container\ContainerInterface; use Psr\Http\Message\ResponseFactoryInterface; use Slim\App; use Slim\Interfaces\CallableResolverInterface; use Slim\Interfaces\RouteCollectorInterface; use Slim\Interfaces\RouteResolverInterface; use Slim\Middleware\ErrorMiddleware; use Slim\Middleware\RoutingMiddleware; use Slim\Psr7\Factory\ServerRequestFactory; /** @var ContainerInterface $container */ $container = require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'bootstrap.php'; $request = ServerRequestFactory::createFromGlobals(); Slim\Factory\AppFactory::create(); $app = new App( $container->get(ResponseFactoryInterface::class), $container, $container->get(CallableResolverInterface::class), $container->get(RouteCollectorInterface::class), $container->get(RouteResolverInterface::class) ); $app->add($container->get(RoutingMiddleware::class)); $app->add($container->get(ErrorMiddleware::class)); $app->run($request);
Проверка.
Запустим консольное приложение:
./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 #} <!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 #} {% 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 #} {% 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
<?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; }); } }
Включим провайдер в бутстрап
<?php // bootstrap.php // ... use App\Provider\RenderProvider; // ... $providers = [ // ... RenderProvider::class, // ... ]; // ...
Добавим рендер в обработчик 404 ошибки
--- 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; } }
--- 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)
<?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); } }
<?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; } }
Добавим провайдер, регистрирующий контроллеры
<?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'); }); } }
Не забудем добавить провайдер в бутстрап
<?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
<?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; } }
Теперь нужен провайдер, который зарегистрирует команду в контейнере и добавит её в маппинг
<?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); } }
Помним про бутстрап
<?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 (как во многочисленных примерах), на нём можно писать качественные и поддерживаемые приложения. Главное — не испугаться в начале пути, когда контейнер пуст, а зависимотсти нужны.
