Skip to content

Commit f8e33bb

Browse files
Copilotlisachenko
andauthored
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>
1 parent 62d1128 commit f8e33bb

5 files changed

Lines changed: 74 additions & 21 deletions

File tree

src/Instrument/ClassLoading/CachePathManager.php

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ class CachePathManager
6262
*/
6363
protected array $newCacheState = [];
6464

65+
/**
66+
* Per-request overrides for the woven (trait) file cache path, keyed by original source URI.
67+
* Populated by WeavingTransformer so that the woven file is written to a PSR-4-compatible
68+
* <cacheDir>/<Namespace/ClassName__AopProxied>.php path instead of the source-relative path,
69+
* preventing collisions with the proxy class file when the namespace root equals appDir.
70+
*
71+
* @var array<string, string>
72+
*/
73+
private array $wovenFilePathOverrides = [];
74+
6575
public function __construct(AspectKernel $kernel)
6676
{
6777
$this->kernel = $kernel;
@@ -165,6 +175,28 @@ public function setCacheState(string $resource, array $metadata): void
165175
$this->newCacheState[$resource] = $metadata;
166176
}
167177

178+
/**
179+
* Registers a PSR-4 woven file path for a given source URI.
180+
*
181+
* Called by {@see WeavingTransformer} after weaving a class so that
182+
* {@see CachingTransformer} stores the trait (woven) file at the correct PSR-4
183+
* location (<cacheDir>/<Namespace/ClassName__AopProxied>.php) rather than the
184+
* source-relative path, which would collide with the proxy class file when the
185+
* PSR-4 namespace root coincides with appDir.
186+
*/
187+
public function registerWovenFilePath(string $originalUri, string $wovenPath): void
188+
{
189+
$this->wovenFilePathOverrides[$originalUri] = $wovenPath;
190+
}
191+
192+
/**
193+
* Returns the registered PSR-4 woven file path for the given source URI, or null if none was set.
194+
*/
195+
public function getWovenFilePath(string $originalUri): ?string
196+
{
197+
return $this->wovenFilePathOverrides[$originalUri] ?? null;
198+
}
199+
168200
/**
169201
* Automatic destructor saves all new changes into the cache
170202
*

src/Instrument/Transformer/CachingTransformer.php

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -66,30 +66,46 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum
6666
return TransformerResultEnum::RESULT_ABORTED;
6767
}
6868

69-
$lastModified = filemtime($originalUri);
70-
$cacheState = $this->cacheManager->queryCacheState($originalUri);
69+
$lastModified = filemtime($originalUri);
70+
$cacheState = $this->cacheManager->queryCacheState($originalUri);
7171
$cacheFilemtime = $cacheState !== null ? ($cacheState['filemtime'] ?? 0) : 0;
7272
$cacheModified = is_int($cacheFilemtime) ? $cacheFilemtime : 0;
7373

74+
// The stored cacheUri may be a PSR-4 __AopProxied path (set by WeavingTransformer).
75+
// Consider the cache stale only when the stored cacheUri belongs to a different cache
76+
// directory (i.e. cacheDir was moved), not merely because it has a different file name.
77+
$cacheDir = $this->cacheManager->getCacheDir() ?? '';
78+
$storedCacheUri = is_array($cacheState) && is_string($cacheState['cacheUri'] ?? null)
79+
? $cacheState['cacheUri']
80+
: null;
81+
$cacheUriOutOfDate = $storedCacheUri !== null
82+
&& $cacheDir !== ''
83+
&& !str_starts_with($storedCacheUri, $cacheDir);
84+
7485
if ($cacheModified < $lastModified
75-
|| (isset($cacheState['cacheUri']) && $cacheState['cacheUri'] !== $cacheUri)
86+
|| $cacheUriOutOfDate
7687
|| !$this->container->hasAnyResourceChangedSince($cacheModified)
7788
) {
7889
$processingResult = $this->processTransformers($metadata);
7990
if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) {
80-
$parentCacheDir = dirname($cacheUri);
91+
// WeavingTransformer may have registered a PSR-4 path for the woven (trait) file.
92+
// Use that when available to avoid collisions with the proxy class file.
93+
$resolvedCacheUri = $this->cacheManager->getWovenFilePath($originalUri) ?? $cacheUri;
94+
$parentCacheDir = dirname($resolvedCacheUri);
8195
if (!is_dir($parentCacheDir)) {
8296
mkdir($parentCacheDir, $this->cacheFileMode, true);
8397
}
84-
file_put_contents($cacheUri, $metadata->source, LOCK_EX);
98+
file_put_contents($resolvedCacheUri, $metadata->source, LOCK_EX);
8599
// For cache files we don't want executable bits by default
86-
chmod($cacheUri, $this->cacheFileMode & (~0111));
100+
chmod($resolvedCacheUri, $this->cacheFileMode & (~0111));
101+
} else {
102+
$resolvedCacheUri = $cacheUri;
87103
}
88104
$this->cacheManager->setCacheState(
89105
$originalUri,
90106
[
91107
'filemtime' => $_SERVER['REQUEST_TIME'] ?? time(),
92-
'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $cacheUri : null
108+
'cacheUri' => ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) ? $resolvedCacheUri : null
93109
]
94110
);
95111

@@ -100,8 +116,9 @@ public function transform(StreamMetaData $metadata): TransformerResultEnum
100116
$processingResult = isset($cacheState['cacheUri']) ? TransformerResultEnum::RESULT_TRANSFORMED : TransformerResultEnum::RESULT_ABORTED;
101117
}
102118
if ($processingResult === TransformerResultEnum::RESULT_TRANSFORMED) {
103-
// Just replace all tokens in the stream
104-
ReflectionEngine::parseFile($cacheUri);
119+
// Use the stored cache URI — it may be a PSR-4 __AopProxied path from a previous run.
120+
$readUri = $storedCacheUri ?? $cacheUri;
121+
ReflectionEngine::parseFile($readUri);
105122
$metadata->setTokenStreamFromRawTokens(
106123
...ReflectionEngine::getParser()->getTokens()
107124
);

src/Instrument/Transformer/WeavingTransformer.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,16 @@ private function processSingleClass(
180180

181181
$contentToInclude = $this->saveProxyToCache($class, $childCode);
182182

183+
// Register the PSR-4 woven (trait) file path so CachingTransformer stores the woven
184+
// content at <cacheDir>/<Namespace/ClassName__AopProxied>.php. Without this, the woven
185+
// file would be stored at the source-relative path, which collides with the proxy class
186+
// file when the PSR-4 namespace root coincides with appDir (e.g. in the demos).
187+
$cacheRootDir = $this->cachePathManager->getCacheDir();
188+
if ($cacheRootDir !== null) {
189+
$wovenRelPath = str_replace('\\', '/', $newFqcn) . '.php';
190+
$this->cachePathManager->registerWovenFilePath($metadata->uri, $cacheRootDir . '/' . $wovenRelPath);
191+
}
192+
183193
// Get last token for this class
184194
$classNode = $class->getNode();
185195
$lastClassToken = $classNode->getAttribute('endTokenPos');

tests/PhpUnit/ClassIsNotWovenConstraint.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
namespace Go\PhpUnit;
1414

15-
use Go\Instrument\PathResolver;
16-
use Go\ParserReflection\ReflectionClass;
1715
use PHPUnit\Framework\Constraint\Constraint;
1816

1917
/**
@@ -33,10 +31,9 @@ public function __construct(array $configuration)
3331
*/
3432
public function matches($other): bool
3533
{
36-
$filename = (new ReflectionClass($other))->getFileName();
37-
$suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir'])));
38-
39-
$transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix);
34+
// Woven trait file uses a PSR-4 layout: <cacheDir>/<Namespace/ClassName__AopProxied>.php
35+
$wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php';
36+
$transformedFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $wovenRelativePath);
4037

4138
// Proxy files use a PSR-4 layout: <cacheDir>/<Namespace/ClassName>.php
4239
$proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php';

tests/PhpUnit/ClassWovenConstraint.php

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212

1313
namespace Go\PhpUnit;
1414

15-
use Go\Instrument\PathResolver;
16-
use Go\ParserReflection\ReflectionClass;
1715
use PHPUnit\Framework\Constraint\Constraint;
1816

1917
/**
@@ -33,10 +31,9 @@ public function __construct(array $configuration)
3331
*/
3432
public function matches($other): bool
3533
{
36-
$filename = (new ReflectionClass($other))->getFileName();
37-
$suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir'])));
38-
39-
$transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix);
34+
// Woven trait file uses a PSR-4 layout: <cacheDir>/<Namespace/ClassName__AopProxied>.php
35+
$wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php';
36+
$transformedFileExists = file_exists($this->configuration['cacheDir'] . DIRECTORY_SEPARATOR . $wovenRelativePath);
4037

4138
// Proxy files use a PSR-4 layout: <cacheDir>/<Namespace/ClassName>.php
4239
$proxyRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '.php';

0 commit comments

Comments
 (0)