Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

[FrameworkBundle] [Command] Event Dispatcher Debug - Display registered listeners #10388

Merged
merged 1 commit into from
@matthieuauger
Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets -
License MIT

[Update] The PR has been updated in order to comply with @stof comments.

Current status :

  • New event dispatcher Descriptor
  • Manage all callables
  • Unit tests
  • Text description
  • XML description
  • Json description
  • Markdown description

Hi. In some big applications with lots of events, it's often hard to debug which classes listen to which events, and what is the order of theses listeners. This PR allows to run

  • event-dispatcher:debug which displays all configured listeners + the events they listen to

capture d cran de 2014-03-07 20 13 56

  • event-dispatcher:debug event which displays configured listeners for this specific event (order by priority desc)

capture d cran de 2014-03-07 20 14 31

The output is similar to container:debug command and is available in all supported formats (txt, xml, json and markdown).

I found another PR with same goal (#8234), but the approach looks too complicated to me plus I think we should fetch the listeners directly with the event_dispatcher.

...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
@@ -212,4 +221,39 @@ private function getContainerAliasData(Alias $alias)
'public' => $alias->isPublic(),
);
}
+
+ /**
+ * @param $eventDispatcher
+ * @param null $event
@cordoval
cordoval added a note

is the event also a class or is only null?

The event is null or a string (the name of the event)

@romainneutron Collaborator

then it should be @param string|null $event The event name

Right, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@saro0h

Nice idea!

...ameworkBundle/Command/EventDispatcherDebugCommand.php
((19 lines not shown))
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * A console command for retrieving information about event dispatcher
+ *
+ * @author Matthieu Auger <mail@matthieuauger.com>
+ */
+class EventDispatcherDebugCommand extends ContainerAwareCommand
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('event-dispatcher:debug')
@stof Collaborator
stof added a note

@fabpot what about grouping the debug command in a debug: namespace rather than having lots of namespaces with a single debug command in it ? We can use aliases for BC for existing commands

@hhamon
hhamon added a note

+1

@stof Collaborator
stof added a note

@fabpot what do you think about this naming ?

@weaverryan Collaborator

