diff --git a/src/Illuminate/Queue/Attributes/EagerLoad.php b/src/Illuminate/Queue/Attributes/EagerLoad.php new file mode 100644 index 000000000000..4abd9ea53519 --- /dev/null +++ b/src/Illuminate/Queue/Attributes/EagerLoad.php @@ -0,0 +1,17 @@ + $relations + */ + public function __construct(public array $relations) + { + // + } +} diff --git a/src/Illuminate/Queue/SerializesModels.php b/src/Illuminate/Queue/SerializesModels.php index db17b98755af..ec390e37c6b7 100644 --- a/src/Illuminate/Queue/SerializesModels.php +++ b/src/Illuminate/Queue/SerializesModels.php @@ -2,6 +2,10 @@ namespace Illuminate\Queue; +use Illuminate\Bus\Queueable; +use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Queue\Attributes\EagerLoad; use Illuminate\Queue\Attributes\WithoutRelations; use ReflectionClass; use ReflectionProperty; @@ -89,9 +93,22 @@ public function __unserialize(array $values) continue; } - $property->setValue( - $this, $this->getRestoredPropertyValue($values[$name]) - ); + $value = $this->getRestoredPropertyValue($values[$name]); + + $property->setValue($this, $value); + + if ( + ($value instanceof Model || $value instanceof Collection) && + ($attribute = $property->getAttributes(EagerLoad::class)[0] ?? null) + ) { + $relations = $attribute->getArguments()[0]; + + $value->load($relations); + } + } + + if (in_array(Queueable::class, class_uses_recursive($this)) && method_exists($this, 'initializeOnQueue')) { + $this->initializeOnQueue(); } } diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php index bd3b401c6576..622d1ed95a99 100644 --- a/tests/Integration/Queue/ModelSerializationTest.php +++ b/tests/Integration/Queue/ModelSerializationTest.php @@ -5,10 +5,14 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Pivot; +use Illuminate\Database\Events\QueryExecuted; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Foundation\Queue\Queueable; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Queue\Attributes\EagerLoad; use Illuminate\Queue\Attributes\WithoutRelations; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\DB; use LogicException; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; @@ -374,6 +378,109 @@ public function test_serialization_types_empty_custom_eloquent_collection() $this->assertTrue(true); } + + public function test_it_can_eagerly_load_relationships_on_models() + { + $user = User::create([ + 'email' => 'taylor@laravel.com', + ]); + + $serialize = serialize(new ModelSerializationEagerLoadRoles($user)); + + $queries = []; + DB::listen(function (QueryExecuted $query) use (&$queries) { + $queries[] = $query; + }); + + $unserialized = unserialize($serialize); + + $this->assertFalse($user->relationLoaded('roles')); + $this->assertTrue($unserialized->value->relationLoaded('roles')); + $this->assertCount(2, $queries); + } + + public function test_it_can_eagerly_load_relationships_on_collections() + { + $users = new Collection([ + User::create(['email' => 'taylor@laravel.com']), + User::create(['email' => 'tim@laravel.com']), + ]); + + $serialize = serialize(new ModelSerializationEagerLoadRoles($users)); + + $queries = []; + DB::listen(function (QueryExecuted $query) use (&$queries) { + $queries[] = $query; + }); + + $unserialized = unserialize($serialize); + + $this->assertTrue($unserialized->value->every->relationLoaded('roles')); + $this->assertFalse($users->some->relationLoaded('roles')); + $this->assertCount(2, $queries); + } + + public function test_it_eager_loads_already_loaded_relationships() + { + $user = User::create([ + 'email' => 'taylor@laravel.com', + ]); + $user->roles()->create(); + $user->load('roles'); + + $serialized = serialize(new ModelSerializationEagerLoadRoles($user)); + + // create a role while the value is serialized... + $user->roles()->create(); + + $queries = []; + DB::listen(function (QueryExecuted $query) use (&$queries) { + $queries[] = $query; + }); + $unserialized = unserialize($serialized); + + $this->assertTrue($unserialized->value->relationLoaded('roles')); + $this->assertTrue($user->relationLoaded('roles')); + $this->assertCount(2, $unserialized->value->roles); + $this->assertCount(1, $user->roles); + $this->assertCount(3, $queries); + } + + public function test_it_can_mix_without_relations_and_eager_load() + { + $order = Order::create()->load('products'); + + $serialize = serialize(new ModelSerializationWithoutRelationsAndEagerLoadLines($order)); + + $queries = []; + DB::listen(function (QueryExecuted $query) use (&$queries) { + $queries[] = $query; + }); + + $unserialized = unserialize($serialize); + + $this->assertTrue($unserialized->value->relationLoaded('lines')); + $this->assertTrue(! $unserialized->value->relationLoaded('products')); + $this->assertCount(2, $queries); + } + + public function test_it_initilize_on_the_queue() + { + $order = Order::create(); + + $serialized = serialize(new ModelSerializationWithInitilizeOnQueueHook($order)); + + $queries = []; + DB::listen(function (QueryExecuted $query) use (&$queries) { + $queries[] = $query; + }); + + $unserialized = unserialize($serialized); + + $this->assertTrue($unserialized->value->relationLoaded('lines')); + $this->assertTrue(! $unserialized->value->relationLoaded('products')); + $this->assertCount(2, $queries); + } } trait TraitBootsAndInitializersTest @@ -567,6 +674,49 @@ public function __construct(public User $user, public DataValueObject $value) } } +class ModelSerializationEagerLoadRoles +{ + use SerializesModels; + + public function __construct( + #[EagerLoad(['roles'])] + public $value, + ) { + // + } +} + +#[WithoutRelations] +class ModelSerializationWithoutRelationsAndEagerLoadLines +{ + use SerializesModels; + + public function __construct( + #[EagerLoad(['lines'])] + public $value + ) { + // + } +} + +class ModelSerializationWithInitilizeOnQueueHook +{ + use Queueable; + + public function __construct( + public $value, + ) { + // + } + + protected function initializeOnQueue() + { + $this->value->load([ + 'lines' => fn ($q) => $q->where('product_id', 5), + ]); + } +} + class ModelRelationSerializationTestClass { use SerializesModels;