Skip to content

Commit 0ea52c9

Browse files
committed
added support for injecting services by tags
1 parent 9c7619c commit 0ea52c9

12 files changed

+579
-58
lines changed

src/DI/Attributes/Inject.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
use Attribute;
1313

1414

15-
#[Attribute(Attribute::TARGET_PROPERTY)]
15+
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_PARAMETER)]
1616
class Inject
1717
{
18+
public function __construct(
19+
public readonly ?string $tag = null,
20+
) {
21+
}
1822
}

src/DI/Autowiring.php

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -42,35 +42,66 @@ public function __construct(ContainerBuilder $builder)
4242
* @throws ServiceCreationException when multiple found
4343
*/
4444
public function getByType(string $type, bool $throw = false): ?string
45+
{
46+
return $this->getByTypeAndTag($type, null, $throw);
47+
}
48+
49+
50+
/**
51+
* Resolves service name by type and tag.
52+
* @return ($throw is true ? string : ?string)
53+
* @throws MissingServiceException when not found
54+
* @throws ServiceCreationException when multiple found
55+
*/
56+
public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string
4557
{
4658
$type = Helpers::normalizeClass($type);
4759
$types = $this->highPriority;
48-
if (empty($types[$type])) {
60+
$services = $types[$type] ?? [];
61+
62+
if ($services === []) {
4963
if ($throw) {
5064
if (!class_exists($type) && !interface_exists($type)) {
5165
throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type));
5266
}
53-
5467
throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type));
5568
}
56-
5769
return null;
70+
}
71+
72+
if ($tag !== null) {
73+
$services = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()[$tag] ?? false) !== false);
74+
if ($services === []) {
75+
if ($throw) {
76+
throw new MissingServiceException(sprintf('Service of type %s with tag "%s" not found.', $type, $tag));
77+
}
78+
return null;
79+
}
80+
}
5881

59-
} elseif (count($types[$type]) === 1) {
60-
return $types[$type][0];
61-
62-
} else {
63-
$list = $types[$type];
64-
natsort($list);
65-
$hint = count($list) === 2 && ($tmp = str_contains($list[0], '.') xor str_contains($list[1], '.'))
66-
? '. If you want to overwrite service ' . $list[$tmp ? 0 : 1] . ', give it proper name.'
67-
: '';
68-
throw new ServiceCreationException(sprintf(
69-
"Multiple services of type $type found: %s%s",
70-
implode(', ', $list),
71-
$hint,
72-
));
82+
if (count($services) === 1) {
83+
return reset($services);
7384
}
85+
86+
if ($tag === null) {
87+
$default = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()['default'] ?? false) !== false);
88+
if (count($default) === 1) {
89+
return reset($default);
90+
}
91+
}
92+
93+
natsort($services);
94+
$hint = count($services) === 2 && ($tmp = str_contains($services[0], '.') xor str_contains($services[1], '.'))
95+
? '. If you want to overwrite service ' . $services[$tmp ? 0 : 1] . ', give it proper name.'
96+
: '';
97+
98+
throw new ServiceCreationException(sprintf(
99+
'Multiple services of type %s%s found: %s%s',
100+
$type,
101+
$tag !== null ? " with tag '$tag'" : '',
102+
implode(', ', $services),
103+
$hint,
104+
));
74105
}
75106

76107

