- Требования к серверу/хостингу
- Введение
- Установка
- Структура проекта
- PSR-4 автозагрузка
- Apache URL rewriting
- Настройка 
- Начало 
- Маршрутизация - routing
- Посредники - middleware
- Контейнер
- Base path
- Ваш первый роутинг
- Красивый URL
- Действия (Actions)
- Запись JSON в ответ (JSON response)
- Домен (Domain)
Это руководство расскажет как работать с фреймворком 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
как зависимость, так как сервис может взаимодействовать с базой данных только через репозиторий.