diff --git a/CLAUDE.md b/CLAUDE.md index e3bcfea113..c52eef3036 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,7 +31,7 @@ Memory limit for PHPStan and Rector is `1G` (already set in composer scripts). ## Architecture -- `src/Config/ECSConfig.php` — Illuminate container subclass, low-level rule registration (`rule()`, `ruleWithConfiguration()`, `sets()`, `import()`). Auto-tags Sniff/FixerInterface/OutputFormatterInterface bindings. +- `src/Config/ECSConfig.php` — `Entropy\Container\Container` subclass, low-level rule registration (`rule()`, `ruleWithConfiguration()`, `sets()`, `import()`). Auto-tags Sniff/FixerInterface/OutputFormatterInterface bindings. - `src/Configuration/ECSConfigBuilder.php` — fluent user-facing API (`withRules`, `withSets`, `withPreparedSets`, `withPhpCsFixerSets`, `withSpacesLevel`, …). Returned by `ECSConfig::configure()`. `__invoke(ECSConfig)` flushes the builder state into the container. - `config/set/common/*.php` — prepared rule sets (spaces, arrays, namespaces, docblock, etc.); each returns a closure consumed by `ECSConfig::import()`. - `src/Config/Level/` — gradual-adoption levels (e.g. `SpacesLevel`). Each level class exposes `RULES` (ordered safest → most invasive) and optionally `RULE_CONFIGURATIONS`. @@ -50,10 +50,6 @@ Memory limit for PHPStan and Rector is `1G` (already set in composer scripts). - Don't introduce new comments unless they explain a non-obvious why; well-named identifiers should carry meaning. - Don't add backwards-compat shims, dead re-exports, or features that aren't required by the task. -## Patched dependency - -`illuminate/container` is patched via `patches/illuminate-container-container-php.patch` (cweagans/composer-patches). Don't update the package without re-checking the patch. - ## Don't - Don't bypass `phpstan`, `rector`, or `check-cs` with skip comments unless the user asks for it. diff --git a/composer.json b/composer.json index b65e9a9ce8..5b411cce9a 100644 --- a/composer.json +++ b/composer.json @@ -13,15 +13,19 @@ ], "require": { "php": ">=8.4", + "clue/ndjson-react": "^1.3", "composer/pcre": "^3.4", "composer/xdebug-handler": "^3.0.5", "entropy/entropy": "^0.4", + "fidry/cpu-core-counter": "^1.3", "friendsofphp/php-cs-fixer": "^3.95.5", "nette/utils": "^4.1", + "react/child-process": "^0.6.7", + "react/event-loop": "^1.6", + "react/socket": "^1.17", "sebastian/diff": "^9.0", "squizlabs/php_codesniffer": "^4.0.1", "symfony/finder": "^7.4", - "symplify/easy-parallel": "dev-main", "webmozart/assert": "^2.4" }, "require-dev": { diff --git a/phpstan.neon b/phpstan.neon index 917a25e542..d36605a346 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -44,7 +44,7 @@ parameters: # set above - path: src/Parallel/Application/ParallelFileProcessor.php - message: '#Cannot call method (.*?)\(\) on Symplify\\EasyParallel\\ValueObject\\ProcessPool\|null#' + message: '#Cannot call method (.*?)\(\) on Symplify\\EasyCodingStandard\\Parallel\\ValueObject\\ProcessPool\|null#' - '#Method Symplify\\EasyCodingStandard\\Application\\SingleFileProcessor\:\:processFilePath\(\) should return array\{file_diffs\?\: array, coding_standard_errors\?\: array\} but returns array<(.*?), array>#' diff --git a/src/Application/EasyCodingStandardApplication.php b/src/Application/EasyCodingStandardApplication.php index fe76185ac8..4e7cb021ec 100644 --- a/src/Application/EasyCodingStandardApplication.php +++ b/src/Application/EasyCodingStandardApplication.php @@ -13,6 +13,8 @@ use Symplify\EasyCodingStandard\FileSystem\StaticRelativeFilePathHelper; use Symplify\EasyCodingStandard\Finder\SourceFinder; use Symplify\EasyCodingStandard\Parallel\Application\ParallelFileProcessor; +use Symplify\EasyCodingStandard\Parallel\CpuCoreCountProvider; +use Symplify\EasyCodingStandard\Parallel\ScheduleFactory; use Symplify\EasyCodingStandard\Parallel\ValueObject\Bridge; use Symplify\EasyCodingStandard\SniffRunner\ValueObject\Error\CodingStandardError; use Symplify\EasyCodingStandard\Utils\ParametersMerger; @@ -20,8 +22,6 @@ use Symplify\EasyCodingStandard\ValueObject\Error\FileDiff; use Symplify\EasyCodingStandard\ValueObject\Error\SystemError; use Symplify\EasyCodingStandard\ValueObject\Option; -use Symplify\EasyParallel\CpuCoreCountProvider; -use Symplify\EasyParallel\ScheduleFactory; final readonly class EasyCodingStandardApplication { diff --git a/src/Console/Command/WorkerCommand.php b/src/Console/Command/WorkerCommand.php index cf31153844..efda2c86f7 100644 --- a/src/Console/Command/WorkerCommand.php +++ b/src/Console/Command/WorkerCommand.php @@ -15,9 +15,9 @@ use Symplify\EasyCodingStandard\Console\ExitCode; use Symplify\EasyCodingStandard\Console\Output\ConsoleOutputFormatter; use Symplify\EasyCodingStandard\MemoryLimitter; +use Symplify\EasyCodingStandard\Parallel\Enum\Action; +use Symplify\EasyCodingStandard\Parallel\Enum\ReactCommand; use Symplify\EasyCodingStandard\Parallel\WorkerRunner; -use Symplify\EasyParallel\Enum\Action; -use Symplify\EasyParallel\Enum\ReactCommand; /** * Inspired at: https://github.com/phpstan/phpstan-src/commit/9124c66dcc55a222e21b1717ba5f60771f7dda92 diff --git a/src/Parallel/Application/ParallelFileProcessor.php b/src/Parallel/Application/ParallelFileProcessor.php index 1b4660fc51..627fd104d3 100644 --- a/src/Parallel/Application/ParallelFileProcessor.php +++ b/src/Parallel/Application/ParallelFileProcessor.php @@ -12,20 +12,20 @@ use React\Socket\TcpServer; use Symplify\EasyCodingStandard\Console\ExitCode; use Symplify\EasyCodingStandard\DependencyInjection\SimpleParameterProvider; +use Symplify\EasyCodingStandard\Parallel\CommandLine\WorkerCommandLineFactory; +use Symplify\EasyCodingStandard\Parallel\Enum\Action; +use Symplify\EasyCodingStandard\Parallel\Enum\Content; +use Symplify\EasyCodingStandard\Parallel\Enum\ReactCommand; +use Symplify\EasyCodingStandard\Parallel\Enum\ReactEvent; use Symplify\EasyCodingStandard\Parallel\ValueObject\Bridge; +use Symplify\EasyCodingStandard\Parallel\ValueObject\ParallelProcess; +use Symplify\EasyCodingStandard\Parallel\ValueObject\ProcessPool; +use Symplify\EasyCodingStandard\Parallel\ValueObject\Schedule; use Symplify\EasyCodingStandard\SniffRunner\ValueObject\Error\CodingStandardError; use Symplify\EasyCodingStandard\ValueObject\Configuration; use Symplify\EasyCodingStandard\ValueObject\Error\FileDiff; use Symplify\EasyCodingStandard\ValueObject\Error\SystemError; use Symplify\EasyCodingStandard\ValueObject\Option; -use Symplify\EasyParallel\CommandLine\WorkerCommandLineFactory; -use Symplify\EasyParallel\Enum\Action; -use Symplify\EasyParallel\Enum\Content; -use Symplify\EasyParallel\Enum\ReactCommand; -use Symplify\EasyParallel\Enum\ReactEvent; -use Symplify\EasyParallel\ValueObject\ParallelProcess; -use Symplify\EasyParallel\ValueObject\ProcessPool; -use Symplify\EasyParallel\ValueObject\Schedule; use Throwable; /** diff --git a/src/Parallel/CommandLine/WorkerCommandLineFactory.php b/src/Parallel/CommandLine/WorkerCommandLineFactory.php new file mode 100644 index 0000000000..c16fb0c038 --- /dev/null +++ b/src/Parallel/CommandLine/WorkerCommandLineFactory.php @@ -0,0 +1,86 @@ + $workerOptionValues option name => value, mirrored to the worker process + * @param string[] $paths + */ + public function create( + string $baseScript, + string $workerCommandName, + ?string $projectConfigFile, + array $workerOptionValues, + array $paths, + string $identifier, + int $port + ): string { + $processCommandArray = [escapeshellarg(PHP_BINARY), escapeshellarg($baseScript), $workerCommandName]; + + if ($projectConfigFile !== null) { + $processCommandArray[] = '--config'; + $processCommandArray[] = escapeshellarg($projectConfigFile); + } + + foreach ($this->mirrorCommandOptions($workerOptionValues) as $processCommandOption) { + $processCommandArray[] = $processCommandOption; + } + + // for TCP local server + $processCommandArray[] = '--port'; + $processCommandArray[] = (string) $port; + + $processCommandArray[] = '--identifier'; + $processCommandArray[] = escapeshellarg($identifier); + + foreach ($paths as $path) { + $processCommandArray[] = escapeshellarg($path); + } + + // set json output + $processCommandArray[] = '--output-format'; + $processCommandArray[] = escapeshellarg('json'); + + return implode(' ', $processCommandArray); + } + + /** + * @param array $workerOptionValues + * @return string[] + */ + private function mirrorCommandOptions(array $workerOptionValues): array + { + $processCommandOptions = []; + + foreach ($workerOptionValues as $optionName => $optionValue) { + // skip clutter + if ($optionValue === null) { + continue; + } + + if (is_bool($optionValue)) { + if ($optionValue) { + $processCommandOptions[] = self::OPTION_DASHES . $optionName; + } + + continue; + } + + if ($optionName === 'memory-limit') { + // does not accept -1 as value without assign + $processCommandOptions[] = '--' . $optionName . '=' . $optionValue; + } else { + $processCommandOptions[] = self::OPTION_DASHES . $optionName; + $processCommandOptions[] = escapeshellarg($optionValue); + } + } + + return $processCommandOptions; + } +} diff --git a/src/Parallel/Contract/SerializableInterface.php b/src/Parallel/Contract/SerializableInterface.php new file mode 100644 index 0000000000..ca296de673 --- /dev/null +++ b/src/Parallel/Contract/SerializableInterface.php @@ -0,0 +1,15 @@ + $json + */ + public static function decode(array $json): self; +} diff --git a/src/Parallel/CpuCoreCountProvider.php b/src/Parallel/CpuCoreCountProvider.php new file mode 100644 index 0000000000..3b38997410 --- /dev/null +++ b/src/Parallel/CpuCoreCountProvider.php @@ -0,0 +1,23 @@ +getCount(); + } catch (NumberOfCpuCoreNotFound) { + return self::DEFAULT_CORE_COUNT; + } + } +} diff --git a/src/Parallel/Enum/Action.php b/src/Parallel/Enum/Action.php new file mode 100644 index 0000000000..a2bd1fa606 --- /dev/null +++ b/src/Parallel/Enum/Action.php @@ -0,0 +1,14 @@ + $files + */ + public function create(int $cpuCores, int $jobSize, int $maxNumberOfProcesses, array $files): Schedule + { + Assert::positiveInteger($jobSize); + + $jobs = array_chunk($files, $jobSize); + $numberOfProcesses = min(count($jobs), $cpuCores); + + $numberOfProcesses = min($maxNumberOfProcesses, $numberOfProcesses); + + return new Schedule($numberOfProcesses, $jobs); + } +} diff --git a/src/Parallel/ValueObject/ParallelProcess.php b/src/Parallel/ValueObject/ParallelProcess.php new file mode 100644 index 0000000000..2b71d87788 --- /dev/null +++ b/src/Parallel/ValueObject/ParallelProcess.php @@ -0,0 +1,155 @@ +stdErr = $tmp; + $this->process = new Process($this->command, null, null, [ + 2 => $this->stdErr, + // todo is it fine to not have 0 and 1 FD? + ]); + $this->process->start($this->loop); + + $this->onData = $onData; + $this->onError = $onError; + + $this->process->on(ReactEvent::EXIT, function ($exitCode) use ($onExit): void { + $stdErr = $this->stdErr; + if ($stdErr === null) { + throw new ParallelShouldNotHappenException(); + } + + $this->cancelTimer(); + + rewind($stdErr); + + /** @var string $streamContents */ + $streamContents = stream_get_contents($stdErr); + $onExit($exitCode, $streamContents); + + fclose($stdErr); + }); + } + + /** + * @param mixed[] $data + */ + public function request(array $data): void + { + $this->cancelTimer(); + $this->encoder->write($data); + $this->timer = $this->loop->addTimer($this->timetoutInSeconds, function (): void { + $onError = $this->onError; + + $errorMessage = sprintf('Child process timed out after %d seconds', $this->timetoutInSeconds); + $onError(new Exception($errorMessage)); + }); + } + + public function quit(): void + { + $this->cancelTimer(); + if (! $this->process->isRunning()) { + return; + } + + foreach ($this->process->pipes as $pipe) { + $pipe->close(); + } + + $this->encoder->end(); + } + + public function bindConnection(Decoder $decoder, Encoder $encoder): void + { + $decoder->on(ReactEvent::DATA, function (array $json): void { + $this->cancelTimer(); + if ($json[ReactCommand::ACTION] !== Action::RESULT) { + return; + } + + $onData = $this->onData; + $onData($json[Content::RESULT]); + }); + $this->encoder = $encoder; + + $decoder->on(ReactEvent::ERROR, function (Throwable $throwable): void { + $onError = $this->onError; + $onError($throwable); + }); + + $encoder->on(ReactEvent::ERROR, function (Throwable $throwable): void { + $onError = $this->onError; + $onError($throwable); + }); + } + + private function cancelTimer(): void + { + if (! $this->timer instanceof TimerInterface) { + return; + } + + $this->loop->cancelTimer($this->timer); + $this->timer = null; + } +} diff --git a/src/Parallel/ValueObject/ProcessPool.php b/src/Parallel/ValueObject/ProcessPool.php new file mode 100644 index 0000000000..54794b2ef9 --- /dev/null +++ b/src/Parallel/ValueObject/ProcessPool.php @@ -0,0 +1,67 @@ + + */ + private array $processes = []; + + public function __construct( + private readonly TcpServer $tcpServer + ) { + } + + public function getProcess(string $identifier): ParallelProcess + { + if (! \array_key_exists($identifier, $this->processes)) { + throw new ParallelShouldNotHappenException(\sprintf('Process "%s" not found.', $identifier)); + } + + return $this->processes[$identifier]; + } + + public function attachProcess(string $identifier, ParallelProcess $parallelProcess): void + { + $this->processes[$identifier] = $parallelProcess; + } + + public function tryQuitProcess(string $identifier): void + { + if (! \array_key_exists($identifier, $this->processes)) { + return; + } + + $this->quitProcess($identifier); + } + + public function quitProcess(string $identifier): void + { + $parallelProcess = $this->getProcess($identifier); + $parallelProcess->quit(); + + unset($this->processes[$identifier]); + if ($this->processes !== []) { + return; + } + + $this->tcpServer->close(); + } + + public function quitAll(): void + { + foreach (\array_keys($this->processes) as $identifier) { + $this->quitProcess($identifier); + } + } +} diff --git a/src/Parallel/ValueObject/Schedule.php b/src/Parallel/ValueObject/Schedule.php new file mode 100644 index 0000000000..2bf298fee9 --- /dev/null +++ b/src/Parallel/ValueObject/Schedule.php @@ -0,0 +1,34 @@ +> $jobs + */ + public function __construct( + private int $numberOfProcesses, + private array $jobs + ) { + } + + public function getNumberOfProcesses(): int + { + return $this->numberOfProcesses; + } + + /** + * @return array> + */ + public function getJobs(): array + { + return $this->jobs; + } +} diff --git a/src/Parallel/WorkerRunner.php b/src/Parallel/WorkerRunner.php index 4991517c4a..e4c71a20fe 100644 --- a/src/Parallel/WorkerRunner.php +++ b/src/Parallel/WorkerRunner.php @@ -7,14 +7,14 @@ use Clue\React\NDJson\Decoder; use Clue\React\NDJson\Encoder; use Symplify\EasyCodingStandard\Application\SingleFileProcessor; +use Symplify\EasyCodingStandard\Parallel\Enum\Action; +use Symplify\EasyCodingStandard\Parallel\Enum\Content; +use Symplify\EasyCodingStandard\Parallel\Enum\ReactCommand; +use Symplify\EasyCodingStandard\Parallel\Enum\ReactEvent; use Symplify\EasyCodingStandard\Parallel\ValueObject\Bridge; use Symplify\EasyCodingStandard\Utils\ParametersMerger; use Symplify\EasyCodingStandard\ValueObject\Configuration; use Symplify\EasyCodingStandard\ValueObject\Error\SystemError; -use Symplify\EasyParallel\Enum\Action; -use Symplify\EasyParallel\Enum\Content; -use Symplify\EasyParallel\Enum\ReactCommand; -use Symplify\EasyParallel\Enum\ReactEvent; use Throwable; final readonly class WorkerRunner diff --git a/src/SniffRunner/ValueObject/Error/CodingStandardError.php b/src/SniffRunner/ValueObject/Error/CodingStandardError.php index 82f356b959..c17aa4f1e0 100644 --- a/src/SniffRunner/ValueObject/Error/CodingStandardError.php +++ b/src/SniffRunner/ValueObject/Error/CodingStandardError.php @@ -4,8 +4,8 @@ namespace Symplify\EasyCodingStandard\SniffRunner\ValueObject\Error; +use Symplify\EasyCodingStandard\Parallel\Contract\SerializableInterface; use Symplify\EasyCodingStandard\Parallel\ValueObject\Name; -use Symplify\EasyParallel\Contract\SerializableInterface; final readonly class CodingStandardError implements SerializableInterface { diff --git a/src/ValueObject/Error/FileDiff.php b/src/ValueObject/Error/FileDiff.php index e962384e0e..dc72810fff 100644 --- a/src/ValueObject/Error/FileDiff.php +++ b/src/ValueObject/Error/FileDiff.php @@ -6,8 +6,8 @@ use PHP_CodeSniffer\Sniffs\Sniff; use PhpCsFixer\Fixer\FixerInterface; +use Symplify\EasyCodingStandard\Parallel\Contract\SerializableInterface; use Symplify\EasyCodingStandard\Parallel\ValueObject\Name; -use Symplify\EasyParallel\Contract\SerializableInterface; final class FileDiff implements SerializableInterface { diff --git a/src/ValueObject/Error/SystemError.php b/src/ValueObject/Error/SystemError.php index ac756810bd..122cec38d4 100644 --- a/src/ValueObject/Error/SystemError.php +++ b/src/ValueObject/Error/SystemError.php @@ -4,8 +4,8 @@ namespace Symplify\EasyCodingStandard\ValueObject\Error; +use Symplify\EasyCodingStandard\Parallel\Contract\SerializableInterface; use Symplify\EasyCodingStandard\Parallel\ValueObject\Name; -use Symplify\EasyParallel\Contract\SerializableInterface; final readonly class SystemError implements SerializableInterface {