11

I have a pivot table that connects users to workspaces. On the pivot table, I also have a column for role, which defines the users role for that workspace. Can I provide Accessor (Getter) & Mutator (Setter) methods on the role inside the pivot table? I have been trying to look all over, but details on pivot tables in eloquent are pretty sparse.

I am not sure if I have to setup a custom pivot model? If I do, an example would be awesome as the documentation on pivot models is very basic.

Thanks.

ATLChris
  • 3,198
  • 7
  • 39
  • 65

4 Answers4

10

If all you need to do is access additional fields on the pivot table, you just need to use the withPivot() method on the relationship definition:

class User extends Model {
    public function workspaces() {
        return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
    }
}

class Workspace extends Model {
    public function users() {
        return $this->belongsToMany('App\Models\User')->withPivot('role');
    }
}

Now your role field will be available on the pivot table:

$user = User::first();

// get data
foreach($user->workspaces as $workspace) {
    var_dump($workspace->pivot->role);
}

// set data
$workspaceId = $user->workspaces->first()->id;
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);

If you really need to create accessors/mutators for your pivot table, you will need to create a custom pivot table class. I have not done this before, so I don't know if this will actually work, but it looks like you would do this:

Create a new pivot class that contains your accessors/mutators. This class should extend the default Pivot class. This new class is the class that is going to get instantiated when User or Workspace creates a Pivot model instance.

namespace App\Models;
use Illuminate\Database\Eloquent\Relations\Pivot;
class UserWorkspacePivot extends Pivot {
    getRoleAttribute() {
        ...
    }
    setRoleAttribute() {
        ...
    }
}

Now, update your User and Workspace models to create this new pivot table class, instead of the default one. This is done by overriding the newPivot() method provided by the Model class. You want to override this method so that you create an instance of your new UserWorkspacePivot class, instead of the default Pivot class.

class User extends Model {
    // normal many-to-many relationship to workspaces
    public function workspaces() {
        // don't forget to add in additional fields using withPivot()
        return $this->belongsToMany('App\Models\Workspace')->withPivot('role');
    }

    // method override to instantiate custom pivot class
    public function newPivot(Model $parent, array $attributes, $table, $exists) {
        return new UserWorkspacePivot($parent, $attributes, $table, $exists);
    }
}

class Workspace extends Model {
    // normal many-to-many relationship to users
    public function users() {
        // don't forget to add in additional fields using withPivot()
        return $this->belongsToMany('App\Models\User')->withPivot('role');
    }

