Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
015f512
[#.x] - removed transport repository replaced it smaller objects
niden Nov 7, 2025
5f51860
[#.x] - new payload class with helper methods
niden Nov 7, 2025
619108f
[#.x] - added user sanitizer for input
niden Nov 7, 2025
0026f43
[#.x] - mapper class to convert objects (domain/db etc.)
niden Nov 7, 2025
e79345d
[#.x] - added user validator (input)
niden Nov 7, 2025
cf62a20
[#.x] - added sanitizer interface
niden Nov 7, 2025
9a45dc9
[#.x] - added auth input and sanitizer
niden Nov 7, 2025
7d4ec11
[#.x] - new user domain object and tests
niden Nov 7, 2025
e01c835
[#.x] - added input class for users
niden Nov 7, 2025
40ee867
[#.x] - refactored middleware after domain transport object changes
niden Nov 7, 2025
a1d655a
[#.x] - refactored jwt token for new DTOs
niden Nov 7, 2025
e117370
[#.x] - added interface for user repository
niden Nov 7, 2025
cf80dbd
[#.x] - introduced abstract class - refactoring
niden Nov 7, 2025
baa077d
[#.x] - adjusted for new dependencies
niden Nov 7, 2025
61649a7
[#.x] - adjusted for new functionality
niden Nov 7, 2025
c5f465a
[#.x] - refactored for new DTOs
niden Nov 7, 2025
496fca0
[#.x] - refactored for new DTOs
niden Nov 7, 2025
38f7827
[#.x] - refactored for new DTOs
niden Nov 7, 2025
d54d8d6
[#.x] - removed old (kinda) dto
niden Nov 7, 2025
19efaf1
[#.x] - corrected references and new objects
niden Nov 7, 2025
791169d
[#.x] - adjusting signature
niden Nov 7, 2025
0499ee7
[#.x] - adjusting objects
niden Nov 7, 2025
a19d744
[#.x] - switched to interfaces
niden Nov 7, 2025
a9a6f11
[#.x] - changed toArray to return just the object as an array
niden Nov 7, 2025
8c61401
[#.x] - adjustments for payload and processPassword
niden Nov 7, 2025
9cf6bdc
[#.x] - refactored payload:notFound, adjusted return payload to inclu…
niden Nov 7, 2025
4442e70
[#.x] - phpstan corrections
niden Nov 7, 2025
cd2ddc9
[#.x] - phpcs
niden Nov 7, 2025
e2bd5ef
[#.x] - removed cache and query repository - major refactoring
niden Nov 8, 2025
82126b0
[#.x] - refactored input classes
niden Nov 8, 2025
9bb1581
[#.x] - adjusted sanitizers and introduced interface
niden Nov 8, 2025
2ca3e12
[#.x] - validator interface, validator classes for endpoints and tests
niden Nov 8, 2025
7806466
[#.x] - introducing token manager and token cache for token operation…
niden Nov 8, 2025
0e834e1
[#.x] - new cache constants
niden Nov 8, 2025
71c9d39
[#.x] - added validation results class
niden Nov 8, 2025
7932249
[#.x] - new mapper interface and adjustments
niden Nov 8, 2025
91d76f1
[#.x] - added auth and user facades for orchestration
niden Nov 8, 2025
6abfde8
[#.x] - correcting references and phpstan after refactoring
niden Nov 8, 2025
460db6b
[#.x] - correcting references and phpstan after refactoring
niden Nov 8, 2025
51b0371
[#.x] - adjusting tests
niden Nov 8, 2025
0246159
[#.x] - user repository cleanup and added interface
niden Nov 8, 2025
0db320a
[#.x] - new array shapes for phpstan
niden Nov 8, 2025
e87f2da
[#.x] - more registrations
niden Nov 8, 2025
ca09a44
[#.x] - phpcs
niden Nov 8, 2025
9a66223
[#.x] - adjusted the readme
niden Nov 8, 2025
37e2e82
[#.x] - added validator enums for different inputs
niden Nov 8, 2025
db39e93
[#.x] - added validator enum interface
niden Nov 8, 2025
133cb85
[#.x] - setting up cache folder for phpstan
niden Nov 8, 2025
d444bfb
[#.x] - separated validators for user insert and update
niden Nov 8, 2025
a12f378
[#.x] - refactored validators for auth to use the new format
niden Nov 8, 2025
34a894f
[#.x] - adjusted tests
niden Nov 8, 2025
fe8341f
[#.x] - new registrations
niden Nov 8, 2025
3023649
[#.x] - new absint validator
niden Nov 8, 2025
64e96cb
[#.x] - changed user put to use the absint validator
niden Nov 8, 2025
51e7ba4
[#.x] - phpstan corrections
niden Nov 8, 2025
bc3fc9a
[#.x] - moved Payload in ADR
niden Nov 9, 2025
2b92021
[#.x] - phpcs and refactoring the container
niden Nov 9, 2025
94ae56b
[#.x] - added evmanager to definitions and phpstan
niden Nov 9, 2025
721590a
[#.x] - renamed folders, moved files to logical positions
niden Nov 11, 2025
5ab3633
[#.x] - adjusted and added more tests
niden Nov 11, 2025
172ab90
[#.x] - adjusting references
niden Nov 11, 2025
c3d6571
[#.x] - updated readme
niden Nov 11, 2025
b92f934
[#.x] - changing class to final
niden Nov 13, 2025
80fe750
[#.x] - correcting payload references
niden Nov 13, 2025
5f1f742
[#.x] - switching sanitizers to use enums
niden Nov 13, 2025
4db5bee
[#.x] - refactoring and correcting validator references
niden Nov 13, 2025
4278501
[#.x] - fixing typo
niden Nov 13, 2025
ec8adc4
[#.x] - reformatting
niden Nov 13, 2025
e6922e2
[#.x] - fixing payload reference
niden Nov 13, 2025
0bc5f3d
[#.x] - phpcs
niden Nov 13, 2025
b8a7607
[#.x] - correcting test
niden Nov 13, 2025
e0bae45
[#.x] - minor changes for clarity
niden Nov 13, 2025
8c45d18
[#.x] - reworked payload for better results
niden Nov 13, 2025
c8ec7f3
[#.x] - phpcs
niden Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 150 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,41 @@
# REST API with Phalcon v6

A REST API developed with Phalcon v6
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.

## Introduction

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

## Directory Structure

The directory structure for this projects follows the recommendations of [pds/skeleton][pds_skeleton]

# Folders
The folders contain:

- `bin`: empty for now, we might use it later on
Expand All @@ -16,51 +47,147 @@ The folders contain:
- `storage`: various storage data such as application logs
- `tests`: tests

# Code organization
## High-level architecture

The application follows the [ADR pattern][adr_pattern] where the application is split into an `Action` layer, the `Domain` layer and a `Responder` layer.

## Action
The folder (under `src/`) contains the handler that is responsible for receiving data from the Domain and injecting it to the Responder so that the response can be generated. It also collects all the Input supplied by the user and injects it into the Domain for further processing.
- `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 a `Payload`.

## Domain
Core files live under `src/` and are registered in the DI container in `src/Domain/Components/Container.php`.

The Domain is organized in folders based on their use. The `Components` folder contains components that are essential to the operation of the application, while the `Services` folder contains classes that map to endpoints of the application
## Main components

### Container
### `Action` layer

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.
Contains a handler that translate HTTP requests into Domain calls. For example, actions route requests to `LoginPostService`, `LogoutPostService`, `RefreshPostService` etc.

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.
### `Domain` layer

#### `ADR`

- `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`

#### `Infrastructure`

##### `Constants`

Classes with constants and helper methods used throughout the application

##### `DataSource`

**`Auth`**

### Enums
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.

##### `Encryption`

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)

##### `Enums`

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).

### Middleware
##### `Env`

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.
The environment manager and adapters. It reads environment variables using [DotEnv][dotenv] as the main adapter but can be extended if necessary.

The middleware execution order is defined in the `RoutesEnum`. The available middleware is:
##### `Exceptions`

Exception classes used in the application.

##### `Container`

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.

#### `Services`:

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`.

### `Responder`

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.

#### Response payload

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 payload
- `code` - the application code returned
- `hash` - a `sha1` hash of the `data`, `errors` and timestamp
- `message` - `success` or `error`
- `timestamp` - the time in UTC format


## Request flow (example: login)

1. Route matches and middleware runs (see Middleware section below).
2. `Action` extracts request body and calls `LoginPostService->handle($data)`.
3. `LoginPostService` calls the `AuthFacade->authenticate($input, $loginValidator)` (method injection).
4. `AuthFacade`:
- Builds DTO via `AuthInput`.
- Calls the supplied validator (`AuthLoginValidator`) which returns a `Result`.
- On success, fetches user via repository and verifies credentials (`Security`).
- Issues tokens via `TokenManager`.
- Returns a `Payload::success(...)`.
5. `Responder` builds JSON and returns HTTP response.

## Validators

- [NotFoundMiddleware.php](src/Domain/Components/Middleware/NotFoundMiddleware.php)
- [HealthMiddleware.php](src/Domain/Components/Middleware/HealthMiddleware.php)
- [ValidateTokenClaimsMiddleware.php](src/Domain/Components/Middleware/ValidateTokenClaimsMiddleware.php)
- [ValidateTokenPresenceMiddleware.php](src/Domain/Components/Middleware/ValidateTokenPresenceMiddleware.php)
- [ValidateTokenRevokedMiddleware.php](src/Domain/Components/Middleware/ValidateTokenRevokedMiddleware.php)
- [ValidateTokenStructureMiddleware.php](src/Domain/Components/Middleware/ValidateTokenStructureMiddleware.php)
- [ValidateTokenUserMiddleware.php](src/Domain/Components/Middleware/ValidateTokenUserMiddleware.php)
- Specific validators exist for each potential input that needs to be validated
- Method injection is used for validators: the `AuthFacade` does not require a single validator in its constructor. Instead, callers pass the appropriate validator to each method: login uses `AuthLoginValidator`, logout/refresh use `AuthTokenValidator`.
- The validation `Result` supports `meta` data. Token validators may perform repository lookups and attach the resolved `User` to `ValidationResult->meta['user']` to avoid repeating DB queries. The facade reads that meta on success.

## Token management and cache

- `TokenManager` depends on a domain-specific `TokenCacheInterface` rather than a raw PSR cache. This keeps token-specific operations discoverable and testable.
- `TokenCache` enhances the Cache operations by providing token specific operations for storing and invalidating tokens.
- `TokenCacheInterface` defines token operations like `storeTokenInCache` and `invalidateForUser`.

## Middleware sequence

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](src/Domain/Infrastructure/Middleware/NotFoundMiddleware.php)
- [HealthMiddleware.php](src/Domain/Infrastructure/Middleware/HealthMiddleware.php)
- [ValidateTokenClaimsMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenClaimsMiddleware.php)
- [ValidateTokenPresenceMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenPresenceMiddleware.php)
- [ValidateTokenRevokedMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenRevokedMiddleware.php)
- [ValidateTokenStructureMiddleware.php](src/Domain/Infrastructure/Middleware/ValidateTokenStructureMiddleware.php)
- [ValidateTokenUserMiddleware.php](src/Domain/Infrastructure/Middleware/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
Expand All @@ -86,22 +213,6 @@ Checks all the claims of the JWT token to ensure that it validates. For instance
Checks if the token has been revoked. If it has, an error is returned


## Responder
The 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.

### Response payload

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 payload
- `code` - the application code returned
- `hash` - a `sha1` hash of the `data`, `errors` and timestamp
- `message` - `success` or `error`
- `timestamp` - the time in UTC format


[adr_pattern]: https://github.com/pmjones/adr
[pds_skeleton]: https://github.com/php-pds/skeleton
[dotenv]: https://github.com/vlucas/phpdotenv
1 change: 1 addition & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ parameters:
level: max
paths:
- src
tmpDir: tests/_output/
6 changes: 3 additions & 3 deletions public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

declare(strict_types=1);

use Phalcon\Api\Domain\Components\Container;
use Phalcon\Api\Domain\Components\Providers\ErrorHandlerProvider;
use Phalcon\Api\Domain\Components\Providers\RouterProvider;
use Phalcon\Api\Domain\Infrastructure\Container;
use Phalcon\Api\Domain\Infrastructure\Providers\ErrorHandlerProvider;
use Phalcon\Api\Domain\Infrastructure\Providers\RouterProvider;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Mvc\Micro;

Expand Down
6 changes: 3 additions & 3 deletions src/Action/ActionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
use Phalcon\Http\ResponseInterface;

/**
* @phpstan-import-type TLoginInput from InputTypes
* @phpstan-import-type TAuthLoginInput from InputTypes
* @phpstan-import-type TUserInput from InputTypes
*/
final readonly class ActionHandler implements ActionInterface
Expand All @@ -37,8 +37,8 @@ public function __construct(
public function __invoke(): void
{
$input = new Input();
/** @var TLoginInput|TUserInput $data */
$data = $input->__invoke($this->request);
/** @var TAuthLoginInput|TUserInput $data */
$data = $input->__invoke($this->request);

$this->responder->__invoke(
$this->response,
Expand Down
6 changes: 2 additions & 4 deletions src/Domain/ADR/DomainInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,14 @@

namespace Phalcon\Api\Domain\ADR;

use Phalcon\Domain\Payload;

/**
* @phpstan-import-type TLoginInput from InputTypes
* @phpstan-import-type TAuthLoginInput from InputTypes
* @phpstan-import-type TUserInput from InputTypes
*/
interface DomainInterface
{
/**
* @param TLoginInput|TUserInput $input
* @param TAuthLoginInput|TUserInput $input
*
* @return Payload
*/
Expand Down
49 changes: 7 additions & 42 deletions src/Domain/ADR/InputTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,18 @@
namespace Phalcon\Api\Domain\ADR;

/**
* @phpstan-type TLoginInput array{
* @phpstan-type TAuthInput TAuthLoginInput|TAuthLogoutInput
*
* @phpstan-type TAuthLoginInput array{
* email?: string,
* password?: string
* }
*
* @phpstan-type TLogoutInput array{
* @phpstan-type TAuthLogoutInput array{
* token?: string
* }
*
* @phpstan-type TRefreshInput array{
* @phpstan-type TAuthRefreshInput array{
* token?: string
* }
*
Expand All @@ -47,46 +49,9 @@
* updatedUserId?: int,
* }
*
* @phpstan-type TUserSanitizedInsertInput array{
* status: int,
* email: string,
* password: string,
* namePrefix: string,
* nameFirst: string,
* nameMiddle: string,
* nameLast: string,
* nameSuffix: string,
* issuer: string,
* tokenPassword: string,
* tokenId: string,
* preferences: string,
* createdDate: string,
* createdUserId: int,
* updatedDate: string,
* updatedUserId: int,
* }
*
* @phpstan-type TUserSanitizedUpdateInput array{
* id: int,
* status: int,
* email: string,
* password: string,
* namePrefix: string,
* nameFirst: string,
* nameMiddle: string,
* nameLast: string,
* nameSuffix: string,
* issuer: string,
* tokenPassword: string,
* tokenId: string,
* preferences: string,
* updatedDate: string,
* updatedUserId: int,
* }
*
* @phpstan-type TRequestQuery array<array-key, bool|int|string>
*
* @phpstan-type TValidationErrors array<int, array<int, string>>
* @phpstan-type TValidatorErrors array{}|array<int, array<int, string>>
* @phpstan-type TInputSanitize TUserInput|TAuthInput
*/
final class InputTypes
{
Expand Down
Loading
Loading