You could provide a number of generic and agnostic attributes for use with dependency-injection solutions without enforcing any sort of contract or implementing structure. This would be achieved by only targetting things that we know will be used. These attributes could be used with auto-wiring or a compilation state that collects all the data for production deployment or something.
These attributes:
- Wouldn't enforce a particular implementation
- Wouldn't restrict the usage of the class
- Wouldn't even require DI.
Binding is a generic enough term and it's something that's used by almost every DI solution. Instead of enforcing a class method responsible for binding, which is already handled by the container PSR, you could provide an attribute that allows a class to define its binding.
#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Binding
{
/** @var class-string */
public string $abstract;
/** @var array<class-string> */
public array $aliases;
/**
* @var class-string $abstract
* @var class-string ...$aliases
*/
public function __construct(string $abstract, string ...$aliases)
{
$this->abstract = $abstract;
$this->aliases = $aliases;
}
}
A package developer would now include this on their class.
#[Binding(MyInterface::class, MyClass::class)]
class MyClass implements MyInterface
{}
Almost every DI solution also has the concept of a "shared" instance, where a single instance is treated as a singleton. This could be achieved with a simple marker attriute.
#[Attribute(Attribute::TARGET_CLASS)]
final class Shared {}
Not every DI solution has the concept of a "factory", but adding support for it would be trivial. Consider the following interface.
#[Attribute(Attribute::TARGET_METHOD)]
final readonly class Factory
{
/** @var class-string */
public string $abstract;
/**
* @var class-string $abstract
*/
public function __construct(string $abstract)
{
$this->abstract = $abstract;
}
}
This particular attribute has two possible usages (which is why the abstract property supports null
).
Consider a class responsible for creating different instances. Rather than create a separate binding for each class it can create, you'd just register a class with the DI solution and it'd parse for methods with this attribute.
class MyFactory {
#[Factory(MyInterface::class)]
public static function interface(): MyInterface {}
#[Factory(MyOtherInterface::class)]
public static function otherInterface(): MyOtherInterface {}
}
You could also use the Factory
attribute with classes marked as Shared
that are actual singletons.
#[Shared]
final class MySingleton
{
private static self $instance;
#[Factory]
public static function instance(): self
{
if (! isset(self::$instance)) {
self::$instance = new self;
}
return self::$instance;
}
}
In this case you'd have code like the following:
$container->bind(MySingleton::class);
Which would process and detect the attributes, and would be the equivelant of something like:
$container->bind(MySingleton::class, function () {
return MySingleton::instance();
}, true);
There are a few other processes commonly included in DI solutions, though not so much with PHP because we didn't have attributes like this before.
Sometimes you'll have a class bound as MyDriver::class
, but specific classes may need specific implementions. A good example of this would be a PaymentProvider
interface with the implementations WorldPayPaymentProvider
and StripePaymentProvider
. Imagine you then have the class StripeHandler
which handles a bunch of different things, and will require an instance of PaymentProvider
. With a qualified binding you can have a default implementation bind to PaymentProvider
, but qualify it by providing other "sub-bindings". It'd be used something like this:
class StripeHandler {
public function __construct(#[QualifiedBy('stripe')] PaymentProvider $paymentProvider) {}
}
This approach is useful because it allows for the requirement of particular drivers without an implementation. A great example of why this would be useful is with something Laravel database connections. By default, all connections are instances of the same class, so don't have any distinguishing feature, beyond the name they're registered with.
The idea behind this one is simple. You mark a class as belonging to a scope, and the DI solution would have a container, that contains multiple other containers, each for a different scope.
Imagine a system where the default binding for Authenticatable
is User
, which would be a totally sensible thing to do. Now, imagine that the same system as an Admin
class that implements Authenticatable
. Your server class can still depend on Authenticatable
, but my marking the class with #[Scope('admin')]
it'd use the container within that scope, and the binding from that.
This is also useful with setups like Swoole and Laravel Octane where a process lives a lot longer than we would ordinarily expect.