diff --git a/CHANGELOG.md b/CHANGELOG.md index dff55ab9..2597010a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## Not released yet +* Allow to get null instead of throwing an exception when calling `task(true)` without a current task +* Add `ProcessStartEvent` and `ProcessTerminateEvent` events +* Allow to listen to the symfony console events * Add a `compile` command that puts together a customizable PHP binary with a repacked castor app into one executable file * Set the process title according to the current application name and task name diff --git a/doc/going-further/extending-castor/events.md b/doc/going-further/extending-castor/events.md index 2be251d3..65679837 100644 --- a/doc/going-further/extending-castor/events.md +++ b/doc/going-further/extending-castor/events.md @@ -39,3 +39,17 @@ Here is the built-in events triggered by Castor: * `Castor\Event\AfterExecuteTaskEvent`: This event is triggered after executing a task. It provides access to the `TaskCommand` instance. + +* `Castor\Event\ProcessStartEvent`: This event is triggered after a process has + been started by the `run` function. It provides access to the `Process` + instance. + +* `Castor\Event\ProcessTerminateEvent`: This event is triggered after a process has + been terminated and launched inside the `run` function. It provides access to + the `Process` instance. + +## Console events + +Castor also provides a set of events related to the symfony console application, +which can be used to listen to the console lifecycle, see the [symfony documentation +to learn more about the console events](https://symfony.com/doc/current/components/console/events.html). diff --git a/doc/going-further/helpers/console-and-io.md b/doc/going-further/helpers/console-and-io.md index e742cf80..e2e1fb18 100644 --- a/doc/going-further/helpers/console-and-io.md +++ b/doc/going-further/helpers/console-and-io.md @@ -67,7 +67,7 @@ object. ## The `task()` function -Castor provides the `task()` to access the current +Castor provides the `task(bool $allowNull = false)` to access the current [`Symfony Command`](https://github.com/symfony/symfony/blob/6.3/src/Symfony/Component/Console/Command/Command.php) object that powers the task currently run by the user. @@ -93,3 +93,8 @@ function bar(): void `castor bar` will output `bar`, not `foo`, even if this is the `foo()` function that triggers the call to `task()`. + +In some cases there may be no task to return, if an event listener is triggered +before the task or during a context initialization for example. In this case, +`task()` will throw an exception. You can use the optional parameter to allow +`task(true)` to return `null` in this case. diff --git a/examples/event-listener.php b/examples/event-listener.php index ab183ec6..752aa597 100644 --- a/examples/event-listener.php +++ b/examples/event-listener.php @@ -6,13 +6,19 @@ use Castor\Attribute\AsTask; use Castor\Event\AfterExecuteTaskEvent; use Castor\Event\BeforeExecuteTaskEvent; +use Castor\Event\ProcessStartEvent; +use Castor\Event\ProcessTerminateEvent; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; use function Castor\io; +use function Castor\run; +use function Castor\task; #[AsTask(description: 'An dummy task with event listeners attached')] function my_task(): void { - io()->writeln('Hello from task!'); + run('echo "Hello from task!"'); } #[AsListener(event: BeforeExecuteTaskEvent::class, priority: 1)] @@ -49,3 +55,35 @@ function my_listener_that_has_higher_priority_for_multiple_events(BeforeExecuteT io()->writeln('Ola from listener! I am listening to multiple events but only showing only for AfterExecuteTaskEvent'); } } + +#[AsListener(event: ConsoleEvents::TERMINATE)] +function console_terminate_event(ConsoleTerminateEvent $event): void +{ + if ('event-listener:my-task' === $event->getCommand()?->getName()) { + io()->writeln('Hello from console terminate event listener!'); + } +} + +#[AsListener(event: ProcessTerminateEvent::class)] +function process_terminate_event(ProcessTerminateEvent $event): void +{ + $command = task(true); + + if ('event-listener:my-task' !== $command?->getName()) { + return; + } + + io()->writeln('Hello after process stop!'); +} + +#[AsListener(event: ProcessStartEvent::class)] +function process_start_event(ProcessStartEvent $event): void +{ + $command = task(true); + + if ('event-listener:my-task' !== $command?->getName()) { + return; + } + + io()->writeln('Hello after process start!'); +} diff --git a/src/Console/Application.php b/src/Console/Application.php index 96ebdda0..45171af4 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -119,9 +119,12 @@ public function getSymfonyStyle(): SymfonyStyle return $this->symfonyStyle ?? throw new \LogicException('SymfonyStyle not available yet.'); } - public function getCommand(): Command + /** + * @return ($allowNull is true ? ?Command : Command) + */ + public function getCommand(bool $allowNull = false): ?Command { - return $this->command ?? throw new \LogicException('Command not available yet.'); + return $this->command ?? ($allowNull ? null : throw new \LogicException('Command not available yet.')); } // We do all the logic as late as possible to ensure the exception handler diff --git a/src/Console/ApplicationFactory.php b/src/Console/ApplicationFactory.php index ce258f9a..e59122d8 100644 --- a/src/Console/ApplicationFactory.php +++ b/src/Console/ApplicationFactory.php @@ -47,6 +47,7 @@ public static function create(): SymfonyApplication $cache = new FilesystemAdapter(directory: $cacheDir); $logger = new Logger('castor', [], [new ProcessProcessor()]); $fs = new Filesystem(); + $eventDispatcher = new EventDispatcher(logger: $logger); /** @var SymfonyApplication */ // @phpstan-ignore-next-line @@ -54,7 +55,7 @@ public static function create(): SymfonyApplication $rootDir, new FunctionFinder($cache, $rootDir), $contextRegistry, - new EventDispatcher(logger: $logger), + $eventDispatcher, new ExpressionLanguage($contextRegistry), new StubsGenerator($logger), $logger, @@ -65,6 +66,7 @@ public static function create(): SymfonyApplication new FingerprintHelper($cache), ); + $application->setDispatcher($eventDispatcher); $application->add(new DebugCommand($rootDir, $cacheDir, $contextRegistry)); if (!class_exists(\RepackedApplication::class)) { diff --git a/src/Event/ProcessStartEvent.php b/src/Event/ProcessStartEvent.php new file mode 100644 index 00000000..27ba9ef2 --- /dev/null +++ b/src/Event/ProcessStartEvent.php @@ -0,0 +1,13 @@ +getSymfonyStyle(); } - public static function getCommand(): Command + /** + * @return ($allowNull is true ? ?Command : Command) + */ + public static function getCommand(bool $allowNull = false): ?Command { - return self::getApplication()->getCommand(); + return self::getApplication()->getCommand($allowNull); } public static function getContext(?string $name = null): Context diff --git a/src/functions.php b/src/functions.php index 8154eb76..fdcf6ab8 100644 --- a/src/functions.php +++ b/src/functions.php @@ -200,6 +200,8 @@ function run( } }); + GlobalHelper::getApplication()->eventDispatcher->dispatch(new Event\ProcessStartEvent($process)); + if (\Fiber::getCurrent()) { while ($process->isRunning()) { GlobalHelper::getSectionOutput()->tickProcess($process); @@ -208,8 +210,12 @@ function run( } } - $exitCode = $process->wait(); - GlobalHelper::getSectionOutput()->finishProcess($process); + try { + $exitCode = $process->wait(); + } finally { + GlobalHelper::getSectionOutput()->finishProcess($process); + GlobalHelper::getApplication()->eventDispatcher->dispatch(new Event\ProcessTerminateEvent($process)); + } if ($context->notify) { notify(sprintf('The command "%s" has been finished %s.', $process->getCommandLine(), 0 === $exitCode ? 'successfully' : 'with an error')); @@ -658,9 +664,12 @@ function variable(string $key, mixed $default = null): mixed return GlobalHelper::getVariable($key, $default); } -function task(): Command +/** + * @return ($allowNull is true ? ?Command : Command) + */ +function task(bool $allowNull = false): ?Command { - return GlobalHelper::getCommand(); + return GlobalHelper::getCommand($allowNull); } function get_command(): Command diff --git a/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt b/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt index 1086bc58..9ce661af 100644 --- a/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt +++ b/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt @@ -1,5 +1,8 @@ Hello from listener! (higher priority) before task execution Hello from listener! (lower priority) Ola from listener! I am listening to multiple events but only showing only for BeforeExecuteTaskEvent +Hello after process start! Hello from task! +Hello after process stop! Ola from listener! I am listening to multiple events but only showing only for AfterExecuteTaskEvent +Hello from console terminate event listener!