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 PsrHttpMessageServerRequestInterface;
use PsrHttpMessageResponseInterface;
use SlimApp;
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 | PsrHttpMessageResponseFactoryInterface |
да | PSR-17 совместимая фабрика серверного http-запроса |
$container | PsrContainerContainerInterface |
нет | Контейнер зависимостей |
$callableResolver | SlimInterfacesCallableResolverInterface |
нет | Обработчик вызываемых методов |
$routeCollector | SlimInterfacesRouteCollectorInterface |
нет | Роутер |
$routeResolver | SlimInterfacesRouteResolverInterface |
нет | Обработчик результатов роутинга |
Так же теперь можно воспользоваться статическим методом create
фабрики приложения SlimFactoryAppFactory
.
Этот метод принимает на вход такие же параметры, только все они опциональные.
<?php
use PsrHttpMessageServerRequestInterface;
use PsrHttpMessageResponseInterface;
use SlimFactoryAppFactory;
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
. Чтобы ошибки обрабатывались корректно, нужно подключить SlimMiddlewareErrorMiddleware
<?php
use PsrHttpMessageServerRequestInterface;
use PsrHttpMessageResponseInterface;
use SlimFactoryAppFactory;
use SlimMiddlewareErrorMiddleware;
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()
интерфейса PsrHttpServerMiddlewareInterface
<?php
use PsrHttpMessageServerRequestInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpServerRequestHandlerInterface;
use SlimFactoryAppFactory;
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 PsrHttpMessageResponseInterface;
use SlimResponseEmitter 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 AppResponseEmitter;
use SlimFactoryAppFactory;
require 'vendor/autoload.php';
$app = AppFactory::create();
$serverRequestFactory = SlimFactoryServerRequestCreatorFactory::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
добавляются перед телом ответа
Разработчики фреймворка предлагают заменить эту опцию промежуточным ПО SlimMiddlewareOutputBufferingMiddleware
, в конструктор которого передается PSR-17 совместимая фаблика потока и режим, который может быть равен append
или prepend
<?php
use SlimFactoryAppFactory;
use SlimFactoryPsr17SlimPsr17Factory;
use SlimMiddlewareOutputBufferingMiddleware;
require 'vendor/autoload.php';
$app = AppFactory::create();
$middleware = new OutputBufferingMiddleware(SlimPsr17Factory::getStreamFactory(), OutputBufferingMiddleware::APPEND);
$app->add($middleware);
// ... Описание роутов и прочее
$app->run();
determineRouteBeforeAppMiddleware
Эта настройка позволяла получить текущий маршрут из объекта запроса в промежуточном ПО
На замену предоставляется SlimMiddlewareRoutingMiddleware
<?php
use SlimFactoryAppFactory;
use SlimMiddlewareRoutingMiddleware;
require 'vendor/autoload.php';
$app = AppFactory::create();
$middleware = new RoutingMiddleware($app->getRouteResolver());
$app->add($middleware);
// ... Описание роутов и прочее
$app->run();
displayErrorDetails
Настройка позволляла выводить подробности ошибок. При отладке это неплохо упрощает жизнь.
Помните SlimMiddlewareErrorMiddleware
? Оно и здесь нас выручит!
<?php
use SlimFactoryAppFactory;
use SlimMiddlewareErrorMiddleware;
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
со значением объема данных в теле ответа
Заменяет опцию промежуточное ПО SlimMiddlewareContentLengthMiddleware
<?php
use SlimFactoryAppFactory;
use SlimMiddlewareContentLengthMiddleware;
require 'vendor/autoload.php';
$app = AppFactory::create();
$middleware = new ContentLengthMiddleware();
$app->add($middleware);
// ... Описание роутов и прочее
$app->run();
routerCacheFile
Теперь вы можете напрямую установить файл кеша роутера
<?php
use SlimFactoryAppFactory;
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(
PsrContainerContainerInterface::get(0),
map([
'' => '@',
])
);
}
Этот код подскажет IDE, что у объекта, реализующего интерфейс PsrContainerContainerInterface
, метод 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 AppSupport;
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 AppSupport;
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 AppSupport;
use PsrHttpMessageResponseFactoryInterface;
use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use SlimInterfacesErrorHandlerInterface;
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(
PsrContainerContainerInterface::get(0),
map([
'' => '@',
])
);
override(
AppSupportConfig::get(0),
map([
'slim.debug' => 'bool',
'templates.dir' => 'string|false',
'templates.cache' => 'string|false',
])
);
}
<?php
// app/Support/ServiceProviderInterface.php
namespace AppSupport;
use UltraLiteContainerContainer;
interface ServiceProviderInterface
{
public function register(Container $container);
}
<?php
// app/Provider/AppProvider.php
namespace AppProvider;
use AppSupportCommandMap;
use AppSupportConfig;
use AppSupportNotFoundHandler;
use AppSupportServiceProviderInterface;
use PsrContainerContainerInterface;
use PsrHttpMessageResponseFactoryInterface;
use SlimCallableResolver;
use SlimExceptionHttpNotFoundException;
use SlimInterfacesCallableResolverInterface;
use SlimInterfacesRouteCollectorInterface;
use SlimInterfacesRouteResolverInterface;
use SlimMiddlewareErrorMiddleware;
use SlimMiddlewareRoutingMiddleware;
use SlimPsr7FactoryResponseFactory;
use SlimRoutingRouteCollector;
use SlimRoutingRouteResolver;
use UltraLiteContainerContainer;
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));
});
}
}
Мы вынесли роутинг в контейнер для того, чтобы можно было с ним работать без инициализации объекта SlimApp
.
<?php
// bootstrap.php
require_once __DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php';
use AppSupportServiceProviderInterface;
use AppProviderAppProvider;
use AppSupportConfig;
use UltraLiteContainerContainer;
/*
* Определяем окружение
*/
$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 AppSupportCommandMap;
use PsrContainerContainerInterface;
use SymfonyComponentConsoleApplication;
use SymfonyComponentConsoleCommandLoaderContainerCommandLoader;
use SymfonyComponentConsoleInputArgvInput;
use SymfonyComponentConsoleOutputConsoleOutput;
/** @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 PsrContainerContainerInterface;
use PsrHttpMessageResponseFactoryInterface;
use SlimApp;
use SlimInterfacesCallableResolverInterface;
use SlimInterfacesRouteCollectorInterface;
use SlimInterfacesRouteResolverInterface;
use SlimMiddlewareErrorMiddleware;
use SlimMiddlewareRoutingMiddleware;
use SlimPsr7FactoryServerRequestFactory;
/** @var ContainerInterface $container */
$container = require dirname(__DIR__) . DIRECTORY_SEPARATOR . 'bootstrap.php';
$request = ServerRequestFactory::createFromGlobals();
SlimFactoryAppFactory::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 AppProvider;
use AppSupportConfig;
use AppSupportServiceProviderInterface;
use PsrContainerContainerInterface;
use TwigEnvironment;
use TwigLoaderFilesystemLoader;
use UltraLiteContainerContainer;
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 AppProviderRenderProvider;
// ...
$providers = [
// ...
RenderProvider::class,
// ...
];
// ...
Добавим рендер в обработчик 404 ошибки
--- a/app/Support/NotFoundHandler.php
+++ b/app/Support/NotFoundHandler.php
@@ -8,15 +8,22 @@ use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use SlimInterfacesErrorHandlerInterface;
use Throwable;
+use TwigEnvironment;
+use TwigErrorLoaderError;
+use TwigErrorRuntimeError;
+use TwigErrorSyntaxError;
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 SlimMiddlewareRoutingMiddleware;
use SlimPsr7FactoryResponseFactory;
use SlimRoutingRouteCollector;
use SlimRoutingRouteResolver;
+use TwigEnvironment;
use UltraLiteContainerContainer;
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 AppController;
use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use SlimInterfacesRouteParserInterface;
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 AppController;
use PsrHttpMessageResponseInterface;
use PsrHttpMessageServerRequestInterface;
use TwigEnvironment as Render;
use TwigErrorLoaderError;
use TwigErrorRuntimeError;
use TwigErrorSyntaxError;
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 AppProvider;
use AppControllerHelloController;
use AppControllerHomeController;
use AppSupportServiceProviderInterface;
use PsrContainerContainerInterface;
use SlimInterfacesRouteCollectorInterface;
use SlimInterfacesRouteCollectorProxyInterface;
use TwigEnvironment;
use UltraLiteContainerContainer;
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 AppProviderWebProvider;
// ...
$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 AppCommand;
use SlimInterfacesRouteCollectorInterface;
use SymfonyComponentConsoleCommandCommand;
use SymfonyComponentConsoleInputInputInterface;
use SymfonyComponentConsoleOutputOutputInterface;
use SymfonyComponentConsoleStyleSymfonyStyle;
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 AppProvider;
use AppCommandRouteListCommand;
use AppSupportCommandMap;
use AppSupportServiceProviderInterface;
use PsrContainerContainerInterface;
use SlimInterfacesRouteCollectorInterface;
use UltraLiteContainerContainer;
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 AppProviderCommandProvider;
// ...
$providers = [
// ...
CommandProvider::class,
// ...
];
// ...
Теперь мы можем ввести команду...
./bin/console route:list
… и увидеть список роутов:
Routes
======
--------------- --------- ------- -------------------------------------
Route Methods Name Handler
--------------- --------- ------- -------------------------------------
/ GET index AppControllerHomeController:index
/hello/{name} GET hello AppControllerHelloController:show
--------------- --------- ------- -------------------------------------
Вот, собственно, и всё!
Как видно из туториала, Slim — это не обязательно вся логика приложения в файле routes.php
(как во многочисленных примерах), на нём можно писать качественные и поддерживаемые приложения. Главное — не испугаться в начале пути, когда контейнер пуст, а зависимотсти нужны.
Ссылка на исходники проекта из статьи
Автор: trawl