diff --git a/composer.json b/composer.json index 971aa5c0b257..0b73cbf466e0 100644 --- a/composer.json +++ b/composer.json @@ -32,7 +32,7 @@ "guzzlehttp/guzzle": "^7.8", "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0", - "laravel/serializable-closure": "^1.3", + "laravel/serializable-closure": "^1.3|^2.0", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", @@ -107,7 +107,7 @@ "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.6.10", "nyholm/psr7": "^1.2", - "orchestra/testbench-core": "^9.5", + "orchestra/testbench-core": "^9.6", "pda/pheanstalk": "^5.0", "phpstan/phpstan": "^1.11.5", "phpunit/phpunit": "^10.5|^11.0", diff --git a/src/Illuminate/Queue/composer.json b/src/Illuminate/Queue/composer.json index 54fddacecb46..95819f4edfeb 100644 --- a/src/Illuminate/Queue/composer.json +++ b/src/Illuminate/Queue/composer.json @@ -23,7 +23,7 @@ "illuminate/filesystem": "^11.0", "illuminate/pipeline": "^11.0", "illuminate/support": "^11.0", - "laravel/serializable-closure": "^1.2.2", + "laravel/serializable-closure": "^1.3|^2.0", "ramsey/uuid": "^4.7", "symfony/process": "^7.0" }, diff --git a/tests/Integration/Queue/Fixtures/Jobs/DeleteUser.php b/tests/Integration/Queue/Fixtures/Jobs/DeleteUser.php new file mode 100644 index 000000000000..ecac9838e335 --- /dev/null +++ b/tests/Integration/Queue/Fixtures/Jobs/DeleteUser.php @@ -0,0 +1,29 @@ +user->delete(); + } +} diff --git a/tests/Integration/Queue/SerializableClosureV1QueueTest.php b/tests/Integration/Queue/SerializableClosureV1QueueTest.php new file mode 100644 index 000000000000..8f72ae239060 --- /dev/null +++ b/tests/Integration/Queue/SerializableClosureV1QueueTest.php @@ -0,0 +1,67 @@ +markTestSkippedWhen($this->usingInMemoryDatabase(), 'Test does not support using :memory: database connection'); + + tap($app->make('config'), function ($config) { + $config->set([ + 'app.key' => 'AckfSECXIvnK5r28GVIWUAxmbBSjTsmF', + 'queue.default' => 'database', + ]); + }); + } + + /** {@inheritDoc} */ + protected function afterRefreshingDatabase() + { + UserFactory::new()->create([ + 'id' => 100, + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', + ]); + + DB::table('jobs')->insert([ + 'queue' => 'default', + 'payload' => "{\"uuid\":\"d7c0856d-733a-4e73-89c8-eca4dea621ff\",\"displayName\":\"Illuminate\\\\Tests\\\\Integration\\\\Queue\\\\Fixtures\\\\Jobs\\\\DeleteUser\",\"job\":\"Illuminate\\\\Queue\\\\CallQueuedHandler@call\",\"maxTries\":null,\"maxExceptions\":null,\"failOnTimeout\":false,\"backoff\":null,\"timeout\":null,\"retryUntil\":null,\"data\":{\"commandName\":\"Illuminate\\\\Tests\\\\Integration\\\\Queue\\\\Fixtures\\\\Jobs\\\\DeleteUser\",\"command\":\"O:59:\\\"Illuminate\\\\Tests\\\\Integration\\\\Queue\\\\Fixtures\\\\Jobs\\\\DeleteUser\\\":3:{s:4:\\\"user\\\";O:45:\\\"Illuminate\\\\Contracts\\\\Database\\\\ModelIdentifier\\\":5:{s:5:\\\"class\\\";s:31:\\\"Illuminate\\\\Foundation\\\\Auth\\\\User\\\";s:2:\\\"id\\\";i:100;s:9:\\\"relations\\\";a:0:{}s:10:\\\"connection\\\";s:6:\\\"sqlite\\\";s:15:\\\"collectionClass\\\";N;}s:7:\\\"chained\\\";a:1:{i:0;s:571:\\\"O:34:\\\"Illuminate\\\\Queue\\\\CallQueuedClosure\\\":1:{s:7:\\\"closure\\\";O:47:\\\"Laravel\\\\SerializableClosure\\\\SerializableClosure\\\":1:{s:12:\\\"serializable\\\";O:46:\\\"Laravel\\\\SerializableClosure\\\\Serializers\\\\Signed\\\":2:{s:12:\\\"serializable\\\";s:282:\\\"O:46:\\\"Laravel\\\\SerializableClosure\\\\Serializers\\\\Native\\\":5:{s:3:\\\"use\\\";a:0:{}s:8:\\\"function\\\";s:57:\\\"function () {\\n \\\\info('Hello world');\\n }\\\";s:5:\\\"scope\\\";s:44:\\\"Illuminate\\\\Foundation\\\\Console\\\\ClosureCommand\\\";s:4:\\\"this\\\";N;s:4:\\\"self\\\";s:32:\\\"000000000000021e0000000000000000\\\";}\\\";s:4:\\\"hash\\\";s:44:\\\"VGMlRmFr2\\/U1E8lksExnzODwffyWR8oD01WOcQ2SUjE=\\\";}}}\\\";}s:19:\\\"chainCatchCallbacks\\\";a:1:{i:0;O:47:\\\"Laravel\\\\SerializableClosure\\\\SerializableClosure\\\":1:{s:12:\\\"serializable\\\";O:46:\\\"Laravel\\\\SerializableClosure\\\\Serializers\\\\Signed\\\":2:{s:12:\\\"serializable\\\";s:309:\\\"O:46:\\\"Laravel\\\\SerializableClosure\\\\Serializers\\\\Native\\\":5:{s:3:\\\"use\\\";a:0:{}s:8:\\\"function\\\";s:84:\\\"function (\\\\Throwable \$e) {\\n \\\\Illuminate\\\\Support\\\\Facades\\\\Log::error(\$e);\\n }\\\";s:5:\\\"scope\\\";s:44:\\\"Illuminate\\\\Foundation\\\\Console\\\\ClosureCommand\\\";s:4:\\\"this\\\";N;s:4:\\\"self\\\";s:32:\\\"00000000000002380000000000000000\\\";}\\\";s:4:\\\"hash\\\";s:44:\\\"RBSD4RFLgmKL9WJEGY66aeZtWDkX\\/aY1J+MJ8LQSYi4=\\\";}}}}\"}}", + 'attempts' => 0, + 'available_at' => 1731919764, + 'created_at' => 1731919764, + ]); + } + + public function testItCanProcessQueueFromSerializableClosureV1() + { + $this->assertDatabaseHas('users', [ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + ]); + + $this->artisan('queue:work', [ + 'connection' => 'database', + '--stop-when-empty' => true, + '--memory' => 1024, + ])->assertExitCode(0); + + $this->assertDatabaseMissing('users', [ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + ]); + } +} diff --git a/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php b/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php new file mode 100644 index 000000000000..ba9c952fe457 --- /dev/null +++ b/tests/Integration/Routing/SerializableClosureV1CacheRouteTest.php @@ -0,0 +1,59 @@ +create(); + + $this->assertTrue($this->app->routesAreCached()); + + $this->get('/')->assertSee('Laravel'); + + $this->get("/users/{$user->getKey()}") + ->assertJson($user->toArray()); + } +} diff --git a/tests/Integration/Routing/stubs/serializable-closure-v1/routes-v7.php b/tests/Integration/Routing/stubs/serializable-closure-v1/routes-v7.php new file mode 100644 index 000000000000..ee6eecd5ff4d --- /dev/null +++ b/tests/Integration/Routing/stubs/serializable-closure-v1/routes-v7.php @@ -0,0 +1,120 @@ +setCompiledRoutes( + [ + 'compiled' => [ + 0 => false, + 1 => [ + '/' => [ + 0 => [ + 0 => [ + '_route' => 'generated::7CFionvE02fEbBNP', + ], + 1 => null, + 2 => [ + 'GET' => 0, + 'HEAD' => 1, + ], + 3 => null, + 4 => false, + 5 => false, + 6 => null, + ], + ], + ], + 2 => [ + 0 => '{^(?|/users/([^/]++)(*:22))/?$}sDu', + ], + 3 => [ + 22 => [ + 0 => [ + 0 => [ + '_route' => 'generated::YmZUvOCRFrqhC2sO', + ], + 1 => [ + 0 => 'user', + ], + 2 => [ + 'GET' => 0, + 'HEAD' => 1, + ], + 3 => null, + 4 => false, + 5 => true, + 6 => null, + ], + 1 => [ + 0 => null, + 1 => null, + 2 => null, + 3 => null, + 4 => false, + 5 => false, + 6 => 0, + ], + ], + ], + 4 => null, + ], + 'attributes' => [ + 'generated::7CFionvE02fEbBNP' => [ + 'methods' => [ + 0 => 'GET', + 1 => 'HEAD', + ], + 'uri' => '/', + 'action' => [ + 'middleware' => [ + 0 => 'web', + ], + 'uses' => 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure":1:{s:12:"serializable";O:46:"Laravel\\SerializableClosure\\Serializers\\Native":5:{s:3:"use";a:0:{}s:8:"function";s:44:"function () { + return \\view(\'welcome\'); +}";s:5:"scope";s:37:"Illuminate\\Routing\\RouteFileRegistrar";s:4:"this";N;s:4:"self";s:32:"00000000000002f00000000000000000";}}', + 'namespace' => null, + 'prefix' => '', + 'where' => [ + ], + 'as' => 'generated::7CFionvE02fEbBNP', + ], + 'fallback' => false, + 'defaults' => [ + ], + 'wheres' => [ + ], + 'bindingFields' => [ + ], + 'lockSeconds' => null, + 'waitSeconds' => null, + 'withTrashed' => false, + ], + 'generated::YmZUvOCRFrqhC2sO' => [ + 'methods' => [ + 0 => 'GET', + 1 => 'HEAD', + ], + 'uri' => 'users/{user}', + 'action' => [ + 'middleware' => [ + 0 => 'web', + ], + 'uses' => 'O:55:"Laravel\\SerializableClosure\\UnsignedSerializableClosure":1:{s:12:"serializable";O:46:"Laravel\\SerializableClosure\\Serializers\\Native":5:{s:3:"use";a:0:{}s:8:"function";s:52:"fn (\\Illuminate\\Foundation\\Auth\\User $user) => $user";s:5:"scope";s:37:"Illuminate\\Routing\\RouteFileRegistrar";s:4:"this";N;s:4:"self";s:32:"00000000000002f20000000000000000";}}', + 'namespace' => null, + 'prefix' => '', + 'where' => [ + ], + 'as' => 'generated::YmZUvOCRFrqhC2sO', + ], + 'fallback' => false, + 'defaults' => [ + ], + 'wheres' => [ + ], + 'bindingFields' => [ + ], + 'lockSeconds' => null, + 'waitSeconds' => null, + 'withTrashed' => false, + ], + ], + ] +);