Skip to content

Loading…

Console dispatcher #7466

Merged
merged 3 commits into from
@fabpot
Symfony member
Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #3889, #6124
License MIT
Doc PR symfony/symfony-docs#2352

refs #1884, #1929

This is an alternative implementation for adding events to console applications.

This implementation has the following features:

  • Available for anyone using the Console component and it is not tied to
    FrameworkBundle (this is important as one thing we are trying to solve is
    email sending from a command, and frameworks like Silex using the Console
    component needs a solution too);

  • Non-intrusive as the current code has not been changed (except for renaming
    an internal variable that was wrongly named -- so that's not strictly needed
    for this PR)

  • The new DispatchableApplication class also works without a dispatcher,
    falling back to the regular behavior. That makes easy to create applications
    that can benefit from a dispatcher when available, but can still work
    otherwise.

  • Besides the before and after events, there is also an exception event
    that is dispatched whenever an exception is thrown.

  • Each event is quite powerful and can manipulate the input, the output, but
    also the command to be executed.

flevour and others added some commits
@flevour flevour Added events for CLI commands
This adds an init and terminate event for commands. They are
dispatched from ContainerAwareCommand.

The cache:clear command can't implement this (cf. #3889 on Github).
f224102
@bamarni bamarni added helperSet to console event objects 4edf29d
@fabpot
Symfony member
@fabpot fabpot referenced this pull request in symfony/symfony-docs
Merged

added documentation for the dispatchable console application #2352

