Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /guides/RestApiModules.md for this page in Markdown format</div>
REST API Modules Documentation
This document outlines the modern, decoupled architecture and conventions used for building REST API modules within the application.
updated at v4.93.5
Enabling the REST API
REST routes are disabled by default. Set the following in your .env to enable them:
APP_REST_API_ENABLED=trueWhen false (or absent), rest_routes.php is not included and all /rest/* endpoints return 404. This allows deploying the platform without exposing the REST layer until it is ready.
Authentication
The REST API uses JWT (JSON Web Tokens) for authentication. Tokens are issued by the Auth controllers and must be sent as a Bearer token in the Authorization header.
Two separate auth flows exist:
| Endpoint | Token type | Purpose |
|---|---|---|
POST /rest/auth/admin/login | backend | Admin panel users |
POST /rest/auth/customer/login | customer | Storefront customers |
Generating SPA Tokens
For SPA applications that cannot perform interactive login (headless frontends, kiosk apps, CI pipelines), a long-lived token can be generated via CLI:
bash
php cli.php utils/generateSpaTokenThis outputs an SPA_TOKEN=... line ready to paste into .env or config files. The token is valid for 10 years, uses the spa-bot identity with full backend (advisable) role, and is signed with the same key/algorithm as regular JWT tokens.
Security note: SPA tokens grant full admin access. Store them securely and rotate them if compromised. Client repos can override
Utils::generateSpaToken()to customize the identity, roles, or expiry.
High-level flow
The REST API follows a layered architecture based on Dependency Injection (DI) and established design patterns. The typical request lifecycle is as follows:
- Routing: An incoming request first hits the routing layer (
application/config/rest_routes.php), which maps the URI and HTTP method to a specific controller action. - Controller: A REST controller (in
src/Rest/) receives the request. It parses request parameters, calls the appropriate Domain Service, and formats the final JSON response using a Resource or Collection. - Domain Service: The controller calls into a
Serviceclass in the Domain layer (insrc/Domains/). The service orchestrates the business logic. For data-related tasks, it builds a set of Specification objects based on the request parameters. - Repository: The service uses a
Repositoryto fetch data. It passes the array ofSpecificationobjects to the repository'smatch()ormatchOne()methods. - Data Fetching & Transformation:
- The
Repositoryapplies the specifications to a query builder to fetch data from the database. - The raw data is hydrated into
Entityobjects. - The service passes the
Entityor array of entities to aResourceorCollectionclass. - The
Resource/Collectiontransforms the data into the final JSON structure, including any requested relations.
- The
- Response: The controller sends the structured data back to the client as a JSON response.
Request -> Route -> Controller -> Service -> Repository + Specifications -> Entity -> Resource/Collection -> JSON ResponseDependency Injection & Customization
The application is built on the Symfony DependencyInjection component. This is the key to the architecture's flexibility and allows for powerful client-specific customizations.
Core Concepts
- Service Container: The DI container manages the lifecycle of all major objects (Services, Repositories, Configurators, etc.).
- Autowiring: The container automatically injects dependencies based on constructor type-hints. For example, if a
PageRepositoryrequires aPageRepositoryConfiguratorin its constructor, the container will automatically find and inject it. - Configuration: Service definitions are located in
container.phpfiles within each domain (e.g.,src/Domains/Cms/container.php). - Customization: The
custom/directory allows for client-specific overrides. By creating acontainer.phpfile in a matchingcustom/Domains/...path, you can override service definitions, which is the primary mechanism for customization.
Example: The Configurator Pattern
To avoid hardcoding dependencies, we use the Configurator Pattern. Instead of a repository knowing how to build its own relationships, it is given a RelationConfigurator object that provides them. This makes the relationships pluggable.
src/Domains/Cms/container.php
php
// ...
// --- Page ---
// Register the service. The container will autowire its dependencies.
$services->set(\Advisable\Domains\Cms\Page\Service::class);
// Register the repository. The container will see it needs a PageRepositoryConfigurator
// and inject the service below.
$services->set(\Advisable\Domains\Cms\Page\Repository\PageRepository::class);
// Register the configurator that defines the Page repository's relations.
$services->set(\Advisable\Domains\Cms\Page\Repository\PageRepositoryConfigurator::class);
// Register the Mui repository and explicitly tell it to use the NullRelationConfigurator
// because it has no relationships of its own.
$services->set(\Advisable\Domains\Cms\Page\Repository\PageMuiRepository::class)
->arg('$configurator', new Reference(NullRelationConfigurator::class));Domain Layer Structure
The core business logic resides in the src/Domains directory. A typical module (e.g., Article) has the following components:
Service.php: Orchestrates business logic. BuildsSpecificationobjects and calls theRepository.Repository.php: Handles all data persistence. ExtendsBaseRepositoryand is injected with aRelationConfigurator.*RepositoryConfigurator.php: Defines the relationships for its corresponding repository.Entity.php: A simple DTO representing a single database record.Resource.php/Collection.php: Transforms one or manyEntityobjects into the final API output.Specification/: A directory containing domain-specificSpecificationclasses for complex queries.
Example Structure
src/Domains/
└── Cms/
└── Blog/
└── Article/
├── Repository/
│ ├── ArticleRepository.php
│ ├── ArticleRepositoryConfigurator.php
│ └── Specification/
│ └── FilterByCategorySlug.php
├── Service.php
├── ListRequest.php
├── Collection.php
├── Entity.php
└── Resource.phpQuery Parameters
The API supports a flexible query builder system via URL query parameters.
Relations (with)
Eager-load relationships using the with parameter. Nested and recursive relations are supported.
- Nested:
GET /rest/cms/blog/article/1?with=author,categories.translations - Recursive:
GET /rest/cms/page/1?with=children*(loads all descendants)
Filters (filter)
Filter data using the filter parameter. The ListRequest class defines which fields are filterable and their type (Exact, Partial, Between).
- Exact:
GET /rest/cms/page?filter[parent]=28 - Exact (Multiple):
GET /rest/cms/blog/article?filter[id]=1,2,5 - Partial (LIKE):
GET /rest/cms/blog/article?filter[title.en]=My First Post - Complex Relation:
GET /rest/cms/blog/article?filter[categorySlug.en]=news
Sorting (sort)
Order results using the sort parameter. Prefix with a hyphen (-) for descending order.
- Ascending:
GET /rest/cms/blog/article?sort=blog_date - Descending:
GET /rest/cms/blog/article?sort=-blog_date
Repository & Entity
Repository
The Repository is the data-mapper layer, extending BaseRepository. It is responsible for all database interaction for an entity.
- Purpose: To abstract all database query logic. It is injected with a
RelationConfiguratorand usesSpecificationobjects to build and execute queries, returning hydratedEntityobjects. - Example (
PageRepository.php):phpclass PageRepository extends BaseRepository { protected string $table = 'categories'; protected string $entity = Entity::class; public function __construct(PageRepositoryConfigurator $configurator) { parent::__construct($configurator); } }
Entity
The Entity class is a lightweight DTO extending BaseEntity.
- Purpose: To provide a structured representation of a database record. It now uses the
DiscoversRepositorytrait for its lazy-loading capabilities. - Lazy Loading: When an unloaded relation property is accessed (e.g.,
$article->author), the__getmagic method inBaseEntitytriggers, finds the correct repository, and calls itsloadRelationsmethod to populate the property.
Relations
Relationships are now defined in dedicated RelationConfigurator classes. This decouples the relationship logic from the repository, making it easy to override for specific clients.
Defining Relations
In a *RepositoryConfigurator.php file, the getRelations() method returns an array of Relation objects.
Example (ArticleRepositoryConfigurator.php):
php
class ArticleRepositoryConfigurator implements RelationConfigurator
{
public function getRelations(): array
{
return [
'author' => new Relation(Relation::BELONGS_TO, AuthorRepository::class, 'blog_author_id'),
'comments' => new Relation(Relation::ONE_TO_MANY, CommentRepository::class, 'blog_id'),
'tags' => new Relation(Relation::MANY_TO_MANY, TagRepository::class, 'blog_id', 'blog_blog_tags', 'blog_tag_id'),
];
}
}How to Customize a Module
This new architecture makes client-specific customizations clean and simple. The primary method is to override the RelationConfigurator for a given repository.
Goal: Add a custom page relationship to the Location entity.
1. Create a Custom Configurator
In the custom/ folder, create a new configurator that extends the default one and adds the new relation.
php
// custom/Domains/Map/Location/Repository/CustomLocationRepositoryConfigurator.php
namespace Custom\Domains\Map\Location\Repository;
use Advisable\Domains\Cms\Page\Repository\PageRepository;
use Advisable\Domains\Map\Location\Repository\LocationRepositoryConfigurator;
use Advisable\Domains\Support\Repository\Relation;
class CustomLocationRepositoryConfigurator extends LocationRepositoryConfigurator
{
public function getRelations(): array
{
// Get all the default relations from the parent
$relations = parent::getRelations();
// Add our new custom relation
$relations['page'] = new Relation(
Relation::BELONGS_TO,
PageRepository::class,
'page_id' // The custom foreign key on the 'location' table
);
return $relations;
}
}2. Override in the Custom DI Container
In custom/Domains/Map/container.php, tell the DI container to use your new configurator instead of the default one by creating an alias.
php
// custom/Domains/Map/container.php
use Custom\Domains\Map\Location\Repository\CustomLocationRepositoryConfigurator;
use Advisable\Domains\Map\Location\Repository\LocationRepositoryConfigurator;
// ...
// Register the custom configurator as a service.
$services->set(CustomLocationRepositoryConfigurator::class);
// Create an alias to override the default configurator with our custom one.
$services->alias(LocationRepositoryConfigurator::class, CustomLocationRepositoryConfigurator::class);That's it. The LocationRepository will now be automatically built with the new page relationship for this client, without ever touching the core source code. The new relation can be requested via ?with=page and will be available for lazy loading.