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

[Request] Custom Pivot Models #2093

Closed
SiCoUK opened this issue Aug 13, 2013 · 29 comments
Closed

[Request] Custom Pivot Models #2093

SiCoUK opened this issue Aug 13, 2013 · 29 comments

Comments

@SiCoUK
Copy link

SiCoUK commented Aug 13, 2013

It would be nice if it was possible to set a custom Pivot table model for use by the BelongsToMany relationship.

Currently the 'new Pivot()' call is hard coded, I propose / request a similar solution to the custom collection that currently exists, i.e set a custom model class name if desired. If not set the pivot class is returned as it is now.

The concept allows you to utilise more data from the pivot table in a cleaner way, i.e. using getters and setters and generally having more control over the data you are trying to use.

Everything else can stay the same and the required fields can still be defined in the creation of the relationship.

Simon

@Anahkiasen
Copy link
Contributor

What would you suggest as syntax for that ? How would you set which model to use ?

@SiCoUK
Copy link
Author

SiCoUK commented Aug 13, 2013

Looking at it my original idea it probably wouldn't work as the custom collection just over rides the Eloquent method to create a collection. You could add another two method to BelongsToMany a setPivotModel and getPivotModel and resolve it in the same way you pass a string as the relationships related model.

I haven't studied the code too closely to have an exact answer.

@rmasters
Copy link

I have a bit of a need for this as well (extra fields on the pivot relationship with custom getters/setters, i.e. who created it), but I've been working around it using hasMany and belongsTo:

A -> b() { hasMany(Pivot) }
Pivot -> A() { belongsTo(A) }
Pivot -> B() { belongsTo(B) }
B -> a() { hasMany (Pivot) }

This works, but I'd prefer to access fields as members of the pivot property.

Syntactically, could this could be an extra parameter to hasMany/belongsToMany?

hasMany( string $related, string $foreignKey = null, string $pivotClass = 'Relations\Pivot' )
belongsToMany( string $related, string $table = null, string $foreignKey = null,
               string $otherKey = null, string $pivotClass = 'Relations\Pivot' )

I'm not going to pretend I know how this would be implemented however :)

@Kindari
Copy link

Kindari commented Aug 14, 2013

This would also allow for query scopes to be usable with many-many relations.

@taylorotwell
Copy link
Member

Basically BelongsToMany.php newPivot method would be modified to call $this->related->newPivot() to get the pivot instance.

@SiCoUK
Copy link
Author

SiCoUK commented Aug 15, 2013

That sounds like the best way of achieving it, thanks Taylor. Will this feature get added or does this request need to become an issue / have some code written for it?

@taylorotwell
Copy link
Member

Done.

@deiucanta
Copy link

Can you give an example on how to use this? I don't get where you specify which model should be used for the pivot.

@rmasters
Copy link

For ref (this doesn't appear to be in docs yet) - this was implemented in 45d7582.

Say you have a User and a Group, and a User can be in many Groups. Implemented as so:

class User extends Eloquent {
    public function groups() {
        return $this->belongsToMany('Group');
    }
}

class Group extends Eloquent {
    public function users() {
        return $this->belongsToMany('User');
    }
}

To add an intermediate/pivot model UserGroup, implement the newPivot method on User and Client. This is called when you try to access the pivot table of a model (i.e. $user->pivot while iterating $groups). This method receives all of the pivot requests for many-to-many's on that model. For example, if you had a many-to-many relationship for Team to User, that would also call newPivot, with the parent model as Team.

class UserGroup extends Eloquent {
    public function user() {
        return $this->belongsTo('User');
    }

    public function group() {
        return $this->belongsTo('Group');
    }

    public function posts($value) {
        return $this->hasMany('Post'); // example relationship on a pivot model
    }
}

Note: you probably want an id on this type of pivot (with relations to it) - so something like a primary key (id) and an index on unique(user_id, group_id).

class Group extends Eloquent {
    public function newPivot(Eloquent $parent, array $attributes, $table, $exists) {
        if ($parent instanceof User) {
            $pivot = UserGroup::where('user_id', '=', $attributes['user_id'])
                ->where('group_id', '=', $attributes['group_id'])
                ->first();
            if ($pivot) return $pivot;
        }

        return parent::pivot($parent, $attributes, $table, $exists);
    }
}

This is called when you use $user->pivot or $group->pivot in a loop:

/** @var Group $group */
/** @var User $user */
foreach ($group->users as $user) {
    assert($user->pivot instanceof UserGroup);
}

@joshuajabbour
Copy link
Contributor

Just wanted to update this issue in case someone comes across the instructions in the previous comment and didn't notice the "Defining A Custom Pivot Model" section in the docs.

The code in the previous comment might not work for you. This code might:

use Illuminate\Database\Eloquent\Model as Eloquent;
use Illuminate\Database\Eloquent\Relations\Pivot;

class User extends Eloquent {
    public function groups() {
        return $this->belongsToMany('Group');
    }

    public function newPivot(Eloquent $parent, array $attributes, $table, $exists) {
        if ($parent instanceof Group) {
            return new UserGroup($parent, $attributes, $table, $exists);
        }
        return parent::newPivot($parent, $attributes, $table, $exists);
    }
}

class Group extends Eloquent {
    public function users() {
        return $this->belongsToMany('User');
    }

    public function newPivot(Eloquent $parent, array $attributes, $table, $exists) {
        if ($parent instanceof User) {
            return new UserGroup($parent, $attributes, $table, $exists);
        }
        return parent::newPivot($parent, $attributes, $table, $exists);
    }
}

class UserGroup extends Pivot {
    public function user() {
        return $this->belongsTo('User');
    }

    public function group() {
        return $this->belongsTo('Group');
    }

    // Note: Adding relationships to a pivot model means
    // you'll probably want a primary key on the pivot table.
    public function posts($value) {
        return $this->hasMany('Post'); // example relationship on a pivot model
    }
}

Note the differences:

  • The pivot model needs to extend Illuminate\Database\Eloquent\Relations\Pivot
  • Fixed calling parent::newPivot (not parent::pivot) in case of other relationships
  • Added the newPivot method to the Group model

Also, adding UserGroup::user() and UserGroup::group() aren't really necessary, but do allow you to do $user_group->user and $user_group->group, which can be useful.

@rbruhn
Copy link

rbruhn commented Apr 7, 2014

@joshuajabbour - Just came across your post while looking how to create a custom pivot. In your example above, how would you save to the pivot table? I've tried the usual $user->pivot->save() with some data but that did not work.

@joshuajabbour
Copy link
Contributor

@rbruhn Using a custom pivot model doesn't change how you interact with them. Whether you're using the default Pivot class, or your own that extends it, you don't interact directly with the pivot model's save method; you can follow the instructions in the docs:

$user->pivot_relationship()->attach(1, array('custom_pivot_field' => 'abc'));
// or
$user->pivot_relationship()->sync(array(1 => array('custom_pivot_field' => 'abc')));

@rbruhn
Copy link

rbruhn commented Apr 7, 2014

Thanks!!

@ppeinsold
Copy link

First, thanks for your post! I'm facing a problem with this I cannot solve. My custom Model works. I have a Game model, a Team model and a custom GameTeam model for the pivot table. Now I have an additional Bet model which has a belongsToMany relationship to the GameTeam model and the custom GameTeam model should also have a belongsToMany relationship to the Bet model. So there is another pivot table between GameTeam and Bet.

This seems to be similar to the posts-method of your code. Could anyone provide me the code of the Post model here? I always get the following error if I want to call $bet->gameTeams

Error:
Argument 1 passed to Illuminate\Database\Eloquent\Relations\Pivot::__construct() must be an instance of Illuminate\Database\Eloquent\Model, none given

Here is my code:

Game Model


class Game extends Eloquent {

public function teams() {
    return $this -> belongsToMany('Team', 'game_team', 'game_id', 'team_id') -> withPivot('id','goals', 'approved');
}

public function newPivot(Eloquent $parent, array $attributes, $table, $exists) {
    if ($parent instanceof Team) {
        return new GameTeam($parent, $attributes, $table, $exists);
    }
    return parent::newPivot($parent, $attributes, $table, $exists);
}

}

Team model


class Team extends Eloquent {

public function games() {
    return $this -> belongsToMany('Game', 'game_team', 'team_id', 'game_id') -> withPivot('id','goals', 'approved');
}

public function newPivot(Eloquent $parent, array $attributes, $table, $exists) {
    if ($parent instanceof Game) {
        return new GameTeam($parent, $attributes, $table, $exists);
    }
    return parent::newPivot($parent, $attributes, $table, $exists);
}

}

GameTeam custom pivot model


class GameTeam extends Pivot {

protected $table = 'game_team';

public function team() {
    return $this -> belongsTo('Team');
}

public function game() {
    return $this -> belongsTo('Game');
}

public function bets() {
    return $this -> belongsToMany('Bet', 'game_team_bet', 'game_team_id', 'bet_id')-> withPivot('goals');
}

}

Bet model


class Bet extends Eloquent {

public function gameTeams() {
    return $this -> belongsToMany('GameTeam', 'game_team_bet', 'bet_id', 'game_team_id')-> withPivot('goals');
}

}

@rhulshof
Copy link

Very nice tut @joshuajabbour ! I'm using it for some weeks/months now and it works like a charm.

One question though; is it possible to hook into a attach/detach/save/delete function on the Custom Pivot Model? I would very much like to have/define Model Events for Pivot Models in the same way as Eloquent Models have; e.g. saving, saved, updating, updated, deleting and deleted.

@hhsadiq
Copy link

hhsadiq commented Jun 27, 2014

I also have exactly same scenario like @peiphb02 . And exact same error. Can anyone help. Could find find solution yet.

@ppeinsold
Copy link

@hhsadiq
The solution which worked for me was to simply create a normal models instead of the pivot table. So in my case GameTeam is now a model and I manually added the relationship for Game and Team. After that it was possible to create a pivot between GameTeam and Bet.

@hhsadiq
Copy link

hhsadiq commented Jun 27, 2014

@peiphb02
Thanks man a lot. You saved the life. I am banging my head to solve this problem for 8 hours. So Laravel does not support creating belongsToMany relation on a pivot table. I think when we can achieve the same functionality by defining two hasMany relations to normal model, why would we ever bother use Larvel Pivot. It just limits the capabilities. But if there are no custom data/relations on pivot, simple Laravel Pivot would work.

@torniker
Copy link

torniker commented Oct 7, 2014

@joshuajabbour I have similar problem and I am not able to user pivot table model (pivot) like a model. I want to use posts method you have described, but I cannot get what is the value parameter for and how to use it? I tried eager loading and normal but I get errors all the time.

@peiphb02 I tried to create model instead of pivot, but not sure what I am doing wrong still cannot get the result, could you please share code sample.

thank you in advance

@prapats
Copy link

prapats commented Feb 4, 2015

@joshuajabbour Hi, from your custom pivot model example above, is it possible to do the inverse relationship on Post? As I tried to do so, but the following error occurred:

Argument 1 passed to Illuminate\Database\Eloquent\Relations\Pivot::__construct() must be an instance of Illuminate\Database\Eloquent\Model, none given, called in /home/vagrant/dmh-rtt-importer/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php on line 733 and defined

@sgelbart
Copy link

sgelbart commented Mar 9, 2015

@peiphb02 @joshuajabbour
I feel like I've been fighting with Pivots the whole time I've been using Laravel. I have basically the same example as peipph02 and I'm really wondering what is lost by having pivots as normal models? Using peiphb02's example, Is it just that you can't do $teams->games, and you have to do $teams->gameteam->games instead? Are there any other disadvantages I'm missing?

@benjaminkohl
Copy link
Contributor

I've had the issue where I can't instantiate the pivot model. If I could do a normal attach and somehow return the instance of the pivot model that was created, that would be great. I can't figure out for the life of me how to create the pivot row then reliably get the row that was created.

@ghost
Copy link

ghost commented May 6, 2016

For those who still having trouble with this, i'm confused too but i did it work with the help on https://softonsofa.com/laravel-custom-pivot-model-in-eloquent/

@sohyl87
Copy link

sohyl87 commented Jul 12, 2016

The above solution from @joshuajabbour worked for me when using a many-to-many relation, however it doesn't work with polymorphic many-to-many relation.

The situation I have is, I need to attach media to multiple models like pages, posts etc, and when I attach the media to a page, then I can add caption to each image, but I need this to be in multiple language, so I need more than one captions (one for each language) to the relation of image & page/post.

any Idea how should I got about this ?

@sohyl87
Copy link

sohyl87 commented Jul 13, 2016

I ended up convert my Polymorphic many-to-many relationship to a normal many-to-many relationship with where clause on type field, e.g

public function media()
    {
        /**
         * Convert polymorphic many-to-many to simmple many-to-many with mediable_type field.
         */
        // return $this->morphToMany('App\Media', 'mediable')->withPivot('id', 'collection');

        return $this->belongsToMany('App\Media', 'mediables', 'mediable_id', 'media_id')
            ->where('mediables.mediable_type', 'App\Page')
            ->withPivot('id', 'collection');
    }

And then I used the solution proposed by @joshuajabbour .

So now when I attach the media to Page, I use

$page->media()->attach($fileId , ['mediable_type' => 'App\Page']);

@Yannici
Copy link

Yannici commented Nov 2, 2016

Same for me. I had to change from Polymorphic many-to-many relationship to normal many-to-many relationship. Would be nice if this could be a little bit nicer.

@Lidbetter
Copy link
Contributor

#14293

@brunocascio
Copy link

brunocascio commented Jul 5, 2017

Take a look at: https://stackoverflow.com/a/44932979/2723301

Tested on Laravel 5.4

@fico7489
Copy link

fico7489 commented Dec 7, 2017

@rhulshof take a look at this package if you want to catch those events https://github.com/fico7489/laravel-pivot

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

No branches or pull requests