diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 5c1d9fd37e68..6bfbdb537e98 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -125,7 +125,7 @@ public static function phpBinary() */ public static function artisanBinary() { - return defined('ARTISAN_BINARY') ? ProcessUtils::escapeArgument(ARTISAN_BINARY) : 'artisan'; + return ProcessUtils::escapeArgument(defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan'); } /** diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 18abb917d570..39a73805abb1 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -73,6 +73,43 @@ class Builder implements BuilderContract */ protected $onDelete; + /** + * The properties that should be returned from query builder. + * + * @var string[] + */ + protected $propertyPassthru = [ + 'from', + ]; + + /** + * The methods that should be returned from query builder. + * + * @var string[] + */ + protected $passthru = [ + 'aggregate', + 'average', + 'avg', + 'count', + 'dd', + 'doesntExist', + 'dump', + 'exists', + 'getBindings', + 'getConnection', + 'getGrammar', + 'insert', + 'insertGetId', + 'insertOrIgnore', + 'insertUsing', + 'max', + 'min', + 'raw', + 'sum', + 'toSql', + ]; + /** * Applied global scopes. * @@ -1572,6 +1609,10 @@ public function __get($key) return new HigherOrderBuilderProxy($this, $key); } + if (in_array($key, $this->propertyPassthru)) { + return $this->toBase()->{$key}; + } + throw new Exception("Property [{$key}] does not exist on the Eloquent builder instance."); } diff --git a/src/Illuminate/Database/Eloquent/SoftDeletes.php b/src/Illuminate/Database/Eloquent/SoftDeletes.php index cac971ccd59b..aa6c81784972 100644 --- a/src/Illuminate/Database/Eloquent/SoftDeletes.php +++ b/src/Illuminate/Database/Eloquent/SoftDeletes.php @@ -64,9 +64,9 @@ public function forceDelete() protected function performDeleteOnModel() { if ($this->forceDeleting) { - $this->exists = false; - - return $this->setKeysForSaveQuery($this->newModelQuery())->forceDelete(); + return tap($this->setKeysForSaveQuery($this->newModelQuery())->forceDelete(), function () { + $this->exists = false; + }); } return $this->runSoftDelete(); diff --git a/src/Illuminate/Database/Schema/ColumnDefinition.php b/src/Illuminate/Database/Schema/ColumnDefinition.php index 5f3be6170114..8c13b5cf4265 100644 --- a/src/Illuminate/Database/Schema/ColumnDefinition.php +++ b/src/Illuminate/Database/Schema/ColumnDefinition.php @@ -17,6 +17,7 @@ * @method $this from(int $startingValue) Set the starting value of an auto-incrementing field (MySQL / PostgreSQL) * @method $this generatedAs(string|Expression $expression = null) Create a SQL compliant identity column (PostgreSQL) * @method $this index(string $indexName = null) Add an index + * @method $this invisible() Specify that the column should be invisible to "SELECT *" (MySQL) * @method $this nullable(bool $value = true) Allow NULL values to be inserted into the column * @method $this persisted() Mark the computed generated column as persistent (SQL Server) * @method $this primary() Add a primary index diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 91efaae141cc..8c53f003b059 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -15,7 +15,7 @@ class MySqlGrammar extends Grammar * @var string[] */ protected $modifiers = [ - 'Unsigned', 'Charset', 'Collate', 'VirtualAs', 'StoredAs', 'Nullable', + 'Unsigned', 'Charset', 'Collate', 'VirtualAs', 'StoredAs', 'Nullable', 'Invisible', 'Srid', 'Default', 'Increment', 'Comment', 'After', 'First', ]; @@ -1057,6 +1057,20 @@ protected function modifyNullable(Blueprint $blueprint, Fluent $column) } } + /** + * Get the SQL for an invisible column modifier. + * + * @param \Illuminate\Database\Schema\Blueprint $blueprint + * @param \Illuminate\Support\Fluent $column + * @return string|null + */ + protected function modifyInvisible(Blueprint $blueprint, Fluent $column) + { + if (! is_null($column->invisible)) { + return ' invisible'; + } + } + /** * Get the SQL for a default column modifier. * diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 1e0c237b6ba7..22cfd7055c08 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -249,7 +249,7 @@ public function bootstrapWith(array $bootstrappers) */ public function afterLoadingEnvironment(Closure $callback) { - return $this->afterBootstrapping( + $this->afterBootstrapping( LoadEnvironmentVariables::class, $callback ); } diff --git a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php index 391356c0b176..d1dfa96f59e2 100644 --- a/src/Illuminate/Foundation/Console/ComponentMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ComponentMakeCommand.php @@ -130,7 +130,20 @@ protected function getView() */ protected function getStub() { - return __DIR__.'/stubs/view-component.stub'; + return $this->resolveStubPath('/stubs/view-component.stub'); + } + + /** + * Resolve the fully-qualified path to the stub. + * + * @param string $stub + * @return string + */ + protected function resolveStubPath($stub) + { + return file_exists($customPath = $this->laravel->basePath(trim($stub, '/'))) + ? $customPath + : __DIR__.$stub; } /** diff --git a/src/Illuminate/Foundation/Console/StubPublishCommand.php b/src/Illuminate/Foundation/Console/StubPublishCommand.php index 13b03ccf3f62..3db2b9e56f7b 100644 --- a/src/Illuminate/Foundation/Console/StubPublishCommand.php +++ b/src/Illuminate/Foundation/Console/StubPublishCommand.php @@ -65,6 +65,7 @@ public function handle() __DIR__.'/stubs/scope.stub' => $stubsPath.'/scope.stub', __DIR__.'/stubs/test.stub' => $stubsPath.'/test.stub', __DIR__.'/stubs/test.unit.stub' => $stubsPath.'/test.unit.stub', + __DIR__.'/stubs/view-component.stub' => $stubsPath.'/view-component.stub', realpath(__DIR__.'/../../Database/Console/Factories/stubs/factory.stub') => $stubsPath.'/factory.stub', realpath(__DIR__.'/../../Database/Console/Seeds/stubs/seeder.stub') => $stubsPath.'/seeder.stub', realpath(__DIR__.'/../../Database/Migrations/stubs/migration.create.stub') => $stubsPath.'/migration.create.stub', diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 36cfea5a4b1f..5416d62b8423 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -889,6 +889,24 @@ public static function substrCount($haystack, $needle, $offset = 0, $length = nu } } + /** + * Replace text within a portion of a string. + * + * @param string|array $string + * @param string|array $replace + * @param array|int $offset + * @param array|int|null $length + * @return string|array + */ + public static function substrReplace($string, $replace, $offset = 0, $length = null) + { + if ($length === null) { + $length = strlen($string); + } + + return substr_replace($string, $replace, $offset, $length); + } + /** * Make a string's first character uppercase. * diff --git a/src/Illuminate/Support/Stringable.php b/src/Illuminate/Support/Stringable.php index 1927112c60b2..f22727578a68 100644 --- a/src/Illuminate/Support/Stringable.php +++ b/src/Illuminate/Support/Stringable.php @@ -709,6 +709,19 @@ public function substrCount($needle, $offset = null, $length = null) return Str::substrCount($this->value, $needle, $offset ?? 0, $length); } + /** + * Replace text within a portion of a string. + * + * @param string|array $replace + * @param array|int $offset + * @param array|int|null $length + * @return string|array + */ + public function substrReplace($replace, $offset = 0, $length = null) + { + return new static(Str::substrReplace($this->value, $replace, $offset, $length)); + } + /** * Trim the string of the given characters. * diff --git a/src/Illuminate/Validation/Rules/Password.php b/src/Illuminate/Validation/Rules/Password.php index 829c2d823109..cce6097f3a5e 100644 --- a/src/Illuminate/Validation/Rules/Password.php +++ b/src/Illuminate/Validation/Rules/Password.php @@ -270,36 +270,39 @@ public function passes($attribute, $value) { $this->messages = []; - $validator = Validator::make($this->data, [ - $attribute => 'string|min:'.$this->min, - ], $this->validator->customMessages, $this->validator->customAttributes); + $validator = Validator::make( + $this->data, + [$attribute => 'string|min:'.$this->min], + $this->validator->customMessages, + $this->validator->customAttributes + )->after(function ($validator) use ($attribute, $value) { + if (! is_string($value)) { + return; + } + + $value = (string) $value; + + if ($this->mixedCase && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { + $validator->errors()->add($attribute, 'The :attribute must contain at least one uppercase and one lowercase letter.'); + } + + if ($this->letters && ! preg_match('/\pL/u', $value)) { + $validator->errors()->add($attribute, 'The :attribute must contain at least one letter.'); + } + + if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/u', $value)) { + $validator->errors()->add($attribute, 'The :attribute must contain at least one symbol.'); + } + + if ($this->numbers && ! preg_match('/\pN/u', $value)) { + $validator->errors()->add($attribute, 'The :attribute must contain at least one number.'); + } + }); if ($validator->fails()) { return $this->fail($validator->messages()->all()); } - $value = (string) $value; - - if ($this->mixedCase && ! preg_match('/(\p{Ll}+.*\p{Lu})|(\p{Lu}+.*\p{Ll})/u', $value)) { - $this->fail('The :attribute must contain at least one uppercase and one lowercase letter.'); - } - - if ($this->letters && ! preg_match('/\pL/u', $value)) { - $this->fail('The :attribute must contain at least one letter.'); - } - - if ($this->symbols && ! preg_match('/\p{Z}|\p{S}|\p{P}/u', $value)) { - $this->fail('The :attribute must contain at least one symbol.'); - } - - if ($this->numbers && ! preg_match('/\pN/u', $value)) { - $this->fail('The :attribute must contain at least one number.'); - } - - if (! empty($this->messages)) { - return false; - } - if ($this->uncompromised && ! Container::getInstance()->make(UncompromisedVerifier::class)->verify([ 'value' => $value, 'threshold' => $this->compromisedThreshold, diff --git a/tests/Console/ConsoleEventSchedulerTest.php b/tests/Console/ConsoleEventSchedulerTest.php index 19c87e6e3249..66a6845f6ad7 100644 --- a/tests/Console/ConsoleEventSchedulerTest.php +++ b/tests/Console/ConsoleEventSchedulerTest.php @@ -101,9 +101,10 @@ public function testCommandCreatesNewArtisanCommand() $events = $schedule->events(); $binary = $escape.PHP_BINARY.$escape; - $this->assertEquals($binary.' artisan queue:listen', $events[0]->command); - $this->assertEquals($binary.' artisan queue:listen --tries=3', $events[1]->command); - $this->assertEquals($binary.' artisan queue:listen --tries=3', $events[2]->command); + $artisan = $escape.'artisan'.$escape; + $this->assertEquals($binary.' '.$artisan.' queue:listen', $events[0]->command); + $this->assertEquals($binary.' '.$artisan.' queue:listen --tries=3', $events[1]->command); + $this->assertEquals($binary.' '.$artisan.' queue:listen --tries=3', $events[2]->command); } public function testCreateNewArtisanCommandUsingCommandClass() @@ -115,7 +116,8 @@ public function testCreateNewArtisanCommandUsingCommandClass() $events = $schedule->events(); $binary = $escape.PHP_BINARY.$escape; - $this->assertEquals($binary.' artisan foo:bar --force', $events[0]->command); + $artisan = $escape.'artisan'.$escape; + $this->assertEquals($binary.' '.$artisan.' foo:bar --force', $events[0]->command); } public function testCallCreatesNewJobWithTimezone() diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 02af03bfe628..e84392a635cc 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -46,7 +46,7 @@ public function testBuildCommandInBackgroundUsingUnix() $scheduleId = '"framework'.DIRECTORY_SEPARATOR.'schedule-eeb46c93d45e928d62aaf684d727e213b7094822"'; - $this->assertSame("(php -i > '/dev/null' 2>&1 ; '".PHP_BINARY."' artisan schedule:finish {$scheduleId} \"$?\") > '/dev/null' 2>&1 &", $event->buildCommand()); + $this->assertSame("(php -i > '/dev/null' 2>&1 ; '".PHP_BINARY."' 'artisan' schedule:finish {$scheduleId} \"$?\") > '/dev/null' 2>&1 &", $event->buildCommand()); } /** diff --git a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php index 9bc91cbbb882..786c9c35242b 100644 --- a/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php +++ b/tests/Database/DatabaseEloquentSoftDeletesIntegrationTest.php @@ -11,6 +11,7 @@ use Illuminate\Pagination\CursorPaginator; use Illuminate\Pagination\Paginator; use Illuminate\Support\Carbon; +use Mockery; use PHPUnit\Framework\TestCase; class DatabaseEloquentSoftDeletesIntegrationTest extends TestCase @@ -189,6 +190,42 @@ public function testForceDeleteActuallyDeletesRecords() $this->assertEquals(1, $users->first()->id); } + public function testForceDeleteUpdateExistsProperty() + { + $this->createUsers(); + $user = SoftDeletesTestUser::find(2); + + $this->assertTrue($user->exists); + + $user->forceDelete(); + + $this->assertFalse($user->exists); + } + + public function testForceDeleteDoesntUpdateExistsPropertyIfFailed() + { + $user = new class() extends SoftDeletesTestUser + { + public $exists = true; + + public function newModelQuery() + { + return Mockery::spy(parent::newModelQuery(), function (Mockery\MockInterface $mock) { + $mock->shouldReceive('forceDelete')->andThrow(new \Exception()); + }); + } + }; + + $this->assertTrue($user->exists); + + try { + $user->forceDelete(); + } catch (\Exception $exception) { + } + + $this->assertTrue($user->exists); + } + public function testRestoreRestoresRecords() { $this->createUsers(); diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index 3215ec174e03..bbe696fdb21d 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -560,6 +560,16 @@ public function testAddingGeneratedColumnWithCharset() $this->assertSame('alter table `links` add `url` varchar(2083) character set ascii not null, add `url_hash_virtual` varchar(64) character set ascii as (sha2(url, 256)), add `url_hash_stored` varchar(64) character set ascii as (sha2(url, 256)) stored', $statements[0]); } + public function testAddingInvisibleColumn() + { + $blueprint = new Blueprint('users'); + $blueprint->string('secret', 64)->nullable(false)->invisible(); + $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `secret` varchar(64) not null invisible', $statements[0]); + } + public function testAddingString() { $blueprint = new Blueprint('users'); diff --git a/tests/Database/PruneCommandTest.php b/tests/Database/PruneCommandTest.php index de5c42f5a228..0e7d286cd728 100644 --- a/tests/Database/PruneCommandTest.php +++ b/tests/Database/PruneCommandTest.php @@ -5,14 +5,13 @@ use Illuminate\Container\Container; use Illuminate\Contracts\Events\Dispatcher as DispatcherContract; use Illuminate\Database\Capsule\Manager as DB; -use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Console\PruneCommand; use Illuminate\Database\Eloquent\MassPrunable; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Prunable; +use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Events\ModelsPruned; use Illuminate\Events\Dispatcher; -use Mockery as m; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; @@ -53,6 +52,36 @@ public function testPrunableTestModelWithoutPrunableRecords() EOF, str_replace("\r", '', $output->fetch())); } + public function testPrunableSoftDeletedModelWithPrunableRecords() + { + $db = new DB; + $db->addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + $db->bootEloquent(); + $db->setAsGlobal(); + DB::connection('default')->getSchemaBuilder()->create('prunables', function ($table) { + $table->string('value')->nullable(); + $table->datetime('deleted_at')->nullable(); + }); + DB::connection('default')->table('prunables')->insert([ + ['value' => 1, 'deleted_at' => null], + ['value' => 2, 'deleted_at' => '2021-12-01 00:00:00'], + ['value' => 3, 'deleted_at' => null], + ['value' => 4, 'deleted_at' => '2021-12-02 00:00:00'], + ]); + + $output = $this->artisan(['--model' => PrunableTestSoftDeletedModelWithPrunableRecords::class]); + + $this->assertEquals(<<<'EOF' +2 [Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords] records have been pruned. + +EOF, str_replace("\r", '', $output->fetch())); + + $this->assertEquals(2, PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count()); + } + public function testNonPrunableTest() { $output = $this->artisan(['--model' => NonPrunableTestModel::class]); @@ -70,6 +99,7 @@ public function testTheCommandMayBePretended() 'driver' => 'sqlite', 'database' => ':memory:', ]); + $db->bootEloquent(); $db->setAsGlobal(); DB::connection('default')->getSchemaBuilder()->create('prunables', function ($table) { $table->string('name')->nullable(); @@ -82,8 +112,6 @@ public function testTheCommandMayBePretended() ['name' => 'stuart', 'value' => 4], ['name' => 'bello', 'value' => 5], ]); - $resolver = m::mock(ConnectionResolverInterface::class, ['connection' => $db->getConnection('default')]); - PrunableTestModelWithPrunableRecords::setConnectionResolver($resolver); $output = $this->artisan([ '--model' => PrunableTestModelWithPrunableRecords::class, @@ -98,6 +126,39 @@ public function testTheCommandMayBePretended() $this->assertEquals(5, PrunableTestModelWithPrunableRecords::count()); } + public function testTheCommandMayBePretendedOnSoftDeletedModel() + { + $db = new DB; + $db->addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + $db->bootEloquent(); + $db->setAsGlobal(); + DB::connection('default')->getSchemaBuilder()->create('prunables', function ($table) { + $table->string('value')->nullable(); + $table->datetime('deleted_at')->nullable(); + }); + DB::connection('default')->table('prunables')->insert([ + ['value' => 1, 'deleted_at' => null], + ['value' => 2, 'deleted_at' => '2021-12-01 00:00:00'], + ['value' => 3, 'deleted_at' => null], + ['value' => 4, 'deleted_at' => '2021-12-02 00:00:00'], + ]); + + $output = $this->artisan([ + '--model' => PrunableTestSoftDeletedModelWithPrunableRecords::class, + '--pretend' => true, + ]); + + $this->assertEquals(<<<'EOF' +2 [Illuminate\Tests\Database\PrunableTestSoftDeletedModelWithPrunableRecords] records will be pruned. + +EOF, str_replace("\r", '', $output->fetch())); + + $this->assertEquals(4, PrunableTestSoftDeletedModelWithPrunableRecords::withTrashed()->count()); + } + protected function artisan($arguments) { $input = new ArrayInput($arguments); @@ -139,6 +200,19 @@ public function prunable() } } +class PrunableTestSoftDeletedModelWithPrunableRecords extends Model +{ + use MassPrunable, SoftDeletes; + + protected $table = 'prunables'; + protected $connection = 'default'; + + public function prunable() + { + return static::where('value', '>=', 3); + } +} + class PrunableTestModelWithoutPrunableRecords extends Model { use Prunable; diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index c98f1591613c..f38c6d531a19 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -533,6 +533,13 @@ public function testSubstrCount() $this->assertSame(1, Str::substrCount('laravelPHPFramework', 'a', -10, -3)); } + public function testSubstrReplace() + { + $this->assertSame('12:00', Str::substrReplace('1200', ':', 2, 0)); + $this->assertSame('The Laravel Framework', Str::substrReplace('The Framework', 'Laravel ', 4, 0)); + $this->assertSame('Laravel – The PHP Framework for Web Artisans', Str::substrReplace('Laravel Framework', '– The PHP Framework for Web Artisans', 8)); + } + public function testUcfirst() { $this->assertSame('Laravel', Str::ucfirst('laravel')); diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 42fff56cca06..706d004840c2 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -621,6 +621,13 @@ public function testSubstrCount() $this->assertSame(1, $this->stringable('laravelPHPFramework')->substrCount('a', -10, -3)); } + public function testSubstrReplace() + { + $this->assertSame('12:00', (string) $this->stringable('1200')->substrReplace(':', 2, 0)); + $this->assertSame('The Laravel Framework', (string) $this->stringable('The Framework')->substrReplace('Laravel ', 4, 0)); + $this->assertSame('Laravel – The PHP Framework for Web Artisans', (string) $this->stringable('Laravel Framework')->substrReplace('– The PHP Framework for Web Artisans', 8)); + } + public function testPadBoth() { $this->assertSame('__Alien___', (string) $this->stringable('Alien')->padBoth(10, '_')); diff --git a/tests/Validation/ValidationPasswordRuleTest.php b/tests/Validation/ValidationPasswordRuleTest.php index c9eb2fbafa90..d81ea553b722 100644 --- a/tests/Validation/ValidationPasswordRuleTest.php +++ b/tests/Validation/ValidationPasswordRuleTest.php @@ -144,8 +144,15 @@ public function testMessagesOrder() 'validation.required', ]); - $this->fails($makeRules(), ['foo', 'azdazd', '1231231'], [ + $this->fails($makeRules(), ['foo', 'azdazd'], [ 'validation.min.string', + 'The my password must contain at least one uppercase and one lowercase letter.', + 'The my password must contain at least one number.', + ]); + + $this->fails($makeRules(), ['1231231'], [ + 'validation.min.string', + 'The my password must contain at least one uppercase and one lowercase letter.', ]); $this->fails($makeRules(), ['4564654564564'], [ @@ -165,8 +172,15 @@ public function testMessagesOrder() $this->passes($makeRules(), [null]); - $this->fails($makeRules(), ['foo', 'azdazd', '1231231'], [ + $this->fails($makeRules(), ['foo', 'azdazd'], [ 'validation.min.string', + 'The my password must contain at least one symbol.', + ]); + + $this->fails($makeRules(), ['1231231'], [ + 'validation.min.string', + 'The my password must contain at least one letter.', + 'The my password must contain at least one symbol.', ]); $this->fails($makeRules(), ['aaaaaaaaa', 'TJQSJQSIUQHS'], [