+1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...dle/FrameworkBundle/Console/Descriptor/Descriptor.php
@@ -266,6 +281,32 @@ protected function findDefinitionsByTag(ContainerBuilder $builder, $showPrivate)
return $definitions;
}
+ /**
+ * @param EventDispatcherInterface $eventDispatcher
+ *
+ * @return array
+ */
+ protected function getListenersGroupedByClass(EventDispatcherInterface $eventDispatcher)
+ {
+ $groupedListeners = array();
+ foreach ($eventDispatcher->getListeners() as $event => $eventListeners) {
+ foreach ($eventListeners as $eventListener) {
+ list($object, $method) = $eventListener;
@stof Collaborator
stof added a note

what if the event listener is not build as array($object, $method) ? Not all PHP callables are build this way

Thanks @stof you're right. I will update this to manage all callable types, probably by adding a describeCallable() to the base Descriptor class

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
@@ -212,4 +221,39 @@ private function getContainerAliasData(Alias $alias)
'public' => $alias->isPublic(),
);
}
+
+ /**
+ * @param $eventDispatcher
+ * @param null $event
+ *
+ * @return array
+ */
+ private function getEventDispatcherListenersData($eventDispatcher, $event = null)
+ {
+ $data = array('listeners' => array());
+
+ if (null !== $event) {
+ foreach ($eventDispatcher->getListeners($event) as $listener) {
+ list($object, $method) = $listener;
+ $className = get_class($object);
@stof Collaborator
stof added a note

This logic is broken when listeners are not array($object, $method) callables

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...eworkBundle/Console/Descriptor/MarkdownDescriptor.php
@@ -171,6 +172,44 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o
/**
* {@inheritdoc}
*/
+ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array())
+ {
+ $event = array_key_exists('event', $options) ? $options['event'] : null;
+
+ $title = 'Registered listeners';
+ if (null !== $event) {
+ $title .= sprintf(' for event `%s` ordered by descending priority', $event);
+ }
+
+ $this->write($title."\n".str_repeat('=', strlen($title))."\n");
+
+ if (null !== $event) {
+ foreach ($eventDispatcher->getListeners($event) as $listener) {
+ list($object, $method) = $listener;
+ $className = get_class($object);
@stof Collaborator
stof added a note

This logic is broken when listeners are not array($object, $method) callables

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/TextDescriptor.php
((8 lines not shown))
+ $label = 'Registered listeners';
+ if (null !== $event) {
+ $label .= sprintf(' for event <info>%s</info> ordered by descending priority', $event);
+ }
+
+ $this->writeText($this->formatSection('event_dispatcher', $label)."\n", $options);
+
+ $table = new TableHelper();
+ $table->setLayout(TableHelper::LAYOUT_COMPACT);
+
+ if (null !== $event) {
+ $table->setHeaders(array('<comment>Class name</comment>', '<comment>Method</comment>'));
+
+ foreach ($eventDispatcher->getListeners($event) as $listener) {
+ list($object, $method) = $listener;
+ $className = get_class($object);
@stof Collaborator
stof added a note

This logic is broken when listeners are not array($object, $method) callables

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
@@ -286,6 +295,45 @@ private function getContainerServicesDocument(ContainerBuilder $builder, $tag =
}
/**
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param null $event
+ *
+ * @return \DOMDocument
+ */
+ private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, $event = null)
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->appendChild($eventDispatcherXML = $dom->createElement('event_dispatcher'));
+
+ $eventDispatcherXML->appendChild($listenersXML = $dom->createElement('listeners'));
+ if (null !== $event) {
+ foreach ($eventDispatcher->getListeners($event) as $listener) {
+ list($object, $method) = $listener;
+ $className = get_class($object);
@stof Collaborator
stof added a note

This logic is broken when listeners are not array($object, $method) callables

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@matthieuauger

The PR has been updated. The descriptor now manages all known callables (found 7 different types) and unit tests have been added.

Thanks for the feedback

@matthieuauger

@fabpot , could you give me your opinion about this ?

@fabpot
Owner

Can you rebase this on master as there are some conflicts? Thanks.

@matthieuauger

The PR is finished, all formats are now supported and all descriptors tested.

You can see the output in the test files :

event-dispatcher:debug
Text
Xml
Json
Markdown

event-dispatcher:debug event1
Text
Xml
Json
Markdown

The diff is quite big due to all the formats but the functionnality is now fully working.

Feedbacks always welcomed

...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
((80 lines not shown))
+
+ return $data;
+ }
+
+ if ($callable instanceof \Closure) {
+ $data['type'] = 'closure';
+
+ return $data;
+ }
+
+ if (method_exists($callable, '__invoke')) {
+ $data['type'] = 'class';
+ $data['name'] = get_class($callable);
+
+ return $data;
+ }
@stloyd
stloyd added a note

What if you will not reach any of those ifs? Shouldn't you throw error or return empty array?

Yes you're right, I should return $data, thanks

Finally opted to raise an exception as if somedays it happens, we should know it instead of silently ignore it

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...eworkBundle/Console/Descriptor/MarkdownDescriptor.php
((31 lines not shown))
+ $this->write("\n".sprintf('### Listener %d', $order + 1)."\n");
+ $this->describeCallable($eventListener);
+ }
+ }
+ }
+ }
+
+ /**
+ * @{inheritdoc}
+ */
+ protected function describeCallable($callable, array $options = array())
+ {
+ $string = '';
+
+ if (is_array($callable)) {
+ $string .= "\n".'- Type: `function`';
@stloyd
stloyd added a note

Instead of merging two strings, you can simply re-use that already opened one.

Not sure to understand what you mean. Are you suggesting this ?
$string .= "\n- Type: function";

@stloyd
stloyd added a note

Yes, it's exactly same approach you do few lines below =)

Ok, did it when the following string is static but not into the sprintf() statements, I think it's more readable to have the \n aligned vertically (and generally prefer single-quoted strings).

Thanks for your feedback !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@matthieuauger

@fabpot : Any chances for this to be shipped in 2.5 along with translation:debug and config:debug ?

@matthieuauger

This is visibly not going to be shipped in 2.5 due to feature freeze, but it would still be much appreciated to have feedback of the core team decision makers @jakzal @stof @Seldaek @lsmith77

@jeremyFreeAgent

I like that kind of feature!

@apfelbox

I like it. :+1:

btw: this PR is asking for the DX label (@javiereguiluz I saw you managing the other labels)

@javiereguiluz javiereguiluz added the DX label
@javiereguiluz

@apfelbox applied the DX label. Thanks for noticing it.

@stof
Collaborator

I like the feature, however I think discussing the naming of the command is important for DX (see #10388 (comment)) so I'm waiting for this discussion to complete before voting on the PR.

@apfelbox

@stof I really like your idea on grouping all debug-related commands under debug:, btw.

@javiereguiluz

@stof I really like your idea about grouping debug commands under debug namespace. I'd love to do something similar with the check namespace. That's why I proposed to create a check:security command and not security:check. We could create the following:

  • check:security looks for known security vulnerabilities
  • check:permissions checks if the project folders have the right permissions to execute Symfony
  • check:yaml would deprecate yaml:lint
  • check:twig would deprecate twig:lint
  • check:doctrine-settings would deprecate doctrine:ensure-production-settings
  • check:configuration would check if there is any problem in all your project configuration files
  • etc.
@hhamon

I like this command very much but I think the name is too long. Should we just keep dispatcher:debug instead of event-dispatcher:debug?

@javiereguiluz

@hhamon and we could even shorten it a bit more: debug:events or debug:listeners

@matthieuauger

I'm not sure the command should be named debug:something. Actually, if we tried to establish a name convention from the existing commands, i would say the pattern is

vendor:object:action

  • doctrine:database:create
  • doctrine:mapping:convert
  • swiftmailer: email:send

Plus an exception for the framework-related commands where the vendor is omitted for simplicity i guess (but the action is almost everytime at the end).

  • cache:clear
  • assetic:dump
  • router:match

The advantage of this pattern is that we follow the general idea of namespaces where the vendor is first.

Grouping all the debug commands in a debug namespace is really convenient, but shouldn't it be the alias instead of the command name ?

@stof
Collaborator

@hhamon given that any command name can be shorten as long as it is not ambiguous, the current name allows to reference it as event:debug (or even ev:d if you want)

@matthieuauger my advice is to apply the renaming to existing commands too. We currently have many debugging commands which are alone in their top-level namespace. It makes more sense to group debug commands together than to group event-dispatcher commands together when you only have 1 IMO

@stof
Collaborator

The only point left here is the discussion about the command name IMO: #10388 (comment)

@fabpot what do you think about this ?

@weaverryan
Collaborator

I agree with grouping into debug: namespace. I think a newer user especially will appreciate having all these debug:* commands grouped together, rather than grouping based on individual components.

@jeremyFreeAgent

What about aliases to keep both?

@stof
Collaborator

@jeremyFreeAgent existing commands will have an alias for the *:debug name (otherwise it would be a BC break). However, I'm not sure adding an alias is needed for new debug commands

@weaverryan
Collaborator

So let me try to summarize and we'll see if we agree.

1) Move existing commands into the debug:* namespace, but then create an alias so that the existing command works (e.g. *:debug)

2) For new debug commands, simply put them into the debug:* namespace. We don't really need an extra alias for these, but we could discuss them on a case-by-case basis.

Action items:

A) Rename the command to debug:event-dispatcher
B) On a separate PR, move existing debug commands into the debug:* namespace and give them aliases for BC.

Is this accurate? What does everyone think about the naming? Remember, the goal was to help clarity by grouping things into debug:*.

Thanks!

@fabpot
Owner

I agree with the move of commands to a new debug namespace.

@stof
Collaborator

@weaverryan yes, this is accurate

@matthieuauger can you update your PR to name the command debug:event-dispatcher ?

@matthieuauger

@stof: PR rebased and command moved to debug namespace. Let me know if something is still missing !

...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
@@ -222,4 +239,99 @@ private function getContainerAliasData(Alias $alias)
'public' => $alias->isPublic(),
);
}
+
+ /**
+ * @param $eventDispatcher
+ * @param string|null $event
+ *
+ * @return array
+ */
+ private function getEventDispatcherListenersData($eventDispatcher, $event = null)
@stof Collaborator
stof added a note

the argument should be typehinted

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
@@ -222,4 +239,99 @@ private function getContainerAliasData(Alias $alias)
'public' => $alias->isPublic(),
);
}
+
+ /**
+ * @param $eventDispatcher
@stof Collaborator
stof added a note

missing type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
@@ -222,4 +239,99 @@ private function getContainerAliasData(Alias $alias)
'public' => $alias->isPublic(),
);
}
+
+ /**
+ * @param $eventDispatcher
+ * @param string|null $event
+ *
+ * @return array
+ */
+ private function getEventDispatcherListenersData($eventDispatcher, $event = null)
+ {
+ $data = array();
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $order => $listener) {
@stof Collaborator
stof added a note

$order is unused

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
((6 lines not shown))
+ * @param $eventDispatcher
+ * @param string|null $event
+ *
+ * @return array
+ */
+ private function getEventDispatcherListenersData($eventDispatcher, $event = null)
+ {
+ $data = array();
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $order => $listener) {
+ array_push(
+ $data,
+ $this->getCallableData($listener)
+ );
@stof Collaborator
stof added a note

I would use $data[] = $this->getCallableData($listener); to avoid turning the $data zval into a reference one (which would probably require copying it when returning it at the end of the method as it does not return by reference. See http://jpauli.github.io/2014/06/27/references-mismatch.html for more explanations).

@jpauli your blog post does not explain how PHP behaves for the case of returned values btw. Is it able to figure that the zval is not used by reference anymore (array_push is done) and it could turn it into a normal zval again when returning it, or will it copy it here ? this might require an update of the blog post

Not sure to fully understand why this is better but i made the modification

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
((14 lines not shown))
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $order => $listener) {
+ array_push(
+ $data,
+ $this->getCallableData($listener)
+ );
+ }
+ } else {
+ ksort($registeredListeners);
+
+ foreach ($registeredListeners as $eventListened => $eventListeners) {
+ $data[$eventListened] = array();
+
+ foreach ($eventListeners as $order => $eventListener) {
@stof Collaborator
stof added a note

$order is not used here either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
((18 lines not shown))
+ array_push(
+ $data,
+ $this->getCallableData($listener)
+ );
+ }
+ } else {
+ ksort($registeredListeners);
+
+ foreach ($registeredListeners as $eventListened => $eventListeners) {
+ $data[$eventListened] = array();
+
+ foreach ($eventListeners as $order => $eventListener) {
+ array_push(
+ $data[$eventListened],
+ $this->getCallableData($eventListener)
+ );
@stof Collaborator
stof added a note

$data[$eventListened][] = $this->getCallableData($eventListener);

Btw, thanks to the way PHP works, this will remove the need for $data[$eventListened] = array(); above

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
((57 lines not shown))
+ $data['name'] = substr($callable[1], 8);
+ $data['class'] = $callable[0];
+ $data['static'] = true;
+ $data['parent'] = true;
+ }
+ }
+
+ return $data;
+ }
+
+ if (is_string($callable)) {
+ $data['type'] = 'function';
+
+ if (false === strpos($callable, '::')) {
+ $data['name'] = $callable;
+ $data['global'] = true;
@stof Collaborator
stof added a note

I don't think this global flag makes much sense here actually

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...FrameworkBundle/Console/Descriptor/JsonDescriptor.php
((76 lines not shown))
+ $data['name'] = $callableParts[1];
+ $data['class'] = $callableParts[0];
+ $data['static'] = true;
+ }
+
+ return $data;
+ }
+
+ if ($callable instanceof \Closure) {
+ $data['type'] = 'closure';
+
+ return $data;
+ }
+
+ if (method_exists($callable, '__invoke')) {
+ $data['type'] = 'class';
@stof Collaborator
stof added a note

the callable is an invokable object, not a class

I replaced 'class' by 'object', do you think the 'invokable' word is needed ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...eworkBundle/Console/Descriptor/MarkdownDescriptor.php
((57 lines not shown))
+ $string .= "\n".sprintf('- Name: `%s`', substr($callable[1], 8));
+ $string .= "\n".sprintf('- Class: `%s`', $callable[0]);
+ $string .= "\n- Static: yes";
+ $string .= "\n- Parent: yes";
+ }
+ }
+
+ return $this->write($string."\n");
+ }
+
+ if (is_string($callable)) {
+ $string .= "\n- Type: `function`";
+
+ if (false === strpos($callable, '::')) {
+ $string .= "\n".sprintf('- Name: `%s`', $callable);
+ $string .= "\n- Global: yes";
@stof Collaborator
stof added a note

I would remove it here as well

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
@@ -393,4 +410,103 @@ private function getContainerParameterDocument($parameter, $options = array())
return $dom;
}
+
+ /**
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param null $event
@stof Collaborator
stof added a note

string|null

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
@@ -393,4 +410,103 @@ private function getContainerParameterDocument($parameter, $options = array())
return $dom;
}
+
+ /**
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param null $event
+ *
+ * @return \DOMDocument
+ */
+ private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, $event = null)
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->appendChild($eventDispatcherXML = $dom->createElement('event_dispatcher'));
@stof Collaborator
stof added a note

