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(
(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.
- 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.
- 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.
- 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');