Slim 4 - руководство по начальной настройке



Это руководство расскажет как работать с фреймворком Slim4

Требования к серверу/хостингу

  • PHP 7.2+
  • MySQL 5.7+ or MariaDB
  • Apache webserver with mod_rewrite and .htaccess
  • Composer (только вовремя разработки)

Введение

Slim Framework - это микрофреймворк для веб-приложений, RESTful API и веб-сайтов.

Наша цель - создать RESTful API с маршрутизацией, бизнес-логикой и операциями с базой данных. В данном руководстве будут использованы стандарты ЗЫК и лучшие практики написания кода на данном фреймворке. 

Установка

Создайте новый каталог проекта и выполните следующую команду, чтобы установить основные компоненты Slim 4:

composer require slim/slim:"4.*"

В Slim 4 реализация PSR-7 отделена от ядра приложения. Это означает, что вы также можете установить другие реализации PSR-7, такие как nyholm/psr7.

В нашем случае мы устанавливаем реализации Slim PSR-7 с помощью команды:

composer require slim/psr7

Далее нам понадобиться реализация контейнера PSR-11 для внедрения зависимостей и автоматического подключения.

Запустите следующую команду для установки PHP-DI:

composer require php-di/php-di

Для тестирования мы усиановим phpunit как зависимость во время разработки с флагом  --dev:

composer require phpunit/phpunit --dev

Таким образом мы установили основные зависимости для нашего проекта. Позже мы добавим и другие.

Примечание. Исключите из коммитов папку vendor/ . Создайте файл с именем .gitignore в корневой папке проекта и добавьте в этот файл следующие строки:

vendor/
.idea/

Структура проекта

Продуманная структура папок позволит вам лучше организовать процесс, упростить установку и разработку, а также в некоторой доле обезопасить проект.

Предлагаю следующую стуктуру проекта (вы можете использовать привычную вам). 

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

.
├── config/             Configuration files
├── public/             Web server files (DocumentRoot)
│   └── .htaccess       Apache redirect rules for the front controller
│   └── index.php       The front controller
├── templates/          Twig templates
├── src/                PHP source code (The App namespace)
├── tmp/                Temporary files (cache and logfiles)
├── vendor/             Reserved for composer
├── .htaccess           Internal redirect to the public/ directory
├── .gitignore          Git ignore rules
└── composer.json       Project dependencies and autoloader

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

Каталог public/ является рабочей папкой  и, следовательно, также будет напрямую доступен для всех браузеров, поисковых систем и клиентов API. Все остальные папки не являются общедоступными и не должны быть доступны из вне. Это можно сделать, указав общую папку в Apache как DocumentRoot вашего веб-сайта. Об этом будет сказано чуть позже

PSR-4 автозагрузка

Или PSR-4 autoloading

Один из самых фундаментальных и важных моментов - наличие работающего автозагрузчика PSR-4. Для следующих шагов мы должны определить каталог src/ как корневой для пространства имен \App.

Добавьте эти настройки автозагрузки в composer.json:

"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
},
"autoload-dev": {
    "psr-4": {
        "App\\Test\\": "tests/"
    }
}

Полный файл composer.json должен выглядеть так:

{
    "require": {
        "php-di/php-di": "^6.0",
        "slim/psr7": "^1",
        "slim/slim": "^4.4"
    },
    "require-dev": {
        "phpunit/phpunit": "^8.4"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Test\\": "tests/"
        }
    },
    "config": {
        "process-timeout": 0,
        "sort-packages": true
    }
}

Запустите команду composer update, чтобы изменения вступили в силу.

Apache URL rewriting

Чтобы запутсить Slim приложение на апаче, нам необходимо настроить url преобразования для редиректа и направления запросов на основной конроллер (точка входа).

Точкой вход традиционно является файл index.php

  • Создайте папку: public/
  • Создайте файл .htaccess в папке  public/ со следующим содержимым:
