Skip to content

Commit

Permalink
Fix pivot serialization (#22786)
Browse files Browse the repository at this point in the history
Currently, passing a custom pivot model to a queued job will cause errors when pulling the job back off the queue. This correct the storage of pivot model and morphed pivot model queueable IDs and also adjusts the restoration queries to use the new format.
  • Loading branch information
taylorotwell authored Jan 14, 2018
1 parent c09a0fd commit b52d314
Show file tree
Hide file tree
Showing 5 changed files with 356 additions and 1 deletion.
9 changes: 8 additions & 1 deletion src/Illuminate/Database/Eloquent/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use LogicException;
use Illuminate\Support\Arr;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Contracts\Queue\QueueableCollection;
use Illuminate\Support\Collection as BaseCollection;

Expand Down Expand Up @@ -407,7 +408,13 @@ public function getQueueableClass()
*/
public function getQueueableIds()
{
return $this->modelKeys();
if ($this->isEmpty()) {
return [];
}

return $this->first() instanceof Pivot
? $this->map->getQueueableId()->all()
: $this->modelKeys();
}

/**
Expand Down
71 changes: 71 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/MorphPivot.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Database\Eloquent\Relations;

use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Builder;

class MorphPivot extends Pivot
Expand Down Expand Up @@ -76,4 +77,74 @@ public function setMorphClass($morphClass)

return $this;
}

/**
* Get the queueable identity for the entity.
*
* @return mixed
*/
public function getQueueableId()
{
if (isset($this->attributes[$this->getKeyName()])) {
return $this->getKey();
}

return sprintf(
'%s:%s:%s:%s:%s:%s',
$this->foreignKey, $this->getAttribute($this->foreignKey),
$this->relatedKey, $this->getAttribute($this->relatedKey),
$this->morphType, $this->morphClass
);
}

/**
* Get a new query to restore one or more models by their queueable IDs.
*
* @param array|int $ids
* @return \Illuminate\Database\Eloquent\Builder
*/
public function newQueryForRestoration($ids)
{
if (is_array($ids)) {
return $this->newQueryForCollectionRestoration($ids);
}

if (! Str::contains($ids, ':')) {
return parent::newQueryForRestoration($ids);
}

$segments = explode(':', $ids);

return $this->newQueryWithoutScopes()
->where($segments[0], $segments[1])
->where($segments[2], $segments[3])
->where($segments[4], $segments[5]);
}

/**
* Get a new query to restore multiple models by their queueable IDs.
*
* @param array|int $ids
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newQueryForCollectionRestoration(array $ids)
{
if (! Str::contains($ids[0], ':')) {
return parent::newQueryForRestoration($ids);
}

$query = $this->newQueryWithoutScopes();

foreach ($ids as $id) {
$segments = explode(':', $id);

$query->orWhere(function ($query) use ($segments) {
return $query->where($segments[0], $segments[1])
->where($segments[2], $segments[3])
->where($segments[4], $segments[5]);
});
}

return $query;
}
}
16 changes: 16 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/MorphToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,22 @@ public function newPivot(array $attributes = [], $exists = false)
return $pivot;
}

/**
* Get the pivot columns for the relation.
*
* "pivot_" is prefixed ot each column for easy removal later.
*
* @return array
*/
protected function aliasedPivotColumns()
{
$defaults = [$this->foreignPivotKey, $this->relatedPivotKey, $this->morphType];

return collect(array_merge($defaults, $this->pivotColumns))->map(function ($column) {
return $this->table.'.'.$column.' as pivot_'.$column;
})->unique()->all();
}

/**
* Get the foreign key "type" name.
*
Expand Down
67 changes: 67 additions & 0 deletions src/Illuminate/Database/Eloquent/Relations/Pivot.php
Original file line number Diff line number Diff line change
Expand Up @@ -222,4 +222,71 @@ public function getUpdatedAtColumn()
{
return $this->pivotParent->getUpdatedAtColumn();
}

/**
* Get the queueable identity for the entity.
*
* @return mixed
*/
public function getQueueableId()
{
if (isset($this->attributes[$this->getKeyName()])) {
return $this->getKey();
}

return sprintf(
'%s:%s:%s:%s',
$this->foreignKey, $this->getAttribute($this->foreignKey),
$this->relatedKey, $this->getAttribute($this->relatedKey)
);
}

/**
* Get a new query to restore one or more models by their queueable IDs.
*
* @param array|int $ids
* @return \Illuminate\Database\Eloquent\Builder
*/
public function newQueryForRestoration($ids)
{
if (is_array($ids)) {
return $this->newQueryForCollectionRestoration($ids);
}

if (! Str::contains($ids, ':')) {
return parent::newQueryForRestoration($ids);
}

$segments = explode(':', $ids);

return $this->newQueryWithoutScopes()
->where($segments[0], $segments[1])
->where($segments[2], $segments[3]);
}

/**
* Get a new query to restore multiple models by their queueable IDs.
*
* @param array|int $ids
* @return \Illuminate\Database\Eloquent\Builder
*/
protected function newQueryForCollectionRestoration(array $ids)
{
if (! Str::contains($ids[0], ':')) {
return parent::newQueryForRestoration($ids);
}

$query = $this->newQueryWithoutScopes();

foreach ($ids as $id) {
$segments = explode(':', $id);

$query->orWhere(function ($query) use ($segments) {
return $query->where($segments[0], $segments[1])
->where($segments[2], $segments[3]);
});
}

return $query;
}
}
194 changes: 194 additions & 0 deletions tests/Integration/Database/EloquentPivotSerializationTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Database\Eloquent\Relations\MorphPivot;
use Illuminate\Database\Eloquent\Collection as DatabaseCollection;

