-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Conversation
$k = $type[1].' '.$k; | ||
} | ||
if ($type && $p->allowsNull()) { | ||
$k = '?'.$k; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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()
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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')))) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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)); | ||
} |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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 ;)
should this deprecate the ContainerAwareEventDispatcher ? |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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')))) { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)); | ||
} |
There was a problem hiding this comment.
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...
47ab06d
to
793cada
Compare
6016ff1
to
463e5f2
Compare
fancy use case: |
463e5f2
to
ddea5de
Compare
Referenced PR with Drupal event dispatcher also reduces addListener calls by injecting map with listeners in constructor |
@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? |
If I get your right, it allows to access private services. But they can be inlined (and most likely they would). |
of course closure proxies work with private services, but the example you quoted ( |
@nicolas-grekas well, I don't understand the purpose on this code then :) |
Tests added. PR embeds #20907 for now, waiting for it to be merged beforehand. |
2cb1ba9
to
5a0ec76
Compare
1f87716
to
8c104a0
Compare
8c104a0
to
ecdf857
Compare
PR rebased & ready. 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)) { |
There was a problem hiding this comment.
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)); | |||
} |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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.
Thank you @nicolas-grekas. |
…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
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
Reverted in #23022 |
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)