# Redirect to front controller
RewriteEngine On
# RewriteBase /
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

Пожалуйста, не меняйте директиву RewriteRule. Это важно для дальнейше работы приложения.

  • Cоздайте следующий файл .htaccess в корне самого проекта и вставьте в него следующий код:
RewriteEngine on
RewriteRule ^$ public/ [L]
RewriteRule (.*) public/$1 [L]

Не пропустите этот шаг. Этот .htaccess файл важен для запуска Slim приложения из подпапки вашего окружения.

  • Создайте контроллер public/index.php со следующим содержимым:
<?php

(require __DIR__ . '/../config/bootstrap.php')->run();

Как уже было сказано, index.php является точкой входа и будет обрабатывать все запросы, перенаправляя их на нужныефайлы, в зависимости от параметров запроса

Настройка 

Папкой для всех конфигурационных файлов является config/

Файл config/settings.php являетс основным конфиг файлом и сочетает в себе настройки по умолчанию и основные настройки среды разработки.

  • Создайте папку(если не создана): config/
  • Создайте конфигурационный файл config/settings.php со следующим содержимым:
<?php

// Error reporting for production
error_reporting(0);
ini_set('display_errors', '0');

// Timezone
date_default_timezone_set('Europe/Berlin');

// Settings
$settings = [];

// Path settings
$settings['root'] = dirname(__DIR__);
$settings['temp'] = $settings['root'] . '/tmp';
$settings['public'] = $settings['root'] . '/public';

// Error Handling Middleware settings
$settings['error'] = [

    // Should be set to false in production
    'display_error_details' => true,

    // Parameter is passed to the default ErrorHandler
    // View in rendered output by enabling the "displayErrorDetails" setting.
    // For the console and unit tests we also disable it
    'log_errors' => true,

    // Display error details in error log
    'log_error_details' => true,
];

return $settings;

Начало 

Запуском проекта является выполнение первоначального кода, при первом запросе приложения. Как помните, в index.php мы прописали вызов bootstrap.php

Процедура начальной загрузки включает автозагрузчик композера, затем сборка контейнера, создается приложение, регистрируется запись маршрутов и посредников (middleware).

Создайте файл начальной загрузки config/bootstrap.php и вставьте в него следующие строки кода:

<?php

use DI\ContainerBuilder;
use Slim\App;

require_once __DIR__ . '/../vendor/autoload.php';

$containerBuilder = new ContainerBuilder();

// Set up settings
$containerBuilder->addDefinitions(__DIR__ . '/container.php');

// Build PHP-DI Container instance
$container = $containerBuilder->build();

// Create App instance
$app = $container->get(App::class);

// Register routes
(require __DIR__ . '/routes.php')($app);

// Register middleware
(require __DIR__ . '/middleware.php')($app);

return $app;

Маршрутизация - routing

Создайте файл для всех маршрутов config/routes.php со следующим содержимым:

<?php

use Slim\App;

return function (App $app) {
    // empty
};

Посредники - middleware

Что такое посредники?

Посредники, они же middlware  является промежуточным программным обеспечением, в нашем случае это промужеточный код, который может быть исполнен либо до, либо после основного кода, тем самым позволяя проводить некоторые манипуляции с данными и настройками, в зависимости от результатов исполнения.

Подробнее можно почитать в документации

Маршрутизации и обработка ошибок посредниками

Создайте глобальный обработчик посредников config/middleware.php :

<?php

use Slim\App;
use Slim\Middleware\ErrorMiddleware;

return function (App $app) {
    // Parse json, form data and xml
    $app->addBodyParsingMiddleware();

    // Add the Slim built-in routing middleware
    $app->addRoutingMiddleware();

    // Catch exceptions and errors
    $app->add(ErrorMiddleware::class);
};

 

Контейнер

Краткое руководство по контейнеру

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

Контейнер  (он же IoC-контейнер) - это инструмент для внедрения зависимостей.

