dakujem/wire-genie

Wire with genie powers.

1.1 2020-07-09 08:35 UTC

This package is auto-updated.

Last update: 2020-07-09 09:52:12 UTC


README

Build Status

💿 composer require dakujem/wire-genie

Allows to easily

  • fetch multiple dependencies from a service container and provide them as arguments to callables
  • automatically detect parameter types of a callable and wire respective dependencies to invoke it
  • automatically detect constructor parameters of a class and wire respective dependencies to construct it

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.

The main purposes of the package are to provide a limited means of wiring services without directly exposing a service container, and to help wire services automatically at runtime.

Note 💡

This approach solves an edge case in certain implementations where dependency injection boilerplate can not be avoided or reduced in a different way.

Normally you want to wire your dependencies when building your app's service container.

How it works

WireGenie is rather simple, it fetches specified dependencies from a container and passes them to a callable, invoking it and returning the result.

$wireGenie = new WireGenie($container);

$factory = function( Dependency $dep1, OtherDependency $dep2, ... ){
    // create stuff...
    return new Service($dep1, $dep2, ... );
};

// create a provider, explicitly specifying dependencies
$provider = $wireGenie->provide( Dependency::class, OtherDependency::class, ... );

// invoke the factory using the provider
$service = $provider->invoke($factory);

With WireInvoker it is possible to omit specifying the dependencies and use automatic dependency wiring:

// invoke the factory without specifying dependencies, using an automatic provider
$service = WireInvoker::employ($wireGenie)->invoke($factory);

WireInvoker will detect type-hinted parameter types or tag-hinted identifiers at runtime and then provide dependencies to the callable.

Note on service containers and conventions

Note that how services in the container are accessed depends on the conventions used.
Services might be accessed by plain string keys, class names or interface names.

WireGenie simply calls methods of PSR-11 Container ContainerInterface::get() and ContainerInterface::has() under the hood, there is no other "magic".

Consider a basic service container (Sleeve) and the different conventions:

$sleeve = new Sleeve();
// using a plain string identifier
$sleeve->set('genie', function (Sleeve $container) {
    return new WireGenie($container);
});
// using a class name identifier
$sleeve->set(WireGenie::class, function (Sleeve $container) {
    return new WireGenie($container);
});

// using a plain string identifier
$sleeve->set('self', $sleeve);
// using an interface name identifier
$sleeve->set(ContainerInterface::class, $sleeve);

The services can be accessed by calling either

$sleeve->get('genie');
$sleeve->get(WireGenie::class);

$sleeve->get('self');
$sleeve->get(ContainerInterface::class);

Different service containers expose services differently. Some offer both conventions, some offer only one.
It is important to understand how your container exposes the services to fully leverage WireGenie and WireInvoker.

Basic usage

Note: In the following example, services are accessed using plain string keys.

// Use any PSR-11 compatible container you like.
$container = AppContainerPopulator::populate(new Sleeve());

// Give Wire Genie full access to your service container,
$genie = new WireGenie($serviceContainer);

// or give it access to limited services only.
// (classes implementing RepositoryInterface in this example)
$repoGenie = new WireGenie(new WireLimiter($container, [
    RepositoryInterface::class,
    // you may whitelist multiple classes or interfaces
]));

// Create a factory function you would like to call, given the dependencies:
$factory = function(MyServiceInterface $s1, MyOtherSeviceInterface $s2){
    // do stuff, create other objects, system services and so on...
    return new ComplexService($s1, $s2);
};

// Wire genie fetches the dependencies you specify in the `provide` call
// and provides them to the callable when you call the `invoke` method:
$complexService = $genie->provide('myService', 'my-other-service')->invoke($factory);

// Repo genie will only be able to fetch repositories,
// the following call would fail
// if 'my-system-service' service did not implement RepositoryInterface:
$repoGenie->provide('my-system-service');

You now have means to allow a part of your application on-demand access to a group of services of a certain type without injecting them all.
This particular use-case breaks IoC if misused, though.

// using $repoGenie from the previous snippet
new RepositoryConsumer($repoGenie);

// ... then inside RepositoryConsumer
$repoGenie->provide(
    'this',
    'that'
)->invoke(function(
    ThisRepository $r1,
    ThatRepository $r2
){
    // do stuff with the repos...
});

In use cases like the one above, it is important to limit access to certain services only, to keep your app layers in good shape.

Automatic dependency resolution

If you find the explicit way of WireGenie too verbose or insufficient, Wire Genie package comes with the WireInvoker class that enables automatic resolution of callable arguments.

Type-hinted service identifiers

Using WireInvoker, it possible to omit explicitly specifying the dependencies:

