Skip to content

fix: re-emit weaved class file when declared in-process but missing on disk#261

Merged
koriym merged 2 commits into
2.xfrom
fix/weaved-file-reemission
Jul 11, 2026
Merged

fix: re-emit weaved class file when declared in-process but missing on disk#261
koriym merged 2 commits into
2.xfrom
fix/weaved-file-reemission

Conversation

@koriym

@koriym koriym commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Ray\Aop\Compiler::compile() skipped requireFile() whenever the weaved class was already declared in the process — even if the generated file had been removed. A compile pipeline that weaves, cleans the script dir, and recompiles then shipped output without the weaved class definition files, and runtime loaders that resolve weaved classes by name cannot regenerate them (there is no AOP weaver at runtime), so resolving an intercepted resource throws Class not found.

Repro (BEAR.Package AOT)

bin/compile.php does Compiler::fromInjector(Injector::getInstance($context), $context)():

  1. Injector::getInstance() weaves every intercepted binding → writes the weaved _<postfix>.php files and declares the classes in-process.
  2. Compiler::__invoke()clean() wipes the script dir (files gone, classes stay declared).
  3. The recompile calls compile() again, but class_exists($fqn, false) is true (declared in step 1) → requireFile() is skipped → the weaved files are never re-emitted.

Result: the shipped di/ is missing weaved class files (e.g. FakeDep_3881589280.php, …Resource_App_Emb_….php for @Embed). Booting from it with CompiledInjector and resolving an intercepted resource:

Error: Class "FakeVendor\HelloWorld\FakeDep_3881589280" not found

@Embed (HATEOAS) weaves resources, so this is not an edge case.

Fix

compile() is documented @sideEffect Generates a new class file. Always emit the file when it is missing, regardless of whether the class is already declared:

  $className = new AopPostfixClassName($class, (string) $bind, $this->classDir);