Общее правило: основное приложение не должно использовать контейнер. Внедрение контейнера в класс - это антипаттерн. Вы должны явно объявить все зависимости классов в конструкторе.

Почему введение контейнера (в большинстве случаев) является антипаттерном?

В Slim 3 Service Locator (антипаттерн) был по умолчанию «стилем» для внедрения всего (Pimple) контейнера и получения от него зависимостей. Однако есть и недостатки:

Локатор служб(Service Locator) (антипаттерн) скрывает фактические зависимости вашего класса.
Локатор услуг (анти-шаблон) также нарушает принцип инверсии управления (IoC) SOLID.


В: Как делать правильнее?

A: Используйте композицию вместо наследования и внедрения зависимостей.

Начиная с Slim 4, вы можете использовать современные инструменты, такие как PHP-DI, с функцией autowire. Это означает: теперь вы можете явно объявить все зависимости в своем конструкторе и позволить DIC внедрить эти зависимости за вас.

Чтобы было понятнее: композиция не имеет ничего общего с функцией «autowire» DIC. Вы можете использовать композицию с чистыми классами и без контейнера или чего-либо еще. Функция autowire просто использует классы PHP Reflection для автоматического разрешения и вставки зависимостей.

Определения контейнеров

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

Вы можете добавить любую библиотеку для испольхования контейнеров, реализующую интерфейс PSR-11.

Создайте новый файл для записей контейнера config/container.php и  вставьте:

<?php

use Psr\Container\ContainerInterface;
use Slim\App;
use Slim\Factory\AppFactory;
use Slim\Middleware\ErrorMiddleware;

return [
    'settings' => function () {
        return require __DIR__ . '/settings.php';
    },

    App::class => function (ContainerInterface $container) {
        AppFactory::setContainer($container);

        return AppFactory::create();
    },

    ErrorMiddleware::class => function (ContainerInterface $container) {
        $app = $container->get(App::class);
        $settings = $container->get('settings')['error'];

        return new ErrorMiddleware(
            $app->getCallableResolver(),
            $app->getResponseFactory(),
            (bool)$settings['display_error_details'],
            (bool)$settings['log_errors'],
            (bool)$settings['log_error_details']
        );
    },

];

Base path

После запуска приложения у большинства пользователей будет 404 ошибка, так как base path не настроен корректно.

Если вы хотите запускать Slim приложение в подпапке, без изменения настройки DocumentRoot вашего веб сервера, то вам необходимо указать корректный base path (основную рабочую папку). В идеале ваше значение DoumentRoot должно ссылаться сразу на папку  public/. В любых других случаях вам необходимо самому настроить эту папку. 

Для примера DocumentRoot является /var/www/domain.com/htdocs/, Но ваше приложение находится по другому пути: /var/www/domain.com/htdocs/my-app/, таким образом вам необходимо натроить папку /my-app как base path.

Чтобы быть более точным: в этом контексте «подпапка» означает подкаталог проекта, а не каталог public/. 

По соображениям безопасности вы всегда должны помещать ваш основной контроллер (index.php) в каталог public /. Не помещайте контроллер  прямо в корневой каталог проекта.

Вы можете вручную установить базовый путь (base path) в Slim с помощью метода setBasePath:

$app->setBasePath('/slim4-tutorial');

Но проблема в том что base path может быть разным для каждого хостинга (хостинг разработки, хостинг тестирования, рабочая среда и так далее) 

Посредник BasePathMiddleware определяет и устанавливает корректный base path в Slim приложении

Для установки  BasePathMiddleware, запустите:

composer require selective/basepath

Добавьте используемый контейнер в config/container.php:

use Selective\BasePath\BasePathMiddleware;
// ...

return [
    // ...

    BasePathMiddleware::class => function (ContainerInterface $container) {
        return new BasePathMiddleware($container->get(App::class));
    },
];

Добавьте BasePathMiddleware::class в config/middleware.php:

