Skip to content

Container

Foundation Container provides a shared container contract and service provider base class. Use it to describe how application services are constructed while keeping dependency resolution and the underlying container implementation out of the services themselves.

Install the split package in applications that define their own container or service providers:

composer require stellarwp/foundation-container

Other Foundation packages install Container automatically when they depend on it. Composer does not require a second explicit installation in that case.

Create one container in the application composition root and register providers in dependency order. These guides establish that structure:

Let the container autowire concrete classes

Section titled “Let the container autowire concrete classes”

The container can construct an unbound concrete class when its constructor dependencies are also concrete classes:

final readonly class Catalog_Synchronizer {

	public function __construct(
		private Product_Repository $products,
		private Remote_Catalog $catalog
	) {
	}
}

Resolve the application entrypoint where it is needed:

$synchronizer = $container->get( Catalog_Synchronizer::class );

Prefer constructor injection throughout application code. Calling get() inside a service hides its dependencies and turns the container into a service locator.

In src/Catalog/Catalog_Provider.php, bind an interface when the container cannot infer which implementation the application wants. Use bind() for a new instance on each resolution and singleton() when every resolution should return the same instance:

<?php declare(strict_types=1);

namespace YourPlugin\Catalog;

use StellarWP\Foundation\Container\Contracts\Provider;

/**
 * Selects the catalog implementation used by the application.
 */
final class Catalog_Provider extends Provider {

	public function register(): void {
		$this->register_catalog();
	}

	private function register_catalog(): void {
		$this->container->singleton(
			Catalog::class,
			Remote_Catalog::class
		);
	}
}

Bindings are lazy. Registering Remote_Catalog does not construct it; the container builds it when another service first requests Catalog.

Every provider receives the shared container and read-only configuration snapshot. Use $this->config when a feature needs configuration; providers that do not need it can simply ignore it. This keeps one provider shape throughout the application instead of requiring developers to choose a base class.

Foundation providers register eagerly. Keep expensive services lazy by binding them in register() and letting the container construct them on first use; do not add provider-level deferred or boot phases.

In the same src/Catalog/Catalog_Provider.php, use a contextual binding when one class needs a scalar or a feature-specific implementation. Target scalar constructor arguments by their $name. Import Foundation’s Resolver as C when a factory callback must resolve another service:

use StellarWP\Foundation\Container\Contracts\Resolver as C;

Then update the provider’s catalog registration:

private function register_catalog(): void {
	$this->container->when( Remote_Catalog::class )
		->needs( '$endpoint' )
		->give( (string) $this->config->get( 'catalog.endpoint' ) );

	$this->container->singleton( Remote_Catalog::class );
	$this->container->singleton(
		Catalog::class,
		static fn ( C $c ): Remote_Catalog => $c->get( Remote_Catalog::class )
	);
}

The callback aliases Catalog to the configured Remote_Catalog singleton. This preserves the contextual bindings registered for the concrete class and ensures both identifiers resolve the same object.

Factory callbacks receive Foundation’s Resolver contract. Application providers should not type-hint DI52 directly; keeping the callback behind the Foundation contract allows the underlying container integration to change without requiring edits throughout application providers.

Use a factory callback only when the value must be computed or fetched from the container. Let the container construct the complete service whenever it can.

In src/Report/Provider.php, use mergeArrayVar() when independent providers contribute to one ordered collection. The provider that owns the collection registers its default and supplies it to the consuming class:

public const string EXPORTERS = 'your-plugin.report.exporters';

private function register_exporter_collection(): void {
	$this->container->mergeArrayVar( self::EXPORTERS, [] );

	$this->container->when( Exporter_Collection::class )
		->needs( '$exporters' )
		->give( static fn ( C $c ): array => $c->get( self::EXPORTERS ) );
}

Other feature providers append their implementations without replacing earlier contributions. For example, src/Report/Csv/Provider.php can contribute the CSV implementation:

