Modules
Ember comes with a module system that allows for creating new dynamic pages & functionalities.
Ember follows the MVC design pattern implemented using Slim and Laravel components.
Examples
There are a couple example modules available to demonstrate the basic structure & functionality of a module.
Module discovery
Ember loads modules from the modules
directory. Modules are registered based on a module.json
file at the root of each module.
modules/example/module.json
.
{
"name": "Example module",
"identifier": "example",
"version": "1.0.0",
"description": "An example module for Ember.",
"providers": [
"Modules\\Example\\Providers\\ExampleServiceProvider"
]
}
WARNING
The identifier must be in kebab-case.
The identifier is converted to PascalCase and prefixed with Modules\
to form a namespace prefix for PSR-4 autoloading. The base directory is the root directory of the module.
For example the identifier example-module
results in the namespace prefix Modules\ExampleModule
.
TIP
The conventional namespace prefix and base directory can be overridden by specifying an autoload
key in the module.json
file.
{
"autoload": {
"psr-4": {
"Modules\\Example\\": "src/"
}
}
}
Dependency constraints
Required versions of the core and other modules may be specified using the require
key in the module.json
file.
{
"require": {
"core": "^2.14",
"example": ">=1.0.0"
}
}
Available operators are those handled by version_compare
and the caret (^x.y.z
meaning >=x.y.z
and <x+1
).
WARNING
The constraints are not validated for whether a module was actually loaded, merely its presence.
The dependent module must handle missing dependencies gracefully, for example by using class_exists
in a guard clause.
Module structure
Modules are conventionally structured as follows.
modules/example
├── Controllers
│ └── ExampleController.php
├── database/migrations
│ ├── migrations.php
│ └── schema.sql
├── Providers
│ └── ExampleServiceProvider.php
├── public
├── resources
│ ├── lang
│ │ └── en.php
│ └── views
│ └── example.twig
├── module.json
└── routes.php
TIP
The structure is fixed only with regards to the module.json
file and the public
directory. The rest of the structure is overridable.
Service providers
Modules are bootstrapped using service providers.
The register
method is called when the module is initially registered with the service container. It should be used only for registering service container bindings.
The boot
method is called after the service providers for Ember and all modules have been registered. It can be used for calling methods on the service provider class and performing other bootstrapping tasks.
<?php
namespace Modules\Example\Providers;
use App\Providers\ModuleServiceProvider;
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->loadRoutesFrom($this->module->getPath('routes.php'));
$this->loadViewsFrom($this->module->getPath('resources/views'));
}
public function register(): void
{
}
}
Navbar links
Links can be added to the navigation bar using the navbarLinks
method.
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->navbarLinks([
[
'icon' => 'fas fa-box',
'name' => 'Example',
'url' => '/example',
'admin_dropdown' => false,
],
]);
}
}
Nested links
Nested links can be specified using the nested_links
key.
$this->navbarLinks([
[
'icon' => 'fas fa-box',
'name' => 'Example',
'nested_links' => [
[
'icon' => 'fas fa-box',
'name' => 'Example',
'url' => '/example',
]
],
],
]);
Events
Event listeners provide a way to listen for changes to Ember's models and act on them.
- Built-in events can be found in the
App\Events
namespace. - The example module contains an event listener.
Event listener mappings are specified in the service provider using either the $listen
property or the eventListeners
method.
class ExampleServiceProvider extends ModuleServiceProvider
{
protected $listen = [
\App\Events\StoreCreditSaving::class => [\Modules\Example\Listeners\StoreCreditSaving::class],
];
}
Assets
Module assets (JavaScript & CSS files) located relative to the module's public
directory can be loaded from the /modules/{identifier}
route by passing the file path to the f
URL parameter, for example /modules/example?f=/js/app.js
.
The moduleasset
and modulemix
Twig filters can be used to generate properly formatted URLs for individual files and files compiled with Laravel Mix, respectively. Required arguments are the module's identifier and the file's name.
<script src="{{ 'example'|modulemix('/js/app.js') }}"></script>
Database
Schema and migrations
Migration files are loaded relative to the path specified using the loadMigrationsFrom
method.
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->loadMigrationsFrom($this->module->getPath('database/migrations'));
}
}
- Database tables are created based on the module's schema in
schema.sql
, if present. - Migrations are located in
migrations.php
and are structured as follows:<?php return [ [ 'version' => '1.0.1', 'sql' => 'ALTER TABLE Foo ADD COLUMN Bar VARCHAR(20);' ] ];
The version number specified in module.json
is used for keeping track of migrations.
WARNING
Incremental migrations are not run for fresh installations. The schema must be kept up-to-date.
Seeding data
Database seeding can be used to initialize a production database or provide data for testing.
Seeders are registered using the developmentSeeders
and productionSeeders
methods.
use Modules\Example\Database\Seeders\DevelopmentSeeder;
use Modules\Example\Database\Seeders\ProductionSeeder;
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->developmentSeeders(DevelopmentSeeder::class);
$this->productionSeeders(ProductionSeeder::class);
}
}
Running seeders
TIP
Production seeders are run automatically after migrations.
The db:seed
command can be used to run development seeders for all modules.
Additionally, individual seeder classes can be run by using the --class
option.
php cli db:seed --class=App\\Database\\Seeders\\Development\\UserSeeder
php cli db:seed --class=Modules\\Example\\Database\\Seeders\\DevelopmentSeeder
Factories
Factories can be used for mock data generation.
WARNING
Factories require Faker to be installed using composer install --dev
. Factories can only be instantiated when running seeders from the CLI, that is, they must not be used in production seeders.
Factory discovery
Factories are instantiated as per the discovery rules specified in App\Providers\SeedingServiceProvider
.
Conventionally the fully qualified class name for the model must contain \\Models\\
. The corresponding factory FQCN must contain \\Database\\Factories\\
in place of \\Models\\
.
TIP
To override the discovery conventions, override the newFactory
method on the model class and define a model
property on the corresponding factory class.
Localization
Translation files define the base translations which can be modified from the localization settings.
Translation files are read from the path specified using the loadTranslationsFrom
method.
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->loadTranslationsFrom($this->module->getPath('resources/lang'));
}
}
Translation files are structured as follows:
resources/lang/en.php
<?php
return [
'category' => [
'key' => 'value'
]
];
Translations can be rendered in templates using the lang
Twig filter.
<p>{{ 'key'|lang }}</p>
The above results in the following output.
<p>value</p>
Placeholder parameters
Untranslated values – such as usernames – may be passed to the lang
filter as arguments to be dynamically substituted at runtime.
The following types are available:
%s
string.
Permissions
Role permissions can be registered using the permissions
method.
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->permissions([
'example_example' => [
'title' => 'Example',
'description' => 'Example permission.',
],
]);
}
}
Permissions can be checked for using the hasPermission
method of the App\Models\User
model.
WARNING
Permission keys should be prefixed with the module identifier to avoid collisions.
TIP
Permissions can be assigned to roles using the role manager.
Partial templates
The partials
method can be used to dynamically insert HTML into existing pages.
The method expects an array of arrays with the following keys:
- route: a string or an array of strings of route names
- template: a relative path to a Twig template
- xpath: an XPath selector used to specify where on the page the template should be rendered
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->partials([
[
'route' => ['profile', 'user'],
'template' => 'partials/_example_profile_card.twig',
'xpath' => '//div [base-card][2]',
],
]);
}
}
Instead of a Twig template, it's possible to specify HTML as a string or a callback which returns a string.
$this->partials([
[
'route' => ['profile', 'user'],
// 'html' => '<h1>Example</h1>',
'html' => static function(ContainerInterface $container): string {
$user = $container->get(User::class)->find(1);
return "<h1>{$user->name}</h1>";
},
'xpath' => '//div [base-card][2]',
'prepend' => true,
],
]);
TIP
For a quick reference of XPath selectors see the XPath cheatsheet.
Store / payment processing
The App\Services\Store
class is used for registering payment processors and processing payments.
See the built-in payment processor integration modules for reference.
Notifications
Dispatching
Notifications can be dispatched with the App\Models\User::notify
method. Accepted parameters are the notification type and an array containing arbitrary metadata (for resolvers).
$user->notify('notification_type', [
'metadata_key' => 'metadata_value',
]);
Translations
Notification messages utilize the translations described above.
The translation key is the notification type suffixed with _notification
. For example notification_type_notification
.
Resolvers
Resolvers enable the use of metadata when rendering notifications.
The message resolver should return an array of arguments to be passed to the translator.
The URL resolver should return a URL which the notification is associated with.
class ExampleServiceProvider extends ModuleServiceProvider
{
public function boot(): void
{
$this->notificationMessageResolver(
'notification_type',
static fn (?object $metadata) => [$metadata->metadata_key ?? '']
);
$this->notificationUrlResolver(
'notification_type',
static fn (?object $metadata) => "/example/{$metadata->metadata_key ?? ''}"
);
}
}