Skip to content

Codex review: architecture and implementation appraisal of Ray.Aop #257

@koriym

Description

@koriym

Summary

This is primarily an appraisal of Ray.Aop as a design and as an implementation.

The key distinction matters:

  • the architecture answers what kind of AOP model this library chooses to be
  • the implementation answers how well the code actually realizes that model

My conclusion is that both are strong.

The architecture is disciplined, coherent, and still defensible today. The implementation is correspondingly careful, performance-aware, and much more complete than a lightweight AOP package often is.

This issue is therefore not mainly a proposal for change. It is a review of why the current design works, what gives it durability, what I deliberately challenged, and which small hardening steps might still be worth considering.

Architecture Appraisal

1. The central architectural decision was the right one

Ray.Aop is built around inheritance-based code generation rather than delegation, copying, or runtime-heavy proxy machinery.

That choice still looks correct.

It gives the library its most valuable properties:

  • generated classes remain real subtypes of the original class
  • concrete-type DI compatibility is preserved
  • instanceof expectations remain valid
  • concrete-class return types remain valid
  • public properties remain transparent
  • internal $this->publicMethod() calls can still hit intercepted overrides

Those are not minor conveniences. They are a large part of what makes the library feel transparent in actual use.

2. The non-goals are part of the architecture's strength

The design is good partly because it refuses to chase everything.

In particular, the lack of final support now looks less like a missing feature and more like an architectural boundary that protects the model. Supporting final would likely require a second AOP strategy with weaker transparency and a much more complex mental model.

The same is true of other implicit boundaries: the current system assumes a fairly clean DI-oriented PHP codebase and optimizes for that world instead of trying to become universal.

That restraint is a strength.

3. The public API shape is unusually disciplined

The public API is small without feeling boxed in.

  • Normal use stays at Aspect::bind() and Aspect::newInstance().
  • More advanced use can step down into Matcher, Pointcut, and Bind.
  • Internal machinery remains inside Compiler, Weaver, and invocation handling.

This layering is one of the best parts of the library design. It gives the package a low-friction entry point while still leaving room for deeper control.

4. The compile-time/runtime cost split is architecturally excellent

The architecture puts complexity where it belongs.

Structural work is done at generation time. Runtime execution remains close to:

  • method override
  • binding lookup
  • interceptor chain

For AOP in PHP, that is exactly the right bias. It respects the runtime cost model rather than introducing abstraction in the hot path for its own sake.

5. A design from 2012 that still reads well today

This architecture dates back to 2012, but it does not read like an old system surviving by inertia.

It reads like a design that made a small number of correct decisions early:

  • preserve subtype behavior
  • keep runtime lean
  • accept explicit boundaries
  • avoid unnecessary machinery

That is why it still feels relevant.

Implementation Appraisal

1. The code structure reflects the architecture cleanly

The main classes have clear and durable responsibilities:

  • Aspect as the user-facing entry point
  • Bind as the binding accumulator
  • Compiler as generation orchestration
  • Weaver as loading and instance construction
  • ReflectiveMethodInvocation as runtime joinpoint execution

This is not just clean in theory. It is reflected well in the code. The implementation does not feel muddled or drifted.

2. The token-based approach was a good implementation decision

The tokenizer-based generation should not be described as “taking the easy route”.

As I understand it, the point was to remove the dependency on PHP-Parser and improve performance. Judged on those terms, it was a good decision.

It keeps:

  • dependency weight lower
  • code generation fast
  • the runtime model simple
  • the implementation understandable

And it still manages to cover a surprisingly broad part of modern PHP method signatures.

3. Signature preservation is handled with real care

This implementation is more thorough than many lightweight AOP generators.

It preserves or reproduces:

  • method visibility and modifiers
  • attributes
  • parameter types
  • nullable types
  • union and intersection types
  • references
  • variadics
  • return types
  • readonly-specific interception behavior

That level of detail matters because inheritance-based AOP only works well if generated subclasses remain structurally credible.

4. Performance awareness is visible throughout

The implementation is not only theoretically efficient; it is written with performance in mind.

  • work is pushed into code generation
  • generated classes are written to files and reused
  • runtime invocation stays lightweight
  • matching and binding are resolved before hot-path execution

This matters because AOP abstractions become unappealing very quickly if they are elegant but slow. Ray.Aop avoids that trap.

5. The implementation is pragmatic without being sloppy

What stands out is not just speed, but discipline.

The code does not over-abstract. It does not try to become a general-purpose source transformation system. It stays focused on the exact amount of generation needed for the chosen model.

That is a good implementation quality in its own right.

What I Deliberately Challenged

I deliberately looked at the places where a library like this often becomes fragile:

  • class selection from source files
  • matching and binding semantics
  • unsupported inheritance edge cases
  • generated method signature correctness
  • transparency tradeoffs versus hypothetical delegate-based alternatives

Things I Challenged and Found Acceptable

1. Multiple classes in one file

I confirmed that weaving assumes a one-class-per-file model and can target the wrong class if that assumption is violated.

I do not think this is a meaningful criticism of the current architecture. In the intended project shape, one primary class per file is a reasonable assumption. I would treat this as an explicit unsupported input rather than something worth complicating the generator for.

2. Duplicate annotatedWith(...) pointcuts

I confirmed that duplicate annotation-based pointcuts currently behave as last-wins rather than being merged.

If intentional, I think this is acceptable. In AOP, deterministic overwrite semantics are often better than implicit composition. This reads more like a contract choice than a flaw.

3. Non-support for final

I spent the most time challenging this one, because it is the most tempting area for expansion.

After walking through delegate-based and copy-based alternatives, I think the current restriction is still correct. Trying to support final would likely weaken some of the architecture's strongest properties while increasing both implementation complexity and user-facing caveats.

Low-Priority Hardening Ideas

These are minor compared with the overall appraisal.

  • It would be reasonable to fail fast with a library exception when a final class or intercepted final method is encountered, instead of reaching a PHP fatal during generated class loading.
  • It would be useful to document the intended boundaries more explicitly:
    • no support for final class
    • no support for intercepted final method
    • assumption of one primary class per file
    • last-wins behavior for duplicate annotatedWith(...) pointcuts

I view these as contract-hardening improvements, not architectural corrections.

Bottom Line

The strongest conclusion from this review is that Ray.Aop still deserves confidence both architecturally and implementation-wise.

The architecture is good because it makes the right tradeoffs. The implementation is good because it realizes those tradeoffs with discipline, performance awareness, and more care than many libraries in this space.

That combination is why the project still reads well today.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions