diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 0000000..f27de9c --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,5 @@ +exclude_paths: + - ".github/**" + - "example/**" + - "test/**" + diff --git a/composer.json b/composer.json index a9ea8aa..47521bb 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,19 @@ } }, + "scripts": { + "phpunit": "vendor/bin/phpunit --configuration phpunit.xml", + "phpstan": "vendor/bin/phpstan analyse --memory-limit=512M --level 6 src", + "phpcs": "vendor/bin/phpcs src --standard=phpcs.xml", + "phpmd": "vendor/bin/phpmd src/ text phpmd.xml", + "test": [ + "@phpunit", + "@phpstan", + "@phpcs", + "@phpmd" + ] + }, + "funding": [ { "type": "github", diff --git a/composer.lock b/composer.lock index f37aade..76cf1bf 100644 --- a/composer.lock +++ b/composer.lock @@ -2154,34 +2154,34 @@ }, { "name": "symfony/config", - "version": "v7.4.7", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5" + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/6c17162555bfb58957a55bb0e43e00035b6ae3d5", - "reference": "6c17162555bfb58957a55bb0e43e00035b6ae3d5", + "url": "https://api.github.com/repos/symfony/config/zipball/ce9cb0c0d281aaf188b802d4968e42bfb60701e9", + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/filesystem": "^7.1|^8.0", + "symfony/filesystem": "^5.4|^6.0|^7.0", "symfony/polyfill-ctype": "~1.8" }, "conflict": { - "symfony/finder": "<6.4", + "symfony/finder": "<5.4", "symfony/service-contracts": "<2.5" }, "require-dev": { - "symfony/event-dispatcher": "^6.4|^7.0|^8.0", - "symfony/finder": "^6.4|^7.0|^8.0", - "symfony/messenger": "^6.4|^7.0|^8.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", "symfony/service-contracts": "^2.5|^3", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -2209,7 +2209,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v7.4.7" + "source": "https://github.com/symfony/config/tree/v6.4.34" }, "funding": [ { @@ -2229,43 +2229,44 @@ "type": "tidelift" } ], - "time": "2026-03-06T10:41:14+00:00" + "time": "2026-02-24T17:34:50+00:00" }, { "name": "symfony/dependency-injection", - "version": "v7.4.7", + "version": "v6.4.35", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db" + "reference": "d95712d0e9446b9f244b64811ffb6af7b7434213" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", - "reference": "0f651e58f4917fb0e2cd261ccbfe3d71e6e0f5db", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/d95712d0e9446b9f244b64811ffb6af7b7434213", + "reference": "d95712d0e9446b9f244b64811ffb6af7b7434213", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "psr/container": "^1.1|^2.0", "symfony/deprecation-contracts": "^2.5|^3", - "symfony/service-contracts": "^3.6", - "symfony/var-exporter": "^6.4.20|^7.2.5|^8.0" + "symfony/service-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4.20|^7.2.5" }, "conflict": { "ext-psr": "<1.1|>=2", - "symfony/config": "<6.4", - "symfony/finder": "<6.4", - "symfony/yaml": "<6.4" + "symfony/config": "<6.1", + "symfony/finder": "<5.4", + "symfony/proxy-manager-bridge": "<6.3", + "symfony/yaml": "<5.4" }, "provide": { "psr/container-implementation": "1.1|2.0", "symfony/service-implementation": "1.1|2.0|3.0" }, "require-dev": { - "symfony/config": "^6.4|^7.0|^8.0", - "symfony/expression-language": "^6.4|^7.0|^8.0", - "symfony/yaml": "^6.4|^7.0|^8.0" + "symfony/config": "^6.1|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/yaml": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -2293,7 +2294,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v7.4.7" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.35" }, "funding": [ { @@ -2313,7 +2314,7 @@ "type": "tidelift" } ], - "time": "2026-03-03T07:48:48+00:00" + "time": "2026-02-26T12:16:01+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2384,25 +2385,25 @@ }, { "name": "symfony/filesystem", - "version": "v7.4.6", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3", + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.8" }, "require-dev": { - "symfony/process": "^6.4|^7.0|^8.0" + "symfony/process": "^5.4|^6.4|^7.0" }, "type": "library", "autoload": { @@ -2430,7 +2431,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + "source": "https://github.com/symfony/filesystem/tree/v6.4.34" }, "funding": [ { @@ -2450,7 +2451,7 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-02-24T17:51:06+00:00" }, { "name": "symfony/polyfill-ctype", @@ -2709,26 +2710,26 @@ }, { "name": "symfony/var-exporter", - "version": "v7.4.0", + "version": "v6.4.26", "source": { "type": "git", "url": "https://github.com/symfony/var-exporter.git", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f" + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-exporter/zipball/03a60f169c79a28513a78c967316fbc8bf17816f", - "reference": "03a60f169c79a28513a78c967316fbc8bf17816f", + "url": "https://api.github.com/repos/symfony/var-exporter/zipball/466fcac5fa2e871f83d31173f80e9c2684743bfc", + "reference": "466fcac5fa2e871f83d31173f80e9c2684743bfc", "shasum": "" }, "require": { - "php": ">=8.2", + "php": ">=8.1", "symfony/deprecation-contracts": "^2.5|^3" }, "require-dev": { - "symfony/property-access": "^6.4|^7.0|^8.0", - "symfony/serializer": "^6.4|^7.0|^8.0", - "symfony/var-dumper": "^6.4|^7.0|^8.0" + "symfony/property-access": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" }, "type": "library", "autoload": { @@ -2766,7 +2767,7 @@ "serialize" ], "support": { - "source": "https://github.com/symfony/var-exporter/tree/v7.4.0" + "source": "https://github.com/symfony/var-exporter/tree/v6.4.26" }, "funding": [ { @@ -2786,7 +2787,7 @@ "type": "tidelift" } ], - "time": "2025-09-11T10:15:23+00:00" + "time": "2025-09-11T09:57:09+00:00" }, { "name": "theseer/tokenizer", diff --git a/src/Pool.php b/src/Pool.php index 02ebab2..67870e2 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -4,13 +4,25 @@ class Pool { /** @var Process[] Associative array of name=>Process */ protected array $processList; + /** @var array */ + protected array $completeCallbackList; public function __construct() { $this->processList = []; + $this->completeCallbackList = []; } public function add(string $name, Process $process):void { $this->processList[$name] = $process; + $process->onComplete( + function(Process $completedProcess):void { + $this->dispatchCompletionCallback($completedProcess); + } + ); + } + + public function onComplete(callable $callback):void { + $this->completeCallbackList[] = $callback; } /** Starts the execution of all processes */ @@ -102,4 +114,10 @@ public function close():array { return $codes; } + + protected function dispatchCompletionCallback(Process $process):void { + foreach($this->completeCallbackList as $callback) { + $callback($process); + } + } } diff --git a/src/Process.php b/src/Process.php index 742f331..48fb0ca 100644 --- a/src/Process.php +++ b/src/Process.php @@ -18,12 +18,17 @@ class Process { protected bool $isBlocking = false; /** @var array */ protected array $env; + /** @var array */ + protected array $completeCallbackList; + protected bool $hasCompleted; public function __construct(string...$command) { $this->command = $command; $this->cwd = getcwd(); $this->pipes = []; $this->env = getenv(); + $this->completeCallbackList = []; + $this->hasCompleted = false; } public function __destruct() { @@ -38,6 +43,15 @@ public function setEnv(string $key, string $value):void { $this->env[$key] = $value; } + public function onComplete(callable $callback):void { + if($this->hasCompleted) { + $callback($this); + return; + } + + array_push($this->completeCallbackList, $callback); + } + /** * Runs the command in a concurrent thread. * Sets the input, output and errors streams. @@ -80,6 +94,7 @@ public function exec():void { } $this->refreshStatus(); + $this->dispatchCompletionCallback(); if($this->status["exitcode"] === 127) { throw new CommandNotFoundException($this->command[0]); @@ -90,6 +105,7 @@ public function exec():void { public function isRunning():bool { $this->refreshStatus(); + $this->dispatchCompletionCallback(); $running = $this->status["running"] ?? false; return (bool)$running; @@ -177,4 +193,19 @@ protected function refreshStatus():void { } } } + + protected function dispatchCompletionCallback():void { + if($this->hasCompleted || empty($this->status)) { + return; + } + + if($this->status["running"] ?? false) { + return; + } + + $this->hasCompleted = true; + foreach($this->completeCallbackList as $callback) { + $callback($this); + } + } } diff --git a/test/phpunit/PoolTest.php b/test/phpunit/PoolTest.php index 2e7bf3e..59e4dd1 100644 --- a/test/phpunit/PoolTest.php +++ b/test/phpunit/PoolTest.php @@ -214,4 +214,39 @@ public function testClose() { $sut->close(); } -} \ No newline at end of file + + public function testOnComplete():void { + $proc1Callback = null; + $proc2Callback = null; + + /** @var MockObject|Process $proc1 */ + $proc1 = self::createMock(Process::class); + $proc1->expects($this->once()) + ->method("onComplete") + ->willReturnCallback(function(callable $callback) use (&$proc1Callback):void { + $proc1Callback = $callback; + }); + + /** @var MockObject|Process $proc2 */ + $proc2 = self::createMock(Process::class); + $proc2->expects($this->once()) + ->method("onComplete") + ->willReturnCallback(function(callable $callback) use (&$proc2Callback):void { + $proc2Callback = $callback; + }); + + $completedProcesses = []; + + $sut = new Pool(); + $sut->onComplete(function(Process $process) use (&$completedProcesses):void { + $completedProcesses[] = $process; + }); + $sut->add("test1", $proc1); + $sut->add("test2", $proc2); + + $proc2Callback($proc2); + $proc1Callback($proc1); + + self::assertSame([$proc2, $proc1], $completedProcesses); + } +} diff --git a/test/phpunit/ProcessTest.php b/test/phpunit/ProcessTest.php index 09dee48..750555e 100644 --- a/test/phpunit/ProcessTest.php +++ b/test/phpunit/ProcessTest.php @@ -181,4 +181,23 @@ public function testSetEnv_multiple() { self::assertStringContainsString("TEST=setEnv\n", $output); self::assertStringContainsString("NAME=PHPUnit\n", $output); } + + public function testOnComplete():void { + $sut = new Process(PHP_BINARY, "-r", "usleep(10000);"); + $sut->setBlocking(); + + $callbackCount = 0; + $completedProcess = null; + $sut->onComplete(function(Process $process) use (&$callbackCount, &$completedProcess) { + $callbackCount++; + $completedProcess = $process; + }); + + $sut->exec(); + $sut->isRunning(); + $sut->getExitCode(); + + self::assertSame(1, $callbackCount); + self::assertSame($sut, $completedProcess); + } }