Действия (Actions) средствами PHP для обработки запросов
Основные подходы к реализации действий в PHP
Действия (actions) представляют собой логические единицы обработки запросов в веб-приложении. В зависимости от архитектуры проекта используются различные способы организации этих действий. Наиболее эффективным решением в современных PHP-приложениях является использование invokable классов – классов с единственным методом __invoke, которые действуют как callable-обработчики.
Как организовать действие в виде отдельного класса, готового к внедрению зависимостей и тестированию?
// Action класс для регистрации пользователя
class RegisterUserAction {
private UserRepository $users;
private Mailer $mailer;
public function __construct(UserRepository $users, Mailer $mailer) {
$this->users = $users;
$this->mailer = $mailer;
}
public function __invoke(array $requestData): array {
$user = $this->users->create($requestData);
$this->mailer->sendWelcomeEmail($user);
return ['status' => 'ok', 'user_id' => $user->id];
}
}Source query php (источник запроса в php)
// Использование в роутере (Slim 4 пример)
$app->post('/register', RegisterUserAction::class);Php request url (url запроса в php)
Пояснение: Класс RegisterUserAction получает зависимости через конструктор. Метод __invoke принимает массив данных запроса и возвращает результат. Контейнер зависимостей автоматически разрешит конструктор. Такой подход обеспечивает слабую связанность, удобное тестирование и переиспользование действия в разных сценариях (например, из консольной команды).
Типичные ошибки:
- Отсутствие регистрации класса в DI-контейнере – вызов
RegisterUserAction::classприведет к ошибке разрешения зависимостей. - Нарушение сигнатуры
__invoke– если роутер ожидает PSR-7 Request, а метод принимает массив, возникнет несоответствие типов. - Слишком много ответственности в одном действии – следует избегать выполнения нескольких операций, не связанных друг с другом.
Вариант 1: Отдельный файл-обработчик
Как выполнить простую обработку запроса без создания классов и автозагрузки?
// register.php
<?php
require 'bootstrap.php';
$userRepo = new UserRepository();
$mailer = new Mailer();
$data = json_decode(file_get_contents('php://input'), true);
$user = $userRepo->create($data);
$mailer->sendWelcomeEmail($user);
echo json_encode(['status' => 'ok']);
Request method php (метод http запроса в php)
Пояснение: Прямой скрипт подходит для очень простых проектов или прототипов. Все зависимости создаются внутри файла, что затрудняет тестирование и переиспользование. При росте проекта файлы становятся неуправляемыми.
Типичные ошибки:
- Отсутствие единого входа (
index.php) – каждый скрипт доступен напрямую, что создает риски безопасности. - Дублирование кода загрузки зависимостей.
Вариант 2: Единый обработчик через switch
Как обрабатывать несколько действий без использования маршрутизатора?
// index.php (упрощенный роутер)
$action = $_GET['action'] ?? 'default';
switch ($action) {
case 'register':
// логика регистрации
break;
case 'login':
// логика входа
break;
default:
// 404
}пример формы php (примеры html-форм с php)
Пояснение: Подходит для микросайтов с малым числом страниц. Быстро пишется, но при увеличении количества действий файл разрастается, теряется читаемость. Ошибка – легкая забыть break, что приводит к выполнению нескольких веток.
Типичная проблема: случайное попадание в неверную ветку из-за отсутствия break.
Вариант 3: Контроллер с методами (MVC)
Как сгруппировать связанные действия в одном классе?
class UserController {
public function register(Request $request): Response { ... }
public function login(Request $request): Response { ... }
}Actions php (действия (actions) в php)
Пояснение: Классический подход в MVC-фреймворках. Действия – это методы контроллера. Плюсы: группировка по сущностям. Минусы: контроллер часто обрастает методами, нарушается принцип единственной ответственности. Рекомендуется для средних проектов, но с осторожностью.
Типичные ошибки: перегруженность контроллера методами, не связанными напрямую, трудности с тестированием отдельных методов.
Вариант 4: Замыкания в роутере
Как задать обработчик маршрута прямо при определении роута?
$app->post('/register', function (Request $request) use ($container) {
$users = $container->get(UserRepository::class);
$mailer = $container->get(Mailer::class);
// ...
});Action php id (действие с параметром id в php)
Пояснение: Удобно для очень простых действий, не требующих повторного использования. Быстро пишется. Но замыкания сложно тестировать изолированно, и они не могут быть легко заменены или декорированы.
Типичные ошибки: захват внешних переменных без учета их изменяемости; невозможность подменить реализацию без изменения роутера.
Вариант 5: Анонимные классы
Как создать одноразовое действие без именованного класса?
$app->post('/register', new class {
public function __invoke(Request $request) {
// ...
}
});Пояснение: Компромисс между замыканием и полным классом. Позволяет определить метод, но без регистрации в DI. Подходит для временных обработчиков при миграции кода. Недостаток: отсутствие автозагрузки, невозможность внедрения зависимостей через контейнер.
Ошибка: забыть определить метод __invoke или другой метод, ожидаемый роутером.
Расширенные примеры работы с действиями в PHP
Invokable action с PSR-7 и DI-контейнером
// src/Action/CreateUserAction.php
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
class CreateUserAction {
public function __construct(
private UserService $userService,
private Validator $validator
) {}
public function __invoke(Request $request, Response $response, array $args): Response {
$data = $request->getParsedBody();
$errors = $this->validator->validate($data, [
'email' => 'required|email',
'password' => 'required|min:8'
]);
if ($errors) {
$response->getBody()->write(json_encode(['errors' => $errors]));
return $response->withStatus(422)->withHeader('Content-Type', 'application/json');
}
$user = $this->userService->register($data);
$response->getBody()->write(json_encode(['id' => $user->getId()]));
return $response->withStatus(201)->withHeader('Content-Type', 'application/json');
}
}// Загрузка в роутере (Slim 4)
$container->set(CreateUserAction::class, function ($c) {
return new CreateUserAction(
$c->get(UserService::class),
$c->get(Validator::class)
);
});
$app->post('/users', CreateUserAction::class);Пример ответа:
HTTP/1.1 201 Created
Content-Type: application/json
{"id":42}В этом примере действие получает PSR-7 запрос и ответ, выполняет валидацию и возвращает JSON. Контейнер явно настраивает создание действия, что позволяет легко подменять зависимости в тестах.
Действие как middleware (промежуточное ПО)
// src/Action/AuthMiddleware.php
class AuthMiddleware {
public function __construct(private TokenService $tokenService) {}
public function __invoke(Request $request, RequestHandler $handler): Response {
$token = $request->getHeaderLine('Authorization');
if (!$this->tokenService->isValid($token)) {
$response = new Response(401);
$response->getBody()->write('Unauthorized');
return $response;
}
return $handler->handle($request->withAttribute('user_id', $this->tokenService->getUserId($token)));
}
}Invokable класс может выступать в роли middleware, что позволяет строить цепочки обработки запроса. Это нестандартное использование, но в некоторых архитектурах (например, в Slim 4) middleware и action это одно и то же.
Действие в виде callable для фильтрации данных
$cleanData = array_filter($inputData, new class (2) {
private int $minLength;
public function __construct(int $minLength) {
$this->minLength = $minLength;
}
public function __invoke($item): bool {
return strlen($item) >= $this->minLength;
}
});Этот пример показывает, как invokable класс может быть использован не только в веб-обработке, но и в обычном коде для array_filter. Класс захватывает параметр $minLength и действует как предикат.
Действие как обработчик событий
// src/Listener/UserRegisteredListener.php
class UserRegisteredListener {
public function __construct(private Logger $logger) {}
public function __invoke(UserRegisteredEvent $event): void {
$this->logger->info('User registered: ' . $event->getUserId());
}
}В событийно-ориентированных системах (например, Symfony Event Dispatcher) обработчики событий часто реализуются как invokable объекты. Это дает возможность легко подключать логирование, уведомления и другие действия.