Skip to content

Commit 7ac40f6

Browse files
committed
Further improve readme
1 parent 7ddcec2 commit 7ac40f6

File tree

1 file changed

+272
-7
lines changed

1 file changed

+272
-7
lines changed

README.md

Lines changed: 272 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,287 @@
44
[![Code Climate](https://codeclimate.com/github/Dhii/module-interface/badges/gpa.svg)](https://codeclimate.com/github/Dhii/module-interface)
55
[![Test Coverage](https://codeclimate.com/github/Dhii/module-interface/badges/coverage.svg)](https://codeclimate.com/github/Dhii/module-interface/coverage)
66
[![Latest Stable Version](https://poser.pugx.org/dhii/module-interface/version)](https://packagist.org/packages/dhii/module-interface)
7-
[![This package complies with Dhii standards](https://img.shields.io/badge/Dhii-Compliant-green.svg?style=flat-square)][Dhii]
87

98
## Details
109
This package contains interfaces that are useful in describing modules and their attributes and behaviour.
11-
12-
### Interfaces
13-
- [`ModuleInterface`][] - The interface for a module. A module is an object that represents an
14-
application fragment. Modules are prepared using `setup()`, which returns a `ServiceProviderInterface` instance that
15-
the application may consume, and invoked using `run()`.
1610

1711
### Requirements
1812
- PHP: 7.1 and up, until 8.
1913

2014
Officially supports at least up to php 7.4.x.
2115

16+
### Interfaces
17+
- [`ModuleInterface`][] - The interface for a module. A module is an object that represents an
18+
application fragment. Modules are prepared using `setup()`, which returns a `ServiceProviderInterface` instance that
19+
the application may consume, and invoked using `run()`, consuming the application's DI container.
20+
- [`ModuleAwareInterface`][] - Something that can have a module retrieved.
21+
- [`ModuleExceptionInterface`][] - An exception thrown by a module.
22+
23+
### Usage
24+
#### Module Package
25+
In your module's pacakge, create a file that returns a module factory. This factory MUST return an instance
26+
of `ModuleInterface` from this pacakge. By convention, this file has
27+
the name `module.php`, and is located in the root directory. Below is a very basic example. In real life,
28+
the service provider and the module will often have named classes of their own, and factories and extensions
29+
will be located in `services.php` and `extensions.php` respectively, by convention.
30+
31+
```php
32+
// module.php
33+
use Dhii\Modular\Module\ModuleInterface;
34+
use Interop\Container\ServiceProviderInterface;
35+
use Psr\Container\ContainerInterface;
36+
37+
return function () {
38+
return new class () implements ModuleInterface {
39+
40+
/**
41+
* Declares services of this module.
42+
*
43+
* @return ServiceProviderInterface The service provider with the factories and extensions of this module.
44+
*/
45+
public function setup() : ServiceProviderInterface
46+
{
47+
return new class () implements ServiceProviderInterface
48+
{
49+
/**
50+
* Only the factory of the last module in load order is applied.
51+
*
52+
* @return array|callable[] A map of service names to service definitions.
53+
*/
54+
public function getFactories()
55+
{
56+
return [
57+
// A factory always gets one parameter: the container.
58+
'my_module/my_service' => function (ContainerInterface $c) {
59+
// Create and return your service instance
60+
return new MyService();
61+
},
62+
];
63+
}
64+
65+
/**
66+
* All extensions are always applied, in load order.
67+
*
68+
* @return array|callable[] A map of service names to extensions.
69+
*/
70+
public function getExtensions()
71+
{
72+
return [
73+
// An extension gets an additional parameter:
74+
// the value returned by the factory or the previously applied extensions.
75+
'other_module/other_service' => function (
76+
ContainerInterface $c,
77+
OtherServiceInterface $previous
78+
): OtherServiceInterface {
79+
// Perhaps decorate $previous and return the decorator
80+
return new MyDecorator($previous);
81+
},
82+
];
83+
}
84+
};
85+
}
86+
87+
/**
88+
* Consumes services of this and other modules.
89+
*
90+
* @param ContainerInterface $c A container with the services of all modules.
91+
*/
92+
public function run(ContainerInterface $c)
93+
{
94+
$myService = $c->get('my_module/my_service');
95+
$myService->doSomething();
96+
}
97+
};
98+
};
99+
```
100+
101+
In the above example, the module declares a service `my_module/my_service`, and an extension
102+
for the `other_module/other_service`, which may be found in another module. Note that by convention,
103+
the service name contains the module name prefix, separated by forward slash `/`. It's possible
104+
to further "nest" services by adding slash-separated "levels". In the future, some container
105+
implementations will add benefits for modules that use this convention.
106+
107+
Applications would often need the ability to do something with the arbitrary set of
108+
modules they require. In order for an application to be able to group all modules
109+
together, declare the package type in your `composer.json` to be `dhii-mod` by convention.
110+
Following this convention would allow all modules written by all authors to be treated
111+
uniformly.
112+
113+
```json
114+
{
115+
"name": "me/my_module",
116+
"type": "dhii-mod"
117+
}
118+
```
119+
120+
What's important here:
121+
122+
1. A module's `setup()` method should not cause side effects.
123+
124+
The setup method is intended for the modules to prepare for action. Modules should not actually
125+
peform the actions during this method. The container is not available in this method, and therefore
126+
the module cannot use any services, whether of itself or of other modules, in this method. Do not
127+
try to make the module use its own services here.
128+
129+
2. Implement the correct interfaces.
130+
131+
A module MUST implement `ModuleInterface`. The module's `setup()` method MUST return `ServiceProviderInterface`.
132+
Even though the [Service Provider][`container-interop/service-provider`] standard is experimental, and has been experimental for a long time, the
133+
module standard relies heavily on it. If the module standard becomes ubiquitous, this could push
134+
FIG to go forward with the Service Provider standard, hopefully making it into a PSR.
135+
136+
3. Observe conventions.
137+
138+
It is important that conventions outlined here are observed. Some are necessary for smooth operation of
139+
modules and/or consuming applications. Some others may not make a difference right now, but could
140+
add benefits in the future. Please observe these conventions to ensure an optimal experience
141+
for yourself and for other users of the standard.
142+
143+
#### Consumer Package
144+
##### Module Installation
145+
The package that consumes modules, which is usually the application, would need to require the modules.
146+
The below example uses the [`oomphinc/composer-installers-extender`][] lib to configure Composer
147+
so that it installs all `dhii-mod` packages into the `modules` directory in the application root.
148+
Packages `me/my_module` and `me/my_other_module` would therefore go into `modules/me/my_module` and
149+
`modules/me/my_other_module` respectively.
150+
151+
```json
152+
{
153+
"name": "me/my_app",
154+
"require": {
155+
"me/my_module": "^0.1",
156+
"me/my_other_module": "^0.1",
157+
"oomphinc/composer-installers-extender": "^1.1"
158+
},
159+
160+
"extra": {
161+
"installer-types": ["dhii-mod"],
162+
"installer-paths": {
163+
"modules/{$vendor}/{$name}": ["type:dhii-mod"]
164+
}
165+
}
166+
}
167+
```
168+
169+
##### Module Loading
170+
Once a module has been required, it must be loaded. Module files must be explicitly loaded by the
171+
application, because the application is what determines module load order. The load order is
172+
the fundamental principle that allows modules to extend and override each other's services
173+
in a simple and intuitive way:
174+
175+
1. Factories in modules that are loaded later will completely override factories of modules loaded earlier.
176+
177+
Ultimately, for each service, only one factory will be used: the one declared last. So if `my_other_module`
178+
is loaded after `my_module`, and it declares a service `my_module/my_service`,
179+
then it will override the `my_module/my_service` service declared by `my_module`.
180+
In short: **last factory wins**.
181+
182+
2. Extensions in modules that are loaded later will be applied after extensions of modules loaded earlier.
183+
184+
Ultimately, extensions from _all_ modules will be applied on top of what is returned by the factory.
185+
So if `my_other_module` declares an extension `other_module/other_service`, it will be applied after
186+
the extension `other_module/other_service` declared by `my_module`.
187+
In short: **later extensions extend previous extensions**.
188+
189+
Continuing from the examples above, if something in the application requests the service `other_module/other_service`
190+
declared by `my_other_module`, this is what is going to happen:
191+
192+
1. The factory in `my_other_module` is invoked.
193+
2. The extension in `my_module` is invoked, and receives the result of the above factory as `$previous`.
194+
3. The extension in `my_other_module` is invoked, and receives the result of the above extension as `$previous`
195+
4. The caller of `get('other_module/other_service')` receives the result of the above extension.
196+
197+
Thus, any module can override and/or extend services from any other module. Below is an example of what
198+
an application's bootstrap code could look like. This example uses classes from [`dhii/containers`][].
199+
200+
```php
201+
// bootstrap.php
202+
203+
use Dhii\Modular\Module\ModuleInterface;
204+
use Interop\Container\ServiceProviderInterface;
205+
use Dhii\Container\CompositeCachingServiceProvider;
206+
use Dhii\Container\DelegatingContainer;
207+
use Dhii\Container\CachingContainer;
208+
209+
(function ($file) {
210+
$baseDir = dirname($file);
211+
$modulesDir = "$baseDir/modules";
212+
213+
// Order is important!
214+
$moduleNames = [
215+
'me/my_module',
216+
'me/my_other_module',
217+
];
218+
219+
// Create and load all modules
220+
/* @var $modules ModuleInterface[] */
221+
$modules = [];
222+
foreach ($moduleNames as $moduleName) {
223+
$moduleFactory = require_once("$modulesDir/$moduleName/module.php");
224+
$module = $moduleFactory();
225+
$modules[$moduleName] = $module;
226+
}
227+
228+
// Retrieve all modules' service providers
229+
/* @var $providers ServiceProviderInterface[] */
230+
$providers = [];
231+
foreach ($modules as $module) {
232+
$providers[] = $module->setup();
233+
}
234+
235+
// Group all service providers into one
236+
$provider = new CompositeCachingServiceProvider();
237+
$container = new CachingContainer(new DelegatingContainer($provider, $parentContainer = null));
238+
239+
// Run all modules
240+
foreach ($modules as $module) {
241+
$module->run($container);
242+
}
243+
})(__FILE__);
244+
```
245+
246+
The above will load, setup, and run modules `me/my_module` and `me/my_other_module`, in that order,
247+
from the `modules` directory, provided that conventions have been followed by those modules.
248+
What's important to note here:
249+
250+
1. First _all_ modules are set up, and then _all_ modules are run.
251+
252+
If you set up and run modules in the same step, it will not work, because the bootstrap
253+
will not have the opportunity to configure the application's DI container with services
254+
from all modules.
255+
256+
2. The `CompositeCachingServiceProvider` is what is responsible for resolving services correctly.
257+
258+
This relieves the application, as the process can seem complicated, and is quite re-usable.
259+
The usage of this class is recommended.
260+
261+
3. The `DelegatingContainer` optionally accepts a parent container.
262+
263+
If your application is a module itself, and needs to be part of a larger application with its
264+
own DI container, supply it as the 2nd parameter. This will ensure that services will always
265+
be retrieved from the top-most container, regardless of where the definition is declared.
266+
267+
4. The `CachingContainer` ensures services are cached.
268+
269+
Effectively, this means that all services are singletons, i.e. there will only be one instance
270+
of each service in the application. This is most commonly the desired behaviour. Without the
271+
`CachingContainer`, i.e. with just the `DelegatingContainer`, service definitions will get
272+
invoked every time `get()` is called, which is usually undesirable.
273+
274+
5. Conventions are important.
275+
276+
If modules did not place the `module.php` file into their root directories, the bootstrap
277+
would not be able to load each module by just its package name. Modules which do not
278+
follow that convention must have their `module.php` file loaded separately, which would
279+
make the bootstrap code more complicated.
280+
22281

23282
[Dhii]: https://github.com/Dhii/dhii
24283

25-
[`ModuleInterface`]: src/ModuleInterface.php
284+
[`dhii/containers`]: https://packagist.org/packages/dhii/containers
285+
[`oomphinc/composer-installers-extender`]: https://packagist.org/packages/oomphinc/composer-installers-extender
286+
[`container-interop/service-provider`]: https://packagist.org/packages/container-interop/service-provider
287+
288+
[`ModuleInterface`]: src/ModuleInterface.php
289+
[`ModuleAwareInterface`]: src/ModuleAwareInterface.php
290+
[`ModuleExceptionInterface`]: src/Exception/ModuleExceptionInterface.php

0 commit comments

Comments
 (0)