Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revision for 0.2 #16

Merged
merged 39 commits into from
Apr 10, 2020
Merged
Changes from 1 commit
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
80c673a
Discontinue support for PHP 5.x
XedinUnknown Nov 5, 2019
8963a33
Added basic Docker setup for PHP 7.0
XedinUnknown Nov 5, 2019
852b450
Removed NetBeans configs
XedinUnknown Nov 5, 2019
6fc1c2b
Added PHPStorm configs
XedinUnknown Nov 5, 2019
45334fc
Updated dependencies
XedinUnknown Nov 5, 2019
28d42b4
Ignoring .env file
XedinUnknown Nov 5, 2019
131f0c2
Removed unused classes
mecha Nov 6, 2019
43b5d99
Updated dependencies
mecha Nov 6, 2019
b921f9f
Set branch alias to 0.2.x
mecha Nov 6, 2019
506b7ca
Rewrote module interface using the 0.2 spec
mecha Nov 6, 2019
b698c0b
Updated IDE project files
mecha Nov 6, 2019
725d71b
Updated the interface description in the readme
mecha Nov 6, 2019
a51ea79
Updated changelog for 0.2.x changes
mecha Nov 6, 2019
2dc8da5
Simplified link in readme
XedinUnknown Nov 6, 2019
edd591c
Changed official requirement to PHP 7.0 in Composer
XedinUnknown Nov 6, 2019
8d58f19
Upgraded PHPUnit to version 6
XedinUnknown Nov 6, 2019
b0610e1
No longer depending on XPMock
XedinUnknown Nov 6, 2019
9e74c77
No longer depending on Dhii exceptions or stringable
XedinUnknown Nov 6, 2019
bcb6285
Fixed tests
XedinUnknown Nov 6, 2019
019b483
Merge branch 'develop' into 0.2.x
XedinUnknown Nov 6, 2019
718f246
Merge branch 'develop' into 0.2.x
XedinUnknown Nov 6, 2019
2b47fb3
Switched to PHP 7.0 compatible coding standard
XedinUnknown Nov 6, 2019
8a73334
Added return type hint to `setup()` method
mecha Nov 6, 2019
10e0d4a
Merge branch '0.2.x' of github.com:Dhii/module-interface into 0.2.x
mecha Nov 6, 2019
47d6543
Build on PHP 7.4 and disallow failure on it
XedinUnknown Mar 13, 2020
6998916
Update deps
XedinUnknown Mar 13, 2020
fcd6113
Remove unused deps and scripts
XedinUnknown Mar 13, 2020
5d7be66
Correct typo in version constraint
XedinUnknown Mar 15, 2020
d12074f
Requiring PSR container and SP packages
mecha Mar 15, 2020
e7fe9b5
Fix obsolete PHPUnit type
XedinUnknown Mar 15, 2020
e2b45e7
Merge remote-tracking branch 'origin/0.2.x' into 0.2.x
XedinUnknown Mar 15, 2020
8124a32
Switch to PHP 7.1
XedinUnknown Mar 15, 2020
02d0b92
Add Composer PHPStorm config
XedinUnknown Mar 15, 2020
77e2287
Add workspace config to separate changelist
XedinUnknown Mar 16, 2020
7ddcec2
Update Readme
XedinUnknown Mar 16, 2020
7ac40f6
Further improve readme
XedinUnknown Mar 16, 2020
9b6571c
Type improvements
XedinUnknown Mar 17, 2020
2236f95
Now testing if exception is throwable
XedinUnknown Mar 17, 2020
1abbf93
Merge remote-tracking branch 'origin/0.2.x' into 0.2.x
XedinUnknown Mar 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 272 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,287 @@
[![Code Climate](https://codeclimate.com/github/Dhii/module-interface/badges/gpa.svg)](https://codeclimate.com/github/Dhii/module-interface)
[![Test Coverage](https://codeclimate.com/github/Dhii/module-interface/badges/coverage.svg)](https://codeclimate.com/github/Dhii/module-interface/coverage)
[![Latest Stable Version](https://poser.pugx.org/dhii/module-interface/version)](https://packagist.org/packages/dhii/module-interface)
[![This package complies with Dhii standards](https://img.shields.io/badge/Dhii-Compliant-green.svg?style=flat-square)][Dhii]

## Details
This package contains interfaces that are useful in describing modules and their attributes and behaviour.

### Interfaces
- [`ModuleInterface`][] - The interface for a module. A module is an object that represents an
application fragment. Modules are prepared using `setup()`, which returns a `ServiceProviderInterface` instance that
the application may consume, and invoked using `run()`.

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

Officially supports at least up to php 7.4.x.

### Interfaces
- [`ModuleInterface`][] - The interface for a module. A module is an object that represents an
application fragment. Modules are prepared using `setup()`, which returns a `ServiceProviderInterface` instance that
the application may consume, and invoked using `run()`, consuming the application's DI container.
- [`ModuleAwareInterface`][] - Something that can have a module retrieved.
- [`ModuleExceptionInterface`][] - An exception thrown by a module.

### Usage
#### Module Package
In your module's pacakge, create a file that returns a module factory. This factory MUST return an instance
of `ModuleInterface` from this pacakge. By convention, this file has
the name `module.php`, and is located in the root directory. Below is a very basic example. In real life,
Comment on lines +25 to +27
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a factory function necessary? A consumer app just call the module's constructor, the class of which is autoloaded by Composer.

Copy link
Member

@XedinUnknown XedinUnknown Apr 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because you cannot and should not standardize the module's constructor, as every module can have their own requirements. But you can and should standardize a factory, in the sense that all module factories will receive the same parameters.

the service provider and the module will often have named classes of their own, and factories and extensions
will be located in `services.php` and `extensions.php` respectively, by convention.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol I forgot about this "convention". Are we keeping it? I don't like it, to be honest. It just adds makes a package feel inconsistent, with a mix of lowercase.php PHP scripts and PascalCase.php PSR-4 style class files.

I'd omit this from the readme. Module authors can get as creative as they want with their modules, but I wouldn't establish something like this as convention unless there's a good reason to.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Module authors can still get as creative as they want, because nobody is forcing them to follow the convention. These files can really be anywhere and be called anything, because only the module itself interacts with them. However, it makes it faster for other module users to understand the module.

A special case is module.php. It contains the module factory function. This convention is useful if an application wants to specify modules by name such as package name. For this to work, all module files have to have the same name and relative location.

I was recently asked by several people to describe how to use the module system, and that's what I did here, because people are confused about how to get started with it. I agree that this all convention and opinion. Ultimately, it belongs in some technical post written by one of us and shared in relevant dev channels. But for now, this is the centralized location which everyone will look at, and they will know how to gets started quickly.


```php
// module.php
use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Psr\Container\ContainerInterface;

return function () {
return new class () implements ModuleInterface {

/**
* Declares services of this module.
*
* @return ServiceProviderInterface The service provider with the factories and extensions of this module.
*/
public function setup() : ServiceProviderInterface
{
return new class () implements ServiceProviderInterface
{
/**
* Only the factory of the last module in load order is applied.
*
* @return array|callable[] A map of service names to service definitions.
*/
public function getFactories()
{
return [
// A factory always gets one parameter: the container.
'my_module/my_service' => function (ContainerInterface $c) {
// Create and return your service instance
return new MyService();
},
];
}

/**
* All extensions are always applied, in load order.
*
* @return array|callable[] A map of service names to extensions.
*/
public function getExtensions()
{
return [
// An extension gets an additional parameter:
// the value returned by the factory or the previously applied extensions.
'other_module/other_service' => function (
ContainerInterface $c,
OtherServiceInterface $previous
): OtherServiceInterface {
// Perhaps decorate $previous and return the decorator
return new MyDecorator($previous);
},
];
}
};
}

/**
* Consumes services of this and other modules.
*
* @param ContainerInterface $c A container with the services of all modules.
*/
public function run(ContainerInterface $c)
{
$myService = $c->get('my_module/my_service');
$myService->doSomething();
}
};
};
```

In the above example, the module declares a service `my_module/my_service`, and an extension
for the `other_module/other_service`, which may be found in another module. Note that by convention,
the service name contains the module name prefix, separated by forward slash `/`. It's possible
to further "nest" services by adding slash-separated "levels". In the future, some container
implementations will add benefits for modules that use this convention.

Applications would often need the ability to do something with the arbitrary set of
modules they require. In order for an application to be able to group all modules
together, declare the package type in your `composer.json` to be `dhii-mod` by convention.
Following this convention would allow all modules written by all authors to be treated
uniformly.

```json
{
"name": "me/my_module",
"type": "dhii-mod"
}
```

What's important here:

1. A module's `setup()` method should not cause side effects.

The setup method is intended for the modules to prepare for action. Modules should not actually
peform the actions during this method. The container is not available in this method, and therefore
the module cannot use any services, whether of itself or of other modules, in this method. Do not
try to make the module use its own services here.

2. Implement the correct interfaces.

A module MUST implement `ModuleInterface`. The module's `setup()` method MUST return `ServiceProviderInterface`.
Even though the [Service Provider][`container-interop/service-provider`] standard is experimental, and has been experimental for a long time, the
module standard relies heavily on it. If the module standard becomes ubiquitous, this could push
FIG to go forward with the Service Provider standard, hopefully making it into a PSR.

3. Observe conventions.

It is important that conventions outlined here are observed. Some are necessary for smooth operation of
modules and/or consuming applications. Some others may not make a difference right now, but could
add benefits in the future. Please observe these conventions to ensure an optimal experience
for yourself and for other users of the standard.

#### Consumer Package
##### Module Installation
The package that consumes modules, which is usually the application, would need to require the modules.
The below example uses the [`oomphinc/composer-installers-extender`][] lib to configure Composer
so that it installs all `dhii-mod` packages into the `modules` directory in the application root.
Packages `me/my_module` and `me/my_other_module` would therefore go into `modules/me/my_module` and
`modules/me/my_other_module` respectively.
XedinUnknown marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"name": "me/my_app",
"require": {
"me/my_module": "^0.1",
"me/my_other_module": "^0.1",
"oomphinc/composer-installers-extender": "^1.1"
},

"extra": {
"installer-types": ["dhii-mod"],
"installer-paths": {
"modules/{$vendor}/{$name}": ["type:dhii-mod"]
}
}
}
```

##### Module Loading
Once a module has been required, it must be loaded. Module files must be explicitly loaded by the
XedinUnknown marked this conversation as resolved.
Show resolved Hide resolved
application, because the application is what determines module load order. The load order is
the fundamental principle that allows modules to extend and override each other's services
in a simple and intuitive way:

1. Factories in modules that are loaded later will completely override factories of modules loaded earlier.

Ultimately, for each service, only one factory will be used: the one declared last. So if `my_other_module`
is loaded after `my_module`, and it declares a service `my_module/my_service`,
then it will override the `my_module/my_service` service declared by `my_module`.
In short: **last factory wins**.

2. Extensions in modules that are loaded later will be applied after extensions of modules loaded earlier.

Ultimately, extensions from _all_ modules will be applied on top of what is returned by the factory.
So if `my_other_module` declares an extension `other_module/other_service`, it will be applied after
the extension `other_module/other_service` declared by `my_module`.
In short: **later extensions extend previous extensions**.

Continuing from the examples above, if something in the application requests the service `other_module/other_service`
declared by `my_other_module`, this is what is going to happen:

1. The factory in `my_other_module` is invoked.
2. The extension in `my_module` is invoked, and receives the result of the above factory as `$previous`.
3. The extension in `my_other_module` is invoked, and receives the result of the above extension as `$previous`
4. The caller of `get('other_module/other_service')` receives the result of the above extension.

Thus, any module can override and/or extend services from any other module. Below is an example of what
an application's bootstrap code could look like. This example uses classes from [`dhii/containers`][].

```php
// bootstrap.php

use Dhii\Modular\Module\ModuleInterface;
use Interop\Container\ServiceProviderInterface;
use Dhii\Container\CompositeCachingServiceProvider;
use Dhii\Container\DelegatingContainer;
use Dhii\Container\CachingContainer;

(function ($file) {
$baseDir = dirname($file);
$modulesDir = "$baseDir/modules";

// Order is important!
$moduleNames = [
'me/my_module',
'me/my_other_module',
];

// Create and load all modules
/* @var $modules ModuleInterface[] */
$modules = [];
foreach ($moduleNames as $moduleName) {
$moduleFactory = require_once("$modulesDir/$moduleName/module.php");
$module = $moduleFactory();
$modules[$moduleName] = $module;
}

// Retrieve all modules' service providers
/* @var $providers ServiceProviderInterface[] */
$providers = [];
foreach ($modules as $module) {
$providers[] = $module->setup();
}

// Group all service providers into one
$provider = new CompositeCachingServiceProvider();
$container = new CachingContainer(new DelegatingContainer($provider, $parentContainer = null));

// Run all modules
foreach ($modules as $module) {
$module->run($container);
}
})(__FILE__);
```

The above will load, setup, and run modules `me/my_module` and `me/my_other_module`, in that order,
from the `modules` directory, provided that conventions have been followed by those modules.
What's important to note here:

1. First _all_ modules are set up, and then _all_ modules are run.

If you set up and run modules in the same step, it will not work, because the bootstrap
will not have the opportunity to configure the application's DI container with services
from all modules.

2. The `CompositeCachingServiceProvider` is what is responsible for resolving services correctly.

This relieves the application, as the process can seem complicated, and is quite re-usable.
The usage of this class is recommended.

3. The `DelegatingContainer` optionally accepts a parent container.

If your application is a module itself, and needs to be part of a larger application with its
own DI container, supply it as the 2nd parameter. This will ensure that services will always
be retrieved from the top-most container, regardless of where the definition is declared.

4. The `CachingContainer` ensures services are cached.

Effectively, this means that all services are singletons, i.e. there will only be one instance
of each service in the application. This is most commonly the desired behaviour. Without the
`CachingContainer`, i.e. with just the `DelegatingContainer`, service definitions will get
invoked every time `get()` is called, which is usually undesirable.

5. Conventions are important.

If modules did not place the `module.php` file into their root directories, the bootstrap
would not be able to load each module by just its package name. Modules which do not
follow that convention must have their `module.php` file loaded separately, which would
make the bootstrap code more complicated.


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

[`ModuleInterface`]: src/ModuleInterface.php
[`dhii/containers`]: https://packagist.org/packages/dhii/containers
[`oomphinc/composer-installers-extender`]: https://packagist.org/packages/oomphinc/composer-installers-extender
[`container-interop/service-provider`]: https://packagist.org/packages/container-interop/service-provider

[`ModuleInterface`]: src/ModuleInterface.php
[`ModuleAwareInterface`]: src/ModuleAwareInterface.php
[`ModuleExceptionInterface`]: src/Exception/ModuleExceptionInterface.php