Pop PHP
The Basics

Modules

A module is like a mini-application object that registers itself into the main application. It accepts the same config array an application does — routes, services, events, middleware, an autoloader prefix — and pushes every one of those keys into the application's own managers. A module is therefore a unit of packaging, not a unit of isolation - it extends the main application. For example, its routes land in the same router and its services in the same locator as everything else.

What a Module Is#

Pop\Module\Module extends Pop\AbstractApplication, the same base your own application class extends, so a module carries a name, a version and a config array of its own. What it does not carry is a router, a service locator or an event manager — it borrows the application's, which is the whole point.

PHP
$app = new Pop\Application(new Pop\Router\Router(null, new Pop\Router\Match\Http()));

$app->register([
    'name'    => 'blog',
    'version' => '1.0.0',
    'routes'  => [
        '/blog[/]' => ['controller' => 'Blog\Http\Controller\IndexController', 'action' => 'index']
    ]
]);

$app->run();

After register() returns, /blog is a route on the application's router like any route the application's own config declared. The module object itself stays reachable by name:

PHP
$app = new Pop\Application();

$app->register(['name' => 'blog', 'version' => '1.0.0']);

$blog = $app->module('blog');

echo $blog->getName();      // blog
echo $blog->getVersion();   // 1.0.0
var_dump($app->isRegistered('blog'));   // true

module() returns null for a name that was never registered rather than throwing, so it pairs with a ?-> or an isRegistered() guard.

Namespace a module's route paths and service names under something the application will not use. A module's routes go into the same route table and its services into the same locator, and the module registers second, so a colliding pattern or name overwrites the application's.

Registration has to happen before run(), because run() matches against whatever is in the route table at that moment. A module registered afterward is filed with the manager and contributes nothing to the request that already went past. load() on the application class is the place for it.

register() is a module's bootstrap hook — the equivalent of load() on the application class described in Applications & Bootstrap.

Registering a Module#

Pop\Application::register() takes either a config array or an object implementing Pop\Module\ModuleInterface. Given an array it wraps it in a Pop\Module\Module for you, and the module's name comes from the config's name key:

PHP
$app = new Pop\Application();

$app->register(['name' => 'blog', 'routes' => []]);          // named by config
$app->register(new Pop\Module\Module('shop', '2.1.0', []));  // named by constructor

echo implode(', ', array_keys($app->modules()->getItems()));  // blog, shop

The Module constructor sorts its arguments by type rather than position, the way the application constructor does: an Application, a config array or ArrayAccess, a string matching a semantic version, and any other string as the name.

register() also takes an optional second $name argument, which names the module as it is filed. It behaves identically for a config array and for a module object — the array is wrapped in a Module first, and the name is applied to that wrapper before it registers itself:

PHP
$app = new Pop\Application();

$app->register(['routes' => []], 'shop');                   // config array + name argument
$app->register(new Pop\Module\Module('2.1.0', []), 'api');  // module object + name argument

echo implode(', ', array_keys($app->modules()->getItems()));  // shop, api

var_dump($app->isRegistered('shop'));       // true
echo $app->module('shop')->getName();       // shop

Name a module one way or the other: a name key in the config wins over the $name argument, since the module applies its own config metadata as part of registering.

Passing neither leaves a fallback name in play, and that fallback is worth knowing for its own sake. An unnamed Pop\Module\Module names itself after its own class, lowercased with backslashes replaced — pop_module_module — so registering two unnamed modules files the second one over the first. Every module wants a name.

The Module Config Array#

The keys a module reads are a subset of the application's, and each is handled by the same code the application uses for the equivalent key. Configuration covers them in full; this is what a module does with each one:

Key Effect when the module registers
name names the module — the key module() and isRegistered() take, and it overrides register()'s $name argument
version sets the version getVersion() returns
prefix, src registers a PSR-4 namespace with the application's autoloader
psr-0 set to true, registers prefix/src as PSR-0 instead
routes added to the application's router
services added to the application's service locator
events attached to the application's event manager
middleware appended to the application's middleware manager

A module's own config file returns that array, and looks like a smaller version of app/config/app.http.php:

PHPmodules/blog/config/module.php
<?php

return [
    'name'    => 'blog',
    'version' => '1.0.0',
    'prefix'  => 'Blog\\',
    'src'     => __DIR__ . '/../src',
    'routes'  => [
        '/blog[/]' => [
            'controller' => 'Blog\Http\Controller\IndexController',
            'action'     => 'index'
        ]
    ],
    'services' => [
        'posts' => ['call' => 'Blog\Model\Posts']
    ],
    'events' => [
        ['name' => 'app.dispatch.post', 'action' => 'Blog\Http\Event\Ping::send', 'priority' => 10]
    ]
];

The events entries take the same name/action/priority shape the application config uses — see Events — and services takes the same callable definitions as Services. Registering the module is then one line in load():

PHP
$app = new Pop\Application();

$app->register(include __DIR__ . '/../../modules/blog/config/module.php');

Autoloading a Module's Classes#

prefix and src exist because a module's source lives outside the application's own PSR-4 map. When the module registers, it calls addPsr4() on the application's autoloader with those two values, which is why the front controller captures the ClassLoader that vendor/autoload.php returns and hands it to the application.

PHP
$autoloader = include __DIR__ . '/../vendor/autoload.php';

$app = new Pop\Application($autoloader, new Pop\Router\Router(null, new Pop\Router\Match\Http()));

$app->register(include __DIR__ . '/../modules/blog/config/module.php');

var_dump(class_exists('Blog\Http\Controller\IndexController'));   // true

Two things make that registration a silent no-op: an application constructed without an autoloader, or a src path that does not exist. The module still registers and its routes still match, so check class_exists() on one of its controllers the first time you wire a module up.

The Module Manager#

$app->modules() returns the Pop\Module\Manager holding every registered module. It's a plain collection: countable, iterable, and addressable by name.

PHP
$app = new Pop\Application();

$app->register(['name' => 'blog', 'version' => '1.0.0']);
$app->register(['name' => 'shop', 'version' => '2.1.0']);

echo count($app->modules());          // 2

foreach ($app->modules() as $name => $module) {
    echo $name . ' ' . $module->getVersion() . PHP_EOL;
}

$app->unregister('shop');
var_dump($app->isRegistered('shop'));   // false

Guard with hasVersion() before reading getVersion() on modules you did not write — the property behind it has no value until a version key sets one.

unregister() removes the module from the manager and nothing else. Routes, services, events and middleware the module contributed were handed to the application's managers when it registered, and they stay there — unregistering is a way to drop the module object, not a way to undo its registration. In a normal request that never comes up, because the process ends at the end of the request; it matters in tests and in long-running workers.

Pop\Module\ModuleInterface is the contract register() accepts, so a module class that cannot extend Pop\Module\Module can implement setName(), getName(), application(), isRegistered() and register() directly and be registered the same way.

Modules can do more than this page covers — merging one application's state into another with mergeApplication(), and the psr-0 autoloading path — see the popphp README.

See Also#

  • Applications & Bootstrap — the application object a module registers itself into
  • Configuration — the config keys a module shares with the application
  • Routing — the routes table a module contributes to
  • Services — what a module's services key can hold
  • Events — the name/action/priority shape a module's events key takes
  • popphp README — the full module API surface