-
Notifications
You must be signed in to change notification settings - Fork 28
[Proposal] Use Macroable trait on eloquent models #1473
Comments
The problem is that the eloquent models support dynamic where clauses and __call() is already in use. |
@imanghafoori1 In this instance, I’d expect a I do this in an application I develop with Cashier’s |
This does not solve the problem, does it ?! The goal is not to do that. Let me shed some light on it. If you think about the tables. The We need almost the same thing for the models of these two tables. Here the |
@imanghafoori1 Then how is your
I would not install a package that is modifying my application’s classes at runtime. What if my model isn’t called You should always be in control of what a package is doing from your application. This is why things like service providers exist. A package should not be making assumptions on how things are structured in your application and what they’re called, and then modifying the behaviour of your application at runtime simply because you’ve installed it. |
I am simplifying the example. I have thought about the idea for months and found all the answers, but it is not possible to describe everything here all at once.
You can put the full namespace of the
You define it in the comments service provider boot method. The name of the relation can also be Also you may take a look at the way october cms has done this very nicely. |
Perhaps it best if you submit a PR to the framework with your proposal. Seems like much discussion isn’t needed here. If you have a good solution to this problem that will work in most situations, then let the code and accompanying PR speak for itself. |
@michaeldyrynda Well, that doesn't work. The community should understand the need and vote for it. If you look for examples you can take a look at october cms plugins. |
Well, I’m against the idea of a packaged modifying my app at run time like you’re suggesting. I’d much rather opt-in to the behaviour explicitly. Sounds like your implementation is application-specific where you control both the app and the package; that’s cool, but implementing this in a useful way for the wider community I’d be interested in seeing. Sounds like there’d be a lot of config just so you avoid using a trait to compose the desired behaviour - this is making assumptions about my app, namespaces, class names, etc. |
It is not a modification of the app. laravel extensively uses the macroable trait. Is it a bad thing? The fact that a model can introduce a method on an other model does not mean that it affects the behavior of the other methods. And let me mention that new modern languages like I do not want to copy/paste go-lang code here. You may google oop in go and see oop the concepts. Do you think that the creators of those languages didn't know what they where doing ?! or there was a good reason for such a design? |
@imanghafoori1 No, but Laravel uses the macroable trait so you can modify the framework; not so the framework or third-party packages have the ability to modify your application’s first-party code.
I beg to differ. If a model is adding methods to other models in my application at runtime, then that can produce undesirable side effects if it’s overriding an already-defined method, and cause difficult-to-diagnose bugs that happen only at runtime.
It sounds like you want to write Go or Rust. Have you thought about using those languages if you feel they’re more appropriate?
Laravel not copying another language’s paradigms is not saying those languages and their creators or wrong or “didn’t know what they [were] doing”. That’s simply a false dichotomy. |
It's worth adding two cents to this discussion:
Not exactly - at least Rust forbids frivolous implementations: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=b82f7fc53c609572b76a3b7cec7928bc. You can implement straight-away custom methods for types created by yourself and declared in your module only, so your case of having many modules fiddling with other modules' types would fail anyway. What you can do is create a trait and then implement that trait for any other type (https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=cf35cc82989eeef903c4c7389a61e9f5), but it's a completely different mechanism that does not affect the original type at all. |
@Patryk27 Thank you for your exact investigation. For eloquent models, whether we are going to affect the original behavior or not... |
I have had this problem in my mind around a couple of months or so. |
After all, it just boils down not to violate the |
After reviewing the L in SOLID I found that adding a method to a type is proven to be harmless in theory, The problem can occur when we The problem actually comes from |
Harmless in theory, but not in practice. Packages should not be modifying client code in the same way the framework doesn’t. Yes, the components Laravel offer are macro-able, but that’s so the client (first party) can add methods to framework classes, and not as a mechanism for third-party code vendors to add methods to first-party classes. You can quote academic texts as much as you want, but it doesn’t mean a mechanism for third-party packages to override code in an application any better. In fact, it makes it an attack vector for third parties if they can take over a popular package, add macros that add methods that collect data, or destroy databases, or some other nefarious action. |
Agree, but as I just said it is not a modification, it is just adding. so the current behavior remains unchanged. What I talked about was safety against Currently any package can setup a listener |
instead of putting a new method on the User Model class CommentingPackageServiceProvider
{
public function boot () {
Comment::belongsTo(User::class, 'user');
User::hasMany(Comment::class, 'comments');
// Or to be flexible and configurable
// Will return an string like : '\App\Models\User'
$userClass = config('commenting.user_model');
Comment::belongsTo($userClass, 'user');
$userClass::hasMany(Comment::class, 'comments');
}
} So when the commenting package (module) gets installed or uninstalled the user package (module) does not need to change at all. |
To me this feels the package should just have a trait, which users of the package can add to the model they want. |
@rs-sliske I know adding traits is the current habit of the community. |
Won't you end up coupling them anyway? Sure the module will register a relation like |
@donnysim Not actually, usage of the Only the So if we uninstall the The user module does not know whether a commeting system is installed or not. The thing I am suggesting is already working for a long time in october CMS without any problem. |
That's assuming the comments will only be used with the user. You won't be using your
That's the problem - the modules which depend. Remove the module and your app breaks. Every module will depend on the |
@donnysim You are totally right and I agree with you, But we are trying to decouple the modules used by the main application. if you provide a |
So if I install a couple a packages that are automatically adding methods to classes in both my application and other packages (i.e. adding likes to comments) then I’m quickly not going to know what the hell’s going on in my application because packages are running amok adding methods left, right, and centre. |
@martinbean I know, You are right, that danger exists anyway. Currently, When you install a package ( A ), that can bind many keys on the container and override other packages keys and the keys you have bound in your
But this is not the end of the world, package developers are careful enough to choose specific keys to avoid conflicts. Plus, the relation names can be read from config files to resolve possible conflicts. Yes, of course, it needs some discipline and the introduced relation names should be declared in the module docs so that package users can understand what is coming from where. Inevitably when people want to share code, they should reach consensus, and conform to contracts to some level, otherwise chaos will happen. |
This idea is now working as a package |
@michaeldyrynda I think there is a useful scenario for having models macroable. Being able to make models macroable will make our code much more readable in cases like testing environment in which we don't want to populate model with methods only used in testing. Let me explain the need with an example: private function assertVideoSourceExists($video) { ... }
private function assertMissingVideoSource($video) { ... } Now let say that we need the assertion in out of a single test case file. In this case, we usually extract assertions in a trait like The problem comes in when we have more models (like $this->assertMissingVideoSource($video);
// ...
$this->assertAudioSourceExists($extracted_audio);
$this->assertVideoSourceExists($video);
$this->assertCoverImageExists($cover); I think it would be awesome and much more readable (since readability is important in tests) to do the assertions like this: $video->assertMissingSource();
// ...
$extracted_audio->assertSourceExists();
$video->assertSourceExists();
$cover->assertImageExists(); Accomplishing this readibility required models to be macroable (again, since we don't want to put test methods in the model itself) and being able to define them like this: Video::macro('assertMissingSource', function() {
// assertion logic
}) The reduced code letters might not be that much but saying that a helper function like Of course, this is only an example out of other uses of macroable models in testing environment. |
It is my understanding that the whole If you need to provide model related functionality in packages you provide a trait that the user adds to whichever model they want. If you're adding methods to models, using Macroable, those methods are going to be super magic and super hidden. Unless the developer knows EXACTLY how your package works they're not really going to understand what's happening, are they? IDEs won't pick this stuff up, and since it's entirely dynamic and done at run time it'll be super hard for anyone to track down. All in all, it's an unnecessary bad idea. @AmirrezaNasiri You absolutely do not, under any circumstance, want to start introducing unit test functionality into models. |
Such functionality is already there. You add method through the scope, and add the autoload attribute through event if necessary /** AppServiceProvider */
public function boot()
{
User::addGlobalScope(new class implements Scope {
/**
* @param Builder $builder
*/
public function extend(Builder $builder) {
$builder->macro("profile", function (Builder $builder) {
return $builder->getModel()->hasOne(Profile::class);
});
}
});
User::first()->profile // return null anyway
User::with('profile')->first()->profile // return profile if exists
User::first()->load('profile')->profile // return profile if exists
// Autoload of profile attribute
User::retrieved(function (User $model) {
$model->load('profile');
$model->makeHidden('profile');
});
} |
Seeing that the macroable ability will probably never be added to the core, I made a little package a few days ago to add macros to Eloquent models. Github repo: Laravel Macroable Models What do you think of the implementation? |
@martinbean |
Seems like a bit of a toxic reply to be honest... |
Anyway, too much resistance may kill off useful ideas. closing the issue since the idea is now merged. |
@imanghafoori1 Iman, get rid of that massive chip on your shoulder. No one “laughed” or called you “stupid” at all. We replied with well-reasoned answers as to why your approach was bad. Just because we didn’t agree with your implementation doesn’t mean those replies were “toxic”. Yes, Taylor’s merged something that does something similar but in a different, safer way. So step off with your, “Ha, I told you so” attitude. |
The fact that laravel packages (or
laravel modules
in other words) can not introduce new relations on the models that exist in the other modules *without touching their code) creates coupling problems.For example if we have a
commenting
package which has aComment
model and some migrations to hold comments in acomments
table.when installed there is no way, except to expose a trait to put on the
User
model to reflect the relation between Comments and User. Ok this is not the end of the world, we can live with it.We use the trait and
$user->comments
would be accessible.But, when we add an other package to help us have
like
anddislike
on theComment
model.then how to put the trait on a model which already exists within the vendor folder ?!
From the theoretical point of view, this is violating the
open-close principle
.For example, when you start to modularize your application with :
https://github.com/nWidart/laravel-modules
you immediately realize that models in different modules can not be totally decoupled.
When you add a add a comment module : the User module code should change to define new relations.
then when you add
like
module both the comment and user module code should be modified.The text was updated successfully, but these errors were encountered: