The Inject Attribute

By annotating function or method parameters with an Inject attribute, you can tell the resolvers and, consequently, the creator how to obtain arguments that cannot be resolved otherwise or to apply arguments that would not be used by default. This means that you can use it to override Wire's default behavior, for example when you want to choose one of several alternatives or when there are literal arguments such as strings, numbers or arrays expected.

Example

Let's assume you have two different functions, each of which requires a Model object as input. But you want to ensure that one of the functions always receives a SubModel instance, which is a subclass of Model. The following example shows how to accomplish that:

use Conia\Wire\Inject;
use Conia\Wire\Wire;

class Model
{
}

class SubModel extends Model
{
}

function expectsModel(Model $model): Model
{
    return $model;
}

function alsoExpectsModel(
    #[Inject(SubModel::class)]
    Model $model
): Model {
    return $model;
}

$resolver = Wire::callableResolver();

// `expectsModel` is not annotated. An object of the base class is created
$args = $resolver->resolve('expectsModel');
$result = expectsModel(...$args);
assert($result instanceof Model);

// `alsoExpectsModel` is annotated. An object of the sub class is created
$args = $resolver->resolve('alsoExpectsModel');
$result = alsoExpectsModel(...$args);
assert($result instanceof SubModel);

You can control the behavior of the function (in this case, alsoExpectsModel) by annotating the parameter it with an Inject attribute. If the parameter is not annotated, the resolver would create an object of the base class Model because the type of the parameter $model is Model.

How to use

Simply add the Inject attribute to a parameter of a callable or constructor that you want to control. You pass a mandatory argument with the value you want the callable's argument to have, or, if it's not a literal, with an identifier from which the value is derived (see below for a detailed description).

The snippet below shows the relevant part of the example above:

function alsoExpectsModel(
    #[Inject(SubModel::class)]
    Model $model
): Model
{ ...

The Inject instance

The first parameter $value of the Inject constructor is required and of type mixed. The second parameter $type is optional and of type Conia\Wire\Type which is a enum. Both are availabe as public instance properties. Every additional argument is avalable via the meta property.

use Conia\Wire\Inject;
use Conia\Wire\Type;

$inject = new Inject('value', Type::Literal, text: 'string', number: 13);

assert($inject->value === 'value');
assert($inject->type === Type::Literal);
assert($inject->meta['text'] === 'string');
assert($inject->meta['number'] === 13);

$inject = new Inject('value', text: 'string', number: 13);

assert($inject->value === 'value');
assert($inject->type === null);
assert($inject->meta['text'] === 'string');
assert($inject->meta['number'] === 13);

$inject = new Inject('value', null, 31, 73, 'text');

assert($inject->value === 'value');
assert($inject->type === null);
assert($inject->meta === [31, 73, 'text']);

Note

In most cases, you will only work directly with an Inject instance if you use the Inject type Type::Callback. See below.

How injected argument values are determined

The resolvers behave differently depending on the type of value that you want to be injected.

Warning

The resolver does not check if a value which was obtained with the help of an Inject attribute matches the parameters type of the callable it should be applied to, so handle with care.

Strings

If the value is a string, like in the following example:

#[Inject('container.id')]
// or
#[Inject(SubModel::class)]

the resolver uses the following rules.

  1. If a container is available, see if it has an entry with and id matching the value of the string. If so, return it, if not continue with step 2.
  2. If the string is the full qualified name of an existing class, try to create it using the creator and return it. If not, continue with step 3.
  3. Return the string as-is.

The literal rest

All other types, like arrays, numbers, booleans or null values are passed to the callable or are returned by the resolver as they are, i. e. as unchanged literals.

function withLiteralParams(
     #[Inject(['number' => 13, 'str' => 'value'])]
     array $arrayParam,

     #[Inject(73)]
     int $integerParam,

     #[Inject(13.37)]
     float $floatParam,

     #[Inject(true)]
     bool $booleanParam,

     #[Inject(null)]
     ?string $nullableParam,
) { ...

Don't follow the rules

If you want to bypass the string rules or be explicit about the values you inject, you can specifiy the type of the injected value. Additionally, with that feature, you can have control over how a value is generated.

The inject type is passed as second argument to the Inject attribute und must be of the data type Conia\Wire\Type:

// a valid array
#[Inject('value', Conia\Wire\Type::Literal)]

The available types are:

Conia\Wire\Type::Literal

Returns the value as is.

#[Inject('a string value', Type::Literal)]
public function myCallable(string $value): void 

Conia\Wire\Type::Entry

Uses the value as id to fetch a value from the container.

$container->add('container.entry.id', new Object());

public function myCallable(
    #[Inject('container.entry.id', Type::Entry)]
    Object $value
): void 
$container->add(\Your\Interface::class, new Object());

public function myCallable(
    #[Inject(\Your\Interface::class, Type::Entry)]
    \Your\Interface $value
): void 

Conia\Wire\Type::Create

Must be a fully qualified class name which the creator attemtps to create.

public function myCallable(
     #[Inject(SubModel::class, Type::Create)] 
     Model $value
): void 

Conia\Wire\Type::Env

The value is assumed to be the name an environment variable. It attempts to read the environment variable using PHP's internal function getenv and then returns its value.


public function myCallable(
    #[Inject('PATH', Type::Env)]
    string|bool $value
): void {
    // $value has now the content of the environment variable PATH
}

Conia\Wire\Type::Callback

All resolving methods, like Creator::create or CallableResolver::resolve, accept a callback function for the parameter $injectCallback that will be passed all Inject attributes of type Type::Callback. The returned value of the callback is then used for the annotated parameter.

use Conia\Wire\Inject;
use Conia\Wire\Tests\Fixtures\Container;
use Conia\Wire\Type;
use Conia\Wire\Wire;

class Value
{
    public function __construct(public readonly string $str)
    {
    }
}

class Model
{
    public function __construct(
        #[Inject(Value::class, Type::Callback, tag: 'tag2')]
        public readonly Value $value
    ) {
    }
}

$container = new Container();
$container->add(Value::class . 'tag1', new Value('Tagged with 1'));
$container->add(Value::class . 'tag2', new Value('Tagged with 2'));

$creator = Wire::creator($container);

$model = $creator->create(
    Model::class,
    injectCallback: function (Inject $inject) use ($container): mixed {
        return $container->get($inject->value . $inject->meta['tag']);
    }
);

assert($model instanceof Model);
assert($model->value->str === 'Tagged with 2');