From c173b5b38bb7c842ef79cf7c278958a652d6711b Mon Sep 17 00:00:00 2001
From: Davide Bellettini <325358+dbellettini@users.noreply.github.com>
Date: Sat, 2 Aug 2025 21:51:13 +0200
Subject: [PATCH 1/5] Add infrastructure
---
.github/workflows/phpunit.yml | 38 ++++++++++++++++++++++++++++++++
.gitignore | 2 +-
Dockerfile | 36 ++++++++++++++++++++++++++++++
Makefile | 41 +++++++++++++++++++++++++++++++++++
compose.yaml | 8 +++++++
5 files changed, 124 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/phpunit.yml
create mode 100644 Dockerfile
create mode 100644 Makefile
create mode 100644 compose.yaml
diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
new file mode 100644
index 0000000..fe1ae53
--- /dev/null
+++ b/.github/workflows/phpunit.yml
@@ -0,0 +1,38 @@
+name: PHPUnit Tests
+
+on:
+ push:
+ branches: [ "master" ]
+ pull_request:
+ branches: [ "master" ]
+
+permissions:
+ contents: read
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Validate composer.json and composer.lock
+ run: composer validate --strict
+
+ - name: Cache Composer packages
+ id: composer-cache
+ uses: actions/cache@v3
+ with:
+ path: vendor
+ key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-php-
+
+ - name: Build the Docker image
+ run: make build
+
+ - name: Install dependencies
+ run: make install
+
+ - name: Run test suite
+ run: make test
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index c76a9ff..3f58acd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,4 @@
-.php_cs.cache
+.*.cache
/.idea/
/vendor/
composer.lock
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..40d0ebe
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,36 @@
+FROM php:8.4-cli
+
+# Install system dependencies
+RUN apt-get update && apt-get install -y \
+ git \
+ unzip \
+ libssl-dev \
+ libcurl4-openssl-dev \
+ pkg-config \
+ openjdk-17-jre-headless \
+ && rm -rf /var/lib/apt/lists/*
+
+# Install MongoDB extension
+RUN pecl install mongodb \
+ && docker-php-ext-enable mongodb \
+ && docker-php-ext-install pcntl
+
+# Copy Composer from official image
+COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
+
+# Set working directory
+WORKDIR /app
+
+# Copy composer files
+COPY composer.json composer.lock* ./
+
+# Install dependencies including dev dependencies for testing
+RUN composer install --optimize-autoloader
+
+# Copy application code
+COPY . .
+
+# Set environment variable for Composer
+ENV COMPOSER_ALLOW_SUPERUSER=1
+
+CMD ["tail", "-f", "/dev/null"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..3155352
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,41 @@
+.PHONY: build up down test test-long fix-cs install update shell logs clean
+
+# Build the Docker image
+build:
+ docker compose build
+
+# Start the services
+up:
+ docker compose up -d
+
+# Stop the services
+down:
+ docker compose down
+
+# Install dependencies
+install:
+ docker compose run --rm php composer install
+
+# Update dependencies
+update:
+ docker compose run --rm php composer update
+
+# Run all tests
+test: up
+ docker compose exec php vendor/bin/phpunit
+
+fix-cs: up
+ docker compose exec php vendor/bin/php-cs-fixer fix -v
+
+# Open a shell in the PHP container
+shell:
+ docker compose exec php bash
+
+# View logs
+logs:
+ docker compose logs -f php
+
+# Clean up containers and volumes
+clean:
+ docker compose down -v
+ docker compose rm -f
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..5509e13
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,8 @@
+services:
+ php:
+ build: .
+ working_dir: /app
+ volumes:
+ - .:/app
+ environment:
+ - COMPOSER_ALLOW_SUPERUSER=1
From dda00bf0ca9221bdb86f6cda2ea3b3d50aea6927 Mon Sep 17 00:00:00 2001
From: Davide Bellettini <325358+dbellettini@users.noreply.github.com>
Date: Sat, 2 Aug 2025 22:00:47 +0200
Subject: [PATCH 2/5] Update PHP, dependencies, and fix tests
---
.php-cs-fixer.dist.php | 25 ++++++++++++++
Makefile | 5 ++-
composer.json | 32 ++++++++++++------
phpstan.neon | 11 +++---
phpunit.xml | 19 +++++++++++
rector.php | 16 +++++++++
src/Command/RobustCommandRunner.php | 41 ++++++++---------------
src/Timing/ConstantPause.php | 12 ++-----
src/Timing/ExponentialBackoffStrategy.php | 18 ++--------
9 files changed, 109 insertions(+), 70 deletions(-)
create mode 100644 .php-cs-fixer.dist.php
create mode 100644 phpunit.xml
create mode 100644 rector.php
diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php
new file mode 100644
index 0000000..b33e48c
--- /dev/null
+++ b/.php-cs-fixer.dist.php
@@ -0,0 +1,25 @@
+setRiskyAllowed(true)
+ ->setRules([
+ '@PSR12' => true,
+ '@Symfony' => true,
+ 'array_indentation' => true,
+ 'array_syntax' => ['syntax' => 'short'],
+ 'concat_space' => ['spacing' => 'one'],
+ 'declare_strict_types' => true,
+ 'string_implicit_backslashes' => true,
+ 'list_syntax' => ['syntax' => 'short'],
+ 'multiline_whitespace_before_semicolons' => ['strategy' => 'new_line_for_chained_calls'],
+ 'ordered_imports' => true,
+ 'phpdoc_to_comment' => false,
+ 'trailing_comma_in_multiline' => ['elements' => ['arrays', 'arguments', 'parameters']],
+ 'visibility_required' => ['elements' => ['property', 'method', 'const']],
+ ])
+ ->setFinder(
+ PhpCsFixer\Finder::create()
+ ->in(__DIR__ . '/src')
+ ->in(__DIR__ . '/tests')
+ )
+;
diff --git a/Makefile b/Makefile
index 3155352..30a5db2 100644
--- a/Makefile
+++ b/Makefile
@@ -1,4 +1,4 @@
-.PHONY: build up down test test-long fix-cs install update shell logs clean
+.PHONY: build up down test phpstan fix-cs install update shell logs clean
# Build the Docker image
build:
@@ -24,6 +24,9 @@ update:
test: up
docker compose exec php vendor/bin/phpunit
+phpstan: up
+ docker compose exec php vendor/bin/phpstan
+
fix-cs: up
docker compose exec php vendor/bin/php-cs-fixer fix -v
diff --git a/composer.json b/composer.json
index bf99c35..7a54298 100644
--- a/composer.json
+++ b/composer.json
@@ -1,31 +1,35 @@
{
"name": "recruiterphp/geezer",
"description": "PHP tools to build robust long-running processes",
+ "license": "MIT",
"type": "library",
- "license": "proprietary",
"authors": [
{
"name": "Easy Welfare Development Team",
"email": "dev@easywelfare.com"
+ },
+ {
+ "name": "Contributors",
+ "homepage": "https://github.com/recruiterphp/geezer/graphs/contributors"
}
],
"require": {
- "php": "^7.2",
+ "php": "^8.4",
"ext-pcntl": "*",
- "symfony/console": "~4.0",
- "recruiterphp/concurrency": "^3.0.0"
+ "recruiterphp/concurrency": "^4.0",
+ "symfony/console": "7.3"
},
"require-dev": {
- "phpunit/phpunit": "^7",
- "friendsofphp/php-cs-fixer": "^2.13",
- "phpstan/phpstan": "^0.10.5",
- "phpstan/phpstan-phpunit": "^0.10.0"
+ "ergebnis/composer-normalize": "^2.47",
+ "friendsofphp/php-cs-fixer": "^3.85",
+ "phpstan/phpstan": "^2.1",
+ "phpunit/phpunit": "^12.3",
+ "rector/rector": "^2.1"
},
"provide": {
- "easywelfare/geezer-implementation": "2.0.0"
+ "easywelfare/geezer-implementation": "self.version"
},
- "minimum-stability": "dev",
- "prefer-stable": true,
+ "minimum-stability": "stable",
"autoload": {
"psr-4": {
"Recruiter\\Geezer\\": "src",
@@ -36,5 +40,11 @@
"psr-4": {
"Recruiter\\Geezer\\": "tests"
}
+ },
+ "config": {
+ "allow-plugins": {
+ "ergebnis/composer-normalize": true,
+ "sort-packages": true
+ }
}
}
diff --git a/phpstan.neon b/phpstan.neon
index cc85d47..2091644 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,8 +1,5 @@
-includes:
- - ./vendor/phpstan/phpstan-phpunit/extension.neon
- - ./vendor/phpstan/phpstan-phpunit/rules.neon
-
parameters:
- level: 7
- excludes_analyse:
- - %currentWorkingDirectory%/vendor/
+ level: 1
+ paths:
+ - src
+ - tests
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..296ff04
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
diff --git a/rector.php b/rector.php
new file mode 100644
index 0000000..9400286
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,16 @@
+withPaths([
+ __DIR__ . '/src',
+ __DIR__ . '/tests',
+ ])
+ // uncomment to reach your current PHP version
+ // ->withPhpSets()
+ ->withTypeCoverageLevel(0)
+ ->withDeadCodeLevel(0)
+ ->withCodeQualityLevel(0);
diff --git a/src/Command/RobustCommandRunner.php b/src/Command/RobustCommandRunner.php
index f5f7376..c524b4d 100644
--- a/src/Command/RobustCommandRunner.php
+++ b/src/Command/RobustCommandRunner.php
@@ -12,49 +12,34 @@
class RobustCommandRunner extends Command
{
- private const CYCLES_BEFORE_GC = 100;
+ private const int CYCLES_BEFORE_GC = 100;
- private const LEADERSHIP_STATUS_ACQUIRED = 'acquired';
- private const LEADERSHIP_STATUS_LOST = 'lost';
+ private const string LEADERSHIP_STATUS_ACQUIRED = 'acquired';
+ private const string LEADERSHIP_STATUS_LOST = 'lost';
- private const STOP_SIGNALS = [
+ private const array STOP_SIGNALS = [
SIGINT,
SIGQUIT,
SIGTERM,
SIGHUP,
];
- /**
- * @var RobustCommand
- */
- private $wrapped;
+ private bool $askedToStop = false;
- /**
- * @var LoggerInterface
- */
- private $logger;
+ private int $garbageCollectorCounter = 0;
- /**
- * @var bool
- */
- private $askedToStop = false;
+ private ?string $leadershipStatus;
- private $garbageCollectorCounter = 0;
-
- private $leadershipStatus = null;
-
- public function __construct(RobustCommand $wrapped, LoggerInterface $logger)
+ public function __construct(private readonly RobustCommand $wrapped, private readonly LoggerInterface $logger)
{
parent::__construct($wrapped->name());
- $this->wrapped = $wrapped;
- $this->logger = $logger;
$this->setDescription($wrapped->description());
$this->setDefinition($wrapped->definition());
$this->leadershipStatus = null;
}
- protected function execute(InputInterface $input, OutputInterface $output)
+ protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->wrapped->init($input);
$this->registerSignalHandlers();
@@ -102,8 +87,10 @@ protected function execute(InputInterface $input, OutputInterface $output)
$leadershipStrategy->release();
if ($occuredException) {
- exit(2);
+ return self::FAILURE;
}
+
+ return self::SUCCESS;
}
private function leadershipWasAcquired()
@@ -134,7 +121,7 @@ private function leadershipStatusChanged($newLeadershipStatus, string $hook)
private function registerSignalHandlers(): void
{
foreach (self::STOP_SIGNALS as $signal) {
- pcntl_signal($signal, function () {
+ pcntl_signal($signal, function (): void {
$this->askedToStop = true;
});
}
@@ -152,7 +139,7 @@ private function askedToStop()
private function sleepIfNotAskedToStop(int $milliSeconds): bool
{
$microSeconds = $milliSeconds * 1000;
- for ($i = 0; $i < $microSeconds && !$this->askedToStop(); $i = $i + 50000) {
+ for ($i = 0; $i < $microSeconds && !$this->askedToStop(); $i += 50000) {
usleep(50000);
}
diff --git a/src/Timing/ConstantPause.php b/src/Timing/ConstantPause.php
index 8f47203..4ef588d 100644
--- a/src/Timing/ConstantPause.php
+++ b/src/Timing/ConstantPause.php
@@ -4,15 +4,9 @@
class ConstantPause implements WaitStrategy
{
- /**
- * @var int
- */
- private $attempt = 0;
+ private int $attempt = 0;
- /**
- * @var int
- */
- private $pause;
+ private int $pause;
/**
* ConstantPause constructor.
@@ -84,7 +78,7 @@ public function rewind(): void
* @see https://php.net/manual/en/iterator.next.php
* @since 5.0.0
*/
- public function next()
+ public function next(): void
{
++$this->attempt;
}
diff --git a/src/Timing/ExponentialBackoffStrategy.php b/src/Timing/ExponentialBackoffStrategy.php
index 1e3f2c5..998106f 100644
--- a/src/Timing/ExponentialBackoffStrategy.php
+++ b/src/Timing/ExponentialBackoffStrategy.php
@@ -7,17 +7,7 @@ class ExponentialBackoffStrategy implements WaitStrategy
/**
* @var int
*/
- private $attempt = 0;
-
- /**
- * @var int
- */
- private $from;
-
- /**
- * @var int
- */
- private $to;
+ private int $attempt = 0;
/**
* ExponentialBackoffStrategy constructor.
@@ -25,10 +15,8 @@ class ExponentialBackoffStrategy implements WaitStrategy
* @param int $from minimum milliseconds to wait (on second attempt)
* @param int $to maximum milliseconds to wait
*/
- public function __construct(int $from, int $to)
+ public function __construct(private readonly int $from, private readonly int $to)
{
- $this->from = $from;
- $this->to = $to;
}
/**
@@ -94,7 +82,7 @@ public function rewind(): void
* @see https://php.net/manual/en/iterator.next.php
* @since 5.0.0
*/
- public function next()
+ public function next(): void
{
++$this->attempt;
}
From c4ea1bb42def9bafccdf5d329c09f7d918544760 Mon Sep 17 00:00:00 2001
From: Davide Bellettini <325358+dbellettini@users.noreply.github.com>
Date: Sat, 2 Aug 2025 22:01:57 +0200
Subject: [PATCH 3/5] Fix Coding Standards
---
src/Command/LeadershipEventsHandler.php | 2 ++
src/Command/RobustCommand.php | 5 +++--
src/Command/RobustCommandRunner.php | 5 +++--
src/Leadership/Anarchy.php | 2 ++
src/Leadership/Dictatorship.php | 5 ++---
src/Leadership/LeadershipStrategy.php | 2 ++
src/Timing/ConstantPause.php | 7 ++-----
src/Timing/ExponentialBackoffStrategy.php | 12 +++---------
src/Timing/WaitStrategy.php | 8 +++-----
tests/bootstrap.php | 2 ++
tests/unit/Timing/ExponentialBackoffStrategyTest.php | 4 +++-
11 files changed, 27 insertions(+), 27 deletions(-)
diff --git a/src/Command/LeadershipEventsHandler.php b/src/Command/LeadershipEventsHandler.php
index fbcb4b9..8b65124 100644
--- a/src/Command/LeadershipEventsHandler.php
+++ b/src/Command/LeadershipEventsHandler.php
@@ -1,5 +1,7 @@
wrapped->execute();
- } catch (Throwable $e) {
+ } catch (\Throwable $e) {
$this->log('Thrown an Exception: ' . get_class($e), ['exception' => $e], LogLevel::ERROR);
$occuredException = $e;
diff --git a/src/Leadership/Anarchy.php b/src/Leadership/Anarchy.php
index aa5b5cc..b794cf5 100644
--- a/src/Leadership/Anarchy.php
+++ b/src/Leadership/Anarchy.php
@@ -1,5 +1,7 @@
attempt - 1) * $this->from,
- $this->to
+ $this->to,
));
}
@@ -40,9 +37,6 @@ public function current(): int
* Return the key of the current element.
*
* @see https://php.net/manual/en/iterator.key.php
- *
- * @return int
- *
* @since 5.0.0
*/
public function key(): int
diff --git a/src/Timing/WaitStrategy.php b/src/Timing/WaitStrategy.php
index 252669e..2b0db89 100644
--- a/src/Timing/WaitStrategy.php
+++ b/src/Timing/WaitStrategy.php
@@ -1,15 +1,13 @@
Date: Sat, 2 Aug 2025 22:06:16 +0200
Subject: [PATCH 4/5] Increase PHPStan to 10
---
phpstan.neon | 2 +-
src/Command/RobustCommandRunner.php | 17 +++++++++--------
src/Timing/WaitStrategy.php | 3 +++
.../Timing/ExponentialBackoffStrategyTest.php | 2 +-
4 files changed, 14 insertions(+), 10 deletions(-)
diff --git a/phpstan.neon b/phpstan.neon
index 2091644..130e9ca 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -1,5 +1,5 @@
parameters:
- level: 1
+ level: 10
paths:
- src
- tests
diff --git a/src/Command/RobustCommandRunner.php b/src/Command/RobustCommandRunner.php
index 3c912d7..f817252 100644
--- a/src/Command/RobustCommandRunner.php
+++ b/src/Command/RobustCommandRunner.php
@@ -94,17 +94,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int
return self::SUCCESS;
}
- private function leadershipWasAcquired()
+ private function leadershipWasAcquired(): void
{
$this->leadershipStatusChanged(self::LEADERSHIP_STATUS_ACQUIRED, 'leadershipAcquired');
}
- private function leadershipWasLost()
+ private function leadershipWasLost(): void
{
$this->leadershipStatusChanged(self::LEADERSHIP_STATUS_LOST, 'leadershipLost');
}
- private function leadershipStatusChanged($newLeadershipStatus, string $hook)
+ private function leadershipStatusChanged(?string $newLeadershipStatus, string $hook): void
{
if ($this->leadershipStatus === $newLeadershipStatus) {
return;
@@ -128,7 +128,7 @@ private function registerSignalHandlers(): void
}
}
- private function askedToStop()
+ private function askedToStop(): bool
{
if (!$this->askedToStop) {
pcntl_signal_dispatch();
@@ -153,15 +153,16 @@ private function sleepAccordingTo(WaitStrategy $waitStrategy): void
$waitStrategy->next();
}
- private function possiblyCollectGarbage(): int
+ private function possiblyCollectGarbage(): void
{
if (0 === (++$this->garbageCollectorCounter % self::CYCLES_BEFORE_GC)) {
- return gc_collect_cycles();
+ gc_collect_cycles();
}
-
- return 0;
}
+ /**
+ * @param array $extra
+ */
private function log(string $message, array $extra = [], string $level = LogLevel::DEBUG): void
{
$this->logger->log($level, "[{hostname}:{pid}] $message", array_merge([
diff --git a/src/Timing/WaitStrategy.php b/src/Timing/WaitStrategy.php
index 2b0db89..92bfd44 100644
--- a/src/Timing/WaitStrategy.php
+++ b/src/Timing/WaitStrategy.php
@@ -4,6 +4,9 @@
namespace Recruiter\Geezer\Timing;
+/**
+ * @extends \Iterator
+ */
interface WaitStrategy extends \Iterator
{
/**
diff --git a/tests/unit/Timing/ExponentialBackoffStrategyTest.php b/tests/unit/Timing/ExponentialBackoffStrategyTest.php
index 67f5ffa..5173975 100644
--- a/tests/unit/Timing/ExponentialBackoffStrategyTest.php
+++ b/tests/unit/Timing/ExponentialBackoffStrategyTest.php
@@ -9,7 +9,7 @@
class ExponentialBackoffStrategyTest extends TestCase
{
- public function testNextWorksCorrectly()
+ public function testNextWorksCorrectly(): void
{
$strategy = new ExponentialBackoffStrategy(100, 1000);
$this->assertEquals(0, $strategy->current());
From 5d948808b23b721214e7a28dca507342db59f0a7 Mon Sep 17 00:00:00 2001
From: Davide Bellettini <325358+dbellettini@users.noreply.github.com>
Date: Sat, 2 Aug 2025 22:07:33 +0200
Subject: [PATCH 5/5] Fix constraint in composer.json
---
composer.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/composer.json b/composer.json
index 7a54298..556a02a 100644
--- a/composer.json
+++ b/composer.json
@@ -17,7 +17,7 @@
"php": "^8.4",
"ext-pcntl": "*",
"recruiterphp/concurrency": "^4.0",
- "symfony/console": "7.3"
+ "symfony/console": "^7.3"
},
"require-dev": {
"ergebnis/composer-normalize": "^2.47",