    // method override to instantiate custom pivot class
    public function newPivot(Model $parent, array $attributes, $table, $exists) {
        return new UserWorkspacePivot($parent, $attributes, $table, $exists);
    }
}
patricus
  • 59,488
  • 15
  • 143
  • 145
  • I very much appreciate your detailed response, but unfortunately, this does not work. The 'getRoleAttribute' and 'setRoleAttribute' never get applied. – ATLChris Mar 08 '15 at 13:49
  • @ATLChris I just tried this is a test environment and it looks like it is working. Can you post your current code, as well as what you're trying to do, what you're expecting to happen, what is actually happening, and any errors you're getting? – patricus Mar 08 '15 at 23:04
  • 1
    @ATLChris this solution is working. I have solved this issue using this approach. For more detail you can read the article [Laravel – Custom pivot model in Eloquent](http://softonsofa.com/laravel-custom-pivot-model-in-eloquent/) – hhsadiq Jun 30 '15 at 11:34
  • @patricus This will not work for mutators when calling `$user->workspaces()->attach($workspace, ['role' => 'some role']);` or `sync` in the same manner. This could cause some big problems later when someone tries to call this. My answer tries to fix this aspect of your answer but it is a lengthy and ugly process. I +1ed your answer because there is some really good info but you should add that it doesn't mutate the data passed in `attach` and `sync`. – DutGRIFF Oct 30 '15 at 17:31
1

I figured out how to use Accessors and Mutators on the Pivot table (I'm using Laravel 5.8)

You must use using() on your belongsToMany relationships, for example:

namespace App;
use Illuminate\Database\Eloquent\Model;

class User extends Model {
    public function workspaces() {
        return $this->belongsToMany('App\Workspace')->using('App\UserWorkspace');
    }
}
namespace App;
use Illuminate\Database\Eloquent\Model;

class Workspace extends Model {
    public function users() {
        return $this->belongsToMany('App\User')->using('App\UserWorkspace');
    }
}

So, use your Pivot model:

namespace App;
use Illuminate\Database\Eloquent\Relations\Pivot;

class UserWorkspace extends Pivot {
    public function getRoleAttribute() {
        // your code to getter here

    }
    public function setRoleAttribute($value) {
        // your code to setter here
    }
}
Jonas WebDev
  • 363
  • 4
  • 12
0

This is a difficult question. The solutions I can think of are smelly and may cause some problems later on.

I am going to extend on Patricus's answer to make it work.

I was going to comment on Patricus's answer but there is simply too much to explain. To make his solution work with attach and sync we must do some ugly things.

The Problem

First let's identify the problem with his solution. His getters and setters do work but the belongsToMany relationship doesn't use the Pivot model when running sync, attach, or detach. This means every time we call one of these with the $attributes parameter the non-mutated data will be put into the database column.

// This will skip the mutator on our extended Pivot class
$user->workspaces()->attach($workspace, ['role' => 'new role value']);

We could just try to remember that every time we call one of these we can't use the second parameter to attach the mutated data and just call updateExistingPivot with the data that must be mutated. So an attach would be what Patricus stated:

$user->workspaces()->attach($workspace);
$user->workspaces()->updateExistingPivot($workspaceId, ['role' => 'new role value']);

and we could never use the correct way of passing the pivot attributes as the attach methods second parameter shown in the first example. This will result in more database statements and code rot because you must always remember not to do the normal way. You could run into serious problems later on if you assume every developer, or even yourself, will just know not to use the attach method with the second parameter as it was intended.

The Solution (untested and imperfect)

To be able to call attach with the mutator on the pivot columns you must do some crazy extending. I haven't tested this but it may get you on the right path if you feel like giving it a try. We must first create our own relationship class that extends BelongsToMany and implements our custom attach method:

use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class UserWorkspaceBelongsToMany extends BelongsToMany {
    public function attach($id, array $attributes = [], $touch = true)
    {
        $role = $attributes['role'];
        unset($attributes['role']);
        parent::attach($id, $attributes, $touch);
        $this->updateExistingPivot($id, ['role' => $role], $touch);
    }
    // You will need sync here too
}

Now we have to make each Model::belongsToMany use our new UserWorkspaceBelongsToMany class instead of the normal BelongsToMany. We do this by mocking the belongsToMany in our User and Workspace class:

// put this in the User and Workspace Class
public function userWorkspaceBelongsToMany($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null)
{
    if (is_null($relation)) {
        $relation = $this->getBelongsToManyCaller();
    }

    $foreignKey = $foreignKey ?: $this->getForeignKey();

    $instance = new $related;

    $otherKey = $otherKey ?: $instance->getForeignKey();

    if (is_null($table)) {
        $table = $this->joiningTable($related);
    }

    $query = $instance->newQuery();

    return new UserWorkspaceBelongsToMany($query, $this, $table, $foreignKey, $otherKey, $relation);
}

As you can see, we are still calling the database more but we don't have to worry about someone calling attach with the pivot attributes and them not getting mutated.

Now use that inside your models instead of the normal belongsToMany:

class User extends Model {
    public function workspaces() {
        return $this->userWorkspaceBelongsToMany('App\Models\Workspace')->withPivot('role');
    }
}

class Workspace extends Model {
    public function users() {
        return $this->userWorkspaceBelongsToMany('App\Models\User')->withPivot('role');
    }
}
DutGRIFF
  • 5,103
  • 1
  • 33
  • 42
0

Its impossible to use setters, will not affect pivot table... make the change in the controller instead.

Afraz Ahmad
  • 5,193
  • 28
  • 38