Skip to content

Created a trait to sort tagged services #18482

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

Closed
wants to merge 3 commits into from
Closed
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 @@ -11,9 +11,9 @@

namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

/**
* Registers the cache warmers.
Expand All @@ -22,6 +22,8 @@
*/
class AddCacheWarmerPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

/**
* {@inheritdoc}
*/
Expand All @@ -31,20 +33,12 @@ public function process(ContainerBuilder $container)
return;
}

$warmers = array();
foreach ($container->findTaggedServiceIds('kernel.cache_warmer') as $id => $attributes) {
$priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0;
$warmers[$priority][] = new Reference($id);
}
$warmers = $this->findAndSortTaggedServices('kernel.cache_warmer', $container);

if (empty($warmers)) {
return;
}

// sort by priority and flatten
krsort($warmers);
$warmers = call_user_func_array('array_merge', $warmers);

$container->getDefinition('cache_warmer')->replaceArgument(0, $warmers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Adds services tagged config_cache.resource_checker to the config_cache_factory service, ordering them by priority.
Expand All @@ -23,23 +23,16 @@
*/
class ConfigCachePass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

public function process(ContainerBuilder $container)
{
$resourceCheckers = array();

foreach ($container->findTaggedServiceIds('config_cache.resource_checker') as $id => $tags) {
$priority = isset($tags[0]['priority']) ? $tags[0]['priority'] : 0;
$resourceCheckers[$priority][] = new Reference($id);
}
$resourceCheckers = $this->findAndSortTaggedServices('config_cache.resource_checker', $container);

if (empty($resourceCheckers)) {
return;
}

// sort by priority and flatten
krsort($resourceCheckers);
$resourceCheckers = call_user_func_array('array_merge', $resourceCheckers);

$container->getDefinition('config_cache_factory')->replaceArgument(0, $resourceCheckers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Gathers and configures the argument value resolvers.
Expand All @@ -22,6 +22,8 @@
*/
class ControllerArgumentValueResolverPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('argument_resolver')) {
Expand All @@ -32,34 +34,4 @@ public function process(ContainerBuilder $container)
$argumentResolvers = $this->findAndSortTaggedServices('controller.argument_value_resolver', $container);
$definition->replaceArgument(1, $argumentResolvers);
}

/**
* Finds all services with the given tag name and order them by their priority.
*
* @param string $tagName
* @param ContainerBuilder $container
*
* @return array
*/
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
{
$services = $container->findTaggedServiceIds($tagName);

$sortedServices = array();
foreach ($services as $serviceId => $tags) {
foreach ($tags as $attributes) {
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$sortedServices[$priority][] = new Reference($serviceId);
}
}

if (empty($sortedServices)) {
return array();
}

krsort($sortedServices);

// Flatten the array
return call_user_func_array('array_merge', $sortedServices);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Adds extractors to the property_info service.
Expand All @@ -22,6 +22,8 @@
*/
class PropertyInfoPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

/**
* {@inheritdoc}
*/
Expand All @@ -45,34 +47,4 @@ public function process(ContainerBuilder $container)
$accessExtractors = $this->findAndSortTaggedServices('property_info.access_extractor', $container);
$definition->replaceArgument(3, $accessExtractors);
}

/**
* Finds all services with the given tag name and order them by their priority.
*
* @param string $tagName
* @param ContainerBuilder $container
*
* @return array
*/
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
{
$services = $container->findTaggedServiceIds($tagName);

$sortedServices = array();
foreach ($services as $serviceId => $tags) {
foreach ($tags as $attributes) {
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$sortedServices[$priority][] = new Reference($serviceId);
}
}

if (empty($sortedServices)) {
return array();
}

krsort($sortedServices);

// Flatten the array
return call_user_func_array('array_merge', $sortedServices);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@

namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

/**
* Adds all services with the tags "serializer.encoder" and "serializer.normalizer" as
Expand All @@ -23,6 +23,8 @@
*/
class SerializerPass implements CompilerPassInterface
{
use PriorityTaggedServiceTrait;

public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('serializer')) {
Expand All @@ -31,42 +33,17 @@ public function process(ContainerBuilder $container)

// Looks for all the services tagged "serializer.normalizer" and adds them to the Serializer service
$normalizers = $this->findAndSortTaggedServices('serializer.normalizer', $container);

if (empty($normalizers)) {
throw new \RuntimeException('You must tag at least one service as "serializer.normalizer" to use the Serializer service');
}
$container->getDefinition('serializer')->replaceArgument(0, $normalizers);

// Looks for all the services tagged "serializer.encoders" and adds them to the Serializer service
$encoders = $this->findAndSortTaggedServices('serializer.encoder', $container);
$container->getDefinition('serializer')->replaceArgument(1, $encoders);
}

/**
* Finds all services with the given tag name and order them by their priority.
*
* @param string $tagName
* @param ContainerBuilder $container
*
* @return array
*
* @throws \RuntimeException
*/
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
{
$services = $container->findTaggedServiceIds($tagName);

if (empty($services)) {
throw new \RuntimeException(sprintf('You must tag at least one service as "%s" to use the Serializer service', $tagName));
}

$sortedServices = array();
foreach ($services as $serviceId => $tags) {
foreach ($tags as $attributes) {
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$sortedServices[$priority][] = new Reference($serviceId);
}
if (empty($encoders)) {
throw new \RuntimeException('You must tag at least one service as "serializer.encoder" to use the Serializer service');
}

krsort($sortedServices);

// Flatten the array
return call_user_func_array('array_merge', $sortedServices);
$container->getDefinition('serializer')->replaceArgument(1, $encoders);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function testThatCacheWarmersAreProcessedInPriorityOrder()
$services = array(
'my_cache_warmer_service1' => array(0 => array('priority' => 100)),
'my_cache_warmer_service2' => array(0 => array('priority' => 200)),
'my_cache_warmer_service3' => array(),
'my_cache_warmer_service3' => array(0 => array()),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

had to fix this in the tests because the container method wouldn't return like this, same goes for the other test.

);

$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function testThatCheckersAreProcessedInPriorityOrder()
$services = array(
'checker_2' => array(0 => array('priority' => 100)),
'checker_1' => array(0 => array('priority' => 200)),
'checker_3' => array(),
'checker_3' => array(0 => array()),
);

$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
Expand Down Expand Up @@ -52,7 +52,6 @@ public function testThatCheckersAreProcessedInPriorityOrder()

public function testThatCheckersCanBeMissing()
{
$definition = $this->getMock('Symfony\Component\DependencyInjection\Definition');
$container = $this->getMock(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was unused

'Symfony\Component\DependencyInjection\ContainerBuilder',
array('findTaggedServiceIds')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

/**
* Trait that allows a generic method to find and sort service by priority option in the tag.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
trait PriorityTaggedServiceTrait
{
/**
* Finds all services with the given tag name and order them by their priority.
*
* @param string $tagName
* @param ContainerBuilder $container
*
* @return Reference[]
*/
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we consider private methods in trait as part of the api and of the bc promises?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think so as they are public to the class they are used in

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would need an update on the bc policy!

{
$services = $container->findTaggedServiceIds($tagName);

$queue = new \SplPriorityQueue();

Copy link
Contributor

@HeahDude HeahDude Apr 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR made me think about handling priorities for formatters in #18450, but even if I love @javiereguiluz proposal to use \SplPriorityQueue (btw thanks for the heads-up :), I did not use it because I want to check that the same class is not used for many instances.

So I'm wondering even if the context is a bit different here, what about checking classes of tagged services to prevent one class to be registered with many service ids? Is this an expected possibility? Shouldn't we throw an exception in such case and add a test for it?

We could hold a $classes = array() before the foreach loops to do something like:

$services = $container->findTaggedServiceIds($tagName);
$queue = new \SplPriorityQueue();
$classes = array();

foreach ($services as $serviceId => $tags) {
    $serviceClass = $container->findDefinition($serviceId)->getClass();
    if ($serviceClass && isset($classes[$serviceClass]) {
        throw new InvalidArgumentException(sprintf('The service "%s" has the same "%s" class as service "%s"', $serviceId, $serviceClass, $classes[$serviceClass]);
    }
    $classes[$attributes['class']] = $serviceId;
    foreach ($tags as $attributes) {
        $priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
        $queue->insert(new Reference($serviceId), $priority * -1);
    }

    return iterator_to_array($queue);
}

What do you think ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've thought about the situation but that would be a BC break as that case is possible right now

Copy link
Contributor

@HeahDude HeahDude Apr 21, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could still trigger a warning as silenced error to throw an exception in 4.0 (if it worths it)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there can be legit use-cases where you would want to something twice with a different priority.. I just don't think it's a smart idea. @stof usually you have a very good idea about things like this, what do you think?

Adding a deprecation warning and adding it anyway (behavioral change only in 4.0) seems fine to me.

Copy link
Member

@GromNaN GromNaN Apr 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having several instances of the same class is a reasonable expectation. Think in terms of dependency injection: you can define 2 services using the same class and having a different configuration.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I think @HeahDude is referring to is not once a class, but once a service

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GromNaN @phansys, I understand your POV, and I agree in a global scope and I don't know about cache warmers.
It can also be very common when you think about factories. But regarding this trait, it's about collecting collections of services (mainly sharing the same helper interface).
We use their ids but we actually need an instance of each class. It sounds really weird to me that a service in the cases used here (e.g serializer, argument resolver) uses the same helper class (e.g encoders, value resolvers) with two different instances and configuration.

If there is a legit use case for it, my guess is that there is also a chance that happens while misconfiguration or unexpected duplication (or overriding) with different id, so the user should know about it.

Or maybe we could just add a test for duplicated class in debug:container command, so we can easily check such case? I mean their are factories for those use cases, it could help debugging definitions issues.

Or what about using a parameter like:

private function findAndSortTaggedServices($tagName, ContainerBuilder $container, $uniqueClasses = false)

To perform a check only if necessary mandatory for a service's tag collection?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HeahDude

services:
    app.listener.log_request_mail:
        class: App\Listener\LogRequest
        arguments: ["@logger.mail"]
        tags: [{name: kernel.request, priority: 100}]
    app.listener.log_request_udp:
        class: App\Listener\LogRequest
        arguments: ["@logger.udp"]
        tags: [{name: kernel.request, priority: 50}]

This is what would register only the first one with your example. If it was unique per service, only the first encountered of each would be logged:

services:
    app.listener.log_request_mail:
        class: App\Listener\LogRequest
        arguments: ["@logger.mail"]
        tags: [{name: kernel.request, priority: 100}, {name: kernel.request, priority: 50}]
    app.listener.log_request_udp:
        class: App\Listener\LogRequest
        arguments: ["@logger.udp"]
        tags: [{name: kernel.request, priority: 50}, {name: kernel.request, priority: 100}]

So by service id is feasible as this example makes no sense in the first place. However, it would also mean that the mail would be registered as 100 and the udp with 50.

Imo regarding this PR, I think I'll leave this as is. If it's a behavior that needs to be changed, a PR can be opened to deprecated it as 3.4 and 4.0 are still 1.5 year away.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is indeed very good as is, thanks for that. I've just been confused by this behavior, sorry for this digression :)

Copy link
Contributor

@HeahDude HeahDude Apr 25, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure (ref #18450 (comment)).

This PR make some passes use a trait to collect tagged services, are concerned:

  1. Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface
  2. Symfony\Component\Config\Resource\ResourceInterface\ResourceCheckerInterface
  3. Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface
  4. Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface
  5. Symfony\Component\Serializer\Encoder\EncoderInterface

Excepted 1 and 4, these interfaces have kind of a supports method (serializer encoders have supportsEncoding).

After a deep look at each of them, I ask again, does it really make sense to allow duplicated service classes with different service IDs or with different priorities? Should't this be tested?

Or is it the responsibility of each service using them to perform that check? Because currently, it seems nothing is preventing it.

foreach ($services as $serviceId => $tags) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use SplPriorityQueue?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit that I've not used it before, but it looks interesting.
I'll check it out next week, thanks!

On Fri, 8 Apr 2016 16:32 Javier Eguiluz, notifications@github.com wrote:

In
src/Symfony/Component/DependencyInjection/Compiler/PriorityTaggedServiceTrait.php
#18482 (comment):

+trait PriorityTaggedServiceTrait
+{

  • /**
  • \* Finds all services with the given tag name and order them by their priority.
    
  • *
    
  • \* @param string           $tagName
    
  • \* @param ContainerBuilder $container
    
  • *
    
  • \* @return Reference[]
    
  • */
    
  • private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
  • {
  •    $services = $container->findTaggedServiceIds($tagName);
    
  •    $sortedServices = array();
    
  •    foreach ($services as $serviceId => $tags) {
    

Could we use SplPriorityQueue
http://php.net/manual/en/class.splpriorityqueue.php?


You are receiving this because you authored the thread.
Reply to this email directly or view it on GitHub
https://github.com/symfony/symfony/pull/18482/files/3eb300496ea0c239dae35520d174830cef878e71#r59033296

foreach ($tags as $attributes) {
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
$queue->insert(new Reference($serviceId), $priority * -1);
}
}

return iterator_to_array($queue);
}
}
Loading