Skip to content

Commit a937917

Browse files
authored
Merge pull request #23 from facile-it/fix-symfony-7
Symfony 7.3 compat
2 parents 2f4e269 + 225a9fc commit a937917

File tree

8 files changed

+238
-78
lines changed

8 files changed

+238
-78
lines changed

.github/workflows/static-analysis.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
- name: Setup PHP
3131
uses: shivammathur/setup-php@v2
3232
with:
33-
php-version: 7.4
33+
php-version: 8.1
3434
- name: Install dependencies
3535
uses: "ramsey/composer-install@v3"
3636
- run: ${{ matrix.script }}

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
77
## Unreleased
88
* ...
99

10+
## 1.2.4 [2025-05-30]
11+
* Fix support for Symfony 7.3 (#23)
12+
1013
## 1.2.3 [2024-11-19]
1114
* Add PHP 8.4 support (#19)
1215

phpunit.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
33
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
4-
bootstrap="vendor/autoload.php"
4+
bootstrap="tests/bootstrap.php"
55
executionOrder="depends,defects"
66
cacheResult="false"
77
colors="true"

psalm.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
<directory name="src" />
1111
<ignoreFiles>
1212
<directory name="vendor" />
13+
<file name="src/AbstractTerminableCommandAfterSymfony7_3.php" />
1314
</ignoreFiles>
1415
</projectFiles>
1516
</psalm>

src/AbstractTerminableCommand.php

Lines changed: 83 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -5,104 +5,117 @@
55
namespace Facile\TerminableLoop;
66

77
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Command\SignalableCommandInterface;
89
use Symfony\Component\Console\Input\InputInterface;
910
use Symfony\Component\Console\Output\OutputInterface;
1011

11-
abstract class AbstractTerminableCommand extends Command
12-
{
13-
private const REQUEST_TO_TERMINATE = 143;
12+
if (
13+
PHP_VERSION_ID >= 8_02_00
14+
&& interface_exists(SignalableCommandInterface::class)
15+
&& in_array(
16+
SignalableCommandInterface::class,
17+
class_implements(Command::class),
18+
true
19+
)
20+
) {
21+
require_once __DIR__ . '/AbstractTerminableCommandAfterSymfony7_3.php';
22+
} else {
23+
abstract class AbstractTerminableCommand extends Command
24+
{
25+
private const REQUEST_TO_TERMINATE = 143;
1426

15-
/** @var int */
16-
private $sleepDuration;
27+
/** @var int */
28+
private $sleepDuration;
1729

18-
/** @var bool */
19-
private $signalShutdownRequested;
30+
/** @var bool */
31+
private $signalShutdownRequested;
2032

21-
public function __construct(?string $name = null)
22-
{
23-
$this->sleepDuration = 0;
24-
$this->signalShutdownRequested = false;
33+
public function __construct(?string $name = null)
34+
{
35+
$this->sleepDuration = 0;
36+
$this->signalShutdownRequested = false;
2537

26-
parent::__construct($name);
27-
}
38+
parent::__construct($name);
39+
}
2840

29-
final protected function execute(InputInterface $input, OutputInterface $output): int
30-
{
31-
$this->trapSignals();
41+
final protected function execute(InputInterface $input, OutputInterface $output): int
42+
{
43+
$this->trapSignals();
3244

33-
$output->writeln('Starting ' . ($this->getName() ?? static::class), OutputInterface::VERBOSITY_VERBOSE);
45+
$output->writeln('Starting ' . ($this->getName() ?? static::class), OutputInterface::VERBOSITY_VERBOSE);
3446

35-
if ($this->signalShutdownRequested) {
36-
$output->writeln('Signal received, skipping execution', OutputInterface::VERBOSITY_NORMAL);
47+
if ($this->signalShutdownRequested) {
48+
$output->writeln('Signal received, skipping execution', OutputInterface::VERBOSITY_NORMAL);
3749

38-
return self::REQUEST_TO_TERMINATE;
39-
}
50+
return self::REQUEST_TO_TERMINATE;
51+
}
4052

41-
$exitCode = $this->commandBody($input, $output);
53+
$exitCode = $this->commandBody($input, $output);
4254

43-
$this->sleep($output);
55+
$this->sleep($output);
4456

45-
/** @psalm-suppress DocblockTypeContradiction */
46-
if ($this->signalShutdownRequested) {
47-
$output->writeln('Signal received, terminating with exit code ' . self::REQUEST_TO_TERMINATE, OutputInterface::VERBOSITY_NORMAL);
57+
/** @psalm-suppress DocblockTypeContradiction */
58+
if ($this->signalShutdownRequested) {
59+
$output->writeln('Signal received, terminating with exit code ' . self::REQUEST_TO_TERMINATE, OutputInterface::VERBOSITY_NORMAL);
4860

49-
return self::REQUEST_TO_TERMINATE;
61+
return self::REQUEST_TO_TERMINATE;
62+
}
63+
64+
return $exitCode;
5065
}
5166

52-
return $exitCode;
53-
}
67+
abstract protected function commandBody(InputInterface $input, OutputInterface $output): int;
68+
69+
public function handleSignal(int $signal): void
70+
{
71+
switch ($signal) {
72+
// Shutdown signals
73+
case SIGTERM:
74+
case SIGINT:
75+
$this->signalShutdownRequested = true;
76+
break;
77+
}
78+
}
5479

55-
abstract protected function commandBody(InputInterface $input, OutputInterface $output): int;
80+
private function trapSignals(): void
81+
{
82+
pcntl_async_signals(true);
5683

57-
public function handleSignal(int $signal): void
58-
{
59-
switch ($signal) {
60-
// Shutdown signals
61-
case SIGTERM:
62-
case SIGINT:
63-
$this->signalShutdownRequested = true;
64-
break;
84+
// Add the signal handler
85+
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
86+
pcntl_signal(SIGINT, [$this, 'handleSignal']);
6587
}
66-
}
6788

68-
private function trapSignals(): void
69-
{
70-
pcntl_async_signals(true);
71-
72-
// Add the signal handler
73-
pcntl_signal(SIGTERM, [$this, 'handleSignal']);
74-
pcntl_signal(SIGINT, [$this, 'handleSignal']);
75-
}
89+
protected function getSleepDuration(): int
90+
{
91+
return $this->sleepDuration;
92+
}
7693

77-
protected function getSleepDuration(): int
78-
{
79-
return $this->sleepDuration;
80-
}
94+
protected function setSleepDuration(int $sleepDuration): void
95+
{
96+
if ($sleepDuration < 0) {
97+
throw new \InvalidArgumentException('Invalid timeout provided to ' . __METHOD__);
98+
}
8199

82-
protected function setSleepDuration(int $sleepDuration): void
83-
{
84-
if ($sleepDuration < 0) {
85-
throw new \InvalidArgumentException('Invalid timeout provided to ' . __METHOD__);
100+
$this->sleepDuration = $sleepDuration;
86101
}
87102

88-
$this->sleepDuration = $sleepDuration;
89-
}
103+
private function sleep(OutputInterface $output): void
104+
{
105+
if (0 === $this->sleepDuration) {
106+
return;
107+
}
90108

91-
private function sleep(OutputInterface $output): void
92-
{
93-
if (0 === $this->sleepDuration) {
94-
return;
95-
}
109+
$sleepCountDown = $this->sleepDuration;
96110

97-
$sleepCountDown = $this->sleepDuration;
111+
while (! $this->signalShutdownRequested && --$sleepCountDown) {
112+
sleep(1);
113+
}
98114

99-
while (! $this->signalShutdownRequested && --$sleepCountDown) {
100-
sleep(1);
115+
$output->writeln(
116+
sprintf('Slept %d second(s)', $this->sleepDuration - $sleepCountDown),
117+
OutputInterface::VERBOSITY_DEBUG
118+
);
101119
}
102-
103-
$output->writeln(
104-
sprintf('Slept %d second(s)', $this->sleepDuration - $sleepCountDown),
105-
OutputInterface::VERBOSITY_DEBUG
106-
);
107120
}
108121
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Facile\TerminableLoop;
6+
7+
use Symfony\Component\Console\Command\Command;
8+
use Symfony\Component\Console\Command\SignalableCommandInterface;
9+
use Symfony\Component\Console\Input\InputInterface;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
12+
if (! class_exists(AbstractTerminableCommand::class)) {
13+
abstract class AbstractTerminableCommand extends Command implements SignalableCommandInterface
14+
{
15+
private const REQUEST_TO_TERMINATE = 143;
16+
17+
/** @var int */
18+
private $sleepDuration;
19+
20+
/** @var bool */
21+
private $signalShutdownRequested;
22+
23+
public function __construct(?string $name = null)
24+
{
25+
$this->sleepDuration = 0;
26+
$this->signalShutdownRequested = false;
27+
28+
parent::__construct($name);
29+
}
30+
31+
final protected function execute(InputInterface $input, OutputInterface $output): int
32+
{
33+
$output->writeln('Starting ' . ($this->getName() ?? static::class), OutputInterface::VERBOSITY_VERBOSE);
34+
35+
if ($this->signalShutdownRequested) {
36+
$output->writeln('Signal received, skipping execution', OutputInterface::VERBOSITY_NORMAL);
37+
38+
return self::REQUEST_TO_TERMINATE;
39+
}
40+
41+
$exitCode = $this->commandBody($input, $output);
42+
43+
$this->sleep($output);
44+
45+
/** @psalm-suppress DocblockTypeContradiction */
46+
if ($this->signalShutdownRequested) {
47+
$output->writeln('Signal received, terminating with exit code ' . self::REQUEST_TO_TERMINATE, OutputInterface::VERBOSITY_NORMAL);
48+
49+
return self::REQUEST_TO_TERMINATE;
50+
}
51+
52+
return $exitCode;
53+
}
54+
55+
abstract protected function commandBody(InputInterface $input, OutputInterface $output): int;
56+
57+
public function handleSignal(int $signal, int|false $previousExitCode = 0): false
58+
{
59+
switch ($signal) {
60+
// Shutdown signals
61+
case SIGTERM:
62+
case SIGINT:
63+
$this->signalShutdownRequested = true;
64+
}
65+
66+
return false;
67+
}
68+
69+
/**
70+
* @return list<int>
71+
*/
72+
public function getSubscribedSignals(): array
73+
{
74+
return [
75+
SIGTERM,
76+
SIGINT,
77+
];
78+
}
79+
80+
protected function getSleepDuration(): int
81+
{
82+
return $this->sleepDuration;
83+
}
84+
85+
protected function setSleepDuration(int $sleepDuration): void
86+
{
87+
if ($sleepDuration < 0) {
88+
throw new \InvalidArgumentException('Invalid timeout provided to ' . __METHOD__);
89+
}
90+
91+
$this->sleepDuration = $sleepDuration;
92+
}
93+
94+
private function sleep(OutputInterface $output): void
95+
{
96+
if (0 === $this->sleepDuration) {
97+
return;
98+
}
99+
100+
$sleepCountDown = $this->sleepDuration;
101+
102+
while (! $this->signalShutdownRequested && --$sleepCountDown) {
103+
sleep(1);
104+
}
105+
106+
$output->writeln(
107+
sprintf('Slept %d second(s)', $this->sleepDuration - $sleepCountDown),
108+
OutputInterface::VERBOSITY_DEBUG
109+
);
110+
}
111+
}
112+
}

tests/Unit/AbstractTerminableCommandTest.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Prophecy\Argument;
1010
use Prophecy\PhpUnit\ProphecyTrait;
1111
use Symfony\Bridge\PhpUnit\ClockMock;
12+
use Symfony\Component\Console\Command\SignalableCommandInterface;
1213
use Symfony\Component\Console\Input\ArrayInput;
1314
use Symfony\Component\Console\Input\InputInterface;
1415
use Symfony\Component\Console\Output\OutputInterface;
@@ -117,12 +118,7 @@ protected function commandBody(InputInterface $input, OutputInterface $output):
117118
*/
118119
public function testReceiveSignalBeforeCommandBody(int $signal): void
119120
{
120-
$stubCommand = new class ('dummy:command') extends AbstractTerminableCommand {
121-
protected function commandBody(InputInterface $input, OutputInterface $output): int
122-
{
123-
return 0;
124-
}
125-
};
121+
$stubCommand = $this->createStubTerminableCommand();
126122

127123
$output = $this->prophesize(OutputInterface::class);
128124
$output->writeln(Argument::containingString('Starting'), OutputInterface::VERBOSITY_VERBOSE)
@@ -138,6 +134,20 @@ protected function commandBody(InputInterface $input, OutputInterface $output):
138134
$this->assertSame(143, $exitCode);
139135
}
140136

137+
/**
138+
* @dataProvider signalProvider
139+
*/
140+
public function testGetSubscribedSignals(int $signal): void
141+
{
142+
$stubCommand = $this->createStubTerminableCommand();
143+
144+
if (! interface_exists(SignalableCommandInterface::class) || ! $stubCommand instanceof SignalableCommandInterface) {
145+
$this->markTestSkipped('This test requires the Symfony 7.3+ implementation');
146+
}
147+
148+
$this->assertContains($signal, $stubCommand->getSubscribedSignals(), 'Signal not subscribed to');
149+
}
150+
141151
/**
142152
* @return array{0: int}[]
143153
*/
@@ -148,4 +158,14 @@ public function signalProvider(): array
148158
[SIGTERM],
149159
];
150160
}
161+
162+
private function createStubTerminableCommand(): AbstractTerminableCommand
163+
{
164+
return new class ('dummy:command') extends AbstractTerminableCommand {
165+
protected function commandBody(InputInterface $input, OutputInterface $output): int
166+
{
167+
return 0;
168+
}
169+
};
170+
}
151171
}

0 commit comments

Comments
 (0)