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

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

KOD

25 апреля 2019 года свет увидела новая мажорная alpha-версия микрофреймворка Slim, а 18 мая она выросла до beta.

Как создать приложение?

<?php

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Slim\Factory\AppFactory;

require 'vendor/autoload.php';

$app = AppFactory::create();
$app->get('/hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response) {
    $name = $request->getAttribute('name');
    $response->getBody()->write("Hello, $name");
    return $response;
});
$app->run();

Верните мне 404 ошибку!

Если мы попробуем открыть несуществующую страницу, получим код ответа 500, а не 404. Чтобы ошибки обрабатывались корректно, нужно подключить \Slim\Middleware\ErrorMiddleware

<?php

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Slim\Factory\AppFactory;
use Slim\Middleware\ErrorMiddleware;

require 'vendor/autoload.php';

$app = AppFactory::create();
$middleware = new ErrorMiddleware(
    $app->getCallableResolver(),
    $app->getResponseFactory(),
    false,
    false,
    false
);
$app->add($middleware);
$app->get('/hello/{name}', function (ServerRequestInterface $request, ResponseInterface $response) {
    $name = $request->getAttribute('name');
    $response->getBody()->write("Hello, $name");
    return $response;
});
$app->run();

Middleware

Промежуточное ПО должно быть реализацией PSR-15. В качестве исключения, можно передавать функции, но сигнатура должна соответствовать методу process() интерфейса \Psr\Http\Server\MiddlewareInterface

<?php

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\Factory\AppFactory;

require 'vendor/autoload.php';

$app = AppFactory::create();
$app->add(function (ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
    $response = $handler->handle($request);
    return $response->withHeader('Content-Type', 'application/json');
});
// ... Описание роутов и прочее
$app->run();

Сигнатура ($request, $response, $next) больше не поддерживается

Создание приложения на Slim-4

Чтобы более подробно рассмотреть фреймворк, напишем небольшое приложение.

Приложение будет иметь следующие роуты:

  • /hello/{name} — страница приветствия;
  • / — редирект на страницу /hello/world
  • Остальные роуты будут возвращать кастомизированную страницу с 404 ошибкой.

Логика будет в контроллерах, рендерить страницу будем через шаблонизатор Twig

Бонусом добавим консольное приложение на базе компонента Symfony Console с командой, отображающей список роутов

Шаг 0. Установка зависимостей

Нам понадобится:

  • микрофреймворк, slim/slim;
  • реализация интерфейса контейнера (PSR-11) psr/container;
  • реализация интерфейсов http-сообщений (PSR-7) psr/http-message;
  • реализация интерфейсов фабрик http-сообщений (PSR-17) psr/http-factory;
  • шаблонизатор twig/twig;
  • консольное приложение symfony/console.

В качестве контенера зависимостей я выбрал ultra-lite/container, как легкий, лаконичный и соответствующий стандарту.

PSR-7 и PSR-17 разработчики Slim предоставляют в одном пакете slim/psr7. Им и воспользуемся

Предполагается, что пакетный менеджер Composer уже установлен.

Создаём папку под проект (в качестве примера будет использоваться /path/to/project) и переходим в неё.

Добавим в проект файл composer.json со следующим содержимым:

{
  "require": {
    "php": ">=7.1",
    "slim/slim": "4.0.0-beta",
    "slim/psr7": "~0.3",
    "ultra-lite/container": "^6.2",
    "symfony/console": "^4.2",
    "twig/twig": "^2.10"
  },
  "autoload": {
    "psr-4": {
      "App\\": "app"
    }
  }
}

и выполним команду 

composer install

Теперь у нас есть все необходимые пакеты и настроен автозагрузчик классов.

Если работаем с git, добавим файл .gitignore и внесем туда директорию vendor (и диреткорию своей IDE при необходимости)

