Architecture overview
Areas and modes
Application can run in different areas and modes.
The areas (constants on Amarant\Framework\Contract\Application\AreaInterface):
AreaInterface::AREA_GLOBALAreaInterface::AREA_FRONTENDAreaInterface::AREA_BACKENDAreaInterface::AREA_CLI
The modes (constants on Amarant\Framework\Contract\Application\ContextInterface):
ContextInterface::MODE_DEVContextInterface::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.
- The unique name of the module.
-
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.
-
Use semantic versioning here to set the current version of a module. If the method is not overridden, the default is 1.0.0.
- Module metadata. If method is not overridden, the default is an empty data object.
- 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.
- You can name this class however you want, as long as it respects the "PSR-4 autoloading standard".
- 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.
- 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.
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.