diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7b91b6fe781..ffe3c5a7b2e4 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: @@ -63,11 +63,20 @@ jobs: max_attempts: 5 command: composer update --${{ matrix.stability }} --prefer-dist --no-interaction --no-progress + - name: Setup DynamoDB Local + uses: rrainn/dynamodb-action@v2.0.0 + with: + port: 8888 + - name: Execute tests run: vendor/bin/phpunit --verbose env: DB_PORT: ${{ job.services.mysql.ports[3306] }} DB_USERNAME: root + DYNAMODB_CACHE_TABLE: laravel_dynamodb_test + DYNAMODB_ENDPOINT: "http://localhost:8888" + AWS_ACCESS_KEY_ID: random_key + AWS_SECRET_ACCESS_KEY: random_secret windows_tests: runs-on: windows-latest diff --git a/CHANGELOG-6.x.md b/CHANGELOG-6.x.md index 860012f0bd0a..1190419cef6e 100644 --- a/CHANGELOG-6.x.md +++ b/CHANGELOG-6.x.md @@ -1,6 +1,64 @@ # 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.25...6.x) + + +## [v6.20.25 (2021-04-27)](https://github.com/laravel/framework/compare/v6.20.24...v6.20.25) + +### Fixed +- Fixed nullable values for required_if ([#37128](https://github.com/laravel/framework/pull/37128), [86fd558](https://github.com/laravel/framework/commit/86fd558b4e5d8d7d45cf457cd1a72d54334297a1)) + + +## [v6.20.24 (2021-04-20)](https://github.com/laravel/framework/compare/v6.20.23...v6.20.24) + +### Fixed +- Fixed required_if boolean validation ([#36969](https://github.com/laravel/framework/pull/36969)) + + +## [v6.20.23 (2021-04-13)](https://github.com/laravel/framework/compare/v6.20.22...v6.20.23) + +### Added +- Added strings to the `DetectsLostConnections.php` ([4210258](https://github.com/laravel/framework/commit/42102589bc7f7b8533ee1b815ef0cc18017d4e45)) + + +## [v6.20.22 (2021-03-31)](https://github.com/laravel/framework/compare/v6.20.21...v6.20.22) + +### Fixed +- Fixed setting DynamoDB credentials ([#36822](https://github.com/laravel/framework/pull/36822)) + + +## [v6.20.21 (2021-03-30)](https://github.com/laravel/framework/compare/v6.20.20...v6.20.21) + +### Added +- Added support of DynamoDB in CI suite ([#36749](https://github.com/laravel/framework/pull/36749)) +- Support username parameter for predis ([#36762](https://github.com/laravel/framework/pull/36762)) + +### Changed +- Use qualified column names in pivot query ([#36720](https://github.com/laravel/framework/pull/36720)) + + +## [v6.20.20 (2021-03-23)](https://github.com/laravel/framework/compare/v6.20.19...v6.20.20) + +### Added +- Added WSREP communication link failure for lost connection detection ([#36668](https://github.com/laravel/framework/pull/36668)) + +### Fixed +- Fixes the issue using cache:clear with PhpRedis and a clustered Redis instance. ([#36665](https://github.com/laravel/framework/pull/36665)) + + +## [v6.20.19 (2021-03-16)](https://github.com/laravel/framework/compare/v6.20.18...v6.20.19) + +### Added +- Added broken pipe exception as lost connection error ([#36601](https://github.com/laravel/framework/pull/36601)) + + +## [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) diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 33d1027bce1a..1f514369d34e 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -225,6 +225,27 @@ protected function createDatabaseDriver(array $config) * @return \Illuminate\Cache\Repository */ protected function createDynamodbDriver(array $config) + { + $client = $this->newDynamodbClient($config); + + return $this->repository( + new DynamoDbStore( + $client, + $config['table'], + $config['attributes']['key'] ?? 'key', + $config['attributes']['value'] ?? 'value', + $config['attributes']['expiration'] ?? 'expires_at', + $this->getPrefix($config) + ) + ); + } + + /** + * Create new DynamoDb Client instance. + * + * @return DynamoDbClient + */ + protected function newDynamodbClient(array $config) { $dynamoConfig = [ 'region' => $config['region'], @@ -232,22 +253,13 @@ protected function createDynamodbDriver(array $config) 'endpoint' => $config['endpoint'] ?? null, ]; - if ($config['key'] && $config['secret']) { + if (isset($config['key']) && isset($config['secret'])) { $dynamoConfig['credentials'] = Arr::only( $config, ['key', 'secret', 'token'] ); } - return $this->repository( - new DynamoDbStore( - new DynamoDbClient($dynamoConfig), - $config['table'], - $config['attributes']['key'] ?? 'key', - $config['attributes']['value'] ?? 'value', - $config['attributes']['expiration'] ?? 'expires_at', - $this->getPrefix($config) - ) - ); + return new DynamoDbClient($dynamoConfig); } /** diff --git a/src/Illuminate/Cache/DynamoDbStore.php b/src/Illuminate/Cache/DynamoDbStore.php index 4e663db4108a..aa28a789fa36 100644 --- a/src/Illuminate/Cache/DynamoDbStore.php +++ b/src/Illuminate/Cache/DynamoDbStore.php @@ -525,4 +525,14 @@ public function setPrefix($prefix) { $this->prefix = ! empty($prefix) ? $prefix.':' : ''; } + + /** + * Get the DynamoDb Client instance. + * + * @return DynamoDbClient + */ + public function getClient() + { + return $this->dynamo; + } } diff --git a/src/Illuminate/Database/DetectsLostConnections.php b/src/Illuminate/Database/DetectsLostConnections.php index 1ecfc96140f4..93be53b2fdc9 100644 --- a/src/Illuminate/Database/DetectsLostConnections.php +++ b/src/Illuminate/Database/DetectsLostConnections.php @@ -50,6 +50,10 @@ 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', + 'SQLSTATE[08S01]: Communication link failure', + 'SQLSTATE[08006] [7] could not connect to server: Connection refused Is the server running on host', + 'SQLSTATE[HY000]: General error: 7 SSL SYSCALL error: No route to host', ]); } } diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index c6812b75a150..b0abf2848590 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -429,7 +429,7 @@ public function detach($ids = null, $touch = true) return 0; } - $query->whereIn($this->relatedPivotKey, (array) $ids); + $query->whereIn($this->getQualifiedRelatedPivotKeyName(), (array) $ids); } // Once we have all of the conditions set on the statement, we are ready @@ -544,7 +544,7 @@ public function newPivotQuery() $query->whereIn(...$arguments); } - return $query->where($this->foreignPivotKey, $this->parent->{$this->parentKey}); + return $query->where($this->getQualifiedForeignPivotKeyName(), $this->parent->{$this->parentKey}); } /** diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index f0a0bfc5190b..88a7df3dc5e9 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -60,8 +60,8 @@ protected function compileColumns(Builder $query, $columns) // If there is a limit on the query, but not an offset, we will add the top // clause to the query, which serves as a "limit" type clause within the // SQL Server system similar to the limit keywords available in MySQL. - if ($query->limit > 0 && $query->offset <= 0) { - $select .= 'top '.$query->limit.' '; + if (is_numeric($query->limit) && $query->limit > 0 && $query->offset <= 0) { + $select .= 'top '.((int) $query->limit).' '; } return $select.$this->columnize($columns); @@ -221,10 +221,10 @@ protected function compileTableExpression($sql, $query) */ protected function compileRowConstraint($query) { - $start = $query->offset + 1; + $start = (int) $query->offset + 1; if ($query->limit > 0) { - $finish = $query->offset + $query->limit; + $finish = (int) $query->offset + (int) $query->limit; return "between {$start} and {$finish}"; } diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 1f46747dbfbd..14872999d6c4 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.26'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Redis/Connections/PhpRedisConnection.php b/src/Illuminate/Redis/Connections/PhpRedisConnection.php index 0c4015df9880..7e9af99ff29e 100644 --- a/src/Illuminate/Redis/Connections/PhpRedisConnection.php +++ b/src/Illuminate/Redis/Connections/PhpRedisConnection.php @@ -484,14 +484,8 @@ public function flushdb() return $this->command('flushdb'); } - foreach ($this->client->_masters() as [$host, $port]) { - $redis = tap(new Redis)->connect($host, $port); - - if (isset($this->config['password']) && ! empty($this->config['password'])) { - $redis->auth($this->config['password']); - } - - $redis->flushDb(); + foreach ($this->client->_masters() as $master) { + $this->client->flushDb($master); } } diff --git a/src/Illuminate/Redis/RedisManager.php b/src/Illuminate/Redis/RedisManager.php index b5d98203c180..8a6c88d1c2ba 100644 --- a/src/Illuminate/Redis/RedisManager.php +++ b/src/Illuminate/Redis/RedisManager.php @@ -192,7 +192,7 @@ protected function parseConnectionConfiguration($config) } return array_filter($parsed, function ($key) { - return ! in_array($key, ['driver', 'username'], true); + return ! in_array($key, ['driver'], true); }, ARRAY_FILTER_USE_KEY); } 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) diff --git a/src/Illuminate/Validation/Concerns/FormatsMessages.php b/src/Illuminate/Validation/Concerns/FormatsMessages.php index 9d011892aaf7..737d4173d174 100644 --- a/src/Illuminate/Validation/Concerns/FormatsMessages.php +++ b/src/Illuminate/Validation/Concerns/FormatsMessages.php @@ -336,6 +336,10 @@ public function getDisplayableValue($attribute, $value) return $value ? 'true' : 'false'; } + if (is_null($value)) { + return 'empty'; + } + return $value; } diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 13fe1a648108..2ddbff3f0365 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -1420,9 +1420,13 @@ public function validateRequiredIf($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'required_if'); + if (! Arr::has($this->data, $parameters[0])) { + return true; + } + [$values, $other] = $this->prepareValuesAndOther($parameters); - if (in_array($other, $values, is_bool($other))) { + if (in_array($other, $values, is_bool($other) || is_null($other))) { return $this->validateRequired($attribute, $value); } @@ -1441,9 +1445,13 @@ public function validateExcludeIf($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'exclude_if'); + if (! Arr::has($this->data, $parameters[0])) { + return true; + } + [$values, $other] = $this->prepareValuesAndOther($parameters); - return ! in_array($other, $values, is_bool($other)); + return ! in_array($other, $values, is_bool($other) || is_null($other)); } /** @@ -1458,9 +1466,38 @@ public function validateExcludeUnless($attribute, $value, $parameters) { $this->requireParameterCount(2, $parameters, 'exclude_unless'); + if (! Arr::has($this->data, $parameters[0])) { + return true; + } + [$values, $other] = $this->prepareValuesAndOther($parameters); - return in_array($other, $values, is_bool($other)); + return in_array($other, $values, is_bool($other) || is_null($other)); + } + + /** + * Validate that an attribute exists when another attribute does not have a given value. + * + * @param string $attribute + * @param mixed $value + * @param mixed $parameters + * @return bool + */ + public function validateRequiredUnless($attribute, $value, $parameters) + { + $this->requireParameterCount(2, $parameters, 'required_unless'); + + if (! Arr::has($this->data, $parameters[0])) { + return true; + } + + [$values, $other] = $this->prepareValuesAndOther($parameters); + + if (! in_array($other, $values, is_bool($other) || is_null($other))) { + return $this->validateRequired($attribute, $value); + } + + return true; } /** @@ -1475,13 +1512,28 @@ protected function prepareValuesAndOther($parameters) $values = array_slice($parameters, 1); - if (is_bool($other)) { + if ($this->shouldConvertToBoolean($parameters[0]) || is_bool($other)) { $values = $this->convertValuesToBoolean($values); } + if (is_null($other)) { + $values = $this->convertValuesToNull($values); + } + return [$values, $other]; } + /** + * Check if parameter should be converted to boolean. + * + * @param string $parameter + * @return bool + */ + protected function shouldConvertToBoolean($parameter) + { + return in_array('boolean', Arr::get($this->rules, $parameter, [])); + } + /** * Convert the given values to boolean if they are string "true" / "false". * @@ -1502,24 +1554,16 @@ protected function convertValuesToBoolean($values) } /** - * Validate that an attribute exists when another attribute does not have a given value. + * Convert the given values to null if they are string "null". * - * @param string $attribute - * @param mixed $value - * @param mixed $parameters - * @return bool + * @param array $values + * @return array */ - public function validateRequiredUnless($attribute, $value, $parameters) + protected function convertValuesToNull($values) { - $this->requireParameterCount(2, $parameters, 'required_unless'); - - [$values, $other] = $this->prepareValuesAndOther($parameters); - - if (! in_array($other, $values, is_bool($other))) { - return $this->validateRequired($attribute, $value); - } - - return true; + return array_map(function ($value) { + return Str::lower($value) === 'null' ? null : $value; + }, $values); } /** diff --git a/tests/Database/DatabaseEloquentMorphToManyTest.php b/tests/Database/DatabaseEloquentMorphToManyTest.php index 0b4a7f511074..dac94e258c39 100644 --- a/tests/Database/DatabaseEloquentMorphToManyTest.php +++ b/tests/Database/DatabaseEloquentMorphToManyTest.php @@ -47,9 +47,9 @@ public function testDetachRemovesPivotTableRecord() $relation = $this->getMockBuilder(MorphToMany::class)->setMethods(['touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); $query = m::mock(stdClass::class); $query->shouldReceive('from')->once()->with('taggables')->andReturn($query); - $query->shouldReceive('where')->once()->with('taggable_id', 1)->andReturn($query); + $query->shouldReceive('where')->once()->with('taggables.taggable_id', 1)->andReturn($query); $query->shouldReceive('where')->once()->with('taggable_type', get_class($relation->getParent()))->andReturn($query); - $query->shouldReceive('whereIn')->once()->with('tag_id', [1, 2, 3]); + $query->shouldReceive('whereIn')->once()->with('taggables.tag_id', [1, 2, 3]); $query->shouldReceive('delete')->once()->andReturn(true); $relation->getQuery()->shouldReceive('getQuery')->andReturn($mockQueryBuilder = m::mock(stdClass::class)); $mockQueryBuilder->shouldReceive('newQuery')->once()->andReturn($query); @@ -63,7 +63,7 @@ public function testDetachMethodClearsAllPivotRecordsWhenNoIDsAreGiven() $relation = $this->getMockBuilder(MorphToMany::class)->setMethods(['touchIfTouching'])->setConstructorArgs($this->getRelationArguments())->getMock(); $query = m::mock(stdClass::class); $query->shouldReceive('from')->once()->with('taggables')->andReturn($query); - $query->shouldReceive('where')->once()->with('taggable_id', 1)->andReturn($query); + $query->shouldReceive('where')->once()->with('taggables.taggable_id', 1)->andReturn($query); $query->shouldReceive('where')->once()->with('taggable_type', get_class($relation->getParent()))->andReturn($query); $query->shouldReceive('whereIn')->never(); $query->shouldReceive('delete')->once()->andReturn(true); diff --git a/tests/Integration/Cache/DynamoDbStoreTest.php b/tests/Integration/Cache/DynamoDbStoreTest.php index 74897fbde8cb..318db8a1ce51 100644 --- a/tests/Integration/Cache/DynamoDbStoreTest.php +++ b/tests/Integration/Cache/DynamoDbStoreTest.php @@ -2,6 +2,9 @@ namespace Illuminate\Tests\Integration\Cache; +use Aws\DynamoDb\DynamoDbClient; +use Aws\Exception\AwsException; +use Illuminate\Contracts\Cache\Repository; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; @@ -13,11 +16,11 @@ class DynamoDbStoreTest extends TestCase { protected function setUp(): void { - parent::setUp(); - if (! env('DYNAMODB_CACHE_TABLE')) { $this->markTestSkipped('DynamoDB not configured.'); } + + parent::setUp(); } public function testItemsCanBeStoredAndRetrieved() @@ -74,15 +77,63 @@ public function testLocksCanBeAcquired() */ protected function getEnvironmentSetUp($app) { + if (! env('DYNAMODB_CACHE_TABLE')) { + $this->markTestSkipped('DynamoDB not configured.'); + } + $app['config']->set('cache.default', 'dynamodb'); - $app['config']->set('cache.stores.dynamodb', [ - 'driver' => 'dynamodb', - 'key' => env('AWS_ACCESS_KEY_ID'), - 'secret' => env('AWS_SECRET_ACCESS_KEY'), - 'region' => 'us-east-1', - 'table' => env('DYNAMODB_CACHE_TABLE', 'laravel_test'), - 'endpoint' => env('DYNAMODB_ENDPOINT'), + $config = $app['config']->get('cache.stores.dynamodb'); + + /** @var \Aws\DynamoDb\DynamoDbClient $client */ + $client = $app->make(Repository::class)->getStore()->getClient(); + + if ($this->dynamoTableExists($client, $config['table'])) { + return; + } + + $client->createTable([ + 'TableName' => $config['table'], + 'KeySchema' => [ + [ + 'AttributeName' => $config['attributes']['key'] ?? 'key', + 'KeyType' => 'HASH', + ], + ], + 'AttributeDefinitions' => [ + [ + 'AttributeName' => $config['attributes']['key'] ?? 'key', + 'AttributeType' => 'S', + ], + ], + 'ProvisionedThroughput' => [ + 'ReadCapacityUnits' => 1, + 'WriteCapacityUnits' => 1, + ], ]); } + + /** + * Determine if the given DynamoDB table exists. + * + * @param \Aws\DynamoDb\DynamoDbClient $client + * @param string $table + * @return bool + */ + public function dynamoTableExists(DynamoDbClient $client, $table) + { + try { + $client->describeTable([ + 'TableName' => $table, + ]); + + return true; + } catch (AwsException $e) { + if (Str::contains($e->getAwsErrorMessage(), ['resource not found', 'Cannot do operations on a non-existent table'])) { + return false; + } + + throw $e; + } + } } diff --git a/tests/Redis/RedisConnectorTest.php b/tests/Redis/RedisConnectorTest.php index 599fa2f2aad3..0203ed9d9b82 100644 --- a/tests/Redis/RedisConnectorTest.php +++ b/tests/Redis/RedisConnectorTest.php @@ -160,4 +160,27 @@ public function testScheme() $this->assertEquals("tcp://{$host}", $phpRedisClient->getHost()); $this->assertEquals($port, $phpRedisClient->getPort()); } + + public function testPredisConfigurationWithUsername() + { + $host = env('REDIS_HOST', '127.0.0.1'); + $port = env('REDIS_PORT', 6379); + $username = 'testuser'; + $password = 'testpw'; + + $predis = new RedisManager(new Application, 'predis', [ + 'default' => [ + 'host' => $host, + 'port' => $port, + 'username' => $username, + 'password' => $password, + 'database' => 5, + 'timeout' => 0.5, + ], + ]); + $predisClient = $predis->connection()->client(); + $parameters = $predisClient->getConnection()->getParameters(); + $this->assertEquals($username, $parameters->username); + $this->assertEquals($password, $parameters->password); + } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 4ac71213c984..b90b7cfdb373 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -1090,6 +1090,48 @@ public function testRequiredIf() $v = new Validator($trans, ['first' => 'dayle', 'last' => ''], ['last' => 'RequiredIf:first,taylor,dayle']); $this->assertFalse($v->passes()); $this->assertSame('The last field is required when first is dayle.', $v->messages()->first('last')); + + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.required_if' => 'The :attribute field is required when :other is :value.'], 'en'); + $v = new Validator($trans, ['foo' => 0], [ + 'foo' => 'nullable|required|boolean', + 'bar' => 'required_if:foo,true', + 'baz' => 'required_if:foo,false', + ]); + $this->assertTrue($v->fails()); + $this->assertCount(1, $v->messages()); + $this->assertSame('The baz field is required when foo is 0.', $v->messages()->first('baz')); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, [], [ + 'foo' => 'nullable|boolean', + 'baz' => 'nullable|required_if:foo,false', + ]); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, ['foo' => null], [ + 'foo' => 'nullable|boolean', + 'baz' => 'nullable|required_if:foo,false', + ]); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $v = new Validator($trans, [], [ + 'foo' => 'nullable|boolean', + 'baz' => 'nullable|required_if:foo,null', + ]); + $this->assertTrue($v->passes()); + + $trans = $this->getIlluminateArrayTranslator(); + $trans->addLines(['validation.required_if' => 'The :attribute field is required when :other is :value.'], 'en'); + $v = new Validator($trans, ['foo' => null], [ + 'foo' => 'nullable|boolean', + 'baz' => 'nullable|required_if:foo,null', + ]); + $this->assertTrue($v->fails()); + $this->assertCount(1, $v->messages()); + $this->assertSame('The baz field is required when foo is empty.', $v->messages()->first('baz')); } public function testRequiredUnless()