<?php

use Selective\BasePath\BasePathMiddleware;
use Slim\App;
use Slim\Middleware\ErrorMiddleware;

return function (App $app) {
    // Parse json, form data and xml
    $app->addBodyParsingMiddleware();

    // Add the Slim built-in routing middleware
    $app->addRoutingMiddleware();

    $app->add(BasePathMiddleware::class); // <--- вот эта строчка

    // Catch exceptions and errors
    $app->add(ErrorMiddleware::class);
};

После установки BasePathMiddleware, удалите строчку установки basePath если она у вас была ранее прописана: $app->setBasePath('...');

Ваш первый роутинг

Откройте файл config/routes.php и вставьте код первого маршрута:

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Slim\App;

return function (App $app) {
    $app->get('/', function (
        ServerRequestInterface $request,
        ResponseInterface $response
    ) {
        $response->getBody()->write('Hello, World!');

        return $response;
    });
};

Теперь откройте ваш сайт http://localhost и увидите сообщение: Hello, World!

Красивый URL

Будьте внимтельны: Папка public/ является только DoumentRoot вашего сервера, но не должна участвовать в url.

Правильный URL:

  • http://www.example.com
  • http://www.example.com/users
  • http://www.example.com/my-app
  • http://www.example.com/my-app/users

Не правильный URL:

  • http://www.example.com/public
  • http://www.example.com/public/users
  • http://www.example.com/my-app/public
  • http://www.example.com/my-app/public/users

Действия (Actions)

Slim предоставляет несколько методов для добавления логики контроллера непосредственно в вызов маршрута. Объект запроса PSR-7 вводится в маршруты вашего Slim-приложения в качестве первого аргумента вызова маршрута следующим образом:

<?php

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

// ...

$app->get('/hello', function (ServerRequestInterface $request, ResponseInterface $response) {
    $response->getBody()->write('Hello World');
    return $response;
});

Хотя такие интерфейсы выглядят интуитивно понятными, они не подходят для сложных сценариев бизнес-логики, особенно когда необходимо зарегистрировать десятки или даже сотни обработчиков маршрутов. Если ваша логика не очень проста, я не рекомендую использовать обратные вызовы маршрута. Правильным решением в такой ситуации будет реализация этих обработчиков в отдельных классах, так называемые контроллеры простого действия (Single Action Controller). 

Каждый такой контроллер представлен своим собственным классом.

Action должен выполнять только следующие действия:

  • Сбор входных данных из HTTP запросов(если необходимо)
  • Вызывает домен согласно входным даным (если требуется) и сохраеят результат
  • Строит HTTP ответ (обычно с результатами вызова домена)

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

Ответ может быть представлен в HTML (например, с помощью шаблонизатора Twig) для стандартного веб-запроса; или это может быть что-то вроде JSON для запросов RESTful API.

Примечание: Замыкания(функции) в качестве обработчиков маршрутизации довольно «дороги», потому что PHP создает все замыкания заново для каждого запроса. Использование имен классов легче, быстрее и лучше масштабируется для более крупных приложений.

Более подробную информацию о всех процессах что происходят при достижении маршрута, и о связи между различными уровнями можно найти здесь: Action

  • Создайте подпапку: src/Action
  • Создайте новый класс в: src/Action/HomeAction.php
<?php

namespace App\Action;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class HomeAction
{
    public function __invoke(
        ServerRequestInterface $request, 
        ResponseInterface $response
    ): ResponseInterface {
        $response->getBody()->write('Hello, World!');

        return $response;
    }
}

Затем откройте config/routes.php и замените маршрут / на следующую строку:

$app->get('/', \App\Action\HomeAction::class)->setName('home');

Конечный результат в config/routes.php будет выглядеть так:

<?php

use Slim\App;

return function (App $app) {
    $app->get('/', \App\Action\HomeAction::class)->setName('home');
};