/.idea/*
/vendor/*

Для комфортной разработки самое время подружить контейнер и IDE.

В корне проекта создадим файл .phpstorm.meta.php и напишем там такой код:

<?php
// .phpstorm.meta.php

namespace PHPSTORM_META {

    override(
        \Psr\Container\ContainerInterface::get(0),
        map([
            '' => '@',
        ])
    );
}

Этот код подскажет IDE, что у объекта, реализующего интерфейс \Psr\Container\ContainerInterface, метод get() вернёт объект класса или реализацию интерфейса, имя которого передано в параметре.

Шаг 1. Каркас приложения

Добавим каталоги:

  • app — код приложения. К нему мы подключим наше пространство имен для автозагрузчика классов;
  • bin — директория для консольной утилиты;
  • config — здесь будут файлы конфигурации приложения;
  • public — директория, открытая в веб (точка входа приложения, стили, js, картинки и т.д.);
  • template — директория шаблонов;
  • var — директория для различных файлов. Логи, кэш, локальное хранилище и т.д.

И файлы:

  • config/app.ini — основной конфиг приложения;
  • config/app.local.ini — конфиг для окружения local;
  • app/Support/CommandMap.php — маппинг команд консольного приложения для ленивой загрузки.
  • app/Support/Config.php — Класс конфигурации (Чтобы IDE знала, какие конфиги у нас имеются).
  • app/Support/NotFoundHandler.php — Класс обработчика 404й ошибки.
  • app/Support/ServiceProviderInterface.php — Интерфейс сервис-провайдера.
  • app/Provider/AppProvider.php — Основной провайдер приложения.
  • bootstrap.php — сборка контейнера;
  • bin/console — точка входа консольного приложения;
  • public/index.php — точка входа веб приложения.

Проверка.

Запустим консольное приложение:

./bin/console

В ответ должно отобразиться окно приветсвия компонета symfony/console с двумя доступными командами — help и list.

Console Tool

Usage:
  command [options] [arguments]

Options:
  -h, --help            Display this help message
  -q, --quiet           Do not output any message
  -V, --version         Display this application version
      --ansi            Force ANSI output
      --no-ansi         Disable ANSI output
  -n, --no-interaction  Do not ask any interactive question
  -v|vv|vvv, --verbose  Increase the verbosity of messages: 1 for normal output, 2 for more verbose output and 3 for debug

Available commands:
  help  Displays help for a command
  list  Lists commands

Теперь запустим веб-сервер.

php -S localhost:8080 -t public public/index.php

И откроем любой урл на localhost:8080.

Все запросы должны возвращать ответ с кодом 404 и пустым телом.

Это происходит, потому что у нас не указан ни один маршрут.

Нам осталось подключить рендер, написать шаблоны, контроллеры и задать маршруты.

Шаг 2. Рендер

Добавим шаблон template/layout.twig. Это базовый шаблон для всех страниц

Добавим шаблон template/layout.twig. Это базовый шаблон для всех страниц


template/layout.twig

{# template/layout.twig #}
<!DOCTYPE html>
<html lang="en">
<head>
    <title>{% block title %}Slim demo{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>


Добавим шаблон страницы приветствия template/hello.twig


template/hello.twig

{# template/hello.twig #}
{% extends 'layout.twig' %}
{% block title %}Slim demo::hello, {{ name }}{% endblock %}
{% block content %}
<h1>Welcome!</h1>
<p>Hello, {{ name }}!</p>
{% endblock %}


И шаблон страницы ошибки template/err404.twig


template/err404.twig

{# template/err404.twig #}
{% extends 'layout.twig' %}
{% block title %}Slim demo::not found{% endblock %}
{% block content %}
<h1>Error!</h1>
<p>Page not found =(</p>
{% endblock %}


Добавим провайдер рендеринга app/Provider/RenderProvider.php


app/Provider/RenderProvider.php

<?php
// app/Provider/RenderProvider.php

namespace App\Provider;

use App\Support\Config;
use App\Support\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
use UltraLite\Container\Container;

class RenderProvider implements ServiceProviderInterface
{
    public function register(Container $container)
    {
        $container->set(Environment::class, function (ContainerInterface $container) {
            $config = $container->get(Config::class);
            $loader = new FilesystemLoader($config->get('templates.dir'));
            $cache = $config->get('templates.cache');
            $options = [
                'cache' => empty($cache) ? false : $cache,
            ];
            $twig = new Environment($loader, $options);
            return $twig;
        });
    }
}


Включим провайдер в бутстрап


bootstrap.php

<?php
// bootstrap.php
// ...
use App\Provider\RenderProvider;
// ...
$providers = [
// ...
    RenderProvider::class,
// ...
];
// ...

Добавим рендер в обработчик 404 ошибки


app/Support/NotFoundHandler.php (DIFF)

--- a/app/Support/NotFoundHandler.php
+++ b/app/Support/NotFoundHandler.php
@@ -8,15 +8,22 @@ use Psr\Http\Message\ResponseInterface;
 use Psr\Http\Message\ServerRequestInterface;
 use Slim\Interfaces\ErrorHandlerInterface;
 use Throwable;
+use Twig\Environment;
+use Twig\Error\LoaderError;
+use Twig\Error\RuntimeError;
+use Twig\Error\SyntaxError;

 class NotFoundHandler implements ErrorHandlerInterface
 {

     private $factory;

-    public function __construct(ResponseFactoryInterface $factory)
+    private $render;
+
+    public function __construct(ResponseFactoryInterface $factory, Environment $render)
     {
         $this->factory = $factory;
+        $this->render = $render;
     }

     /**
@@ -26,10 +33,14 @@ class NotFoundHandler implements ErrorHandlerInterface
      * @param bool $logErrors
      * @param bool $logErrorDetails
      * @return ResponseInterface
+     * @throws LoaderError
+     * @throws RuntimeError
+     * @throws SyntaxError
      */
     public function __invoke(ServerRequestInterface $request, Throwable $exception, bool $displayErrorDetails, bool $logErrors, bool $logErrorDetails): ResponseInterface
     {
         $response = $this->factory->createResponse(404);
+        $response->getBody()->write($this->render->render('err404.twig'));
         return $response;
     }
 }


