Действия (Actions) средствами PHP для обработки запросов

Раздел: Веб-разработка на 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 объекты. Это дает возможность легко подключать логирование, уведомления и другие действия.

Действия (actions) в PHP - comments

En
Actions php (php)