-
Notifications
You must be signed in to change notification settings - Fork 11.3k
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
[11.x] Fix Macroable trait conflicting with class magic methods #54885
Conversation
…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.
This is more of a hassle now instead of convenient. |
I opened an issue to discuss it further and present my point better. #54887 There are some cases, like splitting models relationships into different modules / packages, that there is no easy way around it. And the solution is about 3 lines per method. |
Why are you trying to macro a specific model? If you already have a child model then just add the method to it... |
If the child model is optional, in a different module/package it can't be added to the current Model. There is another issue (which is what is even more important). This means that the user should know that Anyways, the solution is very simple too. |
Is the Two alternative solutions:
|
foreach (class_parents(static::class) as $parent) { | ||
if (method_exists($parent, '__call')) { | ||
/** @phpstan-ignore class.noParent, staticMethod.notFound */ | ||
return parent::__call($method, $parameters); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foreach (class_parents(static::class) as $parent) { | |
if (method_exists($parent, '__call')) { | |
/** @phpstan-ignore class.noParent, staticMethod.notFound */ | |
return parent::__call($method, $parameters); | |
} | |
} | |
if(is_callable(['parent', '__call']) { | |
return parent::__call($method, $parameters); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be nicer, but it is deprecated by now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://www.php.net/manual/en/function.is-callable.php
Can you share resources?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is_callable(['parent', '__call'])
Using parent
as a string on a callable is deprecated.
Reference: https://wiki.php.net/rfc/deprecate_partially_supported_callables
But this is fine:
if(is_callable([parent::class, '__call']) {
return parent::__call($method, $parameters);
}
See this section on the RFC: https://wiki.php.net/rfc/deprecate_partially_supported_callables#backward_incompatible_changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks I thought that is_callable is deprecated!
Thanks for clearing 🫡
foreach (class_parents(static::class) as $parent) { | ||
if (method_exists($parent, '__callStatic')) { | ||
/** @phpstan-ignore class.noParent, staticMethod.notFound */ | ||
return parent::__callStatic($method, $parameters); | ||
} | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
foreach (class_parents(static::class) as $parent) { | |
if (method_exists($parent, '__callStatic')) { | |
/** @phpstan-ignore class.noParent, staticMethod.notFound */ | |
return parent::__callStatic($method, $parameters); | |
} | |
} | |
if(is_callable(['parent', '__callStatic']) { | |
return parent::__callStatic($method, $parameters); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that would be nicer, but it is deprecated by now.
Thanks for your pull request to Laravel! Unfortunately, I'm going to delay merging this code for now. To preserve our ability to adequately maintain the framework, we need to be very careful regarding the amount of code we include. If applicable, please consider releasing your code as a package so that the community can still take advantage of your contributions! |
Problem
Adding the
Macroable
trait to a class that already implements__call
and__callStatic
breaks core functionality. This is especially problematic forEloquent models, where methods like
create()
andhydrate()
fail, butit 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
Macroable
implement__call
and__callStatic
.Macroable
does not attempt to delegate to the parent class.Solution
Updated
Macroable
's magic methods to check if the parent class can handlethe method before throwing an exception. This ensures:
Testing
work together correctly.
Impact
This fix is critical for:
Macroable
.Macroable
alongside magic methods.Without this fix, developers must choose between using
Macroable
or keepingthe class’s original magic method functionality. This is especially problematic
for modular Laravel systems, where extending models dynamically is essential.