Теперь откройте ваш сайт, например http://localhost и увидите сообщение Hello, World!.

 

Запись JSON в ответ (JSON response)

Чтобы получить валидный JSON ответ, вы должны передать корректно закодированную  json строку в тело запроса и добавить  Content-Type header to application/json:

<?php

namespace App\Action;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final class HomeAction
{
    public function __invoke(
        ServerRequestInterface $request, 
        ResponseInterface $response
    ): ResponseInterface {
        $response->getBody()->write(json_encode(['success' => true]));

        return $response->withHeader('Content-Type', 'application/json');
    }
}

Откройте ваш сайт и увидите JSON ответ {"success":true}.

Чтобы изменить http status code,  просто используйте метод $response->withStatus(x). Пример:

$result = ['error' => ['message' => 'Validation failed']];

$response->getBody()->write(json_encode($result));

return $response
    ->withHeader('Content-Type', 'application/json')
    ->withStatus(422);

 

Домен (Domain)

Забудьте о CRUD! Ваш API должен выполнять бизнес задачи, а не просто "технические операции с базой данных" типа CRUD (create/read/update/delete). Не вставляйте бизнес логику в действия. Действия вызывают сервисы (доменный уровень). Если вы хотите использовать ту же самую логику в другом месте (другом действии), то достатточно будет вызвать нужный сервис в вашем коде(действии - action).

Сервисы (Services)

Домен это место для сложной бизнес логики.

Вместо того, чтобы помещать логику в раздутые (толстые) "модели", мы помещаем логику в более мелкие, специализированные классы сервисов.

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

У сервиса могут быть одновременно несколько клиентов, например, какое-нибудь действие(request), другой сервис,  CLI (console) либо какой-нибудь unit-test (phpunit).

Сервис класс это не просто  “Manager” или  “Utility” class.

Каждый сервис-класс должен выполнять только одну задачу, быть ответсвенным за одну операцию, например, перевод денег из пункта A в пункт B, и ничего более.

Отделите данные от поведения, используя сервисы для поведения, а для данных специальный объект передачи данных.

Директория для всех модулей (доменов) и sub-модулей: src/Domain

Создайте код для сервис-класса  src/Domain/User/Service/UserCreator.php:

<?php

namespace App\Domain\User\Service;

use App\Domain\User\Repository\UserCreatorRepository;
use App\Exception\ValidationException;

/**
 * Service.
 */
final class UserCreator
{
    /**
     * @var UserCreatorRepository
     */
    private $repository;

    /**
     * The constructor.
     *
     * @param UserCreatorRepository $repository The repository
     */
    public function __construct(UserCreatorRepository $repository)
    {
        $this->repository = $repository;
    }

    /**
     * Create a new user.
     *
     * @param array $data The form data
     *
     * @return int The new user ID
     */
    public function createUser(array $data): int
    {
        // Input validation
        $this->validateNewUser($data);

        // Insert user
        $userId = $this->repository->insertUser($data);

        // Logging here: User created successfully
        //$this->logger->info(sprintf('User created successfully: %s', $userId));

        return $userId;
    }

    /**
     * Input validation.
     *
     * @param array $data The form data
     *
     * @throws ValidationException
     *
     * @return void
     */
    private function validateNewUser(array $data): void
    {
        $errors = [];

        // Here you can also use your preferred validation library

        if (empty($data['username'])) {
            $errors['username'] = 'Input required';
        }

        if (empty($data['email'])) {
            $errors['email'] = 'Input required';
        } elseif (filter_var($data['email'], FILTER_VALIDATE_EMAIL) === false) {
            $errors['email'] = 'Invalid email address';
        }

        if ($errors) {
            throw new ValidationException('Please check your input', $errors);
        }
    }
}

 

Обратите внимание на конструктор! Вы можете видеть как мы объявляем UserCreatorRepository как зависимость, так как сервис может взаимодействовать с базой данных только через репозиторий.






comments powered by Disqus