diff --git a/app/config.php b/app/config.php index 7b64b923..adcfbb33 100644 --- a/app/config.php +++ b/app/config.php @@ -384,28 +384,19 @@ function (CgiResult $result) use ($c) { ], ], 'code-patcher' => [ - 'cli.verify.start' => [ - containerListener(CodePatchListener::class, 'patch'), - ], - 'cli.verify.finish' => [ - containerListener(CodePatchListener::class, 'revert'), - ], - 'cli.run.start' => [ + 'verify.pre.execute' => [ containerListener(CodePatchListener::class, 'patch'), ], - 'cli.run.finish' => [ + 'verify.post.execute' => [ containerListener(CodePatchListener::class, 'revert'), ], - 'cgi.verify.start' => [ + 'run.start' => [ containerListener(CodePatchListener::class, 'patch'), ], - 'cgi.verify.finish' => [ + 'run.finish' => [ containerListener(CodePatchListener::class, 'revert'), ], - 'cgi.run.start' => [ - containerListener(CodePatchListener::class, 'patch'), - ], - 'cgi.run.finish' => [ + 'application.tear-down' => [ containerListener(CodePatchListener::class, 'revert'), ], ], diff --git a/src/Application.php b/src/Application.php index 3aec2936..ee7978d3 100644 --- a/src/Application.php +++ b/src/Application.php @@ -7,11 +7,14 @@ use DI\Container; use DI\ContainerBuilder; use PhpSchool\PhpWorkshop\Check\CheckRepository; +use PhpSchool\PhpWorkshop\Event\Event; +use PhpSchool\PhpWorkshop\Event\EventDispatcher; use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException; use PhpSchool\PhpWorkshop\Exception\MissingArgumentException; use PhpSchool\PhpWorkshop\Factory\ResultRendererFactory; use PhpSchool\PhpWorkshop\Output\OutputInterface; use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; use RuntimeException; use function class_exists; @@ -69,6 +72,11 @@ final class Application */ private $frameworkConfigLocation = __DIR__ . '/../app/config.php'; + /** + * @var ?ContainerInterface + */ + private $container; + /** * It should be instantiated with the title of * the workshop and the path to the DI configuration file. @@ -163,6 +171,10 @@ public function setBgColour(string $colour): void public function configure(): ContainerInterface { + if ($this->container instanceof ContainerInterface) { + return $this->container; + } + $container = $this->getContainer(); foreach ($this->exercises as $exercise) { @@ -192,6 +204,12 @@ public function configure(): ContainerInterface } } + set_error_handler(function () use ($container): bool { + $this->tearDown($container); + return false; // Use default error handler + }); + + $this->container = $container; return $container; } @@ -227,6 +245,8 @@ public function run(): int $message = str_replace($basePath, '', $message); } + $this->tearDown($container); + $container ->get(OutputInterface::class) ->printError( @@ -268,4 +288,15 @@ private function getContainer(): Container return $containerBuilder->build(); } + + private function tearDown(ContainerInterface $container): void + { + try { + $container + ->get(EventDispatcher::class) + ->dispatch(new Event('application.tear-down')); + } catch (\Throwable $t) { + $container->get(LoggerInterface::class)->error($t->getMessage(), ['exception' => $t]); + } + } } diff --git a/src/CommandRouter.php b/src/CommandRouter.php index 168f2dec..98f8ec74 100644 --- a/src/CommandRouter.php +++ b/src/CommandRouter.php @@ -72,7 +72,7 @@ public function __construct( /** * @param CommandDefinition $c */ - private function addCommand(CommandDefinition $c): void + public function addCommand(CommandDefinition $c): void { if (isset($this->commands[$c->getName()])) { throw new InvalidArgumentException(sprintf('Command with name: "%s" already exists', $c->getName())); diff --git a/src/Output/NullOutput.php b/src/Output/NullOutput.php new file mode 100644 index 00000000..d49a7d11 --- /dev/null +++ b/src/Output/NullOutput.php @@ -0,0 +1,43 @@ + [ + 'event1' => [ + 'entry1', + 'entry2', + ] + ] + ]; +FRAME; + + $localFileContent = <<<'LOCAL' + [ + 'event1' => [ + 'entry3', + ] + ] + ]; +LOCAL; - $frameworkFileContent = ' ["; - $frameworkFileContent .= " 'event1' => ["; - $frameworkFileContent .= " 'entry1',"; - $frameworkFileContent .= " 'entry2',"; - $frameworkFileContent .= ' ]'; - $frameworkFileContent .= ' ]'; - $frameworkFileContent .= '];'; - - $localFileContent = ' ["; - $localFileContent .= " 'event1' => ["; - $localFileContent .= " 'entry3',"; - $localFileContent .= ' ]'; - $localFileContent .= ' ]'; - $localFileContent .= '];'; - - $localFile = sprintf('%s/%s', sys_get_temp_dir(), uniqid($this->getName(), true)); - $frameworkFile = sprintf('%s/%s', sys_get_temp_dir(), uniqid($this->getName(), true)); - file_put_contents($frameworkFile, $frameworkFileContent); - file_put_contents($localFile, $localFileContent); + $localFile = $this->getTemporaryFile(uniqid($this->getName(), true), $localFileContent); + $frameworkFile = $this->getTemporaryFile(uniqid($this->getName(), true), $frameworkFileContent); $app = new Application('Test App', $localFile); @@ -48,7 +57,7 @@ public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void $eventListeners = $container->get('eventListeners'); - $this->assertEquals( + self::assertEquals( [ 'event1' => [ 'entry1', @@ -85,4 +94,76 @@ public function testExceptionIsThrownIfResultRendererClassDoesNotExist(): void $app = new Application('My workshop', __DIR__ . '/../app/config.php'); $app->addResult(\PhpSchool\PhpWorkshop\Result\Success::class, \NotExistingClass::class); } + + public function testTearDownEventIsFiredOnApplicationException(): void + { + $configFile = $this->getTemporaryFile('config.php', 'configure(); + $container->set('basePath', __DIR__); + $container->set(EventDispatcher::class, new MockEventDispatcher()); + $container->set(OutputInterface::class, new NullOutput()); + + /** @var MockEventDispatcher $eventDispatcher */ + $eventDispatcher = $container->get(EventDispatcher::class); + + $commandRouter = $container->get(CommandRouter::class); + $commandRouter->addCommand(new CommandDefinition('Failure', [], function () { + throw new RuntimeException('We failed somewhere...'); + })); + + $_SERVER['argv'] = [$this->getName(), 'Failure']; + + $application->run(); + + self::assertSame(1, $eventDispatcher->getEventDispatchCount('application.tear-down')); + } + + public function testLoggingExceptionDuringTearDown(): void + { + $configFile = $this->getTemporaryFile('config.php', 'configure(); + $container->set('basePath', __DIR__); + $container->set(OutputInterface::class, new NullOutput()); + $container->set(LoggerInterface::class, new MockLogger()); + $container->set('eventListeners', [ + 'testing-failure-logging' => [ + 'application.tear-down' => [ + static function () use ($exception) { + throw $exception; + }, + ] + ] + ]); + + $commandRouter = $container->get(CommandRouter::class); + $commandRouter->addCommand(new CommandDefinition('Failure', [], function () { + throw new RuntimeException('We failed somewhere...'); + })); + + $application->run(); + + /** @var MockLogger $logger */ + $logger = $container->get(LoggerInterface::class); + self::assertCount(1, $logger->messages); + self::assertSame('Unexpected error', $logger->messages[0]['message']); + self::assertSame($exception, $logger->messages[0]['context']['exception']); + } + + public function testConfigureReturnsSameContainerInstance(): void + { + $configFile = $this->getTemporaryFile('config.php', 'configure(), $application->configure()); + } + + public function tearDown(): void + { + parent::tearDown(); + } } diff --git a/test/Asset/MockEventDispatcher.php b/test/Asset/MockEventDispatcher.php new file mode 100644 index 00000000..c3e09778 --- /dev/null +++ b/test/Asset/MockEventDispatcher.php @@ -0,0 +1,51 @@ +dispatches[$event->getName()]) + ? $this->dispatches[$event->getName()]++ + : $this->dispatches[$event->getName()] = 1; + + return $event; + } + + public function listen($eventNames, callable $callback): void + { + if (!is_array($eventNames)) { + $eventNames = [$eventNames]; + } + + foreach ($eventNames as $eventName) { + isset($this->listeners[$eventName]) + ? $this->listeners[$eventName][] = $callback + : $this->listeners[$eventName] = [$callback]; + } + } + + public function getEventDispatchCount(string $eventName): int + { + return $this->dispatches[$eventName] ?? 0; + } + + public function getEventListeners(string $eventName): array + { + return $this->listeners[$eventName] ?? []; + } +} diff --git a/test/CommandRouterTest.php b/test/CommandRouterTest.php index 2629c02c..a7f20964 100644 --- a/test/CommandRouterTest.php +++ b/test/CommandRouterTest.php @@ -26,6 +26,31 @@ public function testInvalidDefaultThrowsException(): void new CommandRouter([], 'cmd', $eventDispatcher, $c); } + public function testAddCommandAppendsToExistingCommands(): void + { + $c = $this->createMock(ContainerInterface::class); + $eventDispatcher = $this->createMock(EventDispatcher::class); + + $callCount = 0; + $routeCallable = function () use (&$callCount) { + $callCount++; + }; + + $router = new CommandRouter( + [new CommandDefinition('verify', [], $routeCallable)], + 'verify', + $eventDispatcher, + $c + ); + + $router->addCommand(new CommandDefinition('run', [], $routeCallable)); + + $router->route(['app', 'verify']); + $router->route(['app', 'run']); + + self::assertSame(2, $callCount); + } + public function testAddCommandThrowsExceptionIfCommandWithSameNameExists(): void { $this->expectException(InvalidArgumentException::class);