From 0038ef28eb07db1078688e1e384980cc60f6047e Mon Sep 17 00:00:00 2001 From: Petr Levtonov Date: Sat, 27 Feb 2021 16:59:00 +0100 Subject: [PATCH 01/91] laravel/framework#36337: Respect serialization and compression of phpredis during locking - Support phpredis serialization when locking (none, php, json, igbinary, msgpack). - Support phpredis compression when locking (none, lzf, zstd, lz4). --- src/Illuminate/Cache/PhpRedisLock.php | 89 +++++ src/Illuminate/Cache/RedisStore.php | 10 +- .../Cache/PhpRedisCacheLockTest.php | 306 ++++++++++++++++++ 3 files changed, 404 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Cache/PhpRedisLock.php create mode 100644 tests/Integration/Cache/PhpRedisCacheLockTest.php diff --git a/src/Illuminate/Cache/PhpRedisLock.php b/src/Illuminate/Cache/PhpRedisLock.php new file mode 100644 index 000000000000..8d0670a167fd --- /dev/null +++ b/src/Illuminate/Cache/PhpRedisLock.php @@ -0,0 +1,89 @@ +redis->eval( + LuaScripts::releaseLock(), + 1, + $this->name, + $this->serializedAndCompressedOwner() + ); + } + + protected function serializedAndCompressedOwner(): string + { + $client = $this->redis->client(); + + /* If a serialization mode such as "php" or "igbinary" and/or a + * compression mode such as "lzf" or "zstd" is enabled, the owner + * must be serialized and/or compressed by us, because phpredis does + * not do this for the eval command. + * + * Name must not be modified! + */ + $owner = $client->_serialize($this->owner); + + /* Once the phpredis extension exposes a compress function like the + * above `_serialize()` function, we should switch to it to guarantee + * consistency in the way the extension serializes and compresses to + * avoid the need to check each compression option ourselves. + * + * @see https://github.com/phpredis/phpredis/issues/1938 + */ + if ($this->compressed()) { + if ($this->lzfCompressed()) { + $owner = \lzf_compress($owner); + } elseif ($this->zstdCompressed()) { + $owner = \zstd_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); + } elseif ($this->lz4Compressed()) { + $owner = \lz4_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); + } else { + throw new UnexpectedValueException(sprintf( + 'Unknown phpredis compression in use (%d). Unable to release lock.', + $client->getOption(Redis::OPT_COMPRESSION) + )); + } + } + + return $owner; + } + + protected function compressed(): bool + { + return $this->redis->client()->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; + } + + protected function lzfCompressed(): bool + { + return defined('Redis::COMPRESSION_LZF') && + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; + } + + protected function zstdCompressed(): bool + { + return defined('Redis::COMPRESSION_ZSTD') && + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; + } + + protected function lz4Compressed(): bool + { + return defined('Redis::COMPRESSION_LZ4') && + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; + } +} diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index cdf1c8fca094..e698bab375d4 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Redis\Factory as Redis; +use Illuminate\Redis\Connections\PhpRedisConnection; class RedisStore extends TaggableStore implements LockProvider { @@ -188,7 +189,14 @@ public function forever($key, $value) */ public function lock($name, $seconds = 0, $owner = null) { - return new RedisLock($this->lockConnection(), $this->prefix.$name, $seconds, $owner); + $lockName = $this->prefix.$name; + $lockConnection = $this->lockConnection(); + + if ($lockConnection instanceof PhpRedisConnection) { + return new PhpRedisLock($lockConnection, $lockName, $seconds, $owner); + } + + return new RedisLock($lockConnection, $lockName, $seconds, $owner); } /** diff --git a/tests/Integration/Cache/PhpRedisCacheLockTest.php b/tests/Integration/Cache/PhpRedisCacheLockTest.php new file mode 100644 index 000000000000..de1048eb30c4 --- /dev/null +++ b/tests/Integration/Cache/PhpRedisCacheLockTest.php @@ -0,0 +1,306 @@ +setUpRedis(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->tearDownRedis(); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithoutSerializationAndCompression() + { + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithPhpSerialization() + { + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithJsonSerialization() + { + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_JSON); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithIgbinarySerialization() + { + if (! defined('Redis::SERIALIZER_IGBINARY')) { + $this->markTestSkipped('Redis extension is not configured to support the igbinary serializer.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_IGBINARY); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithMsgpackSerialization() + { + if (! defined('Redis::SERIALIZER_MSGPACK')) { + $this->markTestSkipped('Redis extension is not configured to support the msgpack serializer.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_MSGPACK); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithLzfCompression() + { + if (! defined('Redis::COMPRESSION_LZF')) { + $this->markTestSkipped('Redis extension is not configured to support the lzf compression.'); + } + + if (! extension_loaded('lzf')) { + $this->markTestSkipped('Lzf extension is not installed.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZF); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithZstdCompression() + { + if (! defined('Redis::COMPRESSION_ZSTD')) { + $this->markTestSkipped('Redis extension is not configured to support the zstd compression.'); + } + + if (! extension_loaded('zstd')) { + $this->markTestSkipped('Zstd extension is not installed.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_ZSTD); + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, Redis::COMPRESSION_ZSTD_DEFAULT); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, Redis::COMPRESSION_ZSTD_MIN); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, Redis::COMPRESSION_ZSTD_MAX); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithLz4Compression() + { + if (! defined('Redis::COMPRESSION_LZ4')) { + $this->markTestSkipped('Redis extension is not configured to support the lz4 compression.'); + } + + if (! extension_loaded('lz4')) { + $this->markTestSkipped('Lz4 extension is not installed.'); + } + + $this->markTestIncomplete( + 'phpredis extension does not compress consistently with the php '. + 'extension lz4. See: https://github.com/phpredis/phpredis/issues/1939' + ); + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_NONE); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZ4); + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, 1); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, 3); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + + $client->setOption(Redis::OPT_COMPRESSION_LEVEL, 12); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } + + public function testRedisLockCanBeAcquiredAndReleasedWithSerializationAndCompression() + { + if (! defined('Redis::COMPRESSION_LZF')) { + $this->markTestSkipped('Redis extension is not configured to support the lzf compression.'); + } + + if (! extension_loaded('lzf')) { + $this->markTestSkipped('Lzf extension is not installed.'); + } + + $this->app['config']->set('database.redis.client', 'phpredis'); + $this->app['config']->set('cache.stores.redis.connection', 'default'); + $this->app['config']->set('cache.stores.redis.lock_connection', 'default'); + + /** @var \Illuminate\Cache\RedisStore $store */ + $store = Cache::store('redis'); + /** @var \Redis $client */ + $client = $store->lockConnection()->client(); + + $client->setOption(Redis::OPT_SERIALIZER, Redis::SERIALIZER_PHP); + $client->setOption(Redis::OPT_COMPRESSION, Redis::COMPRESSION_LZF); + $store->lock('foo')->forceRelease(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + $lock = $store->lock('foo', 10); + $this->assertTrue($lock->get()); + $this->assertFalse($store->lock('foo', 10)->get()); + $lock->release(); + $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); + } +} From d0e4731e92ca88f4a78fe9e0c2c426a3e8c063c8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 2 Mar 2021 08:28:26 -0600 Subject: [PATCH 02/91] patch release --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 236cd15d06f3..264e0deba796 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -31,7 +31,7 @@ class Application extends Container implements ApplicationContract, HttpKernelIn * * @var string */ - const VERSION = '6.20.16'; + const VERSION = '6.20.17'; /** * The base path for the Laravel installation. From e3f48f74b0ff6436f47261882649ad072936f79a Mon Sep 17 00:00:00 2001 From: Lloric Mayuga Garcia Date: Wed, 3 Mar 2021 22:15:52 +0800 Subject: [PATCH 03/91] [8.x] Fix formatWheres in DatabaseRule (#36441) * Fix formatWheres in DatabaseRule * Update DatabaseRule.php * Update DatabaseRule.php * Fix styleci --- src/Illuminate/Validation/Rules/DatabaseRule.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/DatabaseRule.php b/src/Illuminate/Validation/Rules/DatabaseRule.php index b8113b2afadb..88b4d27cd941 100644 --- a/src/Illuminate/Validation/Rules/DatabaseRule.php +++ b/src/Illuminate/Validation/Rules/DatabaseRule.php @@ -196,7 +196,11 @@ public function queryCallbacks() protected function formatWheres() { return collect($this->wheres)->map(function ($where) { - return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; + if (is_bool($where['value'])) { + return $where['column'].','.($where['value'] ? 'true' : 'false'); + } else { + return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; + } })->implode(','); } } From 70f2f019f2f8513c3e775a0d7d0d292c22ebf18c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 3 Mar 2021 08:54:27 -0600 Subject: [PATCH 04/91] Revert "[8.x] Respect custom route key with explicit route model binding (#36375)" (#36449) This reverts commit 8c5292eb96d9cf104b9b3bcdd471bde59a117de1. --- src/Illuminate/Routing/RouteBinding.php | 8 +++---- src/Illuminate/Routing/Router.php | 2 +- tests/Routing/RoutingRouteTest.php | 31 ------------------------- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/src/Illuminate/Routing/RouteBinding.php b/src/Illuminate/Routing/RouteBinding.php index 45b2e8ec8d05..133a84a40b07 100644 --- a/src/Illuminate/Routing/RouteBinding.php +++ b/src/Illuminate/Routing/RouteBinding.php @@ -33,7 +33,7 @@ public static function forCallback($container, $binder) */ protected static function createClassBinding($container, $binding) { - return function ($value, $route, $key) use ($container, $binding) { + return function ($value, $route) use ($container, $binding) { // If the binding has an @ sign, we will assume it's being used to delimit // the class name from the bind method name. This allows for bindings // to run multiple bind methods in a single class for convenience. @@ -41,7 +41,7 @@ protected static function createClassBinding($container, $binding) $callable = [$container->make($class), $method]; - return $callable($value, $route, $key); + return $callable($value, $route); }; } @@ -57,7 +57,7 @@ protected static function createClassBinding($container, $binding) */ public static function forModel($container, $class, $callback = null) { - return function ($value, $route, $key) use ($container, $class, $callback) { + return function ($value) use ($container, $class, $callback) { if (is_null($value)) { return; } @@ -67,7 +67,7 @@ public static function forModel($container, $class, $callback = null) // throw a not found exception otherwise we will return the instance. $instance = $container->make($class); - if ($model = $instance->resolveRouteBinding($value, $route->bindingFieldFor($key))) { + if ($model = $instance->resolveRouteBinding($value)) { return $model; } diff --git a/src/Illuminate/Routing/Router.php b/src/Illuminate/Routing/Router.php index 4e57ffc6ec9d..dfdb7ae7332e 100644 --- a/src/Illuminate/Routing/Router.php +++ b/src/Illuminate/Routing/Router.php @@ -840,7 +840,7 @@ public function substituteImplicitBindings($route) */ protected function performBinding($key, $value, $route) { - return call_user_func($this->binders[$key], $value, $route, $key); + return call_user_func($this->binders[$key], $value, $route); } /** diff --git a/tests/Routing/RoutingRouteTest.php b/tests/Routing/RoutingRouteTest.php index f4244a3bbb3e..a0d4548dc06e 100644 --- a/tests/Routing/RoutingRouteTest.php +++ b/tests/Routing/RoutingRouteTest.php @@ -29,7 +29,6 @@ use Illuminate\Routing\UrlGenerator; use Illuminate\Support\Str; use LogicException; -use Mockery; use PHPUnit\Framework\TestCase; use stdClass; use Symfony\Component\HttpFoundation\Response as SymfonyResponse; @@ -939,36 +938,6 @@ public function testModelBinding() $this->assertSame('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); } - public function testModelBindingWithCustomKey() - { - // Create the router. - $container = new Container(); - $router = new Router(new Dispatcher(), $container); - $container->singleton(Registrar::class, function () use ($router) { - return $router; - }); - - $router->get('foo/{bar:custom}', ['middleware' => SubstituteBindings::class, 'uses' => function ($name) { - return $name; - }]); - $router->model('bar', RouteModelBindingStub::class); - - // Mock the stub so we can verify that the method is called with custom key. - $mock = $container->instance( - RouteModelBindingStub::class, - Mockery::mock(RouteModelBindingStub::class), - ); - - $mock->shouldReceive('resolveRouteBinding') - ->with('taylor', 'custom') - ->once() - ->andReturn('TAYLOR'); - - $this->assertSame('TAYLOR', $router->dispatch(Request::create('foo/taylor', 'GET'))->getContent()); - - Mockery::close(); - } - public function testModelBindingWithNullReturn() { $this->expectException(ModelNotFoundException::class); From ab7e1c19ee0403e15fc59983b7ccb86d85db45e6 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 3 Mar 2021 08:59:13 -0600 Subject: [PATCH 05/91] patch --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index b61e3db92acf..1e35f20d4f46 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -33,7 +33,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '8.30.0'; + const VERSION = '8.30.1'; /** * The base path for the Laravel installation. From f927dc74e959457e939393de2768d72847098e17 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 3 Mar 2021 11:29:19 -0600 Subject: [PATCH 06/91] Revert "[8.x] Fix formatWheres in DatabaseRule (#36441)" (#36452) This reverts commit e3f48f74b0ff6436f47261882649ad072936f79a. --- src/Illuminate/Validation/Rules/DatabaseRule.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Illuminate/Validation/Rules/DatabaseRule.php b/src/Illuminate/Validation/Rules/DatabaseRule.php index 88b4d27cd941..b8113b2afadb 100644 --- a/src/Illuminate/Validation/Rules/DatabaseRule.php +++ b/src/Illuminate/Validation/Rules/DatabaseRule.php @@ -196,11 +196,7 @@ public function queryCallbacks() protected function formatWheres() { return collect($this->wheres)->map(function ($where) { - if (is_bool($where['value'])) { - return $where['column'].','.($where['value'] ? 'true' : 'false'); - } else { - return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; - } + return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; })->implode(','); } } From 018314829ccb70ec20a6fc25fa4bd382d09d6cc0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 3 Mar 2021 12:04:44 -0600 Subject: [PATCH 07/91] Revert "Revert "[8.x] Fix formatWheres in DatabaseRule (#36441)" (#36452)" (#36453) This reverts commit f927dc74e959457e939393de2768d72847098e17. --- src/Illuminate/Validation/Rules/DatabaseRule.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Validation/Rules/DatabaseRule.php b/src/Illuminate/Validation/Rules/DatabaseRule.php index b8113b2afadb..88b4d27cd941 100644 --- a/src/Illuminate/Validation/Rules/DatabaseRule.php +++ b/src/Illuminate/Validation/Rules/DatabaseRule.php @@ -196,7 +196,11 @@ public function queryCallbacks() protected function formatWheres() { return collect($this->wheres)->map(function ($where) { - return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; + if (is_bool($where['value'])) { + return $where['column'].','.($where['value'] ? 'true' : 'false'); + } else { + return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; + } })->implode(','); } } From add249472cd192cabcb4f113ff7915f667394141 Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Wed, 3 Mar 2021 20:07:50 +0100 Subject: [PATCH 08/91] Implement Fluent JSON Assertions --- src/Illuminate/Testing/Fluent/Assert.php | 76 ++ .../Testing/Fluent/Concerns/Debugging.php | 20 + .../Testing/Fluent/Concerns/Has.php | 100 +++ .../Testing/Fluent/Concerns/Interaction.php | 41 ++ .../Testing/Fluent/Concerns/Matching.php | 70 ++ src/Illuminate/Testing/TestResponse.php | 20 +- tests/Testing/Fluent/AssertTest.php | 688 ++++++++++++++++++ tests/Testing/Stubs/ArrayableStubObject.php | 25 + tests/Testing/TestResponseTest.php | 22 + 9 files changed, 1059 insertions(+), 3 deletions(-) create mode 100644 src/Illuminate/Testing/Fluent/Assert.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Debugging.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Has.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Interaction.php create mode 100644 src/Illuminate/Testing/Fluent/Concerns/Matching.php create mode 100644 tests/Testing/Fluent/AssertTest.php create mode 100644 tests/Testing/Stubs/ArrayableStubObject.php diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/Assert.php new file mode 100644 index 000000000000..a5e89e83294c --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Assert.php @@ -0,0 +1,76 @@ +path = $path; + $this->props = $props; + } + + protected function dotPath($key): string + { + if (is_null($this->path)) { + return $key; + } + + return implode('.', [$this->path, $key]); + } + + protected function prop(string $key = null) + { + return Arr::get($this->props, $key); + } + + protected function scope($key, Closure $callback): self + { + $props = $this->prop($key); + $path = $this->dotPath($key); + + PHPUnit::assertIsArray($props, sprintf('Property [%s] is not scopeable.', $path)); + + $scope = new self($props, $path); + $callback($scope); + $scope->interacted(); + + return $this; + } + + public static function fromArray(array $data): self + { + return new self($data); + } + + public static function fromAssertableJsonString(AssertableJsonString $json): self + { + return self::fromArray($json->json()); + } + + public function toArray() + { + return $this->props; + } +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php new file mode 100644 index 000000000000..d604f0e0b21d --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php @@ -0,0 +1,20 @@ +prop($prop)); + + return $this; + } + + public function dd(string $prop = null): void + { + dd($this->prop($prop)); + } + + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php new file mode 100644 index 000000000000..28955a742493 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -0,0 +1,100 @@ +prop($key), + sprintf('Property [%s] does not have the expected size.', $this->dotPath($key)) + ); + + return $this; + } + + public function hasAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop => $count) { + if (is_int($prop)) { + $this->has($count); + } else { + $this->has($prop, $count); + } + } + + return $this; + } + + public function has(string $key, $value = null, Closure $scope = null): self + { + $prop = $this->prop(); + + PHPUnit::assertTrue( + Arr::has($prop, $key), + sprintf('Property [%s] does not exist.', $this->dotPath($key)) + ); + + $this->interactsWith($key); + + // When all three arguments are provided, this indicates a short-hand + // expression that combines both a `count`-assertion, followed by + // directly creating a `scope` on the first element. + if (is_int($value) && ! is_null($scope)) { + $prop = $this->prop($key); + $path = $this->dotPath($key); + + PHPUnit::assertTrue($value > 0, sprintf('Cannot scope directly onto the first entry of property [%s] when asserting that it has a size of 0.', $path)); + PHPUnit::assertIsArray($prop, sprintf('Direct scoping is unsupported for non-array like properties such as [%s].', $path)); + + $this->count($key, $value); + + return $this->scope($key.'.'.array_keys($prop)[0], $scope); + } + + if (is_callable($value)) { + $this->scope($key, $value); + } elseif (! is_null($value)) { + $this->count($key, $value); + } + + return $this; + } + + public function missingAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop) { + $this->missing($prop); + } + + return $this; + } + + public function missing(string $key): self + { + PHPUnit::assertNotTrue( + Arr::has($this->prop(), $key), + sprintf('Property [%s] was found while it was expected to be missing.', $this->dotPath($key)) + ); + + return $this; + } + + abstract protected function prop(string $key = null); + + abstract protected function dotPath($key): string; + + abstract protected function interactsWith(string $key): void; + + abstract protected function scope($key, Closure $callback); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php new file mode 100644 index 000000000000..d938438a618c --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php @@ -0,0 +1,41 @@ +interacted, true)) { + $this->interacted[] = $prop; + } + } + + public function interacted(): void + { + PHPUnit::assertSame( + [], + array_diff(array_keys($this->prop()), $this->interacted), + $this->path + ? sprintf('Unexpected properties were found in scope [%s].', $this->path) + : 'Unexpected properties were found on the root level.' + ); + } + + public function etc(): self + { + $this->interacted = array_keys($this->prop()); + + return $this; + } + + abstract protected function prop(string $key = null); +} diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php new file mode 100644 index 000000000000..3edce127d362 --- /dev/null +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -0,0 +1,70 @@ + $value) { + $this->where($key, $value); + } + + return $this; + } + + public function where($key, $expected): self + { + $this->has($key); + + $actual = $this->prop($key); + + if ($expected instanceof Closure) { + PHPUnit::assertTrue( + $expected(is_array($actual) ? Collection::make($actual) : $actual), + sprintf('Property [%s] was marked as invalid using a closure.', $this->dotPath($key)) + ); + + return $this; + } + + if ($expected instanceof Arrayable) { + $expected = $expected->toArray(); + } + + $this->ensureSorted($expected); + $this->ensureSorted($actual); + + PHPUnit::assertSame( + $expected, + $actual, + sprintf('Property [%s] does not match the expected value.', $this->dotPath($key)) + ); + + return $this; + } + + protected function ensureSorted(&$value): void + { + if (! is_array($value)) { + return; + } + + foreach ($value as &$arg) { + $this->ensureSorted($arg); + } + + ksort($value); + } + + abstract protected function dotPath($key): string; + + abstract protected function prop(string $key = null); + + abstract public function has(string $key, $value = null, Closure $scope = null); +} diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 1bfc75285518..b900418caf79 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -14,6 +14,7 @@ use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; +use Illuminate\Testing\Fluent\Assert as FluentAssert; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -507,13 +508,26 @@ public function assertDontSeeText($value, $escape = true) /** * Assert that the response is a superset of the given JSON. * - * @param array $data + * @param array|callable $value * @param bool $strict * @return $this */ - public function assertJson(array $data, $strict = false) + public function assertJson($value, $strict = false) { - $this->decodeResponseJson()->assertSubset($data, $strict); + $json = $this->decodeResponseJson(); + + if (is_array($value)) { + $json->assertSubset($value, $strict); + } else { + $assert = FluentAssert::fromAssertableJsonString($json); + + $value($assert); + + if ($strict) { + $assert->interacted(); + } + } + return $this; } diff --git a/tests/Testing/Fluent/AssertTest.php b/tests/Testing/Fluent/AssertTest.php new file mode 100644 index 000000000000..f02e366f40a0 --- /dev/null +++ b/tests/Testing/Fluent/AssertTest.php @@ -0,0 +1,688 @@ + 'value', + ]); + + $assert->has('prop'); + } + + public function testAssertHasFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [prop] does not exist.'); + + $assert->has('prop'); + } + + public function testAssertHasNestedProp() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $assert->has('example.nested'); + } + + public function testAssertHasFailsWhenNestedPropMissing() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [example.another] does not exist.'); + + $assert->has('example.another'); + } + + public function testAssertCountItemsInProp() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $assert->has('bar', 2); + } + + public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 1); + } + + public function testAssertCountFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->has('baz', 1); + } + + public function testAssertHasFailsWhenSecondArgumentUnsupportedType() + { + $assert = Assert::fromArray([ + 'bar' => 'baz', + ]); + + $this->expectException(TypeError::class); + + $assert->has('bar', 'invalid'); + } + + public function testAssertMissing() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => true, + ], + ]); + + $assert->missing('foo.baz'); + } + + public function testAssertMissingFailsWhenPropExists() + { + $assert = Assert::fromArray([ + 'prop' => 'value', + 'foo' => [ + 'bar' => true, + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.bar] was found while it was expected to be missing.'); + + $assert->missing('foo.bar'); + } + + public function testAssertMissingAll() + { + $assert = Assert::fromArray([ + 'baz' => 'foo', + ]); + + $assert->missingAll([ + 'foo', + 'bar', + ]); + } + + public function testAssertMissingAllFailsWhenAtLeastOnePropExists() + { + $assert = Assert::fromArray([ + 'baz' => 'foo', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was found while it was expected to be missing.'); + + $assert->missingAll([ + 'bar', + 'baz', + ]); + } + + public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() + { + $assert = Assert::fromArray([ + 'baz' => 'foo', + ]); + + $assert->missingAll('foo', 'bar'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was found while it was expected to be missing.'); + + $assert->missingAll('bar', 'baz'); + } + + public function testAssertWhereMatchesValue() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $assert->where('bar', 'value'); + } + + public function testAssertWhereFailsWhenDoesNotMatchValue() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not match the expected value.'); + + $assert->where('bar', 'invalid'); + } + + public function testAssertWhereFailsWhenMissing() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->where('baz', 'invalid'); + } + + public function testAssertWhereFailsWhenMachingLoosely() + { + $assert = Assert::fromArray([ + 'bar' => 1, + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not match the expected value.'); + + $assert->where('bar', true); + } + + public function testAssertWhereUsingClosure() + { + $assert = Assert::fromArray([ + 'bar' => 'baz', + ]); + + $assert->where('bar', function ($value) { + return $value === 'baz'; + }); + } + + public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() + { + $assert = Assert::fromArray([ + 'bar' => 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] was marked as invalid using a closure.'); + + $assert->where('bar', function ($value) { + return $value === 'invalid'; + }); + } + + public function testAssertWhereClosureArrayValuesAreAutomaticallyCastedToCollections() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'foo', + 'example' => 'value', + ], + ]); + + $assert->where('bar', function ($value) { + $this->assertInstanceOf(Collection::class, $value); + + return $value->count() === 2; + }); + } + + public function testAssertWhereMatchesValueUsingArrayable() + { + $stub = ArrayableStubObject::make(['foo' => 'bar']); + + $assert = Assert::fromArray([ + 'bar' => $stub->toArray(), + ]); + + $assert->where('bar', $stub); + } + + public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'foo', + 'example' => 'value', + ], + ]); + + $assert->where('bar', function ($value) { + $this->assertInstanceOf(Collection::class, $value); + + return $value->count() === 2; + }); + } + + public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() + { + $assert = Assert::fromArray([ + 'bar' => ['id' => 1, 'name' => 'Example'], + 'baz' => [ + 'id' => 1, + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'email_verified_at' => '2021-01-22T10:34:42.000000Z', + 'created_at' => '2021-01-22T10:34:42.000000Z', + 'updated_at' => '2021-01-22T10:34:42.000000Z', + ], + ]); + + $assert + ->where('bar', ArrayableStubObject::make(['name' => 'Example', 'id' => 1])) + ->where('baz', [ + 'name' => 'Taylor Otwell', + 'email' => 'taylor@laravel.com', + 'id' => 1, + 'email_verified_at' => '2021-01-22T10:34:42.000000Z', + 'updated_at' => '2021-01-22T10:34:42.000000Z', + 'created_at' => '2021-01-22T10:34:42.000000Z', + ]); + } + + public function testAssertNestedWhereMatchesValue() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $assert->where('example.nested', 'nested-value'); + } + + public function testAssertNestedWhereFailsWhenDoesNotMatchValue() + { + $assert = Assert::fromArray([ + 'example' => [ + 'nested' => 'nested-value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [example.nested] does not match the expected value.'); + + $assert->where('example.nested', 'another-value'); + } + + public function testScope() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $called = false; + $assert->has('bar', function (Assert $assert) use (&$called) { + $called = true; + $assert + ->where('baz', 'example') + ->where('prop', 'value'); + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } + + public function testScopeFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->has('baz', function (Assert $item) { + $item->where('baz', 'example'); + }); + } + + public function testScopeFailsWhenPropSingleValue() + { + $assert = Assert::fromArray([ + 'bar' => 'value', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] is not scopeable.'); + + $assert->has('bar', function (Assert $item) { + // + }); + } + + public function testScopeShorthand() + { + $assert = Assert::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $called = false; + $assert->has('bar', 2, function (Assert $item) use (&$called) { + $item->where('key', 'first'); + $called = true; + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } + + public function testScopeShorthandFailsWhenAssertingZeroItems() + { + $assert = Assert::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Cannot scope directly onto the first entry of property [bar] when asserting that it has a size of 0.'); + + $assert->has('bar', 0, function (Assert $item) { + $item->where('key', 'first'); + }); + } + + public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() + { + $assert = Assert::fromArray([ + 'bar' => [ + ['key' => 'first'], + ['key' => 'second'], + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [bar] does not have the expected size.'); + + $assert->has('bar', 1, function (Assert $item) { + $item->where('key', 'first'); + }); + } + + public function testFailsWhenNotInteractingWithAllPropsInScope() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found in scope [bar].'); + + $assert->has('bar', function (Assert $item) { + $item->where('baz', 'example'); + }); + } + + public function testDisableInteractionCheckForCurrentScope() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $assert->has('bar', function (Assert $item) { + $item->etc(); + }); + } + + public function testCannotDisableInteractionCheckForDifferentScopes() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => [ + 'foo' => 'bar', + 'example' => 'value', + ], + 'prop' => 'value', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found in scope [bar.baz].'); + + $assert->has('bar', function (Assert $item) { + $item + ->etc() + ->has('baz', function (Assert $item) { + // + }); + }); + } + + public function testTopLevelPropInteractionDisabledByDefault() + { + $assert = Assert::fromArray([ + 'foo' => 'bar', + 'bar' => 'baz', + ]); + + $assert->has('foo'); + } + + public function testTopLevelInteractionEnabledWhenInteractedFlagSet() + { + $assert = Assert::fromArray([ + 'foo' => 'bar', + 'bar' => 'baz', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found on the root level.'); + + $assert + ->has('foo') + ->interacted(); + } + + public function testAssertWhereAllMatchesValues() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->whereAll([ + 'foo.bar' => 'value', + 'foo.example' => ArrayableStubObject::make(['hello' => 'world']), + 'baz' => function ($value) { + return $value === 'another'; + }, + ]); + } + + public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() + { + $assert = Assert::fromArray([ + 'foo' => 'bar', + 'baz' => 'example', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] was marked as invalid using a closure.'); + + $assert->whereAll([ + 'foo' => 'bar', + 'baz' => function ($value) { + return $value === 'foo'; + }, + ]); + } + + public function testAssertHasAll() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->hasAll([ + 'foo.bar', + 'foo.example', + 'baz', + ]); + } + + public function testAssertHasAllFailsWhenAtLeastOnePropMissing() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.baz] does not exist.'); + + $assert->hasAll([ + 'foo.bar', + 'foo.baz', + 'baz', + ]); + } + + public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() + { + $assert = Assert::fromArray([ + 'foo' => [ + 'bar' => 'value', + 'example' => ['hello' => 'world'], + ], + 'baz' => 'another', + ]); + + $assert->hasAll('foo.bar', 'foo.example', 'baz'); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [foo.baz] does not exist.'); + + $assert->hasAll('foo.bar', 'foo.baz', 'baz'); + } + + public function testAssertCountMultipleProps() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'key' => 'value', + 'prop' => 'example', + ], + 'baz' => [ + 'another' => 'value', + ], + ]); + + $assert->hasAll([ + 'bar' => 2, + 'baz' => 1, + ]); + } + + public function testAssertCountMultiplePropsFailsWhenPropMissing() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'key' => 'value', + 'prop' => 'example', + ], + ]); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Property [baz] does not exist.'); + + $assert->hasAll([ + 'bar' => 2, + 'baz' => 1, + ]); + } + + public function testMacroable() + { + Assert::macro('myCustomMacro', function () { + throw new RuntimeException('My Custom Macro was called!'); + }); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('My Custom Macro was called!'); + + $assert = Assert::fromArray(['foo' => 'bar']); + $assert->myCustomMacro(); + } + + public function testTappable() + { + $assert = Assert::fromArray([ + 'bar' => [ + 'baz' => 'example', + 'prop' => 'value', + ], + ]); + + $called = false; + $assert->has('bar', function (Assert $assert) use (&$called) { + $assert->etc(); + $assert->tap(function (Assert $assert) use (&$called) { + $called = true; + }); + }); + + $this->assertTrue($called, 'The scoped query was never actually called.'); + } +} diff --git a/tests/Testing/Stubs/ArrayableStubObject.php b/tests/Testing/Stubs/ArrayableStubObject.php new file mode 100644 index 000000000000..021440e0b287 --- /dev/null +++ b/tests/Testing/Stubs/ArrayableStubObject.php @@ -0,0 +1,25 @@ +data = $data; + } + + public static function make($data = []) + { + return new self($data); + } + + public function toArray() + { + return $this->data; + } +} diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 42d16e72d3fa..055519925cb9 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -9,6 +9,7 @@ use Illuminate\Encryption\Encrypter; use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; +use Illuminate\Testing\Fluent\Assert; use Illuminate\Testing\TestResponse; use JsonSerializable; use Mockery as m; @@ -577,6 +578,27 @@ public function testAssertJsonWithNull() $response->assertJson($resource->jsonSerialize()); } + public function testAssertJsonWithFluent() + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); + + $response->assertJson(function (Assert $json) { + $json->where('0.foo', 'foo 0'); + }); + } + + public function testAssertJsonWithFluentStrict() + { + $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage('Unexpected properties were found on the root level.'); + + $response->assertJson(function (Assert $json) { + $json->where('0.foo', 'foo 0'); + }, true); + } + public function testAssertSimilarJsonWithMixed() { $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableMixedResourcesStub)); From 3b1fa1ae2ccd64547d27236945dfe454ee9a23e2 Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Wed, 3 Mar 2021 21:15:47 +0200 Subject: [PATCH 09/91] [6.x] update changelog --- CHANGELOG-6.x.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index ee1b7ced29b4..5fd976698b85 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,12 @@ # Release Notes for 6.x -## [Unreleased](https://github.com/laravel/framework/compare/v6.20.16...6.x) +## [Unreleased](https://github.com/laravel/framework/compare/v6.20.17...6.x) + + +## [v6.20.17 (2021-03-02)](https://github.com/laravel/framework/compare/v6.20.16...v6.20.17) + +### Added +- Added new line to `DetectsLostConnections` ([#36373](https://github.com/laravel/framework/pull/36373)) ## [v6.20.16 (2021-02-02)](https://github.com/laravel/framework/compare/v6.20.15...v6.20.16) From f31557f76ac5a790d66b717e074537031c48085e Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Wed, 3 Mar 2021 20:20:34 +0100 Subject: [PATCH 10/91] Apply fixes from StyleCI --- src/Illuminate/Testing/TestResponse.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index b900418caf79..eb983f464a6d 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -528,7 +528,6 @@ public function assertJson($value, $strict = false) } } - return $this; } From 44b63329ce9d260b7f9e60f03389c943082721f6 Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Wed, 3 Mar 2021 21:38:46 +0200 Subject: [PATCH 11/91] [8.x] update changelog --- CHANGELOG-8.x.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index 4d383e7fc191..e132fceadef8 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,22 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.29.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.30.0...8.x) + + +## [v8.30.0 (2021-03-02)](https://github.com/laravel/framework/compare/v8.29.0...v8.30.0) + +### Added +- Added new line to `DetectsLostConnections` ([#36373](https://github.com/laravel/framework/pull/36373)) +- Added `Illuminate\Cache\RateLimiting\Limit::perMinutes()` ([#36352](https://github.com/laravel/framework/pull/36352), [86d0a5c](https://github.com/laravel/framework/commit/86d0a5c733b3f22ae2353df538e07605963c3052)) +- Make Database Factory macroable ([#36380](https://github.com/laravel/framework/pull/36380)) +- Added stop on first failure for Validators ([39e1f84](https://github.com/laravel/framework/commit/39e1f84a48fec024859d4e80948aca9bd7878658)) +- Added `containsOneItem()` method to Collections ([#36428](https://github.com/laravel/framework/pull/36428), [5b7ffc2](https://github.com/laravel/framework/commit/5b7ffc2b54dec803bd12541ab9c3d6bf3d4666ca)) + +### Changed +- Respect custom route key with explicit route model binding ([#36375](https://github.com/laravel/framework/pull/36375)) +- Add Buffered Console Output ([#36404](https://github.com/laravel/framework/pull/36404)) +- Don't flash 'current_password' input ([#36415](https://github.com/laravel/framework/pull/36415)) +- Check for context method in Exception Handler ([#36424](https://github.com/laravel/framework/pull/36424)) ## [v8.29.0 (2021-02-23)](https://github.com/laravel/framework/compare/v8.28.1...v8.29.0) From e5963219867edb43c98a1232f45735e14f85d46c Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Wed, 3 Mar 2021 21:42:08 +0200 Subject: [PATCH 12/91] [8.x] update changelog --- CHANGELOG-8.x.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index e132fceadef8..28603d11124f 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,15 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.30.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.30.1...8.x) + + +## [v8.30.1 (2021-03-03)](https://github.com/laravel/framework/compare/v8.30.0...v8.30.1) + +### Reverted +- Reverted [Respect custom route key with explicit route model binding](https://github.com/laravel/framework/pull/36375) ([#36449](https://github.com/laravel/framework/pull/36449)) + +### Fixed +- Fixed `formatWheres()` methods in `DatabaseRule` ([#36441](https://github.com/laravel/framework/pull/36441)) ## [v8.30.0 (2021-03-02)](https://github.com/laravel/framework/compare/v8.29.0...v8.30.0) From 448662c91540e237b846507fdf4cf22c7f8d59b5 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Thu, 4 Mar 2021 13:51:23 +0100 Subject: [PATCH 13/91] Use ubuntu-18.04 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 476648be81cc..c7b91b6fe781 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: jobs: linux_tests: - runs-on: ubuntu-latest + runs-on: ubuntu-18.04 services: memcached: From cada3d21c7f127ba01bb949b2fa045b7faaedbca Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Thu, 4 Mar 2021 14:07:30 +0100 Subject: [PATCH 14/91] Apply fixes from StyleCI (#36461) --- src/Illuminate/View/Engines/CompilerEngine.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/View/Engines/CompilerEngine.php b/src/Illuminate/View/Engines/CompilerEngine.php index 1d38d310144c..afcba767fa88 100755 --- a/src/Illuminate/View/Engines/CompilerEngine.php +++ b/src/Illuminate/View/Engines/CompilerEngine.php @@ -3,8 +3,8 @@ namespace Illuminate\View\Engines; use Illuminate\View\Compilers\CompilerInterface; -use Throwable; use Illuminate\View\ViewException; +use Throwable; class CompilerEngine extends PhpEngine { From 6cbe0eb0ea43ba4c512ba9ce2519fa2c09a62a94 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Thu, 4 Mar 2021 14:06:39 +0000 Subject: [PATCH 15/91] [8.x] Add new `VendorTagPublished` event (#36458) * feature: add new VendorTagPublished event * fix: only fire event when something was published * refactor: fire event before erroring in console * revert: move event dispatch into else arm * chore: update comment on $paths * chore: publish to publishable * chore: add missing comment * Update VendorPublishCommand.php * Update VendorTagPublished.php Co-authored-by: Taylor Otwell --- .../Console/VendorPublishCommand.php | 7 +++- .../Foundation/Events/VendorTagPublished.php | 33 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Foundation/Events/VendorTagPublished.php diff --git a/src/Illuminate/Foundation/Console/VendorPublishCommand.php b/src/Illuminate/Foundation/Console/VendorPublishCommand.php index 17a459e72834..501142f0d63c 100644 --- a/src/Illuminate/Foundation/Console/VendorPublishCommand.php +++ b/src/Illuminate/Foundation/Console/VendorPublishCommand.php @@ -4,6 +4,7 @@ use Illuminate\Console\Command; use Illuminate\Filesystem\Filesystem; +use Illuminate\Foundation\Events\VendorTagPublished; use Illuminate\Support\Arr; use Illuminate\Support\ServiceProvider; use League\Flysystem\Adapter\Local as LocalAdapter; @@ -159,7 +160,9 @@ protected function publishTag($tag) { $published = false; - foreach ($this->pathsToPublish($tag) as $from => $to) { + $pathsToPublish = $this->pathsToPublish($tag); + + foreach ($pathsToPublish as $from => $to) { $this->publishItem($from, $to); $published = true; @@ -167,6 +170,8 @@ protected function publishTag($tag) if ($published === false) { $this->error('Unable to locate publishable resources.'); + } else { + $this->laravel['events']->dispatch(new VendorTagPublished($tag, $pathsToPublish)); } } diff --git a/src/Illuminate/Foundation/Events/VendorTagPublished.php b/src/Illuminate/Foundation/Events/VendorTagPublished.php new file mode 100644 index 000000000000..084c1293fcfd --- /dev/null +++ b/src/Illuminate/Foundation/Events/VendorTagPublished.php @@ -0,0 +1,33 @@ +tag = $tag; + $this->paths = $paths; + } +} From 34418f340f29d7602be2663a761f4911ecf4c5d0 Mon Sep 17 00:00:00 2001 From: Ryan Chandler Date: Thu, 4 Mar 2021 14:09:31 +0000 Subject: [PATCH 16/91] [8.x] Add new `Stringable::test` method (#36462) * feature: add new Stringable::matches method * tests: new Stringable::matches method * refactor: use match instead of matchAll * chore: rename method from matches to test --- src/Illuminate/Support/Stringable.php | 11 +++++++++++ tests/Support/SupportStringableTest.php | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index ff5f44c5fded..0d544a3987cf 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -365,6 +365,17 @@ public function matchAll($pattern) return collect($matches[1] ?? $matches[0]); } + /** + * Determine if the string matches the given pattern. + * + * @param string $pattern + * @return bool + */ + public function test($pattern) + { + return $this->match($pattern)->isNotEmpty(); + } + /** * Pad both sides of the string with another. * diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 15ab04726cb1..d6c010fd20fd 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -62,6 +62,14 @@ public function testMatch() $this->assertTrue($stringable->matchAll('/nothing/')->isEmpty()); } + public function testTest() + { + $stringable = $this->stringable('foo bar'); + + $this->assertTrue($stringable->test('/bar/')); + $this->assertTrue($stringable->test('/foo (.*)/')); + } + public function testTrim() { $this->assertSame('foo', (string) $this->stringable(' foo ')->trim()); From ddb0c5e68ec8037ed31738bbfed4ededa3cd39da Mon Sep 17 00:00:00 2001 From: LuttaMustache <45479378+LuttaMustache@users.noreply.github.com> Date: Thu, 4 Mar 2021 21:10:57 +0600 Subject: [PATCH 17/91] multiplatform newline check (#36464) --- src/Illuminate/Foundation/Console/PolicyMakeCommand.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php index 0fe6964d5940..78873de065ee 100644 --- a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php +++ b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php @@ -131,8 +131,13 @@ protected function replaceModel($stub, $model) array_keys($replace), array_values($replace), $stub ); - return str_replace( - "use {$namespacedModel};\nuse {$namespacedModel};", "use {$namespacedModel};", $stub + return preg_replace( + vsprintf('/use %s;[\r\n]+use %s;/', [ + preg_quote($namespacedModel, '/'), + preg_quote($namespacedModel, '/'), + ]), + "use {$namespacedModel};", + $stub ); } From 5434332367c23230be01391b500005879bd6fa93 Mon Sep 17 00:00:00 2001 From: Andrea Marco Sartori Date: Fri, 5 Mar 2021 01:11:56 +1000 Subject: [PATCH 18/91] Add test for dumping requests --- tests/Http/HttpClientTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index b407d6fb3f0e..95f08521babb 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -11,6 +11,7 @@ use Illuminate\Support\Str; use OutOfBoundsException; use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\VarDumper; class HttpClientTest extends TestCase { @@ -781,4 +782,23 @@ function (Request $request) { $this->factory->assertSentInOrder($executionOrder); } + + public function testCanDump() + { + $dumped = []; + + VarDumper::setHandler(function ($value) use (&$dumped) { + $dumped[] = $value; + }); + + $this->factory->fake()->dump(1, 2, 3)->withOptions(['delay' => 1000])->get('http://foo.com'); + + $this->assertSame(1, $dumped[0]); + $this->assertSame(2, $dumped[1]); + $this->assertSame(3, $dumped[2]); + $this->assertInstanceOf(Request::class, $dumped[3]); + $this->assertSame(1000, $dumped[4]['delay']); + + VarDumper::setHandler(null); + } } From 6699a47565146cd318a22435f99104d54a90c2c1 Mon Sep 17 00:00:00 2001 From: Andrea Marco Sartori Date: Fri, 5 Mar 2021 01:12:44 +1000 Subject: [PATCH 19/91] Implement dump() and dd() methods --- src/Illuminate/Http/Client/PendingRequest.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 836e8b39fe50..41ad5e983840 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use Symfony\Component\VarDumper\VarDumper; class PendingRequest { @@ -452,6 +453,40 @@ public function beforeSending($callback) }); } + /** + * Dump the request. + * + * @return $this + */ + public function dump() + { + $values = func_get_args(); + + return $this->beforeSending(function (Request $request, array $options) use ($values) { + foreach (array_merge($values, [$request, $options]) as $value) { + VarDumper::dump($value); + } + }); + } + + /** + * Dump the request and end the script. + * + * @return $this + */ + public function dd() + { + $values = func_get_args(); + + return $this->beforeSending(function (Request $request, array $options) use ($values) { + foreach (array_merge($values, [$request, $options]) as $value) { + VarDumper::dump($value); + } + + exit(1); + }); + } + /** * Issue a GET request to the given URL. * From b1d76be077a96bc28321e5e289c4f0de324cd0ff Mon Sep 17 00:00:00 2001 From: Andrea Marco Sartori Date: Fri, 5 Mar 2021 01:13:13 +1000 Subject: [PATCH 20/91] Add docblocks for dumping methods --- src/Illuminate/Http/Client/Factory.php | 2 ++ src/Illuminate/Support/Facades/Http.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index 0f182b6a65cb..4db3e1fa98a0 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -32,6 +32,8 @@ * @method \Illuminate\Http\Client\PendingRequest withToken(string $token, string $type = 'Bearer') * @method \Illuminate\Http\Client\PendingRequest withoutRedirecting() * @method \Illuminate\Http\Client\PendingRequest withoutVerifying() + * @method \Illuminate\Http\Client\PendingRequest dump() + * @method \Illuminate\Http\Client\PendingRequest dd() * @method \Illuminate\Http\Client\Response delete(string $url, array $data = []) * @method \Illuminate\Http\Client\Response get(string $url, array $query = []) * @method \Illuminate\Http\Client\Response head(string $url, array $query = []) diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index c6a26b2108cd..426d574789c5 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -30,6 +30,8 @@ * @method static \Illuminate\Http\Client\PendingRequest withToken(string $token, string $type = 'Bearer') * @method static \Illuminate\Http\Client\PendingRequest withoutRedirecting() * @method static \Illuminate\Http\Client\PendingRequest withoutVerifying() + * @method static \Illuminate\Http\Client\PendingRequest dump() + * @method static \Illuminate\Http\Client\PendingRequest dd() * @method static \Illuminate\Http\Client\Response delete(string $url, array $data = []) * @method static \Illuminate\Http\Client\Response get(string $url, array $query = []) * @method static \Illuminate\Http\Client\Response head(string $url, array $query = []) From 26fe23fc6819c20495791e791c97405c38ce4de0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Mar 2021 09:19:57 -0600 Subject: [PATCH 21/91] Revert "Revert "Revert "[8.x] Fix formatWheres in DatabaseRule (#36441)" (#36452)" (#36453)" (#36465) This reverts commit 018314829ccb70ec20a6fc25fa4bd382d09d6cc0. --- src/Illuminate/Validation/Rules/DatabaseRule.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Illuminate/Validation/Rules/DatabaseRule.php b/src/Illuminate/Validation/Rules/DatabaseRule.php index 88b4d27cd941..b8113b2afadb 100644 --- a/src/Illuminate/Validation/Rules/DatabaseRule.php +++ b/src/Illuminate/Validation/Rules/DatabaseRule.php @@ -196,11 +196,7 @@ public function queryCallbacks() protected function formatWheres() { return collect($this->wheres)->map(function ($where) { - if (is_bool($where['value'])) { - return $where['column'].','.($where['value'] ? 'true' : 'false'); - } else { - return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; - } + return $where['column'].','.'"'.str_replace('"', '""', $where['value']).'"'; })->implode(','); } } From 2aa5c2488d25178ebc097052c7897a0e463ddc35 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 4 Mar 2021 09:22:36 -0600 Subject: [PATCH 22/91] version --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 1e35f20d4f46..45be0dd0b96e 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -33,7 +33,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '8.30.1'; + const VERSION = '8.31.0'; /** * The base path for the Laravel installation. From 91bd93f92c26cc58c25849f1a1ac6d7ee6d55411 Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Thu, 4 Mar 2021 17:29:43 +0100 Subject: [PATCH 23/91] Add docblocks & minor cleanup --- src/Illuminate/Testing/Fluent/Assert.php | 58 +++++++++- .../Testing/Fluent/Concerns/Debugging.php | 18 ++++ .../Testing/Fluent/Concerns/Has.php | 100 ++++++++++++++---- .../Testing/Fluent/Concerns/Interaction.php | 28 ++++- .../Testing/Fluent/Concerns/Matching.php | 65 +++++++++--- 5 files changed, 230 insertions(+), 39 deletions(-) diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/Assert.php index a5e89e83294c..926ef09656ac 100644 --- a/src/Illuminate/Testing/Fluent/Assert.php +++ b/src/Illuminate/Testing/Fluent/Assert.php @@ -19,19 +19,39 @@ class Assert implements Arrayable Macroable, Tappable; - /** @var array */ + /** + * The properties in the current scope. + * + * @var array + */ private $props; - /** @var string */ + /** + * The "dot" path to the current scope. + * + * @var string|null + */ private $path; + /** + * Create a new Assert instance. + * + * @param array $props + * @param string|null $path + */ protected function __construct(array $props, string $path = null) { $this->path = $path; $this->props = $props; } - protected function dotPath($key): string + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + protected function dotPath(string $key): string { if (is_null($this->path)) { return $key; @@ -40,12 +60,25 @@ protected function dotPath($key): string return implode('.', [$this->path, $key]); } + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ protected function prop(string $key = null) { return Arr::get($this->props, $key); } - protected function scope($key, Closure $callback): self + /** + * Instantiate a new "scope" at the path of the given key. + * + * @param string $key + * @param Closure $callback + * @return $this + */ + protected function scope(string $key, Closure $callback): self { $props = $this->prop($key); $path = $this->dotPath($key); @@ -59,16 +92,33 @@ protected function scope($key, Closure $callback): self return $this; } + /** + * Create a new instance from an array. + * + * @param array $data + * @return static + */ public static function fromArray(array $data): self { return new self($data); } + /** + * Create a new instance from a AssertableJsonString. + * + * @param AssertableJsonString $json + * @return static + */ public static function fromAssertableJsonString(AssertableJsonString $json): self { return self::fromArray($json->json()); } + /** + * Get the instance as an array. + * + * @return array + */ public function toArray() { return $this->props; diff --git a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php index d604f0e0b21d..f51d119074ae 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Debugging.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Debugging.php @@ -4,6 +4,12 @@ trait Debugging { + /** + * Dumps the given props. + * + * @param string|null $prop + * @return $this + */ public function dump(string $prop = null): self { dump($this->prop($prop)); @@ -11,10 +17,22 @@ public function dump(string $prop = null): self return $this; } + /** + * Dumps the given props and exits. + * + * @param string|null $prop + * @return void + */ public function dd(string $prop = null): void { dd($this->prop($prop)); } + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ abstract protected function prop(string $key = null); } diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php index 28955a742493..746fa49b60f1 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Has.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -8,7 +8,14 @@ trait Has { - protected function count(string $key, $length): self + /** + * Assert that the prop is of the expected size. + * + * @param string $key + * @param int $length + * @return $this + */ + protected function count(string $key, int $length): self { PHPUnit::assertCount( $length, @@ -19,21 +26,14 @@ protected function count(string $key, $length): self return $this; } - public function hasAll($key): self - { - $keys = is_array($key) ? $key : func_get_args(); - - foreach ($keys as $prop => $count) { - if (is_int($prop)) { - $this->has($count); - } else { - $this->has($prop, $count); - } - } - - return $this; - } - + /** + * Ensure that the given prop exists. + * + * @param string $key + * @param null $value + * @param Closure|null $scope + * @return $this + */ public function has(string $key, $value = null, Closure $scope = null): self { $prop = $this->prop(); @@ -69,6 +69,33 @@ public function has(string $key, $value = null, Closure $scope = null): self return $this; } + /** + * Assert that all of the given props exist. + * + * @param array|string $key + * @return $this + */ + public function hasAll($key): self + { + $keys = is_array($key) ? $key : func_get_args(); + + foreach ($keys as $prop => $count) { + if (is_int($prop)) { + $this->has($count); + } else { + $this->has($prop, $count); + } + } + + return $this; + } + + /** + * Assert that none of the given props exist. + * + * @param array|string $key + * @return $this + */ public function missingAll($key): self { $keys = is_array($key) ? $key : func_get_args(); @@ -80,6 +107,12 @@ public function missingAll($key): self return $this; } + /** + * Assert that the given prop does not exist. + * + * @param string $key + * @return $this + */ public function missing(string $key): self { PHPUnit::assertNotTrue( @@ -90,11 +123,36 @@ public function missing(string $key): self return $this; } - abstract protected function prop(string $key = null); - - abstract protected function dotPath($key): string; - + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + abstract protected function dotPath(string $key): string; + + /** + * Marks the property as interacted. + * + * @param string $key + * @return void + */ abstract protected function interactsWith(string $key): void; - abstract protected function scope($key, Closure $callback); + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ + abstract protected function prop(string $key = null); + + /** + * Instantiate a new "scope" at the path of the given key. + * + * @param string $key + * @param Closure $callback + * @return $this + */ + abstract protected function scope(string $key, Closure $callback); } diff --git a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php index d938438a618c..15e7e9508f55 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Interaction.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Interaction.php @@ -7,9 +7,19 @@ trait Interaction { - /** @var array */ + /** + * The list of interacted properties. + * + * @var array + */ protected $interacted = []; + /** + * Marks the property as interacted. + * + * @param string $key + * @return void + */ protected function interactsWith(string $key): void { $prop = Str::before($key, '.'); @@ -19,6 +29,11 @@ protected function interactsWith(string $key): void } } + /** + * Asserts that all properties have been interacted with. + * + * @return void + */ public function interacted(): void { PHPUnit::assertSame( @@ -30,6 +45,11 @@ public function interacted(): void ); } + /** + * Disables the interaction check. + * + * @return $this + */ public function etc(): self { $this->interacted = array_keys($this->prop()); @@ -37,5 +57,11 @@ public function etc(): self return $this; } + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ abstract protected function prop(string $key = null); } diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php index 3edce127d362..052b2d49ab2f 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Matching.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -9,16 +9,14 @@ trait Matching { - public function whereAll(array $bindings): self - { - foreach ($bindings as $key => $value) { - $this->where($key, $value); - } - - return $this; - } - - public function where($key, $expected): self + /** + * Asserts that the property matches the expected value. + * + * @param string $key + * @param mixed|callable $expected + * @return $this + */ + public function where(string $key, $expected): self { $this->has($key); @@ -49,6 +47,27 @@ public function where($key, $expected): self return $this; } + /** + * Asserts that all properties match their expected values. + * + * @param array $bindings + * @return $this + */ + public function whereAll(array $bindings): self + { + foreach ($bindings as $key => $value) { + $this->where($key, $value); + } + + return $this; + } + + /** + * Ensures that all properties are sorted the same way, recursively. + * + * @param mixed $value + * @return void + */ protected function ensureSorted(&$value): void { if (! is_array($value)) { @@ -62,9 +81,29 @@ protected function ensureSorted(&$value): void ksort($value); } - abstract protected function dotPath($key): string; + /** + * Compose the absolute "dot" path to the given key. + * + * @param string $key + * @return string + */ + abstract protected function dotPath(string $key): string; + + /** + * Ensure that the given prop exists. + * + * @param string $key + * @param null $value + * @param Closure|null $scope + * @return $this + */ + abstract public function has(string $key, $value = null, Closure $scope = null); + /** + * Retrieve a prop within the current scope using "dot" notation. + * + * @param string|null $key + * @return mixed + */ abstract protected function prop(string $key = null); - - abstract public function has(string $key, $value = null, Closure $scope = null); } From c5eadc4d9de5a2597dc0c4b64f0d009e287c25a6 Mon Sep 17 00:00:00 2001 From: Claudio Dekker <1752195+claudiodekker@users.noreply.github.com> Date: Thu, 4 Mar 2021 18:11:49 +0100 Subject: [PATCH 24/91] Use FQN in DocBlocks --- src/Illuminate/Testing/Fluent/Assert.php | 4 ++-- src/Illuminate/Testing/Fluent/Concerns/Has.php | 4 ++-- src/Illuminate/Testing/Fluent/Concerns/Matching.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/Assert.php index 926ef09656ac..bde937a0bf23 100644 --- a/src/Illuminate/Testing/Fluent/Assert.php +++ b/src/Illuminate/Testing/Fluent/Assert.php @@ -75,7 +75,7 @@ protected function prop(string $key = null) * Instantiate a new "scope" at the path of the given key. * * @param string $key - * @param Closure $callback + * @param \Closure $callback * @return $this */ protected function scope(string $key, Closure $callback): self @@ -106,7 +106,7 @@ public static function fromArray(array $data): self /** * Create a new instance from a AssertableJsonString. * - * @param AssertableJsonString $json + * @param \Illuminate\Testing\AssertableJsonString $json * @return static */ public static function fromAssertableJsonString(AssertableJsonString $json): self diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php index 746fa49b60f1..19b9ad9915ca 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Has.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -31,7 +31,7 @@ protected function count(string $key, int $length): self * * @param string $key * @param null $value - * @param Closure|null $scope + * @param \Closure|null $scope * @return $this */ public function has(string $key, $value = null, Closure $scope = null): self @@ -151,7 +151,7 @@ abstract protected function prop(string $key = null); * Instantiate a new "scope" at the path of the given key. * * @param string $key - * @param Closure $callback + * @param \Closure $callback * @return $this */ abstract protected function scope(string $key, Closure $callback); diff --git a/src/Illuminate/Testing/Fluent/Concerns/Matching.php b/src/Illuminate/Testing/Fluent/Concerns/Matching.php index 052b2d49ab2f..3cf1f82c471c 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Matching.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Matching.php @@ -13,7 +13,7 @@ trait Matching * Asserts that the property matches the expected value. * * @param string $key - * @param mixed|callable $expected + * @param mixed|\Closure $expected * @return $this */ public function where(string $key, $expected): self @@ -94,7 +94,7 @@ abstract protected function dotPath(string $key): string; * * @param string $key * @param null $value - * @param Closure|null $scope + * @param \Closure|null $scope * @return $this */ abstract public function has(string $key, $value = null, Closure $scope = null); From 0ddc7ab7e94003be1eec3080fce6aea3382912e6 Mon Sep 17 00:00:00 2001 From: Paras Malhotra Date: Fri, 5 Mar 2021 03:06:47 +0530 Subject: [PATCH 25/91] Add circuit breaker job middleware --- .../Queue/Middleware/CircuitBreaker.php | 129 ++++++++++++++++ .../Integration/Queue/CircuitBreakerTest.php | 144 ++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 src/Illuminate/Queue/Middleware/CircuitBreaker.php create mode 100644 tests/Integration/Queue/CircuitBreakerTest.php diff --git a/src/Illuminate/Queue/Middleware/CircuitBreaker.php b/src/Illuminate/Queue/Middleware/CircuitBreaker.php new file mode 100644 index 000000000000..eb764a957561 --- /dev/null +++ b/src/Illuminate/Queue/Middleware/CircuitBreaker.php @@ -0,0 +1,129 @@ +maxAttempts = $maxAttempts; + $this->decayMinutes = $decayMinutes; + $this->retryAfterMinutes = $retryAfterMinutes; + $this->key = $key; + } + + /** + * Process the job. + * + * @param mixed $job + * @param callable $next + * @return mixed + */ + public function handle($job, $next) + { + $this->limiter = Container::getInstance()->make(RateLimiter::class); + + if ($this->limiter->tooManyAttempts($jobKey = $this->getKey($job), $this->maxAttempts)) { + return $job->release($this->getTimeUntilNextRetry($jobKey)); + } + + try { + $next($job); + + $this->limiter->clear($jobKey); + } catch (Throwable $throwable) { + $this->limiter->hit($jobKey, $this->decayMinutes * 60); + + return $job->release($this->retryAfterMinutes * 60); + } + } + + /** + * Set the prefix of the rate limiter key. + * + * @param string $prefix + * @return $this + */ + public function withPrefix(string $prefix) + { + $this->prefix = $prefix; + + return $this; + } + + /** + * Get the number of seconds that should elapse before the job is retried. + * + * @param string $key + * @return int + */ + protected function getTimeUntilNextRetry($key) + { + return $this->limiter->availableIn($key) + 3; + } + + /** + * Get the cache key associated for the rate limiter. + * + * @param mixed $job + * @return string + */ + protected function getKey($job) + { + return md5($this->prefix.(empty($this->key) ? get_class($job) : $this->key)); + } +} diff --git a/tests/Integration/Queue/CircuitBreakerTest.php b/tests/Integration/Queue/CircuitBreakerTest.php new file mode 100644 index 000000000000..977a00a76c0f --- /dev/null +++ b/tests/Integration/Queue/CircuitBreakerTest.php @@ -0,0 +1,144 @@ +assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); + } + + public function testCircuitStaysClosedForSuccessfulJobs() + { + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + } + + public function testCircuitResetsAfterSuccess() + { + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobRanSuccessfully(CircuitBreakerSuccessfulJob::class); + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedImmediately(CircuitBreakerTestJob::class); + $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); + } + + protected function assertJobWasReleasedImmediately($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->with(0)->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } + + protected function assertJobWasReleasedWithDelay($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->withArgs(function ($delay) { + return $delay >= 600; + })->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertFalse($class::$handled); + } + + protected function assertJobRanSuccessfully($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('isReleased')->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } +} + +class CircuitBreakerTestJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [new CircuitBreaker(2, 10, 0, 'test')]; + } +} + +class CircuitBreakerSuccessfulJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + } + + public function middleware() + { + return [new CircuitBreaker(2, 10, 0, 'test')]; + } +} From 18daf4619de068cf106ef49c947087d2b655d67c Mon Sep 17 00:00:00 2001 From: Pat Gagnon-Renaud Date: Fri, 5 Mar 2021 09:40:56 -0500 Subject: [PATCH 26/91] Delete existing links that are broken (#36470) When a link exists but is broken, `file_exists($link)` return false. And when `symlink($link, $target)` is called on a broken link, a PHP Warning is returned and the link is not updated. To fix this, we add an additional check using `is_link($link)` (which return true, even if the link is broken) to detect and delete broken links. --- src/Illuminate/Foundation/Console/StorageLinkCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Foundation/Console/StorageLinkCommand.php b/src/Illuminate/Foundation/Console/StorageLinkCommand.php index 11e5c148c1ce..82b461598338 100644 --- a/src/Illuminate/Foundation/Console/StorageLinkCommand.php +++ b/src/Illuminate/Foundation/Console/StorageLinkCommand.php @@ -31,6 +31,10 @@ public function handle() return $this->error('The "public/storage" directory already exists.'); } + if (is_link(public_path('storage'))) { + $this->laravel->make('files')->delete(public_path('storage')); + } + $this->laravel->make('files')->link( storage_path('app/public'), public_path('storage') ); From 1e19a5668751f3a19b65d551e9253dd8f46a3c48 Mon Sep 17 00:00:00 2001 From: Pat Gagnon-Renaud Date: Fri, 5 Mar 2021 09:41:10 -0500 Subject: [PATCH 27/91] Delete existing links that are broken (#36469) When a link exists but is broken, `file_exists($link)` return false. And when `symlink($link, $target)` is called on a broken link, a PHP Warning is returned and the link is not updated. To fix this, we add an additional check using `is_link($link)` (which return true, even if the link is broken) to detect and delete broken links. --- src/Illuminate/Foundation/Console/StorageLinkCommand.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Illuminate/Foundation/Console/StorageLinkCommand.php b/src/Illuminate/Foundation/Console/StorageLinkCommand.php index a0419bf6c077..0d47ddae7294 100644 --- a/src/Illuminate/Foundation/Console/StorageLinkCommand.php +++ b/src/Illuminate/Foundation/Console/StorageLinkCommand.php @@ -35,6 +35,10 @@ public function handle() continue; } + if (is_link($link)) { + $this->laravel->make('files')->delete($link); + } + if ($relative) { $this->laravel->make('files')->relativeLink($target, $link); } else { From 10f1a935205340ba8954e7075c1d9b67943db27d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Mar 2021 09:17:09 -0600 Subject: [PATCH 28/91] formatting --- src/Illuminate/Cache/PhpRedisLock.php | 57 ++++++++++++++++++--------- src/Illuminate/Cache/RedisStore.php | 1 + 2 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/Illuminate/Cache/PhpRedisLock.php b/src/Illuminate/Cache/PhpRedisLock.php index 8d0670a167fd..f315ae64acb2 100644 --- a/src/Illuminate/Cache/PhpRedisLock.php +++ b/src/Illuminate/Cache/PhpRedisLock.php @@ -8,6 +8,15 @@ class PhpRedisLock extends RedisLock { + /** + * Create a new phpredis lock instance. + * + * @param \Illuminate\Redis\Connections\PhpRedisConnection $redis + * @param string $name + * @param int $seconds + * @param string|null $owner + * @return void + */ public function __construct(PhpRedisConnection $redis, string $name, int $seconds, ?string $owner = null) { parent::__construct($redis, $name, $seconds, $owner); @@ -26,26 +35,18 @@ public function release() ); } + /** + * Get the owner key, serialized and compressed. + * + * @return string + */ protected function serializedAndCompressedOwner(): string { $client = $this->redis->client(); - /* If a serialization mode such as "php" or "igbinary" and/or a - * compression mode such as "lzf" or "zstd" is enabled, the owner - * must be serialized and/or compressed by us, because phpredis does - * not do this for the eval command. - * - * Name must not be modified! - */ $owner = $client->_serialize($this->owner); - /* Once the phpredis extension exposes a compress function like the - * above `_serialize()` function, we should switch to it to guarantee - * consistency in the way the extension serializes and compresses to - * avoid the need to check each compression option ourselves. - * - * @see https://github.com/phpredis/phpredis/issues/1938 - */ + // https://github.com/phpredis/phpredis/issues/1938 if ($this->compressed()) { if ($this->lzfCompressed()) { $owner = \lzf_compress($owner); @@ -55,7 +56,7 @@ protected function serializedAndCompressedOwner(): string $owner = \lz4_compress($owner, $client->getOption(Redis::OPT_COMPRESSION_LEVEL)); } else { throw new UnexpectedValueException(sprintf( - 'Unknown phpredis compression in use (%d). Unable to release lock.', + 'Unknown phpredis compression in use [%d]. Unable to release lock.', $client->getOption(Redis::OPT_COMPRESSION) )); } @@ -64,26 +65,46 @@ protected function serializedAndCompressedOwner(): string return $owner; } + /** + * Determine if compression is enabled. + * + * @return bool + */ protected function compressed(): bool { return $this->redis->client()->getOption(Redis::OPT_COMPRESSION) !== Redis::COMPRESSION_NONE; } + /** + * Determine if LZF compression is enabled. + * + * @return bool + */ protected function lzfCompressed(): bool { return defined('Redis::COMPRESSION_LZF') && - $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZF; } + /** + * Determine if ZSTD compression is enabled. + * + * @return bool + */ protected function zstdCompressed(): bool { return defined('Redis::COMPRESSION_ZSTD') && - $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_ZSTD; } + /** + * Determine if LZ4 compression is enabled. + * + * @return bool + */ protected function lz4Compressed(): bool { return defined('Redis::COMPRESSION_LZ4') && - $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; + $this->redis->client()->getOption(Redis::OPT_COMPRESSION) === Redis::COMPRESSION_LZ4; } } diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index e698bab375d4..4896c9183d03 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -190,6 +190,7 @@ public function forever($key, $value) public function lock($name, $seconds = 0, $owner = null) { $lockName = $this->prefix.$name; + $lockConnection = $this->lockConnection(); if ($lockConnection instanceof PhpRedisConnection) { From d7a4280423518939eeefce1f75f84b6e6bf03ad3 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Mar 2021 09:17:42 -0600 Subject: [PATCH 29/91] Apply fixes from StyleCI (#36477) --- src/Illuminate/Cache/PhpRedisLock.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Cache/PhpRedisLock.php b/src/Illuminate/Cache/PhpRedisLock.php index f315ae64acb2..9d0215f37d6d 100644 --- a/src/Illuminate/Cache/PhpRedisLock.php +++ b/src/Illuminate/Cache/PhpRedisLock.php @@ -46,7 +46,7 @@ protected function serializedAndCompressedOwner(): string $owner = $client->_serialize($this->owner); - // https://github.com/phpredis/phpredis/issues/1938 + // https://github.com/phpredis/phpredis/issues/1938 if ($this->compressed()) { if ($this->lzfCompressed()) { $owner = \lzf_compress($owner); From d2106e6c55a8facc55aed654ffe14ebb1c8b14fa Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Mar 2021 09:22:09 -0600 Subject: [PATCH 30/91] formatting --- src/Illuminate/Http/Client/PendingRequest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 41ad5e983840..5801bc0b70f0 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -454,7 +454,7 @@ public function beforeSending($callback) } /** - * Dump the request. + * Dump the request before sending. * * @return $this */ @@ -470,7 +470,7 @@ public function dump() } /** - * Dump the request and end the script. + * Dump the request before sending and end the script. * * @return $this */ From 883713b084fab51f972503395b14f662297cbab4 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij Date: Fri, 5 Mar 2021 22:13:45 +0100 Subject: [PATCH 31/91] Use user defined url for AwsTemporaryUrl method --- .../Filesystem/FilesystemAdapter.php | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 896bfbd6f35a..f671c85a6a9c 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -23,6 +23,7 @@ use League\Flysystem\Sftp\SftpAdapter as Sftp; use PHPUnit\Framework\Assert as PHPUnit; use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; use RuntimeException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -593,9 +594,18 @@ public function getAwsTemporaryUrl($adapter, $path, $expiration, $options) 'Key' => $adapter->getPathPrefix().$path, ], $options)); - return (string) $client->createPresignedRequest( + $uri = $client->createPresignedRequest( $command, $expiration )->getUri(); + + // If an explicit base URL has been set on the disk configuration then we will use + // it as the base URL instead of the default path. This allows the developer to + // have full control over the base path for this filesystem's generated URLs. + if (! is_null($url = $this->driver->getConfig()->get('url'))) { + $uri = $this->replaceBaseUrl($uri, $url); + } + + return (string) $uri; } /** @@ -610,6 +620,20 @@ protected function concatPathToUrl($url, $path) return rtrim($url, '/').'/'.ltrim($path, '/'); } + /** + * Replace base parts of UriInterface by values from URL. + * + * @param UriInterface $uri + * @param string $url + * @return UriInterface + */ + protected function replaceBaseUrl($uri, $url) + { + $parsed_url = parse_url($url); + + return $uri->withScheme($parsed_url['scheme'])->withHost($parsed_url['host']); + } + /** * Get an array of all files in a directory. * From 21fee7649e1b48a7701b8ba860218741c2c3bcef Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 5 Mar 2021 17:04:38 -0600 Subject: [PATCH 32/91] rename class --- ...uitBreaker.php => ThrottlesExceptions.php} | 48 ++++++++++++++----- ...erTest.php => ThrottlesExceptionsTest.php} | 8 ++-- 2 files changed, 40 insertions(+), 16 deletions(-) rename src/Illuminate/Queue/Middleware/{CircuitBreaker.php => ThrottlesExceptions.php} (78%) rename tests/Integration/Queue/{CircuitBreakerTest.php => ThrottlesExceptionsTest.php} (94%) diff --git a/src/Illuminate/Queue/Middleware/CircuitBreaker.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php similarity index 78% rename from src/Illuminate/Queue/Middleware/CircuitBreaker.php rename to src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index eb764a957561..edb3dd164d33 100644 --- a/src/Illuminate/Queue/Middleware/CircuitBreaker.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -6,10 +6,10 @@ use Illuminate\Container\Container; use Throwable; -class CircuitBreaker +class ThrottlesExceptions { /** - * The maximum number of attempts allowed before the circuit is opened. + * The maximum number of attempts allowed before rate limiting applies. * * @var int */ @@ -36,6 +36,13 @@ class CircuitBreaker */ protected $key; + /** + * The callback that determines if rate limiting should apply. + * + * @var callable + */ + protected $whenCallback; + /** * The prefix of the rate limiter key. * @@ -86,6 +93,10 @@ public function handle($job, $next) $this->limiter->clear($jobKey); } catch (Throwable $throwable) { + if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable)) { + throw $throwable; + } + $this->limiter->hit($jobKey, $this->decayMinutes * 60); return $job->release($this->retryAfterMinutes * 60); @@ -93,27 +104,29 @@ public function handle($job, $next) } /** - * Set the prefix of the rate limiter key. + * Specify a callback that should determine if rate limiting behavior should apply. * - * @param string $prefix + * @param callable $callback * @return $this */ - public function withPrefix(string $prefix) + public function when(callable $callback) { - $this->prefix = $prefix; + $this->whenCallback = $callback; return $this; } /** - * Get the number of seconds that should elapse before the job is retried. + * Set the prefix of the rate limiter key. * - * @param string $key - * @return int + * @param string $prefix + * @return $this */ - protected function getTimeUntilNextRetry($key) + public function withPrefix(string $prefix) { - return $this->limiter->availableIn($key) + 3; + $this->prefix = $prefix; + + return $this; } /** @@ -124,6 +137,17 @@ protected function getTimeUntilNextRetry($key) */ protected function getKey($job) { - return md5($this->prefix.(empty($this->key) ? get_class($job) : $this->key)); + return $this->prefix.md5(empty($this->key) ? get_class($job) : $this->key); + } + + /** + * Get the number of seconds that should elapse before the job is retried. + * + * @param string $key + * @return int + */ + protected function getTimeUntilNextRetry($key) + { + return $this->limiter->availableIn($key) + 3; } } diff --git a/tests/Integration/Queue/CircuitBreakerTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php similarity index 94% rename from tests/Integration/Queue/CircuitBreakerTest.php rename to tests/Integration/Queue/ThrottlesExceptionsTest.php index 977a00a76c0f..b51190e41654 100644 --- a/tests/Integration/Queue/CircuitBreakerTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -8,14 +8,14 @@ use Illuminate\Contracts\Queue\Job; use Illuminate\Queue\CallQueuedHandler; use Illuminate\Queue\InteractsWithQueue; -use Illuminate\Queue\Middleware\CircuitBreaker; +use Illuminate\Queue\Middleware\ThrottlesExceptions; use Mockery as m; use Orchestra\Testbench\TestCase; /** * @group integration */ -class CircuitBreakerTest extends TestCase +class ThrottlesExceptionsTest extends TestCase { protected function tearDown(): void { @@ -122,7 +122,7 @@ public function handle() public function middleware() { - return [new CircuitBreaker(2, 10, 0, 'test')]; + return [new ThrottlesExceptions(2, 10, 0, 'test')]; } } @@ -139,6 +139,6 @@ public function handle() public function middleware() { - return [new CircuitBreaker(2, 10, 0, 'test')]; + return [new ThrottlesExceptions(2, 10, 0, 'test')]; } } From c5fa6935c731405f9e4edd531203ce094ea43e65 Mon Sep 17 00:00:00 2001 From: Oisin O'Neill Date: Sun, 7 Mar 2021 20:26:06 +0000 Subject: [PATCH 33/91] [6.x] Update changelog for v6.18.27 with upgrade info around cookies (#36490) * Update changelog for v6.18.27 with upgrade info around cookies After this this change any existing cookies will be invalid (which may have implications for some apps) * Update CHANGELOG-6.x.md * Update CHANGELOG-6.x.md Co-authored-by: Taylor Otwell --- CHANGELOG-6.x.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index 5fd976698b85..860012f0bd0a 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -259,6 +259,8 @@ ### Changed - Improve cookie encryption ([#33662](https://github.com/laravel/framework/pull/33662)) +This change will invalidate all existing cookies. Please see [this security bulletin](https://blog.laravel.com/laravel-cookie-security-releases) for more information. + ## [v6.18.26 (2020-07-21)](https://github.com/laravel/framework/compare/v6.18.25...v6.18.26) From 466000e4b547d8e18065bcd47c7234745d3988e7 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 7 Mar 2021 16:39:47 -0600 Subject: [PATCH 34/91] formatting --- src/Illuminate/Filesystem/FilesystemAdapter.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index f671c85a6a9c..5d6de6ed3321 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -621,17 +621,17 @@ protected function concatPathToUrl($url, $path) } /** - * Replace base parts of UriInterface by values from URL. + * Replace the scheme and host of the given UriInterface with values from the given URL. * - * @param UriInterface $uri + * @param \Psr\Http\Message\UriInterface $uri * @param string $url - * @return UriInterface + * @return \Psr\Http\Message\UriInterface */ protected function replaceBaseUrl($uri, $url) { - $parsed_url = parse_url($url); + $parsed = parse_url($url); - return $uri->withScheme($parsed_url['scheme'])->withHost($parsed_url['host']); + return $uri->withScheme($parsed['scheme'])->withHost($parsed['host']); } /** From f1b17a28d54c8c8446fc0c71f6c26a9223ad9fd2 Mon Sep 17 00:00:00 2001 From: Victor Dauchy <26772554+vdauchy@users.noreply.github.com> Date: Mon, 8 Mar 2021 08:46:59 -0500 Subject: [PATCH 35/91] Add support to Eloquent Collection on Model::destroy() (#36497) --- src/Illuminate/Database/Eloquent/Model.php | 5 +++++ tests/Database/DatabaseEloquentModelTest.php | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 575148909050..62fcd058e258 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Support\Arrayable; use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\ConnectionResolverInterface as Resolver; +use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\Concerns\AsPivot; use Illuminate\Database\Eloquent\Relations\HasManyThrough; @@ -1061,6 +1062,10 @@ protected function insertAndSetId(Builder $query, $attributes) */ public static function destroy($ids) { + if ($ids instanceof EloquentCollection) { + $ids = $ids->modelKeys(); + } + if ($ids instanceof BaseCollection) { $ids = $ids->all(); } diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 42f3b6563927..b5049151adab 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -290,7 +290,16 @@ public function testDestroyMethodCallsQueryBuilderCorrectly() public function testDestroyMethodCallsQueryBuilderCorrectlyWithCollection() { - EloquentModelDestroyStub::destroy(new Collection([1, 2, 3])); + EloquentModelDestroyStub::destroy(new BaseCollection([1, 2, 3])); + } + + public function testDestroyMethodCallsQueryBuilderCorrectlyWithEloquentCollection() + { + EloquentModelDestroyStub::destroy(new Collection([ + new EloquentModelDestroyStub(['id' => 1]), + new EloquentModelDestroyStub(['id' => 2]), + new EloquentModelDestroyStub(['id' => 3]), + ])); } public function testDestroyMethodCallsQueryBuilderCorrectlyWithMultipleArgs() @@ -2383,6 +2392,10 @@ public function newQuery() class EloquentModelDestroyStub extends Model { + protected $fillable = [ + 'id', + ]; + public function newQuery() { $mock = m::mock(Builder::class); From 3f5af8d446564e685207c3296567d8b52dc2be51 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Mon, 8 Mar 2021 23:53:00 +1000 Subject: [PATCH 36/91] [6.x] Fix validator treating null as true for (required|exclude)_(if|unless) due to loose in_array() check (#36504) * Fix required_if treating true as null * Fix required_unless treating true as null * Fix exclude_if treating true as null * Fix exclude_unless treating true as null --- .../Concerns/ValidatesAttributes.php | 8 +-- tests/Validation/ValidationValidatorTest.php | 66 +++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 84e0964ba147..13fe1a648108 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1422,7 +1422,7 @@ public function validateRequiredIf($attribute, $value, $parameters) [$values, $other] = $this->prepareValuesAndOther($parameters); - if (in_array($other, $values)) { + if (in_array($other, $values, is_bool($other))) { return $this->validateRequired($attribute, $value); } @@ -1443,7 +1443,7 @@ public function validateExcludeIf($attribute, $value, $parameters) [$values, $other] = $this->prepareValuesAndOther($parameters); - return ! in_array($other, $values); + return ! in_array($other, $values, is_bool($other)); } /** @@ -1460,7 +1460,7 @@ public function validateExcludeUnless($attribute, $value, $parameters) [$values, $other] = $this->prepareValuesAndOther($parameters); - return in_array($other, $values); + return in_array($other, $values, is_bool($other)); } /** @@ -1515,7 +1515,7 @@ public function validateRequiredUnless($attribute, $value, $parameters) [$values, $other] = $this->prepareValuesAndOther($parameters); - if (! in_array($other, $values)) { + if (! in_array($other, $values, is_bool($other))) { return $this->validateRequired($attribute, $value); } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a176ce42a055..4ac71213c984 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1056,10 +1056,34 @@ public function testRequiredIf() $v = new Validator($trans, ['foo' => true], ['bar' => 'required_if:foo,false']); $this->assertTrue($v->passes()); + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true], ['bar' => 'required_if:foo,null']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 0], ['bar' => 'required_if:foo,0']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '0'], ['bar' => 'required_if:foo,0']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 1], ['bar' => 'required_if:foo,1']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '1'], ['bar' => 'required_if:foo,1']); + $this->assertTrue($v->fails()); + $trans = $this->getIlluminateArrayTranslator(); $v = new Validator($trans, ['foo' => true], ['bar' => 'required_if:foo,true']); $this->assertTrue($v->fails()); + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false], ['bar' => 'required_if:foo,false']); + $this->assertTrue($v->fails()); + // error message when passed multiple values (required_if:foo,bar,baz) $trans = $this->getIlluminateArrayTranslator(); $trans->addLines(['validation.required_if' => 'The :attribute field is required when :other is :value.'], 'en'); @@ -1098,6 +1122,26 @@ public function testRequiredUnless() $v = new Validator($trans, ['foo' => false], ['bar' => 'required_unless:foo,true']); $this->assertTrue($v->fails()); + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true], ['bar' => 'required_unless:foo,null']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '0'], ['bar' => 'required_unless:foo,0']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 0], ['bar' => 'required_unless:foo,0']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => '1'], ['bar' => 'required_unless:foo,1']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => 1], ['bar' => 'required_unless:foo,1']); + $this->assertTrue($v->passes()); + // error message when passed multiple values (required_unless:foo,bar,baz) $trans = $this->getIlluminateArrayTranslator(); $trans->addLines(['validation.required_unless' => 'The :attribute field is required unless :other is in :values.'], 'en'); @@ -5074,6 +5118,20 @@ public function providesPassingExcludeIfData() 'has_appointment' => false, ], ], + [ + [ + 'has_appointment' => ['nullable', 'bool'], + 'appointment_date' => ['exclude_if:has_appointment,null', 'required', 'date'], + ], + [ + 'has_appointment' => true, + 'appointment_date' => '2021-03-08', + ], + [ + 'has_appointment' => true, + 'appointment_date' => '2021-03-08', + ], + ], [ [ 'has_appointment' => ['required', 'bool'], @@ -5408,6 +5466,14 @@ public function testExcludeUnless() ); $this->assertTrue($validator->fails()); $this->assertSame(['mouse' => ['validation.required']], $validator->messages()->toArray()); + + $validator = new Validator( + $this->getIlluminateArrayTranslator(), + ['foo' => true, 'bar' => 'baz'], + ['foo' => 'nullable', 'bar' => 'exclude_unless:foo,null'] + ); + $this->assertTrue($validator->passes()); + $this->assertSame(['foo' => true], $validator->validated()); } public function testExcludeValuesAreReallyRemoved() From 92a1ce8a4dfaaaa9a7cf2335377cb2a2c037170f Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 8 Mar 2021 08:01:48 -0600 Subject: [PATCH 37/91] rename method --- .../Validation/Concerns/ValidatesAttributes.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 46fcc8441a17..bba094dd61f6 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1425,7 +1425,7 @@ public function validateRequiredIf($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'required_if'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); if (in_array($other, $values)) { return $this->validateRequired($attribute, $value); @@ -1446,7 +1446,7 @@ public function validateExcludeIf($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'exclude_if'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); return ! in_array($other, $values); } @@ -1463,7 +1463,7 @@ public function validateExcludeUnless($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'exclude_unless'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); return in_array($other, $values); } @@ -1493,7 +1493,7 @@ public function validateExcludeWithout($attribute, $value, $parameters) * @param array $parameters * @return array */ - protected function prepareValuesAndOther($parameters) + public function parseDependentRuleParameters($parameters) { $other = Arr::get($this->data, $parameters[0]); @@ -1552,7 +1552,7 @@ public function validateRequiredUnless($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'required_unless'); - [$values, $other] = $this->prepareValuesAndOther($parameters); + [$values, $other] = $this->parseDependentRuleParameters($parameters); if (! in_array($other, $values)) { return $this->validateRequired($attribute, $value); From a5d9b455af1dc91bf73aee95147029fd8b540f78 Mon Sep 17 00:00:00 2001 From: Dan Harrin Date: Mon, 8 Mar 2021 17:22:22 +0000 Subject: [PATCH 38/91] [8.x] Support value callback arguments (#36506) * test * feature: support value function callback arguments --- src/Illuminate/Collections/helpers.php | 4 ++-- tests/Support/SupportHelpersTest.php | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 6ae6dfe68a9b..67669e5ce1c6 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -179,8 +179,8 @@ function last($array) * @param mixed $value * @return mixed */ - function value($value) + function value($value, ...$args) { - return $value instanceof Closure ? $value() : $value; + return $value instanceof Closure ? $value(...$args) : $value; } } diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index af7de6b40f56..c286809fbe23 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -40,6 +40,9 @@ public function testValue() $this->assertSame('foo', value(function () { return 'foo'; })); + $this->assertSame('foo', value(function ($arg) { + return $arg; + }, 'foo')); } public function testObjectGet() From 84f10d707df3c9a0517b5561bcbc60ccc912aee0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 8 Mar 2021 11:49:04 -0600 Subject: [PATCH 39/91] formatting --- .../Fluent/{Assert.php => AssertableJson.php} | 5 +- .../Testing/Fluent/Concerns/Has.php | 6 +- src/Illuminate/Testing/TestResponse.php | 4 +- tests/Testing/Fluent/AssertTest.php | 118 +++++++++--------- tests/Testing/TestResponseTest.php | 5 +- 5 files changed, 71 insertions(+), 67 deletions(-) rename src/Illuminate/Testing/Fluent/{Assert.php => AssertableJson.php} (95%) diff --git a/src/Illuminate/Testing/Fluent/Assert.php b/src/Illuminate/Testing/Fluent/AssertableJson.php similarity index 95% rename from src/Illuminate/Testing/Fluent/Assert.php rename to src/Illuminate/Testing/Fluent/AssertableJson.php index bde937a0bf23..07104e114990 100644 --- a/src/Illuminate/Testing/Fluent/Assert.php +++ b/src/Illuminate/Testing/Fluent/AssertableJson.php @@ -10,7 +10,7 @@ use Illuminate\Testing\AssertableJsonString; use PHPUnit\Framework\Assert as PHPUnit; -class Assert implements Arrayable +class AssertableJson implements Arrayable { use Concerns\Has, Concerns\Matching, @@ -34,10 +34,11 @@ class Assert implements Arrayable private $path; /** - * Create a new Assert instance. + * Create a new fluent, assertable JSON data instance. * * @param array $props * @param string|null $path + * @return void */ protected function __construct(array $props, string $path = null) { diff --git a/src/Illuminate/Testing/Fluent/Concerns/Has.php b/src/Illuminate/Testing/Fluent/Concerns/Has.php index 19b9ad9915ca..dd91ee618790 100644 --- a/src/Illuminate/Testing/Fluent/Concerns/Has.php +++ b/src/Illuminate/Testing/Fluent/Concerns/Has.php @@ -45,9 +45,9 @@ public function has(string $key, $value = null, Closure $scope = null): self $this->interactsWith($key); - // When all three arguments are provided, this indicates a short-hand - // expression that combines both a `count`-assertion, followed by - // directly creating a `scope` on the first element. + // When all three arguments are provided this indicates a short-hand expression + // that combines both a `count`-assertion, followed by directly creating the + // `scope` on the first element. We can simply handle this correctly here. if (is_int($value) && ! is_null($scope)) { $prop = $this->prop($key); $path = $this->dotPath($key); diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index eb983f464a6d..edc85e2a13df 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -15,6 +15,8 @@ use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; use Illuminate\Testing\Fluent\Assert as FluentAssert; +use Illuminate\Testing\Fluent\AssertableJson; +use Illuminate\Testing\Fluent\FluentAssertableJson; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -519,7 +521,7 @@ public function assertJson($value, $strict = false) if (is_array($value)) { $json->assertSubset($value, $strict); } else { - $assert = FluentAssert::fromAssertableJsonString($json); + $assert = AssertableJson::fromAssertableJsonString($json); $value($assert); diff --git a/tests/Testing/Fluent/AssertTest.php b/tests/Testing/Fluent/AssertTest.php index f02e366f40a0..acafd07589d2 100644 --- a/tests/Testing/Fluent/AssertTest.php +++ b/tests/Testing/Fluent/AssertTest.php @@ -3,7 +3,7 @@ namespace Illuminate\Tests\Testing\Fluent; use Illuminate\Support\Collection; -use Illuminate\Testing\Fluent\Assert; +use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Tests\Testing\Stubs\ArrayableStubObject; use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; @@ -14,7 +14,7 @@ class AssertTest extends TestCase { public function testAssertHas() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'prop' => 'value', ]); @@ -23,7 +23,7 @@ public function testAssertHas() public function testAssertHasFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -35,7 +35,7 @@ public function testAssertHasFailsWhenPropMissing() public function testAssertHasNestedProp() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -46,7 +46,7 @@ public function testAssertHasNestedProp() public function testAssertHasFailsWhenNestedPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -60,7 +60,7 @@ public function testAssertHasFailsWhenNestedPropMissing() public function testAssertCountItemsInProp() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -72,7 +72,7 @@ public function testAssertCountItemsInProp() public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -87,7 +87,7 @@ public function testAssertCountFailsWhenAmountOfItemsDoesNotMatch() public function testAssertCountFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -102,7 +102,7 @@ public function testAssertCountFailsWhenPropMissing() public function testAssertHasFailsWhenSecondArgumentUnsupportedType() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'baz', ]); @@ -113,7 +113,7 @@ public function testAssertHasFailsWhenSecondArgumentUnsupportedType() public function testAssertMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => true, ], @@ -124,7 +124,7 @@ public function testAssertMissing() public function testAssertMissingFailsWhenPropExists() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'prop' => 'value', 'foo' => [ 'bar' => true, @@ -139,7 +139,7 @@ public function testAssertMissingFailsWhenPropExists() public function testAssertMissingAll() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'baz' => 'foo', ]); @@ -151,7 +151,7 @@ public function testAssertMissingAll() public function testAssertMissingAllFailsWhenAtLeastOnePropExists() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'baz' => 'foo', ]); @@ -166,7 +166,7 @@ public function testAssertMissingAllFailsWhenAtLeastOnePropExists() public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'baz' => 'foo', ]); @@ -180,7 +180,7 @@ public function testAssertMissingAllAcceptsMultipleArgumentsInsteadOfArray() public function testAssertWhereMatchesValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -189,7 +189,7 @@ public function testAssertWhereMatchesValue() public function testAssertWhereFailsWhenDoesNotMatchValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -201,7 +201,7 @@ public function testAssertWhereFailsWhenDoesNotMatchValue() public function testAssertWhereFailsWhenMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); @@ -213,7 +213,7 @@ public function testAssertWhereFailsWhenMissing() public function testAssertWhereFailsWhenMachingLoosely() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 1, ]); @@ -225,7 +225,7 @@ public function testAssertWhereFailsWhenMachingLoosely() public function testAssertWhereUsingClosure() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'baz', ]); @@ -236,7 +236,7 @@ public function testAssertWhereUsingClosure() public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'baz', ]); @@ -250,7 +250,7 @@ public function testAssertWhereFailsWhenDoesNotMatchValueUsingClosure() public function testAssertWhereClosureArrayValuesAreAutomaticallyCastedToCollections() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'foo', 'example' => 'value', @@ -268,7 +268,7 @@ public function testAssertWhereMatchesValueUsingArrayable() { $stub = ArrayableStubObject::make(['foo' => 'bar']); - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => $stub->toArray(), ]); @@ -277,7 +277,7 @@ public function testAssertWhereMatchesValueUsingArrayable() public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'foo', 'example' => 'value', @@ -293,7 +293,7 @@ public function testAssertWhereMatchesValueUsingArrayableWhenSortedDifferently() public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => ['id' => 1, 'name' => 'Example'], 'baz' => [ 'id' => 1, @@ -319,7 +319,7 @@ public function testAssertWhereFailsWhenDoesNotMatchValueUsingArrayable() public function testAssertNestedWhereMatchesValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -330,7 +330,7 @@ public function testAssertNestedWhereMatchesValue() public function testAssertNestedWhereFailsWhenDoesNotMatchValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'example' => [ 'nested' => 'nested-value', ], @@ -344,7 +344,7 @@ public function testAssertNestedWhereFailsWhenDoesNotMatchValue() public function testScope() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -352,7 +352,7 @@ public function testScope() ]); $called = false; - $assert->has('bar', function (Assert $assert) use (&$called) { + $assert->has('bar', function (AssertableJson $assert) use (&$called) { $called = true; $assert ->where('baz', 'example') @@ -364,7 +364,7 @@ public function testScope() public function testScopeFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -374,28 +374,28 @@ public function testScopeFailsWhenPropMissing() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Property [baz] does not exist.'); - $assert->has('baz', function (Assert $item) { + $assert->has('baz', function (AssertableJson $item) { $item->where('baz', 'example'); }); } public function testScopeFailsWhenPropSingleValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => 'value', ]); $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Property [bar] is not scopeable.'); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { // }); } public function testScopeShorthand() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ ['key' => 'first'], ['key' => 'second'], @@ -403,7 +403,7 @@ public function testScopeShorthand() ]); $called = false; - $assert->has('bar', 2, function (Assert $item) use (&$called) { + $assert->has('bar', 2, function (AssertableJson $item) use (&$called) { $item->where('key', 'first'); $called = true; }); @@ -413,7 +413,7 @@ public function testScopeShorthand() public function testScopeShorthandFailsWhenAssertingZeroItems() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ ['key' => 'first'], ['key' => 'second'], @@ -423,14 +423,14 @@ public function testScopeShorthandFailsWhenAssertingZeroItems() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Cannot scope directly onto the first entry of property [bar] when asserting that it has a size of 0.'); - $assert->has('bar', 0, function (Assert $item) { + $assert->has('bar', 0, function (AssertableJson $item) { $item->where('key', 'first'); }); } public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ ['key' => 'first'], ['key' => 'second'], @@ -440,14 +440,14 @@ public function testScopeShorthandFailsWhenAmountOfItemsDoesNotMatch() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Property [bar] does not have the expected size.'); - $assert->has('bar', 1, function (Assert $item) { + $assert->has('bar', 1, function (AssertableJson $item) { $item->where('key', 'first'); }); } public function testFailsWhenNotInteractingWithAllPropsInScope() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -457,28 +457,28 @@ public function testFailsWhenNotInteractingWithAllPropsInScope() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Unexpected properties were found in scope [bar].'); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { $item->where('baz', 'example'); }); } public function testDisableInteractionCheckForCurrentScope() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', ], ]); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { $item->etc(); }); } public function testCannotDisableInteractionCheckForDifferentScopes() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => [ 'foo' => 'bar', @@ -491,10 +491,10 @@ public function testCannotDisableInteractionCheckForDifferentScopes() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Unexpected properties were found in scope [bar.baz].'); - $assert->has('bar', function (Assert $item) { + $assert->has('bar', function (AssertableJson $item) { $item ->etc() - ->has('baz', function (Assert $item) { + ->has('baz', function (AssertableJson $item) { // }); }); @@ -502,7 +502,7 @@ public function testCannotDisableInteractionCheckForDifferentScopes() public function testTopLevelPropInteractionDisabledByDefault() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => 'bar', 'bar' => 'baz', ]); @@ -512,7 +512,7 @@ public function testTopLevelPropInteractionDisabledByDefault() public function testTopLevelInteractionEnabledWhenInteractedFlagSet() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => 'bar', 'bar' => 'baz', ]); @@ -527,7 +527,7 @@ public function testTopLevelInteractionEnabledWhenInteractedFlagSet() public function testAssertWhereAllMatchesValues() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -546,7 +546,7 @@ public function testAssertWhereAllMatchesValues() public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => 'bar', 'baz' => 'example', ]); @@ -564,7 +564,7 @@ public function testAssertWhereAllFailsWhenAtLeastOnePropDoesNotMatchValue() public function testAssertHasAll() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -581,7 +581,7 @@ public function testAssertHasAll() public function testAssertHasAllFailsWhenAtLeastOnePropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -601,7 +601,7 @@ public function testAssertHasAllFailsWhenAtLeastOnePropMissing() public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'foo' => [ 'bar' => 'value', 'example' => ['hello' => 'world'], @@ -619,7 +619,7 @@ public function testAssertHasAllAcceptsMultipleArgumentsInsteadOfArray() public function testAssertCountMultipleProps() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'key' => 'value', 'prop' => 'example', @@ -637,7 +637,7 @@ public function testAssertCountMultipleProps() public function testAssertCountMultiplePropsFailsWhenPropMissing() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'key' => 'value', 'prop' => 'example', @@ -655,20 +655,20 @@ public function testAssertCountMultiplePropsFailsWhenPropMissing() public function testMacroable() { - Assert::macro('myCustomMacro', function () { + AssertableJson::macro('myCustomMacro', function () { throw new RuntimeException('My Custom Macro was called!'); }); $this->expectException(RuntimeException::class); $this->expectExceptionMessage('My Custom Macro was called!'); - $assert = Assert::fromArray(['foo' => 'bar']); + $assert = AssertableJson::fromArray(['foo' => 'bar']); $assert->myCustomMacro(); } public function testTappable() { - $assert = Assert::fromArray([ + $assert = AssertableJson::fromArray([ 'bar' => [ 'baz' => 'example', 'prop' => 'value', @@ -676,9 +676,9 @@ public function testTappable() ]); $called = false; - $assert->has('bar', function (Assert $assert) use (&$called) { + $assert->has('bar', function (AssertableJson $assert) use (&$called) { $assert->etc(); - $assert->tap(function (Assert $assert) use (&$called) { + $assert->tap(function (AssertableJson $assert) use (&$called) { $called = true; }); }); diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 055519925cb9..1ac9ebba7373 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -10,6 +10,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; use Illuminate\Testing\Fluent\Assert; +use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; use JsonSerializable; use Mockery as m; @@ -582,7 +583,7 @@ public function testAssertJsonWithFluent() { $response = TestResponse::fromBaseResponse(new Response(new JsonSerializableSingleResourceStub)); - $response->assertJson(function (Assert $json) { + $response->assertJson(function (AssertableJson $json) { $json->where('0.foo', 'foo 0'); }); } @@ -594,7 +595,7 @@ public function testAssertJsonWithFluentStrict() $this->expectException(AssertionFailedError::class); $this->expectExceptionMessage('Unexpected properties were found on the root level.'); - $response->assertJson(function (Assert $json) { + $response->assertJson(function (AssertableJson $json) { $json->where('0.foo', 'foo 0'); }, true); } From 0798479b95fd241c829b0e36a1d6f8573a586738 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 8 Mar 2021 11:52:35 -0600 Subject: [PATCH 40/91] Apply fixes from StyleCI (#36508) --- src/Illuminate/Testing/TestResponse.php | 2 -- tests/Testing/TestResponseTest.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index edc85e2a13df..aeee4fe59c7e 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -14,9 +14,7 @@ use Illuminate\Support\Traits\Tappable; use Illuminate\Testing\Assert as PHPUnit; use Illuminate\Testing\Constraints\SeeInOrder; -use Illuminate\Testing\Fluent\Assert as FluentAssert; use Illuminate\Testing\Fluent\AssertableJson; -use Illuminate\Testing\Fluent\FluentAssertableJson; use LogicException; use Symfony\Component\HttpFoundation\StreamedResponse; diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 1ac9ebba7373..b6a1e5531a54 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -9,7 +9,6 @@ use Illuminate\Encryption\Encrypter; use Illuminate\Filesystem\Filesystem; use Illuminate\Http\Response; -use Illuminate\Testing\Fluent\Assert; use Illuminate\Testing\Fluent\AssertableJson; use Illuminate\Testing\TestResponse; use JsonSerializable; From 35071ab6ec3c906c0f26d153e743770e8df86079 Mon Sep 17 00:00:00 2001 From: Paras Malhotra Date: Tue, 9 Mar 2021 12:50:29 +0530 Subject: [PATCH 41/91] Add ThrottlesExceptionsWithRedis job middleware --- .../ThrottlesExceptionsWithRedis.php | 62 +++++++ .../Redis/Limiters/DurationLimiter.php | 54 ++++++ .../ThrottlesExceptionsWithRedisTest.php | 167 ++++++++++++++++++ 3 files changed, 283 insertions(+) create mode 100644 src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php create mode 100644 tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php new file mode 100644 index 000000000000..38790e353e2d --- /dev/null +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -0,0 +1,62 @@ +redis = Container::getInstance()->make(Redis::class); + + $this->limiter = new DurationLimiter( + $this->redis, $this->getKey($job), $this->maxAttempts, $this->decayMinutes * 60 + ); + + if ($this->limiter->tooManyAttempts()) { + return $job->release($this->limiter->decaysAt - $this->currentTime()); + } + + try { + $next($job); + + $this->limiter->clear(); + } catch (Throwable $throwable) { + if ($this->whenCallback && ! call_user_func($this->whenCallback, $throwable)) { + throw $throwable; + } + + $this->limiter->acquire(); + + return $job->release($this->retryAfterMinutes * 60); + } + } +} diff --git a/src/Illuminate/Redis/Limiters/DurationLimiter.php b/src/Illuminate/Redis/Limiters/DurationLimiter.php index 9aa594fb41f4..d4e503ada980 100644 --- a/src/Illuminate/Redis/Limiters/DurationLimiter.php +++ b/src/Illuminate/Redis/Limiters/DurationLimiter.php @@ -111,6 +111,30 @@ public function acquire() return (bool) $results[0]; } + /** + * Determine if the key has been "accessed" too many times. + * + * @return bool + */ + public function tooManyAttempts() + { + [$this->decaysAt, $this->remaining] = $this->redis->eval( + $this->tooManyAttemptsScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks + ); + + return $this->remaining <= 0; + } + + /** + * Clear the limiter. + * + * @return void + */ + public function clear() + { + $this->redis->del($this->name); + } + /** * Get the Lua script for acquiring a lock. * @@ -143,6 +167,36 @@ protected function luaScript() end return {reset(), ARGV[2] + ARGV[3], ARGV[4] - 1} +LUA; + } + + /** + * Get the Lua script to determine if the key has been "accessed" too many times. + * + * KEYS[1] - The limiter name + * ARGV[1] - Current time in microseconds + * ARGV[2] - Current time in seconds + * ARGV[3] - Duration of the bucket + * ARGV[4] - Allowed number of tasks + * + * @return string + */ + protected function tooManyAttemptsScript() + { + return <<<'LUA' + +if redis.call('EXISTS', KEYS[1]) == 0 then + return {0, ARGV[2] + ARGV[3]} +end + +if ARGV[1] >= redis.call('HGET', KEYS[1], 'start') and ARGV[1] <= redis.call('HGET', KEYS[1], 'end') then + return { + redis.call('HGET', KEYS[1], 'end'), + ARGV[4] - redis.call('HGET', KEYS[1], 'count') + } +end + +return {0, ARGV[2] + ARGV[3]} LUA; } } diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php new file mode 100644 index 000000000000..35d9255eb751 --- /dev/null +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -0,0 +1,167 @@ +setUpRedis(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + $this->tearDownRedis(); + + m::close(); + } + + public function testCircuitIsOpenedForJobErrors() + { + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); + $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); + } + + public function testCircuitStaysClosedForSuccessfulJobs() + { + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key = Str::random()); + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); + } + + public function testCircuitResetsAfterSuccess() + { + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); + $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); + $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); + $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); + } + + protected function assertJobWasReleasedImmediately($class, $key) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->with(0)->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class($key)), + ]); + + $this->assertTrue($class::$handled); + } + + protected function assertJobWasReleasedWithDelay($class, $key) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->withArgs(function ($delay) { + return $delay >= 600; + })->once(); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class($key)), + ]); + + $this->assertFalse($class::$handled); + } + + protected function assertJobRanSuccessfully($class, $key) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('isReleased')->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + + $instance->call($job, [ + 'command' => serialize($command = new $class($key)), + ]); + + $this->assertTrue($class::$handled); + } +} + +class CircuitBreakerWithRedisTestJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function __construct($key) + { + $this->key = $key; + } + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [new ThrottlesExceptionsWithRedis(2, 10, 0, $this->key)]; + } +} + +class CircuitBreakerWithRedisSuccessfulJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function __construct($key) + { + $this->key = $key; + } + + public function handle() + { + static::$handled = true; + } + + public function middleware() + { + return [new ThrottlesExceptionsWithRedis(2, 10, 0, $this->key)]; + } +} From 55a14074115486b3f4a6e7f4b25dedd9981c5d6e Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Tue, 9 Mar 2021 13:43:43 +0200 Subject: [PATCH 42/91] ability to slow the workers down --- src/Illuminate/Queue/Console/WorkCommand.php | 4 +++- src/Illuminate/Queue/Worker.php | 2 ++ src/Illuminate/Queue/WorkerOptions.php | 11 ++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index ff092197f53a..da9176be4063 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -33,6 +33,7 @@ class WorkCommand extends Command {--force : Force the worker to run even in maintenance mode} {--memory=128 : The memory limit in megabytes} {--sleep=3 : Number of seconds to sleep when no job is available} + {--rest=0 : Number of seconds to rest between jobs} {--timeout=60 : The number of seconds a child process can run} {--tries=1 : Number of times to attempt a job before logging it failed}'; @@ -134,7 +135,8 @@ protected function gatherWorkerOptions() $this->option('force'), $this->option('stop-when-empty'), $this->option('max-jobs'), - $this->option('max-time') + $this->option('max-time'), + $this->option('rest') ); } diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index f2ba7b1a01ad..4fcacb26d009 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -156,6 +156,8 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $jobsProcessed++; $this->runJob($job, $connectionName, $options); + + $this->sleep($options->rest); } else { $this->sleep($options->sleep); } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 766f4676029a..7e99457e70d1 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -74,6 +74,13 @@ class WorkerOptions */ public $maxTime; + /** + * The number of seconds to rest between jobs. + * + * @var int + */ + public $rest; + /** * Create a new worker options instance. * @@ -87,10 +94,11 @@ class WorkerOptions * @param bool $stopWhenEmpty * @param int $maxJobs * @param int $maxTime + * @param int $rest * @return void */ public function __construct($name = 'default', $backoff = 0, $memory = 128, $timeout = 60, $sleep = 3, $maxTries = 1, - $force = false, $stopWhenEmpty = false, $maxJobs = 0, $maxTime = 0) + $force = false, $stopWhenEmpty = false, $maxJobs = 0, $maxTime = 0, $rest = 0) { $this->name = $name; $this->backoff = $backoff; @@ -102,5 +110,6 @@ public function __construct($name = 'default', $backoff = 0, $memory = 128, $tim $this->stopWhenEmpty = $stopWhenEmpty; $this->maxJobs = $maxJobs; $this->maxTime = $maxTime; + $this->rest = $rest; } } From d9a33b1812c4c0269fd652fc0e534c140afc489e Mon Sep 17 00:00:00 2001 From: Rodrigo Pedra Brum Date: Tue, 9 Mar 2021 10:28:43 -0300 Subject: [PATCH 43/91] [8.x] Allow to override discover events base path (#36515) * Allow to override discover events base path * Update EventServiceProvider.php Co-authored-by: Taylor Otwell --- .../Support/Providers/EventServiceProvider.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index 0573563cf5ac..70ea3086efe9 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -119,7 +119,7 @@ public function discoverEvents() ->reduce(function ($discovered, $directory) { return array_merge_recursive( $discovered, - DiscoverEvents::within($directory, base_path()) + DiscoverEvents::within($directory, $this->eventDiscoveryBasePath()) ); }, []); } @@ -135,4 +135,14 @@ protected function discoverEventsWithin() $this->app->path('Listeners'), ]; } + + /** + * Get the base path to be used during event discovery. + * + * @return string + */ + protected function eventDiscoveryBasePath() + { + return base_path(); + } } From a4678ce95cb9d4f10ec789843cee78df4acceb35 Mon Sep 17 00:00:00 2001 From: Jess Archer Date: Tue, 9 Mar 2021 23:29:22 +1000 Subject: [PATCH 44/91] Add prohibited_if and prohibited_unless validation rules (#36516) --- .../Concerns/ReplacesAttributes.php | 40 +++++++++++ .../Concerns/ValidatesAttributes.php | 42 +++++++++++ src/Illuminate/Validation/Validator.php | 2 + tests/Validation/ValidationValidatorTest.php | 72 +++++++++++++++++++ 4 files changed, 156 insertions(+) diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index d645dbd6d5a6..d4a47af146c4 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -374,6 +374,46 @@ protected function replaceRequiredUnless($message, $attribute, $rule, $parameter return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); } + /** + * Replace all place-holders for the prohibited_if rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceProhibitedIf($message, $attribute, $rule, $parameters) + { + $parameters[1] = $this->getDisplayableValue($parameters[0], Arr::get($this->data, $parameters[0])); + + $parameters[0] = $this->getDisplayableAttribute($parameters[0]); + + return str_replace([':other', ':value'], $parameters, $message); + } + + /** + * Replace all place-holders for the prohibited_unless rule. + * + * @param string $message + * @param string $attribute + * @param string $rule + * @param array $parameters + * @return string + */ + protected function replaceProhibitedUnless($message, $attribute, $rule, $parameters) + { + $other = $this->getDisplayableAttribute($parameters[0]); + + $values = []; + + foreach (array_slice($parameters, 1) as $value) { + $values[] = $this->getDisplayableValue($parameters[0], $value); + } + + return str_replace([':other', ':values'], [$other, implode(', ', $values)], $message); + } + /** * Replace all place-holders for the same rule. * diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index bba094dd61f6..2f1dcf6a2157 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1434,6 +1434,48 @@ public function validateRequiredIf($attribute, $value, $parameters) return true; } + /** + * Validate that an attribute does not exist when another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibitedIf($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'prohibited_if'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (in_array($other, $values, is_bool($other))) { + return ! $this->validateRequired($attribute, $value); + } + + return true; + } + + /** + * Validate that an attribute does not exist unless another attribute has a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateProhibitedUnless($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'prohibited_unless'); + + [$values, $other] = $this->parseDependentRuleParameters($parameters); + + if (! in_array($other, $values, is_bool($other))) { + return ! $this->validateRequired($attribute, $value); + } + + return true; + } + /** * Indicate that an attribute should be excluded when another attribute has a given value. * diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 48d9946386d6..0aa44d6c197e 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -227,6 +227,8 @@ class Validator implements ValidatorContract 'RequiredWithAll', 'RequiredWithout', 'RequiredWithoutAll', + 'ProhibitedIf', + 'ProhibitedUnless', 'Same', 'Unique', ]; diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index a60b93b9abf4..7f21e4ba1688 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1146,6 +1146,78 @@ public function testRequiredUnless() $this->assertSame('The last field is required unless first is in taylor, sven.', $v->messages()->first('last')); } + public function testProhibitedIf() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_if:first,taylor']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor'], ['last' => 'prohibited_if:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true, 'bar' => 'baz'], ['bar' => 'prohibited_if:foo,false']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => true, 'bar' => 'baz'], ['bar' => 'prohibited_if:foo,true']); + $this->assertTrue($v->fails()); + + // error message when passed multiple values (prohibited_if:foo,bar,baz) + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.prohibited_if' => 'The :attribute field is prohibited when :other is :value.'], 'en'); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_if:first,taylor,jess']); + $this->assertFalse($v->passes()); + $this->assertSame('The last field is prohibited when first is jess.', $v->messages()->first('last')); + } + + public function testProhibitedUnless() + { + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->fails()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess'], ['last' => 'prohibited_unless:first,taylor']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'taylor', 'last' => 'otwell'], ['last' => 'prohibited_unless:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['first' => 'jess', 'last' => 'archer'], ['last' => 'prohibited_unless:first,taylor,jess']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false, 'bar' => 'baz'], ['bar' => 'prohibited_unless:foo,false']); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => false, 'bar' => 'baz'], ['bar' => 'prohibited_unless:foo,true']); + $this->assertTrue($v->fails()); + + // error message when passed multiple values (prohibited_unless:foo,bar,baz) + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.'], 'en'); + $v = new Validator($trans, ['first' => 'tim', 'last' => 'macdonald'], ['last' => 'prohibitedUnless:first,taylor,jess']); + $this->assertFalse($v->passes()); + $this->assertSame('The last field is prohibited unless first is in taylor, jess.', $v->messages()->first('last')); + } + public function testFailedFileUploads() { $trans = $this->getIlluminateArrayTranslator(); From 37e48ba864e2f463517429d41cefd94e88136c1c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 08:03:10 -0600 Subject: [PATCH 45/91] formatting and fix key usage --- .../Queue/Middleware/ThrottlesExceptions.php | 52 ++++++++++++++----- .../Queue/ThrottlesExceptionsTest.php | 7 ++- 2 files changed, 43 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index edb3dd164d33..3fff4e914b55 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -8,6 +8,13 @@ class ThrottlesExceptions { + /** + * The developer specified key that the rate limiter should use. + * + * @var string + */ + protected $key; + /** * The maximum number of attempts allowed before rate limiting applies. * @@ -27,14 +34,7 @@ class ThrottlesExceptions * * @var int */ - protected $retryAfterMinutes; - - /** - * The rate limiter key. - * - * @var string - */ - protected $key; + protected $retryAfterMinutes = 0; /** * The callback that determines if rate limiting should apply. @@ -48,7 +48,7 @@ class ThrottlesExceptions * * @var string */ - protected $prefix = 'circuit_breaker:'; + protected $prefix = 'laravel_throttles_exceptions:'; /** * The rate limiter instance. @@ -62,15 +62,13 @@ class ThrottlesExceptions * * @param int $maxAttempts * @param int $decayMinutes - * @param int $retryAfterMinutes * @param string $key + * @return void */ - public function __construct($maxAttempts = 10, $decayMinutes = 10, $retryAfterMinutes = 0, string $key = '') + public function __construct($maxAttempts = 10, $decayMinutes = 10) { $this->maxAttempts = $maxAttempts; $this->decayMinutes = $decayMinutes; - $this->retryAfterMinutes = $retryAfterMinutes; - $this->key = $key; } /** @@ -129,6 +127,19 @@ public function withPrefix(string $prefix) return $this; } + /** + * Specify the number of seconds a job should be delayed when it is released (before it has reached its max exceptions). + * + * @param int $backoff + * @return $this + */ + public function backoff($backoff) + { + $this->retryAfterMinutes = $backoff; + + return $this; + } + /** * Get the cache key associated for the rate limiter. * @@ -137,7 +148,20 @@ public function withPrefix(string $prefix) */ protected function getKey($job) { - return $this->prefix.md5(empty($this->key) ? get_class($job) : $this->key); + return $this->key ? $this->prefix.$this->key : $this->prefix.$job->job->uuid(); + } + + /** + * Set the value that the rate limiter should be keyed by. + * + * @param string $key + * @return $this + */ + public function by($key) + { + $this->key = $key; + + return $this; } /** diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index b51190e41654..002acc30c661 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -58,6 +58,7 @@ protected function assertJobWasReleasedImmediately($class) $job->shouldReceive('release')->with(0)->once(); $job->shouldReceive('isReleased')->andReturn(true); $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); $instance->call($job, [ 'command' => serialize($command = new $class), @@ -79,6 +80,7 @@ protected function assertJobWasReleasedWithDelay($class) })->once(); $job->shouldReceive('isReleased')->andReturn(true); $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); $instance->call($job, [ 'command' => serialize($command = new $class), @@ -98,6 +100,7 @@ protected function assertJobRanSuccessfully($class) $job->shouldReceive('isReleased')->andReturn(false); $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(false); $job->shouldReceive('delete')->once(); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); $instance->call($job, [ 'command' => serialize($command = new $class), @@ -122,7 +125,7 @@ public function handle() public function middleware() { - return [new ThrottlesExceptions(2, 10, 0, 'test')]; + return [(new ThrottlesExceptions(2, 10))->by('test')]; } } @@ -139,6 +142,6 @@ public function handle() public function middleware() { - return [new ThrottlesExceptions(2, 10, 0, 'test')]; + return [(new ThrottlesExceptions(2, 10))->by('test')]; } } From 0443f1c42c20f6a30d0d81050bc43e94c9c51145 Mon Sep 17 00:00:00 2001 From: Jason McCreary Date: Tue, 9 Mar 2021 09:06:15 -0500 Subject: [PATCH 46/91] Add class argument (#36513) --- .../Database/Console/Seeds/SeedCommand.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Console/Seeds/SeedCommand.php b/src/Illuminate/Database/Console/Seeds/SeedCommand.php index ccca6fd5eeda..058e545c234f 100644 --- a/src/Illuminate/Database/Console/Seeds/SeedCommand.php +++ b/src/Illuminate/Database/Console/Seeds/SeedCommand.php @@ -6,6 +6,7 @@ use Illuminate\Console\ConfirmableTrait; use Illuminate\Database\ConnectionResolverInterface as Resolver; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; class SeedCommand extends Command @@ -81,7 +82,7 @@ public function handle() */ protected function getSeeder() { - $class = $this->input->getOption('class'); + $class = $this->input->getArgument('class') ?? $this->input->getOption('class'); if (strpos($class, '\\') === false) { $class = 'Database\\Seeders\\'.$class; @@ -109,6 +110,18 @@ protected function getDatabase() return $database ?: $this->laravel['config']['database.default']; } + /** + * Get the console command arguments. + * + * @return array + */ + protected function getArguments() + { + return [ + ['class', InputArgument::OPTIONAL, 'The class name of the root seeder', null], + ]; + } + /** * Get the console command options. * From c6ea49c80a2ac93aebb8fdf2360161b73cec26af Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 08:15:40 -0600 Subject: [PATCH 47/91] formatting --- src/Illuminate/Queue/Worker.php | 4 +++- src/Illuminate/Queue/WorkerOptions.php | 16 ++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Queue/Worker.php b/src/Illuminate/Queue/Worker.php index 4fcacb26d009..4229fe701691 100644 --- a/src/Illuminate/Queue/Worker.php +++ b/src/Illuminate/Queue/Worker.php @@ -157,7 +157,9 @@ public function daemon($connectionName, $queue, WorkerOptions $options) $this->runJob($job, $connectionName, $options); - $this->sleep($options->rest); + if ($options->rest > 0) { + $this->sleep($options->rest); + } } else { $this->sleep($options->sleep); } diff --git a/src/Illuminate/Queue/WorkerOptions.php b/src/Illuminate/Queue/WorkerOptions.php index 7e99457e70d1..7b8d8dfeea3b 100644 --- a/src/Illuminate/Queue/WorkerOptions.php +++ b/src/Illuminate/Queue/WorkerOptions.php @@ -39,6 +39,13 @@ class WorkerOptions */ public $sleep; + /** + * The number of seconds to rest between jobs. + * + * @var int + */ + public $rest; + /** * The maximum amount of times a job may be attempted. * @@ -74,13 +81,6 @@ class WorkerOptions */ public $maxTime; - /** - * The number of seconds to rest between jobs. - * - * @var int - */ - public $rest; - /** * Create a new worker options instance. * @@ -103,6 +103,7 @@ public function __construct($name = 'default', $backoff = 0, $memory = 128, $tim $this->name = $name; $this->backoff = $backoff; $this->sleep = $sleep; + $this->rest = $rest; $this->force = $force; $this->memory = $memory; $this->timeout = $timeout; @@ -110,6 +111,5 @@ public function __construct($name = 'default', $backoff = 0, $memory = 128, $tim $this->stopWhenEmpty = $stopWhenEmpty; $this->maxJobs = $maxJobs; $this->maxTime = $maxTime; - $this->rest = $rest; } } From bd490fea91f39c22aa281879882b0f1832f51eae Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 08:28:59 -0600 Subject: [PATCH 48/91] formatting --- src/Illuminate/Redis/Limiters/DurationLimiter.php | 4 ++-- tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Redis/Limiters/DurationLimiter.php b/src/Illuminate/Redis/Limiters/DurationLimiter.php index d4e503ada980..56dbba505435 100644 --- a/src/Illuminate/Redis/Limiters/DurationLimiter.php +++ b/src/Illuminate/Redis/Limiters/DurationLimiter.php @@ -119,7 +119,7 @@ public function acquire() public function tooManyAttempts() { [$this->decaysAt, $this->remaining] = $this->redis->eval( - $this->tooManyAttemptsScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks + $this->tooManyAttemptsLuaScript(), 1, $this->name, microtime(true), time(), $this->decay, $this->maxLocks ); return $this->remaining <= 0; @@ -181,7 +181,7 @@ protected function luaScript() * * @return string */ - protected function tooManyAttemptsScript() + protected function tooManyAttemptsLuaScript() { return <<<'LUA' diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php index 35d9255eb751..c789d5d523f6 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -140,7 +140,7 @@ public function handle() public function middleware() { - return [new ThrottlesExceptionsWithRedis(2, 10, 0, $this->key)]; + return [(new ThrottlesExceptionsWithRedis(2, 10))->by($this->key)]; } } @@ -162,6 +162,6 @@ public function handle() public function middleware() { - return [new ThrottlesExceptionsWithRedis(2, 10, 0, $this->key)]; + return [(new ThrottlesExceptionsWithRedis(2, 10))->by($this->key)]; } } From 784f8ff1469a0bb288600d2bb9e02541c8e38a8d Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 09:01:58 -0600 Subject: [PATCH 49/91] patch --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 264e0deba796..1f46747dbfbd 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -31,7 +31,7 @@ class Application extends Container implements ApplicationContract, HttpKernelIn * * @var string */ - const VERSION = '6.20.17'; + const VERSION = '6.20.18'; /** * The base path for the Laravel installation. From b8a70e9a3685871ed46a24fc03c0267849d2d7c8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 09:36:10 -0600 Subject: [PATCH 50/91] adjust behavior --- .../Queue/Middleware/ThrottlesExceptions.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 3fff4e914b55..6364b1d6381a 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -15,6 +15,13 @@ class ThrottlesExceptions */ protected $key; + /** + * Indicates whether the throttle key should use the job's UUID. + * + * @var bool + */ + protected $byJob = false; + /** * The maximum number of attempts allowed before rate limiting applies. * @@ -148,7 +155,13 @@ public function backoff($backoff) */ protected function getKey($job) { - return $this->key ? $this->prefix.$this->key : $this->prefix.$job->job->uuid(); + if ($this->key) { + return $this->prefix.$this->key; + } elseif ($this->byJob) { + return $this->prefix.$job->job->uuid(); + } + + return $this->prefix.md5(get_class($job)); } /** @@ -164,6 +177,18 @@ public function by($key) return $this; } + /** + * Indicate that the throttle key should use the job's UUID. + * + * @return $this + */ + public function byJob() + { + $this->byJob = true; + + return $this; + } + /** * Get the number of seconds that should elapse before the job is retried. * From 7c37b64f8153c16b6406f5c28cf37828ebbe8846 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 9 Mar 2021 09:37:45 -0600 Subject: [PATCH 51/91] patch --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index a62cd53588ac..9542156e2529 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -33,7 +33,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '8.32.0'; + const VERSION = '8.32.1'; /** * The base path for the Laravel installation. From 6eadb692abd726128454cdcb3c634c64933582e0 Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Tue, 9 Mar 2021 20:03:54 +0200 Subject: [PATCH 52/91] [8.x] update changelog --- CHANGELOG-8.x.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index 28603d11124f..8dfc5721f3c9 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,19 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.30.1...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.31.0...8.x) + + +## [v8.31.0 (2021-03-04)](https://github.com/laravel/framework/compare/v8.30.1...v8.31.0) + +### Added +- Added new `VendorTagPublished` event ([#36458](https://github.com/laravel/framework/pull/36458)) +- Added new `Stringable::test()` method ([#36462](https://github.com/laravel/framework/pull/36462)) + +### Reverted +- Reverted [Fixed `formatWheres()` methods in `DatabaseRule`](https://github.com/laravel/framework/pull/36441) ([#36452](https://github.com/laravel/framework/pull/36452)) + +### Changed +- Make user policy command fix (Windows) ([#36464](https://github.com/laravel/framework/pull/36464)) ## [v8.30.1 (2021-03-03)](https://github.com/laravel/framework/compare/v8.30.0...v8.30.1) From bbb6b97c48a22a13fc1c8cdae157ae6c42c9d07f Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Tue, 9 Mar 2021 20:09:54 +0200 Subject: [PATCH 53/91] [6.x] update changelog --- CHANGELOG-6.x.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index 860012f0bd0a..b041da96c128 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,15 @@ # Release Notes for 6.x -## [Unreleased](https://github.com/laravel/framework/compare/v6.20.17...6.x) +## [Unreleased](https://github.com/laravel/framework/compare/v6.20.18...6.x) + + +## [v6.20.18 (2021-03-09)](https://github.com/laravel/framework/compare/v6.20.17...v6.20.18) + +### Fixed +- Fix validator treating null as true for (required|exclude)_(if|unless) due to loose `in_array()` check ([#36504](https://github.com/laravel/framework/pull/36504)) + +### Changed +- Delete existing links that are broken in `Illuminate\Foundation\Console\StorageLinkCommand` ([#36470](https://github.com/laravel/framework/pull/36470)) ## [v6.20.17 (2021-03-02)](https://github.com/laravel/framework/compare/v6.20.16...v6.20.17) From e1ff15be92072d45448e607e90651934b4d22362 Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Tue, 9 Mar 2021 20:32:59 +0200 Subject: [PATCH 54/91] [8.x] update changelog --- CHANGELOG-8.x.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index 8dfc5721f3c9..b70dcd9050d3 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,27 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.31.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.32.0...8.x) + + +## [v8.32.0 (2021-03-09)](https://github.com/laravel/framework/compare/v8.31.0...v8.32.0) + +Added +- Phpredis lock serialization and compression support ([#36412](https://github.com/laravel/framework/pull/36412), [10f1a93](https://github.com/laravel/framework/commit/10f1a935205340ba8954e7075c1d9b67943db27d)) +- Added Fluent JSON Assertions ([#36454](https://github.com/laravel/framework/pull/36454)) +- Added methods to dump requests of the Laravel HTTP client ([#36466](https://github.com/laravel/framework/pull/36466)) +- Added `ThrottlesExceptions` and `ThrottlesExceptionsWithRedis` job middlewares for unstable services ([#36473](https://github.com/laravel/framework/pull/36473), [21fee76](https://github.com/laravel/framework/commit/21fee7649e1b48a7701b8ba860218741c2c3bcef), [36518](https://github.com/laravel/framework/pull/36518), [37e48ba](https://github.com/laravel/framework/commit/37e48ba864e2f463517429d41cefd94e88136c1c)) +- Added support to Eloquent Collection on `Model::destroy()` ([#36497](https://github.com/laravel/framework/pull/36497)) +- Added `rest` option to `php artisan queue:work` command ([#36521](https://github.com/laravel/framework/pull/36521), [c6ea49c](https://github.com/laravel/framework/commit/c6ea49c80a2ac93aebb8fdf2360161b73cec26af)) +- Added `prohibited_if` and `prohibited_unless` validation rules ([#36516](https://github.com/laravel/framework/pull/36516)) +- Added class `argument` to `Illuminate\Database\Console\Seeds\SeedCommand` ([#36513](https://github.com/laravel/framework/pull/36513)) + +### Fixed +- Fix validator treating null as true for (required|exclude)_(if|unless) due to loose `in_array()` check ([#36504](https://github.com/laravel/framework/pull/36504)) + +### Changed +- Delete existing links that are broken in `Illuminate\Foundation\Console\StorageLinkCommand` ([#36470](https://github.com/laravel/framework/pull/36470)) +- Use user provided url in AwsTemporaryUrl method ([#36480](https://github.com/laravel/framework/pull/36480)) +- Allow to override discover events base path ([#36515](https://github.com/laravel/framework/pull/36515)) ## [v8.31.0 (2021-03-04)](https://github.com/laravel/framework/compare/v8.30.1...v8.31.0) From 12b3d57c6f150e1b8def4535c347638113c22664 Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Tue, 9 Mar 2021 20:35:12 +0200 Subject: [PATCH 55/91] [8.x] update changelog --- CHANGELOG-8.x.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index b70dcd9050d3..e5ca8f3668f8 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,12 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.32.0...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.32.1...8.x) + + +## [v8.32.1 (2021-03-09)](https://github.com/laravel/framework/compare/v8.32.0...v8.32.1) + +### Changed +- Changed `Illuminate\Queue\Middleware\ThrottlesExceptions` ([b8a70e9](https://github.com/laravel/framework/commit/b8a70e9a3685871ed46a24fc03c0267849d2d7c8)) ## [v8.32.0 (2021-03-09)](https://github.com/laravel/framework/compare/v8.31.0...v8.32.0) From b7ceae1761de6839e37b70a65177e702b1b4a38f Mon Sep 17 00:00:00 2001 From: Tom Irons Date: Wed, 10 Mar 2021 10:18:48 -0500 Subject: [PATCH 56/91] [8.x] Allow nullable columns for AsArrayObject/AsCollection casts (#36526) * Update AsArrayObject.php * Update AsCollection.php * return null * return null * formatting --- src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php | 2 +- src/Illuminate/Database/Eloquent/Casts/AsCollection.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php index 8d950e2daff4..a939e8acdbdb 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php @@ -18,7 +18,7 @@ public static function castUsing(array $arguments) return new class implements CastsAttributes { public function get($model, $key, $value, $attributes) { - return new ArrayObject(json_decode($attributes[$key], true)); + return isset($attributes[$key]) ? new ArrayObject(json_decode($attributes[$key], true)) : null; } public function set($model, $key, $value, $attributes) diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php index e96834f6e4d3..c2d567b504f7 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php @@ -19,7 +19,7 @@ public static function castUsing(array $arguments) return new class implements CastsAttributes { public function get($model, $key, $value, $attributes) { - return new Collection(json_decode($attributes[$key], true)); + return isset($attributes[$key]) ? new Collection(json_decode($attributes[$key], true)) : null; } public function set($model, $key, $value, $attributes) From 5d093e276abff36b8b4e75541e13ca54829cc75c Mon Sep 17 00:00:00 2001 From: Sibghatullah Mujaddid Date: Thu, 11 Mar 2021 11:31:35 +0800 Subject: [PATCH 57/91] Accept callable class for reportable and renderable in exception handler (#36551) --- .../Foundation/Exceptions/Handler.php | 8 ++++ .../FoundationExceptionsHandlerTest.php | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 38c29a3996c7..bedb1fca3be4 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -139,6 +139,10 @@ public function register() */ public function reportable(callable $reportUsing) { + if (! $reportUsing instanceof Closure) { + $reportUsing = Closure::fromCallable($reportUsing); + } + return tap(new ReportableHandler($reportUsing), function ($callback) { $this->reportCallbacks[] = $callback; }); @@ -152,6 +156,10 @@ public function reportable(callable $reportUsing) */ public function renderable(callable $renderUsing) { + if (! $renderUsing instanceof Closure) { + $renderUsing = Closure::fromCallable($renderUsing); + } + $this->renderCallbacks[] = $renderUsing; return $this; diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index 55b2d54982a5..755a01d9f31e 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -107,6 +107,20 @@ public function testHandlerCallsReportMethodWithDependencies() $this->handler->report(new ReportableException('Exception message')); } + public function testHandlerReportsExceptionUsingCallableClass() + { + $reporter = m::mock(ReportingService::class); + $reporter->shouldReceive('send')->withArgs(['Exception message'])->once(); + + $logger = m::mock(LoggerInterface::class); + $this->container->instance(LoggerInterface::class, $logger); + $logger->shouldNotReceive('error'); + + $this->handler->reportable(new CustomReporter($reporter)); + + $this->handler->report(new CustomException('Exception message')); + } + public function testReturnsJsonWithStackTraceWhenAjaxRequestAndDebugTrue() { $this->config->shouldReceive('get')->with('app.debug', null)->once()->andReturn(true); @@ -134,6 +148,15 @@ public function testReturnsCustomResponseFromRenderableCallback() $this->assertSame('{"response":"My custom exception response"}', $response); } + public function testReturnsCustomResponseFromCallableClass() + { + $this->handler->renderable(new CustomRenderer()); + + $response = $this->handler->render($this->request, new CustomException)->getContent(); + + $this->assertSame('{"response":"The CustomRenderer response"}', $response); + } + public function testReturnsCustomResponseWhenExceptionImplementsResponsable() { $response = $this->handler->render($this->request, new ResponsableException)->getContent(); @@ -302,6 +325,31 @@ public function context() } } +class CustomReporter +{ + private $service; + + public function __construct(ReportingService $service) + { + $this->service = $service; + } + + public function __invoke(CustomException $e) + { + $this->service->send($e->getMessage()); + + return false; + } +} + +class CustomRenderer +{ + public function __invoke(CustomException $e, $request) + { + return response()->json(['response' => 'The CustomRenderer response']); + } +} + interface ReportingService { public function send($message); From dd7274d23a9ee58cc1abdf7107403169a3994b68 Mon Sep 17 00:00:00 2001 From: Ahmed Ashraf Date: Wed, 10 Mar 2021 18:03:35 +0100 Subject: [PATCH 58/91] [8.x] Container - detect circular dependencies --- src/Illuminate/Container/Container.php | 8 +++++- .../CircularDependencyFoundException.php | 11 ++++++++ tests/Container/ContainerTest.php | 26 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/Illuminate/Contracts/Container/CircularDependencyFoundException.php diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 2cfa72f51bb7..abf94c99861c 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -6,6 +6,7 @@ use Closure; use Exception; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Container\CircularDependencyFoundException; use Illuminate\Contracts\Container\Container as ContainerContract; use LogicException; use ReflectionClass; @@ -659,7 +660,7 @@ public function get($id) try { return $this->resolve($id); } catch (Exception $e) { - if ($this->has($id)) { + if ($this->has($id) || $e instanceof CircularDependencyFoundException) { throw $e; } @@ -839,6 +840,11 @@ public function build($concrete) return $this->notInstantiable($concrete); } + // Check for circular dependencies + if(in_array($concrete, $this->buildStack)) { + throw new CircularDependencyFoundException("Circular dependency while initiating [{$concrete}]"); + } + $this->buildStack[] = $concrete; $constructor = $reflector->getConstructor(); diff --git a/src/Illuminate/Contracts/Container/CircularDependencyFoundException.php b/src/Illuminate/Contracts/Container/CircularDependencyFoundException.php new file mode 100644 index 000000000000..d6d0398e93ef --- /dev/null +++ b/src/Illuminate/Contracts/Container/CircularDependencyFoundException.php @@ -0,0 +1,11 @@ +assertInstanceOf(ContainerConcreteStub::class, $class); } + + public function testContainerCanCatchCircularDependency() { + $this->expectException(CircularDependencyFoundException::class); + + $container = new Container; + $container->get(CircularAStub::class); + } +} + +class CircularAStub { + public function __construct(CircularBStub $b) { + + } +} + +class CircularBStub { + public function __construct(CircularCStub $c) { + + } +} + +class CircularCStub { + public function __construct(CircularAStub $a) { + + } } class ContainerConcreteStub From 0f79023f0b176a5ac69a878d642431c402880210 Mon Sep 17 00:00:00 2001 From: Ahmed Ashraf Date: Wed, 10 Mar 2021 18:15:33 +0100 Subject: [PATCH 59/91] update doc --- src/Illuminate/Container/Container.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index abf94c99861c..ad3f6a516f2a 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -671,12 +671,13 @@ public function get($id) /** * Resolve the given type from the container. * - * @param string|callable $abstract - * @param array $parameters - * @param bool $raiseEvents + * @param string|callable $abstract + * @param array $parameters + * @param bool $raiseEvents * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Illuminate\Contracts\Container\CircularDependencyFoundException */ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { @@ -813,10 +814,11 @@ protected function isBuildable($concrete, $abstract) /** * Instantiate a concrete instance of the given type. * - * @param \Closure|string $concrete + * @param \Closure|string $concrete * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException + * @throws \Illuminate\Contracts\Container\CircularDependencyFoundException */ public function build($concrete) { From a712f72ca88f709335576530b31635738abd4c89 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 10 Mar 2021 21:44:47 -0600 Subject: [PATCH 60/91] formatting --- src/Illuminate/Container/Container.php | 21 +++++++++---------- .../CircularDependencyFoundException.php | 11 ---------- tests/Container/ContainerTest.php | 4 ++-- 3 files changed, 12 insertions(+), 24 deletions(-) delete mode 100644 src/Illuminate/Contracts/Container/CircularDependencyFoundException.php diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index ad3f6a516f2a..4ea378983d0d 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -6,7 +6,7 @@ use Closure; use Exception; use Illuminate\Contracts\Container\BindingResolutionException; -use Illuminate\Contracts\Container\CircularDependencyFoundException; +use Illuminate\Contracts\Container\CircularDependencyException; use Illuminate\Contracts\Container\Container as ContainerContract; use LogicException; use ReflectionClass; @@ -660,7 +660,7 @@ public function get($id) try { return $this->resolve($id); } catch (Exception $e) { - if ($this->has($id) || $e instanceof CircularDependencyFoundException) { + if ($this->has($id) || $e instanceof CircularDependencyException) { throw $e; } @@ -671,13 +671,13 @@ public function get($id) /** * Resolve the given type from the container. * - * @param string|callable $abstract - * @param array $parameters - * @param bool $raiseEvents + * @param string|callable $abstract + * @param array $parameters + * @param bool $raiseEvents * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException - * @throws \Illuminate\Contracts\Container\CircularDependencyFoundException + * @throws \Illuminate\Contracts\Container\CircularDependencyException */ protected function resolve($abstract, $parameters = [], $raiseEvents = true) { @@ -814,11 +814,11 @@ protected function isBuildable($concrete, $abstract) /** * Instantiate a concrete instance of the given type. * - * @param \Closure|string $concrete + * @param \Closure|string $concrete * @return mixed * * @throws \Illuminate\Contracts\Container\BindingResolutionException - * @throws \Illuminate\Contracts\Container\CircularDependencyFoundException + * @throws \Illuminate\Contracts\Container\CircularDependencyException */ public function build($concrete) { @@ -842,9 +842,8 @@ public function build($concrete) return $this->notInstantiable($concrete); } - // Check for circular dependencies - if(in_array($concrete, $this->buildStack)) { - throw new CircularDependencyFoundException("Circular dependency while initiating [{$concrete}]"); + if (in_array($concrete, $this->buildStack)) { + throw new CircularDependencyException("Circular dependency detected while resolving [{$concrete}]."); } $this->buildStack[] = $concrete; diff --git a/src/Illuminate/Contracts/Container/CircularDependencyFoundException.php b/src/Illuminate/Contracts/Container/CircularDependencyFoundException.php deleted file mode 100644 index d6d0398e93ef..000000000000 --- a/src/Illuminate/Contracts/Container/CircularDependencyFoundException.php +++ /dev/null @@ -1,11 +0,0 @@ -expectException(CircularDependencyFoundException::class); + $this->expectException(CircularDependencyException::class); $container = new Container; $container->get(CircularAStub::class); From 6f9bb4cdd84295cbcf7908cc4b4684f47f38b8cf Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Wed, 10 Mar 2021 21:44:53 -0600 Subject: [PATCH 61/91] formatting --- .../Container/CircularDependencyException.php | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/Illuminate/Contracts/Container/CircularDependencyException.php diff --git a/src/Illuminate/Contracts/Container/CircularDependencyException.php b/src/Illuminate/Contracts/Container/CircularDependencyException.php new file mode 100644 index 000000000000..6c90381cc0cd --- /dev/null +++ b/src/Illuminate/Contracts/Container/CircularDependencyException.php @@ -0,0 +1,11 @@ + Date: Wed, 10 Mar 2021 21:45:44 -0600 Subject: [PATCH 62/91] Apply fixes from StyleCI (#36553) --- tests/Container/ContainerTest.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 27ddca9a12b5..8b5fe7a23a88 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -564,7 +564,8 @@ public function testContainerCanResolveClasses() $this->assertInstanceOf(ContainerConcreteStub::class, $class); } - public function testContainerCanCatchCircularDependency() { + public function testContainerCanCatchCircularDependency() + { $this->expectException(CircularDependencyException::class); $container = new Container; @@ -572,21 +573,24 @@ public function testContainerCanCatchCircularDependency() { } } -class CircularAStub { - public function __construct(CircularBStub $b) { - +class CircularAStub +{ + public function __construct(CircularBStub $b) + { } } -class CircularBStub { - public function __construct(CircularCStub $c) { - +class CircularBStub +{ + public function __construct(CircularCStub $c) + { } } -class CircularCStub { - public function __construct(CircularAStub $a) { - +class CircularCStub +{ + public function __construct(CircularAStub $a) + { } } From d5cd4b3ee7e4db527c5a089af78183e6742afeb5 Mon Sep 17 00:00:00 2001 From: Sevan Nerse Date: Thu, 11 Mar 2021 16:39:04 +0300 Subject: [PATCH 63/91] Add test for stack prepend (#36555) --- tests/View/ViewFactoryTest.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/View/ViewFactoryTest.php b/tests/View/ViewFactoryTest.php index ab003c3b9f9d..1419f502706c 100755 --- a/tests/View/ViewFactoryTest.php +++ b/tests/View/ViewFactoryTest.php @@ -443,6 +443,27 @@ public function testMultipleStackPush() $this->assertSame('hi, Hello!', $factory->yieldPushContent('foo')); } + public function testSingleStackPrepend() + { + $factory = $this->getFactory(); + $factory->startPrepend('foo'); + echo 'hi'; + $factory->stopPrepend(); + $this->assertSame('hi', $factory->yieldPushContent('foo')); + } + + public function testMultipleStackPrepend() + { + $factory = $this->getFactory(); + $factory->startPrepend('foo'); + echo ', Hello!'; + $factory->stopPrepend(); + $factory->startPrepend('foo'); + echo 'hi'; + $factory->stopPrepend(); + $this->assertSame('hi, Hello!', $factory->yieldPushContent('foo')); + } + public function testSessionAppending() { $factory = $this->getFactory(); From 2d304d4e5dc49c5f80e0ee77cf44d08e8d94325d Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Thu, 11 Mar 2021 14:55:37 +0100 Subject: [PATCH 64/91] Consolidate empty function bodies (#36560) --- src/Illuminate/Events/NullDispatcher.php | 3 +++ src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php | 5 +++++ src/Illuminate/Support/Testing/Fakes/BusFake.php | 1 + tests/Container/ContainerTest.php | 3 +++ tests/Integration/Queue/CustomPayloadTest.php | 1 + tests/Support/SupportReflectorTest.php | 4 ++++ 6 files changed, 17 insertions(+) diff --git a/src/Illuminate/Events/NullDispatcher.php b/src/Illuminate/Events/NullDispatcher.php index 5c020cfdd732..5c539d53a361 100644 --- a/src/Illuminate/Events/NullDispatcher.php +++ b/src/Illuminate/Events/NullDispatcher.php @@ -37,6 +37,7 @@ public function __construct(DispatcherContract $dispatcher) */ public function dispatch($event, $payload = [], $halt = false) { + // } /** @@ -48,6 +49,7 @@ public function dispatch($event, $payload = [], $halt = false) */ public function push($event, $payload = []) { + // } /** @@ -59,6 +61,7 @@ public function push($event, $payload = []) */ public function until($event, $payload = []) { + // } /** diff --git a/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php b/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php index 55681f4d5534..d9661334ce01 100644 --- a/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BatchRepositoryFake.php @@ -33,6 +33,7 @@ public function get($limit, $before) */ public function find(string $batchId) { + // } /** @@ -68,6 +69,7 @@ public function store(PendingBatch $batch) */ public function incrementTotalJobs(string $batchId, int $amount) { + // } /** @@ -102,6 +104,7 @@ public function incrementFailedJobs(string $batchId, string $jobId) */ public function markAsFinished(string $batchId) { + // } /** @@ -112,6 +115,7 @@ public function markAsFinished(string $batchId) */ public function cancel(string $batchId) { + // } /** @@ -122,6 +126,7 @@ public function cancel(string $batchId) */ public function delete(string $batchId) { + // } /** diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index 5b5b68534583..82f3dda5d129 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -517,6 +517,7 @@ public function chain($jobs) */ public function findBatch(string $batchId) { + // } /** diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 8b5fe7a23a88..39638694f802 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -577,6 +577,7 @@ class CircularAStub { public function __construct(CircularBStub $b) { + // } } @@ -584,6 +585,7 @@ class CircularBStub { public function __construct(CircularCStub $c) { + // } } @@ -591,6 +593,7 @@ class CircularCStub { public function __construct(CircularAStub $a) { + // } } diff --git a/tests/Integration/Queue/CustomPayloadTest.php b/tests/Integration/Queue/CustomPayloadTest.php index 3d1088b52c4d..2ce39544be34 100644 --- a/tests/Integration/Queue/CustomPayloadTest.php +++ b/tests/Integration/Queue/CustomPayloadTest.php @@ -60,5 +60,6 @@ class MyJob implements ShouldQueue public function handle() { + // } } diff --git a/tests/Support/SupportReflectorTest.php b/tests/Support/SupportReflectorTest.php index df5b3e414e46..deebed5aabc7 100644 --- a/tests/Support/SupportReflectorTest.php +++ b/tests/Support/SupportReflectorTest.php @@ -81,6 +81,7 @@ class B extends A { public function f(parent $x) { + // } } @@ -92,6 +93,7 @@ class C { public function f(A|Model $x) { + // } }' ); @@ -101,6 +103,7 @@ class TestClassWithCall { public function __call($method, $parameters) { + // } } @@ -108,5 +111,6 @@ class TestClassWithCallStatic { public static function __callStatic($method, $parameters) { + // } } From 36f5aac531f0dfd148b45be7960188db12d80f9a Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Thu, 11 Mar 2021 14:56:28 +0100 Subject: [PATCH 65/91] Fix returns with Mail & Notification components (#36559) These methods were incorrectly returning values. Since nothing was returned in the first place and the proper DocBlocks were already in place, this should be a general safe thing to do. --- src/Illuminate/Mail/Mailable.php | 2 +- src/Illuminate/Mail/Mailer.php | 6 +++--- src/Illuminate/Mail/PendingMail.php | 4 ++-- src/Illuminate/Notifications/ChannelManager.php | 4 ++-- src/Illuminate/Notifications/NotificationSender.php | 2 +- src/Illuminate/Support/Testing/Fakes/EventFake.php | 2 +- src/Illuminate/Support/Testing/Fakes/NotificationFake.php | 2 +- src/Illuminate/Support/Testing/Fakes/PendingMailFake.php | 4 ++-- src/Illuminate/Support/Testing/Fakes/QueueFake.php | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Illuminate/Mail/Mailable.php b/src/Illuminate/Mail/Mailable.php index 1fe0b7ea3d20..903bd5f5f41b 100644 --- a/src/Illuminate/Mail/Mailable.php +++ b/src/Illuminate/Mail/Mailable.php @@ -170,7 +170,7 @@ class Mailable implements MailableContract, Renderable */ public function send($mailer) { - return $this->withLocale($this->locale, function () use ($mailer) { + $this->withLocale($this->locale, function () use ($mailer) { Container::getInstance()->call([$this, 'build']); $mailer = $mailer instanceof MailFactory diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 668d68baaf2a..128f211f7651 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -197,7 +197,7 @@ public function bcc($users) */ public function html($html, $callback) { - return $this->send(['html' => new HtmlString($html)], [], $callback); + $this->send(['html' => new HtmlString($html)], [], $callback); } /** @@ -209,7 +209,7 @@ public function html($html, $callback) */ public function raw($text, $callback) { - return $this->send(['raw' => $text], [], $callback); + $this->send(['raw' => $text], [], $callback); } /** @@ -222,7 +222,7 @@ public function raw($text, $callback) */ public function plain($view, array $data, $callback) { - return $this->send(['text' => $view], $data, $callback); + $this->send(['text' => $view], $data, $callback); } /** diff --git a/src/Illuminate/Mail/PendingMail.php b/src/Illuminate/Mail/PendingMail.php index 43c30961f90b..10d76cb6aa9b 100644 --- a/src/Illuminate/Mail/PendingMail.php +++ b/src/Illuminate/Mail/PendingMail.php @@ -114,11 +114,11 @@ public function bcc($users) * Send a new mailable message instance. * * @param \Illuminate\Contracts\Mail\Mailable $mailable - * @return mixed + * @return void */ public function send(MailableContract $mailable) { - return $this->mailer->send($this->fill($mailable)); + $this->mailer->send($this->fill($mailable)); } /** diff --git a/src/Illuminate/Notifications/ChannelManager.php b/src/Illuminate/Notifications/ChannelManager.php index d2344ab68acc..8eb9c251024d 100644 --- a/src/Illuminate/Notifications/ChannelManager.php +++ b/src/Illuminate/Notifications/ChannelManager.php @@ -34,7 +34,7 @@ class ChannelManager extends Manager implements DispatcherContract, FactoryContr */ public function send($notifiables, $notification) { - return (new NotificationSender( + (new NotificationSender( $this, $this->container->make(Bus::class), $this->container->make(Dispatcher::class), $this->locale) )->send($notifiables, $notification); } @@ -49,7 +49,7 @@ public function send($notifiables, $notification) */ public function sendNow($notifiables, $notification, array $channels = null) { - return (new NotificationSender( + (new NotificationSender( $this, $this->container->make(Bus::class), $this->container->make(Dispatcher::class), $this->locale) )->sendNow($notifiables, $notification, $channels); } diff --git a/src/Illuminate/Notifications/NotificationSender.php b/src/Illuminate/Notifications/NotificationSender.php index 39be0e598796..aff36c7a5b0f 100644 --- a/src/Illuminate/Notifications/NotificationSender.php +++ b/src/Illuminate/Notifications/NotificationSender.php @@ -76,7 +76,7 @@ public function send($notifiables, $notification) return $this->queueNotification($notifiables, $notification); } - return $this->sendNow($notifiables, $notification); + $this->sendNow($notifiables, $notification); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/EventFake.php b/src/Illuminate/Support/Testing/Fakes/EventFake.php index 90f30212ef7b..88fcb84cea48 100644 --- a/src/Illuminate/Support/Testing/Fakes/EventFake.php +++ b/src/Illuminate/Support/Testing/Fakes/EventFake.php @@ -278,7 +278,7 @@ public function forgetPushed() * * @param string|object $event * @param mixed $payload - * @return void + * @return array|null */ public function until($event, $payload = []) { diff --git a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php index cf3a25afd696..28526d592556 100644 --- a/src/Illuminate/Support/Testing/Fakes/NotificationFake.php +++ b/src/Illuminate/Support/Testing/Fakes/NotificationFake.php @@ -210,7 +210,7 @@ protected function notificationsFor($notifiable, $notification) */ public function send($notifiables, $notification) { - return $this->sendNow($notifiables, $notification); + $this->sendNow($notifiables, $notification); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php b/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php index c39012501ae6..52251301ceb9 100644 --- a/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php +++ b/src/Illuminate/Support/Testing/Fakes/PendingMailFake.php @@ -22,11 +22,11 @@ public function __construct($mailer) * Send a new mailable message instance. * * @param \Illuminate\Contracts\Mail\Mailable $mailable - * @return mixed + * @return void */ public function send(Mailable $mailable) { - return $this->mailer->send($this->fill($mailable)); + $this->mailer->send($this->fill($mailable)); } /** diff --git a/src/Illuminate/Support/Testing/Fakes/QueueFake.php b/src/Illuminate/Support/Testing/Fakes/QueueFake.php index e83408fc0c74..64d6414fd81b 100644 --- a/src/Illuminate/Support/Testing/Fakes/QueueFake.php +++ b/src/Illuminate/Support/Testing/Fakes/QueueFake.php @@ -74,7 +74,7 @@ public function assertPushedOn($queue, $job, $callback = null) [$job, $callback] = [$this->firstClosureParameterType($job), $job]; } - return $this->assertPushed($job, function ($job, $pushedQueue) use ($callback, $queue) { + $this->assertPushed($job, function ($job, $pushedQueue) use ($callback, $queue) { if ($pushedQueue !== $queue) { return false; } From 62925b64310bb796dff479aea4acc55a1620d470 Mon Sep 17 00:00:00 2001 From: Olzhas Date: Thu, 11 Mar 2021 21:08:31 +0600 Subject: [PATCH 66/91] Add resource missing option --- .../Routing/PendingResourceRegistration.php | 13 +++++++++++++ src/Illuminate/Routing/ResourceRegistrar.php | 11 +++++++++++ tests/Routing/RouteRegistrarTest.php | 16 ++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src/Illuminate/Routing/PendingResourceRegistration.php b/src/Illuminate/Routing/PendingResourceRegistration.php index 3b6c97e2042d..d2bb33c65de2 100644 --- a/src/Illuminate/Routing/PendingResourceRegistration.php +++ b/src/Illuminate/Routing/PendingResourceRegistration.php @@ -195,6 +195,19 @@ public function shallow($shallow = true) return $this; } + /** + * Define the callable that should be invoked on a missing model exception. + * + * @param $callback + * @return $this + */ + public function missing($callback) + { + $this->options['missing'] = $callback; + + return $this; + } + /** * Indicate that the resource routes should be scoped using the given binding fields. * diff --git a/src/Illuminate/Routing/ResourceRegistrar.php b/src/Illuminate/Routing/ResourceRegistrar.php index c32aa023b4bd..d52264456af1 100644 --- a/src/Illuminate/Routing/ResourceRegistrar.php +++ b/src/Illuminate/Routing/ResourceRegistrar.php @@ -20,6 +20,13 @@ class ResourceRegistrar */ protected $resourceDefaults = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']; + /** + * Actions that use model binding. + * + * @var string[] + */ + protected $modelBoundMethods = ['show', 'edit', 'update', 'destroy']; + /** * The parameters set for this resource instance. * @@ -421,6 +428,10 @@ protected function getResourceAction($resource, $controller, $method, $options) $action['where'] = $options['wheres']; } + if (isset($options['missing']) && in_array($method, $this->modelBoundMethods)) { + $action['missing'] = $options['missing']; + } + return $action; } diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index 47fcb2aa31e5..831ced92fdfb 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -334,6 +334,22 @@ public function testCanRegisterResourcesWithoutOption() } } + public function testCanRegisterResourceWithMissingOption() + { + $this->router->middleware('resource-middleware') + ->resource('users', RouteRegistrarControllerStub::class) + ->missing(function () { return 'missing'; }); + + $this->assertIsCallable($this->router->getRoutes()->getByName('users.show')->getMissing()); + $this->assertIsCallable($this->router->getRoutes()->getByName('users.edit')->getMissing()); + $this->assertIsCallable($this->router->getRoutes()->getByName('users.update')->getMissing()); + $this->assertIsCallable($this->router->getRoutes()->getByName('users.destroy')->getMissing()); + + $this->assertNull($this->router->getRoutes()->getByName('users.index')->getMissing()); + $this->assertNull($this->router->getRoutes()->getByName('users.create')->getMissing()); + $this->assertNull($this->router->getRoutes()->getByName('users.store')->getMissing()); + } + public function testCanAccessRegisteredResourceRoutesAsRouteCollection() { $resource = $this->router->middleware('resource-middleware') From c887875c23f393e3443b1fd2a8dd0c748e6f13ea Mon Sep 17 00:00:00 2001 From: Olzhas Date: Thu, 11 Mar 2021 21:25:07 +0600 Subject: [PATCH 67/91] Add resource missing option --- src/Illuminate/Routing/ResourceRegistrar.php | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/Illuminate/Routing/ResourceRegistrar.php b/src/Illuminate/Routing/ResourceRegistrar.php index d52264456af1..c32de58c291f 100644 --- a/src/Illuminate/Routing/ResourceRegistrar.php +++ b/src/Illuminate/Routing/ResourceRegistrar.php @@ -20,13 +20,6 @@ class ResourceRegistrar */ protected $resourceDefaults = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy']; - /** - * Actions that use model binding. - * - * @var string[] - */ - protected $modelBoundMethods = ['show', 'edit', 'update', 'destroy']; - /** * The parameters set for this resource instance. * @@ -191,6 +184,8 @@ protected function addResourceIndex($name, $base, $controller, $options) { $uri = $this->getResourceUri($name); + unset($options['missing']); + $action = $this->getResourceAction($name, $controller, 'index', $options); return $this->router->get($uri, $action); @@ -209,6 +204,8 @@ protected function addResourceCreate($name, $base, $controller, $options) { $uri = $this->getResourceUri($name).'/'.static::$verbs['create']; + unset($options['missing']); + $action = $this->getResourceAction($name, $controller, 'create', $options); return $this->router->get($uri, $action); @@ -227,6 +224,8 @@ protected function addResourceStore($name, $base, $controller, $options) { $uri = $this->getResourceUri($name); + unset($options['missing']); + $action = $this->getResourceAction($name, $controller, 'store', $options); return $this->router->post($uri, $action); @@ -428,7 +427,7 @@ protected function getResourceAction($resource, $controller, $method, $options) $action['where'] = $options['wheres']; } - if (isset($options['missing']) && in_array($method, $this->modelBoundMethods)) { + if (isset($options['missing'])) { $action['missing'] = $options['missing']; } From 61db3ef0e82b0ff5170100e2ea5198a195ac2743 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Thu, 11 Mar 2021 17:23:18 +0100 Subject: [PATCH 68/91] Update to ubuntu-20.04 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7b91b6fe781..1a81af4975b9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ on: jobs: linux_tests: - runs-on: ubuntu-18.04 + runs-on: ubuntu-20.04 services: memcached: From 65c8e0c13323862d6e65432468bea2924e490629 Mon Sep 17 00:00:00 2001 From: Dries Vints Date: Fri, 12 Mar 2021 00:04:53 +0100 Subject: [PATCH 69/91] Fix Mailer contract returns (#36563) --- src/Illuminate/Contracts/Mail/Factory.php | 2 +- src/Illuminate/Mail/MailManager.php | 2 +- src/Illuminate/Support/Testing/Fakes/MailFake.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Contracts/Mail/Factory.php b/src/Illuminate/Contracts/Mail/Factory.php index 0719c0514595..fe45a2fd9cd8 100644 --- a/src/Illuminate/Contracts/Mail/Factory.php +++ b/src/Illuminate/Contracts/Mail/Factory.php @@ -8,7 +8,7 @@ interface Factory * Get a mailer instance by name. * * @param string|null $name - * @return \Illuminate\Mail\Mailer + * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null); } diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index 97fcda7827c5..86acdde3e3aa 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -63,7 +63,7 @@ public function __construct($app) * Get a mailer instance by name. * * @param string|null $name - * @return \Illuminate\Mail\Mailer + * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null) { diff --git a/src/Illuminate/Support/Testing/Fakes/MailFake.php b/src/Illuminate/Support/Testing/Fakes/MailFake.php index eb548b6910b3..a42fe341f40e 100644 --- a/src/Illuminate/Support/Testing/Fakes/MailFake.php +++ b/src/Illuminate/Support/Testing/Fakes/MailFake.php @@ -276,7 +276,7 @@ protected function queuedMailablesOf($type) * Get a mailer instance by name. * * @param string|null $name - * @return \Illuminate\Mail\Mailer + * @return \Illuminate\Contracts\Mail\Mailer */ public function mailer($name = null) { From be86b91ca8812a2564cba71f859ebf410ca1f571 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= <1005065+DeepDiver1975@users.noreply.github.com> Date: Fri, 12 Mar 2021 14:52:50 +0100 Subject: [PATCH 70/91] [6.x] add make method to App facade (#36574) --- src/Illuminate/Support/Facades/App.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 67e0b4c233e4..0bc5b34254d3 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -19,6 +19,7 @@ * @method static \Illuminate\Support\ServiceProvider register(\Illuminate\Support\ServiceProvider|string $provider, bool $force = false) * @method static void registerDeferredProvider(string $provider, string $service = null) * @method static \Illuminate\Support\ServiceProvider resolveProvider(string $provider) + * @method static mixed make($abstract, array $parameters = []) * @method static void boot() * @method static void booting(callable $callback) * @method static void booted(callable $callback) From cf9b74985e882c2405a9f6068cdd9bacfc9433a0 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 12 Mar 2021 08:11:27 -0600 Subject: [PATCH 71/91] formatting --- src/Illuminate/Routing/PendingResourceRegistration.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Routing/PendingResourceRegistration.php b/src/Illuminate/Routing/PendingResourceRegistration.php index d2bb33c65de2..59e4b8f0b78f 100644 --- a/src/Illuminate/Routing/PendingResourceRegistration.php +++ b/src/Illuminate/Routing/PendingResourceRegistration.php @@ -198,7 +198,7 @@ public function shallow($shallow = true) /** * Define the callable that should be invoked on a missing model exception. * - * @param $callback + * @param callable $callback * @return $this */ public function missing($callback) From 6d1da017689c004efece0bde8c3790202c359a31 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Fri, 12 Mar 2021 08:12:04 -0600 Subject: [PATCH 72/91] Apply fixes from StyleCI (#36577) --- tests/Routing/RouteRegistrarTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Routing/RouteRegistrarTest.php b/tests/Routing/RouteRegistrarTest.php index 831ced92fdfb..9802ad742a61 100644 --- a/tests/Routing/RouteRegistrarTest.php +++ b/tests/Routing/RouteRegistrarTest.php @@ -338,7 +338,9 @@ public function testCanRegisterResourceWithMissingOption() { $this->router->middleware('resource-middleware') ->resource('users', RouteRegistrarControllerStub::class) - ->missing(function () { return 'missing'; }); + ->missing(function () { + return 'missing'; + }); $this->assertIsCallable($this->router->getRoutes()->getByName('users.show')->getMissing()); $this->assertIsCallable($this->router->getRoutes()->getByName('users.edit')->getMissing()); From e4ed3317c2730803925918e1cb1e1e739e20ba5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marco=20Aur=C3=A9lio=20Deleu?= Date: Fri, 12 Mar 2021 15:45:30 +0100 Subject: [PATCH 73/91] [8.x] StringEncrypter interface for better DX (#36578) * StringEncrypter interface for better DX * StyleCI * Update Encrypter.php Co-authored-by: Taylor Otwell --- .../Contracts/Encryption/StringEncrypter.php | 26 +++++++++++++++++++ src/Illuminate/Encryption/Encrypter.php | 3 ++- src/Illuminate/Foundation/Application.php | 2 +- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/Illuminate/Contracts/Encryption/StringEncrypter.php diff --git a/src/Illuminate/Contracts/Encryption/StringEncrypter.php b/src/Illuminate/Contracts/Encryption/StringEncrypter.php new file mode 100644 index 000000000000..1e6938c29a16 --- /dev/null +++ b/src/Illuminate/Contracts/Encryption/StringEncrypter.php @@ -0,0 +1,26 @@ + [\Symfony\Component\Cache\Adapter\Psr16Adapter::class, \Symfony\Component\Cache\Adapter\AdapterInterface::class, \Psr\Cache\CacheItemPoolInterface::class], 'config' => [\Illuminate\Config\Repository::class, \Illuminate\Contracts\Config\Repository::class], 'cookie' => [\Illuminate\Cookie\CookieJar::class, \Illuminate\Contracts\Cookie\Factory::class, \Illuminate\Contracts\Cookie\QueueingFactory::class], - 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class], 'db' => [\Illuminate\Database\DatabaseManager::class, \Illuminate\Database\ConnectionResolverInterface::class], 'db.connection' => [\Illuminate\Database\Connection::class, \Illuminate\Database\ConnectionInterface::class], + 'encrypter' => [\Illuminate\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\Encrypter::class, \Illuminate\Contracts\Encryption\StringEncrypter::class], 'events' => [\Illuminate\Events\Dispatcher::class, \Illuminate\Contracts\Events\Dispatcher::class], 'files' => [\Illuminate\Filesystem\Filesystem::class], 'filesystem' => [\Illuminate\Filesystem\FilesystemManager::class, \Illuminate\Contracts\Filesystem\Factory::class], From a69f15d30522eb3c1db5786c6df8a6d4f87bd0a3 Mon Sep 17 00:00:00 2001 From: Markus Machatschek Date: Mon, 15 Mar 2021 14:32:05 +0100 Subject: [PATCH 74/91] Add ssl broken pipe as lost connection error (#36601) --- src/Illuminate/Database/DetectsLostConnections.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index 1ecfc96140f4..a0bad6718017 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -50,6 +50,7 @@ protected function causedByLostConnection(Throwable $e) 'SSL: Connection timed out', 'SQLSTATE[HY000]: General error: 1105 The last transaction was aborted due to Seamless Scaling. Please retry.', 'Temporary failure in name resolution', + 'SSL: Broken pipe', ]); } } From ddab370272942f456b0632b8f1d5d957266262cd Mon Sep 17 00:00:00 2001 From: Zubair Mohsin Date: Mon, 15 Mar 2021 18:36:05 +0500 Subject: [PATCH 75/91] Initializes CronExpression class using new (#36600) --- src/Illuminate/Console/Scheduling/Event.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index a680c1a64cb0..b3ab9b2db567 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -328,7 +328,7 @@ protected function expressionPasses() $date->setTimezone($this->timezone); } - return CronExpression::factory($this->expression)->isDue($date->toDateTimeString()); + return (new CronExpression($this->expression))->isDue($date->toDateTimeString()); } /** @@ -890,9 +890,8 @@ public function getSummaryForDisplay() */ public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false) { - return Date::instance(CronExpression::factory( - $this->getExpression() - )->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone)); + return Date::instance((new CronExpression($this->getExpression())) + ->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone)); } /** From e0c638167c5e6911d6fc579bc4e04b66f6a94784 Mon Sep 17 00:00:00 2001 From: Roy de Vos Burchart Date: Mon, 15 Mar 2021 14:43:13 +0100 Subject: [PATCH 76/91] Remove null from return in phpdoc (#36593) --- src/Illuminate/Support/Stringable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index 0d544a3987cf..d834260e11cf 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -335,7 +335,7 @@ public function markdown(array $options = []) * Get the string matching the given pattern. * * @param string $pattern - * @return static|null + * @return static */ public function match($pattern) { From c67a685598de8e12e7fcf03095ac168f85122c4e Mon Sep 17 00:00:00 2001 From: Nguyen You Date: Mon, 15 Mar 2021 20:43:55 +0700 Subject: [PATCH 77/91] Update docblock (#36592) --- src/Illuminate/Foundation/Console/stubs/view-component.stub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Console/stubs/view-component.stub b/src/Illuminate/Foundation/Console/stubs/view-component.stub index 5c6ecc586ec4..22eae518c1a4 100644 --- a/src/Illuminate/Foundation/Console/stubs/view-component.stub +++ b/src/Illuminate/Foundation/Console/stubs/view-component.stub @@ -19,7 +19,7 @@ class DummyClass extends Component /** * Get the view / contents that represent the component. * - * @return \Illuminate\Contracts\View\View|string + * @return \Illuminate\Contracts\View\View|\Closure|string */ public function render() { From 1e09138398ff1a6a9812e58041f1f9f31d846846 Mon Sep 17 00:00:00 2001 From: Dominik Date: Mon, 15 Mar 2021 14:48:31 +0100 Subject: [PATCH 78/91] Stack driver fix: respect the defined processors (#36591) This update fixes the issue: https://github.com/laravel/ideas/issues/1735 --- src/Illuminate/Log/LogManager.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index 0e0abcd67c33..ed6c418abc2d 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -244,12 +244,16 @@ protected function createStackDriver(array $config) $handlers = collect($config['channels'])->flatMap(function ($channel) { return $this->channel($channel)->getHandlers(); })->all(); + + $processors = collect($config['channels'])->flatMap(function ($channel) { + return $this->channel($channel)->getProcessors(); + })->all(); if ($config['ignore_exceptions'] ?? false) { $handlers = [new WhatFailureGroupHandler($handlers)]; } - return new Monolog($this->parseChannel($config), $handlers); + return new Monolog($this->parseChannel($config), $handlers, $processors); } /** From c02e3250c3a933b08656ac4c84168353eba771a8 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Mon, 15 Mar 2021 08:48:56 -0500 Subject: [PATCH 79/91] Apply fixes from StyleCI (#36603) --- src/Illuminate/Log/LogManager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index ed6c418abc2d..f5d0ac486e2b 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -244,7 +244,7 @@ protected function createStackDriver(array $config) $handlers = collect($config['channels'])->flatMap(function ($channel) { return $this->channel($channel)->getHandlers(); })->all(); - + $processors = collect($config['channels'])->flatMap(function ($channel) { return $this->channel($channel)->getProcessors(); })->all(); From 580fb32724e9dfbeade884dcd91721f4f47b44b5 Mon Sep 17 00:00:00 2001 From: Matthew Boynes Date: Mon, 15 Mar 2021 19:03:48 -0400 Subject: [PATCH 80/91] Require the correct password to rehash it --- src/Illuminate/Auth/SessionGuard.php | 24 ++++++++++++++++--- tests/Integration/Auth/AuthenticationTest.php | 17 ++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 1f6863141c4c..1748e9af19fb 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -573,6 +573,26 @@ protected function cycleRememberToken(AuthenticatableContract $user) $this->provider->updateRememberToken($user, $token); } + /** + * Rehash the user's password. + * + * @param string $password + * @param string $attribute + * @return bool|null + * + * @throws AuthenticationException If the password is invalid. + */ + protected function rehashUserPassword($password, $attribute) + { + if (! Hash::check($password, $this->user()->$attribute)) { + throw new AuthenticationException('Password mismatch.'); + } + + return tap($this->user()->forceFill([ + $attribute => Hash::make($password), + ]))->save(); + } + /** * Invalidate other sessions for the current user. * @@ -588,9 +608,7 @@ public function logoutOtherDevices($password, $attribute = 'password') return; } - $result = tap($this->user()->forceFill([ - $attribute => Hash::make($password), - ]))->save(); + $result = $this->rehashUserPassword($password, $attribute); if ($this->recaller() || $this->getCookieJar()->hasQueued($this->getRecallerName())) { diff --git a/tests/Integration/Auth/AuthenticationTest.php b/tests/Integration/Auth/AuthenticationTest.php index ec23afba3d18..23862f48d94a 100644 --- a/tests/Integration/Auth/AuthenticationTest.php +++ b/tests/Integration/Auth/AuthenticationTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Auth; +use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\EloquentUserProvider; use Illuminate\Auth\Events\Attempting; use Illuminate\Auth\Events\Authenticated; @@ -211,7 +212,7 @@ public function testLoggingOutOtherDevices() $this->assertEquals(1, $user->id); - $this->app['auth']->logoutOtherDevices('adifferentpassword'); + $this->app['auth']->logoutOtherDevices('password'); $this->assertEquals(1, $user->id); Event::assertDispatched(OtherDeviceLogout::class, function ($event) { @@ -222,6 +223,20 @@ public function testLoggingOutOtherDevices() }); } + public function testPasswordMustBeValidToLogOutOtherDevices() + { + $this->expectException(AuthenticationException::class); + $this->expectExceptionMessage('Password mismatch.'); + + $this->app['auth']->loginUsingId(1); + + $user = $this->app['auth']->user(); + + $this->assertEquals(1, $user->id); + + $this->app['auth']->logoutOtherDevices('adifferentpassword'); + } + public function testLoggingInOutViaAttemptRemembering() { $this->assertTrue( From ef4541d51a805eb711e14af897515aa7b2ed3569 Mon Sep 17 00:00:00 2001 From: Matthew Boynes Date: Mon, 15 Mar 2021 19:18:55 -0400 Subject: [PATCH 81/91] Add @throws to logoutOtherDevices() --- src/Illuminate/Auth/SessionGuard.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 1748e9af19fb..ef7ab4ebecc1 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -601,6 +601,8 @@ protected function rehashUserPassword($password, $attribute) * @param string $password * @param string $attribute * @return bool|null + * + * @throws AuthenticationException If the password is invalid. */ public function logoutOtherDevices($password, $attribute = 'password') { From ceb2f46592007ca63bea9836d5547b38c8c47711 Mon Sep 17 00:00:00 2001 From: Matthew Boynes Date: Mon, 15 Mar 2021 21:14:42 -0400 Subject: [PATCH 82/91] Clarify that the current user's password is rehashing --- src/Illuminate/Auth/SessionGuard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index ef7ab4ebecc1..bc6bad4d6a14 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -574,7 +574,7 @@ protected function cycleRememberToken(AuthenticatableContract $user) } /** - * Rehash the user's password. + * Rehash the current user's password. * * @param string $password * @param string $attribute From 7aabd8f0a8f8e0a3420359f807f42ceacb5c4140 Mon Sep 17 00:00:00 2001 From: Matthew Boynes Date: Tue, 16 Mar 2021 07:50:38 -0400 Subject: [PATCH 83/91] Use FQN in docblock, remove description --- src/Illuminate/Auth/SessionGuard.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index bc6bad4d6a14..74569a7bf599 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -580,7 +580,7 @@ protected function cycleRememberToken(AuthenticatableContract $user) * @param string $attribute * @return bool|null * - * @throws AuthenticationException If the password is invalid. + * @throws \Illuminate\Auth\AuthenticationException */ protected function rehashUserPassword($password, $attribute) { @@ -602,7 +602,7 @@ protected function rehashUserPassword($password, $attribute) * @param string $attribute * @return bool|null * - * @throws AuthenticationException If the password is invalid. + * @throws \Illuminate\Auth\AuthenticationException */ public function logoutOtherDevices($password, $attribute = 'password') { From d98cf8b5baca2d12341cdc1d75888cfe6fb67260 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Tue, 16 Mar 2021 13:43:20 +0100 Subject: [PATCH 84/91] [8.x] Use different config key for overriding temporary url host in AwsTemporaryUrl method (#36612) * Use different config key for overriding temporary url host * Update FilesystemAdapter.php Co-authored-by: Taylor Otwell --- src/Illuminate/Filesystem/FilesystemAdapter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 5d6de6ed3321..935c413cd6c0 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -601,7 +601,7 @@ public function getAwsTemporaryUrl($adapter, $path, $expiration, $options) // If an explicit base URL has been set on the disk configuration then we will use // it as the base URL instead of the default path. This allows the developer to // have full control over the base path for this filesystem's generated URLs. - if (! is_null($url = $this->driver->getConfig()->get('url'))) { + if (! is_null($url = $this->driver->getConfig()->get('temporary_url'))) { $uri = $this->replaceBaseUrl($uri, $url); } From 1e6161250074b8106c1fcf153eeaef7c0bf74c6c Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 16 Mar 2021 09:14:59 -0500 Subject: [PATCH 85/91] formatting --- src/Illuminate/Auth/SessionGuard.php | 41 ++++++++++--------- tests/Integration/Auth/AuthenticationTest.php | 6 +-- 2 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 74569a7bf599..48d64c47d39f 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -20,6 +20,7 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use Illuminate\Support\Traits\Macroable; +use InvalidArgumentException; use RuntimeException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; @@ -573,26 +574,6 @@ protected function cycleRememberToken(AuthenticatableContract $user) $this->provider->updateRememberToken($user, $token); } - /** - * Rehash the current user's password. - * - * @param string $password - * @param string $attribute - * @return bool|null - * - * @throws \Illuminate\Auth\AuthenticationException - */ - protected function rehashUserPassword($password, $attribute) - { - if (! Hash::check($password, $this->user()->$attribute)) { - throw new AuthenticationException('Password mismatch.'); - } - - return tap($this->user()->forceFill([ - $attribute => Hash::make($password), - ]))->save(); - } - /** * Invalidate other sessions for the current user. * @@ -622,6 +603,26 @@ public function logoutOtherDevices($password, $attribute = 'password') return $result; } + /** + * Rehash the current user's password. + * + * @param string $password + * @param string $attribute + * @return bool|null + * + * @throws \InvalidArgumentException + */ + protected function rehashUserPassword($password, $attribute) + { + if (! Hash::check($password, $this->user()->{$attribute})) { + throw new InvalidArgumentException("The given password does not match the current password."); + } + + return tap($this->user()->forceFill([ + $attribute => Hash::make($password), + ]))->save(); + } + /** * Register an authentication attempt event listener. * diff --git a/tests/Integration/Auth/AuthenticationTest.php b/tests/Integration/Auth/AuthenticationTest.php index 23862f48d94a..e2ec79050d18 100644 --- a/tests/Integration/Auth/AuthenticationTest.php +++ b/tests/Integration/Auth/AuthenticationTest.php @@ -2,7 +2,6 @@ namespace Illuminate\Tests\Integration\Auth; -use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\EloquentUserProvider; use Illuminate\Auth\Events\Attempting; use Illuminate\Auth\Events\Authenticated; @@ -20,6 +19,7 @@ use Illuminate\Support\Str; use Illuminate\Support\Testing\Fakes\EventFake; use Illuminate\Tests\Integration\Auth\Fixtures\AuthenticationTestUser; +use InvalidArgumentException; use Orchestra\Testbench\TestCase; /** @@ -225,8 +225,8 @@ public function testLoggingOutOtherDevices() public function testPasswordMustBeValidToLogOutOtherDevices() { - $this->expectException(AuthenticationException::class); - $this->expectExceptionMessage('Password mismatch.'); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('current password'); $this->app['auth']->loginUsingId(1); From ebfa75fee5acef028a09f52a78a7069b1a09a723 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 16 Mar 2021 09:15:38 -0500 Subject: [PATCH 86/91] Apply fixes from StyleCI (#36615) --- src/Illuminate/Auth/SessionGuard.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 48d64c47d39f..4bb3fd4b6f73 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -615,7 +615,7 @@ public function logoutOtherDevices($password, $attribute = 'password') protected function rehashUserPassword($password, $attribute) { if (! Hash::check($password, $this->user()->{$attribute})) { - throw new InvalidArgumentException("The given password does not match the current password."); + throw new InvalidArgumentException('The given password does not match the current password.'); } return tap($this->user()->forceFill([ From c25308a00c7bbd38401a6254ab403146da9cfd69 Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 16 Mar 2021 09:18:55 -0500 Subject: [PATCH 87/91] version --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 1f46747dbfbd..5fa3b2bed350 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -31,7 +31,7 @@ class Application extends Container implements ApplicationContract, HttpKernelIn * * @var string */ - const VERSION = '6.20.18'; + const VERSION = '6.20.19'; /** * The base path for the Laravel installation. From acb117738ddb28ea7fc6a2029cf1d0d06c4b2b57 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Tue, 16 Mar 2021 19:05:22 +0200 Subject: [PATCH 88/91] [8.x] Add DB::resetRecordsModifications (#36617) * add DB::resetRecordsModifications * Update Connection.php Co-authored-by: Taylor Otwell --- src/Illuminate/Database/Connection.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index b4fa6d4c3a0d..b2ded4c9aebb 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -867,6 +867,16 @@ public function recordsHaveBeenModified($value = true) } } + /** + * Reset the record modification state. + * + * @return void + */ + public function forgetRecordModificationState() + { + $this->recordsModified = false; + } + /** * Is Doctrine available? * From 9f986cef11b959679f530eb24d929b39a2690924 Mon Sep 17 00:00:00 2001 From: Tetiana Blindaruk Date: Tue, 16 Mar 2021 19:40:07 +0200 Subject: [PATCH 89/91] [8.x] update changelog --- CHANGELOG-8.x.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/CHANGELOG-8.x.md b/CHANGELOG-8.x.md index e5ca8f3668f8..399b85419411 100644 --- a/CHANGELOG-8.x.md +++ b/CHANGELOG-8.x.md @@ -1,6 +1,26 @@ # Release Notes for 8.x -## [Unreleased](https://github.com/laravel/framework/compare/v8.32.1...8.x) +## [Unreleased](https://github.com/laravel/framework/compare/v8.33.0...8.x) + + +## [v8.33.0 (2021-03-16)](https://github.com/laravel/framework/compare/v8.32.1...v8.33.0) + +### Added +- Added broken pipe exception as lost connection error ([#36601](https://github.com/laravel/framework/pull/36601)) +- Added missing option to resource ([#36562](https://github.com/laravel/framework/pull/36562)) +- Introduce StringEncrypter interface ([#36578](https://github.com/laravel/framework/pull/36578)) + +### Fixed +- Fixed returns with Mail & Notification components ([#36559](https://github.com/laravel/framework/pull/36559)) +- Stack driver fix: respect the defined processors in LogManager ([#36591](https://github.com/laravel/framework/pull/36591)) +- Require the correct password to rehash it when logging out other devices ([#36608](https://github.com/laravel/framework/pull/36608), [1e61612](https://github.com/laravel/framework/commit/1e6161250074b8106c1fcf153eeaef7c0bf74c6c)) + +### Changed +- Allow nullable columns for `AsArrayObject/AsCollection` casts ([#36526](https://github.com/laravel/framework/pull/36526)) +- Accept callable class for reportable and renderable in exception handler ([#36551](https://github.com/laravel/framework/pull/36551)) +- Container - detect circular dependencies ([dd7274d](https://github.com/laravel/framework/commit/dd7274d23a9ee58cc1abdf7107403169a3994b68), [a712f72](https://github.com/laravel/framework/commit/a712f72ca88f709335576530b31635738abd4c89), [6f9bb4c](https://github.com/laravel/framework/commit/6f9bb4cdd84295cbcf7908cc4b4684f47f38b8cf)) +- Initialize CronExpression class using new keyword ([#36600](https://github.com/laravel/framework/pull/36600)) +- Use different config key for overriding temporary url host in AwsTemporaryUrl method ([#36612](https://github.com/laravel/framework/pull/36612)) ## [v8.32.1 (2021-03-09)](https://github.com/laravel/framework/compare/v8.32.0...v8.32.1) From 332844e5bde34f8db91aeca4d21cd4e0925d691e Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 16 Mar 2021 14:42:13 -0500 Subject: [PATCH 90/91] revert circular dep --- src/Illuminate/Container/Container.php | 6 +++--- tests/Container/ContainerTest.php | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 4ea378983d0d..642d1e082e6d 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -842,9 +842,9 @@ public function build($concrete) return $this->notInstantiable($concrete); } - if (in_array($concrete, $this->buildStack)) { - throw new CircularDependencyException("Circular dependency detected while resolving [{$concrete}]."); - } + // if (in_array($concrete, $this->buildStack)) { + // throw new CircularDependencyException("Circular dependency detected while resolving [{$concrete}]."); + // } $this->buildStack[] = $concrete; diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 39638694f802..eefe9366cbfc 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -564,13 +564,13 @@ public function testContainerCanResolveClasses() $this->assertInstanceOf(ContainerConcreteStub::class, $class); } - public function testContainerCanCatchCircularDependency() - { - $this->expectException(CircularDependencyException::class); + // public function testContainerCanCatchCircularDependency() + // { + // $this->expectException(CircularDependencyException::class); - $container = new Container; - $container->get(CircularAStub::class); - } + // $container = new Container; + // $container->get(CircularAStub::class); + // } } class CircularAStub From 354c57b8cb457549114074c500944455a288d6cc Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Tue, 16 Mar 2021 14:42:32 -0500 Subject: [PATCH 91/91] patch --- src/Illuminate/Foundation/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 840265edce7b..cbd1cba90c73 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -33,7 +33,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '8.33.0'; + const VERSION = '8.33.1'; /** * The base path for the Laravel installation.