Skip to content

[DI][EventDispatcher] Add & wire closure-proxy argument type #20953

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
Jan 7, 2017

Conversation

nicolas-grekas
Copy link
Member

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #12521, #12007, #20039
License MIT
Doc PR -

By resolving event subscribers at compile time, then wrapping listeners in a closure, we get laziness almost for free. This should solve/replaced all the above linked issues.

(WIP because tests are missing)

$k = $type[1].' '.$k;
}
if ($type && $p->allowsNull()) {
$k = '?'.$k;
Copy link
Member

Choose a reason for hiding this comment

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

broken on PHP < 7.1

Copy link
Member Author

Choose a reason for hiding this comment

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

The ? is removed when not strictly required, which makes the code compatible with lower PHP versions.

$signature = ($r->returnsReference() ? '&(' : '(').implode(', ', $signature).')';

if (method_exists($r, 'getReturnType') && $type = $r->getReturnType()) {
$signature .= ': '.($type->allowsNull() ? '?' : '').$this->generateTypeHint($type, $r);
Copy link
Member

Choose a reason for hiding this comment

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

broken on PHP 7.0 which has the return type but not nullable ones

Copy link
Member Author

Choose a reason for hiding this comment

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

same: ? removed when not required

Copy link
Member

Choose a reason for hiding this comment

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

the issue here is the call to $type->allowsNull()

Copy link
Member Author

Choose a reason for hiding this comment

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

the line above checks that there is a type. In PHP7, having a type XOR allowsNull, that's why it works there also

Copy link
Member

Choose a reason for hiding this comment

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

hmm, I forgot that PHP 7.0 has the method allowsNull even though return types cannot return null. But it is indeed logical as the same class is used for arguments too

@@ -398,6 +399,12 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
case 'expression':
$arguments[$key] = new Expression($arg->nodeValue);
break;
case 'closure-proxy':
if (2 !== count($id = explode(':', $arg->getAttribute('id')))) {
Copy link
Member

Choose a reason for hiding this comment

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

would be better to use id and method IMO rather than exploding. It would be more consistent with other XML configuration settings

Copy link
Member Author

Choose a reason for hiding this comment

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

updated

@@ -108,7 +102,17 @@ public function process(ContainerBuilder $container)
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}

$definition->addMethodCall('addSubscriberService', array($id, $class));
foreach ($class::getSubscribedEvents() as $eventName => $params) {
Copy link
Member

Choose a reason for hiding this comment

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

this requires adding a ClassResource for the event subscriber class to invalidate the container when changing the class code. Otherwise, adding a new event in a subscriber would require clearing the cache manually in dev

Copy link
Member Author

@nicolas-grekas nicolas-grekas Dec 16, 2016

Choose a reason for hiding this comment

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

added in a generic way in PhpDumper

} else {
foreach ($params as $listener) {
$definition->addMethodCall('addListener', array($eventName, new ClosureProxyArgument(new Reference($id), $listener[0]), isset($listener[1]) ? $listener[1] : 0));
}
Copy link
Member

Choose a reason for hiding this comment

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

is there a way to ensure that this logic stays in sync with the logic of the EventDispatcher to handle the return value of getSubscribedEvents ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so...

Copy link
Member Author

Choose a reason for hiding this comment

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

I found a way! Don't tell me you don't like it, you asked for it ;)

@stof
Copy link
Member

stof commented Dec 16, 2016

should this deprecate the ContainerAwareEventDispatcher ?

Copy link
Member Author

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

I'll deprecate ContainerAwareEventDispatcher in #20937 so that it gets its own line in the changelog

$k = $type[1].' '.$k;
}
if ($type && $p->allowsNull()) {
$k = '?'.$k;
Copy link
Member Author

Choose a reason for hiding this comment

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

The ? is removed when not strictly required, which makes the code compatible with lower PHP versions.

$signature = ($r->returnsReference() ? '&(' : '(').implode(', ', $signature).')';

if (method_exists($r, 'getReturnType') && $type = $r->getReturnType()) {
$signature .= ': '.($type->allowsNull() ? '?' : '').$this->generateTypeHint($type, $r);
Copy link
Member Author

Choose a reason for hiding this comment

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

same: ? removed when not required

@@ -398,6 +399,12 @@ private function getArgumentsAsPhp(\DOMElement $node, $name, $lowercase = true)
case 'expression':
$arguments[$key] = new Expression($arg->nodeValue);
break;
case 'closure-proxy':
if (2 !== count($id = explode(':', $arg->getAttribute('id')))) {
Copy link
Member Author

Choose a reason for hiding this comment

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

updated

@@ -108,7 +102,17 @@ public function process(ContainerBuilder $container)
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}

$definition->addMethodCall('addSubscriberService', array($id, $class));
foreach ($class::getSubscribedEvents() as $eventName => $params) {
Copy link
Member Author

@nicolas-grekas nicolas-grekas Dec 16, 2016

Choose a reason for hiding this comment

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

added in a generic way in PhpDumper

} else {
foreach ($params as $listener) {
$definition->addMethodCall('addListener', array($eventName, new ClosureProxyArgument(new Reference($id), $listener[0]), isset($listener[1]) ? $listener[1] : 0));
}
Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so...

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 17, 2016

fancy use case: <argument type="closure-proxy" id="service_container" method="get" />
ping @unkind

@Koc
Copy link
Contributor

Koc commented Dec 17, 2016

Referenced PR with Drupal event dispatcher also reduces addListener calls by injecting map with listeners in constructor

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Dec 17, 2016

@Koc, true, but I really doubt there is anything to save here. I think most of the perf gain came from turning subscribers into listeners at compile time instead, which this PR does. Would you like to confirm?

@unkind
Copy link
Contributor

unkind commented Dec 17, 2016

fancy use case: <argument type="closure-proxy" id="service_container" method="get" />

If I get your right, it allows to access private services. But they can be inlined (and most likely they would).

@nicolas-grekas
Copy link
Member Author

it allows to access private service

of course closure proxies work with private services, but the example you quoted (<argument type="closure-proxy" id="service_container" method="get" /> doesn't, since it gets through the public interface of the container).

@unkind
Copy link
Contributor

unkind commented Dec 18, 2016

@nicolas-grekas well, I don't understand the purpose on this code then :)

@nicolas-grekas
Copy link
Member Author

Tests added. PR embeds #20907 for now, waiting for it to be merged beforehand.
Status: needs review

@nicolas-grekas
Copy link
Member Author

PR rebased & ready.
The yaml loader expects =closure_proxy to define the proxies.
Eg.

    foo:
        class: FooClass
        arguments:
            - =closure_proxy: ['@bar', getBuz]

xml is:

    <service id="foo" class="FooClass">
      <argument type="closure-proxy" id="bar" method="getBuz"/>
    </service>

Status: needs review

@@ -46,7 +46,13 @@ public function __construct($listener, $name, Stopwatch $stopwatch, EventDispatc
$this->name = is_object($listener[0]) ? get_class($listener[0]) : $listener[0];
$this->pretty = $this->name.'::'.$listener[1];
} elseif ($listener instanceof \Closure) {
$this->pretty = $this->name = 'closure';
$r = new \ReflectionFunction($listener);
if (preg_match('#^/\*\* @closure-proxy ([^: ]++)::([^: ]++) \*/$#', $r->getDocComment(), $m)) {
Copy link
Member

Choose a reason for hiding this comment

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

hehe, I was wondering why you had this phpdoc comment in the container generation :) Note that this trick does only work with a dumped container. Not a big deal though.

@@ -59,10 +61,6 @@ public function process(ContainerBuilder $container)

foreach ($container->findTaggedServiceIds($this->listenerTag) as $id => $events) {
$def = $container->getDefinition($id);
if (!$def->isPublic()) {
throw new InvalidArgumentException(sprintf('The service "%s" must be public as event listeners are lazy-loaded.', $id));
}
Copy link
Member

Choose a reason for hiding this comment

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

Great!

@@ -108,7 +104,26 @@ public function process(ContainerBuilder $container)
throw new InvalidArgumentException(sprintf('Service "%s" must implement interface "%s".', $id, $interface));
}

$definition->addMethodCall('addSubscriberService', array($id, $class));
$r = new \ReflectionClass($class);
$extractingDispatcher->addSubscriber($r->newInstanceWithoutConstructor());
Copy link
Member

Choose a reason for hiding this comment

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

Won't work if the getSubscribedEvents is somewhat configured via "static" properties or a global state. We probably don't care though.

@fabpot
Copy link
Member

fabpot commented Jan 7, 2017

Thank you @nicolas-grekas.

@fabpot fabpot merged commit ecdf857 into symfony:master Jan 7, 2017
fabpot added a commit that referenced this pull request Jan 7, 2017
…t type (nicolas-grekas)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[DI][EventDispatcher] Add & wire closure-proxy argument type

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #12521, #12007, #20039
| License       | MIT
| Doc PR        | -

By resolving event subscribers at compile time, then wrapping listeners in a closure, we get laziness almost for free. This should solve/replaced all the above linked issues.

(WIP because tests are missing)

Commits
-------

ecdf857 [DI][EventDispatcher] Add & wire closure-proxy argument type
@nicolas-grekas nicolas-grekas deleted the di-method-dump branch January 7, 2017 16:32
@xabbuh xabbuh mentioned this pull request Apr 5, 2017
@fabpot fabpot mentioned this pull request May 1, 2017
nicolas-grekas added a commit that referenced this pull request Jun 2, 2017
This PR was merged into the 3.3 branch.

Discussion
----------

[Di] Remove closure-proxy arguments

| Q             | A
| ------------- | ---
| Branch?       | 3.3
| Bug fix?      | no
| New feature?  | no
| BC breaks?    | yes
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

With #23008, we don't need this in core anymore.
Let's drop it now.
Technically that's a BC break, but for a feature that is very new, and still quite hidden.
Doing this now would save us from maintaining this code, and help reduce the overall complexity.

Basically reverts #20953

Commits
-------

57daadb [Di] Remove closure-proxy arguments
@nicolas-grekas
Copy link
Member Author

Reverted in #23022

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.

6 participants