private function register_csv_exporter(): void {
	$this->container->mergeArrayVar(
		Report\Provider::EXPORTERS,
		static fn ( C $c ): array => [
			$c->get( Csv_Exporter::class ),
		]
	);
}

In src/Catalog/Catalog_Provider.php, use callback() to let WordPress resolve a service only when its hook runs:

private function register_catalog_sync(): void {
	$this->container->singleton( Catalog_Synchronizer::class );

	add_action(
		'your_plugin/sync_catalog',
		$this->container->callback( Catalog_Synchronizer::class, 'synchronize' )
	);
}

This avoids constructing the synchronizer during every request merely to register its callback.

Catch StellarWP\Foundation\Container\Exceptions\NotFoundException when a requested identifier may be absent. Failures raised by the container while registering or resolving services use StellarWP\Foundation\Container\Exceptions\ContainerException. Both implement the corresponding PSR container exception interfaces, and the original application failure remains available through getPrevious() when the underlying container wrapped one. Exceptions thrown by a provider’s own register() method remain that provider’s exception and propagate unchanged.

In src/Catalog/Catalog_Provider.php, use a decorator chain when cross-cutting behavior should wrap a service without changing its implementation. List the outermost decorator first and the base implementation last:

private function register_catalog(): void {
	$this->container->singletonDecorators(
		Catalog::class,
		[
			Logging_Catalog::class,
			Caching_Catalog::class,
			Remote_Catalog::class,
		]
	);
}

Resolving Catalog returns one Logging_Catalog that wraps Caching_Catalog, which wraps Remote_Catalog. Use bindDecorators() instead when the application needs a new chain on every resolution.

Replace an implementation in a focused test

Section titled “Replace an implementation in a focused test”

Bind a test double to the same contract before resolving the class under test:

$catalog = new Fake_Catalog();

$this->container->bind( Catalog::class, $catalog );

$synchronizer = $this->container->get( Catalog_Synchronizer::class );
$synchronizer->synchronize();

$this->assertTrue( $catalog->was_synchronized() );

Test application services through their public behavior. Reserve container integration tests for provider graphs where the binding itself is the behavior under test.

Replace application bindings for Adbar\Dot with the Foundation Configuration contract. Construct the supplied ArrayConfiguration from the array returned by config.php, then pass it to ContainerFactory::create(). The factory creates the default backend and registers Foundation’s Container, Resolver, and Configuration contracts.

Prefer ContainerFactory over constructing ContainerAdapter directly. A custom composition root that wraps its own DI52 container must register the Container, Resolver, and Configuration contracts before registering any provider; every Provider resolves that configuration during construction.

The Provider constructor now resolves Foundation’s Configuration contract from the container and is final. Remove application provider constructors that accepted or forwarded the container and Adbar\Dot; providers can read the same configuration through $this->config during register():

public function register(): void {
	$endpoint = (string) $this->config->get( 'catalog.endpoint' );

	// Register this feature using the configured endpoint.
}

Applications that implemented the removed Providable interface directly should extend Provider instead. Update provider lists and class-string annotations to reference Provider as well.

Provider subclasses can no longer declare their own constructors. Move scalar provider settings into config.php and read them through $this->config. Inject runtime collaborators into the application services configured by the provider rather than into the provider itself; use the inherited $this->container only to register those definitions and hooks.

Container factory callbacks should import StellarWP\Foundation\Container\Contracts\Resolver as C instead of lucatume\DI52\Container. Callback bodies can continue calling $c->get() and $c->has().

Catch StellarWP\Foundation\Container\Exceptions\ContainerException or the more specific NotFoundException instead of DI52 exceptions. ContainerAdapter::getContainer() and its forwarding of undocumented DI52 methods have been removed; use only operations declared by the Foundation Container and Resolver contracts.

Provider-level isDeferred(), provides(), and boot() methods have also been removed. Move required bindings, hook registration, and other startup behavior into register() or an explicit application-owned lifecycle. Services bound during register() remain lazy until they are requested.