src/DI/Container.php

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -236,24 +236,76 @@ public function createService(string $name): object
236236
* Returns an instance of the autowired service of the given type. If it has not been created yet, it creates it.
237237
* @template T of object
238238
* @param class-string<T> $type
239+
* @param bool $throw throw exception if service doesn't exist?
239240
* @return ($throw is true ? T : ?T)
240241
* @throws MissingServiceException
241242
*/
242243
public function getByType(string $type, bool $throw = true): ?object
244+
{
245+
return $this->getByTypeAndTag($type, null, $throw);
246+
}
247+
248+
249+
/**
250+
* Returns an instance of the autowired service of the given type and tag. If it has not been created yet, it creates it.
251+
* @template T of object
252+
* @param class-string<T> $type
253+
* @param bool $throw throw exception if service doesn't exist?
254+
* @return ($throw is true ? T : ?T)
255+
* @throws MissingServiceException
256+
*/
257+
public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = true): ?object
243258
{
244259
$type = Helpers::normalizeClass($type);
245260
if (!empty($this->wiring[$type][0])) {
246-
if (count($names = $this->wiring[$type][0]) === 1) {
247-
return $this->getService($names[0]);
261+
$names = $this->wiring[$type][0];
262+
263+
// Filter by tag if specified
264+
if ($tag !== null) {
265+
$taggedNames = [];
266+
foreach ($names as $name) {
267+
$serviceTags = $this->findByTag($tag);
268+
if (isset($serviceTags[$name])) {
269+
$taggedNames[] = $name;
270+
}
271+
}
272+
$names = $taggedNames;
273+
}
274+
275+
// Try to find service with tag default
276+
if ($tag === null && count($names) > 1) {
277+
$defaultTagNames = [];
278+
foreach ($names as $name) {
279+
if (isset($this->findByTag('default')[$name])) {
280+
$defaultTagNames[] = $name;
281+
}
282+
}
283+
284+
if ($defaultTagNames !== []) {
285+
$names = $defaultTagNames;
286+
}
248287
}
249288

250-
natsort($names);
251-
throw new MissingServiceException(sprintf("Multiple services of type $type found: %s.", implode(', ', $names)));
289+
if (count($names) === 1) {
290+
return $this->getService($names[0]);
291+
} elseif (count($names) > 1) {
292+
natsort($names);
293+
294+
throw new MissingServiceException(sprintf(
295+
'Multiple services of type %s%s found: %s.',
296+
$type,
297+
$tag !== null ? " with tag '$tag'" : '',
298+
implode(', ', $names),
299+
));
300+
}
301+
}
252302

253-
} elseif ($throw) {
303+
if ($throw) {
254304
if (!class_exists($type) && !interface_exists($type)) {
255305
throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type));
256-
} elseif ($this->findByType($type)) {
306+
} elseif ($tag !== null) {
307+
throw new MissingServiceException(sprintf("Service of type %s with tag '%s' not found.", $type, $tag));
308+
} elseif ($this->findByType($type) !== []) {
257309
throw new MissingServiceException(sprintf("Service of type %s is not autowired or is missing in di\u{a0}\u{a0}export\u{a0}\u{a0}types.", $type));
258310
} else {
259311
throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type));
@@ -363,7 +415,7 @@ public function callMethod(callable $function, array $args = []): mixed
363415
private function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array
364416
{
365417
return Resolver::autowireArguments($function, $args, fn(string $type, bool $single) => $single
366-
? $this->getByType($type)
418+
? $this->getByType($type, throw: true)
367419
: array_map($this->getService(...), $this->findAutowired($type)));
368420
}
369421

src/DI/ContainerBuilder.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,20 @@ public function addExcludedClasses(array $types): static
215215
* @throws MissingServiceException
216216
*/
217217
public function getByType(string $type, bool $throw = false): ?string
218+
{
219+
return $this->getByTypeAndTag($type, null, $throw);
220+
}
221+
222+
223+
/**
224+
* Resolves autowired service name by type and tag.
225+
* @return ($throw is true ? string : ?string)
226+
* @throws MissingServiceException
227+
*/
228+
public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string
218229
{
219230
$this->needResolved();
220-
return $this->autowiring->getByType($type, $throw);
231+
return $this->autowiring->getByTypeAndTag($type, $tag, $throw);
221232
}
222233

223234

src/DI/Definitions/Reference.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,23 @@ final class Reference extends Expression
2525

2626
private string $value;
2727

28+
private ?string $tag;
2829

29-
public static function fromType(string $value): static
30+
31+
public static function fromType(string $value, ?string $tag = null): static
3032
{
3133
if (!str_contains($value, '\\')) {
3234
$value = '\\' . $value;
3335
}
3436

35-
return new static($value);
37+
return new static($value, $tag);
3638
}
3739

3840

39-
public function __construct(string $value)
41+
public function __construct(string $value, ?string $tag = null)
4042
{
4143
$this->value = $value;
44+
$this->tag = $tag;
4245
}
4346

4447

@@ -94,7 +97,8 @@ public function complete(DI\Resolver $resolver): void
9497

9598
} elseif ($this->isType()) {
9699
try {
97-
$this->value = $resolver->getByType($this->value)->value;
100+
$reference = $resolver->getByType($this->value, $this->tag);
101+
$this->value = $reference->value;
98102
} catch (DI\NotAllowedDuringResolvingException) {
99103
}
100104
return;

0 commit comments

Comments
 (0)