Skip to content

DI Container and related tools to be used at website level.

License

Notifications You must be signed in to change notification settings

inpsyde/wp-app-container

Repository files navigation

WP App Container

DI Container and related tools to be used at website level.

PHP Quality Assurance


Table of Contents


What is and what is not

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.

Concepts overview

App

This is the central class of the package. It is the place where "application bootstrapping" happen, whereService Providersare 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).

Service provider

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".

Container

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.

Env config

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 anSiteConfiginterface with anEnvConfigimplementation 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.

Context

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. TheContextclass of this package is a centralized service that provides info on the current request. Container::context()method returns and instance of Context.

Decisions

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.

Usage at website level

The "website" package, that will glue together all packages, needs to only interact with theAppclass, 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.

Customizing site config

By creating an instance ofAppviaApp::new(),it will take care of creating an instance ofEnvConfigthat will later returned when callingContainer::config(). EnvConfigis 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,developmentenvironment is assumed, otherwiseproduction.

Inanycase, a filter:"wp-app-environment"is available for customization of the determined environment.

Regarding PHP constants,EnvConfigis 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 ofContainerthat uses a customEnvConfiginstance, 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 createdEnvConfiginstance (that will be available viaContainer::config()method) can return settings inAcmeInc\ConfigorAcmeIncnamespaces (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$defaultparameter 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

Hosting provider

EnvConfig::hosting()returns the current Hosting provider. Currently we're automatically detecting following:

  • EnvConfig::HOSTING_VIP- WordPress VIP Go
  • EnvConfig::HOSTING_WPE- WP Engine
  • EnvConfig::HOSTING_SPACES- Mittwald Spaces
  • EnvConfig::HOSTING_OTHER- If none of those above is detected

Custom hosting can be setup via aHOSTINGenv 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.

Locations

Access locations

EnvConfig::locations()returns an instance ofInpsyde\App\Location\Locationswhich allows to resolve following directories and URLs:

  • mu-plugins
  • plugins
  • themes
  • languages
  • vendor

On VIP Go (HOSTINGvalue will beEnvConfig::HOSTING_VIP), additional locations can be obtained:

  • private
  • config
  • vip-config
  • images

In fact,Locationsis 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

Adjust locations

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 aLOCATIONSconstant 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 otherLocationsconstant, e.g.Locations::MU_PLUGINSorLocations::LANGUAGESand so on.

The config provided is merged with defaults that can be fine-tuned depending on hosting.

Custom locations

Besides theLocationsconstants, 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 returnnullbecause no URL was set for the key'logs'in theLOCATIONSconstant.

Set locations via environment variables

In the examples above, both default and custom locations are customized using theLOCATIONSconstant 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}_DIRandWP_APP_{$location}_URLcan be used to set location directories and URLs.

For example, vendor path can be set viaWP_APP_VENDOR_DIRand vendor URL viaWP_APP_VENDOR_URL, just like root path can be set viaWP_APP_ROOT_DIRand 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 inLOCATIONSconstant are set for the same location, the env variable takes precedence.

Usage at package level

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_PROVIDERScan actually more than once (more on this soon), but for now is relevant that even if the hook is fired more than once, theAppclass will be clever enough to add the provider only once.

Contextual registration

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)

Package-dependant registration

BesidesApp::ACTION_ADD_PROVIDERSthere'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_PROVIDERhook 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_PROVIDERhook, which works similarly and it is fired in the moment the provider isadded,so before registration is ever attempted.

Providers workflow

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, theAppclass runs its "bootstrapping procedure"from one to three times,depending on whenApp::boot()is called for first time.

IfApp::boot()is called first timebeforeplugins_loadedhook, it will automatically called again atplugins_loadedand again atinit.For a total of 3 times.

IfApp::boot()is called first timeafter (or during)plugins_loaded,but beforeinitit will automatically called again atinit.For a total of 2 times.

IfApp::boot()is called first timeduringinitit will not be called again, so will run once in total.

IfApp::boot()is called first timeafterinitan exception will be thrown.

