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.