In a package, it is not possible to use services from another package
What is the new behavior
We can "connect" packages and then access services from another package
Does this PR introduce a breaking change?
No, it does not. It introduces a new functionality in a backward-compatible way.
Please check if the PR fulfills these requirements
[x] The commit message follows our guidelines
[x] Tests for the changes have been added (for bug fixes/features)
[x] Docs have been added/updated (for bug fixes/features)
More details
The issue
In Modularity, we currently have "isolated" packages, meaning that a package's modules can only access services of the same package. That makes easy to create "extensions" for a specific theme/plugin/library, but it makes very hard to have "reusable" services that can be used by several themes/plugins/libraries.
The proposed solution
This PR introduces a Package::connect() method, which accepts as argument another package.
In that method, we take the PSR-11 Container from the given package, and we "add" it to the "containers stack", in the same way we would add a container when instantiating the Package, via Package::new($properties, $container1, $containerN).
As a consequence, we need to call Package::connect()before calling Package::boot(): after that, the Package read-only container is already built, and we can't add more inner containers to it.
As additional consequence, in theory, we could connect a package only if that package is booted, otherwise we can't retrieve its read-only container to be added to the calling package's containers stack.
However, that would be too limitating. And to solve that, this PR introduce a PackageProxyContainer class: a PSR-11 container that holds an instance of Package, and resolve services by first resolving package's container, assuming it is booted.
It's nothing more than an implementation of the proxy pattern: it allows us to have an object resembling a PSR-11 container, before the subject PSR-11 container is available.
Example use case
The first use case, could be to have a library that provides services, from one or more modules, but does not really use it, but ony makes them available for other libraries/plugins/themes to use.
Let's assume a library like this:
namespace Acme;
use Inpsyde\Modularity;
function coolLibrary(): Modularity\Package {
static $package;
if (!$package) {
$properties = Modularity\Properties\LibraryProperties::new(__DIR__ . '/composer.json');
$package = Modularity\Package::new($properties);
$package->boot(new LibModuleOne(), new LibModuleTwo());
}
return $package;
}
The function would be available via Composer, but until it is called by some other code, it will do nothing.
But we might have several plugins that could do:
namespace Acme;
use Inpsyde\Modularity;
Modularity\Package::new(Modularity\Properties\PluginProperties::new(__FILE__))
->addModule(new PluginModuleOne())
->addModule(new PluginModuleTwo())
->connect(coolLibrary()) // <- note this
->boot();
Gotcha
In the example above, ->connect(coolLibrary()) is executed before boot and that is a requirement.
The package returned by coolLibrary() is also booted, because of how the coolLibrary() function is written.
Here we're "booting" the library only at wp hook, and not immediately.
Because the plugin calls ->connect(coolLibrary())beforewp hook, the library container will not be ready at that point. But that is fine, assuming plugin's modules take care of accessing library's services after that hook.
In fact, PluginModuleOne could look like this:
use Inpsyde\Modularity\Module;
use Psr\Container\ContainerInterface as Container;
class PluginModuleOne implements Module\ServiceModule, Module\ExecutableModule
{
public function services() : array
{
return [
'my_service' => fn(Container $c) => new PluginService($c->get('a_service_from_cool_library'));
];
}
public function run(Container $c): bool
{
return add_action('template_redirect', fn() => $c->get('my_service')->doSomething());
}
}
The plugin my_service is accessed on template_redirect. That causes a resolution in the container of a_service_from_cool_library which is provided by the connected library, and that works: template_redirect happens afterwp hook (when the library is booted), so at that point is possible to retrieve library services.
Basically, when you connect a package is your responsibility to call its services after the connected package boots.
A "special case" is when we want to use a theme's services from a plugin.
The theme will be booted after the plugin, but we need to connect the theme before the plugin boots.
The problem is we can't call the theme package, because it will not even be loaded yet when the plugin loads.
In that case, our plugin should probably looks like:
namespace Acme;
use Inpsyde\Modularity;
$plugin = Modularity\Package::new(Modularity\Properties\PluginProperties::new(__FILE__))
->addModule(new PluginModuleOne())
->addModule(new PluginModuleTwo());
add_action('after_setup_theme', function () use ($plugin) {
$plugin
->connect(coolTheme()) // <- note this
->boot();
});
Full list of changes
Development
Added Package::connect()
Added ACTION_PACKAGE_CONNECTED and ACTION_FAILED_CONNECTION actions
Added Package::connectedPackages() and Package::isPackageConnected() utility methods
Added PackageProxyContainer class
Unit tests
Added cases in PackageTest to test packages connection
Require WP_Error from real WordPress in tests (no need to mock)
Improve PHPUnit bootstrap: no need to require autoload when PHPUNIT_COMPOSER_INSTALL is defined
Psalm
Remove custom autoloader and use PHP Stubs instead
Update config for latest Psalm version
Fix minor Psalm issues in LibraryProperties
Misc
Restricted accepted range of dev-dependencies
Docs
Add documentation for package connection
We can have a <h1> in every file, and adjust headings hierarchy consequently
What kind of change does this PR introduce?
Feature (and related tests and docs update).
What is the current behavior?
In a package, it is not possible to use services from another package
What is the new behavior
We can "connect" packages and then access services from another package
Does this PR introduce a breaking change?
No, it does not. It introduces a new functionality in a backward-compatible way.
Please check if the PR fulfills these requirements
More details
The issue
In Modularity, we currently have "isolated" packages, meaning that a package's modules can only access services of the same package. That makes easy to create "extensions" for a specific theme/plugin/library, but it makes very hard to have "reusable" services that can be used by several themes/plugins/libraries.
The proposed solution
This PR introduces a
Package::connect()
method, which accepts as argument another package.In that method, we take the PSR-11 Container from the given package, and we "add" it to the "containers stack", in the same way we would add a container when instantiating the
Package
, viaPackage::new($properties, $container1, $containerN)
.As a consequence, we need to call
Package::connect()
before callingPackage::boot()
: after that, the Package read-only container is already built, and we can't add more inner containers to it.As additional consequence, in theory, we could connect a package only if that package is booted, otherwise we can't retrieve its read-only container to be added to the calling package's containers stack.
However, that would be too limitating. And to solve that, this PR introduce a
PackageProxyContainer
class: a PSR-11 container that holds an instance ofPackage
, and resolve services by first resolving package's container, assuming it is booted.It's nothing more than an implementation of the proxy pattern: it allows us to have an object resembling a PSR-11 container, before the subject PSR-11 container is available.
Example use case
The first use case, could be to have a library that provides services, from one or more modules, but does not really use it, but ony makes them available for other libraries/plugins/themes to use.
Let's assume a library like this:
The function would be available via Composer, but until it is called by some other code, it will do nothing.
But we might have several plugins that could do:
Gotcha
In the example above,
->connect(coolLibrary())
is executed before boot and that is a requirement. The package returned bycoolLibrary()
is also booted, because of how thecoolLibrary()
function is written.But we could have written it like this:
Here we're "booting" the library only at
wp
hook, and not immediately.Because the plugin calls
->connect(coolLibrary())
beforewp
hook, the library container will not be ready at that point. But that is fine, assuming plugin's modules take care of accessing library's services after that hook.In fact,
PluginModuleOne
could look like this:The plugin
my_service
is accessed ontemplate_redirect
. That causes a resolution in the container ofa_service_from_cool_library
which is provided by the connected library, and that works:template_redirect
happens afterwp
hook (when the library is booted), so at that point is possible to retrieve library services.Basically, when you connect a package is your responsibility to call its services after the connected package boots.
A "special case" is when we want to use a theme's services from a plugin.
The theme will be booted after the plugin, but we need to connect the theme before the plugin boots.
The problem is we can't call the theme package, because it will not even be loaded yet when the plugin loads.
In that case, our plugin should probably looks like:
Full list of changes
Development
Package::connect()
ACTION_PACKAGE_CONNECTED
andACTION_FAILED_CONNECTION
actionsPackage::connectedPackages()
andPackage::isPackageConnected()
utility methodsPackageProxyContainer
classUnit tests
PHPUNIT_COMPOSER_INSTALL
is definedPsalm
Misc
Docs
<h1>
in every file, and adjust headings hierarchy consequentlyMisc