Skip to content

Architecture overview

Areas and modes


Application can run in different areas and modes.

The areas:

  • global
  • frontend
  • backend
  • cli

The modes:

  • dev
  • prod

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.

Request event flow


Click here to know more about events.

Request

Multiple event subscribers will be triggered by this event, like authentication, security firewall, content negotiation and more.

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

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

Method not allowed

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

No route

If no route was matched, this event is dispatched. If no replacement route is set by an event subscriber, an 404 error response is returned to the client.

Route

Subscribers of this event can set a replacement route and add route parameters. If the response was set, it is returned to the client and the request flow ends.

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

If a controller returned an object of class Amarant\Framework\Routing\Controller\ForwardResult, a replacement route will be resolved, its controller executed and the result saved to a response object.

Response

Subscribers of this event are meant to transform the response. For example if a controller returned a layout result, the subscriber transforms it to a response that can be returned to the client.

Exception

This event is dispatched when an exception is thrown during the request flow. Its subscribers can provide a different response for the client.

If the application is running in developer mode, the exception might be rethrown to be shown by the debugger / developer toolbar.

Response finalized

All responses are dispatched using this event in the end. Subscribers of this event usually set cookies and headers on the response object.

Response send before

This event is dispatched just before the response is sent. The profiler (if active) will stop 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.