Skip to content

Commit bf09946

Browse files
authored
Merge pull request #212 from php-school/feature/application-teardown
Add app tear down process for error states
2 parents 4e0f419 + d6ae518 commit bf09946

7 files changed

+262
-40
lines changed

app/config.php

+5-14
Original file line numberDiff line numberDiff line change
@@ -384,28 +384,19 @@ function (CgiResult $result) use ($c) {
384384
],
385385
],
386386
'code-patcher' => [
387-
'cli.verify.start' => [
388-
containerListener(CodePatchListener::class, 'patch'),
389-
],
390-
'cli.verify.finish' => [
391-
containerListener(CodePatchListener::class, 'revert'),
392-
],
393-
'cli.run.start' => [
387+
'verify.pre.execute' => [
394388
containerListener(CodePatchListener::class, 'patch'),
395389
],
396-
'cli.run.finish' => [
390+
'verify.post.execute' => [
397391
containerListener(CodePatchListener::class, 'revert'),
398392
],
399-
'cgi.verify.start' => [
393+
'run.start' => [
400394
containerListener(CodePatchListener::class, 'patch'),
401395
],
402-
'cgi.verify.finish' => [
396+
'run.finish' => [
403397
containerListener(CodePatchListener::class, 'revert'),
404398
],
405-
'cgi.run.start' => [
406-
containerListener(CodePatchListener::class, 'patch'),
407-
],
408-
'cgi.run.finish' => [
399+
'application.tear-down' => [
409400
containerListener(CodePatchListener::class, 'revert'),
410401
],
411402
],

src/Application.php

+31
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
use DI\Container;
88
use DI\ContainerBuilder;
99
use PhpSchool\PhpWorkshop\Check\CheckRepository;
10+
use PhpSchool\PhpWorkshop\Event\Event;
11+
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
1012
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
1113
use PhpSchool\PhpWorkshop\Exception\MissingArgumentException;
1214
use PhpSchool\PhpWorkshop\Factory\ResultRendererFactory;
1315
use PhpSchool\PhpWorkshop\Output\OutputInterface;
1416
use Psr\Container\ContainerInterface;
17+
use Psr\Log\LoggerInterface;
1518
use RuntimeException;
1619

1720
use function class_exists;
@@ -69,6 +72,11 @@ final class Application
6972
*/
7073
private $frameworkConfigLocation = __DIR__ . '/../app/config.php';
7174

75+
/**
76+
* @var ?ContainerInterface
77+
*/
78+
private $container;
79+
7280
/**
7381
* It should be instantiated with the title of
7482
* the workshop and the path to the DI configuration file.
@@ -163,6 +171,10 @@ public function setBgColour(string $colour): void
163171

164172
public function configure(): ContainerInterface
165173
{
174+
if ($this->container instanceof ContainerInterface) {
175+
return $this->container;
176+
}
177+
166178
$container = $this->getContainer();
167179

168180
foreach ($this->exercises as $exercise) {
@@ -192,6 +204,12 @@ public function configure(): ContainerInterface
192204
}
193205
}
194206

207+
set_error_handler(function () use ($container): bool {
208+
$this->tearDown($container);
209+
return false; // Use default error handler
210+
});
211+
212+
$this->container = $container;
195213
return $container;
196214
}
197215

@@ -227,6 +245,8 @@ public function run(): int
227245
$message = str_replace($basePath, '', $message);
228246
}
229247

248+
$this->tearDown($container);
249+
230250
$container
231251
->get(OutputInterface::class)
232252
->printError(
@@ -268,4 +288,15 @@ private function getContainer(): Container
268288

269289
return $containerBuilder->build();
270290
}
291+
292+
private function tearDown(ContainerInterface $container): void
293+
{
294+
try {
295+
$container
296+
->get(EventDispatcher::class)
297+
->dispatch(new Event('application.tear-down'));
298+
} catch (\Throwable $t) {
299+
$container->get(LoggerInterface::class)->error($t->getMessage(), ['exception' => $t]);
300+
}
301+
}
271302
}

src/CommandRouter.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public function __construct(
7272
/**
7373
* @param CommandDefinition $c
7474
*/
75-
private function addCommand(CommandDefinition $c): void
75+
public function addCommand(CommandDefinition $c): void
7676
{
7777
if (isset($this->commands[$c->getName()])) {
7878
throw new InvalidArgumentException(sprintf('Command with name: "%s" already exists', $c->getName()));

src/Output/NullOutput.php

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PhpWorkshop\Output;
6+
7+
class NullOutput implements OutputInterface
8+
{
9+
public function printError(string $error): void
10+
{
11+
// noop
12+
}
13+
14+
public function write(string $content): void
15+
{
16+
// noop
17+
}
18+
19+
public function writeLines(array $lines): void
20+
{
21+
// noop
22+
}
23+
24+
public function writeLine(string $line): void
25+
{
26+
// noop
27+
}
28+
29+
public function emptyLine(): void
30+
{
31+
// noop
32+
}
33+
34+
public function lineBreak(): void
35+
{
36+
// noop
37+
}
38+
39+
public function writeTitle(string $title): void
40+
{
41+
// noop
42+
}
43+
}

test/ApplicationTest.php

+106-25
Original file line numberDiff line numberDiff line change
@@ -5,35 +5,44 @@
55
namespace PhpSchool\PhpWorkshopTest;
66

77
use PhpSchool\PhpWorkshop\Application;
8-
use PHPUnit\Framework\TestCase;
8+
use PhpSchool\PhpWorkshop\CommandDefinition;
9+
use PhpSchool\PhpWorkshop\CommandRouter;
10+
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
911
use PhpSchool\PhpWorkshop\Exception\InvalidArgumentException;
10-
11-
class ApplicationTest extends TestCase
12+
use PhpSchool\PhpWorkshop\Exception\RuntimeException;
13+
use PhpSchool\PhpWorkshop\Output\NullOutput;
14+
use PhpSchool\PhpWorkshop\Output\OutputInterface;
15+
use PhpSchool\PhpWorkshopTest\Asset\MockEventDispatcher;
16+
use Psr\Log\LoggerInterface;
17+
use Psr\Log\NullLogger;
18+
19+
class ApplicationTest extends BaseTest
1220
{
1321
public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void
1422
{
23+
$frameworkFileContent = <<<'FRAME'
24+
<?php return [
25+
'eventListeners' => [
26+
'event1' => [
27+
'entry1',
28+
'entry2',
29+
]
30+
]
31+
];
32+
FRAME;
33+
34+
$localFileContent = <<<'LOCAL'
35+
<?php return [
36+
'eventListeners' => [
37+
'event1' => [
38+
'entry3',
39+
]
40+
]
41+
];
42+
LOCAL;
1543

16-
$frameworkFileContent = '<?php return [';
17-
$frameworkFileContent .= " 'eventListeners' => [";
18-
$frameworkFileContent .= " 'event1' => [";
19-
$frameworkFileContent .= " 'entry1',";
20-
$frameworkFileContent .= " 'entry2',";
21-
$frameworkFileContent .= ' ]';
22-
$frameworkFileContent .= ' ]';
23-
$frameworkFileContent .= '];';
24-
25-
$localFileContent = '<?php return [';
26-
$localFileContent .= " 'eventListeners' => [";
27-
$localFileContent .= " 'event1' => [";
28-
$localFileContent .= " 'entry3',";
29-
$localFileContent .= ' ]';
30-
$localFileContent .= ' ]';
31-
$localFileContent .= '];';
32-
33-
$localFile = sprintf('%s/%s', sys_get_temp_dir(), uniqid($this->getName(), true));
34-
$frameworkFile = sprintf('%s/%s', sys_get_temp_dir(), uniqid($this->getName(), true));
35-
file_put_contents($frameworkFile, $frameworkFileContent);
36-
file_put_contents($localFile, $localFileContent);
44+
$localFile = $this->getTemporaryFile(uniqid($this->getName(), true), $localFileContent);
45+
$frameworkFile = $this->getTemporaryFile(uniqid($this->getName(), true), $frameworkFileContent);
3746

3847
$app = new Application('Test App', $localFile);
3948

@@ -48,7 +57,7 @@ public function testEventListenersFromLocalAndWorkshopConfigAreMerged(): void
4857

4958
$eventListeners = $container->get('eventListeners');
5059

51-
$this->assertEquals(
60+
self::assertEquals(
5261
[
5362
'event1' => [
5463
'entry1',
@@ -85,4 +94,76 @@ public function testExceptionIsThrownIfResultRendererClassDoesNotExist(): void
8594
$app = new Application('My workshop', __DIR__ . '/../app/config.php');
8695
$app->addResult(\PhpSchool\PhpWorkshop\Result\Success::class, \NotExistingClass::class);
8796
}
97+
98+
public function testTearDownEventIsFiredOnApplicationException(): void
99+
{
100+
$configFile = $this->getTemporaryFile('config.php', '<?php return [];');
101+
$application = new Application('Testing TearDown', $configFile);
102+
103+
$container = $application->configure();
104+
$container->set('basePath', __DIR__);
105+
$container->set(EventDispatcher::class, new MockEventDispatcher());
106+
$container->set(OutputInterface::class, new NullOutput());
107+
108+
/** @var MockEventDispatcher $eventDispatcher */
109+
$eventDispatcher = $container->get(EventDispatcher::class);
110+
111+
$commandRouter = $container->get(CommandRouter::class);
112+
$commandRouter->addCommand(new CommandDefinition('Failure', [], function () {
113+
throw new RuntimeException('We failed somewhere...');
114+
}));
115+
116+
$_SERVER['argv'] = [$this->getName(), 'Failure'];
117+
118+
$application->run();
119+
120+
self::assertSame(1, $eventDispatcher->getEventDispatchCount('application.tear-down'));
121+
}
122+
123+
public function testLoggingExceptionDuringTearDown(): void
124+
{
125+
$configFile = $this->getTemporaryFile('config.php', '<?php return [];');
126+
$application = new Application('Testing tear down logging', $configFile);
127+
$exception = new \Exception('Unexpected error');
128+
129+
$container = $application->configure();
130+
$container->set('basePath', __DIR__);
131+
$container->set(OutputInterface::class, new NullOutput());
132+
$container->set(LoggerInterface::class, new MockLogger());
133+
$container->set('eventListeners', [
134+
'testing-failure-logging' => [
135+
'application.tear-down' => [
136+
static function () use ($exception) {
137+
throw $exception;
138+
},
139+
]
140+
]
141+
]);
142+
143+
$commandRouter = $container->get(CommandRouter::class);
144+
$commandRouter->addCommand(new CommandDefinition('Failure', [], function () {
145+
throw new RuntimeException('We failed somewhere...');
146+
}));
147+
148+
$application->run();
149+
150+
/** @var MockLogger $logger */
151+
$logger = $container->get(LoggerInterface::class);
152+
self::assertCount(1, $logger->messages);
153+
self::assertSame('Unexpected error', $logger->messages[0]['message']);
154+
self::assertSame($exception, $logger->messages[0]['context']['exception']);
155+
}
156+
157+
public function testConfigureReturnsSameContainerInstance(): void
158+
{
159+
$configFile = $this->getTemporaryFile('config.php', '<?php return [];');
160+
$application = new Application('Testing Configure', $configFile);
161+
162+
self::assertSame($application->configure(), $application->configure());
163+
}
164+
165+
public function tearDown(): void
166+
{
167+
parent::tearDown();
168+
}
88169
}

test/Asset/MockEventDispatcher.php

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpSchool\PhpWorkshopTest\Asset;
6+
7+
use PhpSchool\PhpWorkshop\Event\EventDispatcher;
8+
use PhpSchool\PhpWorkshop\Event\EventInterface;
9+
10+
class MockEventDispatcher extends EventDispatcher
11+
{
12+
private $dispatches = [];
13+
private $listeners = [];
14+
15+
public function __construct()
16+
{
17+
// noop
18+
}
19+
20+
public function dispatch(EventInterface $event): EventInterface
21+
{
22+
isset($this->dispatches[$event->getName()])
23+
? $this->dispatches[$event->getName()]++
24+
: $this->dispatches[$event->getName()] = 1;
25+
26+
return $event;
27+
}
28+
29+
public function listen($eventNames, callable $callback): void
30+
{
31+
if (!is_array($eventNames)) {
32+
$eventNames = [$eventNames];
33+
}
34+
35+
foreach ($eventNames as $eventName) {
36+
isset($this->listeners[$eventName])
37+
? $this->listeners[$eventName][] = $callback
38+
: $this->listeners[$eventName] = [$callback];
39+
}
40+
}
41+
42+
public function getEventDispatchCount(string $eventName): int
43+
{
44+
return $this->dispatches[$eventName] ?? 0;
45+
}
46+
47+
public function getEventListeners(string $eventName): array
48+
{
49+
return $this->listeners[$eventName] ?? [];
50+
}
51+
}

0 commit comments

Comments
 (0)