WireInvoker::employ($wireGenie)->invoke(function( Dependency $dep1, OtherDependency $dep2 ){
   return new Service($dep1, $dep2);
});

The automatic resolver will detect parameter types using type hints and make sure that Dependency::class and OtherDependency::class are fetched from the container.
This works, when the services are accessible using their class names.

Tag-hinted service identifiers

In case services are accessible by plain string identifiers (naming conventions unrelated to the actual type-hinted class names), or the type-hint differs from how the service is accessible, doc-comments and "wire tags" can be used:

/**
 * @param $dep1 [wire:my-identifier]
 *              \__________________/
 *                the whole "wire tag"
 *
 * @param $dep2 [wire:other-identifier]
 *                    \______________/
 *                      service identifier
 */
$factory = function( Dependency $dep1, OtherDependency $dep2 ){
  return new Service($dep1, $dep2);
};
WireInvoker::employ($wireGenie)->invoke($factory);

In this case, services registered as my-identifier and other-identifier are fetched from the container.

Tip 💡

An empty wire tag [wire:] (including the colon at the end) can be used to indicate that a service should not be wired.
Useful when you want to pass custom objects to a call.

Filling in for unresolved parameters

When a callable requires passing arguments that are not resolved by the service container, it is possible to provide them as a static argument pool:

// scalars can not be resolved from the container using reflection by default
$func = function( Dependency $dep1, int $size, OtherDependency $dep2, bool $cool ){
   return $cool ? new Service($dep1, $dep2) : new OtherService($size, $dep1, $dep2);
};
// but there is a tool for that too:
WireInvoker::employ($wireGenie)->invoke($func, 42, true); // cool, right?

Values from the static argument pool will be used one by one to fill in for unresolvable parameters.

When to use

Note that WireInvoker resolves the dependencies at the moment of calling its invoke/construct methods, once per each call.
This is contrary to WireGenie::provide*() methods, that resolve the dependencies at the moment of their call and only once, regardless of how many callables are invoked by the provider returned by the methods.

Automatic argument resolution is useful for:

  • async job execution
    • supplying dependencies after a job is deserialized from a queue
  • method dependency injection
    • for controller methods, where dependencies differ between the handler methods
  • generic factories that create instances with varying dependencies

Note that using reflection might have negative performance impact if used heavily.

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 (see WireHelper for full example):

/**
 * Invokes a callable resolving its type-hinted arguments,
 * filling in the unresolved arguments from the static argument pool.
 * Returns the callable's return value.
 * Reading "wire" tags is enabled.
 */ 
public function wiredCall(callable $code, ...$staticArguments)
{
    return WireInvoker::employ(
        $this->wireGenie
    )->invoke($code, ...$staticArguments);
}

This adds a tiny layer for flexibility, in case you decide to tweak the way you wire dependencies later on.

Advanced

Implementing custom logic around WireInvoker's core

It is possible to configure every aspect of WireInvoker.
Pass callables to its constructor to configure how services are wired to invoked callables or created instances.

For exmaple, if every service was accessed by its class name, except the backslashes \ were replaced by dots '.' and in lower case, you could implement the following to invoke $target callable:

$proxy = function(string $identifier, ContainerInterface $container) {
    $key = str_replace('\\', '.', strtolower($identifier)); // alter the service key
    return $container->has($key) ? $container->get($key) : null;
};
new WireInvoker(null, $proxy); // using custom proxy

Example pseudocode for WireGenie

An example with in-depth code comments:

// Given a factory function like the following one:
$factoryFunction = function( /*...dependencies...*/ ){
    // do stuff or create stuff
    return new Service( /*...dependencies...*/ );
};

// Give access to full service container
// or use WireLimiter to limit access to certain services only.
$genie = new WireGenie( $serviceContainer );

// A dependency identifier may be a string key or a class name,
// depending on your container implementation.
// Ath this point, the dependencies are resolved by the container.
$invokableProvider = $genie->provide( /*...dependency-identifier-list...*/ );

// Invoke the factory like this,
$service = $invokableProvider->invoke($factoryFunction);
// or like this.
$service = $invokableProvider($factoryFunction);

Shorthand syntax

As hinted in the example above, the provider instances returned by WireGenie's methods are callable themselves, the following syntax may be used:

// the two lines below are equivalent
$genie->provide( ... )($factoryFunction);
$genie->provide( ... )->invoke($factoryFunction);

// the two lines below are equivalent
$genie->provide( ... )(function( ... ){ ... });
$genie->provide( ... )->invoke(function( ... ){ ... });

The shorthand syntax may also be used with WireInvoker, which itself is callable.

Contributing

Ideas or contribution is welcome. Please send a PR or file an issue.