@stloyd stloyd commented on an outdated diff
...Symfony/Component/Console/DispatchableApplication.php
((9 lines not shown))
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console;
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\ConsoleEvents;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\Console\Event\ConsoleForExceptionEvent;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+
+/**
+ * Optionnally dispatches events during the life time of a command run.
@stloyd
stloyd added a note

Typo: Optionally

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@stloyd stloyd commented on an outdated diff
.../Component/Console/Event/ConsoleForExceptionEvent.php
((8 lines not shown))
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * .
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ConsoleForExceptionEvent extends ConsoleEvent
@stloyd
stloyd added a note

Why not extend the ConsoleTerminateEvent class and reuse exitCode part?

@fabpot Symfony member
fabpot added a note

Because of the setExitCode method that does not belong to this class.

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

Yeah it would be awesome! :+1:

@Koc

Will services tagged as "cache warmer" replaced with listeners for cache:clear command?

@beberlei

best way to introduce this feature by a longshot without changing existing code, +1 from me.

@lyrixx lyrixx commented on an outdated diff
...Symfony/Component/Console/DispatchableApplication.php
((42 lines not shown))
+
+ $event = new ConsoleCommandEvent($command, $input, $output);
+ $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
+
+ $command = $event->getCommand();
+
+ try {
+ $exitCode = parent::doRunCommand($command, $input, $output);
+ } catch (\Exception $e) {
+ $event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode());
+ $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
+
+ $event = new ConsoleForExceptionEvent($command, $input, $output, $e, $event->getExitCode());
+ $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
+
+ throw $event->getException();
@lyrixx
lyrixx added a note

It could be useful to be able to not throw the exception ; like in the HttpKernel.

Use case : you have a long running process like a rabbitmq consumer. With this feature, we could delegate the exception process into a dedicated event listener and make the "raw" command cleaner.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@schmittjoh schmittjoh commented on an outdated diff
...Symfony/Component/Console/DispatchableApplication.php
((13 lines not shown))
+
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\ConsoleEvents;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\Console\Event\ConsoleForExceptionEvent;
+use Symfony\Component\EventDispatcher\EventDispatcher;
+
+/**
+ * Optionally dispatches events during the life time of a command run.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class DispatchableApplication extends Application

DispatchingApplication or EventDispatchingApplication seem better names as the application itself is not dispatched, but is dispatching events.

In general, I think it would make more sense to use composition here and not inheritance to add the event dispatching behavior.

$app = new EventDispatchingApplication(new Application(), new EventDispatcher());

This also allows to add/compose other behaviors more easily.

@fabpot Symfony member
fabpot added a note

I wanted to use composition at first, but as the Application class has so many public methods (about 30 IIRC), it would make the implementation very verbose.

Unfortunately, this will make something like the following very hard:
https://github.com/schmittjoh/JMSJobQueueBundle/blob/master/Console/Application.php

I understand that duplicating 30 methods is not exactly user-friendly/maintainable. Maybe this is a sign that we should refactor the Application class? We could keep the old one untouched and deprecate it, and then add a new one with a more focused interface.

@Koc
Koc added a note

Is this events not enough and you need anyway to create your own Application class?

@fabpot Symfony member
fabpot added a note

I don't think this is a sign of anything and stability is more important here than anything else. As the new Applicaiton class is able to work with and without an event dispatcher, we can even merge everything back to the main Application class without any BC break. What do you/others think about this possibility?

Yeah, having just a single class seems better.

@fabpot Symfony member
fabpot added a note

@schmittjoh see f7336d0

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

@Koc that doesn't make sense imho, Cache Warming should work independent of console listeners.

@Koc

@fabpot can we get all output as a string in terminate event?

@marcqualie

:+1: I've been working with a few Symfony Console apps lately and this functionality would be awesome!

@fabpot
Symfony member

@Koc: you can wrap/change the output instance in the command event, so that you can then get what was displayed in the terminate event. This PR gives you the flexibility, it's then up to you to use it the way you want.

@lyrixx

(I repost my comment, github hide it for now)

It could be useful to be able to not throw the exception in Application::doRunCommand ; like in the HttpKernel.

Use case : you have a long running process like a rabbitmq consumer. With this feature, we could delegate the exception process into a dedicated event listener and make the "raw" command cleaner.

@bamarni

good to see this moving forward :+1:

@stof had previously spotted a bug with this feature and the cache:clear command, see (#3889 (comment) for the beginning of the discussion), basically we came out to the conclusion that the cache:clear command shouldn't have this event dispatching capability, otherwise it would require some dirty hacks, do you think it will be easily doable when integrating this to the framework (as I can see this PR is only about the component part)?

@bamarni bamarni commented on the diff
.../Component/Console/Event/ConsoleForExceptionEvent.php
((8 lines not shown))
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to handle exception thrown in a command.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ConsoleForExceptionEvent extends ConsoleEvent
@bamarni
bamarni added a note

Why "For"? "ConsoleExceptionEvent" seems more consistent with the other ones.

@Seldaek Symfony member
Seldaek added a note

@fabpot did you just miss this? I don't understand the class name either.

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

I'd also have a remark about the ability to change the command to be executed through the pre-event, do you have any use case in mind? Imo this doesn't make so much sense.

I can see it'd use the same input object, It means that the changed command should have the same arguments / options than the first one, or basically that it should be able to work with the same input, it looks like a pretty restricting condition.

It is also not always so easy to instantiate a command object, for example when using this feature with the framework, one would always need to have the container so that it can passes it to the command (as usually they are container aware), it looks like all this would be kind of a "sub-application".

@stof stof commented on the diff
src/Symfony/Component/Console/Application.php
@@ -80,6 +85,11 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
}
}
+ public function setDispatcher(EventDispatcher $dispatcher)
@stof Symfony member
stof added a note

the typehint should be the interface

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

@fabpot Would you use the same listener than for the kernel when integrating it in a Sf2 project ? Because in this case, we would face the bug I reported in the previous PR: if you deleted a subscriber class and the kernel is in non-debug mode, creating the event dispatcher object will break (in debug mode, the container would be refreshed without the subscriber). But the cache:clear command should be able to be used even when you have an outdated cache as its goal is to clear it.

@stof
Symfony member

otherwise, :+1: for this implementation.

Should the listener flushing the swiftmailer memory spool be added in the bridge in this PR or in a subsequent one ?

@Crell Crell commented on an outdated diff
...mfony/Component/Console/Event/ConsoleCommandEvent.php
((4 lines not shown))
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to do things before the command is executed or to change the command to execute altogether.
@Crell
Crell added a note

This is not correct English grammar. I don't think the 3rd person verb fits here as a class description. You probably want something more like "Event object for pre-execution changes. \n\n Listeners may take actions before the command is executed, including changing the command to execute."

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

@bamarni You're right. I've removed the possibility to change the command in a console.command listener.

@fabpot
Symfony member

@lyrixx not sure how to implement that. Let's do that in another PR.

@fabpot
Symfony member

@stof @bamarni I'm aware of the possible problems with cache:clear. But we already have such issues today. When upgrading Symfony where a new parameter in the DIC is required without default value, you have the same problem. So, I propose to deal with that in another PR.

see #6519 for instance

@fabpot
Symfony member

@stof The Swiftmailer listener should be done in another PR, especially because it is not that easy (the best would be do be able to move the listener from the bundle to the bridge).

@fabpot fabpot added a commit that referenced this pull request
@fabpot fabpot merged branch fabpot/console-dispatcher (PR #7466)
This PR was merged into the master branch.

Discussion
----------

Console dispatcher

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #3889, #6124
| License       | MIT
| Doc PR        | symfony/symfony-docs#2352

refs #1884, #1929

This is an alternative implementation for adding events to console applications.

This implementation has the following features:

* Available for anyone using the Console component and it is not tied to
  FrameworkBundle (this is important as one thing we are trying to solve is
  email sending from a command, and frameworks like Silex using the Console
  component needs a solution too);

* Non-intrusive as the current code has not been changed (except for renaming
  an internal variable that was wrongly named -- so that's not strictly needed
  for this PR)

* The new DispatchableApplication class also works without a dispatcher,
  falling back to the regular behavior. That makes easy to create applications
  that can benefit from a dispatcher when available, but can still work
  otherwise.

* Besides the *before* and *after* events, there is also an *exception* event
  that is dispatched whenever an exception is thrown.

* Each event is quite powerful and can manipulate the input, the output, but
  also the command to be executed.

Commits
-------

4f9a55a refactored the implementation of how a console application can handle events
4edf29d added helperSet to console event objects
f224102 Added events for CLI commands
c1bd3b5
@fabpot fabpot merged commit 4f9a55a into symfony:master

1 check failed

Details default Scrutinizer: 1 Comments, 0 Changed Files
@pborreli pborreli pushed a commit that referenced this pull request
@fabpot fabpot merged branch Tobion/console-event-name (PR #8147)
This PR was merged into the 2.3 branch.

Discussion
----------

[Console] renamed ConsoleForExceptionEvent into ConsoleExceptionEvent

I wondered about the name and now I figured the problem was already raised in the PR: #7466 (diff)

So I think it should be fixed for LTS.

BC break: yes
test pass: yes

Commits
-------

554ab9f [Console] renamed ConsoleForExceptionEvent into ConsoleExceptionEvent
0270136
@i7ssan i7ssan referenced this pull request
Commit has since been removed from the repository and is no longer available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 24, 2013
  1. @flevour @fabpot

    Added events for CLI commands

    flevour committed with fabpot
    This adds an init and terminate event for commands. They are
    dispatched from ContainerAwareCommand.
    
    The cache:clear command can't implement this (cf. #3889 on Github).
  2. @bamarni @fabpot
Commits on Mar 25, 2013
  1. @fabpot
This page is out of date. Refresh to see the latest.
View
67 src/Symfony/Component/Console/Application.php
@@ -27,6 +27,10 @@
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\DialogHelper;
use Symfony\Component\Console\Helper\ProgressHelper;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleForExceptionEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\EventDispatcher\EventDispatcher;
/**
* An Application is the container for a collection of commands.
@@ -56,6 +60,7 @@ class Application
private $autoExit;
private $definition;
private $helperSet;
+ private $dispatcher;
/**
* Constructor.
@@ -80,6 +85,11 @@ public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN')
}
}
+ public function setDispatcher(EventDispatcher $dispatcher)
@stof Symfony member
stof added a note

the typehint should be the interface

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ {
+ $this->dispatcher = $dispatcher;
+ }
+
/**
* Runs the current application.
*
@@ -103,7 +113,7 @@ public function run(InputInterface $input = null, OutputInterface $output = null
}
try {
- $statusCode = $this->doRun($input, $output);
+ $exitCode = $this->doRun($input, $output);
} catch (\Exception $e) {
if (!$this->catchExceptions) {
throw $e;
@@ -114,21 +124,21 @@ public function run(InputInterface $input = null, OutputInterface $output = null
} else {
$this->renderException($e, $output);
}
- $statusCode = $e->getCode();
+ $exitCode = $e->getCode();
- $statusCode = is_numeric($statusCode) && $statusCode ? $statusCode : 1;
+ $exitCode = is_numeric($exitCode) && $exitCode ? $exitCode : 1;
}
if ($this->autoExit) {
- if ($statusCode > 255) {
- $statusCode = 255;
+ if ($exitCode > 255) {
+ $exitCode = 255;
}
// @codeCoverageIgnoreStart
- exit($statusCode);
+ exit($exitCode);
// @codeCoverageIgnoreEnd
}
- return $statusCode;
+ return $exitCode;
}
/**
@@ -190,10 +200,10 @@ public function doRun(InputInterface $input, OutputInterface $output)
$command = $this->find($name);
$this->runningCommand = $command;
- $statusCode = $command->run($input, $output);
+ $exitCode = $this->doRunCommand($command, $input, $output);
$this->runningCommand = null;
- return is_numeric($statusCode) ? $statusCode : 0;
+ return is_numeric($exitCode) ? $exitCode : 0;
}
/**
@@ -912,6 +922,45 @@ public function getTerminalDimensions()
}
/**
+ * Runs the current command.
+ *
+ * If an event dispatcher has been attached to the application,
+ * events are also dispatched during the life-cycle of the command.
+ *
+ * @param Command $command A Command instance
+ * @param InputInterface $input An Input instance
+ * @param OutputInterface $output An Output instance
+ *
+ * @return integer 0 if everything went fine, or an error code
+ */
+ protected function doRunCommand(Command $command, InputInterface $input, OutputInterface $output)
+ {
+ if (null === $this->dispatcher) {
+ return $command->run($input, $output);
+ }
+
+ $event = new ConsoleCommandEvent($command, $input, $output);
+ $this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);
+
+ try {
+ $exitCode = $command->run($input, $output);
+ } catch (\Exception $e) {
+ $event = new ConsoleTerminateEvent($command, $input, $output, $e->getCode());
+ $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
+
+ $event = new ConsoleForExceptionEvent($command, $input, $output, $e, $event->getExitCode());
+ $this->dispatcher->dispatch(ConsoleEvents::EXCEPTION, $event);
+
+ throw $event->getException();
+ }
+
+ $event = new ConsoleTerminateEvent($command, $input, $output, $exitCode);
+ $this->dispatcher->dispatch(ConsoleEvents::TERMINATE, $event);
+
+ return $event->getExitCode();
+ }
+
+ /**
* Gets the name of the command based on input.
*
* @param InputInterface $input The input interface
View
1 src/Symfony/Component/Console/CHANGELOG.md
@@ -4,6 +4,7 @@ CHANGELOG
2.3.0
-----
+ * added support for events in `Application`
* added a way to set the progress bar progress via the `setCurrent` method
2.2.0
View
55 src/Symfony/Component/Console/ConsoleEvents.php
@@ -0,0 +1,55 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console;
+
+/**
+ * Contains all events dispatched by an Application.
+ *
+ * @author Francesco Levorato <git@flevour.net>
+ */
+final class ConsoleEvents
+{
+ /**
+ * The COMMAND event allows you to attach listeners before any command is
+ * executed by the console. It also allows you to modify the command, input and output
+ * before they are handled to the command.
+ *
+ * The event listener method receives a Symfony\Component\Console\Event\ConsoleCommandEvent
+ * instance.
+ *
+ * @var string
+ */
+ const COMMAND = 'console.command';
+
+ /**
+ * The TERMINATE event allows you to attach listeners after a command is
+ * executed by the console.
+ *
+ * The event listener method receives a Symfony\Component\Console\Event\ConsoleTerminateEvent
+ * instance.
+ *
+ * @var string
+ */
+ const TERMINATE = 'console.terminate';
+
+ /**
+ * The EXCEPTION event occurs when an uncaught exception appears.
+ *
+ * This event allows you to deal with the exception or
+ * to modify the thrown exception. The event listener method receives
+ * a Symfony\Component\Console\Event\ConsoleForExceptionEvent
+ * instance.
+ *
+ * @var string
+ */
+ const EXCEPTION = 'console.exception';
+}
View
25 src/Symfony/Component/Console/Event/ConsoleCommandEvent.php
@@ -0,0 +1,25 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to do things before the command is executed.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ConsoleCommandEvent extends ConsoleEvent
+{
+}
View
67 src/Symfony/Component/Console/Event/ConsoleEvent.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\EventDispatcher\Event;
+
+/**
+ * Allows to inspect input and output of a command.
+ *
+ * @author Francesco Levorato <git@flevour.net>
+ */
+class ConsoleEvent extends Event
+{
+ protected $command;
+
+ private $input;
+ private $output;
+
+ public function __construct(Command $command, InputInterface $input, OutputInterface $output)
+ {
+ $this->command = $command;
+ $this->input = $input;
+ $this->output = $output;
+ }
+
+ /**
+ * Gets the command that is executed.
+ *
+ * @return Command A Command instance
+ */
+ public function getCommand()
+ {
+ return $this->command;
+ }
+
+ /**
+ * Gets the input instance.
+ *
+ * @return InputInterface An InputInterface instance
+ */
+ public function getInput()
+ {
+ return $this->input;
+ }
+
+ /**
+ * Gets the output instance.
+ *
+ * @return OutputInterface An OutputInterface instance
+ */
+ public function getOutput()
+ {
+ return $this->output;
+ }
+}
View
67 src/Symfony/Component/Console/Event/ConsoleForExceptionEvent.php
@@ -0,0 +1,67 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to handle exception thrown in a command.
+ *
+ * @author Fabien Potencier <fabien@symfony.com>
+ */
+class ConsoleForExceptionEvent extends ConsoleEvent
@bamarni
bamarni added a note

