Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[13.x] Fix Macroable trait conflicting with class magic methods #54884

Closed
wants to merge 8 commits into from

Conversation

rsd
Copy link

@rsd rsd commented Mar 3, 2025

Problem

Adding the Macroable trait to a class that already implements __call and
__callStatic breaks core functionality. This is especially problematic for
Eloquent models, where methods like create() and hydrate() fail, but
it affects any class using these magic methods.

Reproduction: A full demonstration of this issue can be found at
https://github.com/rsd/macroable-test.

Root Cause

  • Both the base class and Macroable implement __call and __callStatic.
  • PHP gives precedence to the trait's implementation.
  • Macroable does not attempt to delegate to the parent class.

Solution

Updated Macroable's magic methods to check if the parent class can handle
the method
before throwing an exception. This ensures:

  • Macro functionality is preserved.
  • Original class methods continue to work.

Testing

  • Added tests using Eloquent models as a real-world example.
  • Verified that both standard Eloquent methods and custom macros
    work together correctly.

Impact

This fix is critical for:

  • Eloquent models that use Macroable.
  • Other Laravel components relying on magic methods (Collections, Query Builder, etc.).
  • Any class using Macroable alongside magic methods.
  • Modular Laravel applications, where modules extend core models dynamically.

Without this fix, developers must choose between using Macroable or keeping
the class’s original magic method functionality. This is especially problematic
for modular Laravel systems, where extending models dynamically is essential.

rsd added 8 commits March 3, 2025 19:31
…odels

When an Eloquent model uses the Macroable trait, core functionality breaks due to
conflicting magic method implementations. Macroable's `__call` and `__callStatic`
override Eloquent's methods, causing exceptions when internal methods aren't macros.

This test demonstrates two failures:
- Static methods like `create()` fail with `BadMethodCallException`.
- Query methods that depend on `hydrate()` also fail with `BadMethodCallException`.

This issue affects modular applications that extend Eloquent models dynamically.
…ic methods

This fixes an issue where adding the Macroable trait to Eloquent models
breaks core functionality. The conflict arises because both Eloquent's
Model class and Macroable implement `__call` and `__callStatic`, and PHP
gives precedence to the trait's implementation.

Issue details:
- Methods like `create()`, `find()`, and `hydrate()` were intercepted by Macroable.
- Since these weren't registered as macros, `BadMethodCallException` was thrown.

Fix implemented:
- Updated Macroable's magic methods to check if the parent can handle the call
  before throwing an exception.
- Ensures compatibility between dynamic macros and Eloquent's method resolution.

Impact:
- Prevents breaking core Eloquent functionality in modular applications.
- Allows extending models dynamically while preserving Eloquent behavior.

Tests:
- Added test cases to validate that both Eloquent methods and custom macros work.
…rovider

Updated the Macroable trait test to ensure compatibility with macros registered
via a ServiceProvider. This improves test coverage by validating that macros
added in a ServiceProvider are recognized by Eloquent models.
Reformatted the test files to conform to Laravel Pint's coding style standards.
This ensures consistency across the codebase and improves readability.
…heritance

Updated method resolution by replacing `method_exists()` with `is_callable()`
to better support multi-level inheritance. This ensures that overridden methods
in parent classes are properly detected.

Additional improvements:
- Updated return type checking for better type safety.
Reformatted the test files to conform to Laravel Pint's coding style standards.
This ensures consistency across the codebase and improves readability.
…gration checks

Refactored the logic by replacing explicit integration error checks with a
`try-catch` block. This simplifies error handling and improves maintainability.
Fixed PHPStan errors related to parent method calls in the `Macroable` trait
by improving method existence checks and adding PHPStan ignore directives.

Changes include:
- Checked each parent class for `__call` and `__callStatic` before invoking them.
- Ensured parent methods are only called if they exist.
- Added PHPStan ignore directives for specific error types:
  - `class.noParent` for cases where the class has no parent.
  - `staticMethod.notFound` for cases where the parent doesn't have the method.
@stancl
Copy link
Contributor

stancl commented Mar 4, 2025

You don't need to open a PR for 13.x if there's already one for 12.x.

@crynobone
Copy link
Member

Duplicate of #54885

@crynobone crynobone marked this as a duplicate of #54885 Mar 4, 2025
@crynobone crynobone closed this Mar 4, 2025
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.

3 participants