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

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

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.