DI Container and related tools to be used at website level.
- What is and what is not
- Concepts overview
- Decisions
- Usage at website level
- Usage at package level
- Advanced topics
- Installation
- Crafted by Inpsyde
- License
- Contributing
This is a package aimed to solve dependency injection container, service providers, and application "bootstrapping", atapplication, i.e. website, level.
The typical use case is when building a website for a client, for which we foresee to write several "packages": library, plugins, and theme(s), that will be then "glued" together using Composer.
Thanks to this package will be possible to have a centralized dependency resolution, and a quite standardized and consistent structure for the backend of those packages.
Technically speaking, right now, there's nothing that prevents the use at package level, however for several reasons, that is a no-goal of this package and no code will be added here to comply with that.
This package was not written to be "just a standard", i.e. provide just the abstraction leaving the implementations to consumers, but instead had been written to be a ready-to-use implementation.
However, an underlying support forPSR-11allows for very flexible usage.
This is the central class of the package. It is the place where "application bootstrapping" happen, whereService Providers
are registered, and it is very likely the only object that need to be used from the website "package" (that one that "glues" other packages/plugins/themes via Composer).
The package provides a single service provider interface (plus several abstract classes that partially implement it). The objects are used to "compose" the Container. Moreover, in this package implementation, service providers are (or better, could be) responsible to tell how tousethe registered services. In WordPress world that very likely means to "add hooks".
This is a "storage" that is capable of storing, and retrieve objects by an unique identifier. On retrieval (oftentimes just the first time they are retrieved), objects are "resolved", meaning that any other object that is required for the target object to be constructed, will first recursively resolved in the container, and then injected in the target object before it is returned. The container implementation shipped here is an extension of Pimple, with added PSR-11 support, with the capability to act as a "proxy" to several other PSR-11 containers. Which means that Service Providers can "compose" the dependency tree in the Container either by directlyadding services factories to the underlying Pimple containeror they can "append" to the main container a ready-made PSR-11 container.
As stated above, this package targets websites development, and something that is going to be required at website level is configuration. Working with WordPress configuration often means PHP constants, but when using Composer at website level, in combination with, for example, WP Starter it also also means environment variables. The package ships anSiteConfig
interface with anEnvConfig
implementation that does nothing in the regard ofstoringconfiguration, but offers a very flexible way toreadconfiguration both from constants and env vars.
Container::config()
method returns and instance ofSiteConfig
.
Service providers job is to both add services in the container and add the hooks that make use of them, however in WordPress it often happens that services are required under a specific "context". For example, a service provider responsible to register and enqueue assets for the front-end is not required in backoffice (dashboard), nor in AJAX or REST requests, and so on. Using the proper hooks to execute code is something that can be often addressed, but often not. E.g. distinguish a REST request is not very easy at an early hook, or there's no function or constants that tell us when we are on a login page and so on. Moreover, even storing objects factories in the Container for things we aresureare not going to be used is waste of memory we can avoid. TheContext
class of this package is a centralized service that provides info on the current request.
Container::context()
method returns and instance of Context.
Because we wanted a ready-to-use package, we needed to pick a DI containerimplementation,and we went forPimple,for the very reason that it is one of the simplest implementation out there.
However, as shown later, anyone who want to use a different PSR-11 container will be very able to do so.
In the "Concepts Overview" above, the last two concepts ( "Env Config" and "Context" ) are not really something that are naturally coupled with the other three, however, the assumption that this package will be used for WordPresswebsitesallow us to introduce this "coupling" without many risks (or sense of guilt): assuming we would ship these packages separately, when building websites (which, again, is the only goal of this package) we will very likely going to require those separate packages anyway, making this the perfect example for theCommon-Reuse Principle:classes that tend to be reused together belong in the same package together.
The "website" package, that will glue together all packages, needs to only interact with theApp
class, in a very simple way:
<?php
namespaceAcmeInc;
add_action('muplugins_loaded',[\Inpsyde\App\App::new(),'boot']);
That's it. This code is assumed to be placed in MU plugin, but as better explained later, it is possible to do it outside any MU plugin or plugin, either wrapping theApp::boot()
call in a hook or not.
This one-liner will create the relevant objects and will fire actions that will enable other packages to register service providers.
By creating an instance ofApp
viaApp::new()
,it will take care of creating an instance ofEnvConfig
that will later returned when callingContainer::config()
.
EnvConfig
is an object that allows to retrieve information regarding current environment (e.g.production,staging,development...) and also to get settings stored as PHP constants or environment variables.
Information regarding running environment are auto-discovered from env variables supported byWP Starteror from configurations defined in well-known hosting like Automattic VIP or WP Engine.
There's a fallback in case no environment can be determined: ifWP_DEBUG
:is true,development
environment is assumed, otherwiseproduction
.
Inanycase, a filter:"wp-app-environment"
is available for customization of the determined environment.
Regarding PHP constants,EnvConfig
is capable to search for constants defined in the root namespace, but also inside other namespaces.
For the latter case, the class has to configured to let it know which "alternative" namespaces are supported.
That can be done by creating an instance ofContainer
that uses a customEnvConfig
instance, and then pass it toApp::new()
.For example:
<?php
namespaceAcmeInc;
useInpsyde\App;
$container=newApp\Container(newApp\EnvConfig('AcmeInc\Config','AcmeInc'));
App\App::new($container)->boot();
With the code in the above snippet, the createdEnvConfig
instance (that will be available viaContainer::config()
method) can return settings inAcmeInc\Config
orAcmeInc
namespaces (besides root namespace).
For example, if some configuration file contains:
<?php
define('AcmeInc\Config\ONE',1);
define('AcmeInc\TWO',2);
it will be possible to do:
<?php
/** @var Inpsyde\App\Container $container */
$container->config()->get('ONE');// 1
$container->config()->get('TWO');// 2
Note thatEnvConfig::get()
accepts an optional second$default
parameter to be returned in case no constant and no matching environment variable is set for given name:
<?php
/** @var Inpsyde\App\Container $container */
$container->config()->get('SOMETHING_NOT_DEFINED',3);// 3
EnvConfig::hosting()
returns the current Hosting provider. Currently we're automatically detecting following:
EnvConfig::HOSTING_VIP
- WordPress VIP GoEnvConfig::HOSTING_WPE
- WP EngineEnvConfig::HOSTING_SPACES
- Mittwald SpacesEnvConfig::HOSTING_OTHER
- If none of those above is detected
Custom hosting can be setup via aHOSTING
env variable or constant.
To check in code which is the current solution, there's aEnvConfig::hostingIs()
method that
accepts an hosting name string and returns true when the given hosting matches the current hosting.
EnvConfig::locations()
returns an instance ofInpsyde\App\Location\Locations
which allows to
resolve following directories and URLs:
- mu-plugins
- plugins
- themes
- languages
- vendor
On VIP Go (HOSTING
value will beEnvConfig::HOSTING_VIP
), additional locations can be obtained:
- private
- config
- vip-config
- images
In fact,Locations
is an interface, and currently there are three implementation of it, one for
"generic" hosting, one for VIP Go and one for WP Engine.
An example:
/** @var Inpsyde\App\EnvConfig $envConfig */
$location=$envConfig->locations();
$vendorPath=$location->vendorDir();// vendor directory path
$wonologPath=$location->vendorDir('inpsyde/wonolog');// specific package path
$pluginsUrl=$location->pluginsUrl();// plugins directory URL
$yoastSeoUrl=$location->pluginsUrl('/wordpress-seo/');// specific plugin URL
In case the package is not capable of discovering paths and URLs automatically (e.g. because a very custom setup)
they can be set by using aLOCATIONS
constant that is an an array with two top-level elements, one for
URLs and one for paths, each being a map in form of array with location name as keys and location
URL / path as value:
For example:
namespaceAwesomeWebsite\Config;
useInpsyde\App\Location\Locations;
useInpsyde\App\Location\LocationResolver;
constLOCATIONS= [
LocationResolver::URL=> [
Locations::VENDOR=>'http://example /wp/wp-content/composer/vendor/',
Locations::ROOT=>__DIR__,
Locations::CONTENT=>'http://content.example /',
],
LocationResolver::DIR=> [
Locations::VENDOR=>'/var/www/wp/wp-content/composer/vendor/',
Locations::ROOT=>dirname(__DIR__),
Locations::CONTENT=>'/var/www/content/',
],
];
As array key, besidesLocations::VENDOR
,Locations::ROOT
,andLocations::CONTENT
,it is also possible
to use any otherLocations
constant, e.g.Locations::MU_PLUGINS
orLocations::LANGUAGES
and so on.
The config provided is merged with defaults that can be fine-tuned depending on hosting.
Besides theLocations
constants, it is also possible to use custom keys, and retrieve them using
theLocations::resolveDir()
andLocations::resolveUrl()
methods.
For example:
namespaceAwesomeWebsite\Config;
useInpsyde\App\Location\LocationResolver;
constLOCATIONS= [
LocationResolver::DIR=> [
'logs'=>'/var/www/logs/',
],
];
and then:
/** @var Inpsyde\App\EnvConfig $envConfig */
/** @var Inpsyde\App\Location\Locations $locations */
$locations=$envConfig->locations();
echo$locations->resolveDir('logs','2019/10/08.log');
"/var/www/logs/2019/10/08.log"
In the example above, calling$locations->resolveUrl('logs')
will returnnull
because
no URL was set for the key'logs'
in theLOCATIONS
constant.
In the examples above, both default and custom locations are customized using theLOCATIONS
constant
that, for obvious reasons, can only be set in PHP configuration files.
For websites that rely on environment variables to set configuration, the package provides a different approach.
Environment variables in the formatWP_APP_{$location}_DIR
andWP_APP_{$location}_URL
can be used
to set location directories and URLs.
For example, vendor path can be set viaWP_APP_VENDOR_DIR
and vendor URL viaWP_APP_VENDOR_URL
,
just like root path can be set viaWP_APP_ROOT_DIR
and root URL viaWP_APP_ROOT_URL
.
This works also for custom paths.
For example, by setting environment variables like this:
WP_APP_VENDOR_DIR="/var/www/shared/vendor/"
WP_APP_LOGS_DIR="/var/www/logs/"
it is then possible to retrieve them like this:
/** @var Inpsyde\App\EnvConfig $envConfig */
/** @var Inpsyde\App\Location\Locations $locations */
$locations=$envConfig->locations();
echo$locations->vendorDir('inpsyde/wp-app-container');
"/var/www/shared/vendor/inpsyde/wp-app-container"
echo$locations->resolveDir('logs','2019/10');
"/var/www/logs/2019/10"
Please note that ifbothWP_APP_
* env variable and value inLOCATIONS
constant are set for the
same location, the env variable takes precedence.
At package level there are two ways to register services (will be shown later), but first providers need to be added to the App:
<?php
namespaceAcmeInc\Foo;
useInpsyde\App\App;
useInpsyde\WpContext;
add_action(
App::ACTION_ADD_PROVIDERS,
function(App$app) {
$app
->addProvider(newMainProvider(),WpContext::CORE)
->addProvider(newCronRestProvider(),WpContext::CRON,WpContext::REST)
->addProvider(newAdminProvider(),WpContext::BACKOFFICE);
}
);
The hookApp::ACTION_ADD_PROVIDERS
can actually more than once (more on this soon), but for now is relevant that even if the hook is fired more than once, theApp
class will be clever enough to add the provider only once.
As shown in the example above,App::addProvider()
,besides the service provider itself, accepts a variadic number of "Context" constants, that tell the App the given provider should be only used in the listed contexts.
The full list of possible constants is:
CORE
,which is basically means "always", or at least"if WordPress is loaded"FRONTOFFICE
BACKOFFICE
( "admin" requests, excluding AJAX request )AJAX
REST
CRON
LOGIN
CLI
(in the context of WP CLI)
BesidesApp::ACTION_ADD_PROVIDERS
there's another hook that packages can use to add service providers to the App. It is:App::ACTION_REGISTERED_PROVIDER
.
This hook is fired right after any provider is registered. Using this hook it is possible to register providers only if a given package is registered, allowing to ship libraries / plugins that will likely do nothing if other library / plugin are not available.
<?php
namespaceAcmeInc\Foo\Extension;
useInpsyde\App\App;
useInpsyde\WpContext;
useAcmeInc\Foo\MainProvider;
add_action(
App::ACTION_REGISTERED_PROVIDER,
function(string$providerId,App$app) {
if($providerId===MainProvider::class) {
$app->addProvider(newExtensionProvider(),WpContext::CORE);
}
},
10,
2
);
The just-registered package ID is passed as first argument by the hook. By default the package ID is the FQCN of the provider class, but that can be easily changed, so to be dependant on a package it is necessary to know the ID it uses.
One think important to note is thatApp::ACTION_REGISTERED_PROVIDER
hook is fired only if the target service providerregister()
method returnstrue
.If e.g. the provider is a "booted only" provider (more on this below) the hook will not be fired.
In that case it is possible to useApp::ACTION_ADDED_PROVIDER
hook, which works similarly and it is fired in the moment the provider isadded,so before registration is ever attempted.
As already stated multiple times, the scope of the library is to provide a common ground for service registration and bootstrapping of all packages that compose a website.
This means that it is necessary to allow generic libraries, MU plugins, plugins, and themes, to register their services, which means that, in theory, application should "wait" for all of those packages to be available. However, at same time, it is very possible that some packages will need to run at an early stage in the WordPress loading workflow.
To satisfy both these requirements, theApp
class runs its "bootstrapping procedure"from one to three times,depending on whenApp::boot()
is called for first time.
IfApp::boot()
is called first timebeforeplugins_loaded
hook, it will automatically called again atplugins_loaded
and again atinit
.For a total of 3 times.
IfApp::boot()
is called first timeafter (or during)plugins_loaded
,but beforeinit
it will automatically called again atinit
.For a total of 2 times.
IfApp::boot()
is called first timeduringinit
it will not be called again, so will run once in total.
IfApp::boot()
is called first timeafterinit
an exception will be thrown.
Each timeApp::boot()
is called, theApp::ACTION_ADD_PROVIDERS
action is fired allowing packages to add service providers.
Added service providersregister()
method, that add services in the container, is normallyimmediatelycalled, unless the just added service provider declares to support "delayed registration" (more on this soon).
Added service providersboot()
method, that makes use of the registered services, is normally delayed until last timeApp::boot()
is called (WP is atinit
hook), but service providers can declare to support "early booting" (more on this soon), in which case theirboot()
method is called after theregister
method, without waitingboot()
to be called for last time atinit
.
In the case a service provider supports bothdelayed registrationandearly booting,itsregister()
method will still be called before itsboot()
method, butafterhaving called theregister()
method of all non-delayed providers that are going to be booted in the sameboot()
cycle.
Considering the case in whichApp::boot()
is ran 3 times, (beforeplugins_loaded
,onplugins_loaded
,and oninit
) the order of events is the following:
-
Core is atbefore
plugins_loaded
- added service providerswithoutsupport fordelayed registrationare registered
- added service providerswithsupport fordelayed registrationand alsowithsupport forearly bootingare registered
- added service providerswithsupport forearly bootingare booted
-
Core isat
plugins_loaded
- added service providerswithoutsupport fordelayed registrationare registered
- added service providerswithsupport fordelayed registrationand alsowithsupport forearly bootingare registered
- added service providerswithsupport forearly bootingare booted
-
Core isat
init
- all added service providerswithoutsupport fordelayed registrationwhich are not registered yet, are registered
- all added service providerswithsupport fordelayed registrationwhich are not registered yet, are registered
- all added service providers which are not booted yet, are booted
To understand if a provider has support fordelayed registrationor forearly booting,we have to look at two methods of theServiceProvider
interface, respectivelyregisterLater()
andbootEarly()
,both returns a boolean.
TheServiceProvider
interface has a total of 5 methods.
Besides the two already mentioned there's also anid()
method, and then the two most relevant:register()
andboot()
.
The package ships several abstract classes that provides definitions for some of the methods. All of them as anid()
method that by default returns the name of the class (more on this soon) and define different combination ofregisterLater()
andbootEarly()
.Some of theme also register emptyboot()
orregister()
for provider that needs to, respectively, only register services or only bootstrap them.
Provider\Booted
is a provider that requires bothregister()
andboot()
methods to be implemented. It hasno support for delayed registrationandno support for early booting.Provider\BootedOnly
is a provider that requires onlyboot()
method to be implemented (register()
is implemented with no body). It hasno support for early booting.Provider\EarlyBooted
is a provider that requires bothregister()
andboot()
methods to be implemented. It hasno support for delayed registration,butsupports early booting.Provider\EarlyBootedOnly
is a provider that requires onlyboot()
method to be implemented (register()
is implemented with no body). Itsupports early booting.Provider\RegisteredLater
is a provider that requires bothregister()
andboot()
methods to be implemented. It hassupport for delayed registration,butno support for early booting.Provider\RegisteredLaterEarlyBooted
is a provider that requires bothregister()
andboot()
methods to be implemented. It has bothsupport for delayed registrationandfor early booting.Provider\RegisteredLaterOnly
is a providers that requires onlyregister()
method to be implemented (boot()
is implemented with no body). It hassupport for delayed registration.Provider\RegisteredOnly
is a providers that requires onlyregister()
method to be implemented (boot()
is implemented with no body). It hasno support for delayed registration.
By extending one of these classes, consumers can focus only on the methods that matter.
If the reason behind "normal"VS"early" booted providers has been already mentioned (some providersneedsto run early, but some other will not be available early) that's not the case for the "delayed registration" that providers can support.
To explain why this is a thing, let's do an example.
Let's assume aAcme Advanced Loggerplugin ships a service provider that registers anAcme\Logger
service.
Then, let's assume a separate pluginAcme Authenticationships a service provider that registers several other services that requireAcme\Logger
service.
TheAcme Authenticationservice provider will need to make sure that theAcme\Logger
service is available. One common strategy is tocheck the container for its availability,and in case of missing (e.g.Acme Advanced Loggerplugin is deactivated),Acme Authenticationregisters an alternative logger that could replace the missing service.
For that check for availability to be effective, it must be doneafterAcme Advanced Loggerservice provider has been registered. By supporting delayed registration,Acme Authenticationservice provider will surely be registered afterAcme Advanced Loggeris eventually registered (assuming that is not delayed as well) and so on itsregister
method can reliably check ifAcme\Logger
service is already available or not.
ServiceProvider
interfaceid()
method returns an identifier used in several places.
For example, as shown in the"Package-dependant registration"section above, it is passed as argument to theApp::ACTION_REGISTERED_PROVIDER
to allow packages to depend on other packages.
The service provider ID can also be passed to theContainer::hasProvider()
method to know if the given provider has been registered.
All the abstract service provider classes shipped with the package use a trait which, in order:
- checks for the existence of a
$id
public property in the class, and use it if so. - in case no
$id
public property, checks for the existence of a publicID
constant in the class, and use it if so. - if none of the previous apply, uses the class fully qualified name as ID.
So by extending one of the abstract classes and doing nothing else there's already an ID defined, which is the class name.
In case this is not fine for some reason, e.g. the same service provider class is used for several providers, it is possible to define the property, or just override theid()
method.
Note:Provider IDs must be unique. Trying to add a provider with an ID that was already used will just skip the addition, doing nothing else.
ServiceProvider::register()
is where providers add services to the Container, so that they will be available to be "consumed" in theServiceProvider::boot()
method.
ServiceProvider::register()
signature is the following:
publicfunctionregister(Container$container):void;
Receiving an instance of theContainer
service providers canaddthings to it in two ways:
- directly using Pimple
\ArrayAccess
method - using
Container::addContainer()
which accepts any PSR-11 compatible container and make all the services available in it accessible through the application Container
The container shipped with the package is a PSR-11 container with basic features foraddingservices that usePimplebehind the scenes.
Besides the two PSR-11 methods, the container has the methods:
Container::addService()
to add service factory callbacks by ID. Factories passed to this method will be called only once, and then every timeContainer::get()
is called, same instance is returned. UsesPimple\Container::offsetSet()
behind the scenes.Container::addFactory()
to add service factory callbacks by ID, but factories passed to this method will always be called whenContainer::get()
is called, returning a difference instance. UsesPimple\Container::factory()
behind the scenes.Container::extendService()
to add a callback that receives a service previously added to the container and the container and return a modified version of the same service. UsesPimple\Container::extend()
behind the scenes.
<?php
namespaceAcmeInc\Redirector;
useInpsyde\App\Container;
useInpsyde\App\Provider\Booted;
finalclassProviderextendsBooted{
privateconstCONFIG_KEY='REDIRECTOR_CONFIG';
publicfunctionregister(Container$container):bool
{
// class names are used as service ids...
$container->addService(
Config::class,
staticfunction(Container$container):Config{
returnConfig::load($container->config()->get(self::CONFIG_KEY));
}
);
$container->addService(
Redirector::class,
staticfunction(Container$container):Redirector{
returnnewRedirector($container->get(Config::class));
}
);
returntrue;
}
publicfunctionboot(Container$container):bool
{
returnadd_action(
'template_redirect',
staticfunction()use($container) {
/** @var AcmeInc\Redirector\Redirector $redirector */
$redirector=$container->get(Redirector::class);
$redirector->redirect();
}
);
}
}
In the following example I will usePHP-DI,but any PSR-11-compatible container will do.
<?php
namespaceAcmeInc\Redirector;
useInpsyde\App\Provider\Booted;
useInpsyde\App\Container;
finalclassProviderextendsBooted{
publicfunctionregister(Container$container):bool
{
$diBuilder=new\DI\ContainerBuilder();
if($container->config()->isProduction()) {
$cachePath=$container->config()->get('ACME_INC_CACHE_PATH');
$diBuilder->enableCompilation($cachePath);
}
$defsPath=$container->config()->get('ACME_INC_DEFS_PATH');
$diBuilder->addDefinitions("{$defsPath}/redirector/defs.php");
$container->addContainer($diBuilder->build());
returntrue;
}
publicfunctionboot(Container$container):bool
{
returnadd_action(
'template_redirect',
staticfunction()use($container) {
/** @var AcmeInc\Redirector\Redirector $redirector */
$redirector=$container->get(Redirector::class);
$redirector->redirect();
}
);
}
}
Please refer toPHP-DI documentationto better understand the code, but again, any PSR-11 compatible Container can be "pushed" to the library Container.
App::new()
returns an instance of theApp
so that it is possible to add providers on the spot, without having to hookApp::ACTION_ADD_PROVIDERS
.
This allow to immediately add service providers shipped at website level.
namespaceAcmeInc;
\Inpsyde\App\App::new()
->addProvider(newSomeWebsiteProvider())
->addProvider(newAnotherWebsiteProvider());
Often times, when using this package, there's need of creating a "package" that is no more than a "collection" of providers. Not being a plugin or MU plugin, such package will need to be "loaded" manually, because WordPress will not load it, and using autoload for the purpose is not really doable, because using a "file" autoload strategy, the file would be loaded too early, before WP environment is loaded.
The suggested way to deal with this issue is to "load" the package from the same MU plugin that bootstrap the application.
To ease this workflow, the package provides aServiceProviders
class, which resemble a collection of providers.
For example, let's assume we are creating a package to provide an authorization system to our application.
The reason why we will create a "library" and not a plugin is that there should be no way to "deactivate" it, being a core feature of the website, and also other plugins and libraries will require it as a dependency.
What we would doin the packageis to create a package class, that will implementInpsyde\App\Provider\Package
:an interface with a single method:Package::providers()
.
<?php
namespaceAcmeInc\Auth;
useInpsyde\App\Provider;
useInpsyde\WpContext;
classAuthimplementsProvider\Package
{
publicfunctionproviders():Provider\ServiceProviders
{
returnProvider\ServiceProviders::new()
->add(newCoreProvider(),WpContext::CORE)
->add(newAdminProvider(),WpContext::BACKOFFICE,WpContext::AJAX)
->add(newRestProvider(),WpContext::REST,WpContext::AJAX)
->add(newFrontProvider(),WpContext::FRONTOFFICE,WpContext::AJAX);
}
}
With such class in place (and autoloadable), in the MU plugin that bootstrap the application we could do:
<?php
namespaceAcmeInc;
\Inpsyde\App\App::new()->addPackage(newAuth\Auth());
In several places in this README has been said that the last timeApp::boot()
is called isinit
.
But reality is that is just the default, and even if this is fine in many cases, it is actually possible to useanyhook that runs afterplugins_loaded
for the last "cycle", just keep in mind that:
- using anything earlier that
after_setup_theme
means that themes will not be able to add providers. - using a late hook, the added providers
boot()
method will not be able to add hooks to anything that happen before the chosen hook, reducing a lot their possibilities
In any case, the way to customize the "last step" hook is to callApp::runLastBootAt()
method:
<?php
namespaceAcmeInc;
\Inpsyde\App\App::new()
->runLastBootAt('after_setup_theme')
->boot();
Please note thatApp::runLastBootAt()
must be calledbeforeApp::boot()
is called for first time, or an exception will be thrown.
Sometimes might be desirable to use a pre-built container to be used for the App. This for example allows for easier usage of a differentSiteConfig
instance (of whichEnvConfig
is an implementation) or adding an arbitrary PSR-11 containerbeforethe container is passed to Service Providers.
This is possible by passing a creating an instance ofApp\Container
,adding one (or more) PSR-11 container s to it (via theContainer::addContainer
method), then finally passing it toApp\App::new
.For example:
<?php
namespaceAcmeInc;
useInpsyde\App;
// An helper to create App on first call, then always access same instance
functionapp():App\App
{
static$app;
if(!$app) {
$env=newApp\EnvConfig(__NAMESPACE__.'\\Config',__NAMESPACE__);
// Build the App container using custom config class
$container=newApp\Container($env);
// Create PSR-11 container and push into the App container
$diBuilder=new\DI\ContainerBuilder();
$diBuilder->addDefinitions('./definitions.php');
$container->addContainer($diBuilder->build());
// Instantiate the app with the container
$app=App\App::new($container);
}
return$app;
}
// Finally create and bootstrap app
add_action('muplugins_loaded',[app(),'boot']);
App
class has a staticApp::make()
method that can be used to access objects from container outside any provider.
This can be used in plugins that just want to "quickly" access a service in the Container without writing a provider.
$someService=App::make(AcmeInc\SomeService::class);
Because the method is static, it needs to refer to a booted instance ofApp
.The one that will be used isthe firstApp
that is instantiatedduring a request.
Considering that the great majority of times there will be a single application, that is fine and convenient, because allows to resolve services in the container having no access to the container nor to theApp
instance.
IfApp::make()
is called before any App has been created at all, an exception will be thrown.
In the case, for any reason, more instances ofApp
are created, to resolve a service in a specificApp
instance it is necessary to have access to it and callresolve()
method on it.
Assuming the code in the previous section, where we defined theapp()
function, we could do something like this to resolve a service:
$someService=app()->resolve(AcmeInc\SomeService::class);
TheApp
class collects information on the added providers and their status whenWP_DEBUG
istrue
.
App::debugInfo()
,when debug is on, will return an array that could be something like this:
[
'status' => 'Done with themes'
'providers' => [
'AcmeInc\FooProvider' => 'Registered (Registered when registering early),
'AcmeInc\BarProvider' => 'Booted (Registered when registering early, Booted when booting early),
'AcmeInc\CliProvider' => 'Skipped (Skipped when registering plugins)',
'AcmeInc\LoremProvider' => 'Booted (Booted when booting plugins)',
'AcmeInc\IpsumProvider' => 'Booted (Registered when registering plugins, Booted when booting themes),
'AcmeInc\DolorProvider' => 'Booted (Registered when registering themes, Booted when booting themes),
'AcmeInc\SicProvider' => 'Registered (Registered when registering themes),
'AcmeInc\AmetProvider' => 'Booted (Registered with delay when registering themes, Booted when booting themes),
]
]
When debug is off,App::debugInfo()
returnsnull
.
To force enabling debug even ifWP_DEBUG
is false, it is possible to callApp::enableDebug()
.
It is also possible to force debug to be disabled, even ifWP_DEBUG
is true, viaApp::disableDebug()
.
<?php
namespaceAcmeInc;
\Inpsyde\App\App::new()->enableDebug();
The best way to use this package is through Composer:
$ composer require inpsyde/wp-app-container
This repository is a free software, and is released under the terms of the GNU General Public License version 2 or (at your option) any later version. SeeLICENSEfor complete license.
All feedback / bug reports / pull requests are welcome.
Before sending a PR make sure thatcomposer run qa
will output no errors.
It will run, in turn: