dakujem / wire-genie
Autowiring Tool & Dependency Provider. Wire with genie powers.
Installs: 5 880
Dependents: 1
Suggesters: 0
Security: 0
Stars: 2
Watchers: 3
Forks: 0
Open Issues: 0
Requires
- php: ^8.0
- psr/container: ^1
Requires (Dev)
- dakujem/sleeve: ^1
- phpunit/phpunit: ^8 || ^9.1
This package is auto-updated.
Last update: 2024-12-09 13:22:20 UTC
README
Autowiring Tool & Dependency Provider for PSR-11 service containers. Wire with genie powers.
๐ฟ
composer require dakujem/wire-genie
๐ Changelog
What?
A superpowered call_user_func
? Yup! And more.
Wire Genie uses your PSR-11 service container to "magically" provide arguments (dependencies).
Allows you to:
- invoke any callables
- construct any objects
... with high level of control over the arguments. ๐ช
Usage
$container = new Any\Psr11\Container([ Thing::class => new Thing(), MyService::class => new MyService(), ]); $callable = function (MyService $service, Thing $thing){ ... }; class Something { public function __construct(MyService $service, Thing $thing) { ... } } $g = new Dakujem\Wire\Genie($container); // Magic! The dependencies are resolved from the container. $value = $g->invoke($callable); $object = $g->construct(Something::class);
That is only the basis, the process is customizable and more powerful.
For each parameter it is possible to:
- override type-hint and wire an explicit dependency (override the type-hint)
- construct missing services on demand (resolves cascading dependencies too)
- skip wiring (treat as unresolvable)
- override value (bypass the container)
// override type-hint(s) $callable = function ( #[Wire(MyService::class)] AnInterface $service, #[Wire(Thing::class)] $thing ){ ... }; $value = $g->invoke($callable); // construct object(s) if not present in the container $callable = function ( #[Hot] Something $some, #[Make(Thing::class)] $thing ){ ... }; $value = $g->invoke($callable); // provide arguments for scalar-type, no-type and otherwise unresolvable parameters $callable = function (string $question, MyService $service, int $answer){ ... }; $g->invoke( $callable, 'The Ultimate Question of Life, the Universe, and Everything.', 42, ); $g->invoke( $callable, answer: 42, question: 'The Ultimate Question of Life, the Universe, and Everything.', ); // skip wiring for a parameter... $callable = function (#[Skip] MyService $service){ ... }; $g->invoke($callable, new MyService(...)); // ...and provide your own argument(s)
How it works
There are two primary methods:
Genie::invoke( callable $target, ...$pool ); Genie::construct( string $target, ...$pool );
... where the variadic $pool
is a list of values that will be used for unresolvable parameters.
The resolution algorithm works like the following. If any step succeeds, the rest is skipped.
For each parameter...
- If the parameter name matches a named argument from the pool, use it.
- If
#[Skip]
hint is present, skip steps 3-6 and treat the parameter as unresolvable. - If a
#[Wire(Identifier::class)]
hint (attribute) is present, resolve the hinted identifier using the container. - Resolve the type-hinted identifier using the container.
- If
#[Hot]
hint is present, attempt to create the type-hinted class. Resolve cascading dependencies. - If
#[Make(Name::class)]
hint is present, attempt to create the hinted class. Resolve cascading dependencies. - When a parameter is unresolvable, try filling in an argument from the pool.
- If a default parameter value is defined, use it.
- If the parameter is nullable, use
null
. - Fail utterly.
Hints / attributes
As you can see, the algorithm uses native attributes as hints to control the wiring.
#[Wire(Identifier::class)]
tells Genie to try to wire the service registered as Identifier
from the container
#[Wire('identifier')]
tells Genie to try to wire service with 'identifier'
identifier from the container
#[Hot]
tells Genie to try to create the type-hinted class (works with union types too)
#[Make(Service::class, 42, 'argument')]
tells Genie to try to create Service
class using 42
and 'argument'
as the argument pool for the construction
#Skip
tells Genie not to use the container at all
Hot
and Make
work recursively,
their constructor dependencies will be resolved from the container or created on the fly too.
What can it be used for?
- middleware / pipeline dispatchers
- asynchronous job execution
- supplying dependencies after a job is deserialized from a queue
- generic factories that create instances with varying dependencies
- method dependency injection
- for controllers, where dependencies are wired at runtime
Examples
A word of caution
Fetching services from the service container on-the-fly might solve an edge case in certain implementations where dependency injection boilerplate can not be avoided or reduced in a different way.
It is also the only way to invoke callables with dependencies not known at the time of compilation.
Normally, however, you want to wire your dependencies when building your app's service container.
Disclaimer ๐ค
Improper use of this package might break established IoC principles and degrade your dependency injection container to a service locator, so use the package with caution.
Remember, it is always better to inject a service into a working class, then to fetch the service from within the working class (this is called "Inversion of Control", "IoC").
Integration
As with many other third-party libraries, you should consider wrapping code using Wire Genie into a helper class with methods like the following one:
/** * Invokes a callable resolving its type-hinted arguments, * filling in the unresolved arguments from the static argument pool. * Returns the callable's return value. * Also allows to create objects passing in a class name. */ public function call(callable|string $target, ...$pool): mixed { return Genie::employ($this->container)($target, ...$pool); }
This adds a tiny layer for flexibility, in case you decide to tweak the way you wire dependencies later on.
Static provisioning
Genie::provide()
can be used to provision a callable with a fixed list of services without using reflection.
$factory = function( Dependency $dep1, OtherDependency $dep2 ): MyObject { return new MyObject($dep1, $dep2); }; $object = $g->provide( Dependency::class, OtherDependency::class )->invoke($factory);
Limiting access to services
You can limit the services accessible through Genie
by using a filtering proxy Limiter
:
$repoGenie = new Dakujem\Wire\Genie( new Dakujem\Wire\Limiter($container, [ RepositoryInterface::class, // you may whitelist multiple classes or interfaces ]) );
The proxy uses the instanceof
type operator
and throws if the requested service does not match at least one of the whitelisted classes or interface names.
Customization
A custom strategy can be inserted into Genie
,
and the default AttributeBasedStrategy
allows for customization of the resolver mechanism,
thus providing ultimate configurability.
Compatibility
Framework agnostic. Any PSR-11 container can be used.
Wonderful lamp
// If we happen to find a magical lamp... $lamp = new Dakujem\Wire\Lamp($container); // we can rub it, and a genie might come out! $genie = $lamp->rub(); // My wish number one is... $genie->construct(Palace::class);
Flying carpet
We've already got a lamp ๐ช and a genie ๐ง ... so?
Installation
๐ฟ composer require dakujem/wire-genie
Never heard of Composer? Go get it!
Testing
Run unit tests using the following command:
$
composer test
or
$
php vendor/phpunit/phpunit/phpunit tests
Contributing
Ideas, feature requests and other contribution is welcome. Please send a PR or create an issue.
Now go, do some wiring!