From 7daa13afe3831b6553f582cf9b5734e18130aad0 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 10:56:20 +0100 Subject: [PATCH 1/7] feat(event): allow to listen to symfony console events --- examples/event-listener.php | 10 ++++++++++ src/Console/Application.php | 1 + .../Generated/EventListenerMyTaskTest.php.output.txt | 1 + 3 files changed, 12 insertions(+) diff --git a/examples/event-listener.php b/examples/event-listener.php index ab183ec6..22106372 100644 --- a/examples/event-listener.php +++ b/examples/event-listener.php @@ -7,6 +7,8 @@ use Castor\Event\AfterExecuteTaskEvent; use Castor\Event\BeforeExecuteTaskEvent; +use Symfony\Component\Console\ConsoleEvents; +use Symfony\Component\Console\Event\ConsoleTerminateEvent; use function Castor\io; #[AsTask(description: 'An dummy task with event listeners attached')] @@ -49,3 +51,11 @@ 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, priority: 10)] +function console_terminate_event(ConsoleTerminateEvent $event): void +{ + if ($event->getCommand()->getName() === 'event-listener:my-task') { + io()->writeln('Hello from console terminate event listener!'); + } +} diff --git a/src/Console/Application.php b/src/Console/Application.php index 96ebdda0..448a92fd 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -90,6 +90,7 @@ public function __construct( ]); $this->setCatchErrors(true); + $this->setDispatcher($eventDispatcher); AbstractCloner::$defaultCasters[self::class] = ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals']; AbstractCloner::$defaultCasters[AfterApplicationInitializationEvent::class] = ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals']; diff --git a/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt b/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt index 1086bc58..83d1528b 100644 --- a/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt +++ b/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt @@ -3,3 +3,4 @@ Hello from listener! (lower priority) Ola from listener! I am listening to multiple events but only showing only for BeforeExecuteTaskEvent Hello from task! Ola from listener! I am listening to multiple events but only showing only for AfterExecuteTaskEvent +Hello from console terminate event listener! From ddcde4639fc9e884776bb77cc6bb729814075788 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 11:09:19 +0100 Subject: [PATCH 2/7] feat(event): add process events --- examples/event-listener.php | 31 ++++++++++++++++--- src/Event/ProcessStartEvent.php | 15 +++++++++ src/Event/ProcessTerminateEvent.php | 15 +++++++++ src/functions.php | 11 +++++-- .../EventListenerMyTaskTest.php.output.txt | 2 ++ 5 files changed, 68 insertions(+), 6 deletions(-) create mode 100644 src/Event/ProcessStartEvent.php create mode 100644 src/Event/ProcessTerminateEvent.php diff --git a/examples/event-listener.php b/examples/event-listener.php index 22106372..ed52e6aa 100644 --- a/examples/event-listener.php +++ b/examples/event-listener.php @@ -6,15 +6,18 @@ 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; #[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)] @@ -52,10 +55,30 @@ function my_listener_that_has_higher_priority_for_multiple_events(BeforeExecuteT } } -#[AsListener(event: ConsoleEvents::TERMINATE, priority: 10)] +#[AsListener(event: ConsoleEvents::TERMINATE)] function console_terminate_event(ConsoleTerminateEvent $event): void { - if ($event->getCommand()->getName() === 'event-listener:my-task') { + 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 +{ + if ('event-listener:my-task' !== $event->command->getName()) { + return; + } + + io()->writeln('Hello after process stop!'); +} + +#[AsListener(event: ProcessStartEvent::class)] +function process_start_event(ProcessStartEvent $event): void +{ + if ('event-listener:my-task' !== $event->command->getName()) { + return; + } + + io()->writeln('Hello after process start!'); +} diff --git a/src/Event/ProcessStartEvent.php b/src/Event/ProcessStartEvent.php new file mode 100644 index 00000000..8901e3c4 --- /dev/null +++ b/src/Event/ProcessStartEvent.php @@ -0,0 +1,15 @@ +eventDispatcher->dispatch(new Event\ProcessStartEvent($process, $command)); + if (\Fiber::getCurrent()) { while ($process->isRunning()) { GlobalHelper::getSectionOutput()->tickProcess($process); @@ -208,8 +211,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, $command)); + } if ($context->notify) { notify(sprintf('The command "%s" has been finished %s.', $process->getCommandLine(), 0 === $exitCode ? 'successfully' : 'with an error')); diff --git a/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt b/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt index 83d1528b..9ce661af 100644 --- a/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt +++ b/tests/Examples/Generated/EventListenerMyTaskTest.php.output.txt @@ -1,6 +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! From 94179daf280cb70be5922dc694bdbb0511279b9a Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 11:12:13 +0100 Subject: [PATCH 3/7] feat(event): add doc about event --- doc/going-further/extending-castor/events.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/doc/going-further/extending-castor/events.md b/doc/going-further/extending-castor/events.md index 2be251d3..39f3daec 100644 --- a/doc/going-further/extending-castor/events.md +++ b/doc/going-further/extending-castor/events.md @@ -39,3 +39,15 @@ 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. It provides access to the `Process` instance and the current `Command`. + +* `Castor\Event\ProcessTerminateEvent`: This event is triggered after a process has + been terminated. It provides access to the `Process` instance and the current `Command`. + +## 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). From 5b2cea1bfa748ed2645eba9982590e796718b8df Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 11:30:43 +0100 Subject: [PATCH 4/7] feat(command): allow to return null for current command --- examples/event-listener.php | 9 +++++++-- src/Console/Application.php | 7 +++++-- src/Event/ProcessStartEvent.php | 2 -- src/Event/ProcessTerminateEvent.php | 2 -- src/GlobalHelper.php | 7 +++++-- src/functions.php | 12 +++++++----- 6 files changed, 24 insertions(+), 15 deletions(-) diff --git a/examples/event-listener.php b/examples/event-listener.php index ed52e6aa..752aa597 100644 --- a/examples/event-listener.php +++ b/examples/event-listener.php @@ -13,6 +13,7 @@ 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 @@ -66,7 +67,9 @@ function console_terminate_event(ConsoleTerminateEvent $event): void #[AsListener(event: ProcessTerminateEvent::class)] function process_terminate_event(ProcessTerminateEvent $event): void { - if ('event-listener:my-task' !== $event->command->getName()) { + $command = task(true); + + if ('event-listener:my-task' !== $command?->getName()) { return; } @@ -76,7 +79,9 @@ function process_terminate_event(ProcessTerminateEvent $event): void #[AsListener(event: ProcessStartEvent::class)] function process_start_event(ProcessStartEvent $event): void { - if ('event-listener:my-task' !== $event->command->getName()) { + $command = task(true); + + if ('event-listener:my-task' !== $command?->getName()) { return; } diff --git a/src/Console/Application.php b/src/Console/Application.php index 448a92fd..aa68fc3e 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -120,9 +120,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/Event/ProcessStartEvent.php b/src/Event/ProcessStartEvent.php index 8901e3c4..27ba9ef2 100644 --- a/src/Event/ProcessStartEvent.php +++ b/src/Event/ProcessStartEvent.php @@ -2,14 +2,12 @@ namespace Castor\Event; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Process\Process; class ProcessStartEvent { public function __construct( public readonly Process $process, - public readonly Command $command, ) { } } diff --git a/src/Event/ProcessTerminateEvent.php b/src/Event/ProcessTerminateEvent.php index 98a7ed17..cffa4254 100644 --- a/src/Event/ProcessTerminateEvent.php +++ b/src/Event/ProcessTerminateEvent.php @@ -2,14 +2,12 @@ namespace Castor\Event; -use Symfony\Component\Console\Command\Command; use Symfony\Component\Process\Process; class ProcessTerminateEvent { public function __construct( public readonly Process $process, - public readonly Command $command, ) { } } diff --git a/src/GlobalHelper.php b/src/GlobalHelper.php index ab5cb90f..c6a210ab 100644 --- a/src/GlobalHelper.php +++ b/src/GlobalHelper.php @@ -77,9 +77,12 @@ public static function getSymfonyStyle(): SymfonyStyle return self::getApplication()->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 feb1eb56..fdcf6ab8 100644 --- a/src/functions.php +++ b/src/functions.php @@ -200,8 +200,7 @@ function run( } }); - $command = GlobalHelper::getCommand(); - GlobalHelper::getApplication()->eventDispatcher->dispatch(new Event\ProcessStartEvent($process, $command)); + GlobalHelper::getApplication()->eventDispatcher->dispatch(new Event\ProcessStartEvent($process)); if (\Fiber::getCurrent()) { while ($process->isRunning()) { @@ -215,7 +214,7 @@ function run( $exitCode = $process->wait(); } finally { GlobalHelper::getSectionOutput()->finishProcess($process); - GlobalHelper::getApplication()->eventDispatcher->dispatch(new Event\ProcessTerminateEvent($process, $command)); + GlobalHelper::getApplication()->eventDispatcher->dispatch(new Event\ProcessTerminateEvent($process)); } if ($context->notify) { @@ -665,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 From 99487667f17741d01a990e9be017f090b2908780 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 11:38:52 +0100 Subject: [PATCH 5/7] feat(event): update doc, set event dispatcher in factory --- doc/going-further/extending-castor/events.md | 6 ++++-- doc/going-further/helpers/console-and-io.md | 7 ++++++- src/Console/Application.php | 1 - src/Console/ApplicationFactory.php | 4 +++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/doc/going-further/extending-castor/events.md b/doc/going-further/extending-castor/events.md index 39f3daec..aa0ae5a9 100644 --- a/doc/going-further/extending-castor/events.md +++ b/doc/going-further/extending-castor/events.md @@ -41,10 +41,12 @@ Here is the built-in events triggered by Castor: a task. It provides access to the `TaskCommand` instance. * `Castor\Event\ProcessStartEvent`: This event is triggered after a process has - been started. It provides access to the `Process` instance and the current `Command`. + been started by the `run` function. It provides access to the `Process` + instance and the current `Command`. * `Castor\Event\ProcessTerminateEvent`: This event is triggered after a process has - been terminated. It provides access to the `Process` instance and the current `Command`. + been terminated and launched inside the `run` function. It provides access to + the `Process` instance and the current `Command`. ## Console events 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/src/Console/Application.php b/src/Console/Application.php index aa68fc3e..45171af4 100644 --- a/src/Console/Application.php +++ b/src/Console/Application.php @@ -90,7 +90,6 @@ public function __construct( ]); $this->setCatchErrors(true); - $this->setDispatcher($eventDispatcher); AbstractCloner::$defaultCasters[self::class] = ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals']; AbstractCloner::$defaultCasters[AfterApplicationInitializationEvent::class] = ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals']; 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)) { From 27616561574e2e71c2480b184e9ae7d91d70bb5d Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 11:40:53 +0100 Subject: [PATCH 6/7] feat(event): update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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 From dfb3dc911bc32ddc1ec72b378d866d29522cf644 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 20 Feb 2024 11:42:49 +0100 Subject: [PATCH 7/7] feat(event): fix doc with command removal --- doc/going-further/extending-castor/events.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/going-further/extending-castor/events.md b/doc/going-further/extending-castor/events.md index aa0ae5a9..65679837 100644 --- a/doc/going-further/extending-castor/events.md +++ b/doc/going-further/extending-castor/events.md @@ -42,11 +42,11 @@ Here is the built-in events triggered by Castor: * `Castor\Event\ProcessStartEvent`: This event is triggered after a process has been started by the `run` function. It provides access to the `Process` - instance and the current `Command`. + 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 and the current `Command`. + the `Process` instance. ## Console events