I would use event-dispatcher for the node name to follow the XML conventions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
@@ -393,4 +410,103 @@ private function getContainerParameterDocument($parameter, $options = array())
return $dom;
}
+
+ /**
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param null $event
+ *
+ * @return \DOMDocument
+ */
+ private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, $event = null)
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->appendChild($eventDispatcherXML = $dom->createElement('event_dispatcher'));
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $order => $listener) {
@stof Collaborator
stof added a note

$order is unused

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
((61 lines not shown))
+ $callableXML->setAttribute('name', substr($callable[1], 8));
+ $callableXML->setAttribute('class', $callable[0]);
+ $callableXML->setAttribute('static', 'true');
+ $callableXML->setAttribute('parent', 'true');
+ }
+ }
+
+ return $dom;
+ }
+
+ if (is_string($callable)) {
+ $callableXML->setAttribute('type', 'function');
+
+ if (false === strpos($callable, '::')) {
+ $callableXML->setAttribute('name', $callable);
+ $callableXML->setAttribute('global', 'true');
@stof Collaborator
stof added a note

same here about global

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@matthieuauger

@stof : PR updated, thanks for the review

@stof stof commented on the diff
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
((27 lines not shown))
+ $eventDispatcherXML->appendChild($eventXML = $dom->createElement('event'));
+ $eventXML->setAttribute('name', $eventListened);
+
+ foreach ($eventListeners as $eventListener) {
+ $callableXML = $this->getCallableDocument($eventListener);
+
+ $eventXML->appendChild($eventXML->ownerDocument->importNode($callableXML->childNodes->item(0), true));
+ }
+ }
+ }
+
+ return $dom;
+ }
+
+ /**
+ * @param callable $callable
@stof Collaborator
stof added a note

missing @return \DOMDocument

Done. I rechecked all the PHPDoc to ensure that nothing is missing. I just didn't add the "@throws \InvalidArgumentException" on the describeCallable function because developers should not have to handle this exception but I can add it if you think it's necessary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
.../FrameworkBundle/Console/Descriptor/XmlDescriptor.php
((29 lines not shown))
+
+ foreach ($eventListeners as $eventListener) {
+ $callableXML = $this->getCallableDocument($eventListener);
+
+ $eventXML->appendChild($eventXML->ownerDocument->importNode($callableXML->childNodes->item(0), true));
+ }
+ }
+ }
+
+ return $dom;
+ }
+
+ /**
+ * @param callable $callable
+ */
+ protected function getCallableDocument($callable)
@stof Collaborator
stof added a note

