diff --git a/benchmarks/.gitignore b/benchmarks/.gitignore new file mode 100644 index 0000000..73a2c81 --- /dev/null +++ b/benchmarks/.gitignore @@ -0,0 +1,6 @@ +/vendor/ +/.phpbench/ +/.phpunit.cache/ +/results/local/ +/results/tmp/ +/profiles/ diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 0000000..2ed6838 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,99 @@ +# EvolvePHP 2 Benchmarks + +EvolvePHP performance work starts from measurements instead of guesses. This benchmark harness provides correctness checks, environment fingerprinting, local baseline output, and measured evidence for later optimization work. + +This harness is benchmark infrastructure only. It does not optimize production framework code and it does not make a fastest-framework claim. There is no current fastest-framework claim and no current top-three performance claim. + +## Install + +From the repository root: + +```powershell +composer update --working-dir=benchmarks --no-interaction +``` + +Benchmark-only dependencies are installed inside `benchmarks/vendor/` and locked in `benchmarks/composer.lock`. + +## Protocol + +The primary comparison lane is PHP 8.4 with OPcache enabled, JIT disabled, production-like framework configuration, and no debugging or profiling extension altering timings. PHP 8.5 results may be useful, but they must not be combined with PHP 8.4 results as if they came from the same environment. + +one-off stopwatch results are not performance evidence. Warmup is required. Multiple iterations are required. The environment fingerprint must match before two baselines are treated as comparable. Shared GitHub-hosted runners are useful for smoke validation, but they are not authoritative sources for absolute wall-clock regression budgets. Future blocking budgets must come from a controlled or proven low-noise environment. + +## Commands + +Validate the benchmark Composer root: + +```powershell +composer validate --working-dir=benchmarks --strict +``` + +Run correctness tests: + +```powershell +php benchmarks\vendor\bin\phpunit --configuration benchmarks\phpunit.xml.dist +``` + +Run syntax checks: + +```powershell +php benchmarks\bin\check-syntax.php +``` + +Capture the environment fingerprint: + +```powershell +php benchmarks\bin\capture-environment.php --output results/local/environment.json +``` + +Run the fast benchmark smoke: + +```powershell +php benchmarks\bin\benchmark-smoke.php +``` + +Run internal Core scenarios: + +```powershell +php benchmarks\vendor\bin\phpbench run --config=benchmarks\phpbench.json --group=core --report=aggregate +``` + +Run representative HTTP scenarios: + +```powershell +php benchmarks\vendor\bin\phpbench run --config=benchmarks\phpbench.json --group=http --report=aggregate +``` + +Run a local full benchmark pass and store XML: + +```powershell +php benchmarks\vendor\bin\phpbench run --config=benchmarks\phpbench.json --report=aggregate --dump-file=benchmarks\results\local\phpbench.xml +``` + +Normalize an XML result: + +```powershell +php benchmarks\bin\normalize-results.php --input benchmarks\results\local\phpbench.xml --output benchmarks\results\local\normalized-results.json +``` + +## Scenarios + +Internal scenarios cover application boot/component preparation, service resolution for application, execution and transient lifetimes, execution orchestration with no sink versus a no-op sink, reset participant overhead, and persistent-style sequential execution evidence. + +HTTP scenarios cover real `Route`, `RouteCollection`, `RouteMatcher`, `RoutingRequestHandler`, middleware dispatch, and full `HttpKernel` execution. Route tables cover 10, 100, and 1000 routes with first, middle, and last hit positions, plus miss and 405 paths. Middleware depths cover 0, 1, 5, 10, and 20 layers. + +The persistent-style run is repeated sequential execution evidence only. It is not FrankenPHP certification, RoadRunner certification, or any other runtime-adapter claim. + +## Results + +Local results are written under `benchmarks/results/local/` and are ignored. Treat local output as: + +```text +LOCAL / NON-CANONICAL BASELINE +``` + +A result qualifies as a reference baseline only when the documented PHP 8.4 protocol is run on the selected controlled environment with the exact source SHA, dependency lock state, and environment fingerprint recorded for comparison. + +Baseline comparison must reject casual comparisons when the environment fingerprint differs. The normalized result schema includes scenario identifiers, sample counts, timing statistics, percentiles when enough samples exist, relative standard deviation, throughput where derivable, memory fields, environment fingerprint, source SHA, and schema version. + +Cross-framework comparison, optimization work, and regression budgets are intentionally outside this initial harness. They should be introduced only after baseline measurements are reproducible and the relevant comparison methodology is defined. diff --git a/benchmarks/benchmarks/Core/ApplicationBootBench.php b/benchmarks/benchmarks/Core/ApplicationBootBench.php new file mode 100644 index 0000000..775a178 --- /dev/null +++ b/benchmarks/benchmarks/Core/ApplicationBootBench.php @@ -0,0 +1,34 @@ +boot(); + } + + /** + * @return array + */ + public function componentCounts(): array + { + return [ + 'minimal' => ['components' => 0], + 'small-graph' => ['components' => 5], + 'larger-graph' => ['components' => 50], + ]; + } +} diff --git a/benchmarks/benchmarks/Core/ContainerResolutionBench.php b/benchmarks/benchmarks/Core/ContainerResolutionBench.php new file mode 100644 index 0000000..fae9eb6 --- /dev/null +++ b/benchmarks/benchmarks/Core/ContainerResolutionBench.php @@ -0,0 +1,136 @@ +container->get($this->applicationServiceId); + } + + #[Bench\BeforeMethods(['setUpApplicationCachedResolution'])] + #[Bench\Groups(['core', 'container'])] + #[Bench\ParamProviders(['serviceCounts'])] + public function benchApplicationCachedResolution(array $params): void + { + $this->container->get($this->applicationServiceId); + } + + #[Bench\BeforeMethods(['setUpExecutionFirstResolution'])] + #[Bench\AfterMethods(['tearDownExecutionScope'])] + #[Bench\Revs(1)] + #[Bench\Warmup(0)] + #[Bench\Groups(['core', 'container'])] + #[Bench\ParamProviders(['serviceCounts'])] + public function benchExecutionFirstResolution(array $params): void + { + $this->executionScope->get($this->executionServiceId); + } + + #[Bench\BeforeMethods(['setUpExecutionCachedResolution'])] + #[Bench\AfterMethods(['tearDownExecutionScope'])] + #[Bench\Groups(['core', 'container'])] + #[Bench\ParamProviders(['serviceCounts'])] + public function benchExecutionCachedResolution(array $params): void + { + $this->executionScope->get($this->executionServiceId); + } + + #[Bench\BeforeMethods(['setUpTransientResolution'])] + #[Bench\Groups(['core', 'container'])] + #[Bench\ParamProviders(['serviceCounts'])] + public function benchTransientRepeatedResolution(array $params): void + { + $this->container->get($this->transientServiceId); + } + + public function setUpApplicationFirstResolution(array $params): void + { + $fixture = BenchmarkFixtureFactory::containerFixture($params['services']); + $this->container = $fixture['container']; + $this->applicationServiceId = 'bench.application.' . ($params['services'] - 1); + } + + public function setUpApplicationCachedResolution(array $params): void + { + $this->setUpApplicationFirstResolution($params); + $this->container->get($this->applicationServiceId); + } + + public function setUpExecutionFirstResolution(array $params): void + { + $fixture = BenchmarkFixtureFactory::containerFixture($params['services']); + $this->container = $fixture['container']; + $this->executionScope = $this->container->createExecutionScope(); + $this->executionServiceId = 'bench.execution.' . ($params['services'] - 1); + } + + public function setUpExecutionCachedResolution(array $params): void + { + $this->setUpExecutionFirstResolution($params); + $this->executionScope->get($this->executionServiceId); + } + + public function setUpTransientResolution(array $params): void + { + $fixture = BenchmarkFixtureFactory::containerFixture($params['services']); + $this->container = $fixture['container']; + $this->transientServiceId = 'bench.transient.' . ($params['services'] - 1); + } + + public function tearDownExecutionScope(): void + { + if ($this->executionScope !== null) { + $this->executionScope->close(); + $this->executionScope = null; + } + } + + public function resolvePreparedApplicationCachedService(): object + { + return $this->container->get($this->applicationServiceId); + } + + public function applicationServiceId(): string + { + return $this->applicationServiceId; + } + + /** + * @return array + */ + public function serviceCounts(): array + { + return [ + '10-services' => ['services' => 10], + '100-services' => ['services' => 100], + '1000-services' => ['services' => 1000], + ]; + } +} diff --git a/benchmarks/benchmarks/Core/ExecutionOrchestratorBench.php b/benchmarks/benchmarks/Core/ExecutionOrchestratorBench.php new file mode 100644 index 0000000..188ba2a --- /dev/null +++ b/benchmarks/benchmarks/Core/ExecutionOrchestratorBench.php @@ -0,0 +1,48 @@ +execute(ExecutionKind::HttpRequest, $fixture['operation']); + } + + #[Bench\Groups(['core', 'execution', 'instrumentation'])] + public function benchSuccessfulExecutionNoOpSinkZeroResetParticipants(): void + { + $fixture = BenchmarkFixtureFactory::executionOrchestratorFixture(resetParticipants: 0, withObservationSink: true); + $fixture['orchestrator']->execute(ExecutionKind::HttpRequest, $fixture['operation']); + } + + #[Bench\Groups(['core', 'execution', 'reset'])] + #[Bench\ParamProviders(['resetParticipantCounts'])] + public function benchSuccessfulExecutionWithResetParticipants(array $params): void + { + $fixture = BenchmarkFixtureFactory::executionOrchestratorFixture(resetParticipants: $params['reset_participants']); + $fixture['orchestrator']->execute(ExecutionKind::HttpRequest, $fixture['operation']); + } + + /** + * @return array + */ + public function resetParticipantCounts(): array + { + return [ + 'one-reset-participant' => ['reset_participants' => 1], + 'ten-reset-participants' => ['reset_participants' => 10], + ]; + } +} diff --git a/benchmarks/benchmarks/Core/PersistentSequentialExecutionBench.php b/benchmarks/benchmarks/Core/PersistentSequentialExecutionBench.php new file mode 100644 index 0000000..cb38e50 --- /dev/null +++ b/benchmarks/benchmarks/Core/PersistentSequentialExecutionBench.php @@ -0,0 +1,32 @@ + + */ + public function iterationCounts(): array + { + return [ + '1000-executions' => ['iterations' => 1000, 'checkpoint_every' => 250], + '10000-executions' => ['iterations' => 10000, 'checkpoint_every' => 2500], + ]; + } +} diff --git a/benchmarks/benchmarks/Http/HttpKernelBench.php b/benchmarks/benchmarks/Http/HttpKernelBench.php new file mode 100644 index 0000000..365e616 --- /dev/null +++ b/benchmarks/benchmarks/Http/HttpKernelBench.php @@ -0,0 +1,52 @@ +handle($fixture['request']); + } catch (Throwable $exception) { + if (!in_array($params['scenario'], ['not-found', 'method-mismatch'], true)) { + throw $exception; + } + } + } + + #[Bench\Groups(['http', 'kernel', 'warm'])] + public function benchRepeatedWarmStaticRequestsThroughSameKernel(): void + { + $fixture = BenchmarkFixtureFactory::httpKernelFixture('static'); + $fixture['kernel']->handle($fixture['request']); + $fixture['kernel']->handle($fixture['request']); + } + + /** + * @return array + */ + public function kernelScenarios(): array + { + return [ + 'successful-static-route' => ['scenario' => 'static'], + 'successful-parameterized-route' => ['scenario' => 'parameterized'], + 'middleware-heavy-successful-route' => ['scenario' => 'middleware-heavy'], + '404' => ['scenario' => 'not-found'], + '405' => ['scenario' => 'method-mismatch'], + ]; + } +} diff --git a/benchmarks/benchmarks/Http/MiddlewareBench.php b/benchmarks/benchmarks/Http/MiddlewareBench.php new file mode 100644 index 0000000..d9b8658 --- /dev/null +++ b/benchmarks/benchmarks/Http/MiddlewareBench.php @@ -0,0 +1,41 @@ +handle($fixture['request']); + } + + /** + * @return array + */ + public function middlewareScenarios(): array + { + $scenarios = []; + + foreach ([0, 1, 5, 10, 20] as $depth) { + $scenarios['pass-through-' . $depth] = ['depth' => $depth, 'mode' => 'pass-through']; + + if ($depth > 0) { + $scenarios['early-short-circuit-' . $depth] = ['depth' => $depth, 'mode' => 'early-short-circuit']; + $scenarios['middle-short-circuit-' . $depth] = ['depth' => $depth, 'mode' => 'middle-short-circuit']; + } + } + + return $scenarios; + } +} diff --git a/benchmarks/benchmarks/Http/RouteMatcherBench.php b/benchmarks/benchmarks/Http/RouteMatcherBench.php new file mode 100644 index 0000000..0d9c52b --- /dev/null +++ b/benchmarks/benchmarks/Http/RouteMatcherBench.php @@ -0,0 +1,105 @@ +matcher->match($this->request); + $match?->route()->path(); + } + + #[Bench\BeforeMethods(['setUpAllowedMethodsScenario'])] + #[Bench\Groups(['http', 'routing'])] + #[Bench\ParamProviders(['routeMissScenarios'])] + public function benchAllowedMethodsForMissAndMethodMismatch(array $params): void + { + $this->matcher->allowedMethods($this->allowedMethodsPath); + } + + public function setUpRouteScenario(array $params): void + { + $fixture = BenchmarkFixtureFactory::routeMatchingFixture( + $params['routes'], + $params['category'], + $params['position'], + ); + + $this->matcher = $fixture['matcher']; + $this->request = $fixture['request']; + } + + public function setUpAllowedMethodsScenario(array $params): void + { + $fixture = BenchmarkFixtureFactory::routeMatchingFixture($params['routes'], $params['category']); + $this->matcher = $fixture['matcher']; + $this->request = $fixture['request']; + $this->allowedMethodsPath = $this->request->getUri()->getPath(); + } + + public function matchPreparedRoute(): ?RouteMatch + { + return $this->matcher->match($this->request); + } + + /** + * @return array + */ + public function routeMatchScenarios(): array + { + $scenarios = []; + + foreach ([10, 100, 1000] as $routes) { + foreach (['first', 'middle', 'last'] as $position) { + $scenarios['static-' . $routes . '-' . $position] = [ + 'routes' => $routes, + 'category' => 'static', + 'position' => $position, + ]; + $scenarios['parameterized-' . $routes . '-' . $position] = [ + 'routes' => $routes, + 'category' => 'parameterized', + 'position' => $position, + ]; + } + } + + return $scenarios; + } + + /** + * @return array + */ + public function routeMissScenarios(): array + { + $scenarios = []; + + foreach ([10, 100, 1000] as $routes) { + $scenarios['miss-' . $routes] = ['routes' => $routes, 'category' => 'miss']; + $scenarios['method-mismatch-' . $routes] = ['routes' => $routes, 'category' => 'method-mismatch']; + } + + return $scenarios; + } +} diff --git a/benchmarks/bin/benchmark-smoke.php b/benchmarks/bin/benchmark-smoke.php new file mode 100644 index 0000000..4087f34 --- /dev/null +++ b/benchmarks/bin/benchmark-smoke.php @@ -0,0 +1,69 @@ + ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $benchmarkRoot); + +if (!is_resource($process)) { + fwrite(STDERR, 'Unable to start PHPBench smoke run.' . PHP_EOL); + exit(1); +} + +$stdout = stream_get_contents($pipes[1]); +$stderr = stream_get_contents($pipes[2]); +fclose($pipes[1]); +fclose($pipes[2]); +$exitCode = proc_close($process); + +if ($exitCode !== 0) { + fwrite(STDERR, trim((string) $stdout . PHP_EOL . (string) $stderr) . PHP_EOL); + exit($exitCode); +} + +$normalized = ResultNormalizer::fromPhpBenchXml($xmlPath, $environment); + +if (($normalized['scenarios'] ?? []) === []) { + fwrite(STDERR, 'Smoke run produced no normalized scenarios.' . PHP_EOL); + exit(1); +} + +file_put_contents( + $resultDirectory . DIRECTORY_SEPARATOR . 'normalized-results.json', + json_encode($normalized, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL, +); + +echo 'Benchmark smoke OK: ' . count($normalized['scenarios']) . ' normalized scenario(s)' . PHP_EOL; diff --git a/benchmarks/bin/capture-environment.php b/benchmarks/bin/capture-environment.php new file mode 100644 index 0000000..63f4a98 --- /dev/null +++ b/benchmarks/bin/capture-environment.php @@ -0,0 +1,34 @@ +getExtension() !== 'php') { + continue; + } + + $command = [PHP_BINARY, '-l', $file->getPathname()]; + $process = proc_open($command, [1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes, $benchmarkRoot); + + if (!is_resource($process)) { + $failures[] = $file->getPathname() . ': unable to start PHP lint process'; + continue; + } + + $output = stream_get_contents($pipes[1]); + $error = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + $failures[] = $file->getPathname() . ': ' . trim((string) $output . (string) $error); + } + } +} + +if ($failures !== []) { + fwrite(STDERR, implode(PHP_EOL, $failures) . PHP_EOL); + exit(1); +} + +echo 'Benchmark PHP syntax OK' . PHP_EOL; diff --git a/benchmarks/bin/normalize-results.php b/benchmarks/bin/normalize-results.php new file mode 100644 index 0000000..02d22b9 --- /dev/null +++ b/benchmarks/bin/normalize-results.php @@ -0,0 +1,56 @@ + $input = $argv[++$i] ?? null, + '--environment' => $environmentPath = $argv[++$i] ?? null, + '--output' => $output = $argv[++$i] ?? null, + default => null, + }; +} + +if ($input === null) { + fwrite(STDERR, 'Usage: php bin/normalize-results.php --input [--environment environment.json] [--output normalized.json]' . PHP_EOL); + exit(1); +} + +$environment = $environmentPath !== null && is_file($environmentPath) + ? json_decode((string) file_get_contents($environmentPath), true, flags: JSON_THROW_ON_ERROR) + : BenchmarkEnvironment::capture($repositoryRoot); + +if (!is_array($environment)) { + fwrite(STDERR, 'Environment file must decode to an object.' . PHP_EOL); + exit(1); +} + +$extension = strtolower(pathinfo($input, PATHINFO_EXTENSION)); +$normalized = $extension === 'xml' + ? ResultNormalizer::fromPhpBenchXml($input, $environment) + : ResultNormalizer::normalize(json_decode((string) file_get_contents($input), true, flags: JSON_THROW_ON_ERROR)); + +$json = json_encode($normalized, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . PHP_EOL; + +if ($output !== null) { + $directory = dirname($output); + + if (!is_dir($directory)) { + mkdir($directory, 0777, true); + } + + file_put_contents($output, $json); +} + +echo $json; diff --git a/benchmarks/composer.json b/benchmarks/composer.json new file mode 100644 index 0000000..b97de14 --- /dev/null +++ b/benchmarks/composer.json @@ -0,0 +1,65 @@ +{ + "name": "evolvephp/benchmarks", + "description": "Reproducible benchmark harness and baseline tooling for EvolvePHP 2.", + "type": "project", + "license": "BSD-3-Clause", + "minimum-stability": "dev", + "prefer-stable": true, + "repositories": [ + { + "type": "path", + "url": "../packages/*", + "options": { + "versions": { + "evolvephp/contracts": "2.0.x-dev", + "evolvephp/core": "2.0.x-dev", + "evolvephp/dev-tools": "2.0.x-dev", + "evolvephp/http": "2.0.x-dev", + "evolvephp/module": "2.0.x-dev", + "evolvephp/plugin": "2.0.x-dev", + "evolvephp/testing": "2.0.x-dev" + }, + "reference": "config" + } + } + ], + "require": { + "php": "^8.4", + "evolvephp/core": "^2.0@dev", + "evolvephp/http": "^2.0@dev", + "evolvephp/module": "^2.0@dev", + "evolvephp/plugin": "^2.0@dev", + "evolvephp/testing": "^2.0@dev", + "nyholm/psr7": "^1.8" + }, + "require-dev": { + "phpbench/phpbench": "^1.7", + "phpunit/phpunit": "^13.2" + }, + "autoload": { + "psr-4": { + "Evolve\\Benchmarks\\": "src/", + "Evolve\\Benchmarks\\PhpBench\\": "benchmarks/" + } + }, + "autoload-dev": { + "psr-4": { + "Evolve\\Benchmarks\\Tests\\": "tests/" + } + }, + "scripts": { + "bench:local": "@php vendor/bin/phpbench run --config=phpbench.json --report=aggregate", + "capture-environment": "@php bin/capture-environment.php", + "smoke": "@php bin/benchmark-smoke.php", + "syntax": "@php bin/check-syntax.php", + "test": "@php vendor/bin/phpunit --configuration phpunit.xml.dist", + "quality": [ + "@syntax", + "@test", + "@smoke" + ] + }, + "config": { + "sort-packages": true + } +} diff --git a/benchmarks/composer.lock b/benchmarks/composer.lock new file mode 100644 index 0000000..c7a68e2 --- /dev/null +++ b/benchmarks/composer.lock @@ -0,0 +1,4024 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "789507a6cf9cc70ee6e66fcfd1a979c7", + "packages": [ + { + "name": "evolvephp/contracts", + "version": "2.0.x-dev", + "dist": { + "type": "path", + "url": "../packages/contracts", + "reference": "c82982b8fa82f0c8028daa158a03393b1073d727" + }, + "require": { + "php": "^8.4", + "psr/container": "^1.1 || ^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evolve\\Contracts\\": "src/" + } + }, + "license": [ + "BSD-3-Clause" + ], + "description": "Foundational public contracts for EvolvePHP 2.", + "transport-options": { + "relative": true + } + }, + { + "name": "evolvephp/core", + "version": "2.0.x-dev", + "dist": { + "type": "path", + "url": "../packages/core", + "reference": "bf25bc1d02d1fe9eec98ddcec991a5e62e24a804" + }, + "require": { + "evolvephp/contracts": "^2.0", + "php": "^8.4", + "psr/container": "^1.1 || ^2.0" + }, + "provide": { + "psr/container-implementation": "1.0.0" + }, + "bin": [ + "bin/evolve" + ], + "type": "library", + "autoload": { + "psr-4": { + "Evolve\\Core\\": "src/" + } + }, + "license": [ + "BSD-3-Clause" + ], + "description": "Application kernel and runtime-neutral orchestration for EvolvePHP 2.", + "transport-options": { + "relative": true + } + }, + { + "name": "evolvephp/http", + "version": "2.0.x-dev", + "dist": { + "type": "path", + "url": "../packages/http", + "reference": "5408f617b399ebb5b18e8eebd1960f2002bdd255" + }, + "require": { + "evolvephp/contracts": "^2.0", + "evolvephp/core": "^2.0", + "php": "^8.4", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "psr/http-server-handler": "^1.0", + "psr/http-server-middleware": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evolve\\Http\\": "src/" + } + }, + "license": [ + "BSD-3-Clause" + ], + "description": "HTTP lifecycle, routing and middleware foundations for EvolvePHP 2.", + "transport-options": { + "relative": true + } + }, + { + "name": "evolvephp/module", + "version": "2.0.x-dev", + "dist": { + "type": "path", + "url": "../packages/module", + "reference": "07aa67ad9e14920771aa6fcb193bfa0db2e0652c" + }, + "require": { + "evolvephp/contracts": "^2.0", + "php": "^8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evolve\\Module\\": "src/" + } + }, + "license": [ + "BSD-3-Clause" + ], + "description": "Application module SDK and lifecycle support for EvolvePHP 2.", + "transport-options": { + "relative": true + } + }, + { + "name": "evolvephp/plugin", + "version": "2.0.x-dev", + "dist": { + "type": "path", + "url": "../packages/plugin", + "reference": "c4878830ce15932ae492e776e560bf60806db784" + }, + "require": { + "evolvephp/contracts": "^2.0", + "php": "^8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evolve\\Plugin\\": "src/" + } + }, + "license": [ + "BSD-3-Clause" + ], + "description": "Framework plugin SDK and lifecycle support for EvolvePHP 2.", + "transport-options": { + "relative": true + } + }, + { + "name": "evolvephp/testing", + "version": "2.0.x-dev", + "dist": { + "type": "path", + "url": "../packages/testing", + "reference": "0c3fef62e713b38539a715fc3aaa06e4b841ef44" + }, + "require": { + "evolvephp/contracts": "^2.0", + "evolvephp/core": "^2.0", + "evolvephp/http": "^2.0", + "evolvephp/module": "^2.0", + "evolvephp/plugin": "^2.0", + "php": "^8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evolve\\Testing\\": "src/" + } + }, + "license": [ + "BSD-3-Clause" + ], + "description": "Testing utilities for EvolvePHP 2 packages and applications.", + "transport-options": { + "relative": true + } + }, + { + "name": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/http-server-handler", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-handler.git", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-handler/zipball/84c4fb66179be4caaf8e97bd239203245302e7d4", + "reference": "84c4fb66179be4caaf8e97bd239203245302e7d4", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side request handler", + "keywords": [ + "handler", + "http", + "http-interop", + "psr", + "psr-15", + "psr-7", + "request", + "response", + "server" + ], + "support": { + "source": "https://github.com/php-fig/http-server-handler/tree/1.0.2" + }, + "time": "2023-04-10T20:06:20+00:00" + }, + { + "name": "psr/http-server-middleware", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-server-middleware.git", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-server-middleware/zipball/c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "reference": "c1481f747daaa6a0782775cd6a8c26a1bf4a3829", + "shasum": "" + }, + "require": { + "php": ">=7.0", + "psr/http-message": "^1.0 || ^2.0", + "psr/http-server-handler": "^1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Server\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP server-side middleware", + "keywords": [ + "http", + "http-interop", + "middleware", + "psr", + "psr-15", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-server-middleware/issues", + "source": "https://github.com/php-fig/http-server-middleware/tree/1.0.2" + }, + "time": "2023-04-11T06:14:47+00:00" + } + ], + "packages-dev": [ + { + "name": "doctrine/annotations", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/901c2ee5d26eb64ff43c47976e114bf00843acf7", + "reference": "901c2ee5d26eb64ff43c47976e114bf00843acf7", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2 || ^3", + "ext-tokenizer": "*", + "php": "^7.2 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^2.0", + "doctrine/coding-standard": "^10", + "phpstan/phpstan": "^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/2.0.2" + }, + "abandoned": true, + "time": "2024-09-05T10:17:24+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.14.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "reference": "8680aa248f8e07bc8fb43f56f0f5fc77a0c96aae", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.14.0" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + } + ], + "time": "2026-08-11T10:17:44+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.8.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0" + }, + "time": "2026-07-04T14:30:18+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpbench/container", + "version": "2.2.3", + "source": { + "type": "git", + "url": "https://github.com/phpbench/container.git", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/container/zipball/0c7b2d36c1ea53fe27302fb8873ded7172047196", + "reference": "0c7b2d36c1ea53fe27302fb8873ded7172047196", + "shasum": "" + }, + "require": { + "psr/container": "^1.0|^2.0", + "symfony/options-resolver": "^4.2 || ^5.0 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "php-cs-fixer/shim": "^3.89", + "phpstan/phpstan": "^0.12.52", + "phpunit/phpunit": "^8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpBench\\DependencyInjection\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "Simple, configurable, service container.", + "support": { + "issues": "https://github.com/phpbench/container/issues", + "source": "https://github.com/phpbench/container/tree/2.2.3" + }, + "time": "2025-11-06T09:05:13+00:00" + }, + { + "name": "phpbench/phpbench", + "version": "1.7.0", + "source": { + "type": "git", + "url": "https://github.com/phpbench/phpbench.git", + "reference": "3d13c0d5dcf8730a67b70fa7fb03b4556b5cc0fe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpbench/phpbench/zipball/3d13c0d5dcf8730a67b70fa7fb03b4556b5cc0fe", + "reference": "3d13c0d5dcf8730a67b70fa7fb03b4556b5cc0fe", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^2.0", + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "ext-tokenizer": "*", + "php": "^8.2", + "phpbench/container": "^2.2", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "seld/jsonlint": "^1.1", + "symfony/console": "^6.1 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.1 || ^7.0 || ^8.0", + "symfony/finder": "^6.1 || ^7.0 || ^8.0", + "symfony/options-resolver": "^6.1 || ^7.0 || ^8.0", + "symfony/process": "^6.1 || ^7.0 || ^8.0", + "webmozart/glob": "^4.6" + }, + "require-dev": { + "dantleech/invoke": "^2.0", + "ergebnis/composer-normalize": "^2.39", + "jangregor/phpstan-prophecy": "^2.0", + "php-cs-fixer/shim": "^3.9", + "phpspec/prophecy": "^1.22", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^11.5", + "rector/rector": "^2.2", + "sebastian/exporter": "^6.3.2", + "symfony/error-handler": "^6.1 || ^7.0 || ^8.0", + "symfony/var-dumper": "^6.1 || ^7.0 || ^8.0" + }, + "suggest": { + "ext-xdebug": "For Xdebug profiling extension." + }, + "bin": [ + "bin/phpbench" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "files": [ + "lib/Report/Func/functions.php" + ], + "psr-4": { + "PhpBench\\": "lib/", + "PhpBench\\Extensions\\XDebug\\": "extensions/xdebug/lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Leech", + "email": "daniel@dantleech.com" + } + ], + "description": "PHP Benchmarking Framework", + "keywords": [ + "benchmarking", + "optimization", + "performance", + "profiling", + "testing" + ], + "support": { + "issues": "https://github.com/phpbench/phpbench/issues", + "source": "https://github.com/phpbench/phpbench/tree/1.7.0" + }, + "funding": [ + { + "url": "https://github.com/dantleech", + "type": "github" + } + ], + "time": "2026-06-08T19:09:20+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "14.3.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "6ce313bb110384148d1dc7695a99175f59529069" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/6ce313bb110384148d1dc7695a99175f59529069", + "reference": "6ce313bb110384148d1dc7695a99175f59529069", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.2", + "sebastian/version": "^7.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "14.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.3.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-08-16T05:23:47+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/9bb4e6c58b62c1e043be995c66abec7c97307aae", + "reference": "9bb4e6c58b62c1e043be995c66abec7c97307aae", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-08-25T14:47:43+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^13.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-invoker", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:34:47+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-text-template", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:36:37+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "9.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-timer", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:37:53+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "13.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "22104a5ceb8d642e6b30ab00211d53d88e2db368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/22104a5ceb8d642e6b30ab00211d53d88e2db368", + "reference": "22104a5ceb8d642e6b30ab00211d53d88e2db368", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-filter": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.14.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.3.1", + "phpunit/php-file-iterator": "^7.0.2", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.1", + "sebastian/comparator": "^8.4", + "sebastian/diff": "^9.0", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.2.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.1.0", + "sebastian/recursion-context": "^8.0.1", + "sebastian/type": "^7.0.2", + "sebastian/version": "^7.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "13.3-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.3.2" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-08-27T08:40:49+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-08-01T04:27:14+00:00" + }, + { + "name": "sebastian/comparator", + "version": "8.4.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "reference": "3b070e608146cba00fd6fd1f0ffba89e5a8897fb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.2" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/8.4.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:23:13+00:00" + }, + { + "name": "sebastian/complexity", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/complexity", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:41:32+00:00" + }, + { + "name": "sebastian/diff", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a2df6626c1baf31d5a88674882a3072f151b5a26", + "reference": "a2df6626c1baf31d5a88674882a3072f151b5a26", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.1", + "symfony/process": "^7.4.17" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" + } + ], + "time": "2026-08-25T15:38:55+00:00" + }, + { + "name": "sebastian/environment", + "version": "9.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.1.11" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:41:38+00:00" + }, + { + "name": "sebastian/exporter", + "version": "8.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "reference": "24a3b69bba4a12ab615fca9d34680c5598d9ab7a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/8.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-08-07T07:22:06+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "9.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^13.1.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:11:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-07-09T08:42:34+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/511064ecde82bd747e2ba2fab3dda8d977b59576", + "reference": "511064ecde82bd747e2ba2fab3dda8d977b59576", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "sebastian/recursion-context": "^8.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-enumerator", + "type": "tidelift" + } + ], + "time": "2026-08-13T07:05:05+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/f71bbcdc4f95456b4622810bec64eb06372e25b2", + "reference": "f71bbcdc4f95456b4622810bec64eb06372e25b2", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/object-reflector", + "type": "tidelift" + } + ], + "time": "2026-08-13T06:34:36+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "8.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.2.6" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2026-08-03T05:58:12+00:00" + }, + { + "name": "sebastian/type", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "bd1df467864cb95140414059a535b2d906173fcf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/bd1df467864cb95140414059a535b2d906173fcf", + "reference": "bd1df467864cb95140414059a535b2d906173fcf", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-08-10T08:00:57+00:00" + }, + { + "name": "sebastian/version", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/version", + "type": "tidelift" + } + ], + "time": "2026-02-06T04:52:52+00:00" + }, + { + "name": "seld/jsonlint", + "version": "1.12.1", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/jsonlint.git", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/jsonlint/zipball/9a90eb5d32d5a500296bf43f946d60246444d5f7", + "reference": "9a90eb5d32d5a500296bf43f946d60246444d5f7", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0 || ^8.5.13" + }, + "bin": [ + "bin/jsonlint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Seld\\JsonLint\\": "src/Seld/JsonLint/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "JSON Linter", + "keywords": [ + "json", + "linter", + "parser", + "validator" + ], + "support": { + "issues": "https://github.com/Seldaek/jsonlint/issues", + "source": "https://github.com/Seldaek/jsonlint/tree/1.12.1" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/seld/jsonlint", + "type": "tidelift" + } + ], + "time": "2026-06-12T11:32:29+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d07c06839e33047e2c894a6793248f3fb66c8129" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d07c06839e33047e2c894a6793248f3fb66c8129", + "reference": "d07c06839e33047e2c894a6793248f3fb66c8129", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T14:29:57+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/f3202fa1b5097b0af062dc978b32ecf63404e31d", + "reference": "f3202fa1b5097b0af062dc978b32ecf63404e31d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-05T06:23:12+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "6b2f4a0eeb28b5d74f90862592923a654bc629b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/6b2f4a0eeb28b5d74f90862592923a654bc629b3", + "reference": "6b2f4a0eeb28b5d74f90862592923a654bc629b3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "8d7acede2b2ae07605783d1c43e49b5767036474" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/8d7acede2b2ae07605783d1c43e49b5767036474", + "reference": "8d7acede2b2ae07605783d1c43e49b5767036474", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T12:16:08+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/88f9c561f678a02d54b897014049fa839e33ff82", + "reference": "88f9c561f678a02d54b897014049fa839e33ff82", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T08:25:59+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.42.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-07T06:33:24+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "d863f5e70d7c87abb906ac11b61f83036093000b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/d863f5e70d7c87abb906ac11b61f83036093000b", + "reference": "d863f5e70d7c87abb906ac11b61f83036093000b", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-08-21T17:47:34+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", + "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-06-16T09:55:08+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-28T07:35:25+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/glob", + "version": "4.7.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/glob.git", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/glob/zipball/8a2842112d6916e61e0e15e316465b611f3abc17", + "reference": "8a2842112d6916e61e0e15e316465b611f3abc17", + "shasum": "" + }, + "require": { + "php": "^7.3 || ^8.0.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "symfony/filesystem": "^5.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Glob\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "A PHP implementation of Ant's glob.", + "support": { + "issues": "https://github.com/webmozarts/glob/issues", + "source": "https://github.com/webmozarts/glob/tree/4.7.0" + }, + "time": "2024-03-07T20:33:40+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": { + "evolvephp/core": 20, + "evolvephp/http": 20, + "evolvephp/module": 20, + "evolvephp/plugin": 20, + "evolvephp/testing": 20 + }, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.4" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/benchmarks/phpbench.json b/benchmarks/phpbench.json new file mode 100644 index 0000000..c634c5f --- /dev/null +++ b/benchmarks/phpbench.json @@ -0,0 +1,8 @@ +{ + "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "benchmarks", + "runner.executor": "remote", + "runner.retry_threshold": 10, + "storage.xml_storage_path": ".phpbench/storage" +} diff --git a/benchmarks/phpunit.xml.dist b/benchmarks/phpunit.xml.dist new file mode 100644 index 0000000..7c94ac3 --- /dev/null +++ b/benchmarks/phpunit.xml.dist @@ -0,0 +1,24 @@ + + + + + tests + + + + + src + benchmarks + + + diff --git a/benchmarks/results/README.md b/benchmarks/results/README.md new file mode 100644 index 0000000..5d70de2 --- /dev/null +++ b/benchmarks/results/README.md @@ -0,0 +1,12 @@ +# Benchmark Results + +Committed files in this directory describe the result protocol only. + +Local and disposable output belongs under ignored paths: + +- `results/local/` +- `results/tmp/` +- `profiles/` +- `.phpbench/` + +A reference baseline should be recorded only after the documented protocol is run in a controlled, low-noise PHP 8.4 environment with the matching OPcache/JIT policy and environment fingerprint. diff --git a/benchmarks/src/Support/BenchmarkEnvironment.php b/benchmarks/src/Support/BenchmarkEnvironment.php new file mode 100644 index 0000000..df3acd2 --- /dev/null +++ b/benchmarks/src/Support/BenchmarkEnvironment.php @@ -0,0 +1,245 @@ + + */ + public static function capture(string $repositoryRoot): array + { + $repositoryRoot = rtrim($repositoryRoot, DIRECTORY_SEPARATOR); + $extensions = get_loaded_extensions(); + sort($extensions, SORT_STRING); + + $environment = [ + 'schema_version' => self::SCHEMA_VERSION, + 'captured_at' => (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format(DATE_ATOM), + 'source' => [ + 'git_sha' => self::command($repositoryRoot, ['git', 'rev-parse', 'HEAD']) ?? 'unavailable', + 'dirty' => self::command($repositoryRoot, ['git', 'status', '--short']) !== '', + ], + 'runtime' => [ + 'php_version' => PHP_VERSION, + 'php_sapi' => PHP_SAPI, + 'php_binary' => PHP_BINARY, + 'php_ini_loaded_file' => php_ini_loaded_file() ?: null, + 'memory_limit' => ini_get('memory_limit') ?: null, + ], + 'platform' => [ + 'os' => PHP_OS_FAMILY, + 'php_os' => PHP_OS, + 'php_uname' => php_uname(), + 'cpu_model' => self::cpuModel(), + 'logical_cpu_count' => self::logicalCpuCount(), + 'memory_total_bytes' => self::totalMemoryBytes(), + ], + 'composer' => [ + 'version' => self::composerVersion($repositoryRoot), + ], + 'phpbench' => [ + 'version' => self::installedVersion('phpbench/phpbench'), + ], + 'opcache' => self::opcache(), + 'jit' => self::jit(), + 'extensions' => $extensions, + 'lock' => self::lockState($repositoryRoot), + ]; + + $environment['fingerprint'] = EnvironmentFingerprint::fromEnvironment($environment); + self::sortRecursive($environment); + + return $environment; + } + + /** + * @return array{enabled: bool, configuration: array} + */ + private static function opcache(): array + { + return [ + 'enabled' => filter_var(ini_get('opcache.enable_cli'), FILTER_VALIDATE_BOOLEAN), + 'configuration' => [ + 'opcache.enable' => ini_get('opcache.enable'), + 'opcache.enable_cli' => ini_get('opcache.enable_cli'), + 'opcache.validate_timestamps' => ini_get('opcache.validate_timestamps'), + 'opcache.memory_consumption' => ini_get('opcache.memory_consumption'), + 'opcache.interned_strings_buffer' => ini_get('opcache.interned_strings_buffer'), + 'opcache.max_accelerated_files' => ini_get('opcache.max_accelerated_files'), + ], + ]; + } + + /** + * @return array{enabled: bool, configuration: array} + */ + private static function jit(): array + { + $bufferSize = ini_get('opcache.jit_buffer_size'); + + return [ + 'enabled' => $bufferSize !== false && $bufferSize !== '' && $bufferSize !== '0', + 'configuration' => [ + 'opcache.jit' => ini_get('opcache.jit'), + 'opcache.jit_buffer_size' => $bufferSize, + ], + ]; + } + + /** + * @return array{path: string, exists: bool, hash: string|null} + */ + private static function lockState(string $repositoryRoot): array + { + $path = $repositoryRoot . DIRECTORY_SEPARATOR . 'benchmarks' . DIRECTORY_SEPARATOR . 'composer.lock'; + + return [ + 'path' => 'benchmarks/composer.lock', + 'exists' => is_file($path), + 'hash' => is_file($path) ? hash_file('sha256', $path) : null, + ]; + } + + private static function installedVersion(string $package): ?string + { + if (!class_exists(InstalledVersions::class) || !InstalledVersions::isInstalled($package)) { + return null; + } + + return InstalledVersions::getPrettyVersion($package) ?? InstalledVersions::getVersion($package); + } + + private static function composerVersion(string $repositoryRoot): ?string + { + $binary = getenv('COMPOSER_BINARY') ?: null; + + if ($binary !== null && $binary !== '') { + return self::command($repositoryRoot, [PHP_BINARY, $binary, '--version']); + } + + $windowsComposer = 'D:\\tools\\composer84\\composer.phar'; + + if (is_file($windowsComposer)) { + return self::command($repositoryRoot, [PHP_BINARY, $windowsComposer, '--version']); + } + + return self::command($repositoryRoot, ['composer', '--version']); + } + + private static function cpuModel(): ?string + { + $processor = getenv('PROCESSOR_IDENTIFIER'); + + if (is_string($processor) && $processor !== '') { + return $processor; + } + + if (is_readable('/proc/cpuinfo')) { + $cpuinfo = file('/proc/cpuinfo', FILE_IGNORE_NEW_LINES); + + foreach ($cpuinfo === false ? [] : $cpuinfo as $line) { + if (str_starts_with($line, 'model name')) { + return trim((string) preg_replace('/^model name\s*:\s*/', '', $line)); + } + } + } + + return null; + } + + private static function logicalCpuCount(): ?int + { + $windowsCount = getenv('NUMBER_OF_PROCESSORS'); + + if (is_string($windowsCount) && ctype_digit($windowsCount)) { + return (int) $windowsCount; + } + + $linuxCount = self::command(getcwd() ?: __DIR__, ['getconf', '_NPROCESSORS_ONLN']); + + return $linuxCount !== null && ctype_digit($linuxCount) ? (int) $linuxCount : null; + } + + private static function totalMemoryBytes(): ?int + { + if (PHP_OS_FAMILY === 'Windows') { + $memory = self::command(getcwd() ?: __DIR__, ['wmic', 'ComputerSystem', 'get', 'TotalPhysicalMemory', '/value']); + + if (is_string($memory) && preg_match('/TotalPhysicalMemory=(\d+)/', $memory, $match) === 1) { + return (int) $match[1]; + } + } + + if (is_readable('/proc/meminfo')) { + $meminfo = file('/proc/meminfo', FILE_IGNORE_NEW_LINES); + + foreach ($meminfo === false ? [] : $meminfo as $line) { + if (preg_match('/^MemTotal:\s+(\d+)\s+kB$/', $line, $match) === 1) { + return (int) $match[1] * 1024; + } + } + } + + return null; + } + + /** + * @param list $command + */ + private static function command(string $cwd, array $command): ?string + { + $descriptorSpec = [ + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $process = @proc_open($command, $descriptorSpec, $pipes, $cwd); + + if (!is_resource($process)) { + return null; + } + + $output = stream_get_contents($pipes[1]); + $error = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + + $exitCode = proc_close($process); + + if ($exitCode !== 0) { + return null; + } + + $value = trim((string) $output); + + if ($value === '' && trim((string) $error) !== '') { + return null; + } + + return $value; + } + + /** + * @param array $value + */ + private static function sortRecursive(array &$value): void + { + foreach ($value as &$entry) { + if (is_array($entry)) { + self::sortRecursive($entry); + } + } + + if (!array_is_list($value)) { + ksort($value); + } + } +} diff --git a/benchmarks/src/Support/BenchmarkFixtureFactory.php b/benchmarks/src/Support/BenchmarkFixtureFactory.php new file mode 100644 index 0000000..a5af8c5 --- /dev/null +++ b/benchmarks/src/Support/BenchmarkFixtureFactory.php @@ -0,0 +1,394 @@ +register('bench.application.' . $i, ServiceLifetime::Application, static fn(): object => new \stdClass()); + $registry->register('bench.execution.' . $i, ServiceLifetime::Execution, static fn(): object => new \stdClass()); + $registry->register('bench.transient.' . $i, ServiceLifetime::Transient, static fn(): object => new \stdClass()); + } + + return [ + 'registry' => $registry, + 'container' => $registry->freeze(), + ]; + } + + /** + * @return array{matcher: RouteMatcher, request: ServerRequestInterface, routes: RouteCollection} + */ + public static function routeMatchingFixture(int $routeCount, string $category, string $position = 'first'): array + { + $target = self::targetOffset($routeCount, $position); + $routes = self::routes($routeCount, $category, $target); + $factory = new Psr17Factory(); + + $method = $category === 'method-mismatch' ? 'POST' : 'GET'; + $path = match ($category) { + 'parameterized' => '/bench/' . $target, + 'miss', 'not-found' => '/bench/missing', + default => '/bench/static-' . $target, + }; + + return [ + 'matcher' => new RouteMatcher($routes), + 'request' => $factory->createServerRequest($method, $path), + 'routes' => $routes, + ]; + } + + /** + * @return array{handler: RoutingRequestHandler, request: ServerRequestInterface, last_request: callable(): ?ServerRequestInterface} + */ + public static function routingHandlerFixture(int $routeCount, string $category): array + { + $state = (object) ['lastRequest' => null]; + $terminal = self::terminalHandler($state); + $routes = self::routes($routeCount, $category, 0, $terminal); + $factory = new Psr17Factory(); + $request = match ($category) { + 'method-mismatch' => $factory->createServerRequest('POST', '/bench/static-0'), + 'not-found' => $factory->createServerRequest('GET', '/bench/missing'), + default => $factory->createServerRequest('GET', '/bench/static-0'), + }; + + return [ + 'handler' => new RoutingRequestHandler(new RouteMatcher($routes)), + 'request' => $request, + 'last_request' => static fn(): ?ServerRequestInterface => $state->lastRequest, + ]; + } + + /** + * @return array{pipeline: MiddlewarePipeline, request: ServerRequestInterface, counter: callable(): int} + */ + public static function middlewareFixture(int $depth, string $mode): array + { + $factory = new Psr17Factory(); + $state = (object) ['counter' => 0]; + $middleware = []; + $shortCircuitIndex = match ($mode) { + 'early-short-circuit' => 0, + 'middle-short-circuit' => max(0, intdiv($depth, 2)), + default => null, + }; + + for ($i = 0; $i < $depth; ++$i) { + $middleware[] = new class($factory, $state, $i, $shortCircuitIndex) implements MiddlewareInterface { + public function __construct( + private Psr17Factory $factory, + private object $state, + private int $index, + private ?int $shortCircuitIndex, + ) {} + + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + ++$this->state->counter; + + if ($this->shortCircuitIndex === $this->index) { + return $this->factory->createResponse(204); + } + + return $handler->handle($request); + } + }; + } + + $terminal = new class($factory) implements RequestHandlerInterface { + public function __construct(private Psr17Factory $factory) {} + + public function handle(ServerRequestInterface $request): ResponseInterface + { + return $this->factory->createResponse(200); + } + }; + + return [ + 'pipeline' => new MiddlewarePipeline($middleware, $terminal), + 'request' => $factory->createServerRequest('GET', '/bench/middleware'), + 'counter' => static fn(): int => $state->counter, + ]; + } + + /** + * @return array{kernel: HttpKernel, request: ServerRequestInterface, last_request: callable(): ?ServerRequestInterface} + */ + public static function httpKernelFixture(string $scenario): array + { + $state = (object) ['lastRequest' => null]; + $terminal = self::terminalHandler($state); + $middlewareDepth = $scenario === 'middleware-heavy' ? 20 : 0; + $category = $scenario === 'parameterized' ? 'parameterized' : 'static'; + $routes = self::routes(10, $category, 5, $terminal); + $handler = new RoutingRequestHandler( + new RouteMatcher($routes), + self::passThroughMiddleware($middlewareDepth), + ); + + $registry = new ServiceRegistry(); + $registry->freeze(); + + $factory = new Psr17Factory(); + $request = match ($scenario) { + 'parameterized' => $factory->createServerRequest('GET', '/bench/5'), + 'not-found' => $factory->createServerRequest('GET', '/bench/missing'), + 'method-mismatch' => $factory->createServerRequest('POST', '/bench/static-5'), + default => $factory->createServerRequest('GET', '/bench/static-5'), + }; + + return [ + 'kernel' => new HttpKernel($handler, new ExecutionOrchestrator($registry)), + 'request' => $request, + 'last_request' => static fn(): ?ServerRequestInterface => $state->lastRequest, + ]; + } + + /** + * @return array{ + * orchestrator: ExecutionOrchestrator, + * operation: callable(mixed, ExecutionScope): string, + * reset_count: callable(): int, + * observation_count: callable(): int, + * last_scope: callable(): ?ExecutionScope + * } + */ + public static function executionOrchestratorFixture(int $resetParticipants = 0, bool $withObservationSink = false): array + { + $registry = new ServiceRegistry(); + $registry->register('bench.execution', ServiceLifetime::Execution, static fn(): object => new \stdClass()); + $registry->freeze(); + $state = (object) [ + 'resetCount' => 0, + 'observationCount' => 0, + 'lastScope' => null, + ]; + + $sink = $withObservationSink + ? new class($state) implements ObservationSink { + public function __construct(private object $state) {} + + public function observe(Observation $observation): void + { + ++$this->state->observationCount; + } + } + : null; + + return [ + 'orchestrator' => new ExecutionOrchestrator($registry, $sink), + 'operation' => static function ($_context, ExecutionScope $scope) use ($resetParticipants, $state): string { + $state->lastScope = $scope; + $scope->get('bench.execution'); + + for ($i = 0; $i < $resetParticipants; ++$i) { + $scope->registerResetParticipant('participant-' . $i, new class($state) implements ResetParticipant { + public function __construct(private object $state) {} + + public function reset(): void + { + ++$this->state->resetCount; + } + }); + } + + return 'ok'; + }, + 'reset_count' => static fn(): int => $state->resetCount, + 'observation_count' => static fn(): int => $state->observationCount, + 'last_scope' => static fn(): ?ExecutionScope => $state->lastScope, + ]; + } + + /** + * @return array{ + * kernel: ApplicationKernel, + * register_count: callable(): int, + * boot_count: callable(): int, + * ready_count: callable(): int + * } + */ + public static function applicationBootFixture(int $componentCount): array + { + $state = (object) [ + 'registerCount' => 0, + 'bootCount' => 0, + 'readyCount' => 0, + ]; + $definitions = []; + $enabled = []; + + for ($i = 0; $i < $componentCount; ++$i) { + $identifier = 'bench/component-' . $i; + $enabled[] = $identifier; + $dependencies = $i === 0 + ? [] + : [new ComponentDependency(new ComponentIdentifier('bench/component-' . ($i - 1)), ComponentDependencyKind::Required)]; + + $definitions[] = new ComponentDefinitionFixture( + new ComponentIdentifier($identifier), + ComponentType::Module, + static fn() => new ComponentEntryPointFixture( + register: static function (ServiceDefinitionRegistrar $registrar) use ($state, $i): void { + ++$state->registerCount; + $registrar->registerApplication('bench.component.' . $i, static fn(): object => new \stdClass()); + }, + boot: static function () use ($state): void { + ++$state->bootCount; + }, + ready: static function () use ($state): void { + ++$state->readyCount; + }, + ), + new ComponentGraphRelations(dependencies: $dependencies), + ); + } + + return [ + 'kernel' => new ApplicationKernel( + new ArrayConfiguration(['evolve' => ['components' => ['enabled' => $enabled]]]), + services: new ServiceRegistry(), + components: new ComponentBootstrapper($definitions), + ), + 'register_count' => static fn(): int => $state->registerCount, + 'boot_count' => static fn(): int => $state->bootCount, + 'ready_count' => static fn(): int => $state->readyCount, + ]; + } + + /** + * @return array + */ + public static function persistentSequentialExecutionEvidence(int $iterations, int $checkpointEvery): array + { + $fixture = self::executionOrchestratorFixture(resetParticipants: 1); + $checkpoints = [[ + 'execution_count' => 0, + 'current_bytes' => memory_get_usage(true), + 'peak_bytes' => memory_get_peak_usage(true), + ]]; + + for ($i = 1; $i <= $iterations; ++$i) { + $fixture['orchestrator']->execute(ExecutionKind::HttpRequest, $fixture['operation']); + + if ($i % $checkpointEvery === 0 || $i === $iterations) { + $checkpoints[] = [ + 'execution_count' => $i, + 'current_bytes' => memory_get_usage(true), + 'peak_bytes' => memory_get_peak_usage(true), + ]; + } + } + + return [ + 'label' => 'persistent-style sequential execution evidence', + 'execution_count' => $iterations, + 'reset_count' => $fixture['reset_count'](), + 'checkpoints' => $checkpoints, + 'retained_bytes_after_cleanup' => memory_get_usage(true), + ]; + } + + private static function targetOffset(int $routeCount, string $position): int + { + return match ($position) { + 'middle' => intdiv($routeCount, 2), + 'last' => max(0, $routeCount - 1), + default => 0, + }; + } + + private static function routes(int $routeCount, string $category, int $target, ?RequestHandlerInterface $handler = null): RouteCollection + { + $handler ??= self::terminalHandler((object) ['lastRequest' => null]); + $routes = []; + + for ($i = 0; $i < $routeCount; ++$i) { + $path = $category === 'parameterized' && $i === $target ? '/bench/{id}' : '/bench/static-' . $i; + $routes[] = new Route(['GET'], $path, $handler); + } + + return new RouteCollection($routes); + } + + private static function terminalHandler(object $state): RequestHandlerInterface + { + $factory = new Psr17Factory(); + + return new class($factory, $state) implements RequestHandlerInterface { + public function __construct( + private Psr17Factory $factory, + private object $state, + ) {} + + public function handle(ServerRequestInterface $request): ResponseInterface + { + $this->state->lastRequest = $request; + + return $this->factory->createResponse(200); + } + }; + } + + /** + * @return list + */ + private static function passThroughMiddleware(int $depth): array + { + $middleware = []; + + for ($i = 0; $i < $depth; ++$i) { + $middleware[] = new class implements MiddlewareInterface { + public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface + { + return $handler->handle($request); + } + }; + } + + return $middleware; + } +} diff --git a/benchmarks/src/Support/EnvironmentFingerprint.php b/benchmarks/src/Support/EnvironmentFingerprint.php new file mode 100644 index 0000000..9fd352c --- /dev/null +++ b/benchmarks/src/Support/EnvironmentFingerprint.php @@ -0,0 +1,50 @@ + $environment + * @return array{hash: string, fields: array} + */ + public static function fromEnvironment(array $environment): array + { + $fields = [ + 'schema_version' => $environment['schema_version'] ?? null, + 'runtime' => $environment['runtime'] ?? [], + 'platform' => $environment['platform'] ?? [], + 'composer_version' => $environment['composer']['version'] ?? null, + 'phpbench_version' => $environment['phpbench']['version'] ?? null, + 'opcache' => $environment['opcache'] ?? [], + 'jit' => $environment['jit'] ?? [], + 'extensions' => $environment['extensions'] ?? [], + 'lock_hash' => $environment['lock']['hash'] ?? null, + ]; + + self::sortRecursive($fields); + + return [ + 'hash' => hash('sha256', json_encode($fields, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES)), + 'fields' => $fields, + ]; + } + + /** + * @param array $value + */ + private static function sortRecursive(array &$value): void + { + foreach ($value as &$entry) { + if (is_array($entry)) { + self::sortRecursive($entry); + } + } + + if (!array_is_list($value)) { + ksort($value); + } + } +} diff --git a/benchmarks/src/Support/ResultNormalizer.php b/benchmarks/src/Support/ResultNormalizer.php new file mode 100644 index 0000000..c8848dd --- /dev/null +++ b/benchmarks/src/Support/ResultNormalizer.php @@ -0,0 +1,200 @@ + $raw + * @return array + */ + public static function normalize(array $raw): array + { + $environment = $raw['environment'] ?? []; + $normalized = [ + 'schema_version' => self::SCHEMA_VERSION, + 'source_sha' => $environment['source']['git_sha'] ?? null, + 'environment_fingerprint' => $environment['fingerprint']['hash'] ?? null, + 'generated_at' => gmdate(DATE_ATOM), + 'scenarios' => [], + ]; + + foreach ($raw['scenarios'] ?? [] as $scenario) { + if (!is_array($scenario)) { + continue; + } + + $samples = array_values(array_map('floatval', $scenario['samples'] ?? [])); + $normalized['scenarios'][] = self::normalizeScenario( + (string) ($scenario['id'] ?? 'unknown'), + $samples, + (string) ($scenario['unit'] ?? 'microseconds'), + is_array($scenario['memory'] ?? null) ? $scenario['memory'] : [], + ); + } + + return $normalized; + } + + /** + * @param array $environment + * @return array + */ + public static function fromPhpBenchXml(string $xmlPath, array $environment): array + { + if (!is_file($xmlPath)) { + throw new RuntimeException('PHPBench XML result file does not exist.'); + } + + $xml = simplexml_load_file($xmlPath); + + if (!$xml instanceof SimpleXMLElement) { + throw new RuntimeException('PHPBench XML result file could not be parsed.'); + } + + $scenarios = []; + + foreach ($xml->xpath('//benchmark') ?: [] as $benchmark) { + $benchmarkClass = (string) $benchmark['class']; + + foreach ($benchmark->subject as $subject) { + $subjectName = (string) $subject['name']; + + foreach ($subject->variant as $variant) { + $parameterSet = $variant->{'parameter-set'}; + $variantName = (string) ($parameterSet['name'] ?? ''); + $id = trim($benchmarkClass . '::' . $subjectName . ($variantName !== '' ? '#' . $variantName : '')); + $revs = max(1, (int) ($variant['revs'] ?? 1)); + $samples = []; + $memory = []; + + foreach ($variant->iteration as $iteration) { + $attributes = $iteration->attributes(); + + if (isset($attributes['time-net'])) { + $samples[] = ((float) $attributes['time-net']) / $revs; + } + + if (isset($attributes['mem-final'])) { + $memory['current_bytes'] = (int) $attributes['mem-final']; + } + + if (isset($attributes['mem-peak'])) { + $memory['peak_bytes'] = max($memory['peak_bytes'] ?? 0, (int) $attributes['mem-peak']); + } + } + + $scenarios[] = [ + 'id' => $id, + 'samples' => $samples, + 'unit' => 'microseconds', + 'memory' => $memory, + ]; + } + } + } + + return self::normalize([ + 'environment' => $environment, + 'scenarios' => $scenarios, + ]); + } + + /** + * @param list $samples + * @param array $memory + * @return array + */ + private static function normalizeScenario(string $id, array $samples, string $unit, array $memory): array + { + sort($samples, SORT_NUMERIC); + $count = count($samples); + $sum = array_sum($samples); + $mean = $count > 0 ? $sum / $count : null; + $standardDeviation = $count > 1 && $mean !== null ? self::standardDeviation($samples, $mean) : null; + + return [ + 'id' => $id, + 'unit' => $unit, + 'sample_count' => $count, + 'total' => $count > 0 ? $sum : null, + 'mean' => $mean, + 'min' => $count > 0 ? min($samples) : null, + 'max' => $count > 0 ? max($samples) : null, + 'p50' => self::percentile($samples, 50, 1), + 'p50_status' => $count >= 1 ? 'available' : 'insufficient_samples', + 'p95' => self::tailPercentile($samples, 95, 20), + 'p95_status' => $count >= 20 ? 'available' : 'insufficient_samples', + 'p99' => self::tailPercentile($samples, 99, 100), + 'p99_status' => $count >= 100 ? 'available' : 'insufficient_samples', + 'relative_standard_deviation_percent' => $mean !== null && $mean > 0 && $standardDeviation !== null + ? ($standardDeviation / $mean) * 100 + : null, + 'throughput_per_second' => $mean !== null && $mean > 0 ? 1_000_000 / $mean : null, + 'memory' => $memory, + ]; + } + + /** + * @param list $samples + */ + private static function percentile(array $samples, int $percentile, int $minimumSamples): ?float + { + $count = count($samples); + + if ($count < $minimumSamples) { + return null; + } + + if ($count === 1) { + return $samples[0]; + } + + $position = ($percentile / 100) * ($count - 1); + $lower = (int) floor($position); + $upper = (int) ceil($position); + + if ($lower === $upper) { + return $samples[$lower]; + } + + return $samples[$lower] + (($samples[$upper] - $samples[$lower]) * ($position - $lower)); + } + + /** + * @param list $samples + */ + private static function tailPercentile(array $samples, int $percentile, int $minimumSamples): ?float + { + $count = count($samples); + + if ($count < $minimumSamples) { + return null; + } + + $index = max(0, min($count - 1, (int) ceil(($percentile / 100) * $count) - 1)); + + return $samples[$index]; + } + + /** + * @param list $samples + */ + private static function standardDeviation(array $samples, float $mean): float + { + $sum = 0.0; + + foreach ($samples as $sample) { + $sum += ($sample - $mean) ** 2; + } + + return sqrt($sum / (count($samples) - 1)); + } +} diff --git a/benchmarks/tests/BenchmarkEnvironmentTest.php b/benchmarks/tests/BenchmarkEnvironmentTest.php new file mode 100644 index 0000000..bae71ad --- /dev/null +++ b/benchmarks/tests/BenchmarkEnvironmentTest.php @@ -0,0 +1,60 @@ + '2026-01-01T00:00:00+00:00', + 'runtime' => ['php_version' => '8.4.0'], + 'platform' => ['os' => 'WINNT'], + ]; + + $changedTimestamp = $environment; + $changedTimestamp['captured_at'] = '2026-01-01T00:00:01+00:00'; + + self::assertSame( + EnvironmentFingerprint::fromEnvironment($environment)['hash'], + EnvironmentFingerprint::fromEnvironment($changedTimestamp)['hash'], + ); + } +} diff --git a/benchmarks/tests/FixtureCorrectnessTest.php b/benchmarks/tests/FixtureCorrectnessTest.php new file mode 100644 index 0000000..edeb120 --- /dev/null +++ b/benchmarks/tests/FixtureCorrectnessTest.php @@ -0,0 +1,155 @@ +match($fixture['request']); + + self::assertNotNull($match); + self::assertSame('/bench/{id}', $match->route()->path()); + self::assertSame(['id' => '50'], $match->parameters()); + } + + public function testRoutingHandlerDistinguishes404And405Paths(): void + { + $notFound = BenchmarkFixtureFactory::routingHandlerFixture(10, 'not-found'); + + $this->expectException(RouteNotFound::class); + $notFound['handler']->handle($notFound['request']); + } + + public function testRoutingHandlerReports405AllowedMethods(): void + { + $methodMismatch = BenchmarkFixtureFactory::routingHandlerFixture(10, 'method-mismatch'); + + try { + $methodMismatch['handler']->handle($methodMismatch['request']); + self::fail('Expected method mismatch to throw.'); + } catch (MethodNotAllowed $exception) { + self::assertSame(['GET'], $exception->allowedMethods()); + } + } + + public function testMiddlewareFixtureInvokesEveryPassThroughLayer(): void + { + $fixture = BenchmarkFixtureFactory::middlewareFixture(5, 'pass-through'); + + $response = $fixture['pipeline']->handle($fixture['request']); + + self::assertSame(200, $response->getStatusCode()); + self::assertSame(5, $fixture['counter']()); + } + + public function testHttpKernelFixtureExecutesKernelRoutingAndExecutionScope(): void + { + $fixture = BenchmarkFixtureFactory::httpKernelFixture('parameterized'); + $outcome = $fixture['kernel']->handle($fixture['request']); + + self::assertTrue($outcome->primarySucceeded()); + self::assertSame(200, $outcome->primaryResult()->getStatusCode()); + self::assertTrue($outcome->isReusable()); + self::assertNotNull($fixture['last_request']()->getAttribute(RouteMatch::class)); + } + + public function testExecutionFixtureClosesScopesAndRunsResetParticipants(): void + { + $fixture = BenchmarkFixtureFactory::executionOrchestratorFixture(resetParticipants: 3); + + $outcome = $fixture['orchestrator']->execute( + ExecutionKind::HttpRequest, + $fixture['operation'], + ); + + self::assertTrue($outcome->primarySucceeded()); + self::assertSame(3, $fixture['reset_count']()); + $this->expectException(ExecutionScopeClosed::class); + $fixture['last_scope']()->get('bench.execution'); + } + + public function testApplicationBootFixtureRunsComponentLifecycle(): void + { + $fixture = BenchmarkFixtureFactory::applicationBootFixture(5); + + $fixture['kernel']->boot(); + + self::assertSame(5, $fixture['register_count']()); + self::assertSame(5, $fixture['boot_count']()); + self::assertSame(5, $fixture['ready_count']()); + } + + public function testPersistentStyleHarnessRecordsMemoryAndResetsWithoutRuntimeAdapterClaims(): void + { + $evidence = BenchmarkFixtureFactory::persistentSequentialExecutionEvidence(25, 5); + + self::assertSame('persistent-style sequential execution evidence', $evidence['label']); + self::assertSame(25, $evidence['execution_count']); + self::assertSame(25, $evidence['reset_count']); + self::assertCount(6, $evidence['checkpoints']); + } + + public function testRouteMatcherBenchmarkPreparesMatcherOutsideTimedMatch(): void + { + $bench = new \Evolve\Benchmarks\PhpBench\Http\RouteMatcherBench(); + $bench->setUpRouteScenario(['routes' => 10, 'category' => 'static', 'position' => 'first']); + + $match = $bench->matchPreparedRoute(); + + self::assertNotNull($match); + self::assertSame('/bench/static-0', $match->route()->path()); + } + + public function testContainerResolutionBenchmarkPreparesCachedResolutionBeforeMeasurement(): void + { + $bench = new \Evolve\Benchmarks\PhpBench\Core\ContainerResolutionBench(); + $bench->setUpApplicationCachedResolution(['services' => 10]); + + $first = $bench->resolvePreparedApplicationCachedService(); + $second = $bench->resolvePreparedApplicationCachedService(); + + self::assertSame($first, $second); + self::assertSame('bench.application.9', $bench->applicationServiceId()); + } + + public function testFirstResolutionBenchmarkMethodsDeclareSingleRevAndZeroWarmup(): void + { + $appMethod = new \ReflectionMethod(\Evolve\Benchmarks\PhpBench\Core\ContainerResolutionBench::class, 'benchApplicationFirstResolution'); + $executionMethod = new \ReflectionMethod(\Evolve\Benchmarks\PhpBench\Core\ContainerResolutionBench::class, 'benchExecutionFirstResolution'); + + self::assertSame([1], $this->revsFor($appMethod)); + self::assertSame([0], $this->warmupFor($appMethod)); + self::assertSame([1], $this->revsFor($executionMethod)); + self::assertSame([0], $this->warmupFor($executionMethod)); + } + + private function revsFor(\ReflectionMethod $method): array + { + $attributes = $method->getAttributes('PhpBench\\Attributes\\Revs'); + + self::assertNotEmpty($attributes); + + return $attributes[0]->newInstance()->revs; + } + + private function warmupFor(\ReflectionMethod $method): array + { + $attributes = $method->getAttributes('PhpBench\\Attributes\\Warmup'); + + self::assertNotEmpty($attributes); + + return $attributes[0]->newInstance()->revs; + } +} diff --git a/benchmarks/tests/ResultNormalizerTest.php b/benchmarks/tests/ResultNormalizerTest.php new file mode 100644 index 0000000..69a3f56 --- /dev/null +++ b/benchmarks/tests/ResultNormalizerTest.php @@ -0,0 +1,65 @@ + [ + 'source' => ['git_sha' => 'abc123'], + 'fingerprint' => ['hash' => str_repeat('a', 64)], + ], + 'scenarios' => [ + [ + 'id' => 'route.static.10.first', + 'samples' => [100.0, 120.0, 110.0, 130.0], + 'unit' => 'microseconds', + 'memory' => ['current_bytes' => 1024, 'peak_bytes' => 2048], + ], + ], + ]); + + self::assertSame('evolvephp.benchmark.results.v1', $normalized['schema_version']); + self::assertSame('abc123', $normalized['source_sha']); + self::assertSame(str_repeat('a', 64), $normalized['environment_fingerprint']); + self::assertSame('route.static.10.first', $normalized['scenarios'][0]['id']); + self::assertSame(4, $normalized['scenarios'][0]['sample_count']); + self::assertSame(115.0, $normalized['scenarios'][0]['mean']); + self::assertSame(100.0, $normalized['scenarios'][0]['min']); + self::assertSame(130.0, $normalized['scenarios'][0]['max']); + self::assertSame(115.0, $normalized['scenarios'][0]['p50']); + self::assertNull($normalized['scenarios'][0]['p95']); + self::assertNull($normalized['scenarios'][0]['p99']); + self::assertSame('insufficient_samples', $normalized['scenarios'][0]['p95_status']); + } + + public function testNormalizerKeepsHighPercentilesWhenEnoughSamplesExist(): void + { + $samples = range(1, 100); + $normalized = ResultNormalizer::normalize([ + 'environment' => [ + 'source' => ['git_sha' => 'abc123'], + 'fingerprint' => ['hash' => str_repeat('b', 64)], + ], + 'scenarios' => [ + [ + 'id' => 'execution.no-sink', + 'samples' => $samples, + 'unit' => 'microseconds', + ], + ], + ]); + + self::assertSame(50.5, $normalized['scenarios'][0]['p50']); + self::assertSame(95.0, $normalized['scenarios'][0]['p95']); + self::assertSame(99.0, $normalized['scenarios'][0]['p99']); + self::assertSame('available', $normalized['scenarios'][0]['p99_status']); + } +} diff --git a/tests/Architecture/EvolvePhp2BenchmarkHarnessTest.php b/tests/Architecture/EvolvePhp2BenchmarkHarnessTest.php new file mode 100644 index 0000000..7709a5a --- /dev/null +++ b/tests/Architecture/EvolvePhp2BenchmarkHarnessTest.php @@ -0,0 +1,150 @@ +root = dirname(__DIR__, 2); + } + + public function testBenchmarkToolingIsIsolatedUnderBenchmarksComposerRoot(): void + { + $this->assertFileExists($this->projectPath('benchmarks/composer.json')); + $this->assertFileExists($this->projectPath('benchmarks/phpbench.json')); + $this->assertFileExists($this->projectPath('benchmarks/bin/benchmark-smoke.php')); + $this->assertFileExists($this->projectPath('benchmarks/bin/capture-environment.php')); + $this->assertFileExists($this->projectPath('benchmarks/bin/normalize-results.php')); + $this->assertFileExists($this->projectPath('benchmarks/results/README.md')); + + $benchmarkManifest = $this->readJsonFile('benchmarks/composer.json'); + $this->assertArrayHasKey('phpbench/phpbench', $benchmarkManifest['require-dev']); + $this->assertArrayHasKey('nyholm/psr7', $benchmarkManifest['require']); + + $rootManifest = $this->readJsonFile('composer.json'); + $this->assertPackageAbsentFromManifest('phpbench/phpbench', $rootManifest, 'composer.json'); + $this->assertPackageAbsentFromManifest('nyholm/psr7', $rootManifest, 'composer.json'); + + foreach ($this->packageManifests() as $path) { + $manifest = $this->readJsonFile($path); + $this->assertPackageAbsentFromManifest('phpbench/phpbench', $manifest, $path); + $this->assertPackageAbsentFromManifest('nyholm/psr7', $manifest, $path); + } + } + + public function testBenchmarkOutputPolicyIsScopedAndProductionSourceIsNotOwnedByHarness(): void + { + $gitignore = $this->readProjectFile('benchmarks/.gitignore'); + + foreach ([ + '/vendor/', + '/.phpbench/', + '/results/local/', + '/results/tmp/', + '/profiles/', + ] as $ignoredPath) { + $this->assertStringContainsString($ignoredPath, $gitignore); + } + + $phpbenchConfig = $this->readProjectFile('benchmarks/phpbench.json'); + $this->assertStringContainsString('"runner.path": "benchmarks"', $phpbenchConfig); + $this->assertStringNotContainsString('packages/core/src', $phpbenchConfig); + $this->assertStringNotContainsString('packages/http/src', $phpbenchConfig); + + foreach ($this->trackedFiles() as $file) { + $normalized = str_replace('\\', '/', $file); + $this->assertDoesNotMatchRegularExpression('#^packages/(contracts|core|http|module|plugin|testing|dev-tools)/src/.+Bench\.php$#', $normalized); + } + } + + public function testBenchmarkDocumentationRecordsProtocolAndNoMarketingClaim(): void + { + $readme = $this->readProjectFile('benchmarks/README.md'); + + foreach ([ + 'LOCAL / NON-CANONICAL BASELINE', + 'PHP 8.4', + 'OPcache enabled', + 'JIT disabled', + 'one-off stopwatch results are not performance evidence', + 'environment fingerprint', + 'optimization work', + 'Cross-framework comparison', + 'no current fastest-framework claim', + 'no current top-three performance claim', + ] as $phrase) { + $this->assertStringContainsString($phrase, $readme); + } + } + + /** + * @return list + */ + private function packageManifests(): array + { + return [ + 'packages/contracts/composer.json', + 'packages/core/composer.json', + 'packages/dev-tools/composer.json', + 'packages/http/composer.json', + 'packages/module/composer.json', + 'packages/plugin/composer.json', + 'packages/testing/composer.json', + ]; + } + + private function assertPackageAbsentFromManifest(string $package, array $manifest, string $path): void + { + $this->assertArrayNotHasKey($package, $manifest['require'] ?? [], $path . ' must not require ' . $package . '.'); + $this->assertArrayNotHasKey($package, $manifest['require-dev'] ?? [], $path . ' must not require-dev ' . $package . '.'); + } + + /** + * @return list + */ + private function trackedFiles(): array + { + $output = []; + $exitCode = 0; + + exec('git ls-files --cached --others --exclude-standard', $output, $exitCode); + + $this->assertSame(0, $exitCode, 'git ls-files should succeed.'); + + sort($output); + + return $output; + } + + private function projectPath(string $path): string + { + return $this->root . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + private function readProjectFile(string $path): string + { + $fullPath = $this->projectPath($path); + $this->assertFileExists($fullPath, $path . ' should exist before it is read.'); + + $content = file_get_contents($fullPath); + $this->assertIsString($content); + + return $content; + } + + /** + * @return array + */ + private function readJsonFile(string $path): array + { + $json = json_decode($this->readProjectFile($path), true); + + $this->assertSame(JSON_ERROR_NONE, json_last_error(), $path . ' should contain valid JSON: ' . json_last_error_msg()); + $this->assertIsArray($json, $path . ' should decode to a JSON object.'); + + return $json; + } +} diff --git a/tests/Documentation/EvolvePhp2ReadmeAndMetadataConsistencyTest.php b/tests/Documentation/EvolvePhp2ReadmeAndMetadataConsistencyTest.php index 19edae2..9036be7 100644 --- a/tests/Documentation/EvolvePhp2ReadmeAndMetadataConsistencyTest.php +++ b/tests/Documentation/EvolvePhp2ReadmeAndMetadataConsistencyTest.php @@ -17,6 +17,8 @@ public function testTrackedReadmeInventoryIsTheExpectedCanonicalSet(): void array( 'DEVELOPMENT.md', 'README.md', + 'benchmarks/README.md', + 'benchmarks/results/README.md', 'docs/rfcs/README.md', 'packages/README.md', 'packages/contracts/README.md',