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.
$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:
$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:
$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:
$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:
<?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():
$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.
$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.
$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
routestable a module contributes to - Services — what a module's
serviceskey can hold - Events — the
name/action/priorityshape a module'seventskey takes - popphp README — the full module API surface