Each timeApp::boot()is called, theApp::ACTION_ADD_PROVIDERSaction 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 atinithook), but service providers can declare to support "early booting" (more on this soon), in which case theirboot()method is called after theregistermethod, 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 atbeforeplugins_loaded

    1. added service providerswithoutsupport fordelayed registrationare registered
    2. added service providerswithsupport fordelayed registrationand alsowithsupport forearly bootingare registered
    3. added service providerswithsupport forearly bootingare booted
  • Core isatplugins_loaded

    1. added service providerswithoutsupport fordelayed registrationare registered
    2. added service providerswithsupport fordelayed registrationand alsowithsupport forearly bootingare registered
    3. added service providerswithsupport forearly bootingare booted
  • Core isatinit

    1. all added service providerswithoutsupport fordelayed registrationwhich are not registered yet, are registered
    2. all added service providerswithsupport fordelayed registrationwhich are not registered yet, are registered
    3. 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 theServiceProviderinterface, respectivelyregisterLater()andbootEarly(),both returns a boolean.

TheServiceProviderinterface 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.

Available service provider abstract classes

  • Provider\Bootedis a provider that requires bothregister()andboot()methods to be implemented. It hasno support for delayed registrationandno support for early booting.
  • Provider\BootedOnlyis a provider that requires onlyboot()method to be implemented (register()is implemented with no body). It hasno support for early booting.
  • Provider\EarlyBootedis a provider that requires bothregister()andboot()methods to be implemented. It hasno support for delayed registration,butsupports early booting.
  • Provider\EarlyBootedOnlyis a provider that requires onlyboot()method to be implemented (register()is implemented with no body). Itsupports early booting.
  • Provider\RegisteredLateris a provider that requires bothregister()andboot()methods to be implemented. It hassupport for delayed registration,butno support for early booting.
  • Provider\RegisteredLaterEarlyBootedis a provider that requires bothregister()andboot()methods to be implemented. It has bothsupport for delayed registrationandfor early booting.
  • Provider\RegisteredLaterOnlyis a providers that requires onlyregister()method to be implemented (boot()is implemented with no body). It hassupport for delayed registration.
  • Provider\RegisteredOnlyis 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.

The case for delayed registration

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\Loggerservice.

Then, let's assume a separate pluginAcme Authenticationships a service provider that registers several other services that requireAcme\Loggerservice.

TheAcme Authenticationservice provider will need to make sure that theAcme\Loggerservice 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 itsregistermethod can reliably check ifAcme\Loggerservice is already available or not.

Service providers ID

ServiceProviderinterfaceid()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_PROVIDERto 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$idpublic property in the class, and use it if so.
  • in case no$idpublic property, checks for the existence of a publicIDconstant 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.

Composing the container

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 theContainerservice providers canaddthings to it in two ways:

  • directly using Pimple\ArrayAccessmethod
  • usingContainer::addContainer()which accepts any PSR-11 compatible container and make all the services available in it accessible through the application Container

Simple service provider example

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();
}
);
}
}

Service provider example using any PSR-11 container

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.

Website-level providers

App::new()returns an instance of theAppso 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());

Providers Package

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 aServiceProvidersclass, 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());

Advanced topics

Custom last boot hook

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_loadedfor the last "cycle", just keep in mind that:

  • using anything earlier thatafter_setup_thememeans that themes will not be able to add providers.
  • using a late hook, the added providersboot()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.

Building custom container upfront

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 differentSiteConfiginstance (of whichEnvConfigis 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::addContainermethod), 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']);

Resolve objects outside providers

Appclass 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 firstAppthat 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 theAppinstance.

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 ofAppare created, to resolve a service in a specificAppinstance 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);

Debug info

TheAppclass collects information on the added providers and their status whenWP_DEBUGistrue.

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_DEBUGis false, it is possible to callApp::enableDebug().

It is also possible to force debug to be disabled, even ifWP_DEBUGis true, viaApp::disableDebug().

<?php
namespaceAcmeInc;

\Inpsyde\App\App::new()->enableDebug();

Installation

The best way to use this package is through Composer:

$ composer require inpsyde/wp-app-container

License

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.

Contributing

All feedback / bug reports / pull requests are welcome.

Before sending a PR make sure thatcomposer run qawill output no errors.

It will run, in turn: