diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 26d62b273fee..c9fd5868e9fa 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -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; @@ -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(); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php index b7a2f34195b8..a8a9210f0e45 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphPivot.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent\Relations; +use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Builder; class MorphPivot extends Pivot @@ -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; + } } diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 15f5cfd028c4..da6658513af9 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -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. * diff --git a/src/Illuminate/Database/Eloquent/Relations/Pivot.php b/src/Illuminate/Database/Eloquent/Relations/Pivot.php index e80aa911d567..c25bb011f3b9 100755 --- a/src/Illuminate/Database/Eloquent/Relations/Pivot.php +++ b/src/Illuminate/Database/Eloquent/Relations/Pivot.php @@ -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; + } } diff --git a/tests/Integration/Database/EloquentPivotSerializationTest.php b/tests/Integration/Database/EloquentPivotSerializationTest.php new file mode 100644 index 000000000000..6092be09b89b --- /dev/null +++ b/tests/Integration/Database/EloquentPivotSerializationTest.php @@ -0,0 +1,194 @@ +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'; +}