Skip to content

Single Action Controller

samuelgfeller edited this page Apr 2, 2024 · 10 revisions

Introduction

Single Action Controller means that each action class serves a specific purpose or functionality within the application unlike classic controllers that may comprise multiple use-cases.

When the application receives a request, routing directs the request to the action that should be invoked which then returns the response.

The reason actions are used over controllers is to be in line with the Single Responsibility Principle which states that a class should have only one reason to change and only a single responsibility.
This cannot be said of a controller, which is responsible for multiple actions.

Action

Simply said, the action receives the request, triggers business logic and returns a response.

It is responsible for extracting the data from the request such as GET or POST params if there are any, calling the Domain service with this optional data and returning the HTTP response.

All logic should be delegated to the domain layer.

Responder

The client may expect different types of responses depending on the request. For example, a page request may expect an HTML response, while an Ajax request a JSON response or after for instance a login request, a redirect may be expected.

To take this weight off the action, different responders take over the job of building the HTTP Response in the desired format.
The slim-example-project currently implements three responders:

  • JsonResponder.php for Ajax or API calls
  • RedirectHandler.php for redirects
  • TemplateRenderer.php for page requests where PHP-View templates are rendered

They can be injected into to the action and used to build the response like in the example below.

Action class example

All action classes have the same __invoke method with $request and $response parameters.

This is an example of the ClientCreateAction.php that should be invoked when a new client should be created after submitting the form values via an AJAX request:

<?php

namespace App\Application\Action\Client\Ajax;

use App\Application\Responder\JsonResponder;
use App\Domain\Client\Service\ClientCreator;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;

final readonly class ClientCreateAction
{
    public function __construct(
        private JsonResponder $jsonResponder,
        private ClientCreator $clientCreator,
    ) {
    }

    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        array $args
    ): ResponseInterface {
        // Extract POST values from the request
        $clientValues = (array)$request->getParsedBody();

        // Call domain service
        // Validation and Authorization exception caught in middlewares
        $insertId = $this->clientCreator->createClient($clientValues);
        
        // Return response built with the jsonResponder
        return $this->jsonResponder->respondWithJson($response, ['status' => 'success', 'data' => null], 201);
    }
}

Catching errors

Sometimes the domain can throw exceptions that require different responses. For example, if a user tries to log in but the account is locked, the action can catch this error and return a tailored response. Example can be found in the LoginSubmitAction.php.

For generic errors that may be thrown in different actions and always require a similar response, middlewares can be used to catch the errors and format the response accordingly (e.g. ValidationExceptionMiddleware.php or ForbiddenExceptionMiddleware).

Naming Action classes

Actions are organized into "Page" and "Ajax" folders based on whether they handle page requests or Ajax requests.

The structure looks like this:

├── Application
│   ├── Action
│   │    ├── Module       # (e.g. Client)
│   │    │   ├── Ajax       # Actions for ajax requests with a JSON response
│   │    │   ├── Page       # Actions that render pages
│   │    └── etc.

The following rules should be taken into consideration when naming action classes:

  1. Use Case Specific: The action name should clearly indicate the use case it handles. The name of the resource should be the first word and in singular form (this is to be able to find them quickly on a project wide search).
    For example, if an action handles updating a client, it could be named ClientUpdateAction.php.
  2. Request Type Specific: The action name should also indicate the type of request it handles.
    For example, for fetch requests, Fetch could be used in the action name like ClientFetchAction.php.
    For actions that display a page, the word Page should be in the action name like LoginPageAction.php.
  3. Suffix with "Action": Action at the end of the action names indicates that the class is an action.
  4. Prefix with "Api": Only for Api requests add Api at the beginning of the action name to indicate that the request is made from another application via this interface.

Based on these guidelines, here are some examples for different types of requests:

  • Show page actions: LoginPageAction.php, UserProfilePageAction.php
  • Fetch a collection of data: ClientFetchListAction.php, NoteFetchListAction.php
  • Read, get a specific set of data: ClientReadAction.php, UserReadAction.php
  • Submit/Process requests: LoginSubmitAction.php, PasswordForgottenSubmitEmailAction.php, NewPasswordResetSubmitAction.php, AccountUnlockProcessAction.php
  • Create requests: ClientCreateAction.php, UserCreateAction.php
  • Update requests: ClientUpdateAction.php, NoteUpdateAction.php
  • Delete requests: ClientDeleteAction.php, NoteDeleteAction.php
  • Api requests: ApiClientCreateAction.php

More on this topic

Clone this wiki locally