Skip to content

Commit a79adc1

Browse files
committed
added support for injecting services by tags
1 parent e3a59b6 commit a79adc1

14 files changed

+591
-61
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
@@ -40,35 +40,66 @@ public function __construct(ContainerBuilder $builder)
4040
* @throws ServiceCreationException when multiple found
4141
*/
4242
public function getByType(string $type, bool $throw = false): ?string
43+
{
44+
return $this->getByTypeAndTag($type, null, $throw);
45+
}
46+
47+
48+
/**
49+
* Resolves service name by type and tag.
50+
* @return ($throw is true ? string : ?string)
51+
* @throws MissingServiceException when not found
52+
* @throws ServiceCreationException when multiple found
53+
*/
54+
public function getByTypeAndTag(string $type, ?string $tag = null, bool $throw = false): ?string
4355
{
4456
$type = Helpers::normalizeClass($type);
4557
$types = $this->highPriority;
46-
if (empty($types[$type])) {
58+
$services = $types[$type] ?? [];
59+
60+
if ($services === []) {
4761
if ($throw) {
4862
if (!class_exists($type) && !interface_exists($type)) {
4963
throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type));
5064
}
51-
5265
throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type));
5366
}
54-
5567
return null;
68+
}
69+
70+
if ($tag !== null) {
71+
$services = array_filter($services, fn(string $name) => ($this->builder->getDefinition($name)->getTags()[$tag] ?? false) !== false);
72+
if ($services === []) {
73+
if ($throw) {
74+
throw new MissingServiceException(sprintf('Service of type %s with tag "%s" not found.', $type, $tag));
75+
}
76+
return null;
77+
}
78+
}
5679

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

74105

src/DI/Container.php

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

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

252-
} elseif ($throw) {
302+
if ($throw) {
253303
if (!class_exists($type) && !interface_exists($type)) {
254304
throw new MissingServiceException(sprintf("Service of type '%s' not found. Check the class name because it cannot be found.", $type));
255-
} elseif ($this->findByType($type)) {
305+
} elseif ($tag !== null) {
306+
throw new MissingServiceException(sprintf("Service of type %s with tag '%s' not found.", $type, $tag));
307+
} elseif ($this->findByType($type) !== []) {
256308
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));
257309
} else {
258310
throw new MissingServiceException(sprintf('Service of type %s not found. Did you add it to configuration file?', $type));
@@ -359,7 +411,7 @@ public function callMethod(callable $function, array $args = []): mixed
359411
private function autowireArguments(\ReflectionFunctionAbstract $function, array $args = []): array
360412
{
361413
return Resolver::autowireArguments($function, $args, fn(string $type, bool $single) => $single
362-
? $this->getByType($type)
414+
? $this->getByType($type, throw: true)
363415
: array_map($this->getService(...), $this->findAutowired($type)));
364416
}
365417

src/DI/ContainerBuilder.php

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

222233

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)