Skip to content

feat: PSR-4 compatible cache structure for generated proxy files#568

Draft
Copilot wants to merge 4 commits intomasterfrom
copilot/improve-cache-structure-psr4
Draft

feat: PSR-4 compatible cache structure for generated proxy files#568
Copilot wants to merge 4 commits intomasterfrom
copilot/improve-cache-structure-psr4

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 30, 2026

Cache files were generated under a convoluted _proxies/<original-relative-path>/<FQCN>.php layout that encoded the source file path rather than the class identity, making the cache directory unusable as a PSR-4 autoloader root and breaking correctness when classes are renamed or moved.

Proxy file path layout

Before: <cacheDir>/_proxies/src/Application/Main.php/Go/Tests/TestProject/Application/Main.php
After: <cacheDir>/Go/Tests/TestProject/Application/Main.php

Each proxy file now lives at a path derived purely from its FQCN, matching PSR-4 conventions.

ReflectionClass::getFileName() on proxy classes

With PSR-4 paths the proxy location no longer encodes the source path, so MagicConstantTransformer::resolveFileName() needed a new mechanism. Each generated proxy file now ends with a self-registration call:

// appended at the bottom of every generated proxy file:
\Go\Instrument\Transformer\MagicConstantTransformer::registerProxyFile(
    __FILE__,
    'src/Application/Main.php'   // relative to appDir; portable across environments
);

MagicConstantTransformer gains a static $proxyFileMap registry populated at include_once time, giving resolveFileName() an O(1) lookup before falling back to the existing cache-dir→app-dir path substitution for woven files.

DebugWeavingCommand proxy discovery

Replaced the hardcoded _proxies/ subdirectory scan with a recursive scan of the full cacheDir, filtering on the presence of \Go\Aop\Proxy in file content. The filter is broadened from implements \Go\Aop\Proxy to just \Go\Aop\Proxy to correctly match classes that implement additional interfaces before the proxy marker.

Cross-platform path handling

Both saveProxyToCache() and resolveFileName() normalize paths to forward slashes before computing relative paths so that Windows backslash paths from ReflectionClass::getFileName() are handled correctly.

Test/snapshot updates

  • All *-woven.php snapshot files updated with new include_once AOP_CACHE_DIR . '/Ns/ClassName.php' paths.
  • All *-proxy.php snapshot files updated with the registerProxyFile() footer.
  • Regex patterns in WeavingTransformerTest fixed to escape the literal PHP concatenation dot (\.).

Copilot AI linked an issue Apr 30, 2026 that may be closed by this pull request
Copilot AI changed the title [WIP] Refactor cache files to adhere to PSR-4 structure feat: PSR-4 compatible cache structure for generated proxy files Apr 30, 2026
Copilot AI requested a review from lisachenko April 30, 2026 11:22
@lisachenko
Copy link
Copy Markdown
Member

@copilot I've opened demos - and all cases are failing with error:

Fatal error: Cannot redeclare trait Demo\Example\CacheableDemo__AopProxied (previously declared in ./framework/demos/Demo/Example/CacheableDemo.php:20) in ./framework/demos/cache/Demo/Example/CacheableDemo.php on line 20

I opened a file in the cache - and CacheableDemo.php file contains now trait CacheableDemo__AopProxied. This file should be named according to the entity name - CacheableDemo__AopProxied.php and transformed class should be stored in the CacheableDemo.php in the cache

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 1, 2026

@copilot I've opened demos - and all cases are failing with error:

Fatal error: Cannot redeclare trait Demo\Example\CacheableDemo__AopProxied (previously declared in ./framework/demos/Demo/Example...

Fixed in f8e33bb.

The root cause was a path collision: when the PSR-4 namespace root coincides with appDir (as in the demos where Demo\Example\CacheableDemo lives at demos/Demo/Example/CacheableDemo.php), CachingTransformer was storing the woven trait file at cache/Demo/Example/CacheableDemo.php — the same path my proxy class uses.

The fix introduces proper PSR-4 naming for both files:

  • Woven trait file: <cacheDir>/Demo/Example/CacheableDemo__AopProxied.php (named after the trait)
  • Proxy class file: <cacheDir>/Demo/Example/CacheableDemo.php (named after the class)

WeavingTransformer now registers the __AopProxied woven path via CachePathManager.registerWovenFilePath(), and CachingTransformer uses that registered path when writing and reading the woven file.

Copilot AI and others added 4 commits May 1, 2026 17:08
- WeavingTransformer.saveProxyToCache() now generates proxy files at PSR-4
  paths (<cacheDir>/<Namespace/ClassName>.php) instead of the old convoluted
  <cacheDir>/_proxies/<original-file-path>/<FQCN>.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>
- 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>
…llision

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>
@lisachenko lisachenko force-pushed the copilot/improve-cache-structure-psr4 branch from f8e33bb to f329ce8 Compare May 1, 2026 14:09
* 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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drop this method, it should not be used, together with getWovenFilePath()

*/
foreach ($iterator as $splFileInfo) {
if ($splFileInfo->isFile()) {
if ($splFileInfo->isFile() && $splFileInfo->getExtension() === 'php') {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exclude this filtering and check of file extension

// 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')) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use "Go\Aop\Proxy" without leading slash, as it may be used in the "use" section without leading ""

*
* @var array<string, string>
*/
private array $wovenFilePathOverrides = [];
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed here in the first place

$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;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably not needed, not sure why it is here

$filename = (new ReflectionClass($other))->getFileName();
$suffix = substr($filename, strlen(PathResolver::realpath($this->configuration['appDir'])));
// Woven trait file uses a PSR-4 layout: <cacheDir>/<Namespace/ClassName__AopProxied>.php
$wovenRelativePath = str_replace('\\', DIRECTORY_SEPARATOR, $other) . '__AopProxied.php';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use constant for class suffix "__AopProxied"


$transformedFileExists = file_exists($this->configuration['cacheDir'] . $suffix);
$proxyFileExists = file_exists($this->configuration['cacheDir'] . '/_proxies' . $suffix);
// Proxy files use a PSR-4 layout: <cacheDir>/<Namespace/ClassName>.php
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see comment above

$classSuffix = str_replace('\\', DIRECTORY_SEPARATOR, $className) . '.php';
$proxyRelativePath = $relativePath . DIRECTORY_SEPARATOR . $classSuffix;
$proxyFileName = $configuration['cacheDir'] . '/_proxies/' . $proxyRelativePath;
// Proxy files use a PSR-4 layout: <cacheDir>/<Namespace/ClassName>.php
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

keep previous logic, but not with "__proxies/" subdirectories, we aiming to have mirrored structure of directories and files, with only difference that original files will have "__AopProxied" suffix

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';
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this change looks exactly what I was expected, keep it as foundation base

use Go\Aop\Intercept\StaticMethodInvocation;
class TestClass implements \Go\Aop\Proxy
{
use \Test\ns1\TestClass__AopProxied {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generated proxies use unnecessarily FQDN for trait adoptation:

class CacheableDemo implements \Go\Aop\Proxy
{
    use \Demo\Example\CacheableDemo__AopProxied {
        \Demo\Example\CacheableDemo__AopProxied::getReport as private __aop__getReport;
    }

as our original code moved to the trait in the same namespace and proxy also uses the same namespace, we can use just relative class names:

class CacheableDemo implements \Go\Aop\Proxy
{
    use CacheableDemo__AopProxied {
        CacheableDemo__AopProxied::getReport as private __aop__getReport;
    }

such code looks more nicely

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement] PSR-4 compatible structure of cache files

2 participants