Why "For"? "ConsoleExceptionEvent" seems more consistent with the other ones.

@Seldaek Symfony member
Seldaek added a note

@fabpot did you just miss this? I don't understand the class name either.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+{
+ private $exception;
+ private $exitCode;
+
+ public function __construct(Command $command, InputInterface $input, OutputInterface $output, \Exception $exception, $exitCode)
+ {
+ parent::__construct($command, $input, $output);
+
+ $this->setException($exception);
+ $this->exitCode = $exitCode;
+ }
+
+ /**
+ * Returns the thrown exception.
+ *
+ * @return \Exception The thrown exception
+ */
+ public function getException()
+ {
+ return $this->exception;
+ }
+
+ /**
+ * Replaces the thrown exception.
+ *
+ * This exception will be thrown if no response is set in the event.
+ *
+ * @param \Exception $exception The thrown exception
+ */
+ public function setException(\Exception $exception)
+ {
+ $this->exception = $exception;
+ }
+
+ /**
+ * Gets the exit code.
+ *
+ * @return integer The command exit code
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+}
View
58 src/Symfony/Component/Console/Event/ConsoleTerminateEvent.php
@@ -0,0 +1,58 @@
+<?php
+
+/*
+ * This file is part of the Symfony package.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Console\Event;
+
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+/**
+ * Allows to manipulate the exit code of a command after its execution.
+ *
+ * @author Francesco Levorato <git@flevour.net>
+ */
+class ConsoleTerminateEvent extends ConsoleEvent
+{
+ /**
+ * The exit code of the command.
+ *
+ * @var integer
+ */
+ private $exitCode;
+
+ public function __construct(Command $command, InputInterface $input, OutputInterface $output, $exitCode)
+ {
+ parent::__construct($command, $input, $output);
+
+ $this->setExitCode($exitCode);
+ }
+
+ /**
+ * Sets the exit code.
+ *
+ * @param integer $exitCode The command exit code
+ */
+ public function setExitCode($exitCode)
+ {
+ $this->exitCode = $exitCode;
+ }
+
+ /**
+ * Gets the exit code.
+ *
+ * @return integer The command exit code
+ */
+ public function getExitCode()
+ {
+ return $this->exitCode;
+ }
+}
View
89 src/Symfony/Component/Console/Tests/ApplicationTest.php
@@ -15,12 +15,18 @@
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Input\ArrayInput;
+use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\Output;
+use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Tester\ApplicationTester;
+use Symfony\Component\Console\Event\ConsoleCommandEvent;
+use Symfony\Component\Console\Event\ConsoleForExceptionEvent;
+use Symfony\Component\Console\Event\ConsoleTerminateEvent;
+use Symfony\Component\EventDispatcher\EventDispatcher;
class ApplicationTest extends \PHPUnit_Framework_TestCase
{
@@ -634,6 +640,89 @@ public function testSettingCustomInputDefinitionOverwritesDefaultValues()
$this->assertTrue($inputDefinition->hasOption('custom'));
}
+
+ public function testRunWithDispatcher()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $application = new Application();
+ $application->setAutoExit(false);
+ $application->setDispatcher($this->getDispatcher());
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $output->write('foo.');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ $this->assertEquals('before.foo.after.', $tester->getDisplay());
+ }
+
+ /**
+ * @expectedException \LogicException
+ * @expectedExceptionMessage caught
+ */
+ public function testRunWithExceptionAndDispatcher()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $application = new Application();
+ $application->setDispatcher($this->getDispatcher());
+ $application->setAutoExit(false);
+ $application->setCatchExceptions(false);
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ throw new \RuntimeException('foo');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ }
+
+ public function testRunDispatchesAllEventsWithException()
+ {
+ if (!class_exists('Symfony\Component\EventDispatcher\EventDispatcher')) {
+ $this->markTestSkipped('The "EventDispatcher" component is not available');
+ }
+
+ $application = new Application();
+ $application->setDispatcher($this->getDispatcher());
+ $application->setAutoExit(false);
+
+ $application->register('foo')->setCode(function (InputInterface $input, OutputInterface $output) {
+ $output->write('foo.');
+
+ throw new \RuntimeException('foo');
+ });
+
+ $tester = new ApplicationTester($application);
+ $tester->run(array('command' => 'foo'));
+ $this->assertContains('before.foo.after.caught.', $tester->getDisplay());
+ }
+
+ protected function getDispatcher()
+ {
+ $dispatcher = new EventDispatcher;
+ $dispatcher->addListener('console.command', function (ConsoleCommandEvent $event) {
+ $event->getOutput()->write('before.');
+ });
+ $dispatcher->addListener('console.terminate', function (ConsoleTerminateEvent $event) {
+ $event->getOutput()->write('after.');
+
+ $event->setExitCode(128);
+ });
+ $dispatcher->addListener('console.exception', function (ConsoleForExceptionEvent $event) {
+ $event->getOutput()->writeln('caught.');
+
+ $event->setException(new \LogicException('caught.', $event->getExitCode(), $event->getException()));
+ });
+
+ return $dispatcher;
+ }
}
class CustomApplication extends Application
View
3 src/Symfony/Component/Console/composer.json
@@ -18,6 +18,9 @@
"require": {
"php": ">=5.3.3"
},
+ "require-dev": {
+ "symfony/event-dispatcher": "~2.1"
+ },
"autoload": {
"psr-0": { "Symfony\\Component\\Console\\": "" }
},
Something went wrong with that request. Please try again.