From 9bc637808f2df539af24886d795aafe31efd5354 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 10:45:39 +0000 Subject: [PATCH 1/5] Initial plan From 4839082f6a7df312aacf1cc7edc414a57f8e26e7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:18:33 +0000 Subject: [PATCH 2/5] feat: implement PSR-4 compatible cache structure for proxy files - WeavingTransformer.saveProxyToCache() now generates proxy files at PSR-4 paths (/.php) instead of the old convoluted /_proxies//.php layout - Each generated proxy file includes a MagicConstantTransformer::registerProxyFile() call at the end so that wrapped ReflectionClass::getFileName() calls in woven sources correctly resolve back to the original source path at runtime - MagicConstantTransformer gains a static $proxyFileMap registry and registerProxyFile() / resolveFileName() updated to use it with portable relative paths - DebugWeavingCommand.getProxies() now scans the full cacheDir instead of the removed _proxies/ subdirectory, filtered by \Go\Aop\Proxy presence - Update woven snapshot files (*-woven.php) with new include_once paths - Update proxy snapshot files (*-proxy.php) with registerProxyFile() footer - ClassWovenConstraint, ClassIsNotWovenConstraint, ProxyClassReflectionHelper already updated to use PSR-4 paths in earlier commits Agent-Logs-Url: https://github.com/goaop/framework/sessions/0600c407-30da-4f37-81df-a43730e6099d Co-authored-by: lisachenko <640114+lisachenko@users.noreply.github.com> --- src/Console/Command/DebugWeavingCommand.php | 19 ++++++++-- .../Transformer/MagicConstantTransformer.php | 37 +++++++++++++++++-- .../Transformer/WeavingTransformer.php | 36 +++++++++++++----- .../Transformer/WeavingTransformerTest.php | 7 +++- .../Transformer/_files/class-proxy.php | 1 + .../_files/class-typehint-woven.php | 2 +- .../Transformer/_files/class-woven.php | 2 +- .../_files/final-readonly-class-proxy.php | 1 + .../_files/final-readonly-class-woven.php | 2 +- .../_files/multiple-classes-woven.php | 6 +-- .../Transformer/_files/multiple-ns-woven.php | 4 +- .../Transformer/_files/php7-class-proxy.php | 1 + .../Transformer/_files/php7-class-woven.php | 2 +- .../Transformer/_files/php81-enum-proxy.php | 1 + .../Transformer/_files/php81-enum-woven.php | 2 +- .../_files/php83-override-proxy.php | 1 + .../_files/php83-override-woven.php | 2 +- tests/PhpUnit/ClassIsNotWovenConstraint.php | 5 ++- tests/PhpUnit/ClassWovenConstraint.php | 5 ++- tests/PhpUnit/ProxyClassReflectionHelper.php | 19 +++------- 20 files changed, 108 insertions(+), 47 deletions(-) diff --git a/src/Console/Command/DebugWeavingCommand.php b/src/Console/Command/DebugWeavingCommand.php index 5dffef51..549ec455 100644 --- a/src/Console/Command/DebugWeavingCommand.php +++ b/src/Console/Command/DebugWeavingCommand.php @@ -99,15 +99,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int /** * Gets Go! AOP generated proxy classes (paths and their contents) from the cache. + * Proxy files are identified by the presence of `implements \Go\Aop\Proxy` in their content + * (covers both class and enum proxies). Woven trait files and function proxies are excluded. * * @return array */ private function getProxies(CachePathManager $cachePathManager): array { - $path = $cachePathManager->getCacheDir() . '/_proxies'; + $cacheDir = $cachePathManager->getCacheDir(); + if ($cacheDir === null || !is_dir($cacheDir)) { + return []; + } + $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator( - $path, + $cacheDir, FilesystemIterator::SKIP_DOTS | FilesystemIterator::UNIX_PATHS ), RecursiveIteratorIterator::CHILD_FIRST @@ -119,9 +125,14 @@ private function getProxies(CachePathManager $cachePathManager): array * @var SplFileInfo $splFileInfo */ foreach ($iterator as $splFileInfo) { - if ($splFileInfo->isFile()) { + if ($splFileInfo->isFile() && $splFileInfo->getExtension() === 'php') { $content = file_get_contents($splFileInfo->getPathname()); - if ($content !== false) { + // Only include files that implement \Go\Aop\Proxy (class/enum proxies). + // We look for the FQCN anywhere in the file to handle the case where the proxy + // class implements additional interfaces before \Go\Aop\Proxy in the list. + // Woven trait files, function proxies, and the transformation cache file never + // reference \Go\Aop\Proxy, so this filter is precise. + if ($content !== false && str_contains($content, '\Go\Aop\Proxy')) { $proxies[$splFileInfo->getPathname()] = $content; } } diff --git a/src/Instrument/Transformer/MagicConstantTransformer.php b/src/Instrument/Transformer/MagicConstantTransformer.php index a107a85e..dd5a1ab9 100644 --- a/src/Instrument/Transformer/MagicConstantTransformer.php +++ b/src/Instrument/Transformer/MagicConstantTransformer.php @@ -39,6 +39,14 @@ class MagicConstantTransformer extends BaseSourceTransformer */ protected static string $rewriteToPath = ''; + /** + * Registry that maps PSR-4 proxy file paths to their original source file paths. + * Populated at runtime via registerProxyFile() calls embedded in each proxy file header. + * + * @var array + */ + private static array $proxyFileMap = []; + /** * Class constructor */ @@ -49,6 +57,19 @@ public function __construct(AspectKernel $kernel) self::$rewriteToPath = $this->options['cacheDir'] ?? ''; } + /** + * Registers the mapping from a PSR-4 proxy file path to its original source file path + * (expressed as a path relative to the application root directory). + * This is called from the header of each generated proxy file when it is first included. + * + * @param string $proxyPath Absolute path of the proxy file (provided via __FILE__) + * @param string $relativeSourcePath Path to the original source file relative to {@see $rootPath} + */ + public static function registerProxyFile(string $proxyPath, string $relativeSourcePath): void + { + self::$proxyFileMap[$proxyPath] = $relativeSourcePath; + } + /** * This method may transform the supplied source and return a new replacement for it */ @@ -62,17 +83,25 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum } /** - * Resolves file name from the cache directory to the real application root dir + * Resolves file name from the cache directory to the real application root dir. + * For PSR-4 proxy files the mapping is looked up in the runtime registry populated + * by {@see registerProxyFile()} calls embedded in the generated proxy file headers. */ public static function resolveFileName(string $fileName): string { + // Fast path: PSR-4 proxy files register themselves on first include. + // The map stores relative paths, so we reconstruct the absolute source path. + if (isset(self::$proxyFileMap[$fileName])) { + return rtrim(self::$rootPath, '/\\') . DIRECTORY_SEPARATOR . self::$proxyFileMap[$fileName]; + } + $suffix = '.php'; $pathParts = explode($suffix, str_replace( - [self::$rewriteToPath, DIRECTORY_SEPARATOR . '_proxies'], - [self::$rootPath, ''], + self::$rewriteToPath, + self::$rootPath, $fileName )); - // throw away namespaced path from actual filename + // throw away any trailing path after the first .php suffix return $pathParts[0] . $suffix; } diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index 6373c858..3178b897 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -40,7 +40,6 @@ class WeavingTransformer extends BaseSourceTransformer { private const FUNCTIONS_CACHE_SUFFIX = '/_functions/'; - private const PROXIES_CACHE_SUFFIX = '/_proxies/'; /** * Advice matcher for class @@ -704,35 +703,52 @@ private function processFunctions( } /** - * Save AOP proxy to the separate file anr returns the php source code for inclusion + * Save AOP proxy to the separate file and returns the php source code for inclusion. + * Proxy files are stored in a PSR-4 compatible layout under the cache root directory: + * /.php + * + * Each proxy file header contains a {@see MagicConstantTransformer::registerProxyFile()} call + * so that {@see MagicConstantTransformer::resolveFileName()} can map the PSR-4 proxy path back + * to the original source file at runtime (needed for wrapped ReflectionClass::getFileName() calls). */ private function saveProxyToCache(ReflectionClass $class, string $childCode): string { - $cacheRootDir = $this->cachePathManager->getCacheDir(); + $cacheRootDir = $this->cachePathManager->getCacheDir(); if ($cacheRootDir === null) { return ''; } - $cacheDir = $cacheRootDir . self::PROXIES_CACHE_SUFFIX; - $classFileName = $class->getFileName(); + + $classFileName = $class->getFileName(); if ($classFileName === false) { return ''; } - $relativePath = str_replace($this->options['appDir'] . DIRECTORY_SEPARATOR, '', $classFileName); - $proxyRelativePath = str_replace('\\', '/', $relativePath . '/' . $class->getName() . '.php'); - $proxyFileName = $cacheDir . $proxyRelativePath; + + // Build a PSR-4 compatible relative path from the class FQCN, e.g. "Ns/Sub/ClassName.php" + $proxyRelativePath = str_replace('\\', '/', $class->getName()) . '.php'; + $proxyFileName = $cacheRootDir . '/' . $proxyRelativePath; $dirname = dirname($proxyFileName); if (!file_exists($dirname)) { mkdir($dirname, $this->options['cacheFileMode'], true); } - $body = 'options['appDir'], '/\\') . DIRECTORY_SEPARATOR; + $relativeSourcePath = str_replace($appDir, '', $classFileName); + + // Append the registerProxyFile() call at the END of the proxy file. + // This is valid PHP regardless of namespace and ensures the proxy path → source path + // mapping is registered the moment the proxy file is first included, before control + // returns to the caller (the include_once statement in the woven file). + $registerCall = '\\' . MagicConstantTransformer::class . '::registerProxyFile(__FILE__, ' . var_export($relativeSourcePath, true) . ');'; + $body = 'options['cacheFileMode'] & (~0111)); - return 'include_once AOP_CACHE_DIR . ' . var_export(self::PROXIES_CACHE_SUFFIX . $proxyRelativePath, true) . ';'; + return 'include_once AOP_CACHE_DIR . ' . var_export('/' . $proxyRelativePath, true) . ';'; } /** diff --git a/tests/Instrument/Transformer/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index 15b63f2d..db38f1a7 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -128,8 +128,11 @@ public function testWeaverForTypeHint(): void $expected = $this->normalizeWhitespaces($this->loadTestMetadata('class-typehint-woven')->source); $this->assertEquals($expected, $actual); - $proxyContent = file_get_contents($this->cachePathManager->getCacheDir() . '_proxies/Transformer/_files/class-typehint.php/TestClassTypehint.php'); - $this->assertFalse(strpos($proxyContent, '\\\\Exception')); + if (preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)) { + $proxyContent = file_get_contents('vfs://' . $matches[1]); + $this->assertNotFalse($proxyContent, 'Proxy file should exist at PSR-4 path'); + $this->assertFalse(strpos($proxyContent, '\\\\Exception')); + } } /** diff --git a/tests/Instrument/Transformer/_files/class-proxy.php b/tests/Instrument/Transformer/_files/class-proxy.php index 4f3d06a9..729c13e5 100644 --- a/tests/Instrument/Transformer/_files/class-proxy.php +++ b/tests/Instrument/Transformer/_files/class-proxy.php @@ -58,3 +58,4 @@ public function methodWithSpecialTypeArguments(self $instance) return $__joinPoint->__invoke($this, [$instance]); } } +\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/class.php'); diff --git a/tests/Instrument/Transformer/_files/class-typehint-woven.php b/tests/Instrument/Transformer/_files/class-typehint-woven.php index f84f6da0..3b053472 100644 --- a/tests/Instrument/Transformer/_files/class-typehint-woven.php +++ b/tests/Instrument/Transformer/_files/class-typehint-woven.php @@ -5,4 +5,4 @@ trait TestClassTypehint__AopProxied { public function publicMethodFixedArguments(Exception $a, $b, $c = null) {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/class-typehint.php/TestClassTypehint.php'; +include_once AOP_CACHE_DIR . '/TestClassTypehint.php'; diff --git a/tests/Instrument/Transformer/_files/class-woven.php b/tests/Instrument/Transformer/_files/class-woven.php index 7d0c5979..749b1ffe 100644 --- a/tests/Instrument/Transformer/_files/class-woven.php +++ b/tests/Instrument/Transformer/_files/class-woven.php @@ -22,4 +22,4 @@ public function publicMethodFixedArguments($a, $b, $c = null) {} public function methodWithSpecialTypeArguments(self $instance) {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/class.php/Test/ns1/TestClass.php'; +include_once AOP_CACHE_DIR . '/Test/ns1/TestClass.php'; diff --git a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php index 1495118c..062620f7 100644 --- a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php +++ b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php @@ -30,3 +30,4 @@ public static function staticMethod(): string return $__joinPoint->__invoke(static::class); } } +\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/final-readonly-class.php'); diff --git a/tests/Instrument/Transformer/_files/final-readonly-class-woven.php b/tests/Instrument/Transformer/_files/final-readonly-class-woven.php index 5c4a93d6..ceb26d2f 100644 --- a/tests/Instrument/Transformer/_files/final-readonly-class-woven.php +++ b/tests/Instrument/Transformer/_files/final-readonly-class-woven.php @@ -19,4 +19,4 @@ public static function staticMethod(): string return static::class; } } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/final-readonly-class.php/Test/ns1/TestReadonlyClass.php'; +include_once AOP_CACHE_DIR . '/Test/ns1/TestReadonlyClass.php'; diff --git a/tests/Instrument/Transformer/_files/multiple-classes-woven.php b/tests/Instrument/Transformer/_files/multiple-classes-woven.php index 9bbfc806..ed9b4323 100644 --- a/tests/Instrument/Transformer/_files/multiple-classes-woven.php +++ b/tests/Instrument/Transformer/_files/multiple-classes-woven.php @@ -5,15 +5,15 @@ trait TestClass1__AopProxied { public static function test() {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/multiple-classes.php/Test/ns3/TestClass1.php'; +include_once AOP_CACHE_DIR . '/Test/ns3/TestClass1.php'; TestClass1::test(); trait TestClass11__AopProxied { public static function test() {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/multiple-classes.php/Test/ns3/TestClass11.php'; +include_once AOP_CACHE_DIR . '/Test/ns3/TestClass11.php'; TestClass11::test(); trait TestClass2__AopProxied { public static function test() {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/multiple-classes.php/Test/ns3/TestClass2.php'; +include_once AOP_CACHE_DIR . '/Test/ns3/TestClass2.php'; TestClass2::test(); diff --git a/tests/Instrument/Transformer/_files/multiple-ns-woven.php b/tests/Instrument/Transformer/_files/multiple-ns-woven.php index ce35abd7..907218ee 100644 --- a/tests/Instrument/Transformer/_files/multiple-ns-woven.php +++ b/tests/Instrument/Transformer/_files/multiple-ns-woven.php @@ -4,11 +4,11 @@ trait TestClass1__AopProxied { public static function test() {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/multiple-ns.php/Test/ns1/TestClass1.php'; +include_once AOP_CACHE_DIR . '/Test/ns1/TestClass1.php'; } namespace Test\ns2 { trait TestClass2__AopProxied { public static function test() {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/multiple-ns.php/Test/ns2/TestClass2.php'; +include_once AOP_CACHE_DIR . '/Test/ns2/TestClass2.php'; } diff --git a/tests/Instrument/Transformer/_files/php7-class-proxy.php b/tests/Instrument/Transformer/_files/php7-class-proxy.php index 15d9b5b7..a02b3aba 100644 --- a/tests/Instrument/Transformer/_files/php7-class-proxy.php +++ b/tests/Instrument/Transformer/_files/php7-class-proxy.php @@ -127,3 +127,4 @@ public function returnSelf(): self return $__joinPoint->__invoke($this); } } +\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/php7-class.php'); diff --git a/tests/Instrument/Transformer/_files/php7-class-woven.php b/tests/Instrument/Transformer/_files/php7-class-woven.php index 3834b448..7f110572 100644 --- a/tests/Instrument/Transformer/_files/php7-class-woven.php +++ b/tests/Instrument/Transformer/_files/php7-class-woven.php @@ -21,4 +21,4 @@ public function exceptionRth(\Exception $exception) : \Exception {} public function noRth(LocalException $exception) {} public function returnSelf(): self {} } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/php7-class.php/Test/ns1/TestPhp7Class.php'; +include_once AOP_CACHE_DIR . '/Test/ns1/TestPhp7Class.php'; diff --git a/tests/Instrument/Transformer/_files/php81-enum-proxy.php b/tests/Instrument/Transformer/_files/php81-enum-proxy.php index a34b9361..8bddfa5c 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-proxy.php @@ -17,3 +17,4 @@ public function label(): string return $__joinPoint->__invoke($this); } } +\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/php81-enum.php'); diff --git a/tests/Instrument/Transformer/_files/php81-enum-woven.php b/tests/Instrument/Transformer/_files/php81-enum-woven.php index 1789184b..4d369b16 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-woven.php +++ b/tests/Instrument/Transformer/_files/php81-enum-woven.php @@ -18,4 +18,4 @@ public function label(): string }; } } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/php81-enum.php/Test/ns1/TestStatus.php'; +include_once AOP_CACHE_DIR . '/Test/ns1/TestStatus.php'; diff --git a/tests/Instrument/Transformer/_files/php83-override-proxy.php b/tests/Instrument/Transformer/_files/php83-override-proxy.php index 2644de5c..1e06c886 100644 --- a/tests/Instrument/Transformer/_files/php83-override-proxy.php +++ b/tests/Instrument/Transformer/_files/php83-override-proxy.php @@ -28,3 +28,4 @@ public function normalMethod(): int return $__joinPoint->__invoke($this); } } +\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/php83-override.php'); diff --git a/tests/Instrument/Transformer/_files/php83-override-woven.php b/tests/Instrument/Transformer/_files/php83-override-woven.php index d02d7980..1f546359 100644 --- a/tests/Instrument/Transformer/_files/php83-override-woven.php +++ b/tests/Instrument/Transformer/_files/php83-override-woven.php @@ -19,4 +19,4 @@ public function normalMethod(): int return 42; } } -include_once AOP_CACHE_DIR . '/_proxies/Transformer/_files/php83-override.php/Test/ns1/TestClassWithOverride.php'; +include_once AOP_CACHE_DIR . '/Test/ns1/TestClassWithOverride.php'; diff --git a/tests/PhpUnit/ClassIsNotWovenConstraint.php b/tests/PhpUnit/ClassIsNotWovenConstraint.php index 4bb5bad5..8c69832f 100644 --- a/tests/PhpUnit/ClassIsNotWovenConstraint.php +++ b/tests/PhpUnit/ClassIsNotWovenConstraint.php @@ -37,7 +37,10 @@ public function matches($other): bool $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); - $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix); + + // Proxy files use a PSR-4 layout: /.php + $proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php'; + $proxyFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $proxyRelativePath); // if any of files exists, assert has to fail return !$transformedFileExists && !$proxyFileExists; diff --git a/tests/PhpUnit/ClassWovenConstraint.php b/tests/PhpUnit/ClassWovenConstraint.php index 6c393c98..5f974ce9 100644 --- a/tests/PhpUnit/ClassWovenConstraint.php +++ b/tests/PhpUnit/ClassWovenConstraint.php @@ -37,7 +37,10 @@ public function matches($other): bool $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); - $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix); + + // Proxy files use a PSR-4 layout: /.php + $proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php'; + $proxyFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $proxyRelativePath); // if any of files is missing, assert has to fail return $transformedFileExists && $proxyFileExists; diff --git a/tests/PhpUnit/ProxyClassReflectionHelper.php b/tests/PhpUnit/ProxyClassReflectionHelper.php index 89b74440..db14ebb1 100644 --- a/tests/PhpUnit/ProxyClassReflectionHelper.php +++ b/tests/PhpUnit/ProxyClassReflectionHelper.php @@ -12,7 +12,6 @@ namespace Go\PhpUnit; -use Go\Instrument\PathResolver; use Go\ParserReflection\ReflectionClass; use Go\ParserReflection\ReflectionEngine; use Go\ParserReflection\ReflectionFile; @@ -45,14 +44,9 @@ private function __construct() */ public static function extractAdvicesFromProxyFile(string $className, array $configuration): array { - $parsedReflectionClass = new ReflectionClass($className); - $originalClassFile = $parsedReflectionClass->getFileName(); - - $appDir = PathResolver::realpath($configuration['appDir']); - $relativePath = str_replace($appDir . DIRECTORY_SEPARATOR, '', $originalClassFile); - $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; - $proxyRelativePath = $relativePath . DIRECTORY_SEPARATOR . $classSuffix; - $proxyFileName = $configuration['cacheDir'] . '/_proxies/' . $proxyRelativePath; + // Proxy files use a PSR-4 layout: /.php + $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; + $proxyFileName = $configuration['cacheDir'] . DIRECTORY_SEPARATOR . $classSuffix; if (!file_exists($proxyFileName)) { return []; @@ -209,14 +203,11 @@ private static function extractAdvicesFromInjectorCalls(array $injectorCalls): a public static function createReflectionClass(string $className, array $configuration): ReflectionClass { $parsedReflectionClass = new ReflectionClass($className); - $originalClassFile = $parsedReflectionClass->getFileName(); $originalNamespace = $parsedReflectionClass->getNamespaceName(); - $appDir = PathResolver::realpath($configuration['appDir']); - $relativePath = str_replace($appDir . DIRECTORY_SEPARATOR, '', $originalClassFile); + // Proxy files use a PSR-4 layout: /.php $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; - $proxyRelativePath = $relativePath . DIRECTORY_SEPARATOR . $classSuffix; - $proxyFileName = $configuration['cacheDir'] . '/_proxies/' . $proxyRelativePath; + $proxyFileName = $configuration['cacheDir'] . DIRECTORY_SEPARATOR . $classSuffix; $proxyFileContent = file_get_contents($proxyFileName); // To prevent deep analysis of parents, we just cut everything after "extends" From 7bbb64bb93a2221dfe194315eb812bedacd719f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:21:18 +0000 Subject: [PATCH 3/5] refactor: fix path normalization and regex precision per code review - Normalize both appDir and classFileName to forward slashes in WeavingTransformer::saveProxyToCache() for cross-platform correctness - Normalize rootPath to forward slashes in MagicConstantTransformer::resolveFileName() - Escape literal dot in AOP_CACHE_DIR regex patterns in WeavingTransformerTest Agent-Logs-Url: https://github.com/goaop/framework/sessions/0600c407-30da-4f37-81df-a43730e6099d Co-authored-by: lisachenko <640114+lisachenko@users.noreply.github.com> --- .../Transformer/MagicConstantTransformer.php | 5 +++-- src/Instrument/Transformer/WeavingTransformer.php | 6 ++++-- .../Transformer/WeavingTransformerTest.php | 14 +++++++------- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Instrument/Transformer/MagicConstantTransformer.php b/src/Instrument/Transformer/MagicConstantTransformer.php index dd5a1ab9..3ad4938b 100644 --- a/src/Instrument/Transformer/MagicConstantTransformer.php +++ b/src/Instrument/Transformer/MagicConstantTransformer.php @@ -90,9 +90,10 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum public static function resolveFileName(string $fileName): string { // Fast path: PSR-4 proxy files register themselves on first include. - // The map stores relative paths, so we reconstruct the absolute source path. + // The map stores relative paths (always forward slashes). We normalize $rootPath to + // forward slashes too so the returned path is consistent on all platforms. if (isset(self::$proxyFileMap[$fileName])) { - return rtrim(self::$rootPath, '/\\') . DIRECTORY_SEPARATOR . self::$proxyFileMap[$fileName]; + return rtrim(str_replace('\\', '/', self::$rootPath), '/') . '/' . self::$proxyFileMap[$fileName]; } $suffix = '.php'; diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index 3178b897..b097b921 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -733,8 +733,10 @@ private function saveProxyToCache(ReflectionClass $class, string $childCode): st // Compute the source file path relative to appDir so the registration call is portable // across different environments (avoids embedding absolute paths in the proxy file). - $appDir = rtrim($this->options['appDir'], '/\\') . DIRECTORY_SEPARATOR; - $relativeSourcePath = str_replace($appDir, '', $classFileName); + // Normalize both paths to forward slashes before comparison so the result is correct + // on all platforms (Windows PHP may return backslash paths from getFileName()). + $appDirNormalized = rtrim(str_replace('\\', '/', $this->options['appDir']), '/') . '/'; + $relativeSourcePath = str_replace($appDirNormalized, '', str_replace('\\', '/', $classFileName)); // Append the registerProxyFile() call at the END of the proxy file. // This is valid PHP regardless of namespace and ensures the proxy path → source path diff --git a/tests/Instrument/Transformer/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index db38f1a7..64b9bc5b 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -128,7 +128,7 @@ public function testWeaverForTypeHint(): void $expected = $this->normalizeWhitespaces($this->loadTestMetadata('class-typehint-woven')->source); $this->assertEquals($expected, $actual); - if (preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)) { + if (preg_match("/AOP_CACHE_DIR \. '(.+)';$/m", $actual, $matches)) { $proxyContent = file_get_contents('vfs://' . $matches[1]); $this->assertNotFalse($proxyContent, 'Proxy file should exist at PSR-4 path'); $this->assertFalse(strpos($proxyContent, '\\\\Exception')); @@ -146,7 +146,7 @@ public function testWeaverForPhp7Class(): void $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTestMetadata('php7-class-woven')->source); $this->assertEquals($expected, $actual); - if (preg_match("/AOP_CACHE_DIR . '(.+)';$/", $actual, $matches)) { + if (preg_match("/AOP_CACHE_DIR \. '(.+)';$/", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTestMetadata('php7-class-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); @@ -189,7 +189,7 @@ public function testTransformerWithIncludePaths(): void $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTestMetadata('class-woven')->source); $this->assertEquals($expected, $actual); - if (preg_match("/AOP_CACHE_DIR . '(.+)';$/", $actual, $matches)) { + if (preg_match("/AOP_CACHE_DIR \. '(.+)';$/", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTestMetadata('class-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); @@ -211,7 +211,7 @@ public function testWeaverForFinalReadonlyClass(): void $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTestMetadata('final-readonly-class-woven')->source); $this->assertEquals($expected, $actual); - if (preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)) { + if (preg_match("/AOP_CACHE_DIR \. '(.+)';$/m", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTestMetadata('final-readonly-class-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); @@ -229,7 +229,7 @@ public function testWeaverForEnum(): void $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTestMetadata('php81-enum-woven')->source); $this->assertEquals($expected, $actual); - if (preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)) { + if (preg_match("/AOP_CACHE_DIR \. '(.+)';$/m", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTestMetadata('php81-enum-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); @@ -288,7 +288,7 @@ public function testWeaverStripsOverrideAttributeFromInterceptedMethods(): void $actual = $this->normalizeWhitespaces($metadata->source); $expected = $this->normalizeWhitespaces($this->loadTestMetadata('php83-override-woven')->source); $this->assertEquals($expected, $actual); - if (preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)) { + if (preg_match("/AOP_CACHE_DIR \. '(.+)';$/m", $actual, $matches)) { $actualProxyContent = $this->normalizeWhitespaces(file_get_contents('vfs://' . $matches[1])); $expectedProxyContent = $this->normalizeWhitespaces($this->loadTestMetadata('php83-override-proxy')->source); $this->assertEquals($expectedProxyContent, $actualProxyContent); @@ -356,7 +356,7 @@ public function testWeaverMovesInterceptedPropertiesToProxyHooks(): void $this->assertStringContainsString("public string \$plain = 'plain';", $actualWoven); $matches = []; - $this->assertSame(1, preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actualWoven, $matches)); + $this->assertSame(1, preg_match("/AOP_CACHE_DIR \. '(.+)';$/m", $actualWoven, $matches)); $proxyContent = $this->normalizeWhitespaces((string) file_get_contents('vfs://' . $matches[1])); $this->assertStringContainsString("public string \$value = 'test' {", $proxyContent); From f329ce8a8e89f3993e43d275f0626cdd376a6862 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 10:12:15 +0000 Subject: [PATCH 4/5] fix: store woven trait files at PSR-4 __AopProxied path to prevent collision When the PSR-4 namespace root coincides with appDir (e.g. demos where Demo\Example\CacheableDemo is at demos/Demo/Example/CacheableDemo.php), the woven file was stored at the same path as the proxy file, causing "Cannot redeclare trait" fatal errors. Fix: WeavingTransformer registers the PSR-4 __AopProxied path via CachePathManager.registerWovenFilePath(), and CachingTransformer uses that path when writing and reading the woven file. The stale check is also updated to detect moved cacheDir without false-positiving on the renamed path. Agent-Logs-Url: https://github.com/goaop/framework/sessions/762375e5-7940-43fd-930c-9bf97ff99bcb Co-authored-by: lisachenko <640114+lisachenko@users.noreply.github.com> --- .../ClassLoading/CachePathManager.php | 32 +++++++++++++++++ .../Transformer/CachingTransformer.php | 35 ++++++++++++++----- .../Transformer/WeavingTransformer.php | 10 ++++++ tests/PhpUnit/ClassIsNotWovenConstraint.php | 9 ++--- tests/PhpUnit/ClassWovenConstraint.php | 9 ++--- 5 files changed, 74 insertions(+), 21 deletions(-) diff --git a/src/Instrument/ClassLoading/CachePathManager.php b/src/Instrument/ClassLoading/CachePathManager.php index 20f50293..4e9c3cfd 100644 --- a/src/Instrument/ClassLoading/CachePathManager.php +++ b/src/Instrument/ClassLoading/CachePathManager.php @@ -62,6 +62,16 @@ class CachePathManager */ protected array $newCacheState = []; + /** + * Per-request overrides for the woven (trait) file cache path, keyed by original source URI. + * Populated by WeavingTransformer so that the woven file is written to a PSR-4-compatible + * /.php path instead of the source-relative path, + * preventing collisions with the proxy class file when the namespace root equals appDir. + * + * @var array + */ + private array $wovenFilePathOverrides = []; + public function __construct(AspectKernel $kernel) { $this->kernel = $kernel; @@ -165,6 +175,28 @@ public function setCacheState(string $resource, array $metadata): void $this->newCacheState[$resource] = $metadata; } + /** + * Registers a PSR-4 woven file path for a given source URI. + * + * Called by {@see WeavingTransformer} after weaving a class so that + * {@see CachingTransformer} stores the trait (woven) file at the correct PSR-4 + * location (/.php) rather than the + * source-relative path, which would collide with the proxy class file when the + * PSR-4 namespace root coincides with appDir. + */ + public function registerWovenFilePath(string $originalUri, string $wovenPath): void + { + $this->wovenFilePathOverrides[$originalUri] = $wovenPath; + } + + /** + * Returns the registered PSR-4 woven file path for the given source URI, or null if none was set. + */ + public function getWovenFilePath(string $originalUri): ?string + { + return $this->wovenFilePathOverrides[$originalUri] ?? null; + } + /** * Automatic destructor saves all new changes into the cache * diff --git a/src/Instrument/Transformer/CachingTransformer.php b/src/Instrument/Transformer/CachingTransformer.php index 0990dff5..87cd15b0 100644 --- a/src/Instrument/Transformer/CachingTransformer.php +++ b/src/Instrument/Transformer/CachingTransformer.php @@ -66,30 +66,46 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum return TransformerResultEnum::RESULT_ABORTED; } - $lastModified = filemtime($originalUri); - $cacheState = $this->cacheManager->queryCacheState($originalUri); + $lastModified = filemtime($originalUri); + $cacheState = $this->cacheManager->queryCacheState($originalUri); $cacheFilemtime = $cacheState !== null ? ($cacheState['filemtime'] ?? 0) : 0; $cacheModified = is_int($cacheFilemtime) ? $cacheFilemtime : 0; + // The stored cacheUri may be a PSR-4 __AopProxied path (set by WeavingTransformer). + // Consider the cache stale only when the stored cacheUri belongs to a different cache + // directory (i.e. cacheDir was moved), not merely because it has a different file name. + $cacheDir = $this->cacheManager->getCacheDir() ?? ''; + $storedCacheUri = is_array($cacheState) && is_string($cacheState['cacheUri'] ?? null) + ? $cacheState['cacheUri'] + : null; + $cacheUriOutOfDate = $storedCacheUri !== null + && $cacheDir !== '' + && !str_starts_with($storedCacheUri, $cacheDir); + if ($cacheModified < $lastModified - || (isset($cacheState['cacheUri']) && $cacheState['cacheUri'] !== $cacheUri) + || $cacheUriOutOfDate || !$this->container->hasAnyResourceChangedSince($cacheModified) ) { $processingResult = $this->processTransformers($metadata); if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) { - $parentCacheDir = dirname($cacheUri); + // WeavingTransformer may have registered a PSR-4 path for the woven (trait) file. + // Use that when available to avoid collisions with the proxy class file. + $resolvedCacheUri = $this->cacheManager->getWovenFilePath($originalUri) ?? $cacheUri; + $parentCacheDir = dirname($resolvedCacheUri); if (!is_dir($parentCacheDir)) { mkdir($parentCacheDir, $this->cacheFileMode, true); } - file_put_contents($cacheUri, $metadata->source, LOCK_EX); + file_put_contents($resolvedCacheUri, $metadata->source, LOCK_EX); // For cache files we don't want executable bits by default - chmod($cacheUri, $this->cacheFileMode & (~0111)); + chmod($resolvedCacheUri, $this->cacheFileMode & (~0111)); + } else { + $resolvedCacheUri = $cacheUri; } $this->cacheManager->setCacheState( $originalUri, [ 'filemtime' => $_SERVER['REQUEST_TIME'] ?? time(), - 'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $cacheUri : null + 'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $resolvedCacheUri : null ] ); @@ -100,8 +116,9 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum $processingResult = isset($cacheState['cacheUri']) ? TransformerResultEnum::RESULT_TRANSFORMED : TransformerResultEnum::RESULT_ABORTED; } if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) { - // Just replace all tokens in the stream - ReflectionEngine::parseFile($cacheUri); + // Use the stored cache URI — it may be a PSR-4 __AopProxied path from a previous run. + $readUri = $storedCacheUri ?? $cacheUri; + ReflectionEngine::parseFile($readUri); $metadata->setTokenStreamFromRawTokens( ...ReflectionEngine::getParser()->getTokens() ); diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index b097b921..4ce647e3 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -180,6 +180,16 @@ private function processSingleClass( $contentToInclude = $this->saveProxyToCache($class, $childCode); + // Register the PSR-4 woven (trait) file path so CachingTransformer stores the woven + // content at /.php. Without this, the woven + // file would be stored at the source-relative path, which collides with the proxy class + // file when the PSR-4 namespace root coincides with appDir (e.g. in the demos). + $cacheRootDir = $this->cachePathManager->getCacheDir(); + if ($cacheRootDir !== null) { + $wovenRelPath = str_replace('\\', '/', $newFqcn) . '.php'; + $this->cachePathManager->registerWovenFilePath($metadata->uri, $cacheRootDir . '/' . $wovenRelPath); + } + // Get last token for this class $classNode = $class->getNode(); $lastClassToken = $classNode->getAttribute('endTokenPos'); diff --git a/tests/PhpUnit/ClassIsNotWovenConstraint.php b/tests/PhpUnit/ClassIsNotWovenConstraint.php index 8c69832f..f1f0717a 100644 --- a/tests/PhpUnit/ClassIsNotWovenConstraint.php +++ b/tests/PhpUnit/ClassIsNotWovenConstraint.php @@ -12,8 +12,6 @@ namespace Go\PhpUnit; -use Go\Instrument\PathResolver; -use Go\ParserReflection\ReflectionClass; use PHPUnit\Framework\Constraint\Constraint; /** @@ -33,10 +31,9 @@ public function __construct(array $configuration) */ public function matches($other): bool { - $filename = (new ReflectionClass($other))->getFileName(); - $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); - - $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); + // Woven trait file uses a PSR-4 layout: /.php + $wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php'; + $transformedFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $wovenRelativePath); // Proxy files use a PSR-4 layout: /.php $proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php'; diff --git a/tests/PhpUnit/ClassWovenConstraint.php b/tests/PhpUnit/ClassWovenConstraint.php index 5f974ce9..9cba006c 100644 --- a/tests/PhpUnit/ClassWovenConstraint.php +++ b/tests/PhpUnit/ClassWovenConstraint.php @@ -12,8 +12,6 @@ namespace Go\PhpUnit; -use Go\Instrument\PathResolver; -use Go\ParserReflection\ReflectionClass; use PHPUnit\Framework\Constraint\Constraint; /** @@ -33,10 +31,9 @@ public function __construct(array $configuration) */ public function matches($other): bool { - $filename = (new ReflectionClass($other))->getFileName(); - $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); - - $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); + // Woven trait file uses a PSR-4 layout: /.php + $wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php'; + $transformedFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $wovenRelativePath); // Proxy files use a PSR-4 layout: /.php $proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php'; From 3533c99144d44d0dd1f2aa322addda84ad5a485c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 1 May 2026 16:07:51 +0000 Subject: [PATCH 5/5] Changes before error encountered Agent-Logs-Url: https://github.com/goaop/framework/sessions/97056ec6-4f06-45ec-890f-9a26c0013935 Co-authored-by: lisachenko <640114+lisachenko@users.noreply.github.com> --- src/Console/Command/DebugWeavingCommand.php | 11 +-- .../ClassLoading/AopComposerLoader.php | 15 ++++ .../ClassLoading/CachePathManager.php | 32 ------- .../Transformer/CachingTransformer.php | 35 +++----- .../Transformer/MagicConstantTransformer.php | 90 +++++++++++++------ .../Transformer/WeavingTransformer.php | 30 +------ src/Proxy/Generator/ClassGenerator.php | 26 ++++-- src/Proxy/Generator/EnumGenerator.php | 25 ++++-- .../Transformer/_files/class-proxy.php | 17 ++-- .../_files/final-readonly-class-proxy.php | 9 +- .../Transformer/_files/php7-class-proxy.php | 37 ++++---- .../Transformer/_files/php81-enum-proxy.php | 5 +- .../_files/php83-override-proxy.php | 7 +- tests/PhpUnit/ClassIsNotWovenConstraint.php | 38 ++++++-- tests/PhpUnit/ClassWovenConstraint.php | 53 +++++++++-- tests/PhpUnit/ProxyClassReflectionHelper.php | 6 +- 16 files changed, 253 insertions(+), 183 deletions(-) diff --git a/src/Console/Command/DebugWeavingCommand.php b/src/Console/Command/DebugWeavingCommand.php index 549ec455..8dd7d229 100644 --- a/src/Console/Command/DebugWeavingCommand.php +++ b/src/Console/Command/DebugWeavingCommand.php @@ -125,14 +125,11 @@ private function getProxies(CachePathManager $cachePathManager): array * @var SplFileInfo $splFileInfo */ foreach ($iterator as $splFileInfo) { - if ($splFileInfo->isFile() && $splFileInfo->getExtension() === 'php') { + if ($splFileInfo->isFile()) { $content = file_get_contents($splFileInfo->getPathname()); - // Only include files that implement \Go\Aop\Proxy (class/enum proxies). - // We look for the FQCN anywhere in the file to handle the case where the proxy - // class implements additional interfaces before \Go\Aop\Proxy in the list. - // Woven trait files, function proxies, and the transformation cache file never - // reference \Go\Aop\Proxy, so this filter is precise. - if ($content !== false && str_contains($content, '\Go\Aop\Proxy')) { + // Only include files that implement Go\Aop\Proxy (class/enum proxies). + // Woven trait files and function proxies never reference Go\Aop\Proxy. + if ($content !== false && str_contains($content, 'Go\Aop\Proxy')) { $proxies[$splFileInfo->getPathname()] = $content; } } diff --git a/src/Instrument/ClassLoading/AopComposerLoader.php b/src/Instrument/ClassLoading/AopComposerLoader.php index 4b76d937..c95ca8af 100644 --- a/src/Instrument/ClassLoading/AopComposerLoader.php +++ b/src/Instrument/ClassLoading/AopComposerLoader.php @@ -67,6 +67,21 @@ class AopComposerLoader */ private bool $isProduction = false; + /** + * Returns the original (pre-AOP) Composer ClassLoader from the registered autoload stack, + * or null if AOP has not been initialised yet. + */ + public static function getOriginalClassLoader(): ?ClassLoader + { + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && isset($autoloader[0]) && $autoloader[0] instanceof self) { + return $autoloader[0]->original; + } + } + + return null; + } + /** * Constructs an wrapper for the composer loader * diff --git a/src/Instrument/ClassLoading/CachePathManager.php b/src/Instrument/ClassLoading/CachePathManager.php index 4e9c3cfd..20f50293 100644 --- a/src/Instrument/ClassLoading/CachePathManager.php +++ b/src/Instrument/ClassLoading/CachePathManager.php @@ -62,16 +62,6 @@ class CachePathManager */ protected array $newCacheState = []; - /** - * Per-request overrides for the woven (trait) file cache path, keyed by original source URI. - * Populated by WeavingTransformer so that the woven file is written to a PSR-4-compatible - * /.php path instead of the source-relative path, - * preventing collisions with the proxy class file when the namespace root equals appDir. - * - * @var array - */ - private array $wovenFilePathOverrides = []; - public function __construct(AspectKernel $kernel) { $this->kernel = $kernel; @@ -175,28 +165,6 @@ public function setCacheState(string $resource, array $metadata): void $this->newCacheState[$resource] = $metadata; } - /** - * Registers a PSR-4 woven file path for a given source URI. - * - * Called by {@see WeavingTransformer} after weaving a class so that - * {@see CachingTransformer} stores the trait (woven) file at the correct PSR-4 - * location (/.php) rather than the - * source-relative path, which would collide with the proxy class file when the - * PSR-4 namespace root coincides with appDir. - */ - public function registerWovenFilePath(string $originalUri, string $wovenPath): void - { - $this->wovenFilePathOverrides[$originalUri] = $wovenPath; - } - - /** - * Returns the registered PSR-4 woven file path for the given source URI, or null if none was set. - */ - public function getWovenFilePath(string $originalUri): ?string - { - return $this->wovenFilePathOverrides[$originalUri] ?? null; - } - /** * Automatic destructor saves all new changes into the cache * diff --git a/src/Instrument/Transformer/CachingTransformer.php b/src/Instrument/Transformer/CachingTransformer.php index 87cd15b0..47c0cfab 100644 --- a/src/Instrument/Transformer/CachingTransformer.php +++ b/src/Instrument/Transformer/CachingTransformer.php @@ -13,6 +13,7 @@ namespace Go\Instrument\Transformer; use Closure; +use Go\Core\AspectContainer; use Go\Core\AspectKernel; use Go\Instrument\ClassLoading\CachePathManager; use Go\ParserReflection\ReflectionEngine; @@ -66,46 +67,34 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum return TransformerResultEnum::RESULT_ABORTED; } + // Woven (trait) file is stored with the __AopProxied suffix before .php, + // mirroring the original directory structure under cacheDir. + $wovenCacheUri = substr($cacheUri, 0, -4) . AspectContainer::AOP_PROXIED_SUFFIX . '.php'; + $lastModified = filemtime($originalUri); $cacheState = $this->cacheManager->queryCacheState($originalUri); $cacheFilemtime = $cacheState !== null ? ($cacheState['filemtime'] ?? 0) : 0; $cacheModified = is_int($cacheFilemtime) ? $cacheFilemtime : 0; - // The stored cacheUri may be a PSR-4 __AopProxied path (set by WeavingTransformer). - // Consider the cache stale only when the stored cacheUri belongs to a different cache - // directory (i.e. cacheDir was moved), not merely because it has a different file name. - $cacheDir = $this->cacheManager->getCacheDir() ?? ''; - $storedCacheUri = is_array($cacheState) && is_string($cacheState['cacheUri'] ?? null) - ? $cacheState['cacheUri'] - : null; - $cacheUriOutOfDate = $storedCacheUri !== null - && $cacheDir !== '' - && !str_starts_with($storedCacheUri, $cacheDir); - if ($cacheModified < $lastModified - || $cacheUriOutOfDate + || (isset($cacheState['cacheUri']) && !file_exists($wovenCacheUri)) || !$this->container->hasAnyResourceChangedSince($cacheModified) ) { $processingResult = $this->processTransformers($metadata); if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) { - // WeavingTransformer may have registered a PSR-4 path for the woven (trait) file. - // Use that when available to avoid collisions with the proxy class file. - $resolvedCacheUri = $this->cacheManager->getWovenFilePath($originalUri) ?? $cacheUri; - $parentCacheDir = dirname($resolvedCacheUri); + $parentCacheDir = dirname($wovenCacheUri); if (!is_dir($parentCacheDir)) { mkdir($parentCacheDir, $this->cacheFileMode, true); } - file_put_contents($resolvedCacheUri, $metadata->source, LOCK_EX); + file_put_contents($wovenCacheUri, $metadata->source, LOCK_EX); // For cache files we don't want executable bits by default - chmod($resolvedCacheUri, $this->cacheFileMode & (~0111)); - } else { - $resolvedCacheUri = $cacheUri; + chmod($wovenCacheUri, $this->cacheFileMode & (~0111)); } $this->cacheManager->setCacheState( $originalUri, [ 'filemtime' => $_SERVER['REQUEST_TIME'] ?? time(), - 'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $resolvedCacheUri : null + 'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $wovenCacheUri : null ] ); @@ -116,9 +105,7 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum $processingResult = isset($cacheState['cacheUri']) ? TransformerResultEnum::RESULT_TRANSFORMED : TransformerResultEnum::RESULT_ABORTED; } if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) { - // Use the stored cache URI — it may be a PSR-4 __AopProxied path from a previous run. - $readUri = $storedCacheUri ?? $cacheUri; - ReflectionEngine::parseFile($readUri); + ReflectionEngine::parseFile($wovenCacheUri); $metadata->setTokenStreamFromRawTokens( ...ReflectionEngine::getParser()->getTokens() ); diff --git a/src/Instrument/Transformer/MagicConstantTransformer.php b/src/Instrument/Transformer/MagicConstantTransformer.php index 3ad4938b..19b8fdbd 100644 --- a/src/Instrument/Transformer/MagicConstantTransformer.php +++ b/src/Instrument/Transformer/MagicConstantTransformer.php @@ -12,7 +12,10 @@ namespace Go\Instrument\Transformer; +use Composer\Autoload\ClassLoader; +use Go\Core\AspectContainer; use Go\Core\AspectKernel; +use Go\Instrument\ClassLoading\AopComposerLoader; use PhpParser\Node; use PhpParser\Node\Expr\MethodCall; use PhpParser\Node\Scalar\MagicConst; @@ -40,12 +43,9 @@ class MagicConstantTransformer extends BaseSourceTransformer protected static string $rewriteToPath = ''; /** - * Registry that maps PSR-4 proxy file paths to their original source file paths. - * Populated at runtime via registerProxyFile() calls embedded in each proxy file header. - * - * @var array + * Cached Composer ClassLoader instance, used for resolving proxy file paths to original sources. */ - private static array $proxyFileMap = []; + private static ?ClassLoader $composerLoader = null; /** * Class constructor @@ -57,19 +57,6 @@ public function __construct(AspectKernel $kernel) self::$rewriteToPath = $this->options['cacheDir'] ?? ''; } - /** - * Registers the mapping from a PSR-4 proxy file path to its original source file path - * (expressed as a path relative to the application root directory). - * This is called from the header of each generated proxy file when it is first included. - * - * @param string $proxyPath Absolute path of the proxy file (provided via __FILE__) - * @param string $relativeSourcePath Path to the original source file relative to {@see $rootPath} - */ - public static function registerProxyFile(string $proxyPath, string $relativeSourcePath): void - { - self::$proxyFileMap[$proxyPath] = $relativeSourcePath; - } - /** * This method may transform the supplied source and return a new replacement for it */ @@ -84,26 +71,71 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum /** * Resolves file name from the cache directory to the real application root dir. - * For PSR-4 proxy files the mapping is looked up in the runtime registry populated - * by {@see registerProxyFile()} calls embedded in the generated proxy file headers. + * + * Two cases are handled: + * 1. Woven (trait) cache files — identified by the {@see AspectContainer::AOP_PROXIED_SUFFIX} + * in their name. The cache-to-app directory substitution plus suffix stripping recovers + * the original source path. + * 2. Proxy class cache files — FQCN-based paths that may differ from the PSR-4 source path + * when the application's PSR-4 namespace root is not the same as `appDir`. In this case + * Composer's ClassLoader is used to resolve the original file. */ public static function resolveFileName(string $fileName): string { - // Fast path: PSR-4 proxy files register themselves on first include. - // The map stores relative paths (always forward slashes). We normalize $rootPath to - // forward slashes too so the returned path is consistent on all platforms. - if (isset(self::$proxyFileMap[$fileName])) { - return rtrim(str_replace('\\', '/', self::$rootPath), '/') . '/' . self::$proxyFileMap[$fileName]; - } - $suffix = '.php'; $pathParts = explode($suffix, str_replace( self::$rewriteToPath, self::$rootPath, $fileName )); - // throw away any trailing path after the first .php suffix - return $pathParts[0] . $suffix; + $baseName = $pathParts[0]; + + // Case 1: woven trait file — strip the __AopProxied suffix to get the original source path. + if (str_ends_with($baseName, AspectContainer::AOP_PROXIED_SUFFIX)) { + return substr($baseName, 0, -strlen(AspectContainer::AOP_PROXIED_SUFFIX)) . $suffix; + } + + // Case 2: proxy class file (FQCN-based path in the cache directory). + // Derive the FQCN from the path and ask Composer for the canonical source file. + if (str_starts_with($fileName, self::$rewriteToPath)) { + $relPath = ltrim(substr($fileName, strlen(self::$rewriteToPath)), '/\\'); + // Remove .php extension and convert path separators to namespace separators + $fqcn = str_replace('/', '\\', substr($relPath, 0, -strlen($suffix))); + $loader = self::getComposerLoader(); + if ($loader !== null) { + $file = $loader->findFile($fqcn); + if ($file !== false) { + return realpath($file) ?: $file; + } + } + } + + return $baseName . $suffix; + } + + /** + * Returns the Composer ClassLoader, cached after the first successful lookup. + * When AOP is active, the ClassLoader is wrapped by AopComposerLoader — in that case + * the original loader is accessed via {@see AopComposerLoader::getOriginalClassLoader()}. + */ + private static function getComposerLoader(): ?ClassLoader + { + if (self::$composerLoader !== null) { + return self::$composerLoader; + } + // When AOP is active, the original ClassLoader is wrapped by AopComposerLoader + $loader = AopComposerLoader::getOriginalClassLoader(); + if ($loader !== null) { + return self::$composerLoader = $loader; + } + // When AOP is not yet active, find the ClassLoader directly in the autoload stack + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && isset($autoloader[0]) && $autoloader[0] instanceof ClassLoader) { + return self::$composerLoader = $autoloader[0]; + } + } + + return null; } /** diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index 4ce647e3..58cd5977 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -180,16 +180,6 @@ private function processSingleClass( $contentToInclude = $this->saveProxyToCache($class, $childCode); - // Register the PSR-4 woven (trait) file path so CachingTransformer stores the woven - // content at /.php. Without this, the woven - // file would be stored at the source-relative path, which collides with the proxy class - // file when the PSR-4 namespace root coincides with appDir (e.g. in the demos). - $cacheRootDir = $this->cachePathManager->getCacheDir(); - if ($cacheRootDir !== null) { - $wovenRelPath = str_replace('\\', '/', $newFqcn) . '.php'; - $this->cachePathManager->registerWovenFilePath($metadata->uri, $cacheRootDir . '/' . $wovenRelPath); - } - // Get last token for this class $classNode = $class->getNode(); $lastClassToken = $classNode->getAttribute('endTokenPos'); @@ -717,9 +707,9 @@ private function processFunctions( * Proxy files are stored in a PSR-4 compatible layout under the cache root directory: * /.php * - * Each proxy file header contains a {@see MagicConstantTransformer::registerProxyFile()} call - * so that {@see MagicConstantTransformer::resolveFileName()} can map the PSR-4 proxy path back - * to the original source file at runtime (needed for wrapped ReflectionClass::getFileName() calls). + * The woven (trait) file is written by CachingTransformer to a path derived from the + * original source URI with an {@see AspectContainer::AOP_PROXIED_SUFFIX} before .php, + * mirroring the original directory structure under cacheDir. */ private function saveProxyToCache(ReflectionClass $class, string $childCode): string { @@ -741,19 +731,7 @@ private function saveProxyToCache(ReflectionClass $class, string $childCode): st mkdir($dirname, $this->options['cacheFileMode'], true); } - // Compute the source file path relative to appDir so the registration call is portable - // across different environments (avoids embedding absolute paths in the proxy file). - // Normalize both paths to forward slashes before comparison so the result is correct - // on all platforms (Windows PHP may return backslash paths from getFileName()). - $appDirNormalized = rtrim(str_replace('\\', '/', $this->options['appDir']), '/') . '/'; - $relativeSourcePath = str_replace($appDirNormalized, '', str_replace('\\', '/', $classFileName)); - - // Append the registerProxyFile() call at the END of the proxy file. - // This is valid PHP regardless of namespace and ensures the proxy path → source path - // mapping is registered the moment the proxy file is first included, before control - // returns to the caller (the include_once statement in the woven file). - $registerCall = '\\' . MagicConstantTransformer::class . '::registerProxyFile(__FILE__, ' . var_export($relativeSourcePath, true) . ');'; - $body = 'traitAliases as $info) { - $traitNameNode = str_contains($info['trait'], '\\') - ? new Name\FullyQualified($info['trait']) - : new Name($info['trait']); + $traitNameNode = $this->resolveTraitName($info['trait']); $adaptations[] = new TraitUseAdaptation\Alias( $traitNameNode, new Identifier($info['method']), @@ -223,9 +221,7 @@ public function getNode(): ClassNode } $traitNames = array_map( - static fn(string $t) => str_contains($t, '\\') - ? new Name\FullyQualified($t) - : new Name($t), + fn(string $t) => $this->resolveTraitName(ltrim($t, '\\')), $traitFqcns ); $builder->addStmt(new TraitUse($traitNames, $adaptations)); @@ -272,6 +268,24 @@ public function generate(): string return self::getPrinter()->prettyPrint($stmts); } + /** + * Resolves a trait FQCN to a Name AST node, using a relative (unqualified) name when + * the trait resides in the same namespace as the proxy class. This keeps the generated + * code readable: `use FooTrait` instead of `use \Ns\FooTrait`. + */ + private function resolveTraitName(string $traitFqcn): Name + { + $normalized = ltrim($traitFqcn, '\\'); + if ($this->namespace !== null && $this->namespace !== '' && str_starts_with($normalized, $this->namespace . '\\')) { + // Trait is in the same namespace — use just the short name + return new Name(substr($normalized, strlen($this->namespace) + 1)); + } + + return str_contains($normalized, '\\') + ? new Name\FullyQualified($normalized) + : new Name($normalized); + } + /** * Maps ReflectionMethod visibility flag to PhpParser Modifiers constant. * ReflectionMethod::IS_PUBLIC = 1, IS_PROTECTED = 2, IS_PRIVATE = 4 match Modifiers directly. diff --git a/src/Proxy/Generator/EnumGenerator.php b/src/Proxy/Generator/EnumGenerator.php index a86c5361..877a9bde 100644 --- a/src/Proxy/Generator/EnumGenerator.php +++ b/src/Proxy/Generator/EnumGenerator.php @@ -155,9 +155,7 @@ public function getNode(): EnumNode $adaptations = []; foreach ($this->traitAliases as $info) { - $traitNameNode = str_contains($info['trait'], '\\') - ? new Name\FullyQualified($info['trait']) - : new Name($info['trait']); + $traitNameNode = $this->resolveTraitName($info['trait']); $adaptations[] = new TraitUseAdaptation\Alias( $traitNameNode, new Identifier($info['method']), @@ -167,9 +165,7 @@ public function getNode(): EnumNode } $traitNames = array_map( - static fn(string $t) => str_contains($t, '\\') - ? new Name\FullyQualified($t) - : new Name($t), + fn(string $t) => $this->resolveTraitName(ltrim($t, '\\')), $traitFqcns ); $stmts[] = new TraitUse($traitNames, $adaptations); @@ -229,6 +225,23 @@ public function generate(): string return self::getPrinter()->prettyPrint($stmts); } + /** + * Resolves a trait FQCN to a Name AST node, using a relative (unqualified) name when + * the trait resides in the same namespace as the enum. This keeps the generated + * code readable: `use FooTrait` instead of `use \Ns\FooTrait`. + */ + private function resolveTraitName(string $traitFqcn): Name + { + $normalized = ltrim($traitFqcn, '\\'); + if ($this->namespace !== null && $this->namespace !== '' && str_starts_with($normalized, $this->namespace . '\\')) { + return new Name(substr($normalized, strlen($this->namespace) + 1)); + } + + return str_contains($normalized, '\\') + ? new Name\FullyQualified($normalized) + : new Name($normalized); + } + /** * Maps ReflectionMethod visibility flag to PhpParser Modifiers constant. */ diff --git a/tests/Instrument/Transformer/_files/class-proxy.php b/tests/Instrument/Transformer/_files/class-proxy.php index 729c13e5..375369da 100644 --- a/tests/Instrument/Transformer/_files/class-proxy.php +++ b/tests/Instrument/Transformer/_files/class-proxy.php @@ -6,14 +6,14 @@ use Go\Aop\Intercept\StaticMethodInvocation; class TestClass implements \Go\Aop\Proxy { - use \Test\ns1\TestClass__AopProxied { - \Test\ns1\TestClass__AopProxied::publicMethod as private __aop__publicMethod; - \Test\ns1\TestClass__AopProxied::protectedMethod as private __aop__protectedMethod; - \Test\ns1\TestClass__AopProxied::publicStaticMethod as private __aop__publicStaticMethod; - \Test\ns1\TestClass__AopProxied::protectedStaticMethod as private __aop__protectedStaticMethod; - \Test\ns1\TestClass__AopProxied::publicMethodDynamicArguments as private __aop__publicMethodDynamicArguments; - \Test\ns1\TestClass__AopProxied::publicMethodFixedArguments as private __aop__publicMethodFixedArguments; - \Test\ns1\TestClass__AopProxied::methodWithSpecialTypeArguments as private __aop__methodWithSpecialTypeArguments; + use TestClass__AopProxied { + TestClass__AopProxied::publicMethod as private __aop__publicMethod; + TestClass__AopProxied::protectedMethod as private __aop__protectedMethod; + TestClass__AopProxied::publicStaticMethod as private __aop__publicStaticMethod; + TestClass__AopProxied::protectedStaticMethod as private __aop__protectedStaticMethod; + TestClass__AopProxied::publicMethodDynamicArguments as private __aop__publicMethodDynamicArguments; + TestClass__AopProxied::publicMethodFixedArguments as private __aop__publicMethodFixedArguments; + TestClass__AopProxied::methodWithSpecialTypeArguments as private __aop__methodWithSpecialTypeArguments; } public function publicMethod() { @@ -58,4 +58,3 @@ public function methodWithSpecialTypeArguments(self $instance) return $__joinPoint->__invoke($this, [$instance]); } } -\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/class.php'); diff --git a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php index 062620f7..4156d57c 100644 --- a/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php +++ b/tests/Instrument/Transformer/_files/final-readonly-class-proxy.php @@ -6,10 +6,10 @@ use Go\Aop\Intercept\StaticMethodInvocation; final readonly class TestReadonlyClass implements \Go\Aop\Proxy { - use \Test\ns1\TestReadonlyClass__AopProxied { - \Test\ns1\TestReadonlyClass__AopProxied::publicMethod as private __aop__publicMethod; - \Test\ns1\TestReadonlyClass__AopProxied::anotherMethod as private __aop__anotherMethod; - \Test\ns1\TestReadonlyClass__AopProxied::staticMethod as private __aop__staticMethod; + use TestReadonlyClass__AopProxied { + TestReadonlyClass__AopProxied::publicMethod as private __aop__publicMethod; + TestReadonlyClass__AopProxied::anotherMethod as private __aop__anotherMethod; + TestReadonlyClass__AopProxied::staticMethod as private __aop__staticMethod; } public function publicMethod(): string { @@ -30,4 +30,3 @@ public static function staticMethod(): string return $__joinPoint->__invoke(static::class); } } -\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/final-readonly-class.php'); diff --git a/tests/Instrument/Transformer/_files/php7-class-proxy.php b/tests/Instrument/Transformer/_files/php7-class-proxy.php index a02b3aba..ec77845a 100644 --- a/tests/Instrument/Transformer/_files/php7-class-proxy.php +++ b/tests/Instrument/Transformer/_files/php7-class-proxy.php @@ -5,24 +5,24 @@ use Go\Aop\Intercept\DynamicMethodInvocation; class TestPhp7Class implements \Go\Aop\Proxy { - use \Test\ns1\TestPhp7Class__AopProxied { - \Test\ns1\TestPhp7Class__AopProxied::stringSth as private __aop__stringSth; - \Test\ns1\TestPhp7Class__AopProxied::floatSth as private __aop__floatSth; - \Test\ns1\TestPhp7Class__AopProxied::boolSth as private __aop__boolSth; - \Test\ns1\TestPhp7Class__AopProxied::intSth as private __aop__intSth; - \Test\ns1\TestPhp7Class__AopProxied::callableSth as private __aop__callableSth; - \Test\ns1\TestPhp7Class__AopProxied::arraySth as private __aop__arraySth; - \Test\ns1\TestPhp7Class__AopProxied::variadicStringSthByRef as private __aop__variadicStringSthByRef; - \Test\ns1\TestPhp7Class__AopProxied::exceptionArg as private __aop__exceptionArg; - \Test\ns1\TestPhp7Class__AopProxied::stringRth as private __aop__stringRth; - \Test\ns1\TestPhp7Class__AopProxied::floatRth as private __aop__floatRth; - \Test\ns1\TestPhp7Class__AopProxied::boolRth as private __aop__boolRth; - \Test\ns1\TestPhp7Class__AopProxied::intRth as private __aop__intRth; - \Test\ns1\TestPhp7Class__AopProxied::callableRth as private __aop__callableRth; - \Test\ns1\TestPhp7Class__AopProxied::arrayRth as private __aop__arrayRth; - \Test\ns1\TestPhp7Class__AopProxied::exceptionRth as private __aop__exceptionRth; - \Test\ns1\TestPhp7Class__AopProxied::noRth as private __aop__noRth; - \Test\ns1\TestPhp7Class__AopProxied::returnSelf as private __aop__returnSelf; + use TestPhp7Class__AopProxied { + TestPhp7Class__AopProxied::stringSth as private __aop__stringSth; + TestPhp7Class__AopProxied::floatSth as private __aop__floatSth; + TestPhp7Class__AopProxied::boolSth as private __aop__boolSth; + TestPhp7Class__AopProxied::intSth as private __aop__intSth; + TestPhp7Class__AopProxied::callableSth as private __aop__callableSth; + TestPhp7Class__AopProxied::arraySth as private __aop__arraySth; + TestPhp7Class__AopProxied::variadicStringSthByRef as private __aop__variadicStringSthByRef; + TestPhp7Class__AopProxied::exceptionArg as private __aop__exceptionArg; + TestPhp7Class__AopProxied::stringRth as private __aop__stringRth; + TestPhp7Class__AopProxied::floatRth as private __aop__floatRth; + TestPhp7Class__AopProxied::boolRth as private __aop__boolRth; + TestPhp7Class__AopProxied::intRth as private __aop__intRth; + TestPhp7Class__AopProxied::callableRth as private __aop__callableRth; + TestPhp7Class__AopProxied::arrayRth as private __aop__arrayRth; + TestPhp7Class__AopProxied::exceptionRth as private __aop__exceptionRth; + TestPhp7Class__AopProxied::noRth as private __aop__noRth; + TestPhp7Class__AopProxied::returnSelf as private __aop__returnSelf; } public function stringSth(string $arg) { @@ -127,4 +127,3 @@ public function returnSelf(): self return $__joinPoint->__invoke($this); } } -\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/php7-class.php'); diff --git a/tests/Instrument/Transformer/_files/php81-enum-proxy.php b/tests/Instrument/Transformer/_files/php81-enum-proxy.php index 8bddfa5c..55acd06e 100644 --- a/tests/Instrument/Transformer/_files/php81-enum-proxy.php +++ b/tests/Instrument/Transformer/_files/php81-enum-proxy.php @@ -5,8 +5,8 @@ use Go\Aop\Intercept\DynamicMethodInvocation; enum TestStatus : string implements \Go\Aop\Proxy { - use \Test\ns1\TestStatus__AopProxied { - \Test\ns1\TestStatus__AopProxied::label as private __aop__label; + use TestStatus__AopProxied { + TestStatus__AopProxied::label as private __aop__label; } case Active = 'active'; case Inactive = 'inactive'; @@ -17,4 +17,3 @@ public function label(): string return $__joinPoint->__invoke($this); } } -\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/php81-enum.php'); diff --git a/tests/Instrument/Transformer/_files/php83-override-proxy.php b/tests/Instrument/Transformer/_files/php83-override-proxy.php index 1e06c886..dddec077 100644 --- a/tests/Instrument/Transformer/_files/php83-override-proxy.php +++ b/tests/Instrument/Transformer/_files/php83-override-proxy.php @@ -10,9 +10,9 @@ */ class TestClassWithOverride implements \Go\Aop\Proxy { - use \Test\ns1\TestClassWithOverride__AopProxied { - \Test\ns1\TestClassWithOverride__AopProxied::overriddenMethod as private __aop__overriddenMethod; - \Test\ns1\TestClassWithOverride__AopProxied::normalMethod as private __aop__normalMethod; + use TestClassWithOverride__AopProxied { + TestClassWithOverride__AopProxied::overriddenMethod as private __aop__overriddenMethod; + TestClassWithOverride__AopProxied::normalMethod as private __aop__normalMethod; } #[\Override] public function overriddenMethod(): string @@ -28,4 +28,3 @@ public function normalMethod(): int return $__joinPoint->__invoke($this); } } -\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(__FILE__, 'Transformer/_files/php83-override.php'); diff --git a/tests/PhpUnit/ClassIsNotWovenConstraint.php b/tests/PhpUnit/ClassIsNotWovenConstraint.php index f1f0717a..d3d1a967 100644 --- a/tests/PhpUnit/ClassIsNotWovenConstraint.php +++ b/tests/PhpUnit/ClassIsNotWovenConstraint.php @@ -12,6 +12,9 @@ namespace Go\PhpUnit; +use Composer\Autoload\ClassLoader; +use Go\Core\AspectContainer; +use Go\Instrument\PathResolver; use PHPUnit\Framework\Constraint\Constraint; /** @@ -31,16 +34,23 @@ public function __construct(array $configuration) */ public function matches($other): bool { - // Woven trait file uses a PSR-4 layout: /.php - $wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php'; - $transformedFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $wovenRelativePath); + $filename = $this->findOriginalSourceFile($other); + if ($filename === false) { + return true; + } - // Proxy files use a PSR-4 layout: /.php + // Cache mirrors the original directory structure. + // Woven trait file uses the source-relative path with an AopProxied suffix. + $appDir = PathResolver::realpath($this->configuration['appDir']); + $suffix = substr($filename, strlen($appDir)); + $wovenPath = $this->configuration['cacheDir'] . substr($suffix, 0, -4) . AspectContainer::AOP_PROXIED_SUFFIX . '.php'; + + // Proxy file follows FQCN-based path (mirrors PSR-4/PSR-0 namespace structure) $proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php'; $proxyFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $proxyRelativePath); // if any of files exists, assert has to fail - return !$transformedFileExists && !$proxyFileExists; + return !file_exists($wovenPath) && !$proxyFileExists; } /** @@ -50,4 +60,22 @@ public function toString(): string { return 'is not woven class.'; } + + /** + * Returns the original source file path for the given class via Composer's ClassLoader, + * regardless of whether the class is already loaded in memory (possibly as an AOP proxy). + */ + private function findOriginalSourceFile(string $className): string|false + { + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && isset($autoloader[0]) && $autoloader[0] instanceof ClassLoader) { + $file = $autoloader[0]->findFile($className); + if ($file !== false) { + return realpath($file) ?: $file; + } + } + } + + return false; + } } diff --git a/tests/PhpUnit/ClassWovenConstraint.php b/tests/PhpUnit/ClassWovenConstraint.php index 9cba006c..22c268c6 100644 --- a/tests/PhpUnit/ClassWovenConstraint.php +++ b/tests/PhpUnit/ClassWovenConstraint.php @@ -12,10 +12,14 @@ namespace Go\PhpUnit; +use Composer\Autoload\ClassLoader; +use Go\Core\AspectContainer; +use Go\Instrument\ClassLoading\AopComposerLoader; +use Go\Instrument\PathResolver; use PHPUnit\Framework\Constraint\Constraint; /** - * Asserts that class is not woven. + * Asserts that class is woven. */ final class ClassWovenConstraint extends Constraint { @@ -31,16 +35,23 @@ public function __construct(array $configuration) */ public function matches($other): bool { - // Woven trait file uses a PSR-4 layout: /.php - $wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php'; - $transformedFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $wovenRelativePath); + $filename = $this->findOriginalSourceFile($other); + if ($filename === false) { + return false; + } - // Proxy files use a PSR-4 layout: /.php + // Cache mirrors the original directory structure. + // Woven trait file uses the source-relative path with an AopProxied suffix. + $appDir = PathResolver::realpath($this->configuration['appDir']); + $suffix = substr($filename, strlen($appDir)); + $wovenPath = $this->configuration['cacheDir'] . substr($suffix, 0, -4) . AspectContainer::AOP_PROXIED_SUFFIX . '.php'; + + // Proxy file follows FQCN-based path (mirrors PSR-4/PSR-0 namespace structure) $proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php'; $proxyFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $proxyRelativePath); // if any of files is missing, assert has to fail - return $transformedFileExists && $proxyFileExists; + return file_exists($wovenPath) && $proxyFileExists; } /** @@ -50,4 +61,34 @@ public function toString(): string { return 'is woven class.'; } + + /** + * Returns the original source file path for the given class via Composer's ClassLoader, + * regardless of whether the class is already loaded in memory (possibly as an AOP proxy). + * When AOP is active the ClassLoader is wrapped by AopComposerLoader — in that case the + * original loader is retrieved via {@see AopComposerLoader::getOriginalClassLoader()}. + */ + private function findOriginalSourceFile(string $className): string|false + { + // When AOP is active, the ClassLoader is wrapped; ask AopComposerLoader for it. + $loader = AopComposerLoader::getOriginalClassLoader(); + if ($loader !== null) { + $file = $loader->findFile($className); + if ($file !== false) { + return realpath($file) ?: $file; + } + } + + // Fallback: AOP not yet active — find ClassLoader directly in the autoload stack. + foreach (spl_autoload_functions() as $autoloader) { + if (is_array($autoloader) && isset($autoloader[0]) && $autoloader[0] instanceof ClassLoader) { + $file = $autoloader[0]->findFile($className); + if ($file !== false) { + return realpath($file) ?: $file; + } + } + } + + return false; + } } diff --git a/tests/PhpUnit/ProxyClassReflectionHelper.php b/tests/PhpUnit/ProxyClassReflectionHelper.php index db14ebb1..3d2eca79 100644 --- a/tests/PhpUnit/ProxyClassReflectionHelper.php +++ b/tests/PhpUnit/ProxyClassReflectionHelper.php @@ -44,7 +44,8 @@ private function __construct() */ public static function extractAdvicesFromProxyFile(string $className, array $configuration): array { - // Proxy files use a PSR-4 layout: /.php + // Proxy files use a FQCN-based layout mirroring PSR-4/PSR-0 namespace structure: + // /.php $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; $proxyFileName = $configuration['cacheDir'] . DIRECTORY_SEPARATOR . $classSuffix; @@ -205,7 +206,8 @@ public static function createReflectionClass(string $className, array $configura $parsedReflectionClass = new ReflectionClass($className); $originalNamespace = $parsedReflectionClass->getNamespaceName(); - // Proxy files use a PSR-4 layout: /.php + // Proxy files use a FQCN-based layout mirroring PSR-4/PSR-0 namespace structure: + // /.php $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; $proxyFileName = $configuration['cacheDir'] . DIRECTORY_SEPARATOR . $classSuffix; $proxyFileContent = file_get_contents($proxyFileName);