Микрофреймворк slim

  • Tutorial

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 — точка входа веб приложения.

config/app.ini
; режим отладки
slim.debug=Off

; директория шаблонов
templates.dir=template

; кэш шаблонов
templates.cache=var/cache/template

config/app.local.ini
; В этом файле мы только переопределим некоторые параметры. Остальные значения подставятся из основного конфига
; В локальном окружении полезно видеть подробности ошибок
slim.debug=On

; кэш шаблонов для разработки не нужен
templates.cache=

Ах да, ещё неплохо исключить конфиги окружения из репозитория. Ведь там могут быть явки/пароли. Кэш тоже исключим.


.gitignore
/.idea/*
/config/*
/vendor/*
/var/cache/*
!/config/app.ini
!/var/cache/.gitkeep

app/Support/CommandMap.php
<?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;
    }
}

app/Support/Config.php
<?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;
    }
}

app/Support/NotFoundHandler.php
<?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 понимать, какие у конфига есть ключи и какого они типа


.phpstorm.meta.php
<?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',
        ])
    );
}

app/Support/ServiceProviderInterface.php
<?php
// app/Support/ServiceProviderInterface.php

namespace App\Support;

use UltraLite\Container\Container;

interface ServiceProviderInterface
{

    public function register(Container $container);
}

app/Provider/AppProvider.php
<?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.


bootstrap.php
<?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;

bin/console
#!/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

public/index.php
<?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
{# 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 (как во многочисленных примерах), на нём можно писать качественные и поддерживаемые приложения. Главное — не испугаться в начале пути, когда контейнер пуст, а зависимотсти нужны.


Ссылка на исходники проекта из статьи

Поддержать автора
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 29

    0

    Перешёл по первой ссылке… интересно…

      0

      Прошу прощения. Поправил ссылку.

      +2
      Имхо, теперь микрофреймворк Slim.
        +1

        Опять же ИМХО, но как можно его назвать фреймворком, если в нём из коробки только роутинг? Даже реализации контейнера нет...

        +2
        trawl, app/Provider/AppProvider.php — такую портянку теперь обязательно писать?
        Прекрасно что ребята повыкидывали всё на свете из фреймворка, так что даже не понятно, а что там вообще осталось, но вот такой бойлер плейт огорчает.

        Добавлена фабрика для создания экземпляра приложения;

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

        Обработчик запроса приложения теперь принимает только объект запроса (в старой версии принимал объекты запроса и ответа).

        Раньше можно было на каждом этапе что то в ответ добавить, но видимо это ни кому не нужно.

        Лично мне не понятно зачем всё это было сделано. Фреймворк это всё такие готовый набор инструментов и какие то направляющие для работы, а теперь Slim это что угодно и как угодно.

        Slim PHP — собери свой набор!

        Slim был микрофреймворком, на следующем этапе он проскочил нанофреймворки и просто растворился — перестал быть фреймворком — ни каких рамок, творчество без границ.
          +2
          Раньше можно было на каждом этапе что то в ответ добавить, но видимо это ни кому не нужно.

          Вы имеете ввиду middleware? Если да, то это можно делать и сейчас, при этом нет неоднозначности:


          // До
          function middleware($request, $response1, $next) {
              $response2 = $next($request, $response1);
              // Есть 2 ответа, какой использовать?
              return $response2->withHeader('foo', 'bar');
          }
          
          // После
          function middleware($request, $next) {
              $response = $next($request);
              return $response->withHeader('foo', 'bar');
          }
            0
            app/Provider/AppProvider.php — такую портянку теперь обязательно писать?

            Конечно же не обязательно.


            Можно сократить, сразу забиндив интерфейс к реализации, заменить


            <?php
            // app/Provider/AppProvider.php
            
            namespace App\Provider;
            
            // ...
            
            class AppProvider implements ServiceProviderInterface
            {
            
                public function register(Container $container)
                {
                    // ...
                    /*
                     * Регистрируем обработчик результатов роутера
                     */
                    $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);
                    });
                    // ...
                }
            }

            на


            <?php
            // app/Provider/AppProvider.php
            
            namespace App\Provider;
            
            // ...
            
            class AppProvider implements ServiceProviderInterface
            {
            
                public function register(Container $container)
                {
                    // ...
                    /*
                     * Регистрируем обработчик результатов роутера
                     */
                    $container->set(RouteResolverInterface::class, function (ContainerInterface $container) {
                        return new RouteResolver($container->get(RouteCollectorInterface::class));
                    });
                    // ...
                }
            }

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


            Если нет необходимости использовать роутер в сервисах, контроллерах, командах и т.д., можно вообще через фабрику создать экземпляр приложения, вынести обработчик ошибок в public/index.php и вообще удалить AppProvider.


            Сделать можно так, как вам удобно.


            Раньше можно было на каждом этапе что то в ответ добавить, но видимо это ни кому не нужно.

            Как уже ответили, и сейчас можно.
            Я полагаю, что на данный шаг разработчики пошли во имя следования стандартам PSR (В частности, PSR-15 Middleware)


            Лично мне не понятно зачем всё это было сделано. Фреймворк это всё такие готовый набор инструментов и какие то направляющие для работы, а теперь Slim это что угодно и как угодно.

            Slim — это микрофреймворк. А направляющие для работы в нём — пакеты psr/*.

              +1

              А можно еще подключить контейнер который поддерживает autowire, и жить сразу станет легче и веселей. Описывать придется только какие-то исключения.

                0

                Именно так!
                Можно воспользоваться любым контейнером, который реализует PSR-11.

                  +1
                  Slim использует PSR-11-совместимый контейнер, но не благоприятствует контейнерам с autowire:
                  Обращаемся к исходному коду:
                  github.com/slimphp/Slim/blob/4.x/Slim/MiddlewareDispatcher.php#L162-L172
                  О каком полноценном Autowire можно говорить, если не объявленный заранее в контейнере Middleware будет создаваться с передачей одного параметра ContainerInterface?
                  Причём MiddlewareDispatcher создаётся в конструкторе App вот таким незатейливым кодом:

                  $this->middlewareDispatcher = new MiddlewareDispatcher($routeRunner, $container);
                  

                  Т.е. его не переопределить
                    0

                    Можно, учитывая это обстоятельство, писать промежуточное ПО, принимающее в конструкторе контейнер.
                    Можно переопределить \Slim\MiddlewareDispatcher путём наследования, а потом наследоваться от \Slim\App.


                    Но оба варианта такое себе...

                      0
                      но не благоприятствует контейнерам с autowire

                      Или я не верно Вас понял, или Вы не правы. В коде, на который Вы ссылаетесь, идет сначала вызов ContainerInterface::has(). Нормальный autowire-контейнер должен возвращать true, если он может инициализировать запрошенный элемент. А слим потом его из контейнера спокойно себе получит. Не вижу, честно говоря, проблем с автовайрингом

                        0
                        Если контейнер от ларавеля (пробовал Slim 4 Alpha/beta именно с ним) не является нормальным autowire-контейнером, то Вы, вероятно, правы и я ошибаюсь на этот счёт
                          0

                          Я не знаю, как устроен контейнер от ларавеля, но если он при вызове метода get может вернуть запрошенный объект, а при вызове has с тем же аргументом возвращает false, то он не является нормальным:)

                            0
                            Ну да, в интерфейсе комментарий к ->has():
                            * Returns true if the container can return an entry for the given identifier.
                            А в Illuminate\Container ->has() возвращает значение $this->bound()
                            Ну да, как-то не нормально :)
                              0

                              Исходя из кода (пробежался глазами диагонально) можно предположить, что контейнер требует предварительной настройки связей запрашиваемых имен и возвращаемых элементов. То есть, это не совсем авто вайринг:)

                                0

                                Нет, я был не прав. Он действительно autowire через рефлексию. Но вот метод has не проверяет возможность автоматической инициализации! Действительно, странно

              0
              Slim всё же привычнее Falcon и легче Lumen. Кажется, дальше уже только чистый php… ну как Gentoo =)
                0
                Забавно, что чистый PHP, считающийся одним из самых простых языков, теперь сравнивают с Gentoo.
                +1
                Если работаем с git, добавим файл .gitignore и внесем туда директорию vendor (и диреткорию своей IDE при необходимости)

                Не засоряйте .gitignore локальными правилами, используйте для этого глобальный файл, напрмер:
                git config --global core.excludesfile ~/.gitignore_global
                  0

                  т.е. после выполнения команды можно внести /.idea в ~/.gitignore_global и забыть?

                    0
                    Все верно. У меня там
                    .idea/
                    .DS_Store

                    После внесения легко проверяется с помощью git status
                      0

                      Не знал. Спасибо!

                    +1
                    И сразу ваши коллеги понапихают туда запрещенки, если владелец репозитория не следит за этим и не проверяет весь код.
                    Я сторонник прописывания правил для ОС и популярных IDE: понятно, что принципиально это некорректно, однако на практике полезно.
                      0

                      Спасибо, не знал. После git init всегда делал 'echo ".idea/" >.git/info/exclude'

                        0
                        на самом деле

                      0

                      И как теперь гуглить подключение шаблонизатора slim к фреймворку slim? Провальное название, не делайте так, всегда проверяйте нейминг перед!

                        0

                        Не понял, о чём Вы

                          +2

                          Гугление вряд ли бы помогло.
                          Первый комит шаблонизатора: 12 Sep 2010
                          Первый комит фреймворка: 20 Sep 2010
                          Ну и сомневаюсь, что кто-то захочет подключать шаблонизатор на Ruby к фреймворку на php

                        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                        Самое читаемое