Skip to content

Commit a05736d

Browse files
authored
Merge pull request #2 from WonderNetwork/feature/fingers-crossed-handler
`FindersCrossedHandler`
2 parents 2e4df3b + 241bb3b commit a05736d

File tree

4 files changed

+194
-0
lines changed

4 files changed

+194
-0
lines changed

Readme.md

+27
Original file line numberDiff line numberDiff line change
@@ -237,3 +237,30 @@ it throws if it encounters some unexpected input.
237237
* `allString()`, `allInt()`, `allBool()`
238238
* ensures all items of payload match a given type
239239
* returns an array of scalars of that type (`string[]`, `int[]`, `bool[]`)
240+
241+
## Fingers Crossed CLI handler
242+
243+
For all those pesky cron jobs, where on one hand you’d like to silence the output
244+
because it causes noisy emails for no reason, but at the same time you don’t want
245+
to lose context when something bad happens. Wrap your commands in a
246+
`FingersCrossedHandler` like so:
247+
248+
```php
249+
public function execute(InputInterface $input, OutputInterface $output): int {
250+
return FingersCrossedHandler::of($input, $output)->run(
251+
function (InputParams $input, FingersCrossedOutput $output) {
252+
$output->write("Hello world!")
253+
return 1;
254+
},
255+
);
256+
}
257+
```
258+
259+
The output will be silenced except for the following situatios:
260+
261+
* You increase the output verbosity using the `-v` flag or the `setVerbosity()` method
262+
* The command returns a non-zero exit code
263+
* The command throws
264+
265+
In the former case, the output will be written in realtime, and in the two
266+
latter ones you can expect it writtein in bulk at the end.

src/Cli/FingersCrossedHandler.php

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace WonderNetwork\SlimKernel\Cli;
5+
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputInterface;
8+
use Symfony\Component\Console\Output\OutputInterface;
9+
use Throwable;
10+
11+
final class FingersCrossedHandler {
12+
private InputInterface $input;
13+
private OutputInterface $output;
14+
15+
public static function of(InputInterface $input, OutputInterface $output): self {
16+
return new self($input, $output);
17+
}
18+
19+
private function __construct(InputInterface $input, OutputInterface $output) {
20+
$this->input = $input;
21+
$this->output = $output;
22+
}
23+
24+
/**
25+
* @param callable(InputParams $input, FingersCrossedOutput $output):int $closure
26+
* @return int
27+
* @throws Throwable
28+
*/
29+
public function run(callable $closure): int {
30+
$output = new FingersCrossedOutput($this->output);
31+
try {
32+
$result = $closure(InputParams::ofInput($this->input), $output);
33+
if (Command::SUCCESS !== $result) {
34+
$output->flush();
35+
}
36+
} catch (Throwable $e) {
37+
$output->flush();
38+
throw $e;
39+
}
40+
return $result;
41+
}
42+
}

src/Cli/FingersCrossedOutput.php

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace WonderNetwork\SlimKernel\Cli;
5+
6+
use Symfony\Component\Console\Output\OutputInterface;
7+
8+
final class FingersCrossedOutput {
9+
private OutputInterface $output;
10+
/** @var string[] */
11+
private array $messages = [];
12+
13+
public function __construct(OutputInterface $output) {
14+
$this->output = $output;
15+
}
16+
17+
public function writeln(string $message): void {
18+
if ($this->isBuffering()) {
19+
$this->messages[] = $message;
20+
return;
21+
}
22+
$this->flush();
23+
$this->output->writeln($message);
24+
}
25+
26+
public function flush(): void {
27+
$this->output->writeln(array_slice($this->messages, 0));
28+
}
29+
30+
public function isBuffering(): bool {
31+
return false === $this->output->isVerbose();
32+
}
33+
}
+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace WonderNetwork\SlimKernel\Cli;
5+
6+
use PHPUnit\Framework\TestCase;
7+
use RuntimeException;
8+
use Symfony\Component\Console\Command\Command;
9+
use Symfony\Component\Console\Input\ArrayInput;
10+
use Symfony\Component\Console\Output\BufferedOutput;
11+
use Symfony\Component\Console\Output\OutputInterface;
12+
use Throwable;
13+
14+
class FingersCrossedHandlerTest extends TestCase {
15+
private BufferedOutput $spy;
16+
private FingersCrossedHandler $sut;
17+
18+
protected function setUp(): void {
19+
$this->spy = new BufferedOutput();
20+
$this->sut = FingersCrossedHandler::of(new ArrayInput([]), $this->spy);
21+
}
22+
23+
public function testHidesNormalOutputOnSuccess(): void {
24+
$result = $this->sut->run(
25+
function (InputParams $input, FingersCrossedOutput $output) {
26+
$output->writeln("Hello world");
27+
return Command::SUCCESS;
28+
},
29+
);
30+
self::assertSame(0, $result);
31+
self::assertSame("", $this->spy->fetch());
32+
}
33+
34+
public function testDisplaysAllOutputOnFailure(): void {
35+
$result = $this->sut->run(
36+
function (InputParams $input, FingersCrossedOutput $output) {
37+
$output->writeln("Hello world");
38+
return Command::FAILURE;
39+
},
40+
);
41+
self::assertSame(1, $result);
42+
self::assertSame("Hello world\n", $this->spy->fetch());
43+
}
44+
45+
public function testCorrectlyFormatsAllOutput(): void {
46+
$this->sut->run(
47+
function (InputParams $input, FingersCrossedOutput $output) {
48+
$output->writeln("Alpha");
49+
$output->writeln("Bravo");
50+
return Command::FAILURE;
51+
},
52+
);
53+
self::assertSame("Alpha\nBravo\n", $this->spy->fetch());
54+
}
55+
56+
public function testDisplaysAllOutputWhenThrows(): void {
57+
$e = null;
58+
try {
59+
$this->sut->run(
60+
function (InputParams $input, FingersCrossedOutput $output) {
61+
$output->writeln("Hello world");
62+
throw new RuntimeException();
63+
},
64+
);
65+
} catch (Throwable $e) {}
66+
self::assertSame("Hello world\n", $this->spy->fetch());
67+
self::assertInstanceOf(Throwable::class, $e);
68+
}
69+
70+
public function testDisplaysAllOutputWhenVerbose(): void {
71+
$this->spy->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
72+
$this->sut->run(
73+
function (InputParams $input, FingersCrossedOutput $output) {
74+
$output->writeln("Hello world");
75+
return Command::SUCCESS;
76+
},
77+
);
78+
self::assertSame("Hello world\n", $this->spy->fetch());
79+
}
80+
81+
public function testOutputIsInOrderWhenVerbositiIsIncreased(): void {
82+
$this->sut->run(
83+
function (InputParams $input, FingersCrossedOutput $output) {
84+
$output->writeln("Alpha");
85+
$this->spy->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
86+
$output->writeln("Bravo");
87+
return Command::SUCCESS;
88+
},
89+
);
90+
self::assertSame("Alpha\nBravo\n", $this->spy->fetch());
91+
}
92+
}

0 commit comments

Comments
 (0)