this should be private

@stof Collaborator
stof added a note

btw, instead of creating a DOMDocument with a single child which is then imported in another document, I would rather pass the DOMDocument as argument to use it to create a DOMElement (your $callableXML variable) and retrun the DOMElement itself. This way, you would not need to import it again in the right document when using it.

In fact this is the same logic applied for the router and container descriptions (https://github.com/matthieuauger/symfony/blob/feature-framework-bundle-event-dispatcher-command/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php#L144). We should keep consistency between theses functions, don't you think ?

Another thing is that you can describe a callable alone (https://github.com/matthieuauger/symfony/blob/feature-framework-bundle-event-dispatcher-command/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php#L73). In this case the creation of the DOMDocument here seems pertinent

@stof Collaborator
stof added a note

ah, I missed that it can be describe standalone. Forget these comments then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@nicolas-grekas nicolas-grekas referenced this pull request from a commit
@nicolas-grekas nicolas-grekas minor #11627 [FrameworkBundle] [TwigBundle] Move debug commands to de…
…bug namespace (matthieuauger)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[FrameworkBundle] [TwigBundle] Move debug commands to debug namespace

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

Instead of having several namespaces with only one debug command (container:debug, event-dispatcher:debug), move all these debug commands to a new **debug** namespace.

Related to #10388 (comment)

I don't how to tag these aliases as deprecated as there are only here for backward compatibility.
The renaming should also be done in the Swiftmailer Bundle.

Commits
-------

fd0e229 Move debug commands to debug namespace
9680c35
@matthieuauger

@weaverryan : The 2 points you mentioned earlier are now OK

Action items:

A) Rename the command to debug:event-dispatcher
B) On a separate PR, move existing debug commands into the debug:* namespace and give them aliases for BC

The A) is updated here and B) has been merged to master a week ago. The only missing point here is :+1: or new feedback from the core team decision makers

@stof
Collaborator

:+1: from me

@matthieuauger

Awesome, thanks @stof

@matthieuauger

ping @symfony/deciders ( @jakzal @Seldaek @lsmith77 )

