Registers services and injects them into consumers.
composer require ob-ivan/sd-dependency-injection
In large applications a situation when some class instances are to be shared among many other classes is common. Think of a database connection instance shared among various controller classes.
Those shared classes are called services (they provide some useful functionality to others), and those sharing are called consumers (they consume the services provided).
The process of letting a consumer know all services it requires access to is called injection.
A consumer may declare formally a list of services it requires. This is called listing its dependencies. The process of taking a consumer's dependency list and injecting corresponging services into it is called, unsurprisingly, dependency injection.
A dependency injection container (DIC) is a way to implement dependency injection. It requires that each service is registered within the container under a certain common name, and that consumers list common names of their required services as formal dependency lists.
Dependecies may be injected into consumers in several different ways. In this library, these three types of dependency injection are supported:
- Setter injection aka Interface injection - given a consumer Instance implementing DeclarerInterface call a setter for each declared dependency.
- Constructor injection - given a consumer's ClassName resolve its constructor arguments as dependency names and call constructor with appropriate services. Setter injection is then invoked on the created instance.
- Argument injection - given a consumer Function resolve its arguments as dependency names and call the Function with appropriate services.
You can use a container to register services, or to produce consumers, or to inject dependencies into arbitrary code.
- Consumer = Function | Instance
- ConsumerInitializer = ClassName | Consumer
- ServiceInitializer = ConsumerInitializer | Value
A container is initialized with raw values, no Initalizers allowed:
$container = new SD\DependencyInjection\Container([
'name' => 'Chewbaka', // not interpreted as a class name
]);
If you have several containers, you can create a new one by merging them:
$mergedContainer = SD\DependencyInjection\Container::merge($container1, $container2);
Services are registered with ServiceInitializers:
// Setter injection - use when constructing a Service is cheap and doesn't involve resource allocating.
$container->register('helloWorld', new HelloWorldService('Anakin'));
// Constructor injection + Setter injection - use when Service does not require constructor arguments.
$container->register('helloWorld', HelloWorldService::class);
// Argument injection + Setter injection - use when Service requires a constructor argument or
// Service construction is anyhow a complicated process.
$container->register('helloWorld', function ($name) {
if ($name === strtoupper($name)) {
return new HelloShouterService($name);
}
return new HelloWorldService($name);
});
// No injection - use for setting parameters.
$container->register('name', $container->value('Skywalker'));
You do not have to register container within itself, as it is self-aware by default:
// The 'container' common name is reserved to refer to the container itself.
$container === $container->get('container');
You can extend registered services:
// Initial registering:
$container->register('currency', SD\Currency\Repository::class);
// Later on:
$container->extend('currency', function ($container, $currency) {
$store = $container->produce(SD\CurrencyStore\Wpdb::class);
$currency->setStore($store);
return $currency;
});
Consumers are produced with ConsumerInitializers (Value is not supported):
$controller = $container->produce(HelloWorldController::class);
// or:
$controller = $container->produce(new HelloWorldController('Luke'));
// or:
$controller = $container->produce(function ($catchPhrase) {
return new HelloWorldController($catchPhrase);
});
You can inject services into any Consumer:
// Inject into an already instantiated consumer:
$serviceAwareCalculator = $container->inject($serviceUnawareCalculator);
// Inject into a callable:
$response = $container->inject(function ($helloWorldService) use ($name) {
return $helloWorldService->greet($name);
});
Though discouraged, you can use container as service locator, if execution context does not let you use the power of dependency injection:
$legacyConsumer = new LegacyConsumer($container->get('brand_new_service'));
Class consumers (as opposed to callable consumers) declare their dependencies in any of following ways --- or a combination of both:
- By implementing
DeclarerInterface
and returning a list of common names fromdeclareDependencies
method. - By implementing
AutoDeclarerInterface
and importingAutoDeclarerTrait
andAwareTrait
s of their respective dependencies.
As listing common names in declareDependencies
method causes heavy reduplication of common names,
we recommend to define an AwareTrait
for each service you define.
Here's a sample code:
trait ExampleAwareTrait
{
// Here's the magic. Create a field named starting with $autoDeclare and containing the service's
// common name as a value. Don't forget to import AutoDeclarerTrait!
protected $autoDeclareExample = 'example';
private $example;
// Setter name must match the common name.
public function setExample(ExampleService $example) {
$this->example = $example;
}
// An example way to access an example service instance.
protected function getExample()
{
return $this->example;
}
}
This way defining a consumer in a following way will tell a container to inject ExampleService into it --- or to throw an exception if it's not available.
use SD\DependencyInjection\AutoDeclarerInterface;
use SD\DependencyInjection\AutoDeclarerTrait;
class ExampleConsumer implements AutoDeclarerInterface
{
use AutoDeclarerTrait;
use ExampleAwareTrait;
public function run()
{
return $this->getExample()->doSomething();
}
}
You can encapsulate a service's common name further by putting it into a provider.
use SD\DependencyInjection\ProviderInterface;
// This provides a correspondence between the service instance and its common name.
class ExampleProvider implements ProviderInterface
{
public function getServiceName(): string
{
return 'example'; // the common name
}
public function provide()
{
return new ExampleService();
}
}
Then the service is registered with its provider's instance:
$container->connect(new ExampleProvider());
This way neither registration code nor consumers need to know the common name. They just refer to Provider instance and the AwareTrait, and it just works. Hey, it's magic!
To run tests:
composer install
vendor/bin/phpunit