Modules

Ember comes with a module system that allows for creating new dynamic pages & functionalities.

Ember follows the MVC design pattern implemented using Slimopen in new window and Laravelopen in new window 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-4open in new window 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_compareopen in new window 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_existsopen in new window 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 providersopen in new window.

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
    {
    }
}

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 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 listenersopen in new window provide a way to listen for changes to Ember's models and act on them.

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 filtersopen in new window can be used to generate properly formatted URLs for individual files and files compiled with Laravel Mixopen in new window, 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 seedingopen in new window 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

Factoriesopen in new window can be used for mock data generation.

WARNING

Factories require Fakeropen in new window 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 conventionsopen in new window, 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 filteropen in new window.

<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 cheatsheetopen in new window.

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 ?? ''}"
        );
    }
}