Skip to content

[DependencyInjection] Fix ServiceLocatorTagPass indexes handling #60691

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 6, 2025

Conversation

MatTheCat
Copy link
Contributor

Q A
Branch? 7.2
Bug fix? yes
New feature? no
Deprecations? no
Issues Fix #60651
License MIT

#50578 changed the ServiceLocatorTagPass’ behavior so that if the map is a list, its indexes always are replaced by the corresponding reference ID, even if they come from the PriorityTaggedServiceTrait. That means if your service map ends up as a list because e.g. your default_index_method returned contiguous integer, its indexes will be replaced.

This PR reverts this change so that it works the same than v6.

@stof
Copy link
Member

stof commented Jun 4, 2025

Can you add a test covering the case that broke, to prevent regressions ?

@MatTheCat
Copy link
Contributor Author

Added testServicesKeysAreKept.

@nicolas-grekas
Copy link
Member

I think I figured out a better fix. Can you please have a look and update the PR + tests potentially?

diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
index 2e649bdeaa..e0a1fd3610 100644
--- a/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
+++ b/src/Symfony/Component/DependencyInjection/Attribute/AsTaggedItem.php
@@ -21,7 +21,7 @@ class AsTaggedItem
 {
     /**
      * @param string|null $index    The property or method to use to index the item in the locator
-     * @param int|null    $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the locator
+     * @param int|null    $priority The priority of the item; the higher the number, the earlier the tagged service will be located in the iterator/locator
      */
     public function __construct(
         public ?string $index = null,
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
index 77a1d7ef8f..e3a4eba275 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
@@ -87,8 +87,7 @@ trait PriorityTaggedServiceTrait
                 if (null === $index && null === $defaultIndex && $defaultPriorityMethod && $class) {
                     $defaultIndex = PriorityTaggedServiceUtil::getDefault($container, $serviceId, $class, $defaultIndexMethod ?? 'getDefaultName', $tagName, $indexAttribute, $checkTaggedItem);
                 }
-                $decorated = $definition->getTag('container.decorator')[0]['id'] ?? null;
-                $index = $index ?? $defaultIndex ?? $defaultIndex = $decorated ?? $serviceId;
+                $index ??= $defaultIndex ??= $definition->getTag('container.decorator')[0]['id'] ?? $serviceId;
 
                 $services[] = [$priority, ++$i, $index, $serviceId, $class];
             }
diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
index 81c14ac5cc..6de3e5ed4d 100644
--- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
+++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceLocatorTagPass.php
@@ -54,17 +54,41 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass
             $value->setClass(ServiceLocator::class);
         }
 
-        $services = $value->getArguments()[0] ?? null;
+        $values = $value->getArguments()[0] ?? null;
+        $services = [];
 
-        if ($services instanceof TaggedIteratorArgument) {
-            $services = $this->findAndSortTaggedServices($services, $this->container);
-        }
-
-        if (!\is_array($services)) {
+        if ($values instanceof TaggedIteratorArgument) {
+            foreach ($this->findAndSortTaggedServices($values, $this->container) as $k => $v) {
+                $services[$k] = new ServiceClosureArgument($v);
+            }
+        } elseif (!\is_array($values)) {
             throw new InvalidArgumentException(\sprintf('Invalid definition for service "%s": an array of references is expected as first argument when the "container.service_locator" tag is set.', $this->currentId));
+        } else {
+            $i = 0;
+
+            foreach ($values as $k => $v) {
+                if ($v instanceof ServiceClosureArgument) {
+                    $services[$k] = $v;
+                    continue;
+                }
+
+                if ($i === $k) {
+                    if ($v instanceof Reference) {
+                        $k = (string) $v;
+                    }
+                    ++$i;
+                } elseif (\is_int($k)) {
+                    $i = null;
+                }
+
+                $services[$k] = new ServiceClosureArgument($v);
+            }
+            if (0 === $i) {
+                ksort($services);
+            }
         }
 
-        $value->setArgument(0, self::map($services));
+        $value->setArgument(0, $services);
 
         $id = '.service_locator.'.ContainerBuilder::hash($value);
 
@@ -83,8 +107,12 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass
 
     public static function register(ContainerBuilder $container, array $map, ?string $callerId = null): Reference
     {
+        foreach ($map as $k => $v) {
+            $map[$k] = new ServiceClosureArgument($v);
+        }
+
         $locator = (new Definition(ServiceLocator::class))
-            ->addArgument(self::map($map))
+            ->addArgument($map)
             ->addTag('container.service_locator');
 
         if (null !== $callerId && $container->hasDefinition($callerId)) {
@@ -109,29 +137,4 @@ final class ServiceLocatorTagPass extends AbstractRecursivePass
 
         return new Reference($id);
     }
-
-    public static function map(array $services): array
-    {
-        $i = 0;
-
-        foreach ($services as $k => $v) {
-            if ($v instanceof ServiceClosureArgument) {
-                continue;
-            }
-
-            if ($i === $k) {
-                if ($v instanceof Reference) {
-                    unset($services[$k]);
-                    $k = (string) $v;
-                }
-                ++$i;
-            } elseif (\is_int($k)) {
-                $i = null;
-            }
-
-            $services[$k] = new ServiceClosureArgument($v);
-        }
-
-        return $services;
-    }
 }

@MatTheCat
Copy link
Contributor Author

It seems the only change is the

if (0 === $i) {
    ksort($services);
}

bit? It doesn’t affect the tests though.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Jun 6, 2025

There's also the foreach ($this->findAndSortTaggedServices being unprocessed in case the args are TaggedIteratorArgument (which means we respect the order managed by this call)
Not sure that's covered actually.
The ksort is present on 6.4 and it allows aggregating locators that have the same services in when when they're declared as a list.

@MatTheCat
Copy link
Contributor Author

Not sure that's covered actually.

Will add tests if needed.

Not sure what “aggregating locators” means however?

@nicolas-grekas
Copy link
Member

Not sure what “aggregating locators” means however?

if one service defines it needs foo and bar, and another needs bar and foo

@MatTheCat MatTheCat changed the title [DependencyInjection] Keep map indexes in ServiceLocatorTagPass::register [DependencyInjection] Fix ServiceLocatorTagPass indexes handling Jun 6, 2025
@MatTheCat
Copy link
Contributor Author

MatTheCat commented Jun 6, 2025

Just noticed 0 === $i doesn’t mean the services are declared as a list; shouldn’t this line be count($services) === $i? Or 0 !== $i?

@nicolas-grekas
Copy link
Member

count($services) === $i works for me

@nicolas-grekas
Copy link
Member

Thank you @MatTheCat.

@nicolas-grekas nicolas-grekas merged commit 16efe96 into symfony:7.2 Jun 6, 2025
8 of 11 checks passed
@MatTheCat
Copy link
Contributor Author

What about testInheritedKeyOverwritesPreviousServiceWithKey? If you simply move the 16 reference on top of the map it will fail; is that expected?

@MatTheCat MatTheCat deleted the ticket_60651 branch June 6, 2025 16:09
@nicolas-grekas
Copy link
Member

It'd say yes. Such lists of services are weird anyway...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy