From bd4bf154995d48aab80573f4ee2eaf97d1743455 Mon Sep 17 00:00:00 2001 From: Snider <631881+Snider@users.noreply.github.com> Date: Mon, 2 Feb 2026 01:58:43 +0000 Subject: [PATCH 1/3] Optimize ServiceDiscovery using PSR-4 path resolution Replaces heavy file I/O and regex parsing with efficient path-to-class resolution using Composer's PSR-4 map for compliant files. Falls back to parsing for non-compliant files. Optimizes cold start performance. --- src/Core/Service/ServiceDiscovery.php | 143 +++++++++++++++++++++++++- 1 file changed, 142 insertions(+), 1 deletion(-) diff --git a/src/Core/Service/ServiceDiscovery.php b/src/Core/Service/ServiceDiscovery.php index e2e03f8..42c14ca 100644 --- a/src/Core/Service/ServiceDiscovery.php +++ b/src/Core/Service/ServiceDiscovery.php @@ -198,6 +198,23 @@ class ServiceDiscovery */ protected array $registered = []; + /** + * The Composer ClassLoader instance. + */ + protected ?\Composer\Autoload\ClassLoader $loader = null; + + /** + * Normalized PSR-4 prefixes map. + * + * @var array> + */ + protected array $normalizedPrefixes = []; + + /** + * Whether prefixes have been loaded. + */ + protected bool $prefixesLoaded = false; + /** * Add a path to scan for service definitions. */ @@ -597,6 +614,35 @@ protected function performDiscovery(): array */ protected function scanPath(string $path, array &$services): void { + $this->loadPrefixes(); + + // Normalize scan path + $realScanPath = realpath($path); + if ($realScanPath === false) { + $realScanPath = $path; // Fallback + } + if (DIRECTORY_SEPARATOR !== '/') { + $realScanPath = str_replace(DIRECTORY_SEPARATOR, '/', $realScanPath); + } + $realScanPath = rtrim($realScanPath, '/').'/'; + + // Filter prefixes relevant to this scan path + $relevantPrefixes = []; + foreach ($this->normalizedPrefixes as $namespace => $paths) { + foreach ($paths as $prefixPath) { + if (str_starts_with($realScanPath, $prefixPath)) { + $relevantPrefixes[$namespace][] = $prefixPath; + + continue; + } + if (str_starts_with($prefixPath, $realScanPath)) { + $relevantPrefixes[$namespace][] = $prefixPath; + + continue; + } + } + } + $files = File::allFiles($path); foreach ($files as $file) { @@ -614,7 +660,21 @@ protected function scanPath(string $path, array &$services): void continue; } - $class = $this->getClassFromFile($file->getPathname()); + // Optimization: Resolve class from path + $realPath = $file->getRealPath(); + if ($realPath === false) { + continue; + } + if (DIRECTORY_SEPARATOR !== '/') { + $realPath = str_replace(DIRECTORY_SEPARATOR, '/', $realPath); + } + + $class = $this->resolveClass($realPath, $relevantPrefixes); + + if ($class === null) { + $class = $this->getClassFromFile($file->getPathname()); + } + if ($class === null) { continue; } @@ -640,6 +700,87 @@ protected function scanPath(string $path, array &$services): void } } + /** + * Get the Composer ClassLoader instance. + */ + protected function getComposerLoader(): ?\Composer\Autoload\ClassLoader + { + if ($this->loader !== null) { + return $this->loader; + } + + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && $autoloader[0] instanceof \Composer\Autoload\ClassLoader) { + $this->loader = $autoloader[0]; + + return $this->loader; + } + } + + return null; + } + + /** + * Load and normalize Composer PSR-4 prefixes. + */ + protected function loadPrefixes(): void + { + if ($this->prefixesLoaded) { + return; + } + + $loader = $this->getComposerLoader(); + if ($loader === null) { + $this->prefixesLoaded = true; + + return; + } + + $prefixes = $loader->getPrefixesPsr4(); + + foreach ($prefixes as $namespace => $paths) { + foreach ($paths as $path) { + $realPath = realpath($path); + if ($realPath === false) { + continue; + } + + if (DIRECTORY_SEPARATOR !== '/') { + $realPath = str_replace(DIRECTORY_SEPARATOR, '/', $realPath); + } + + $this->normalizedPrefixes[$namespace][] = rtrim($realPath, '/').'/'; + } + } + + $this->prefixesLoaded = true; + } + + /** + * Resolve class name from real file path using a subset of prefixes. + * + * @param string $realPath Normalized real path of the file + * @param array> $prefixes Subset of prefixes to check + */ + protected function resolveClass(string $realPath, array $prefixes): ?string + { + foreach ($prefixes as $namespace => $paths) { + foreach ($paths as $path) { + if (str_starts_with($realPath, $path)) { + // Found matching prefix + $relativePath = substr($realPath, strlen($path)); + + // Convert path to namespace + $relativeClass = str_replace(['/', '.php'], ['\\', ''], $relativePath); + + return $namespace.$relativeClass; + } + } + } + + return null; + } + /** * Extract class name from a PHP file. */ From 8da0951c9d6010346377c4f8ed9c39ca5f7fd2a4 Mon Sep 17 00:00:00 2001 From: Snider <631881+Snider@users.noreply.github.com> Date: Mon, 2 Feb 2026 02:04:16 +0000 Subject: [PATCH 2/3] Fix stderr redirection in CI workflow Removes `2>&1` redirection from Psalm, PHPStan, Pint, and Composer Audit commands in `.github/workflows/qa.yml`. This prevents stderr output (such as progress bars or debug messages) from corrupting the JSON/SARIF output files, which caused the CI pipeline to fail with JSON syntax errors. --- .github/workflows/qa.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 0d3ff49..0db4786 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -130,11 +130,11 @@ jobs: id: stan run: | set +e - vendor/bin/phpstan analyse --error-format=json --no-progress > phpstan.json 2>&1 + vendor/bin/phpstan analyse --error-format=json --no-progress > phpstan.json EXIT_CODE=$? # Also run for GitHub format - vendor/bin/phpstan analyse --error-format=github --no-progress 2>&1 || true + vendor/bin/phpstan analyse --error-format=github --no-progress || true ERRORS=$(jq '.totals.file_errors // 0' phpstan.json 2>/dev/null || echo "0") echo "errors=${ERRORS}" >> $GITHUB_OUTPUT @@ -181,11 +181,11 @@ jobs: id: psalm run: | set +e - vendor/bin/psalm --output-format=json --show-info=false > psalm.json 2>&1 + vendor/bin/psalm --output-format=json --show-info=false > psalm.json EXIT_CODE=$? # Generate SARIF for GitHub Security - vendor/bin/psalm --output-format=sarif --show-info=false > psalm.sarif 2>&1 || true + vendor/bin/psalm --output-format=sarif --show-info=false > psalm.sarif || true ERRORS=$(jq 'length' psalm.json 2>/dev/null || echo "0") echo "errors=${ERRORS}" >> $GITHUB_OUTPUT @@ -242,7 +242,7 @@ jobs: id: fmt run: | set +e - OUTPUT=$(vendor/bin/pint --test --format=json 2>&1) + OUTPUT=$(vendor/bin/pint --test --format=json) EXIT_CODE=$? echo "$OUTPUT" > pint.json @@ -292,7 +292,7 @@ jobs: id: audit run: | set +e - composer audit --format=json > audit.json 2>&1 + composer audit --format=json > audit.json EXIT_CODE=$? VULNS=$(jq '.advisories | to_entries | map(.value | length) | add // 0' audit.json 2>/dev/null || echo "0") From 20d9274461abebd601cecc2687309fc8da80c895 Mon Sep 17 00:00:00 2001 From: Snider <631881+Snider@users.noreply.github.com> Date: Mon, 2 Feb 2026 02:09:19 +0000 Subject: [PATCH 3/3] Fix Psalm SARIF output validation in CI Sanitizes Psalm's SARIF output in `.github/workflows/qa.yml` using `jq` to ensure all physical location regions (lines/columns) have values of at least 1. This prevents GitHub Code Scanning from rejecting the report due to invalid 0 values. --- .github/workflows/qa.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/qa.yml b/.github/workflows/qa.yml index 0db4786..578882e 100644 --- a/.github/workflows/qa.yml +++ b/.github/workflows/qa.yml @@ -185,7 +185,14 @@ jobs: EXIT_CODE=$? # Generate SARIF for GitHub Security - vendor/bin/psalm --output-format=sarif --show-info=false > psalm.sarif || true + vendor/bin/psalm --output-format=sarif --show-info=false > psalm_raw.sarif || true + + # Sanitize SARIF: Ensure all region values are >= 1 + if [ -s psalm_raw.sarif ]; then + jq 'walk(if type == "object" and has("region") then .region |= with_entries(.value = if .value < 1 then 1 else .value end) else . end)' psalm_raw.sarif > psalm.sarif + else + touch psalm.sarif + fi ERRORS=$(jq 'length' psalm.json 2>/dev/null || echo "0") echo "errors=${ERRORS}" >> $GITHUB_OUTPUT