app/Provider/AppProvider.php (DIFF)

--- a/app/Provider/AppProvider.php
+++ b/app/Provider/AppProvider.php
@@ -19,6 +19,7 @@ use Slim\Middleware\RoutingMiddleware;
 use Slim\Psr7\Factory\ResponseFactory;
 use Slim\Routing\RouteCollector;
 use Slim\Routing\RouteResolver;
+use Twig\Environment;
 use UltraLite\Container\Container;

 class AppProvider implements ServiceProviderInterface
@@ -99,7 +100,7 @@ class AppProvider implements ServiceProviderInterface
          * Регистрируем обработчика ошибки 404
          */
         $container->set(NotFoundHandler::class, function (ContainerInterface $container) {
-            return new NotFoundHandler($container->get(ResponseFactoryInterface::class));
+            return new NotFoundHandler($container->get(ResponseFactoryInterface::class), $container->get(Environment::class));
         });

         /*

Теперь наша 404 ошибка приобрела тело.

Шаг 3. Контроллеры

Теперь можно браться за контроллеры

У нас их будет 2:


  • app/Controller/HomeController.php — главная страница
  • app/Controller/HelloController.php — страница приветствия


Контроллеру главной страницы из зависимостей необходим роутер (для построения URL редиректа), а контроллеру страницы приветствия — рендер (для рендегинга html)


app/Controller/HomeController.php

<?php
// app/Controller/HomeController.php

namespace App\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\Interfaces\RouteParserInterface;

class HomeController
{

    /**
     * @var RouteParserInterface
     */
    private $router;

    public function __construct(RouteParserInterface $router)
    {
        $this->router = $router;
    }

    public function index(ServerRequestInterface $request, ResponseInterface $response)
    {
        $uri = $this->router->fullUrlFor($request->getUri(), 'hello', ['name' => 'world']);
        return $response
            ->withStatus(301)
            ->withHeader('location', $uri);
    }
}


app/Controller/HelloController.php

<?php
// app/Controller/HelloController.php

namespace App\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Twig\Environment as Render;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Error\SyntaxError;

class HelloController
{

    /**
     * @var Render
     */
    private $render;

    public function __construct(Render $render)
    {
        $this->render = $render;
    }

    /**
     * @param ServerRequestInterface $request
     * @param ResponseInterface $response
     * @return ResponseInterface
     * @throws LoaderError
     * @throws RuntimeError
     * @throws SyntaxError
     */
    public function show(ServerRequestInterface $request, ResponseInterface $response)
    {
        $response->getBody()->write($this->render->render('hello.twig', ['name' => $request->getAttribute('name')]));
        return $response;
    }
}


Добавим провайдер, регистрирующий контроллеры


app/Provider/WebProvider.php

<?php
// app/Provider/WebProvider.php

namespace App\Provider;

use App\Controller\HelloController;
use App\Controller\HomeController;
use App\Support\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Slim\Interfaces\RouteCollectorInterface;
use Slim\Interfaces\RouteCollectorProxyInterface;
use Twig\Environment;
use UltraLite\Container\Container;