@jakzal jakzal commented on the diff
...ameworkBundle/Command/EventDispatcherDebugCommand.php
((21 lines not shown))
+/**
+ * A console command for retrieving information about event dispatcher
+ *
+ * @author Matthieu Auger <mail@matthieuauger.com>
+ */
+class EventDispatcherDebugCommand extends ContainerAwareCommand
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('debug:event-dispatcher')
+ ->setDefinition(array(
+ new InputArgument('event', InputArgument::OPTIONAL, 'An event name (foo)'),
@jakzal Collaborator
jakzal added a note

I'd remove (foo). It looks a bit like a default.

Thanks for your feedback, I copied that from the debug:container command (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php#L46). I should be able to remove it by tomorrow

@jakzal Collaborator
jakzal added a note

Oh, if it's there I'd keep it here too to be consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@jakzal
Collaborator

Apart from a minor comment, big :+1:

@Nicofuma

big :thumbsup: it can be very helpful

@matthieuauger

Thanks, we are almost there :metal: !

@fabpot
Owner

:+1:

@fabpot
Owner

Thank you @matthieuauger.

@fabpot fabpot merged commit ce53c8a into from
@fabpot fabpot referenced this pull request from a commit
@fabpot fabpot feature #10388 [FrameworkBundle] [Command] Event Dispatcher Debug - D…
…isplay registered listeners (matthieuauger)

This PR was merged into the 2.6-dev branch.

Discussion
----------

[FrameworkBundle] [Command] Event Dispatcher Debug - Display registered listeners

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

------------------------------------------
[Update] The PR has been updated in order to comply with @stof comments.

Current status :
- [x] New event dispatcher Descriptor
- [x] Manage all callables
- [x] Unit tests
- [x] Text description
- [x] XML description
- [x] Json description
- [x] Markdown description

-----------------------------------------
Hi. In some big applications with lots of events, it's often hard to debug which classes listen to which events, and what is the order of theses listeners. This PR allows to run

- *event-dispatcher:debug* which displays all configured listeners + the events they listen to

![capture d cran de 2014-03-07 20 13 56](https://f.cloud.github.com/assets/1172099/2361104/40a86a62-a62d-11e3-9ccd-360a8d75b2a4.png)

- *event-dispatcher:debug* **event** which displays configured listeners for this specific event (order by priority desc)

![capture d cran de 2014-03-07 20 14 31](https://f.cloud.github.com/assets/1172099/2361100/31e0d12c-a62d-11e3-963b-87623d05642c.png)

The output is similar to *container:debug* command and is available in all supported formats (txt, xml, json and markdown).

I found another PR with same goal (#8234), but the approach looks too complicated to me plus I think we should fetch the listeners directly with the event_dispatcher.

Commits
-------

ce53c8a [FrameworkBundle] Add Event Dispatcher debug command
9046c48
@weaverryan weaverryan referenced this pull request in symfony/symfony-docs
Closed

Document the new debug:event-dispatcher #4230

@matthieuauger matthieuauger deleted the branch
@egulias

Maybe I'm a bit later to this, but you may know https://github.com/egulias/ListenersDebugCommandBundle which is a command that does exactly this since 2012, and has been added as dependency of ez Publish. It allows to order on priority, filter by event name and gather detailed information to display into the console.
I have released https://github.com/egulias/TagDebugCommandBundle (and its lib) which adds flexibility when inspecting tags in the container.

@matthieuauger

Hi @egulias ! IMO this feature has place in core, you could have open a PR for that 2 years ago !

I think we implemented this in two different ways :

  • Your command is deeply coupled with the Symfony DIC (definitions, alias, tags, visibility). The EventDispatcher is a standalone component and we should be able to describe listeners just with an EventDispatcher object (without knowledge of the DIC behind)
  • You fetch the listeners by filtering the services tagged with kernel.event_listener tag, but what if listeners are not added within the DI ? For example in a CompilerPass ?

Anyway, thanks for sharing, this debug:event-dispatcher command is very basic and can still be significantly improved !

@egulias

Hi @matthieuauger yes, probably I should have done that :).

Yes, we did and I can't remember why I went the DIC way.

For the record, the tag name inspected is a regex for any tag name ending with .event_listener or event_subscriber.

This command won't see doctrine's listeners since they use their own event dispatcher, right? I think is the expected behaviour.

May be I can PR with the addition of the DIC to the events list, now that the logic is in a lib of its own.

In any case, nice work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Aug 18, 2014
  1. @matthieuauger
This page is out of date. Refresh to see the latest.
Showing with 778 additions and 0 deletions.
  1. +84 −0 src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php
  2. +26 −0 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php
  3. +109 −0 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php
  4. +101 −0 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php
  5. +83 −0 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php
  6. +117 −0 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php
  7. +41 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php
  8. +45 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php
  9. +4 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.json
  10. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.md
  11. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.txt
  12. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.xml
  13. +6 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.json
  14. +4 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.md
  15. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.txt
  16. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.xml
  17. +5 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.json
  18. +3 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.md
  19. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.txt
  20. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.xml
  21. +6 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.json
  22. +4 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.md
  23. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.txt
  24. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.xml
  25. +7 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.json
  26. +5 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.md
  27. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.txt
  28. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.xml
  29. +3 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.json
  30. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.md
  31. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.txt
  32. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.xml
  33. +4 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.json
  34. +3 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.md
  35. +1 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.txt
  36. +2 −0  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.xml
  37. +9 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json
  38. +10 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md
  39. +8 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt
  40. +5 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml
  41. +17 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json
  42. +19 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md
  43. +16 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt
  44. +10 −0 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml
View
84 src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php
@@ -0,0 +1,84 @@
+<?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\Bundle\FrameworkBundle\Command;
+
+use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * A console command for retrieving information about event dispatcher
+ *
+ * @author Matthieu Auger <mail@matthieuauger.com>
+ */
+class EventDispatcherDebugCommand extends ContainerAwareCommand
+{
+ /**
+ * {@inheritdoc}
+ */
+ protected function configure()
+ {
+ $this
+ ->setName('debug:event-dispatcher')
+ ->setDefinition(array(
+ new InputArgument('event', InputArgument::OPTIONAL, 'An event name (foo)'),
@jakzal Collaborator
jakzal added a note

I'd remove (foo). It looks a bit like a default.

Thanks for your feedback, I copied that from the debug:container command (https://github.com/symfony/symfony/blob/master/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php#L46). I should be able to remove it by tomorrow

@jakzal Collaborator
jakzal added a note

Oh, if it's there I'd keep it here too to be consistent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ new InputOption('format', null, InputOption::VALUE_REQUIRED, 'To output description in other formats', 'txt'),
+ new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'),
+ ))
+ ->setDescription('Displays configured listeners for an application')
+ ->setHelp(<<<EOF
+The <info>%command.name%</info> command displays all configured listeners:
+
+ <info>php %command.full_name%</info>
+
+To get specific listeners for an event, specify its name:
+
+ <info>php %command.full_name% kernel.request</info>
+EOF
+ )
+ ;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * @throws \LogicException
+ */
+ protected function execute(InputInterface $input, OutputInterface $output)
+ {
+ if ($event = $input->getArgument('event')) {
+ $options = array('event' => $event);
+ } else {
+ $options = array();
+ }
+
+ $dispatcher = $this->getEventDispatcher();
+
+ $helper = new DescriptorHelper();
+ $options['format'] = $input->getOption('format');
+ $options['raw_text'] = $input->getOption('raw');
+ $helper->describe($output, $dispatcher, $options);
+ }
+
+ /**
+ * Loads the Event Dispatcher from the container
+ *
+ * @return EventDispatcherInterface
+ */
+ protected function getEventDispatcher()
+ {
+ return $this->getContainer()->get('event_dispatcher');
+ }
+}
View
26 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php
@@ -18,6 +18,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -66,6 +67,12 @@ public function describe(OutputInterface $output, $object, array $options = arra
case $object instanceof Alias:
$this->describeContainerAlias($object, $options);
break;
+ case $object instanceof EventDispatcherInterface:
+ $this->describeEventDispatcherListeners($object, $options);
+ break;
+ case is_callable($object):
+ $this->describeCallable($object, $options);
+ break;
default:
throw new \InvalidArgumentException(sprintf('Object of type "%s" is not describable.', get_class($object)));
}
@@ -177,6 +184,25 @@ protected function renderTable(TableHelper $table, $decorated = false)
abstract protected function describeContainerParameter($parameter, array $options = array());
/**
+ * Describes event dispatcher listeners.
+ *
+ * Common options are:
+ * * name: name of listened event
+ *
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param array $options
+ */
+ abstract protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array());
+
+ /**
+ * Describes a callable.
+ *
+ * @param callable $callable
+ * @param array $options
+ */
+ abstract protected function describeCallable($callable, array $options = array());
+
+ /**
* Formats a value as string.
*
* @param mixed $value
View
109 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php
@@ -19,6 +19,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -137,6 +138,22 @@ protected function describeContainerAlias(Alias $alias, array $options = array()
/**
* {@inheritdoc}
*/
+ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array())
+ {
+ $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, array_key_exists('event', $options) ? $options['event'] : null), $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function describeCallable($callable, array $options = array())
+ {
+ $this->writeData($this->getCallableData($callable, $options), $options);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
protected function describeContainerParameter($parameter, array $options = array())
{
$key = isset($options['parameter']) ? $options['parameter'] : '';
@@ -222,4 +239,96 @@ private function getContainerAliasData(Alias $alias)
'public' => $alias->isPublic(),
);
}
+
+ /**
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param string|null $event
+ *
+ * @return array
+ */
+ private function getEventDispatcherListenersData(EventDispatcherInterface $eventDispatcher, $event = null)
+ {
+ $data = array();
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $listener) {
+ $data[] = $this->getCallableData($listener);
+ }
+ } else {
+ ksort($registeredListeners);
+
+ foreach ($registeredListeners as $eventListened => $eventListeners) {
+ foreach ($eventListeners as $eventListener) {
+ $data[$eventListened][] = $this->getCallableData($eventListener);
+ }
+ }
+ }
+
+ return $data;
+ }
+
+ /**
+ * @param callable $callable
+ * @param array $options
+ *
+ * @return array
+ */
+ private function getCallableData($callable, array $options = array())
+ {
+ $data = array();
+
+ if (is_array($callable)) {
+ $data['type'] = 'function';
+
+ if (is_object($callable[0])) {
+ $data['name'] = $callable[1];
+ $data['class'] = get_class($callable[0]);
+ } else {
+ if (0 !== strpos($callable[1], 'parent::')) {
+ $data['name'] = $callable[1];
+ $data['class'] = $callable[0];
+ $data['static'] = true;
+ } else {
+ $data['name'] = substr($callable[1], 8);
+ $data['class'] = $callable[0];
+ $data['static'] = true;
+ $data['parent'] = true;
+ }
+ }
+
+ return $data;
+ }
+
+ if (is_string($callable)) {
+ $data['type'] = 'function';
+
+ if (false === strpos($callable, '::')) {
+ $data['name'] = $callable;
+ } else {
+ $callableParts = explode('::', $callable);
+
+ $data['name'] = $callableParts[1];
+ $data['class'] = $callableParts[0];
+ $data['static'] = true;
+ }
+
+ return $data;
+ }
+
+ if ($callable instanceof \Closure) {
+ $data['type'] = 'closure';
+
+ return $data;
+ }
+
+ if (method_exists($callable, '__invoke')) {
+ $data['type'] = 'object';
+ $data['name'] = get_class($callable);
+
+ return $data;
+ }
+
+ throw new \InvalidArgumentException('Callable is not describable.');
+ }
}
View
101 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php
@@ -15,6 +15,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -215,6 +216,106 @@ protected function describeContainerParameter($parameter, array $options = array
$this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', strlen($options['parameter'])), $this->formatParameter($parameter)): $parameter);
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array())
+ {
+ $event = array_key_exists('event', $options) ? $options['event'] : null;
+
+ $title = 'Registered listeners';
+ if (null !== $event) {
+ $title .= sprintf(' for event `%s` ordered by descending priority', $event);
+ }
+
+ $this->write(sprintf('# %s', $title)."\n");
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $order => $listener) {
+ $this->write("\n".sprintf('## Listener %d', $order + 1)."\n");
+ $this->describeCallable($listener);
+ }
+ } else {
+ ksort($registeredListeners);
+
+ foreach ($registeredListeners as $eventListened => $eventListeners) {
+ $this->write("\n".sprintf('## %s', $eventListened)."\n");
+
+ foreach ($eventListeners as $order => $eventListener) {
+ $this->write("\n".sprintf('### Listener %d', $order + 1)."\n");
+ $this->describeCallable($eventListener);
+ }
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function describeCallable($callable, array $options = array())
+ {
+ $string = '';
+
+ if (is_array($callable)) {
+ $string .= "\n- Type: `function`";
+
+ if (is_object($callable[0])) {
+ $string .= "\n".sprintf('- Name: `%s`', $callable[1]);
+ $string .= "\n".sprintf('- Class: `%s`', get_class($callable[0]));
+ } else {
+ if (0 !== strpos($callable[1], 'parent::')) {
+ $string .= "\n".sprintf('- Name: `%s`', $callable[1]);
+ $string .= "\n".sprintf('- Class: `%s`', $callable[0]);
+ $string .= "\n- Static: yes";
+ } else {
+ $string .= "\n".sprintf('- Name: `%s`', substr($callable[1], 8));
+ $string .= "\n".sprintf('- Class: `%s`', $callable[0]);
+ $string .= "\n- Static: yes";
+ $string .= "\n- Parent: yes";
+ }
+ }
+
+ return $this->write($string."\n");
+ }
+
+ if (is_string($callable)) {
+ $string .= "\n- Type: `function`";
+
+ if (false === strpos($callable, '::')) {
+ $string .= "\n".sprintf('- Name: `%s`', $callable);
+ } else {
+ $callableParts = explode('::', $callable);
+
+ $string .= "\n".sprintf('- Name: `%s`', $callableParts[1]);
+ $string .= "\n".sprintf('- Class: `%s`', $callableParts[0]);
+ $string .= "\n- Static: yes";
+ }
+
+ return $this->write($string."\n");
+ }
+
+ if ($callable instanceof \Closure) {
+ $string .= "\n- Type: `closure`";
+
+ return $this->write($string."\n");
+ }
+
+ if (method_exists($callable, '__invoke')) {
+ $string .= "\n- Type: `object`";
+ $string .= "\n".sprintf('- Name: `%s`', get_class($callable));
+
+ return $this->write($string."\n");
+ }
+
+ throw new \InvalidArgumentException('Callable is not describable.');
+ }
+
+ /**
+ * @param array $array
+ *
+ * @return string
+ */
private function formatRouterConfig(array $array)
{
if (!count($array)) {
View
83 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php
@@ -16,6 +16,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -285,6 +286,58 @@ protected function describeContainerParameter($parameter, array $options = array
}
/**
+ * {@inheritdoc}
+ */
+ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array())
+ {
+ $event = array_key_exists('event', $options) ? $options['event'] : null;
+
+ $label = 'Registered listeners';
+ if (null !== $event) {
+ $label .= sprintf(' for event <info>%s</info>', $event);
+ } else {
+ $label .= ' by event';
+ }
+
+ $this->writeText($this->formatSection('event_dispatcher', $label)."\n", $options);
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ $this->writeText("\n");
+ $table = new TableHelper();
+ $table->setHeaders(array('Order', 'Callable'));
+
+ foreach ($registeredListeners as $order => $listener) {
+ $table->addRow(array(sprintf('#%d', $order + 1), $this->formatCallable($listener)));
+ }
+
+ $this->renderTable($table);
+ } else {
+ ksort($registeredListeners);
+ foreach ($registeredListeners as $eventListened => $eventListeners) {
+ $this->writeText(sprintf("\n<info>[Event]</info> %s\n", $eventListened), $options);
+
+ $table = new TableHelper();
+ $table->setHeaders(array('Order', 'Callable'));
+
+ foreach ($eventListeners as $order => $eventListener) {
+ $table->addRow(array(sprintf('#%d', $order + 1), $this->formatCallable($eventListener)));
+ }
+
+ $this->renderTable($table);
+ }
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function describeCallable($callable, array $options = array())
+ {
+ $this->writeText($this->formatCallable($callable), $options);
+ }
+
+ /**
* @param array $array
*
* @return string
@@ -312,6 +365,36 @@ private function formatSection($section, $message)
}
/**
+ * @param callable $callable
+ *
+ * @return string
+ */
+ private function formatCallable($callable)
+ {
+ if (is_array($callable)) {
+ if (is_object($callable[0])) {
+ return sprintf('%s::%s()', get_class($callable[0]), $callable[1]);
+ }
+
+ return sprintf('%s::%s()', $callable[0], $callable[1]);
+ }
+
+ if (is_string($callable)) {
+ return sprintf('%s()', $callable);
+ }
+
+ if ($callable instanceof \Closure) {
+ return '\Closure()';
+ }
+
+ if (method_exists($callable, '__invoke')) {
+ return sprintf('%s::__invoke()', get_class($callable));
+ }
+
+ throw new \InvalidArgumentException('Callable is not describable.');
+ }
+
+ /**
* @param string $content
* @param array $options
*/
View
117 src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php
@@ -15,6 +15,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -94,6 +95,22 @@ protected function describeContainerAlias(Alias $alias, array $options = array()
/**
* {@inheritdoc}
*/
+ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = array())
+ {
+ $this->writeDocument($this->getEventDispatcherListenersDocument($eventDispatcher, array_key_exists('event', $options) ? $options['event'] : null));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function describeCallable($callable, array $options = array())
+ {
+ $this->writeDocument($this->getCallableDocument($callable));
+ }
+
+ /**
+ * {@inheritdoc}
+ */
protected function describeContainerParameter($parameter, array $options = array())
{
$this->writeDocument($this->getContainerParameterDocument($parameter, $options));
@@ -393,4 +410,104 @@ private function getContainerParameterDocument($parameter, $options = array())
return $dom;
}
+
+ /**
+ * @param EventDispatcherInterface $eventDispatcher
+ * @param string|null $event
+ *
+ * @return \DOMDocument
+ */
+ private function getEventDispatcherListenersDocument(EventDispatcherInterface $eventDispatcher, $event = null)
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->appendChild($eventDispatcherXML = $dom->createElement('event-dispatcher'));
+
+ $registeredListeners = $eventDispatcher->getListeners($event);
+ if (null !== $event) {
+ foreach ($registeredListeners as $listener) {
+ $callableXML = $this->getCallableDocument($listener);
+
+ $eventDispatcherXML->appendChild($eventDispatcherXML->ownerDocument->importNode($callableXML->childNodes->item(0), true));
+ }
+ } else {
+ ksort($registeredListeners);
+
+ foreach ($registeredListeners as $eventListened => $eventListeners) {
+ $eventDispatcherXML->appendChild($eventXML = $dom->createElement('event'));
+ $eventXML->setAttribute('name', $eventListened);
+
+ foreach ($eventListeners as $eventListener) {
+ $callableXML = $this->getCallableDocument($eventListener);
+
+ $eventXML->appendChild($eventXML->ownerDocument->importNode($callableXML->childNodes->item(0), true));
+ }
+ }
+ }
+
+ return $dom;
+ }
+
+ /**
+ * @param callable $callable
@stof Collaborator
stof added a note

missing @return \DOMDocument

Done. I rechecked all the PHPDoc to ensure that nothing is missing. I just didn't add the "@throws \InvalidArgumentException" on the describeCallable function because developers should not have to handle this exception but I can add it if you think it's necessary

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ *
+ * @return \DOMDocument
+ */
+ private function getCallableDocument($callable)
+ {
+ $dom = new \DOMDocument('1.0', 'UTF-8');
+ $dom->appendChild($callableXML = $dom->createElement('callable'));
+
+ if (is_array($callable)) {
+ $callableXML->setAttribute('type', 'function');
+
+ if (is_object($callable[0])) {
+ $callableXML->setAttribute('name', $callable[1]);
+ $callableXML->setAttribute('class', get_class($callable[0]));
+ } else {
+ if (0 !== strpos($callable[1], 'parent::')) {
+ $callableXML->setAttribute('name', $callable[1]);
+ $callableXML->setAttribute('class', $callable[0]);
+ $callableXML->setAttribute('static', 'true');
+ } else {
+ $callableXML->setAttribute('name', substr($callable[1], 8));
+ $callableXML->setAttribute('class', $callable[0]);
+ $callableXML->setAttribute('static', 'true');
+ $callableXML->setAttribute('parent', 'true');
+ }
+ }
+
+ return $dom;
+ }
+
+ if (is_string($callable)) {
+ $callableXML->setAttribute('type', 'function');
+
+ if (false === strpos($callable, '::')) {
+ $callableXML->setAttribute('name', $callable);
+ } else {
+ $callableParts = explode('::', $callable);
+
+ $callableXML->setAttribute('name', $callableParts[1]);
+ $callableXML->setAttribute('class', $callableParts[0]);
+ $callableXML->setAttribute('static', 'true');
+ }
+
+ return $dom;
+ }
+
+ if ($callable instanceof \Closure) {
+ $callableXML->setAttribute('type', 'closure');
+
+ return $dom;
+ }
+
+ if (method_exists($callable, '__invoke')) {
+ $callableXML->setAttribute('type', 'object');
+ $callableXML->setAttribute('name', get_class($callable));
+
+ return $dom;
+ }
+
+ throw new \InvalidArgumentException('Callable is not describable.');
+ }
}
View
41 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php
@@ -16,6 +16,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -102,6 +103,28 @@ public function getDescribeContainerParameterTestData()
return $data;
}
+ /** @dataProvider getDescribeEventDispatcherTestData */
+ public function testDescribeEventDispatcher(EventDispatcher $eventDispatcher, $expectedDescription, array $options)
+ {
+ $this->assertDescription($expectedDescription, $eventDispatcher, $options);
+ }
+
+ public function getDescribeEventDispatcherTestData()
+ {
+ return $this->getEventDispatcherDescriptionTestData(ObjectsProvider::getEventDispatchers());
+ }
+
+ /** @dataProvider getDescribeCallableTestData */
+ public function testDescribeCallable($callable, $expectedDescription)
+ {
+ $this->assertDescription($expectedDescription, $callable);
+ }
+
+ public function getDescribeCallableTestData()
+ {
+ return $this->getDescriptionTestData(ObjectsProvider::getCallables());
+ }
+
abstract protected function getDescriptor();
abstract protected function getFormat();
@@ -148,4 +171,22 @@ private function getContainerBuilderDescriptionTestData(array $objects)
return $data;
}
+
+ private function getEventDispatcherDescriptionTestData(array $objects)
+ {
+ $variations = array(
+ 'events' => array(),
+ 'event1' => array('event' => 'event1'),
+ );
+
+ $data = array();
+ foreach ($objects as $name => $object) {
+ foreach ($variations as $suffix => $options) {
+ $description = file_get_contents(sprintf('%s/../../Fixtures/Descriptor/%s_%s.%s', __DIR__, $name, $suffix, $this->getFormat()));
+ $data[] = array($object, $description, $options);
+ }
+ }
+
+ return $data;
+ }
}
View
45 src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php
@@ -15,6 +15,7 @@
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
+use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
@@ -121,4 +122,48 @@ public static function getContainerAliases()
'alias_2' => new Alias('service_2', false),
);
}
+
+ public static function getEventDispatchers()
+ {
+ $eventDispatcher = new EventDispatcher();
+
+ $eventDispatcher->addListener('event1', 'global_function');
+ $eventDispatcher->addListener('event1', function () { return 'Closure'; });
+ $eventDispatcher->addListener('event2', new CallableClass());
+
+ return array('event_dispatcher_1' => $eventDispatcher);
+ }
+
+ public static function getCallables()
+ {
+ return array(
+ 'callable_1' => 'array_key_exists',
+ 'callable_2' => array('Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass', 'staticMethod'),
+ 'callable_3' => array(new CallableClass(), 'method'),
+ 'callable_4' => 'Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass::staticMethod',
+ 'callable_5' => array('Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\ExtendedCallableClass', 'parent::staticMethod'),
+ 'callable_6' => function () { return 'Closure'; },
+ 'callable_7' => new CallableClass()
+ );
+ }
+}
+
+class CallableClass
+{
+ public function __invoke()
+ {
+ }
+ public static function staticMethod()
+ {
+ }
+ public function method()
+ {
+ }
+}
+
+class ExtendedCallableClass extends CallableClass
+{
+ public static function staticMethod()
+ {
+ }
}
View
4 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.json
@@ -0,0 +1,4 @@
+{
+ "type": "function",
+ "name": "array_key_exists"
+}
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.md
@@ -0,0 +1,2 @@
+- Type: `function`
+- Name: `array_key_exists`
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.txt
@@ -0,0 +1 @@
+array_key_exists()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_1.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="function" name="array_key_exists"/>
View
6 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.json
@@ -0,0 +1,6 @@
+{
+ "type": "function",
+ "name": "staticMethod",
+ "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass",
+ "static": true
+}
View
4 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.md
@@ -0,0 +1,4 @@
+- Type: `function`
+- Name: `staticMethod`
+- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass`
+- Static: yes
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.txt
@@ -0,0 +1 @@
+Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::staticMethod()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_2.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="function" name="staticMethod" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass" static="true"/>
View
5 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.json
@@ -0,0 +1,5 @@
+{
+ "type": "function",
+ "name": "method",
+ "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass"
+}
View
3  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.md
@@ -0,0 +1,3 @@
+- Type: `function`
+- Name: `method`
+- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass`
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.txt
@@ -0,0 +1 @@
+Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::method()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_3.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="function" name="method" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass"/>
View
6 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.json
@@ -0,0 +1,6 @@
+{
+ "type": "function",
+ "name": "staticMethod",
+ "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass",
+ "static": true
+}
View
4 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.md
@@ -0,0 +1,4 @@
+- Type: `function`
+- Name: `staticMethod`
+- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass`
+- Static: yes
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.txt
@@ -0,0 +1 @@
+Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::staticMethod()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_4.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="function" name="staticMethod" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass" static="true"/>
View
7 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.json
@@ -0,0 +1,7 @@
+{
+ "type": "function",
+ "name": "staticMethod",
+ "class": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\ExtendedCallableClass",
+ "static": true,
+ "parent": true
+}
View
5 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.md
@@ -0,0 +1,5 @@
+- Type: `function`
+- Name: `staticMethod`
+- Class: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\ExtendedCallableClass`
+- Static: yes
+- Parent: yes
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.txt
@@ -0,0 +1 @@
+Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\ExtendedCallableClass::parent::staticMethod()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_5.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="function" name="staticMethod" class="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\ExtendedCallableClass" static="true" parent="true"/>
View
3  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.json
@@ -0,0 +1,3 @@
+{
+ "type": "closure"
+}
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.md
@@ -0,0 +1 @@
+- Type: `closure`
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.txt
@@ -0,0 +1 @@
+\Closure()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_6.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="closure"/>
View
4 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.json
@@ -0,0 +1,4 @@
+{
+ "type": "object",
+ "name": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass"
+}
View
3  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.md
@@ -0,0 +1,3 @@
+- Type: `object`
+- Name: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass`
+
View
1  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.txt
@@ -0,0 +1 @@
+Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::__invoke()
View
2  src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/callable_7.xml
@@ -0,0 +1,2 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<callable type="object" name="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass"/>
View
9 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json
@@ -0,0 +1,9 @@
+[
+ {
+ "type": "function",
+ "name": "global_function"
+ },
+ {
+ "type": "closure"
+ }
+]
View
10 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md
@@ -0,0 +1,10 @@
+# Registered listeners for event `event1` ordered by descending priority
+
+## Listener 1
+
+- Type: `function`
+- Name: `global_function`
+
+## Listener 2
+
+- Type: `closure`
View
8 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt
@@ -0,0 +1,8 @@
+<info>[event_dispatcher]</info> Registered listeners for event <info>event1</info>
+
++-------+-------------------+
+| Order | Callable |
++-------+-------------------+
+| #1 | global_function() |
+| #2 | \Closure() |
++-------+-------------------+
View
5 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event-dispatcher>
+ <callable type="function" name="global_function"/>
+ <callable type="closure"/>
+</event-dispatcher>
View
17 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json
@@ -0,0 +1,17 @@
+{
+ "event1": [
+ {
+ "type": "function",
+ "name": "global_function"
+ },
+ {
+ "type": "closure"
+ }
+ ],
+ "event2": [
+ {
+ "type": "object",
+ "name": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass"
+ }
+ ]
+}
View
19 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md
@@ -0,0 +1,19 @@
+# Registered listeners
+
+## event1
+
+### Listener 1
+
+- Type: `function`
+- Name: `global_function`
+
+### Listener 2
+
+- Type: `closure`
+
+## event2
+
+### Listener 1
+
+- Type: `object`
+- Name: `Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass`
View
16 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt
@@ -0,0 +1,16 @@
+<info>[event_dispatcher]</info> Registered listeners by event
+
+<info>[Event]</info> event1
++-------+-------------------+
+| Order | Callable |
++-------+-------------------+
+| #1 | global_function() |
+| #2 | \Closure() |
++-------+-------------------+
+
+<info>[Event]</info> event2
++-------+-----------------------------------------------------------------------------------+
+| Order | Callable |
++-------+-----------------------------------------------------------------------------------+
+| #1 | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass::__invoke() |
++-------+-----------------------------------------------------------------------------------+
View
10 src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<event-dispatcher>
+ <event name="event1">
+ <callable type="function" name="global_function"/>
+ <callable type="closure"/>
+ </event>
+ <event name="event2">
+ <callable type="object" name="Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\CallableClass"/>
+ </event>
+</event-dispatcher>
Something went wrong with that request. Please try again.