Skip to content

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

Home | Changelog

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=true

When 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:

EndpointToken typePurpose
POST /rest/auth/admin/loginbackendAdmin panel users
POST /rest/auth/customer/logincustomerStorefront 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/generateSpaToken

This 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:

  1. 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.
  2. 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.
  3. Domain Service: The controller calls into a Service class in the Domain layer (in src/Domains/). The service orchestrates the business logic. For data-related tasks, it builds a set of Specification objects based on the request parameters.
  4. Repository: The service uses a Repository to fetch data. It passes the array of Specification objects to the repository's match() or matchOne() methods.
  5. Data Fetching & Transformation:
    • The Repository applies the specifications to a query builder to fetch data from the database.
    • The raw data is hydrated into Entity objects.
    • The service passes the Entity or array of entities to a Resource or Collection class.
    • The Resource/Collection transforms the data into the final JSON structure, including any requested relations.
  6. 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 Response

Dependency 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 PageRepository requires a PageRepositoryConfigurator in its constructor, the container will automatically find and inject it.
  • Configuration: Service definitions are located in container.php files within each domain (e.g., src/Domains/Cms/container.php).
  • Customization: The custom/ directory allows for client-specific overrides. By creating a container.php file in a matching custom/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. Builds Specification objects and calls the Repository.
  • Repository.php: Handles all data persistence. Extends BaseRepository and is injected with a RelationConfigurator.
  • *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 many Entity objects into the final API output.
  • Specification/: A directory containing domain-specific Specification classes 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.php

Query 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 RelationConfigurator and uses Specification objects to build and execute queries, returning hydrated Entity objects.
  • Example (PageRepository.php):
    php
    class 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 DiscoversRepository trait for its lazy-loading capabilities.
  • Lazy Loading: When an unloaded relation property is accessed (e.g., $article->author), the __get magic method in BaseEntity triggers, finds the correct repository, and calls its loadRelations method 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.