class WebProvider implements ServiceProviderInterface
{
    public function register(Container $container)
    {
        /*
         * Зарегистрируем контроллеры
         */
        $container->set(HomeController::class, function (ContainerInterface $container) {
            return new HomeController($container->get(RouteCollectorInterface::class)->getRouteParser());
        });

        $container->set(HelloController::class, function (ContainerInterface $container) {
            return new HelloController($container->get(Environment::class));
        });

        /*
         * Зарегистрируем маршруты
         */
        $router = $container->get(RouteCollectorInterface::class);
        $router->group('/', function(RouteCollectorProxyInterface $router) {
            $router->get('', HomeController::class . ':index')->setName('index');
            $router->get('hello/{name}', HelloController::class . ':show')->setName('hello');
        });
    }
}


Не забудем добавить провайдер в бутстрап


bootstrap.php

<?php
// bootstrap.php
// ...
use App\Provider\WebProvider;
// ...
$providers = [
// ...
    WebProvider::class,
// ...
];
// ...


Мы можем запустить веб-сервер (если останавливали)...


php -S localhost:8080 -t public public/index.php


… открыть в браузере http://localhost:8080 и увидеть, что браузер нас перенаправил на http://localhost:8080/hello/world


Мы видим теперь приветствие world'а.

Мы можем открыть http://localhost:8080/hello/ivan и бразуер поприветствует ivan'а.


Несуществующая страница, например, http://localhost:8080/helo/world отображает наш кастомный текст и отдаёт 404 статус.


Шаг 4. Консольная команда

Напишем команду route:list

app/Command/RouteListCommand.php

<?php
// app/Command/RouteListCommand.php

namespace App\Command;

use Slim\Interfaces\RouteCollectorInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

class RouteListCommand extends Command
{

    /*
     * Имя вынесено в константу, чтобы было меньше ошибок при маппинге команд
     */
    const NAME = 'route:list';

    /**
     * @var RouteCollectorInterface
     */
    private $router;

    public function __construct(RouteCollectorInterface $router)
    {
        $this->router = $router;
        parent::__construct();
    }

    protected function configure()
    {
        $this->setName(self::NAME)
            ->setDescription('List of routes.')
            ->setHelp('List of routes.')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        $io = new SymfonyStyle($input, $output);
        $io->title('Routes');
        $rows = [];
        $routes = $this->router->getRoutes();
        if (!$routes) {
            $io->text('Routes list is empty');
            return 0;
        }
        foreach ($routes as $route) {
            $rows[] = [
                'path' => $route->getPattern(),
                'methods' => implode(', ', $route->getMethods()),
                'name' => $route->getName(),
                'handler' => $route->getCallable(),
            ];
        }
        $io->table(
            ['Route', 'Methods', 'Name', 'Handler'],
            $rows
        );
        return 0;
    }
}


Теперь нужен провайдер, который зарегистрирует команду в контейнере и добавит её в маппинг


app/Provider/CommandProvider.php

<?php
// app/Provider/CommandProvider.php

namespace App\Provider;

use App\Command\RouteListCommand;
use App\Support\CommandMap;
use App\Support\ServiceProviderInterface;
use Psr\Container\ContainerInterface;
use Slim\Interfaces\RouteCollectorInterface;
use UltraLite\Container\Container;

class CommandProvider implements ServiceProviderInterface
{
    public function register(Container $container)
    {
        /*
         * Добавим команду списка маршрутов в контейнер
         */
        $container->set(RouteListCommand::class, function (ContainerInterface $container) {
            return new RouteListCommand($container->get(RouteCollectorInterface::class));
        });

        /*
         * Добавим команду списка маршрутов в маппинг команд
         */
        $container->get(CommandMap::class)->set(RouteListCommand::NAME, RouteListCommand::class);
    }
}


Помним про бутстрап


bootstrap.php

<?php
// bootstrap.php
// ...
use App\Provider\CommandProvider;
// ...
$providers = [
// ...
    CommandProvider::class,
// ...
];
// ...


Теперь мы можем ввести команду...


./bin/console route:list


… и увидеть список роутов:


Routes
======

 --------------- --------- ------- ------------------------------------- 
  Route           Methods   Name    Handler                              
 --------------- --------- ------- ------------------------------------- 
  /               GET       index   App\Controller\HomeController:index  
  /hello/{name}   GET       hello   App\Controller\HelloController:show  
 --------------- --------- ------- ------------------------------------- 


Заключение

Slim — это не обязательно вся логика приложения в файле routes.php (как во многочисленных примерах), на нём можно писать качественные и поддерживаемые приложения. Главное — не испугаться в начале пути, когда контейнер пуст, а зависимотсти нужны.

Report Page