Single Action Controller
Table of contents
Loading...Introduction
Single Action Controller means that each action class only serves one 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 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 callsRedirectHandler.php
for redirectsTemplateRenderer.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->encodeAndAddToResponse($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:
- 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 namedClientUpdateAction.php
. - 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 likeClientFetchAction.php
.
For actions that display a page, the wordPage
should be in the action name likeLoginPageAction.php
. - Suffix with "Action":
Action
at the end of the action names indicates that the class is an action. - 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