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

[5.5] Belongs to many related local key #17915

Merged
merged 4 commits into from
Mar 9, 2017
Merged

[5.5] Belongs to many related local key #17915

merged 4 commits into from
Mar 9, 2017

Conversation

alberto-bottarini
Copy link
Contributor

Hi, I've added also the configurability of the "related" local key using BelongsToMany relationship.

Take this example

USERS
- id
- email

ROLES
- id
- code

ROLE_USER
- user_email
- role_code

With this PR we can define this relationship like this:

class User
{
    public function roles() 
    {
         return $this->belongsToMany('App/Role', 'role_user', 'user_email', 'role_code', 'email', 'code');
    }
}
class Role
{
    public function users() 
    {
         return $this->belongsToMany('App/User', 'role_user', 'role_code', 'user_email', 'code', 'email');
    }
}

PS sorry for this second PR, I should have done one.

@taylorotwell
Copy link
Member

In this really needed. It's starting to feel silly. Passing 6-7 arguments to belongs to many?

@alberto-bottarini
Copy link
Contributor Author

There are a lot of needed and potentially useful options. It's a compromise, most of them are optional arguments. Why HasOne, HasMany and BelongsTo have all the available options out-of-the-box and BelongsToMany not?

Working with legacy dbs, I had a lot of issue about this topic.
The only way to use BelongsToMany, without extend and override it, was to change at runtime the models' primary keys, but I consider this an anti-pattern.

What do you think?

@GrahamCampbell GrahamCampbell changed the title Belongs to many related local key [5.5] Belongs to many related local key Feb 13, 2017
@riesjart
Copy link
Contributor

One suggestion would be to keep the belongsToMany method in the HasRelationships trait the same and use setters for both keys instead. The setters can return '$this'. This way the parameter count remains the same, but one can adjust the relation for that one particular case.

@KKSzymanowski
Copy link
Contributor

It would be nice to have a fluent interface for setting up relationships.
Post-construct setters won't work because eg. joins are set up during object instantiation and they rely on these parameters, but how about:

return $this->belongsToMany('App/Role', function ($relationship) {
    $relationship->setTable('role_user')
                 ->setForeignKey('user_email')
                 ->setRelatedKey('role_code')
                 ->setParentKey('email')
                 ->setLocalKey('code')
                 ->setRelationName('foo');
});

This way you wouldn't have to remember the order of arguments and also if you just want to change eg. the relation name you wouldn't have to pass the silly null, null, null, null, null in the belongsToMany call.

@KKSzymanowski
Copy link
Contributor

KKSzymanowski commented Feb 15, 2017

There is at least one problem with implementing this solution.

set* methods need to be public in order to use them in a closure but exposing them would confuse users because they might think they can eg. set the table name after the relationship has been instantiated.

We could always bind the closure to the relationship object(and use protected methods) but this would be probably even more confusing to use $this meaning BelongsToMany::$this not Model::$this.

One way to solve this is to introduce an intermediate object accepting all needed parameters and then using it to copy the values to the relationship object in the constructor.
Here's an example implementation: https://gist.github.com/KKSzymanowski/512261a9afea64e0c0ffeb1b69915514
The Setter(or whatever you wanna call it) class could check the debug_backtrace()[2]['class'] to provide more meaningful error when an inappropriate method is called but I don't know how reliable would that be so I didn't include that.
Provided implementation is about 20x slower then explicitly passing all arguments(tested for 7), but it's still quite fast(1 000 000 instantiations take 4 seconds) and most of the time the default values are good enough so this will be used only in edge cases.
EDIT: Without reflections it will probably be a lot faster, haven't tested it though.

Or, you know, the Model::belongsToMany could accept an array of options merged with the defaults, but it's 2017, we don't do that anymore.

@lkmadushan
Copy link
Contributor

lkmadushan commented Feb 15, 2017

@KKSzymanowski nice idea.

@arjasco
Copy link
Contributor

arjasco commented Feb 15, 2017

@KKSzymanowski I was definitely going to have a go with this idea myself. 😄 Makes much more sense to me, I do sometimes forget the order of the foreign key and local key. With your example it makes it very obvious what all the columns are.

Not sure on the closure business, but I'm think this could be adapted to allow both ways. I would lose the set bit too. Just..

$this->belongsToMany(Role::class)->foreignKey(...)->relatedKey(...)

👍

@alberto-bottarini
Copy link
Contributor Author

The idea is cool, more fluent and clear (clearer than the list of parameters).
But for consistency, this approach should be adapted for all the optional parameters in all the relation type object (BelongsTo, HasMany...).

@arjasco your example collides with the confusion issue raised by @KKSzymanowski: the new methods you suggest, will be available also after the relationship definition.

@KKSzymanowski
Copy link
Contributor

@taylorotwell Are you interested in my proposal? I can have it done by the weekend for all relationships.

@oceanwap
Copy link

oceanwap commented Mar 5, 2017

Things get complicated sometimes, might not be useful for everyday but optional configuration is a must thing.

@taylorotwell
Copy link
Member

@KKSzymanowski i think it looks pretty good. I would avoid "set" on the front of all the methods and just have it be like ->table ->foreignKey, etc.

@taylorotwell taylorotwell merged commit c0f425c into laravel:master Mar 9, 2017
@alberto-bottarini alberto-bottarini deleted the belongs-to-many-local-key branch March 10, 2017 10:11
@KKSzymanowski
Copy link
Contributor

@taylorotwell Do you want it for 5.4 or 5.5?

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.

None yet

7 participants