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/"
}
}
}
Module structure
Modules are conventionally structured as follows.
modules/example
├── Controllers
│ └── ExampleController.php
├── database/migrations
│ ├── migrations.php
│ └── schema.sql
├── Providers
│ └── ExampleServiceProvider.php
├── public
├── resources
│ └── 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,
],
]);
}
}
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 either using the $listen
property or 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>
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) {
$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.