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

[5.6] Fix pivot serialization #22786

Merged
merged 9 commits into from
Jan 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding getQueueableId() to the Model base class, and overriding it in Pivot? Then you can always map to getQueueableId() and get rid of the instanceof. 😉

}

/**
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo. "prefixed at", should be?

*
* @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';
}