Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,16 @@
class AsTaggedItem
{
/**
* @param string|null $index The index at which the service will be found when consuming tagged iterators/locators
* @param int|null $priority The priority of the service in iterators/locators; the higher the number, the earlier it will
* @param string|null $index The index at which the service will be found when consuming tagged iterators/locators
* @param int|null $priority The priority of the service in iterators/locators; the higher the number, the earlier it will
* @param string|string[]|null $before Service ids or classes this service must be placed before
* @param string|string[]|null $after Service ids or classes this service must be placed after
*/
public function __construct(
public ?string $index = null,
public ?int $priority = null,
public string|array|null $before = null,
public string|array|null $after = null,
) {
}
}
2 changes: 2 additions & 0 deletions src/Symfony/Component/DependencyInjection/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ CHANGELOG
8.2
---

* Order tagged services with the `before` and `after` tag attributes, also available as arguments of `#[AsTaggedItem]`
* Add `BeforeAfterSorter` to order a list according to `before`/`after` constraints
* Name the package to install when an extension is missing, for the configuration keys declared in the `.container.extension_packages` build parameter
* Add the `container.remove_if_missing` tag to drop a definition when a service, a class or a package it needs is not there
* Add `ContainerBuilder::setExtensionConfig()`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

/**
* Reorders items to satisfy "before" and "after" constraints, keeping the seed order everywhere else.
*
* Items are emitted depth-first in seed order, which keeps the seed order intact wherever the
* constraints allow it. "A before B" and "B after A" describe the same edge and yield the same
* order. References to items that are not in the seed are ignored: the package declaring them may
* simply not be installed. An item referencing itself is ignored too.
*
* @author Nicolas Grekas <[email protected]>
*/
final class BeforeAfterSorter
{
/**
* @param list<string> $seed Items in the order they would have without any constraint
* @param array<string, array{before?: list<string>, after?: list<string>}> $constraints
* @param array<string, list<string>> $aliases Alternative names, each designating the items it stands for; a name that is not listed here designates the item bearing it
*
* @return list<string>
*
* @throws InvalidArgumentException when the constraints are cyclic
*/
public static function sort(array $seed, array $constraints, array $aliases = []): array
{
if (!$constraints) {
return $seed;
}

$predecessors = array_fill_keys($seed, []);

foreach ($constraints as $item => $constraint) {
if (!isset($predecessors[$item])) {
continue;
}

foreach (['before', 'after'] as $direction) {
foreach ($constraint[$direction] ?? [] as $target) {
foreach ($aliases[$target] ?? [$target] as $targetItem) {
if ($targetItem === $item || !isset($predecessors[$targetItem])) {
continue;
}

if ('before' === $direction) {
$predecessors[$targetItem][] = $item;
} else {
$predecessors[$item][] = $targetItem;
}
}
}
}
}

$sorted = [];
$states = [];

foreach ($seed as $item) {
self::visit($item, $predecessors, $states, $sorted, []);
}

return $sorted;
}

/**
* @param array<string, list<string>> $predecessors
* @param array<string, int> $states
* @param list<string> $sorted
* @param list<string> $path
*/
private static function visit(string $item, array $predecessors, array &$states, array &$sorted, array $path): void
{
if (2 === ($states[$item] ?? 0)) {
return;
}

if (1 === ($states[$item] ?? 0)) {
$cycle = \array_slice($path, array_search($item, $path, true));
$cycle[] = $item;

throw new InvalidArgumentException(\sprintf('Cycle detected in the "before"/"after" constraints: "%s".', implode('" -> "', $cycle)));
}

$states[$item] = 1;
$path[] = $item;

foreach ($predecessors[$item] as $predecessor) {
self::visit($predecessor, $predecessors, $states, $sorted, $path);
}

$states[$item] = 2;
$sorted[] = $item;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam

$parameterBag = $container->getParameterBag();
$services = [];
$constraints = [];
$classById = [];

foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $attributes) {
if (\in_array($serviceId, $exclude, true)) {
Expand All @@ -60,23 +62,29 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam

$defaultPriority = $defaultAttributePriority = null;
$defaultIndex = $defaultAttributeIndex = null;
$attributeConstraints = [];
$indexes = [];
$definition = $container->getDefinition($serviceId);
$class = $definition->getClass();
$class = $container->getParameterBag()->resolveValue($class) ?: null;
$classById[$serviceId] = $class;
$reflector = null !== $class ? $container->getReflectionClass($class) : null;
$phpAttributes = $definition->isAutoconfigured() && !$definition->hasTag('container.ignore_attributes') ? $reflector?->getAttributes(AsTaggedItem::class) : [];

foreach ($phpAttributes ??= [] as $i => $attribute) {
$attribute = $attribute->newInstance();
$phpAttributes[$i] = [
'priority' => $attribute->priority,
'before' => $attribute->before,
'after' => $attribute->after,
$indexAttribute ?? '' => $attribute->index,
];
if (null === $defaultAttributePriority) {
$defaultAttributePriority = $attribute->priority ?? 0;
$defaultAttributeIndex = $attribute->index;
}
$attributeConstraints['before'] ??= $attribute->before;
$attributeConstraints['after'] ??= $attribute->after;
}
if (1 >= \count($phpAttributes)) {
$phpAttributes = [];
Expand Down Expand Up @@ -113,6 +121,14 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam
}
$priority ??= $defaultPriority ??= 0;

foreach (['before', 'after'] as $direction) {
$targets = \array_key_exists($direction, $attribute) ? $attribute[$direction] : ($attributeConstraints[$direction] ?? null);

if ($targets = (array) ($targets ?? [])) {
$constraints[$serviceId][$direction] = array_merge($constraints[$serviceId][$direction] ?? [], $targets);
}
}

if (null === $indexAttribute && !$defaultIndexMethod && !$needsIndexes) {
$services[] = [$priority, $i, null, $serviceId, null];
continue 2;
Expand Down Expand Up @@ -146,6 +162,10 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam

uasort($services, static fn ($a, $b) => $b[0] <=> $a[0] ?: $a[1] <=> $b[1]);

if ($constraints) {
$services = PriorityTaggedServiceUtil::applyConstraints($services, $constraints, $classById, $tagName);
}

$refs = [];
foreach ($services as [, , $index, $serviceId, $class]) {
$reference = match (true) {
Expand All @@ -170,6 +190,48 @@ private function findAndSortTaggedServices(string|TaggedIteratorArgument $tagNam
*/
class PriorityTaggedServiceUtil
{
/**
* @param array<array{0: int, 1: int, 2: string|null, 3: string, 4: string|null}> $services
* @param array<string, array{before?: list<string>, after?: list<string>}> $constraints
* @param array<string, string|null> $classById
*
* @return list<array{0: int, 1: int, 2: string|null, 3: string, 4: string|null}>
*/
public static function applyConstraints(array $services, array $constraints, array $classById, string $tagName): array
{
$entries = [];
$aliases = [];

foreach ($services as $service) {
$entries[$service[3]][] = $service;

if (null !== $class = $classById[$service[3]] ?? null) {
$aliases[$class][$service[3]] = $service[3];
}
}

// a service id always designates itself, whatever class it happens to share a name with
foreach ($entries as $serviceId => $service) {
$aliases[$serviceId] = [$serviceId];
}

try {
$sortedIds = BeforeAfterSorter::sort(array_keys($entries), $constraints, array_map(array_values(...), $aliases));
} catch (InvalidArgumentException $e) {
throw new InvalidArgumentException(\sprintf('Invalid "before"/"after" constraints on tag "%s": ', $tagName).lcfirst($e->getMessage()), previous: $e);
}

$sorted = [];

foreach ($sortedIds as $serviceId) {
foreach ($entries[$serviceId] as $service) {
$sorted[] = $service;
}
}

return $sorted;
}

public static function getDefault(string $serviceId, \ReflectionClass $r, string $defaultMethod, string $tagName, ?string $indexAttribute): string|int|null
{
if ($r->isInterface() || !$r->hasMethod($defaultMethod)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\DependencyInjection\Tests\Compiler;

use PHPUnit\Framework\TestCase;
use Symfony\Component\DependencyInjection\Compiler\BeforeAfterSorter;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;

class BeforeAfterSorterTest extends TestCase
{
public function testSeedOrderIsKeptWhenThereIsNoConstraint()
{
$this->assertSame(['a', 'b', 'c'], BeforeAfterSorter::sort(['a', 'b', 'c'], []));
}

public function testBeforeMovesTheConstrainedItemNotItsTarget()
{
$this->assertSame(['c', 'a', 'b'], BeforeAfterSorter::sort(['a', 'b', 'c'], [
'c' => ['before' => ['a']],
]));
}

public function testAfterMovesTheConstrainedItemNotItsTarget()
{
$this->assertSame(['b', 'a', 'c'], BeforeAfterSorter::sort(['a', 'b', 'c'], [
'a' => ['after' => ['b']],
]));
}

public function testAfterIsTheMirrorOfBefore()
{
$withBefore = BeforeAfterSorter::sort(['a', 'b', 'c'], ['c' => ['before' => ['a']]]);
$withAfter = BeforeAfterSorter::sort(['a', 'b', 'c'], ['a' => ['after' => ['c']]]);

$this->assertSame($withBefore, $withAfter);
$this->assertSame(['c', 'a', 'b'], $withAfter);
}

public function testAnItemIsInsertedBetweenTwoOthers()
{
$this->assertSame(['b', 'e', 'c'], BeforeAfterSorter::sort(['b', 'c', 'e'], [
'e' => ['after' => ['b'], 'before' => ['c']],
]));
}

public function testOnlyTheConstrainedItemMovesInALongerList()
{
$this->assertSame(['a', 'j', 'b', 'c', 'd', 'e'], BeforeAfterSorter::sort(['a', 'b', 'c', 'd', 'e', 'j'], [
'j' => ['before' => ['b']],
]));
}

public function testConstraintsAreTransitive()
{
$this->assertSame(['c', 'b', 'a'], BeforeAfterSorter::sort(['a', 'b', 'c'], [
'b' => ['before' => ['a']],
'c' => ['before' => ['b']],
]));
}

public function testReferencesToUnknownItemsAreIgnored()
{
$this->assertSame(['a', 'b'], BeforeAfterSorter::sort(['a', 'b'], [
'a' => ['before' => ['not_in_the_collection']],
'b' => ['after' => ['gone_too']],
]));
}

public function testConstraintsOnUnknownItemsAreIgnored()
{
$this->assertSame(['a', 'b'], BeforeAfterSorter::sort(['a', 'b'], [
'not_in_the_collection' => ['before' => ['a']],
]));
}

public function testMultipleTargetsAreSupported()
{
$this->assertSame(['c', 'a', 'b'], BeforeAfterSorter::sort(['a', 'b', 'c'], [
'c' => ['before' => ['a', 'b']],
]));
}

public function testADirectCycleIsReported()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cycle detected in the "before"/"after" constraints: "a" -> "b" -> "a".');

BeforeAfterSorter::sort(['a', 'b'], [
'a' => ['before' => ['b']],
'b' => ['before' => ['a']],
]);
}

public function testAnIndirectCycleIsReported()
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Cycle detected in the "before"/"after" constraints: "a" -> "c" -> "b" -> "a".');

BeforeAfterSorter::sort(['a', 'b', 'c'], [
'a' => ['before' => ['b']],
'b' => ['before' => ['c']],
'c' => ['before' => ['a']],
]);
}

public function testAnItemReferencingItselfIsIgnored()
{
$this->assertSame(['a', 'b'], BeforeAfterSorter::sort(['a', 'b'], ['a' => ['before' => ['a']]]));
}

public function testAnAliasDesignatesTheItemsItStandsFor()
{
$this->assertSame(['c', 'a', 'b'], BeforeAfterSorter::sort(['a', 'b', 'c'], [
'c' => ['before' => ['Some\\Class']],
], ['Some\\Class' => ['a']]));
}

public function testAnAliasCanDesignateSeveralItems()
{
$this->assertSame(['c', 'a', 'b'], BeforeAfterSorter::sort(['a', 'b', 'c'], [
'c' => ['before' => ['Some\\Class']],
], ['Some\\Class' => ['a', 'b']]));
}

public function testAnAliasResolvingToTheItemItselfIsIgnored()
{
$this->assertSame(['a', 'b'], BeforeAfterSorter::sort(['a', 'b'], [
'a' => ['before' => ['Some\\Class']],
], ['Some\\Class' => ['a']]));
}
}
Loading
Loading