diff --git a/src/Console/Command/DebugWeavingCommand.php b/src/Console/Command/DebugWeavingCommand.php index 5dffef51..8dd7d229 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 @@ -121,7 +127,9 @@ private function getProxies(CachePathManager $cachePathManager): array foreach ($iterator as $splFileInfo) { if ($splFileInfo->isFile()) { $content = file_get_contents($splFileInfo->getPathname()); - if ($content !== false) { + // 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/Transformer/CachingTransformer.php b/src/Instrument/Transformer/CachingTransformer.php index 0990dff5..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,30 +67,34 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum return TransformerResultEnum::RESULT_ABORTED; } - $lastModified = filemtime($originalUri); - $cacheState = $this->cacheManager->queryCacheState($originalUri); + // 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; if ($cacheModified < $lastModified - || (isset($cacheState['cacheUri']) && $cacheState['cacheUri'] !== $cacheUri) + || (isset($cacheState['cacheUri']) && !file_exists($wovenCacheUri)) || !$this->container->hasAnyResourceChangedSince($cacheModified) ) { $processingResult = $this->processTransformers($metadata); if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) { - $parentCacheDir = dirname($cacheUri); + $parentCacheDir = dirname($wovenCacheUri); if (!is_dir($parentCacheDir)) { mkdir($parentCacheDir, $this->cacheFileMode, true); } - file_put_contents($cacheUri, $metadata->source, LOCK_EX); + file_put_contents($wovenCacheUri, $metadata->source, LOCK_EX); // For cache files we don't want executable bits by default - chmod($cacheUri, $this->cacheFileMode & (~0111)); + chmod($wovenCacheUri, $this->cacheFileMode & (~0111)); } $this->cacheManager->setCacheState( $originalUri, [ 'filemtime' => $_SERVER['REQUEST_TIME'] ?? time(), - 'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $cacheUri : null + 'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $wovenCacheUri : null ] ); @@ -100,8 +105,7 @@ 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); + ReflectionEngine::parseFile($wovenCacheUri); $metadata->setTokenStreamFromRawTokens( ...ReflectionEngine::getParser()->getTokens() ); diff --git a/src/Instrument/Transformer/MagicConstantTransformer.php b/src/Instrument/Transformer/MagicConstantTransformer.php index a107a85e..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; @@ -39,6 +42,11 @@ class MagicConstantTransformer extends BaseSourceTransformer */ protected static string $rewriteToPath = ''; + /** + * Cached Composer ClassLoader instance, used for resolving proxy file paths to original sources. + */ + private static ?ClassLoader $composerLoader = null; + /** * Class constructor */ @@ -62,18 +70,72 @@ 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. + * + * 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 { $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 - 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 6373c858..58cd5977 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,42 @@ 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 + * + * 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 { - $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['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/src/Proxy/Generator/ClassGenerator.php b/src/Proxy/Generator/ClassGenerator.php index 094d230e..a655fd37 100644 --- a/src/Proxy/Generator/ClassGenerator.php +++ b/src/Proxy/Generator/ClassGenerator.php @@ -211,9 +211,7 @@ public function getNode(): ClassNode // Build adaptations for all aliases $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']), @@ -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/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index 15b63f2d..64b9bc5b 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')); + } } /** @@ -143,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); @@ -186,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); @@ -208,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); @@ -226,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); @@ -285,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); @@ -353,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); diff --git a/tests/Instrument/Transformer/_files/class-proxy.php b/tests/Instrument/Transformer/_files/class-proxy.php index 4f3d06a9..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() { 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..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 { 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..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) { 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..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'; 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..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 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..d3d1a967 100644 --- a/tests/PhpUnit/ClassIsNotWovenConstraint.php +++ b/tests/PhpUnit/ClassIsNotWovenConstraint.php @@ -12,8 +12,9 @@ namespace Go\PhpUnit; +use Composer\Autoload\ClassLoader; +use Go\Core\AspectContainer; use Go\Instrument\PathResolver; -use Go\ParserReflection\ReflectionClass; use PHPUnit\Framework\Constraint\Constraint; /** @@ -33,14 +34,23 @@ public function __construct(array $configuration) */ public function matches($other): bool { - $filename = (new ReflectionClass($other))->getFileName(); - $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); + $filename = $this->findOriginalSourceFile($other); + if ($filename === false) { + return true; + } - $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); - $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix); + // 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 6c393c98..22c268c6 100644 --- a/tests/PhpUnit/ClassWovenConstraint.php +++ b/tests/PhpUnit/ClassWovenConstraint.php @@ -12,12 +12,14 @@ namespace Go\PhpUnit; +use Composer\Autoload\ClassLoader; +use Go\Core\AspectContainer; +use Go\Instrument\ClassLoading\AopComposerLoader; use Go\Instrument\PathResolver; -use Go\ParserReflection\ReflectionClass; use PHPUnit\Framework\Constraint\Constraint; /** - * Asserts that class is not woven. + * Asserts that class is woven. */ final class ClassWovenConstraint extends Constraint { @@ -33,14 +35,23 @@ public function __construct(array $configuration) */ public function matches($other): bool { - $filename = (new ReflectionClass($other))->getFileName(); - $suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir']))); + $filename = $this->findOriginalSourceFile($other); + if ($filename === false) { + return false; + } - $transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix); - $proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix); + // 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 89b74440..3d2eca79 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,10 @@ 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 FQCN-based layout mirroring PSR-4/PSR-0 namespace structure: + // /.php + $classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php'; + $proxyFileName = $configuration['cacheDir'] . DIRECTORY_SEPARATOR . $classSuffix; if (!file_exists($proxyFileName)) { return []; @@ -209,14 +204,12 @@ 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 FQCN-based layout mirroring PSR-4/PSR-0 namespace structure: + // /.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"