A REST API developed with Phalcon v6. This document explains how the project is organised, how the main components interact, and the important design decisions to keep in mind when extending the codebase.
Our goal is to build a REST API that has:
- Slim/efficient design
- Middleware
- JSON (or other) responses
- Action/Domain/Responder implementation
- JWT token authentication
This is not THE way to build a REST API with Phalcon. It is simply A way to do that. You can adopt this implementation if you wish, parts of it or none of it.
This application has evolved significantly with every video release. Several areas were implemented in one way and later refactored to demonstrate the design trade-offs and how earlier choices affect the codebase.
The main takeaways that we want to convey to developers are:
- The code has to be easy to read and understand
- Each component must do one thing and one thing only
- Components can be swapped out with others so the use of interfaces is essential
- Static analysis tools (PHPStan) must not produce errors
- Code coverage for tests must be at 100%
Part 1 https://youtu.be/f3wP_M_NFKc Part 2 https://youtu.be/VEZvUf_PdSY Part 3 https://youtu.be/LP64Doh0t4g Part 4 https://youtu.be/jCEZ2WMil8Q Part 5 https://youtu.be/syU_3cIXFMM Part 6 https://youtu.be/AgCbqW-leCM Part 7 https://youtu.be/tGV4pSyVLdI Part 8 https://youtu.be/GaJhNnw_1cE Part 9 https://youtu.be/CWofDyTdToI
The directory structure for this projects follows the recommendations of pds/skeleton
The folders contain:
bin: empty for now, we might use it later onconfig: .env configuration files for CI and exampledocs: documentation (TODO)public: entry point of the application whereindex.phplivesresources: stores database migrations and docker files for local developmentsrc: source code of the projectstorage: various storage data such as application logstests: tests
The application follows the ADR pattern where the application is split into an Action layer, the Domain layer and a Responder layer.
Action— receives HTTP input, collects and sanitizes request data, and calls a Domain service.Domain— contains the application logic. Implements small components, services that map to endpoints, validators, repositories and helpers.Responder— builds and emits the HTTP response from aPayload.
Core files live under src/ and are registered in the DI container in src/Domain/Components/Container.php.
Contains a handler that translate HTTP requests into Domain calls. For example, actions route requests to LoginPostService, LogoutPostService, RefreshPostService etc.
Payload: A uniform result object used across Domain → Responder.Input: Class collecting request input and used to pass it to the domain- Interfaces for domain and
Input
Classes with constants and helper methods used throughout the application
Auth
Contains Data Transfer Objects (DTOs) to move data from input to domain and from database back to domain. A Facade is available for orchestration, sanitizer for input as well as validators.
Interfaces
Mapper and Sanitizer interfaces
User
Contains Data Transfer Objects (DTOs) to move data from input to domain and from database back to domain. A Facade is available for orchestration, a repository for database operations, sanitizer for input as well as validators.
Validation
Contains the ValidatorInterface for all validators, a Result object for returning back validation results/errors and the AbsInt validator to check the id for Put operations.
Contains components for JWT handling and passwords. The Security component is a wrapper for the password_* PHP classes, which are used for password hashing and verification.
The TokenManager offers methods to issue, refresh and revoke tokens. It works in conjunction with the TokenCache to store or invalidate tokens stored in Cache (Redis)
There are several enumerations present in the application. Those help with common values for tasks. For example the FlagsEnum holds the values for the co_users.usr_status_flag field. We could certainly introduce a lookup table in the database for "status" and hold the values there, joining it to the co_users table with a lookup table. However, this will introduce an extra join in our query which will inevitable reduce performance. Since the FlagsEnum can keep the various statuses, we keep everything in code instead of the database. Thorough tests for enumerations ensure that if a change is made in the future, tests will fail, so that database integrity can be kept.
The RoutesEnum holds the various routes of the application. Every route is represented by a specific element in the enumeration and the relevant prefix/suffix are calculated for each endpoint. Also, each endpoint is mapped to a particular service, registered in the DI container, so that the action handler can invoke it when the route is matched.
Finally, the RoutesEnum also holds the middleware array, which defines their execution and the "hook" they will execute in (before/after).
The environment manager and adapters. It reads environment variables using DotEnv as the main adapter but can be extended if necessary.
Exception classes used in the application.
The application uses the Phalcon\Di\Di container with minimal components lazy loaded. Each non "core" component is also registered there (i.e. domain services, responder etc.) and all necessary dependencies are injected based on the service definitions.
Additionally there are two Providers that are also registered in the DI container for further functionality. The ErrorHandlerProvider which caters for the starting up/shut down of the application and error logging, and the very important RoutesProvider which handles registering all the routes that the application serves.
Separated also in User and Auth it contains the classes that the action handler will invoke. The naming of these services shows what endpoint they are targeting and what HTTP method will invoke them. For example the LoginPostService will be a POST to the /auth/login.
The JsonResponder responder is responsible for constructing the response with the desired output, and emitting it back to the caller. For the moment we have only implemented a JSON response with a specified array as the payload to be sent back.
The responder receives the outcome of the Domain, by means of a Payload object. The object contains all the data necessary to inject in the response.
The application responds always with a specific JSON payload. The payload contains the following nodes:
data- contains any data that are returned back (can be empty)errors- contains any errors occurred (can be empty)meta- array of information regarding the payloadcode- the application code returnedhash- asha1hash of thedata,errorsand timestampmessage-successorerrortimestamp- the time in UTC format
- Route matches and middleware runs (see Middleware section below).
Actionextracts request body and callsLoginPostService->handle($data).LoginPostServicecalls theAuthFacade->authenticate($input, $loginValidator)(method injection).AuthFacade:- Builds DTO via
AuthInput. - Calls the supplied validator (
AuthLoginValidator) which returns aResult. - On success, fetches user via repository and verifies credentials (
Security). - Issues tokens via
TokenManager. - Returns a
Payload::success(...).
- Builds DTO via
Responderbuilds JSON and returns HTTP response.
- Specific validators exist for each potential input that needs to be validated
- Method injection is used for validators: the
AuthFacadedoes not require a single validator in its constructor. Instead, callers pass the appropriate validator to each method: login usesAuthLoginValidator, logout/refresh useAuthTokenValidator. - The validation
Resultsupportsmetadata. Token validators may perform repository lookups and attach the resolvedUsertoValidationResult->meta['user']to avoid repeating DB queries. The facade reads that meta on success.
TokenManagerdepends on a domain-specificTokenCacheInterfacerather than a raw PSR cache. This keeps token-specific operations discoverable and testable.TokenCacheenhances the Cache operations by providing token specific operations for storing and invalidating tokens.TokenCacheInterfacedefines token operations likestoreTokenInCacheandinvalidateForUser.
There are several middleware registered for this application and they are being executed one after another (order matters) before the action is executed. As a result, the application will stop executing if an error occurs, or if certain validations fail. Middleware returns early with a Payload error when validation fails.
The middleware execution order is defined in the RoutesEnum. The available middleware is:
- NotFoundMiddleware.php
- HealthMiddleware.php
- ValidateTokenClaimsMiddleware.php
- ValidateTokenPresenceMiddleware.php
- ValidateTokenRevokedMiddleware.php
- ValidateTokenStructureMiddleware.php
- ValidateTokenUserMiddleware.php
NotFoundMiddleware
Checks if the route has been matched. If not, it will return a Resource Not Found payload
HealthMiddleware
Invoked when the /health endpoint is called and returns a OK payload
ValidateTokenPresenceMiddleware
Checks if a JWT token is present in the Authorization header. If not, an error is returned
ValidateTokenStructureMiddleware
Gets the JWT token and checks if it can be parsed. If not, an error is returned
ValidateTokenUserMiddleware
Gets the userId from the JWT token, along with other information, and tries to match it with a user in the database. If the user is not found, an error is returned
ValidateTokenClaimsMiddleware
Checks all the claims of the JWT token to ensure that it validates. For instance, this checks the token validity (expired, not before), the claims, etc. If a validation error happens, then an error is returned.
ValidateTokenRevokedMiddleware
Checks if the token has been revoked. If it has, an error is returned