The Container class provides essential features for managing dependencies and class instances in a PHP application. It allows you to:
- Retrieve a single, shared container instance (singleton pattern).
- Automatically resolve class instances using reflection, including handling constructor dependencies.
- Bind interfaces to concrete classes for dependency injection.
- Alias one class to another, allowing different class implementations to be used interchangeably.
- Manage shared (singleton) instances, ensuring that the same instance is returned for multiple usages.
- Use service providers to control how specific services are resolved.
- Configure transient services, which return different instances each time they are accessed.
- PHP >= 8
Using composer
composer require xtompie/containerThe Container class implements the singleton pattern, ensuring that the same instance of the container is used throughout the application. This is useful for managing global services and dependencies.
use Xtompie\Container\Container;
$container1 = Container::container();
$container2 = Container::container();
var_dump($container1 === $container2); // trueThe Container automatically resolves class instances using PHP's reflection capabilities. It inspects the class constructor to determine required dependencies and resolves them automatically.
use Xtompie\Container\Container;
class Foo
{
// Foo class logic
}
$container = new Container();
$foo = $container->get(Foo::class);
var_dump($foo instanceof Foo); // trueThe Container class provides the resolve method, which allows you to pass specific values directly to a class's constructor when resolving dependencies. This is useful when you want to override or inject certain parameters that are not automatically resolvable by the container.
Unlike the get method, which can return shared instances, the resolve method always returns a new instance of the class, even if called multiple times. This ensures transient behavior and avoids shared state between instances.
use Xtompie\Container\Container;
class Bar
{
public function __construct(
public Foo $foo,
public ?string $qux = null
) {
}
}
$container = new Container();
$values = ['qux' => 'quxx'];
$bar = $container->resolve(Bar::class, $values);
var_dump($bar instanceof Bar); // true
var_dump($bar->qux); // 'quxx'In this example, the resolve() method manually provides the value quxx for the qux parameter, while the container automatically resolves the Foo dependency. Each call to resolve creates a new Bar instance.
If a class requires dependencies in its constructor, the Container resolves them as well. It inspects the constructor and creates instances of required classes.
use Xtompie\Container\Container;
class Bar
{
public function __construct(
public Foo $foo,
public ?string $qux = null
) {
}
}
$container = new Container();
$bar = $container->get(Bar::class);
var_dump($bar instanceof Bar); // true
var_dump($bar->foo instanceof Foo); // true
var_dump($bar->qux); // nullYou can bind an interface to a concrete class, allowing the container to resolve the correct implementation when the interface is requested.
use Xtompie\Container\Container;
interface FooInterface {}
class Foo implements FooInterface {}
$container = new Container();
$container->bind(FooInterface::class, Foo::class);
$fooInterface = $container->get(FooInterface::class);
var_dump($fooInterface instanceof Foo); // trueThe container allows you to alias one class to another, making it possible to use different class implementations interchangeably.
use Xtompie\Container\Container;
class Foo2 {}
$container = new Container();
$container->bind(Foo::class, Foo2::class);
$foo = $container->get(Foo::class);
var_dump($foo instanceof Foo2); // trueThe container manages shared services, ensuring the same instance is returned each time the service is retrieved. This is useful for managing singletons.
use Xtompie\Container\Container;
$container = new Container();
$instance1 = $container->get(Foo::class);
$instance2 = $container->get(Foo::class);
var_dump($instance1 === $instance2); // trueTransient services return a new instance each time they are accessed. By marking a class as transient, the Container ensures that different instances are returned on every access.
You can either define a service as transient manually using the transient() method or implement the Transient interface to automatically mark a class as transient.
use Xtompie\Container\Container;
$container = new Container();
$container->transient(Foo::class);
$instance1 = $container->get(Foo::class);
$instance2 = $container->get(Foo::class);
var_dump($instance1 === $instance2); // falseAlternatively, if a service class implements the Transient interface, the Container will automatically treat it as transient, meaning a new instance is returned on each retrieval:
use Xtompie\Container\Container;
use Xtompie\Container\Transient;
class Foo implements Transient {}
$container = new Container();
$instance1 = $container->get(Foo::class);
$instance2 = $container->get(Foo::class);
var_dump($instance1 === $instance2); // falseIn both cases, this ensures that transient objects are always new instances, as opposed to shared services, which return the same object every time.
Providers can be used for more complex service resolution. A provider is a class that implements the Provider interface, which allows you to control how specific services are created or managed within the container. Providers inject additional logic into the resolution process.
Here's the Provider interface:
namespace Xtompie\Container;
interface Provider
{
public static function provide(string $abstract, Container $container): mixed;
}To use a provider, first create a class that implements the Provider interface. The provide() method should return the service instance, and it will be invoked by the container when needed.
Example of a provider class:
use Xtompie\Container\Container;
use Xtompie\Container\Provider;
class Baz {}
class BazProvider implements Provider
{
public static function provide(string $abstract, Container $container): mixed
{
return new Baz();
}
}You can then register the provider with the container and retrieve the service:
use Xtompie\Container\Container;
$container = new Container();
$container->provider('Quux', BazProvider::class);
$quux = $container->get('Quux');
var_dump($quux instanceof Baz); // trueThis approach allows for more complex or conditional logic when resolving services within the container.
A service can provide its own provider by implementing the Provider interface. This allows a class to automatically supply instances for the container without needing to register the provider in the container.
If a class implements the Provider interface, the container will recognize this and automatically use it as a provider when creating instances.
use Xtompie\Container\Container;
use Xtompie\Container\Provider;
class Qux implements Provider
{
public static function provide(string $abstract, Container $container): object
{
return new Qux(val: 42);
}
public function __construct(
public int $val,
) {
}
}
$container = new Container();
$qux = $container->get(Qux::class);
var_dump($qux instanceof Qux); // true
var_dump($qux->val); // 42In this example, the Qux class implements the Provider interface, meaning the container will automatically find the provider within the class and use it to create an instance. This removes the need to manually register the provider in the container.
If you bind multiple classes, the container resolves the most concrete class available. This is useful for managing multiple levels of abstraction or class inheritance.
use Xtompie\Container\Container;
$container = new Container();
$container->bind(FooInterface::class, Foo::class);
$container->bind(Foo::class, Foo2::class);
$fooInterface = $container->get(FooInterface::class);
var_dump($fooInterface instanceof Foo2); // trueContainer class includes a call method that allows you to invoke any callback with automatic dependency injection. This method works for closures (anonymous functions), static class methods, and instance methods. The container automatically resolves and injects the required dependencies into the callback.
Using a closure (anonymous function):
use Xtompie\Container\Container;
class Foo
{
public function method(): string
{
return 'Foo';
}
}
$container = new Container();
$result = $container->call(function(Foo $foo) {
return $foo->method(); // 'Foo'
});
echo $result; // FooIn this example, the container automatically resolves the Foo dependency and injects it into the closure.
Using a static class method:
use Xtompie\Container\Container;
class Foo
{
public function method(): string
{
return 'Foo';
}
}
class Call
{
public static function f1(Foo $foo): string
{
return $foo->method();
}
}
$container = new Container();
$result = $container->call([Call::class, 'f1']);
echo $result; // FooFor static class methods, the container resolves the dependencies and injects them before invoking the static method.
Using an instance method:
use Xtompie\Container\Container;
class Foo
{
public function method(): string
{
return 'Foo';
}
}
class Call
{
public function f2(Foo $foo): string
{
return $foo->method();
}
}
$container = new Container();
$call = new Call();
$result = $container->call([$call, 'f2']);
echo $result; // FooHere, the container injects the Foo dependency into the instance method and then invokes the method on the provided object.
The Container class includes a call method that allows you to invoke a callable with automatic dependency injection. Additionally, you can pass custom values that override the container's automatic resolution.
use Xtompie\Container\Container;
class Foo
{
public function method(): string
{
return 'Foo';
}
}
class Bar
{
public function execute(Foo $foo, string $name): string
{
return $foo->method() . ' ' . $name;
}
}
$container = new Container();
$customValues = [
'name' => 'CustomName',
];
$bar = new Bar();
$result = $container->call([$bar, 'execute'], $customValues);
echo $result; // Foo CustomNameIn this example:
- The
Containerresolves an instance ofFoo. - We pass a custom string value
'CustomName'for the$nameparameter, which overrides the need for the container to resolve it.
The callArgs method allows you to retrieve the arguments needed to call a given callable (function, method, etc.) with automatic dependency resolution. It inspects the callable’s parameters and resolves the necessary dependencies, optionally allowing custom values or a resolver to be used.
use Xtompie\Container\Container;
class Foo
{
public function method(): string
{
return 'Foo';
}
}
$container = new Container();
$args = $container->callArgs(function(Foo $foo) {
return $foo->method();
});
var_dump($args); // Array with resolved arguments, e.g., [Foo instance]Custom Values
You can pass custom values to callArgs that override the container's default resolution behavior.
$customValues = ['foo' => new Foo2()];
$args = $container->callArgs(function(Foo $foo) {
return $foo->method();
}, $customValues);
var_dump($args); // Custom values take precedence over container resolutionCustom Argument Resolver ($arg)
The callArgs method also accepts an optional $arg callable that resolves parameters before falling back to container resolution or custom values. If the $arg callable returns null, the resolution falls back to the container or the custom values.
$customResolver = function (\ReflectionParameter $parameter) {
if ($parameter->getType()?->getName() === Foo::class) {
return new Foo2();
}
return null;
};
$args = $container->callArgs(function(Foo $foo) {
return $foo->method();
}, [], $customResolver);
var_dump($args[0] instanceof Foo2); // trueIn this case, the custom resolver takes precedence. If the resolver does not handle the argument (returns null), the container or provided custom values are used as fallback.
Key Points
- The
$argresolver has the highest priority. - Custom values are checked next.
- If no match is found, the container will resolve dependencies automatically.
You can extend the Container class to add custom functionality or specific behavior for your application.
use Xtompie\Container\Container;
class CustomContainer extends Container
{
// Custom methods or properties
}This allows you to tailor the container to meet the specific needs of your project, adding custom logic or behavior to the dependency resolution process.