可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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);
}
}
回答2:
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.
回答3:
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:
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:
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;
}
}