How To Cast Eloquent Pivot Parameters?

2020-02-08 07:54发布

I have the following Eloquent Models with relationships:

class Lead extends Model 
{
    public function contacts() 
    {
        return $this->belongsToMany('App\Contact')
                    ->withPivot('is_primary');
    }
}

class Contact extends Model 
{
    public function leads() 
    {
        return $this->belongsToMany('App\Lead')
                    ->withPivot('is_primary');
    }
}

The pivot table contains an additional param (is_primary) that marks a relationship as the primary. Currently, I see returns like this when I query for a contact:

{
    "id": 565,
    "leads": [
        {
            "id": 349,
             "pivot": {
                "contact_id": "565",
                "lead_id": "349",
                "is_primary": "0"
             }
        }
    ]
}

Is there a way to cast the is_primary in that to a boolean? I've tried adding it to the $casts array of both models but that did not change anything.

5条回答
我命由我不由天
2楼-- · 2020-02-08 08:03

Since this is an attribute on the pivot table, using the $casts attribute won't work on either the Lead or Contact model.

One thing you can try, however, is to use a custom Pivot model with the $casts attribute defined. Documentation on custom pivot models is here. Basically, you create a new Pivot model with your customizations, and then update the Lead and the Contact models to use this custom Pivot model instead of the base one.

First, create your custom Pivot model which extends the base Pivot model:

<?php namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class PrimaryPivot extends Pivot {
    protected $casts = ['is_primary' => 'boolean'];
}

Now, override the newPivot() method on the Lead and the Contact models:

class Lead extends Model {
    public function newPivot(Model $parent, array $attributes, $table, $exists) {
        return new \App\PrimaryPivot($parent, $attributes, $table, $exists);
    }
}

class Contact extends Model {
    public function newPivot(Model $parent, array $attributes, $table, $exists) {
        return new \App\PrimaryPivot($parent, $attributes, $table, $exists);
    }
}
查看更多
等我变得足够好
3楼-- · 2020-02-08 08:03

Good news! Tylor already fixed this bug:

https://github.com/laravel/framework/issues/10533

In Laravel 5.1 or higher you can use dot notation for pivot casts:

protected $casts = [
    'id' => 'integer',
    'courses.pivot.course_id' => 'integer',
    'courses.pivot.active' => 'boolean'
]
查看更多
走好不送
4楼-- · 2020-02-08 08:10

The answer provided by @patricus above is absolutely correct, however, if like me you're looking to also benefit from casting out from JSON-encoded strings inside a pivot table then read on.

The Problem

I believe that there's a bug in Laravel at this stage. The problem is that when you instantiate a pivot model, it uses the native Illuminate-Model setAttributes method to "copy" the values of the pivot record table over to the pivot model.

This is fine for most attributes, but gets sticky when it sees the $casts array contains a JSON-style cast - it actually double-encodes the data.

A Solution

The way I overcame this is as follows:

1. Set up your own Pivot base class from which to extend your pivot subclasses (more on this in a bit)

2. In your new Pivot base class, redefine the setAttribute method, commenting out the lines that handle JSON-castable attributes

class MyPivot extends Pivot {
  public function setAttribute($key, $value)
  {
    if ($this->hasSetMutator($key))
    {
      $method = 'set'.studly_case($key).'Attribute';

      return $this->{$method}($value);
    }
    elseif (in_array($key, $this->getDates()) && $value)
    {
      $value = $this->fromDateTime($value);
    }

    /*
    if ($this->isJsonCastable($key))
    {
      $value = json_encode($value);
    }
    */

    $this->attributes[$key] = $value;
  }
}

This highlights the removal of the isJsonCastable method call, which will return true for any attributes you have casted as json, array, object or collection in your whizzy pivot subclasses.

3. Create your pivot subclasses using some sort of useful naming convention (I do {PivotTable}Pivot e.g. FeatureProductPivot)

4. In your base model class, change/create your newPivot method override to something a little more useful

Mine looks like this:

public function newPivot(Model $parent, array $attributes, $table, $exists)
{
  $class = 'App\Models\\' . studly_case($table) . 'Pivot';

  if ( class_exists( $class ) )
  {
    return new $class($parent, $attributes, $table, $exists);
  }
  else
  {
    return parent::newPivot($parent, $attributes, $table, $exists);
  }
}

Then just make sure you Models extend from your base model and you create your pivot-table "models" to suit your naming convention and voilà you will have working JSON casts on pivot table columns on the way out of the DB!

NB: This hasn't been thoroughly tested and may have problems saving back to the DB.

查看更多
别忘想泡老子
5楼-- · 2020-02-08 08:11

I had to add some extra checks to have the save and load functions working properly in Laravel 5.

class BasePivot extends Pivot
{
    private $loading = false;

    public function __construct(Model $parent, array $attributes, $table, $exists)
    {
        $this->loading = true;
        parent::__construct($parent, $attributes, $table, $exists);
        $this->loading = false;
    }

    public function setAttribute($key, $value)
    {
        // First we will check for the presence of a mutator for the set operation
        // which simply lets the developers tweak the attribute as it is set on
        // the model, such as "json_encoding" an listing of data for storage.
        if ($this->hasSetMutator($key)) {
            $method = 'set'.Str::studly($key).'Attribute';

            return $this->{$method}($value);
        }

        // If an attribute is listed as a "date", we'll convert it from a DateTime
        // instance into a form proper for storage on the database tables using
        // the connection grammar's date format. We will auto set the values.
        elseif ($value && (in_array($key, $this->getDates()) || $this->isDateCastable($key))) {
            $value = $this->fromDateTime($value);
        }

        /**
         * @bug
         * BUG, double casting
         */
        if (!$this->loading && $this->isJsonCastable($key) && ! is_null($value)) {
            $value = $this->asJson($value);
        }


        $this->attributes[$key] = $value;

        return $this;
    }
}
查看更多
疯言疯语
6楼-- · 2020-02-08 08:12

In Laravel 5.4.14 this issue has been resolved. You are able to define a custom pivot model and tell your relationships to use this custom model when they are defined. See the documentation, under the heading Defining Custom Intermediate Table Models.

To do this you need to create a class to represent your pivot table and have it extend the Illuminate\Database\Eloquent\Relations\Pivot class. On this class you may define your $casts property.

<?php

namespace App;

use Illuminate\Database\Eloquent\Relations\Pivot;

class CustomPivot extends Pivot
{
    protected $casts = [
        'is_primary' => 'boolean'
    ];
}

You can then use the using method on the BelongsToMany relationship to tell Laravel that you want your pivot to use the specified custom pivot model.

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Lead extends Model
{
    public function contacts()
    {
        return $this->belongsToMany('App\Contact')->using('App\CustomPivot');
    }
}

Now, whenever you access your pivot by using ->pivot, you should find that it is an instance of your custom pivot class and the $casts property should be honoured.


Update 1st June 2017

The issue raised in the comments by @cdwyer regarding updating the pivot table using the usual sync/attach/save methods is expected to be fixed in Laravel 5.5 which is due to be released next month (July 2017).

See Taylor's comment at the bottom of this bug report and his commit, fixing the issue here.

查看更多
登录 后发表回答