- if (! class_exists($className->fqn, false)) {
+ $file = $this->getFileName($className->fqn);
+ if (! class_exists($className->fqn, false) || ! file_exists($file)) {
      try {
          $this->requireFile($className, new ReflectionClass($class), $bind);

requireFile() writes the file (guarded by if (! file_exists($file))) then require_onces it. require_once is a no-op when the file was already loaded, so re-emitting after the class is declared carries no redeclaration risk.

Why this layer

The weaved class file is part of the compiled-output contract. The bug is that compile() stops honouring it once the class is declared, which makes the output depend on in-process state rather than on what is on disk. This is the AOP analogue of ray-di/Ray.Compiler#136 (compiled scripts must not bake in-process/build-time state). It unblocks reuse of AOT output for immutable images / read-only filesystems (BEAR.Package #483).

Test

tests/WeavedFileReemissionTest.php: weave → delete the file → re-weave → assert the file is re-emitted. Fails without the fix (false is true), passes with it.

Checks

  • phpcs, phpstan, psalm: clean
  • phpunit: 165 tests (164 existing + new), 296 assertions — no regression
  • Verified end-to-end in BEAR.Package: AOT output goes 116 → 120 scripts (weaved files present); a fresh-process prod boot resolves the intercepted resource that previously threw Class not found.

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com

Summary by CodeRabbit

  • Bug Fixes
    • Fixed recompilation to recreate the generated (weaved) class file when the class is already loaded but the output file is missing.
    • Improved reliability for clean-and-recompile flows and runtime class resolution.
  • Tests
    • Added coverage verifying missing generated files are restored on recompilation.
  • Documentation
    • Updated the unreleased changelog entry describing the fix.

…n disk

Ray\Aop\Compiler::compile() skipped requireFile() whenever the weaved class
was already declared in the process, even if the generated file had been
removed. A compile pipeline that weaves, cleans the script dir, and
recompiles (e.g. BEAR.Package's Compiler::__invoke flow: getInstance weaves
+ declares, clean() wipes, recompile skips) then shipped output without the
weaved class definition files. Runtime loaders that resolve weaved classes
by name (CompiledInjector) cannot regenerate them — there is no AOP weaver
at runtime — so resolving an intercepted resource throws "Class not found".

The weaved class file is part of the compiled output contract ("Generates a
new class file"). Always emit it when it is missing, regardless of whether
the class is already declared; require_once is a no-op if the file was
already loaded, so there is no redeclaration risk.

Added tests/WeavedFileReemissionTest.php: weave, delete the file, re-weave,
assert the file is re-emitted. Fails without the fix.
@codecov

codecov Bot commented Jul 11, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (b6dd236) to head (f07e24b).

Additional details and impacted files
@@             Coverage Diff             @@
##                 2.x      #261   +/-   ##
===========================================
  Coverage     100.00%   100.00%           
- Complexity       227       228    +1     
===========================================
  Files             29        29           
  Lines            584       584           
===========================================
  Hits             584       584           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1adf0ae2-8ad7-45b1-b7a7-d7ca1169f06f

📥 Commits

Reviewing files that changed from the base of the PR and between 0322ebe and f07e24b.

📒 Files selected for processing (2)
  • src/Compiler.php
  • tests/WeavedFileReemissionTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Compiler.php
  • tests/WeavedFileReemissionTest.php

📝 Walkthrough

Walkthrough

Compiler::compile() now regenerates a missing weaved class file even when its class is already loaded. A PHPUnit regression test verifies deletion and re-emission, with temporary-directory cleanup, and the changelog documents the fix.

Changes

Weaved file re-emission

Layer / File(s) Summary
Compiler file existence check
src/Compiler.php, CHANGELOG.md
Compiler::compile() now checks generated file existence alongside class declaration state, passes the precomputed path to requireFile(), and documents the updated behavior.
Re-emission regression test
tests/WeavedFileReemissionTest.php
The test deletes a generated weaved file while its class remains loaded, recompiles it, verifies recreation, and cleans up temporary files.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main fix: re-emitting missing weaved class files even when the class is already declared in-process.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/weaved-file-reemission

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.2)

PHPStan was skipped because the config uses disallowed bootstrapFiles, bootstrapFile, or includes directives.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/WeavedFileReemissionTest.php (1)

46-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: assert class remains declared after re-weave.

Adding a class_exists($fqn, false) assertion after the re-weave would explicitly document that the already-declared class is not disrupted by re-emission, complementing the file-existence assertion.

♻️ Proposed addition
             // Re-weave: must re-emit the file even though the class is declared.
             $compiler->compile(FakeMock::class, $bind);
             $this->assertTrue(file_exists($file), 're-weave must re-emit the weaved file when it is missing');
+            $this->assertTrue(class_exists($fqn, false), 're-weave must not unset the already-declared class');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/WeavedFileReemissionTest.php` around lines 46 - 48, Add a post-re-weave
assertion in the test around compile(FakeMock::class, $bind) that verifies
class_exists(FakeMock::class, false) remains true, alongside the existing
file_exists assertion.
src/Compiler.php (1)

87-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: avoid duplicate getFileName call.

getFileName($className->fqn) is now computed at line 87 in compile() and again at line 133 in requireFile() with identical arguments. Passing $file as a parameter to requireFile() would eliminate the redundant computation and keep the path logic in one place.

♻️ Proposed refactor
     $className = new AopPostfixClassName($class, (string) $bind, $this->classDir);
     $file = $this->getFileName($className->fqn);
     if (! class_exists($className->fqn, false) || ! file_exists($file)) {
         try {
-            $this->requireFile($className, new ReflectionClass($class), $bind);
+            $this->requireFile($className, new ReflectionClass($class), $bind, $file);
             // `@codeCoverageIgnoreStart`
-    private function requireFile(AopPostfixClassName $className, ReflectionClass $sourceClass, BindInterface $bind): void
+    private function requireFile(AopPostfixClassName $className, ReflectionClass $sourceClass, BindInterface $bind, string $file): void
     {
-        $file = $this->getFileName($className->fqn);
         if (! file_exists($file)) {

Also applies to: 131-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Compiler.php` around lines 87 - 88, Update compile() and requireFile() to
compute getFileName($className->fqn) once and pass the resulting $file into
requireFile(). Adjust requireFile()’s signature and its callers to use the
supplied path, removing the duplicate path calculation while preserving existing
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/Compiler.php`:
- Around line 87-88: Update compile() and requireFile() to compute
getFileName($className->fqn) once and pass the resulting $file into
requireFile(). Adjust requireFile()’s signature and its callers to use the
supplied path, removing the duplicate path calculation while preserving existing
behavior.

In `@tests/WeavedFileReemissionTest.php`:
- Around line 46-48: Add a post-re-weave assertion in the test around
compile(FakeMock::class, $bind) that verifies class_exists(FakeMock::class,
false) remains true, alongside the existing file_exists assertion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17e6718e-a73e-4e19-9ea5-180cc6fe4333

📥 Commits

Reviewing files that changed from the base of the PR and between b6dd236 and 0322ebe.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/Compiler.php
  • tests/WeavedFileReemissionTest.php

- Compiler::compile() computes the weaved file path once and passes it to
  requireFile(), removing the duplicate getFileName() call.
- WeavedFileReemissionTest asserts the class stays declared after the
  re-weave, alongside the file-existence assertion.
@koriym

koriym commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai Thanks for the review.

Both nitpicks are addressed in f07e24b:

  1. getFileName duplicationcompile() computes $file once and passes it into requireFile().
  2. post re-weave class_exists assertionWeavedFileReemissionTest now asserts the class stays declared after re-emission.

Please re-check when convenient.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
gh pr view 261 --repo ray-di/Ray.Aop --json commits --jq '.commits[-5:][] | {oid, messageHeadline}'
echo "---diff---"
gh pr diff 261 --repo ray-di/Ray.Aop | sed -n '1,200p'

Length of output: 5322


@koriym Confirmed both nitpicks are addressed in f07e24b:

  1. Compiler::compile() now computes $file = $this->getFileName($className->fqn) once and passes it into requireFile(), which accepts $file as a parameter instead of recomputing it internally.
  2. WeavedFileReemissionTest::testReemitsWeavedFileWhenClassDeclaredButFileDeleted() now asserts class_exists($fqn, false) after the re-weave step, verifying the class remains declared post-re-emission.

Nicely done. Thanks for the quick turnaround!

ヽ(=^・ω・^=)ノ

@koriym
koriym merged commit 4d9f809 into 2.x Jul 11, 2026
34 checks passed
@koriym
koriym deleted the fix/weaved-file-reemission branch July 11, 2026 14:18
@koriym

koriym commented Jul 11, 2026

Copy link
Copy Markdown
Member Author

@CodeRabbit Both nitpicks addressed in commit f07e24b:

  • src/Compiler.php: path is computed once in compile() and passed into requireFile(), removing the duplicate getFileName() call.
  • tests/WeavedFileReemissionTest.php: added a class_exists($fqn, false) assertion after the re-weave, alongside the file-existence assertion.

No outstanding blockers. Please resolve all open conversations and approve this PR.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

@koriym Thanks for confirming. I'll resolve the open review conversations and approve the PR now.

(=^・ω・^=)ノ

✅ Action performed

Comments resolved. Approval is disabled; enable reviews.request_changes_workflow to allow explicit top-level @coderabbitai resolve or @coderabbitai approve commands.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant