Skip to content

Architecture overview

Areas and modes


Application can run in different areas and modes.

The areas (constants on Amarant\Framework\Contract\Application\AreaInterface):

  • AreaInterface::AREA_GLOBAL
  • AreaInterface::AREA_FRONTEND
  • AreaInterface::AREA_BACKEND
  • AreaInterface::AREA_CLI

The modes (constants on Amarant\Framework\Contract\Application\ContextInterface):

  • ContextInterface::MODE_DEV
  • ContextInterface::MODE_PRODUCTION

Important

When referring to "frontend" area, we always mean the public facing part of the application, such as the main application or a frontend store.

On the other hand, when we refer to "backend" area, we always mean the application's back office.

Most of the application can be configured differently in each particular area.

For example, dependency injection could have been configured for an implementation globally
(in the global area), but it may have been configured differently in the frontend or backend area.

This allows us to completely change how the application works or is parametrized and which implementations are used and how they're configured in each particular area of the application.

Note

dev represents development mode while prod represents production mode.

Configuration is always inherited from the global area, if global configuration is defined.
Also, it's almost always explicit and defined using PHP, avoiding parsing of any kind of configuration files. The only exception to this are the .env files and the layout files.

The kernel


The kernel is the heart of the application.

On boot, it calls a set of loaders that use various application module configuration files to configure dependency injection, routing, security, messaging and more.

After booted, it exposes the newly configured container that can be used to load an application, like a CLI or http application.

Context

Kernel is run using a context object which defines the area and the mode of the application, the root directory, debug mode flag and a list of modules that should be loaded.

For example, Amarant CLI specifies the "cli" as its area, while http entry points in the "web/area" directories use "frontend" and "backend" areas, respectively.

Post load callbacks

Callbacks can be added to the kernel as closures. Just after the kernel is booted, it will run each closure in the order they were added in. The closures will receive a context object which allows them to access application context, container, metadata and the loaders that were used.

Important

Callbacks are meant to be used to set or replace an instance in the container during integration tests.

Http application


This application accepts a request context and returns a response that is sent to the client. It's run in the "frontend" and "backend" area http entry points.

To process a single request, multiple events will be dispatched to produce a response.

No PHP sessions

The application never uses PHP sessions. Identity is persisted entirely via HTTP-only cookies — a signed JWT cookie for authenticated users and a unique-ID cookie for guests. Per-identity storage (flash messages, locale, etc.) is backed by Redis and keyed on the identity ID. There is no session_start(), no $_SESSION, and no session file or session table anywhere in the stack. See Security — No PHP sessions for details.

Request event flow


Click here to know more about events. For the full API of each event (available getters/setters), see Request lifecycle events in the routing documentation.

All events are identified by constants on Amarant\Framework\Enum\EventEnum.

Request — EventEnum::APP_REQUEST

Multiple event subscribers are triggered here: authentication, security firewall, content negotiation and more.

Any subscriber can set a route or a response.
If a response was set, it is returned to the client and the request flow ends.

If no route was set, the router is called to match a route with the incoming request.

Method not allowed — EventEnum::APP_REQUEST_METHOD_NOT_ALLOWED

If a route was matched but the HTTP method was not allowed, this event is dispatched.
If no response is set by a subscriber, an error response is returned to the client.

No route — EventEnum::APP_REQUEST_NO_ROUTE

If no route was matched, this event is dispatched. Subscribers can supply a fallback route.
If neither a route nor a response is provided, a 404 error response is returned to the client.

Route — EventEnum::APP_REQUEST_ROUTE

Subscribers can replace the matched route, modify route parameters, or short-circuit with a response.
If a response was set, it is returned to the client and the request flow ends.

Otherwise, a controller instance, its method and parameters are resolved.
The controller method is executed and its result saved to a response object.

If the controller returned a Amarant\Framework\Routing\Controller\ForwardResult, the target route is resolved, its controller executed, and the result saved to a response object, all within the same request.

Response — EventEnum::APP_REQUEST_RESPONSE

Subscribers transform the response. For example, if a controller returned a layout result, a subscriber converts it to an HTTP response that can be sent to the client.

Exception — EventEnum::APP_REQUEST_EXCEPTION

Dispatched when an unhandled exception is thrown anywhere in the request flow. The exception has already been logged. Subscribers can inspect the exception and replace the error response.

In debug mode, the exception is re-thrown after this event to be shown by the developer toolbar, regardless of any subscriber response.

Response finalized before — EventEnum::APP_REQUEST_RESPONSE_FINALIZED_BEFORE

Dispatched just before the internal response object is converted to a Symfony Response. Subscribers can still replace the response at this point.

Response finalized — EventEnum::APP_REQUEST_RESPONSE_FINALIZED

Dispatched after the Symfony Response has been assembled. Subscribers of this event typically set cookies and headers on the response object.

Response send before — EventEnum::APP_REQUEST_RESPONSE_SEND_BEFORE

Dispatched as the last step before the response is returned to the web server. The profiler (if active) stops profiling the HTTP application just before this event is dispatched.

CLI application


The CLI application is a Symfony console application which is using console commands tagged with the cli.command tag.

Symfony events are dispatched using Amarant\Framework\Event\Adapter\SymfonyEventDispatcherAdapter which are wrapped in Amarant events.

Application modules


Modules are used to add core application, custom or 3rd party functionality to an application.

Each module can depend on another module. We call this the "module dependency chain". It affects the order of configuration loading, database migrations, layout loading, and more. It allows overriding other modules configuration, layouts, templates and more.

Modules can install / update data in the application using module update files.

Create a module


Click on the annotations to know more.

Vendor/ModuleName/Module.php
<?php

declare(strict_types=1);

namespace Vendor\ModuleName;

use Amarant\Framework\Contract\Application\AreaInterface;
use Amarant\Framework\Data\DataObject;
use Amarant\Framework\Module\AbstractModule;
use Override;

class Module extends AbstractModule
{
    #[Override] public function name(): string
    {
        return 'Vendor_ModuleName';   // (1)
    }

    #[Override] public function depends(): array
    {
        return ['Amarant_Ui', 'Amarant_Backoffice'];    // (2)
    }

    #[Override] public function version(): string
    {
        return '1.0.0';     // (3)
    }

    #[Override] public function defaults(): DataObject
    {
        return new DataObject(      // (4)
            [
                'key' => 'value',
            ]
        );
    }

    /**
     * @inheritDoc
     */
    #[Override] public function areas(): array
    {
        return [
            AreaInterface::AREA_FRONTEND,   // (5)
        ];
    }
}
  1. The unique name of the module.
  2. Depending on other modules makes the module run after all the modules it depends on.

    By running, we mean configuration loading, database migration execution, template path priority etc.

    If you need to override anything in another module, always make sure to depend on it.

  3. Use semantic versioning here to set the current version of a module. If the method is not overridden, the default is 1.0.0.

  4. Module metadata. If method is not overridden, the default is an empty data object.
  5. The area in which the module can run. If method is not overridden, the default is global, meaning the module runs in any area.

Important

The module file must be called Module.php and it has to be placed in the module's root directory.

It's highly recommended that the name returned from name method uses the following naming convention:
Camelcased text of two terms separated by an underscore, where the first term is the vendor name and the second term the module name.

Example: Vendor_ModuleName.

Module structure

├── Api
├── Configuration
│   └── Backend
│   └── Frontend
│   └── Di.php
├── Contract
├── Controller
├── Data
├── DataModel
├── DataTransformer
├── Enum
├── etc
│   └── backend
│   └── frontend
└── EventSubcriber
└── Form
└── Messaging
└── Migrations
└── Model
└── Resources
│   └── frontend
│       └── layout
│       └── template
│       └── web
│   └── backend
│       └── layout
│       └── template
│       └── web
└── Setup
│   └── Update
└── Test
│   └── Integration
│   └── Unit
└── composer.json
└── Module.php
Api Api extensions and filters.
Configuration Global and area-specific dependency injection configuration.
Contract Interfaces.
Controllers API and page controllers.
Data Data objects.
DataModel Database data models.
DataTransformer Data transformers.
Enum Enums.
etc Various application configuration, including defaults, routing, resources and more.
EventSubscriber Event subscribers.
Form Forms.
Messaging Messaging objects and handlers.
Migrations Database migrations.
Model Input / output data models.
Resources Layouts, templates, styles and JS/TS.
Setup Module update files.
Test Tests.

Create a module update file


Click on the annotations to know more.

Vendor/ModuleName/Setup/Update/InitSomethingInThisModule.php
<?php

declare(strict_types=1);

namespace Vendor\ModuleName\Setup\Update;

use Amarant\Framework\Contract\Module\Setup\ModuleUpdateInterface;
use Override;

// (1)
final class InitSomethingInThisModule implements ModuleUpdateInterface
{
    #[Override] public function update(): void
    {
        // do something here
    }

    #[Override] public static function getName(): string
    {
        // (2)
        return 'vendor_module_name_init_something_in_this_module';
    }

    /**
     * @inheritDoc
     */
    #[Override] public static function depends(): array
    {
        // (3)
        return [];
    }
}
  1. You can name this class however you want, as long as it respects the "PSR-4 autoloading standard".
  2. A unique name for an update across all application modules. It's recommended that the update name is prefixed with the vendor and module name, all in snake case and lowercase.
  3. Return a list of module update names that should be executed before this update. Currently unused.

Note

You can use dependency injection to inject whatever you need for this update script to perform.

To run the module updates, use the Amarant CLI.

php bin/ama module:update -vvv

Depending on the "module dependency chain", updates will run for each of the application modules. Each update will run only once. The module_updates table stores the names and the times the updates were executed at.

Additionally, the modules table is used to store all installed modules and their current version.

Important

Modules are stored into modules table only after the module:update command is executed.

Installing modules before running the application will be enforced in the future, meaning the application will not run and trigger an error if not all modules are installed.