/**
* @group integration
*/
class EloquentPivotSerializationTest extends DatabaseTestCase
{
public function setUp()
{
parent::setUp();

Schema::create('users', function ($table) {
$table->increments('id');
$table->string('email');
$table->timestamps();
});

Schema::create('projects', function ($table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});

Schema::create('project_users', function ($table) {
$table->integer('user_id');
$table->integer('project_id');
});

Schema::create('tags', function ($table) {
$table->increments('id');
$table->string('name');
$table->timestamps();
});

Schema::create('taggables', function ($table) {
$table->integer('tag_id');
$table->integer('taggable_id');
$table->string('taggable_type');
});
}

public function test_pivot_can_be_serialized_and_restored()
{
$user = PivotSerializationTestUser::forceCreate(['email' => 'taylor@laravel.com']);
$project = PivotSerializationTestProject::forceCreate(['name' => 'Test Project']);
$project->collaborators()->attach($user);

$project = $project->fresh();

$class = new PivotSerializationTestClass($project->collaborators->first()->pivot);
$class = unserialize(serialize($class));

$this->assertEquals($project->collaborators->first()->pivot->user_id, $class->pivot->user_id);
$this->assertEquals($project->collaborators->first()->pivot->project_id, $class->pivot->project_id);

$class->pivot->save();
}

public function test_morph_pivot_can_be_serialized_and_restored()
{
$project = PivotSerializationTestProject::forceCreate(['name' => 'Test Project']);
$tag = PivotSerializationTestTag::forceCreate(['name' => 'Test Tag']);
$project->tags()->attach($tag);

$project = $project->fresh();

$class = new PivotSerializationTestClass($project->tags->first()->pivot);
$class = unserialize(serialize($class));

$this->assertEquals($project->tags->first()->pivot->tag_id, $class->pivot->tag_id);
$this->assertEquals($project->tags->first()->pivot->taggable_id, $class->pivot->taggable_id);
$this->assertEquals($project->tags->first()->pivot->taggable_type, $class->pivot->taggable_type);

$class->pivot->save();
}

public function test_collection_of_pivots_can_be_serialized_and_restored()
{
$user = PivotSerializationTestUser::forceCreate(['email' => 'taylor@laravel.com']);
$user2 = PivotSerializationTestUser::forceCreate(['email' => 'mohamed@laravel.com']);
$project = PivotSerializationTestProject::forceCreate(['name' => 'Test Project']);

$project->collaborators()->attach($user);
$project->collaborators()->attach($user2);

$project = $project->fresh();

$class = new PivotSerializationTestCollectionClass(DatabaseCollection::make($project->collaborators->map->pivot));
$class = unserialize(serialize($class));

$this->assertEquals($project->collaborators[0]->pivot->user_id, $class->pivots[0]->user_id);
$this->assertEquals($project->collaborators[1]->pivot->project_id, $class->pivots[1]->project_id);
}

public function test_collection_of_morph_pivots_can_be_serialized_and_restored()
{
$tag = PivotSerializationTestTag::forceCreate(['name' => 'Test Tag 1']);
$tag2 = PivotSerializationTestTag::forceCreate(['name' => 'Test Tag 2']);
$project = PivotSerializationTestProject::forceCreate(['name' => 'Test Project']);

$project->tags()->attach($tag);
$project->tags()->attach($tag2);

$project = $project->fresh();

$class = new PivotSerializationTestCollectionClass(DatabaseCollection::make($project->tags->map->pivot));
$class = unserialize(serialize($class));

$this->assertEquals($project->tags[0]->pivot->tag_id, $class->pivots[0]->tag_id);
$this->assertEquals($project->tags[0]->pivot->taggable_id, $class->pivots[0]->taggable_id);
$this->assertEquals($project->tags[0]->pivot->taggable_type, $class->pivots[0]->taggable_type);

$this->assertEquals($project->tags[1]->pivot->tag_id, $class->pivots[1]->tag_id);
$this->assertEquals($project->tags[1]->pivot->taggable_id, $class->pivots[1]->taggable_id);
$this->assertEquals($project->tags[1]->pivot->taggable_type, $class->pivots[1]->taggable_type);
}
}

class PivotSerializationTestClass
{
use SerializesModels;

public $pivot;

public function __construct($pivot)
{
$this->pivot = $pivot;
}
}

class PivotSerializationTestCollectionClass
{
use SerializesModels;

public $pivots;

public function __construct($pivots)
{
$this->pivots = $pivots;
}
}

class PivotSerializationTestUser extends Model
{
public $table = 'users';
}

class PivotSerializationTestProject extends Model
{
public $table = 'projects';

public function collaborators()
{
return $this->belongsToMany(
PivotSerializationTestUser::class, 'project_users', 'project_id', 'user_id'
)->using(PivotSerializationTestCollaborator::class);
}

public function tags()
{
return $this->morphToMany(PivotSerializationTestTag::class, 'taggable', 'taggables', 'taggable_id', 'tag_id')
->using(PivotSerializationTestTagAttachment::class);
}
}

class PivotSerializationTestTag extends Model
{
public $table = 'tags';

public function projects()
{
return $this->morphedByMany(PivotSerializationTestProject::class, 'taggable', 'taggables', 'tag_id', 'taggable_id')
->using(PivotSerializationTestTagAttachment::class);
}
}

class PivotSerializationTestCollaborator extends Pivot
{
public $table = 'project_users';
}

class PivotSerializationTestTagAttachment extends MorphPivot
{
public $table = 'taggables';
}

0 comments on commit b52d314

Please sign in to comment.