diff --git a/.github/workflows/facades.yml b/.github/workflows/facades.yml index c94399c184b4..ade0dcbcc43e 100644 --- a/.github/workflows/facades.yml +++ b/.github/workflows/facades.yml @@ -81,7 +81,7 @@ jobs: Illuminate\\Support\\Facades\\Vite - name: Commit facade docblocks - uses: stefanzweifel/git-auto-commit-action@v4 + uses: stefanzweifel/git-auto-commit-action@v5 with: commit_message: Update facade docblocks file_pattern: src/ diff --git a/composer.json b/composer.json index e830ecddba7b..439d904e1aa3 100644 --- a/composer.json +++ b/composer.json @@ -28,30 +28,30 @@ "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.3.2", "egulias/email-validator": "^3.2.1|^4.0", - "fruitcake/php-cors": "^1.2", + "fruitcake/php-cors": "^1.3", "guzzlehttp/uri-template": "^1.0", - "laravel/prompts": "^0.1.9", + "laravel/prompts": "^0.1.12", "laravel/serializable-closure": "^1.3", "league/commonmark": "^2.2.1", "league/flysystem": "^3.8.0", "monolog/monolog": "^3.0", "nesbot/carbon": "^2.67", - "nunomaduro/termwind": "^1.13", + "nunomaduro/termwind": "^2.0", "psr/container": "^1.1.1|^2.0.1", "psr/log": "^1.0|^2.0|^3.0", "psr/simple-cache": "^1.0|^2.0|^3.0", "ramsey/uuid": "^4.7", - "symfony/console": "^6.3", - "symfony/error-handler": "^6.3", - "symfony/finder": "^6.3", - "symfony/http-foundation": "^6.3", - "symfony/http-kernel": "^6.3", - "symfony/mailer": "^6.3", - "symfony/mime": "^6.3", - "symfony/process": "^6.3", - "symfony/routing": "^6.3", - "symfony/uid": "^6.3", - "symfony/var-dumper": "^6.3", + "symfony/console": "^7.0", + "symfony/error-handler": "^7.0", + "symfony/finder": "^7.0", + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0", + "symfony/mailer": "^7.0", + "symfony/mime": "^7.0", + "symfony/process": "^7.0", + "symfony/routing": "^7.0", + "symfony/uid": "^7.0", + "symfony/var-dumper": "^7.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5", "vlucas/phpdotenv": "^5.4.1", "voku/portable-ascii": "^2.0" @@ -95,7 +95,7 @@ "ext-gmp": "*", "ably/ably-php": "^1.0", "aws/aws-sdk-php": "^3.235.5", - "doctrine/dbal": "^3.5.1", + "doctrine/dbal": "^4.0", "fakerphp/faker": "^1.21", "guzzlehttp/guzzle": "^7.6", "league/flysystem-aws-s3-v3": "^3.0", @@ -104,13 +104,15 @@ "league/flysystem-read-only": "^3.3", "league/flysystem-sftp-v3": "^3.0", "mockery/mockery": "^1.5.1", + "nyholm/psr7": "^1.2", "orchestra/testbench-core": "^9.0", "pda/pheanstalk": "^4.0", "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^10.0.7", + "phpunit/phpunit": "^10.1", "predis/predis": "^2.0.2", - "symfony/cache": "^6.3", - "symfony/http-client": "^6.3.4" + "symfony/cache": "^7.0", + "symfony/http-client": "^7.0", + "symfony/psr-http-message-bridge": "^v7.0.0-BETA1" }, "provide": { "psr/container-implementation": "1.1|2.0", @@ -161,7 +163,7 @@ "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).", "brianium/paratest": "Required to run tests in parallel (^6.0).", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^4.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).", "filp/whoops": "Required for friendly error pages in development (^2.14.3).", "guzzlehttp/guzzle": "Required to use the HTTP Client and the ping methods on schedules (^7.6).", diff --git a/src/Illuminate/Cache/ArrayStore.php b/src/Illuminate/Cache/ArrayStore.php index bfb8a2ca70dc..4cca8cbf6960 100644 --- a/src/Illuminate/Cache/ArrayStore.php +++ b/src/Illuminate/Cache/ArrayStore.php @@ -3,6 +3,7 @@ namespace Illuminate\Cache; use Illuminate\Contracts\Cache\LockProvider; +use Illuminate\Support\Carbon; use Illuminate\Support\InteractsWithTime; class ArrayStore extends TaggableStore implements LockProvider @@ -57,7 +58,7 @@ public function get($key) $expiresAt = $item['expiresAt'] ?? 0; - if ($expiresAt !== 0 && (now()->getPreciseTimestamp(3) / 1000) >= $expiresAt) { + if ($expiresAt !== 0 && (Carbon::now()->getPreciseTimestamp(3) / 1000) >= $expiresAt) { $this->forget($key); return; @@ -188,7 +189,7 @@ protected function calculateExpiration($seconds) */ protected function toTimestamp($seconds) { - return $seconds > 0 ? (now()->getPreciseTimestamp(3) / 1000) + $seconds : 0; + return $seconds > 0 ? (Carbon::now()->getPreciseTimestamp(3) / 1000) + $seconds : 0; } /** diff --git a/src/Illuminate/Cache/RateLimiting/GlobalLimit.php b/src/Illuminate/Cache/RateLimiting/GlobalLimit.php index 4f084eb1095f..965352ba78d9 100644 --- a/src/Illuminate/Cache/RateLimiting/GlobalLimit.php +++ b/src/Illuminate/Cache/RateLimiting/GlobalLimit.php @@ -8,11 +8,11 @@ class GlobalLimit extends Limit * Create a new limit instance. * * @param int $maxAttempts - * @param int $decayMinutes + * @param int $decaySeconds * @return void */ - public function __construct(int $maxAttempts, int $decayMinutes = 1) + public function __construct(int $maxAttempts, int $decaySeconds = 60) { - parent::__construct('', $maxAttempts, $decayMinutes); + parent::__construct('', $maxAttempts, $decaySeconds); } } diff --git a/src/Illuminate/Cache/RateLimiting/Limit.php b/src/Illuminate/Cache/RateLimiting/Limit.php index 9bf058bb0728..00dc0c898b1e 100644 --- a/src/Illuminate/Cache/RateLimiting/Limit.php +++ b/src/Illuminate/Cache/RateLimiting/Limit.php @@ -12,18 +12,18 @@ class Limit public $key; /** - * The maximum number of attempts allowed within the given number of minutes. + * The maximum number of attempts allowed within the given number of seconds. * * @var int */ public $maxAttempts; /** - * The number of minutes until the rate limit is reset. + * The number of seconds until the rate limit is reset. * * @var int */ - public $decayMinutes; + public $decaySeconds; /** * The response generator callback. @@ -37,14 +37,25 @@ class Limit * * @param mixed $key * @param int $maxAttempts - * @param int $decayMinutes + * @param int $decaySeconds * @return void */ - public function __construct($key = '', int $maxAttempts = 60, int $decayMinutes = 1) + public function __construct($key = '', int $maxAttempts = 60, int $decaySeconds = 60) { $this->key = $key; $this->maxAttempts = $maxAttempts; - $this->decayMinutes = $decayMinutes; + $this->decaySeconds = $decaySeconds; + } + + /** + * Create a new rate limit. + * + * @param int $maxAttempts + * @return static + */ + public static function perSecond($maxAttempts) + { + return new static('', $maxAttempts, 1); } /** @@ -55,7 +66,7 @@ public function __construct($key = '', int $maxAttempts = 60, int $decayMinutes */ public static function perMinute($maxAttempts) { - return new static('', $maxAttempts); + return new static('', $maxAttempts, 60); } /** @@ -67,7 +78,7 @@ public static function perMinute($maxAttempts) */ public static function perMinutes($decayMinutes, $maxAttempts) { - return new static('', $maxAttempts, $decayMinutes); + return new static('', $maxAttempts, 60 * $decayMinutes); } /** @@ -79,7 +90,7 @@ public static function perMinutes($decayMinutes, $maxAttempts) */ public static function perHour($maxAttempts, $decayHours = 1) { - return new static('', $maxAttempts, 60 * $decayHours); + return new static('', $maxAttempts, 60 * 60 * $decayHours); } /** @@ -91,7 +102,7 @@ public static function perHour($maxAttempts, $decayHours = 1) */ public static function perDay($maxAttempts, $decayDays = 1) { - return new static('', $maxAttempts, 60 * 24 * $decayDays); + return new static('', $maxAttempts, 60 * 60 * 24 * $decayDays); } /** diff --git a/src/Illuminate/Cache/RedisStore.php b/src/Illuminate/Cache/RedisStore.php index 1d41b5b7c12d..369e2c6e3d7a 100755 --- a/src/Illuminate/Cache/RedisStore.php +++ b/src/Illuminate/Cache/RedisStore.php @@ -317,7 +317,7 @@ protected function currentTags($chunkSize = 1000) yield $tag; } } while (((string) $cursor) !== $defaultCursorValue); - })->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix).'tag:(.*):entries$/', $tagKey)); + })->map(fn (string $tagKey) => Str::match('/^'.preg_quote($prefix, '/').'tag:(.*):entries$/', $tagKey)); } /** diff --git a/src/Illuminate/Cache/composer.json b/src/Illuminate/Cache/composer.json index d486a4335f46..ec0d26e18469 100755 --- a/src/Illuminate/Cache/composer.json +++ b/src/Illuminate/Cache/composer.json @@ -40,7 +40,7 @@ "illuminate/database": "Required to use the database cache driver (^11.0).", "illuminate/filesystem": "Required to use the file cache driver (^11.0).", "illuminate/redis": "Required to use the redis cache driver (^11.0).", - "symfony/cache": "Required to use PSR-6 cache bridge (^6.3)." + "symfony/cache": "Required to use PSR-6 cache bridge (^7.0)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index 68e93b3a9843..bc6614f9b97d 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -36,6 +36,7 @@ * @property-read HigherOrderCollectionProxy $max * @property-read HigherOrderCollectionProxy $min * @property-read HigherOrderCollectionProxy $partition + * @property-read HigherOrderCollectionProxy $percentage * @property-read HigherOrderCollectionProxy $reject * @property-read HigherOrderCollectionProxy $skipUntil * @property-read HigherOrderCollectionProxy $skipWhile @@ -82,6 +83,7 @@ trait EnumeratesValues 'max', 'min', 'partition', + 'percentage', 'reject', 'skipUntil', 'skipWhile', diff --git a/src/Illuminate/Collections/composer.json b/src/Illuminate/Collections/composer.json index 80671fb4dade..1924032ab915 100644 --- a/src/Illuminate/Collections/composer.json +++ b/src/Illuminate/Collections/composer.json @@ -33,7 +33,7 @@ } }, "suggest": { - "symfony/var-dumper": "Required to use the dump method (^6.3)." + "symfony/var-dumper": "Required to use the dump method (^7.0)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Console/Application.php b/src/Illuminate/Console/Application.php index 98536ce41eec..f594258e9e76 100755 --- a/src/Illuminate/Console/Application.php +++ b/src/Illuminate/Console/Application.php @@ -206,9 +206,9 @@ public function output() * Add a command to the console. * * @param \Symfony\Component\Console\Command\Command $command - * @return \Symfony\Component\Console\Command\Command + * @return \Symfony\Component\Console\Command\Command|null */ - public function add(SymfonyCommand $command) + public function add(SymfonyCommand $command): ?SymfonyCommand { if ($command instanceof Command) { $command->setLaravel($this->laravel); diff --git a/src/Illuminate/Console/BufferedConsoleOutput.php b/src/Illuminate/Console/BufferedConsoleOutput.php index aa4e6ceedc4e..d4ee3954f393 100644 --- a/src/Illuminate/Console/BufferedConsoleOutput.php +++ b/src/Illuminate/Console/BufferedConsoleOutput.php @@ -27,10 +27,8 @@ public function fetch() /** * {@inheritdoc} - * - * @return void */ - protected function doWrite(string $message, bool $newline) + protected function doWrite(string $message, bool $newline): void { $this->buffer .= $message; diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index 7e1b3a1ff6ed..9e0290ebcc6d 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -190,9 +190,8 @@ public function run(InputInterface $input, OutputInterface $output): int * * @param \Symfony\Component\Console\Input\InputInterface $input * @param \Symfony\Component\Console\Output\OutputInterface $output - * @return int */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { if ($this instanceof Isolatable && $this->option('isolated') !== false && ! $this->commandIsolationMutex()->create($this)) { diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index b0543b36ab4d..4afcb4306113 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -449,9 +449,12 @@ protected function userProviderModel() */ protected function isReservedName($name) { - $name = strtolower($name); - - return in_array($name, $this->reservedNames); + return in_array( + strtolower($name), + collect($this->reservedNames) + ->transform(fn ($name) => strtolower($name)) + ->all() + ); } /** diff --git a/src/Illuminate/Console/OutputStyle.php b/src/Illuminate/Console/OutputStyle.php index cbfc257220e7..b0d6f94e8b17 100644 --- a/src/Illuminate/Console/OutputStyle.php +++ b/src/Illuminate/Console/OutputStyle.php @@ -64,7 +64,7 @@ public function askQuestion(Question $question): mixed /** * {@inheritdoc} */ - public function write(string|iterable $messages, bool $newline = false, int $options = 0) + public function write(string|iterable $messages, bool $newline = false, int $options = 0): void { $this->newLinesWritten = $this->trailingNewLineCount($messages) + (int) $newline; $this->newLineWritten = $this->newLinesWritten > 0; @@ -74,10 +74,8 @@ public function write(string|iterable $messages, bool $newline = false, int $opt /** * {@inheritdoc} - * - * @return void */ - public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL) + public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void { $this->newLinesWritten = $this->trailingNewLineCount($messages) + 1; $this->newLineWritten = true; @@ -87,10 +85,8 @@ public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORM /** * {@inheritdoc} - * - * @return void */ - public function newLine(int $count = 1) + public function newLine(int $count = 1): void { $this->newLinesWritten += $count; $this->newLineWritten = $this->newLinesWritten > 0; diff --git a/src/Illuminate/Console/QuestionHelper.php b/src/Illuminate/Console/QuestionHelper.php index 43b4cf8296c1..a3bf62a520ab 100644 --- a/src/Illuminate/Console/QuestionHelper.php +++ b/src/Illuminate/Console/QuestionHelper.php @@ -17,7 +17,7 @@ class QuestionHelper extends SymfonyQuestionHelper * * @return void */ - protected function writePrompt(OutputInterface $output, Question $question) + protected function writePrompt(OutputInterface $output, Question $question): void { $text = OutputFormatter::escapeTrailingBackslash($question->getQuestion()); diff --git a/src/Illuminate/Console/composer.json b/src/Illuminate/Console/composer.json index 38ecc805c087..68e50920d317 100755 --- a/src/Illuminate/Console/composer.json +++ b/src/Illuminate/Console/composer.json @@ -21,10 +21,10 @@ "illuminate/macroable": "^11.0", "illuminate/support": "^11.0", "illuminate/view": "^11.0", - "laravel/prompts": "^0.1.9", - "nunomaduro/termwind": "^1.13", - "symfony/console": "^6.3", - "symfony/process": "^6.3" + "laravel/prompts": "^0.1.12", + "nunomaduro/termwind": "^2.0", + "symfony/console": "^7.0", + "symfony/process": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Cookie/composer.json b/src/Illuminate/Cookie/composer.json index b2ffb16f88dc..e8514738ce54 100755 --- a/src/Illuminate/Cookie/composer.json +++ b/src/Illuminate/Cookie/composer.json @@ -20,8 +20,8 @@ "illuminate/contracts": "^11.0", "illuminate/macroable": "^11.0", "illuminate/support": "^11.0", - "symfony/http-foundation": "^6.3", - "symfony/http-kernel": "^6.3" + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Database/Concerns/BuildsQueries.php b/src/Illuminate/Database/Concerns/BuildsQueries.php index df1f40ce08ed..0d45b0a6f6fc 100644 --- a/src/Illuminate/Database/Concerns/BuildsQueries.php +++ b/src/Illuminate/Database/Concerns/BuildsQueries.php @@ -112,6 +112,35 @@ public function each(callable $callback, $count = 1000) * @return bool */ public function chunkById($count, callable $callback, $column = null, $alias = null) + { + return $this->orderedChunkById($count, $callback, $column, $alias); + } + + /** + * Chunk the results of a query by comparing IDs in descending order. + * + * @param int $count + * @param callable $callback + * @param string|null $column + * @param string|null $alias + * @return bool + */ + public function chunkByIdDesc($count, callable $callback, $column = null, $alias = null) + { + return $this->orderedChunkById($count, $callback, $column, $alias, descending: true); + } + + /** + * Chunk the results of a query by comparing IDs in a given order. + * + * @param int $count + * @param callable $callback + * @param string|null $column + * @param string|null $alias + * @param bool $descending + * @return bool + */ + public function orderedChunkById($count, callable $callback, $column = null, $alias = null, $descending = false) { $column ??= $this->defaultKeyName(); @@ -127,7 +156,11 @@ public function chunkById($count, callable $callback, $column = null, $alias = n // We'll execute the query for the given page and get the results. If there are // no results we can just break and return from here. When there are results // we will call the callback with the current chunk of these results here. - $results = $clone->forPageAfterId($count, $lastId, $column)->get(); + if ($descending) { + $results = $clone->forPageBeforeId($count, $lastId, $column)->get(); + } else { + $results = $clone->forPageAfterId($count, $lastId, $column)->get(); + } $countResults = $results->count(); diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index 80dac1c57b12..a690f7b5cb46 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -47,6 +47,8 @@ public function transaction(Closure $callback, $attempts = 1) $this->getPdo()->commit(); } + $this->transactions = max(0, $this->transactions - 1); + if ($this->afterCommitCallbacksShouldBeExecuted()) { $this->transactionsManager?->commit($this->getName()); } @@ -56,8 +58,6 @@ public function transaction(Closure $callback, $attempts = 1) ); continue; - } finally { - $this->transactions = max(0, $this->transactions - 1); } $this->fireConnectionEvent('committed'); @@ -194,12 +194,12 @@ public function commit() $this->getPdo()->commit(); } + $this->transactions = max(0, $this->transactions - 1); + if ($this->afterCommitCallbacksShouldBeExecuted()) { $this->transactionsManager?->commit($this->getName()); } - $this->transactions = max(0, $this->transactions - 1); - $this->fireConnectionEvent('committed'); } @@ -210,8 +210,7 @@ public function commit() */ protected function afterCommitCallbacksShouldBeExecuted() { - return $this->transactions == 0 || - $this->transactionsManager?->afterCommitCallbacksShouldBeExecuted($this->transactions); + return $this->transactionsManager?->afterCommitCallbacksShouldBeExecuted($this->transactions) || $this->transactions == 0; } /** diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index 6fdfc4946b54..51f028ff19fa 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -688,6 +688,27 @@ public function pretend(Closure $callback) }); } + /** + * Execute the given callback without "pretending". + * + * @param \Closure $callback + * @return mixed + */ + public function withoutPretending(Closure $callback) + { + if (! $this->pretending) { + return $callback(); + } + + $this->pretending = false; + + $result = $callback(); + + $this->pretending = true; + + return $result; + } + /** * Execute the given callback in "dry run" mode. * @@ -862,6 +883,10 @@ public function logQuery($query, $bindings, $time = null) $this->event(new QueryExecuted($query, $bindings, $time, $this)); + $query = $this->pretending === true + ? $this->queryGrammar?->substituteBindingsIntoRawSql($query, $bindings) ?? $query + : $query; + if ($this->loggingQueries) { $this->queryLog[] = compact('query', 'bindings', 'time'); } diff --git a/src/Illuminate/Database/Console/DatabaseInspectionCommand.php b/src/Illuminate/Database/Console/DatabaseInspectionCommand.php index 3cad6c2f697e..42568fc2c02d 100644 --- a/src/Illuminate/Database/Console/DatabaseInspectionCommand.php +++ b/src/Illuminate/Database/Console/DatabaseInspectionCommand.php @@ -224,7 +224,7 @@ protected function ensureDependenciesExist() protected function installDependencies() { $command = collect($this->composer->findComposer()) - ->push('require doctrine/dbal') + ->push('require doctrine/dbal:^3.5.1') ->implode(' '); $process = Process::fromShellCommandline($command, null, null, null, null); diff --git a/src/Illuminate/Database/Console/PruneCommand.php b/src/Illuminate/Database/Console/PruneCommand.php index 5144247eeff0..0c51beb345b2 100644 --- a/src/Illuminate/Database/Console/PruneCommand.php +++ b/src/Illuminate/Database/Console/PruneCommand.php @@ -138,10 +138,10 @@ protected function models() return $models->reject(function ($model) use ($except) { return in_array($model, $except); }); - })->filter(function ($model) { - return $this->isPrunable($model); })->filter(function ($model) { return class_exists($model); + })->filter(function ($model) { + return $this->isPrunable($model); })->values(); } diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index 8e9b5ac2177d..3ef912004e8c 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -7,6 +7,7 @@ use Doctrine\DBAL\Schema\Index; use Doctrine\DBAL\Types\DecimalType; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; @@ -158,7 +159,7 @@ protected function getVirtualAttributes($model, $columns) ->reject( fn (ReflectionMethod $method) => $method->isStatic() || $method->isAbstract() - || $method->getDeclaringClass()->getName() !== get_class($model) + || $method->getDeclaringClass()->getName() === Model::class ) ->mapWithKeys(function (ReflectionMethod $method) use ($model) { if (preg_match('/^get(.+)Attribute$/', $method->getName(), $matches) === 1) { @@ -198,7 +199,7 @@ protected function getRelations($model) ->reject( fn (ReflectionMethod $method) => $method->isStatic() || $method->isAbstract() - || $method->getDeclaringClass()->getName() !== get_class($model) + || $method->getDeclaringClass()->getName() === Model::class ) ->filter(function (ReflectionMethod $method) { $file = new SplFileObject($method->getFileName()); diff --git a/src/Illuminate/Database/DBAL/TimestampType.php b/src/Illuminate/Database/DBAL/TimestampType.php index e702523925a1..aee4a2a0130b 100644 --- a/src/Illuminate/Database/DBAL/TimestampType.php +++ b/src/Illuminate/Database/DBAL/TimestampType.php @@ -2,19 +2,14 @@ namespace Illuminate\Database\DBAL; -use Doctrine\DBAL\Exception as DBALException; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Platforms\MariaDb1027Platform; -use Doctrine\DBAL\Platforms\MariaDb1052Platform; +use Doctrine\DBAL\Platforms\Exception\NotSupported; +use Doctrine\DBAL\Platforms\MariaDB1052Platform; use Doctrine\DBAL\Platforms\MariaDBPlatform; -use Doctrine\DBAL\Platforms\MySQL57Platform; use Doctrine\DBAL\Platforms\MySQL80Platform; use Doctrine\DBAL\Platforms\MySQLPlatform; -use Doctrine\DBAL\Platforms\PostgreSQL100Platform; -use Doctrine\DBAL\Platforms\PostgreSQL94Platform; use Doctrine\DBAL\Platforms\PostgreSQLPlatform; -use Doctrine\DBAL\Platforms\SqlitePlatform; -use Doctrine\DBAL\Platforms\SQLServer2012Platform; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\DBAL\Platforms\SQLServerPlatform; use Doctrine\DBAL\Types\PhpDateTimeMappingType; use Doctrine\DBAL\Types\Type; @@ -24,24 +19,19 @@ class TimestampType extends Type implements PhpDateTimeMappingType /** * {@inheritdoc} * - * @throws DBALException + * @throws \Doctrine\DBAL\Platforms\Exception\NotSupported */ public function getSQLDeclaration(array $column, AbstractPlatform $platform): string { return match (get_class($platform)) { MySQLPlatform::class, - MySQL57Platform::class, MySQL80Platform::class, MariaDBPlatform::class, - MariaDb1027Platform::class, - MariaDb1052Platform::class, => $this->getMySqlPlatformSQLDeclaration($column), - PostgreSQLPlatform::class, - PostgreSQL94Platform::class, - PostgreSQL100Platform::class => $this->getPostgresPlatformSQLDeclaration($column), - SQLServerPlatform::class, - SQLServer2012Platform::class => $this->getSqlServerPlatformSQLDeclaration($column), - SqlitePlatform::class => 'DATETIME', - default => throw new DBALException('Invalid platform: '.substr(strrchr(get_class($platform), '\\'), 1)), + MariaDB1052Platform::class, => $this->getMySqlPlatformSQLDeclaration($column), + PostgreSQLPlatform::class => $this->getPostgresPlatformSQLDeclaration($column), + SQLServerPlatform::class => $this->getSqlServerPlatformSQLDeclaration($column), + SQLitePlatform::class => 'DATETIME', + default => throw NotSupported::new('TIMESTAMP'), }; } diff --git a/src/Illuminate/Database/DatabaseTransactionsManager.php b/src/Illuminate/Database/DatabaseTransactionsManager.php index a39924db761b..e198f4f3f6d6 100755 --- a/src/Illuminate/Database/DatabaseTransactionsManager.php +++ b/src/Illuminate/Database/DatabaseTransactionsManager.php @@ -11,13 +11,6 @@ class DatabaseTransactionsManager */ protected $transactions; - /** - * The database transaction that should be ignored by callbacks. - * - * @var \Illuminate\Database\DatabaseTransactionRecord|null - */ - protected $callbacksShouldIgnore; - /** * Create a new database transactions manager instance. * @@ -54,10 +47,6 @@ public function rollback($connection, $level) $this->transactions = $this->transactions->reject( fn ($transaction) => $transaction->connection == $connection && $transaction->level > $level )->values(); - - if ($this->transactions->isEmpty()) { - $this->callbacksShouldIgnore = null; - } } /** @@ -75,10 +64,6 @@ public function commit($connection) $this->transactions = $forOtherConnections->values(); $forThisConnection->map->executeCallbacks(); - - if ($this->transactions->isEmpty()) { - $this->callbacksShouldIgnore = null; - } } /** @@ -96,19 +81,6 @@ public function addCallback($callback) $callback(); } - /** - * Specify that callbacks should ignore the given transaction when determining if they should be executed. - * - * @param \Illuminate\Database\DatabaseTransactionRecord $transaction - * @return $this - */ - public function callbacksShouldIgnore(DatabaseTransactionRecord $transaction) - { - $this->callbacksShouldIgnore = $transaction; - - return $this; - } - /** * Get the transactions that are applicable to callbacks. * @@ -127,7 +99,7 @@ public function callbackApplicableTransactions() */ public function afterCommitCallbacksShouldBeExecuted($level) { - return $level === 1; + return $level === 0; } /** diff --git a/src/Illuminate/Database/Eloquent/Builder.php b/src/Illuminate/Database/Eloquent/Builder.php index 5d2f2b503657..6a42e60511b4 100755 --- a/src/Illuminate/Database/Eloquent/Builder.php +++ b/src/Illuminate/Database/Eloquent/Builder.php @@ -563,11 +563,11 @@ public function firstOrNew(array $attributes = [], array $values = []) */ public function firstOrCreate(array $attributes = [], array $values = []) { - if (! is_null($instance = $this->where($attributes)->first())) { + if (! is_null($instance = (clone $this)->where($attributes)->first())) { return $instance; } - return $this->create(array_merge($attributes, $values)); + return $this->createOrFirst($attributes, $values); } /** @@ -595,8 +595,10 @@ public function createOrFirst(array $attributes = [], array $values = []) */ public function updateOrCreate(array $attributes, array $values = []) { - return tap($this->firstOrNew($attributes), function ($instance) use ($values) { - $instance->fill($values)->save(); + return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values)->save(); + } }); } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 1347cc4d9366..762f0c84c5ef 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -39,6 +39,7 @@ use ReflectionClass; use ReflectionMethod; use ReflectionNamedType; +use RuntimeException; use ValueError; trait HasAttributes @@ -1362,7 +1363,19 @@ public static function encryptUsing($encrypter) */ protected function castAttributeAsHashedString($key, $value) { - return $value !== null && ! Hash::isHashed($value) ? Hash::make($value) : $value; + if ($value === null) { + return null; + } + + if (! Hash::isHashed($value)) { + return Hash::make($value); + } + + if (! Hash::verifyConfiguration($value)) { + throw new RuntimeException("Could not verify the hashed value's configuration."); + } + + return $value; } /** diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index aad9ae748759..b65541ccc93f 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1728,6 +1728,7 @@ public function replicate(array $except = null) $this->getKeyName(), $this->getCreatedAtColumn(), $this->getUpdatedAtColumn(), + ...$this->uniqueIds(), ])); $attributes = Arr::except( diff --git a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php index dbf71768ccc3..37c698f3d80f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -622,7 +622,7 @@ public function firstOrCreate(array $attributes = [], array $values = [], array { if (is_null($instance = (clone $this)->where($attributes)->first())) { if (is_null($instance = $this->related->where($attributes)->first())) { - $instance = $this->create(array_merge($attributes, $values), $joining, $touch); + $instance = $this->createOrFirst($attributes, $values, $joining, $touch); } else { try { $this->getQuery()->withSavepointIfNeeded(fn () => $this->attach($instance, $joining, $touch)); @@ -672,19 +672,13 @@ public function createOrFirst(array $attributes = [], array $values = [], array */ public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true) { - if (is_null($instance = (clone $this)->where($attributes)->first())) { - if (is_null($instance = $this->related->where($attributes)->first())) { - return $this->create(array_merge($attributes, $values), $joining, $touch); - } else { - $this->attach($instance, $joining, $touch); - } - } - - $instance->fill($values); + return tap($this->firstOrCreate($attributes, $values, $joining, $touch), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values); - $instance->save(['touch' => false]); - - return $instance; + $instance->save(['touch' => false]); + } + }); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php index d8aa1809a9d8..b0b4b1fdebe1 100644 --- a/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasManyThrough.php @@ -273,11 +273,11 @@ public function firstOrNew(array $attributes = [], array $values = []) */ public function firstOrCreate(array $attributes = [], array $values = []) { - if (! is_null($instance = $this->where($attributes)->first())) { + if (! is_null($instance = (clone $this)->where($attributes)->first())) { return $instance; } - return $this->create(array_merge($attributes, $values)); + return $this->createOrFirst(array_merge($attributes, $values)); } /** @@ -305,11 +305,11 @@ public function createOrFirst(array $attributes = [], array $values = []) */ public function updateOrCreate(array $attributes, array $values = []) { - $instance = $this->firstOrNew($attributes); - - $instance->fill($values)->save(); - - return $instance; + return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values)->save(); + } + }); } /** diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index f107ec310e41..af263baf854f 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -235,8 +235,8 @@ public function firstOrNew(array $attributes = [], array $values = []) */ public function firstOrCreate(array $attributes = [], array $values = []) { - if (is_null($instance = $this->where($attributes)->first())) { - $instance = $this->create(array_merge($attributes, $values)); + if (is_null($instance = (clone $this)->where($attributes)->first())) { + $instance = $this->createOrFirst($attributes, $values); } return $instance; @@ -267,10 +267,10 @@ public function createOrFirst(array $attributes = [], array $values = []) */ public function updateOrCreate(array $attributes, array $values = []) { - return tap($this->firstOrNew($attributes), function ($instance) use ($values) { - $instance->fill($values); - - $instance->save(); + return tap($this->firstOrCreate($attributes, $values), function ($instance) use ($values) { + if (! $instance->wasRecentlyCreated) { + $instance->fill($values)->save(); + } }); } diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index 0d2b0eff42a7..9ce0ec352595 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -281,7 +281,7 @@ public function setTablePrefix($prefix) /** * Set the grammar's database connection. * - * @param \Illuminate\Database\Connection $prefix + * @param \Illuminate\Database\Connection $connection * @return $this */ public function setConnection($connection) diff --git a/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php b/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php index d2a8d6006df3..a2354182bc2c 100644 --- a/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php +++ b/src/Illuminate/Database/PDO/Concerns/ConnectsToDatabase.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\PDO\Concerns; +use Doctrine\DBAL\Driver\Connection as ConnectionContract; use Illuminate\Database\PDO\Connection; use InvalidArgumentException; use PDO; @@ -12,14 +13,11 @@ trait ConnectsToDatabase * Create a new database connection. * * @param mixed[] $params - * @param string|null $username - * @param string|null $password - * @param mixed[] $driverOptions * @return \Illuminate\Database\PDO\Connection * * @throws \InvalidArgumentException */ - public function connect(array $params, $username = null, $password = null, array $driverOptions = []) + public function connect(array $params): ConnectionContract { if (! isset($params['pdo']) || ! $params['pdo'] instanceof PDO) { throw new InvalidArgumentException('Laravel requires the "pdo" property to be set and be a PDO instance.'); diff --git a/src/Illuminate/Database/PDO/Connection.php b/src/Illuminate/Database/PDO/Connection.php index 427bda2cc4b3..a8318867d741 100644 --- a/src/Illuminate/Database/PDO/Connection.php +++ b/src/Illuminate/Database/PDO/Connection.php @@ -2,18 +2,18 @@ namespace Illuminate\Database\PDO; +use Doctrine\DBAL\Driver\Connection as ConnectionContract; use Doctrine\DBAL\Driver\PDO\Exception; use Doctrine\DBAL\Driver\PDO\Result; use Doctrine\DBAL\Driver\PDO\Statement; use Doctrine\DBAL\Driver\Result as ResultInterface; -use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Driver\Statement as StatementInterface; use Doctrine\DBAL\ParameterType; use PDO; use PDOException; use PDOStatement; -class Connection implements ServerInfoAwareConnection +class Connection implements ConnectionContract { /** * The underlying PDO connection. @@ -94,11 +94,11 @@ public function query(string $sql): ResultInterface * Get the last insert ID. * * @param string|null $name - * @return mixed + * @return string|int * * @throws \Doctrine\DBAL\Driver\PDO\Exception */ - public function lastInsertId($name = null) + public function lastInsertId($name = null): string|int { try { if ($name === null) { @@ -125,31 +125,31 @@ protected function createStatement(PDOStatement $stmt): Statement /** * Begin a new database transaction. * - * @return bool + * @return void */ - public function beginTransaction() + public function beginTransaction(): void { - return $this->connection->beginTransaction(); + $this->connection->beginTransaction(); } /** * Commit a database transaction. * - * @return bool + * @return void */ - public function commit() + public function commit(): void { - return $this->connection->commit(); + $this->connection->commit(); } /** * Rollback a database transaction. * - * @return bool + * @return void */ - public function rollBack() + public function rollBack(): void { - return $this->connection->rollBack(); + $this->connection->rollBack(); } /** @@ -159,7 +159,7 @@ public function rollBack() * @param string $type * @return string */ - public function quote($input, $type = ParameterType::STRING) + public function quote($input, $type = ParameterType::STRING): string { return $this->connection->quote($input, $type); } @@ -169,17 +169,17 @@ public function quote($input, $type = ParameterType::STRING) * * @return string */ - public function getServerVersion() + public function getServerVersion(): string { return $this->connection->getAttribute(PDO::ATTR_SERVER_VERSION); } /** - * Get the wrapped PDO connection. + * Get the native PDO connection. * * @return \PDO */ - public function getWrappedConnection(): PDO + public function getNativeConnection(): PDO { return $this->connection; } diff --git a/src/Illuminate/Database/PDO/SqlServerConnection.php b/src/Illuminate/Database/PDO/SqlServerConnection.php index d32d3c3e0a6d..9da969333e2f 100644 --- a/src/Illuminate/Database/PDO/SqlServerConnection.php +++ b/src/Illuminate/Database/PDO/SqlServerConnection.php @@ -2,14 +2,14 @@ namespace Illuminate\Database\PDO; +use Doctrine\DBAL\Driver\Connection as ConnectionContract; use Doctrine\DBAL\Driver\PDO\SQLSrv\Statement; use Doctrine\DBAL\Driver\Result; -use Doctrine\DBAL\Driver\ServerInfoAwareConnection; use Doctrine\DBAL\Driver\Statement as StatementInterface; use Doctrine\DBAL\ParameterType; use PDO; -class SqlServerConnection implements ServerInfoAwareConnection +class SqlServerConnection implements ConnectionContract { /** * The underlying connection instance. @@ -68,9 +68,9 @@ public function exec(string $statement): int * Get the last insert ID. * * @param string|null $name - * @return mixed + * @return string|int */ - public function lastInsertId($name = null) + public function lastInsertId($name = null): string|int { if ($name === null) { return $this->connection->lastInsertId($name); @@ -84,31 +84,31 @@ public function lastInsertId($name = null) /** * Begin a new database transaction. * - * @return bool + * @return void */ - public function beginTransaction() + public function beginTransaction(): void { - return $this->connection->beginTransaction(); + $this->connection->beginTransaction(); } /** * Commit a database transaction. * - * @return bool + * @return void */ - public function commit() + public function commit(): void { - return $this->connection->commit(); + $this->connection->commit(); } /** * Rollback a database transaction. * - * @return bool + * @return void */ - public function rollBack() + public function rollBack(): void { - return $this->connection->rollBack(); + $this->connection->rollBack(); } /** @@ -118,7 +118,7 @@ public function rollBack() * @param int $type * @return string */ - public function quote($value, $type = ParameterType::STRING) + public function quote($value, $type = ParameterType::STRING): string { $val = $this->connection->quote($value, $type); @@ -135,17 +135,17 @@ public function quote($value, $type = ParameterType::STRING) * * @return string */ - public function getServerVersion() + public function getServerVersion(): string { return $this->connection->getServerVersion(); } /** - * Get the wrapped PDO connection. + * Get the native PDO connection. * * @return \PDO */ - public function getWrappedConnection(): PDO + public function getNativeConnection(): PDO { return $this->connection->getWrappedConnection(); } diff --git a/src/Illuminate/Database/PDO/SqlServerDriver.php b/src/Illuminate/Database/PDO/SqlServerDriver.php index 1b0d9574e748..ac7b8a1aedef 100644 --- a/src/Illuminate/Database/PDO/SqlServerDriver.php +++ b/src/Illuminate/Database/PDO/SqlServerDriver.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\PDO; use Doctrine\DBAL\Driver\AbstractSQLServerDriver; +use Doctrine\DBAL\Driver\Connection as ConnectionContract; class SqlServerDriver extends AbstractSQLServerDriver { @@ -10,12 +11,9 @@ class SqlServerDriver extends AbstractSQLServerDriver * Create a new database connection. * * @param mixed[] $params - * @param string|null $username - * @param string|null $password - * @param mixed[] $driverOptions * @return \Illuminate\Database\PDO\SqlServerConnection */ - public function connect(array $params, $username = null, $password = null, array $driverOptions = []) + public function connect(array $params): ConnectionContract { return new SqlServerConnection( new Connection($params['pdo']) diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 5419ad07fbb3..3b4f117693f6 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Query\Grammars; +use Illuminate\Contracts\Database\Query\Expression; use Illuminate\Database\Concerns\CompilesJsonPaths; use Illuminate\Database\Grammar as BaseGrammar; use Illuminate\Database\Query\Builder; @@ -246,7 +247,7 @@ protected function concatenateWhereClauses($query, $sql) */ protected function whereRaw(Builder $query, $where) { - return $where['sql']; + return $where['sql'] instanceof Expression ? $where['sql']->getValue($this) : $where['sql']; } /** diff --git a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php index ce9183858d2b..3aa8c9cb9852 100644 --- a/src/Illuminate/Database/Query/Processors/MySqlProcessor.php +++ b/src/Illuminate/Database/Query/Processors/MySqlProcessor.php @@ -7,6 +7,8 @@ class MySqlProcessor extends Processor /** * Process the results of a column listing query. * + * @deprecated Will be removed in a future Laravel version. + * * @param array $results * @return array */ @@ -16,4 +18,28 @@ public function processColumnListing($results) return ((object) $result)->column_name; }, $results); } + + /** + * Process the results of a columns query. + * + * @param array $results + * @return array + */ + public function processColumns($results) + { + return array_map(function ($result) { + $result = (object) $result; + + return [ + 'name' => $result->name, + 'type_name' => $result->type_name, + 'type' => $result->type, + 'collation' => $result->collation, + 'nullable' => $result->nullable === 'YES', + 'default' => $result->default, + 'auto_increment' => $result->extra === 'auto_increment', + 'comment' => $result->comment, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php index 5956a8fb3148..0ec35de6853b 100755 --- a/src/Illuminate/Database/Query/Processors/PostgresProcessor.php +++ b/src/Illuminate/Database/Query/Processors/PostgresProcessor.php @@ -33,6 +33,8 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu /** * Process the results of a column listing query. * + * @deprecated Will be removed in a future Laravel version. + * * @param array $results * @return array */ @@ -42,4 +44,30 @@ public function processColumnListing($results) return ((object) $result)->column_name; }, $results); } + + /** + * Process the results of a columns query. + * + * @param array $results + * @return array + */ + public function processColumns($results) + { + return array_map(function ($result) { + $result = (object) $result; + + $autoincrement = $result->default !== null && str_starts_with($result->default, 'nextval('); + + return [ + 'name' => $result->name, + 'type_name' => $result->type_name, + 'type' => $result->type, + 'collation' => $result->collation, + 'nullable' => (bool) $result->nullable, + 'default' => $autoincrement ? null : $result->default, + 'auto_increment' => $autoincrement, + 'comment' => $result->comment, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/Processor.php b/src/Illuminate/Database/Query/Processors/Processor.php index 0069b436d553..8fb8b0e7ab48 100755 --- a/src/Illuminate/Database/Query/Processors/Processor.php +++ b/src/Illuminate/Database/Query/Processors/Processor.php @@ -39,6 +39,8 @@ public function processInsertGetId(Builder $query, $sql, $values, $sequence = nu /** * Process the results of a column listing query. * + * @deprecated Will be removed in a future Laravel version. + * * @param array $results * @return array */ @@ -46,4 +48,15 @@ public function processColumnListing($results) { return $results; } + + /** + * Process the results of a columns query. + * + * @param array $results + * @return array + */ + public function processColumns($results) + { + return $results; + } } diff --git a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php index 65da1dff7c26..cb851efb4cc6 100644 --- a/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SQLiteProcessor.php @@ -7,6 +7,8 @@ class SQLiteProcessor extends Processor /** * Process the results of a column listing query. * + * @deprecated Will be removed in a future Laravel version. + * * @param array $results * @return array */ @@ -16,4 +18,32 @@ public function processColumnListing($results) return ((object) $result)->name; }, $results); } + + /** + * Process the results of a columns query. + * + * @param array $results + * @return array + */ + public function processColumns($results) + { + $hasPrimaryKey = array_sum(array_column($results, 'primary')) === 1; + + return array_map(function ($result) use ($hasPrimaryKey) { + $result = (object) $result; + + $type = strtolower($result->type); + + return [ + 'name' => $result->name, + 'type_name' => strtok($type, '('), + 'type' => $type, + 'collation' => null, + 'nullable' => (bool) $result->nullable, + 'default' => $result->default, + 'auto_increment' => $hasPrimaryKey && $result->primary && $type === 'integer', + 'comment' => null, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php index 49476f095594..0384335ac6db 100755 --- a/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php +++ b/src/Illuminate/Database/Query/Processors/SqlServerProcessor.php @@ -58,6 +58,8 @@ protected function processInsertGetIdForOdbc(Connection $connection) /** * Process the results of a column listing query. * + * @deprecated Will be removed in a future Laravel version. + * * @param array $results * @return array */ @@ -67,4 +69,35 @@ public function processColumnListing($results) return ((object) $result)->name; }, $results); } + + /** + * Process the results of a columns query. + * + * @param array $results + * @return array + */ + public function processColumns($results) + { + return array_map(function ($result) { + $result = (object) $result; + + $type = match ($typeName = $result->type_name) { + 'binary', 'varbinary', 'char', 'varchar', 'nchar', 'nvarchar' => $result->length == -1 ? $typeName.'(max)' : $typeName."($result->length)", + 'decimal', 'numeric' => $typeName."($result->precision,$result->places)", + 'float', 'datetime2', 'datetimeoffset', 'time' => $typeName."($result->precision)", + default => $typeName, + }; + + return [ + 'name' => $result->name, + 'type_name' => $result->type_name, + 'type' => $type, + 'collation' => $result->collation, + 'nullable' => (bool) $result->nullable, + 'default' => $result->default, + 'auto_increment' => (bool) $result->autoincrement, + 'comment' => $result->comment, + ]; + }, $results); + } } diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index f41123aca642..ec5a87bb2399 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -977,7 +977,7 @@ public function float($column, $total = 8, $places = 2, $unsigned = false) * @param bool $unsigned * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function double($column, $total = null, $places = null, $unsigned = false) + public function double($column, $total = 15, $places = 6, $unsigned = false) { return $this->addColumn('double', $column, compact('total', 'places', 'unsigned')); } diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index a38b526c9a33..7a1b58d4ddb1 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -233,13 +233,28 @@ public function whenTableDoesntHaveColumn(string $table, string $column, Closure * * @param string $table * @param string $column + * @param bool $fullDefinition * @return string */ - public function getColumnType($table, $column) + public function getColumnType($table, $column, $fullDefinition = false) { $table = $this->connection->getTablePrefix().$table; - return $this->connection->getDoctrineColumn($table, $column)->getType()->getName(); + if (! $this->connection->usingNativeSchemaOperations()) { + $type = $this->connection->getDoctrineColumn($table, $column)->getType(); + + return $type::lookupName($type); + } + + $columns = $this->getColumns($table); + + foreach ($columns as $value) { + if (strtolower($value['name']) === $column) { + return $fullDefinition ? $value['type'] : $value['type_name']; + } + } + + throw new InvalidArgumentException("There is no column with name '$column' on table '$table'."); } /** @@ -250,11 +265,22 @@ public function getColumnType($table, $column) */ public function getColumnListing($table) { - $results = $this->connection->selectFromWriteConnection($this->grammar->compileColumnListing( - $this->connection->getTablePrefix().$table - )); + return array_column($this->getColumns($table), 'name'); + } - return $this->connection->getPostProcessor()->processColumnListing($results); + /** + * Get the columns for a given table. + * + * @param string $table + * @return array + */ + public function getColumns($table) + { + $table = $this->connection->getTablePrefix().$table; + + return $this->connection->getPostProcessor()->processColumns( + $this->connection->selectFromWriteConnection($this->grammar->compileColumns($table)) + ); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php b/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php index 009f7ab93758..7f429c0eccc5 100644 --- a/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php +++ b/src/Illuminate/Database/Schema/Grammars/ChangeColumn.php @@ -118,22 +118,22 @@ protected static function getDoctrineColumn(Table $table, Fluent $fluent) */ protected static function getDoctrineColumnChangeOptions(Fluent $fluent) { - $options = ['type' => static::getDoctrineColumnType($fluent['type'])]; + $options = ['Type' => static::getDoctrineColumnType($fluent['type'])]; if (! in_array($fluent['type'], ['smallint', 'integer', 'bigint'])) { - $options['autoincrement'] = false; + $options['Autoincrement'] = false; } if (in_array($fluent['type'], ['tinyText', 'text', 'mediumText', 'longText'])) { - $options['length'] = static::calculateDoctrineTextLength($fluent['type']); + $options['Length'] = static::calculateDoctrineTextLength($fluent['type']); } if ($fluent['type'] === 'char') { - $options['fixed'] = true; + $options['Fixed'] = true; } if (static::doesntNeedCharacterOptions($fluent['type'])) { - $options['customSchemaOptions'] = [ + $options['PlatformOptions'] = [ 'collation' => '', 'charset' => '', ]; diff --git a/src/Illuminate/Database/Schema/Grammars/Grammar.php b/src/Illuminate/Database/Schema/Grammars/Grammar.php index c6325b57cd04..9933a38be0fc 100755 --- a/src/Illuminate/Database/Schema/Grammars/Grammar.php +++ b/src/Illuminate/Database/Schema/Grammars/Grammar.php @@ -332,7 +332,7 @@ public function getDoctrineTableDiff(Blueprint $blueprint, SchemaManager $schema $table = $schema->introspectTable($tableName); - return new TableDiff(tableName: $tableName, fromTable: $table); + return $schema->createComparator()->compareTables(oldTable: $table, newTable: $table); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 124aeb7a2fed..7f940b95f975 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -78,6 +78,8 @@ public function compileTableExists() /** * Compile the query to determine the list of columns. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileColumnListing() @@ -85,6 +87,26 @@ public function compileColumnListing() return 'select column_name as `column_name` from information_schema.columns where table_schema = ? and table_name = ?'; } + /** + * Compile the query to determine the columns. + * + * @param string $database + * @param string $table + * @return string + */ + public function compileColumns($database, $table) + { + return sprintf( + 'select column_name as `name`, data_type as `type_name`, column_type as `type`, ' + .'collation_name as `collation`, is_nullable as `nullable`, ' + .'column_default as `default`, column_comment AS `comment`, extra as `extra` ' + .'from information_schema.columns where table_schema = %s and table_name = %s ' + .'order by ordinal_position asc', + $this->quoteString($database), + $this->quoteString($table) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 2efb2541525f..845e818de542 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -81,6 +81,8 @@ public function compileTableExists() /** * Compile the query to determine the list of columns. * + * @deprecated Will be removed in a future Laravel version. + * * @return string */ public function compileColumnListing() @@ -88,6 +90,30 @@ public function compileColumnListing() return 'select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'; } + /** + * Compile the query to determine the columns. + * + * @param string $database + * @param string $schema + * @param string $table + * @return string + */ + public function compileColumns($database, $schema, $table) + { + return sprintf( + 'select quote_ident(a.attname) as name, t.typname as type_name, format_type(a.atttypid, a.atttypmod) as type, ' + .'(select tc.collcollate from pg_catalog.pg_collation tc where tc.oid = a.attcollation) as collation, ' + .'not a.attnotnull as nullable, ' + .'(select pg_get_expr(adbin, adrelid) from pg_attrdef where c.oid = pg_attrdef.adrelid and pg_attrdef.adnum = a.attnum) as default, ' + .'(select pg_description.description from pg_description where pg_description.objoid = c.oid and a.attnum = pg_description.objsubid) as comment ' + .'from pg_attribute a, pg_class c, pg_type t, pg_namespace n ' + .'where c.relname = %s and n.nspname = %s and a.attnum > 0 and a.attrelid = c.oid and a.atttypid = t.oid and n.oid = c.relnamespace ' + .'order by a.attnum', + $this->quoteString($table), + $this->quoteString($schema) + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/Grammars/RenameColumn.php b/src/Illuminate/Database/Schema/Grammars/RenameColumn.php index a10e72b2f1f0..ff611c93160a 100644 --- a/src/Illuminate/Database/Schema/Grammars/RenameColumn.php +++ b/src/Illuminate/Database/Schema/Grammars/RenameColumn.php @@ -62,11 +62,20 @@ protected static function getRenamedDiff(Grammar $grammar, Blueprint $blueprint, */ protected static function setRenamedColumns(TableDiff $tableDiff, Fluent $command, Column $column) { - $tableDiff->renamedColumns = [ - $command->from => new Column($command->to, $column->getType(), self::getWritableColumnOptions($column)), - ]; - - return $tableDiff; + return new TableDiff( + $tableDiff->getOldTable(), + $tableDiff->getAddedColumns(), + $tableDiff->getModifiedColumns(), + $tableDiff->getDroppedColumns(), + [$command->from => new Column($command->to, $column->getType(), self::getWritableColumnOptions($column))], + $tableDiff->getAddedIndexes(), + $tableDiff->getModifiedIndexes(), + $tableDiff->getDroppedIndexes(), + $tableDiff->getRenamedIndexes(), + $tableDiff->getAddedForeignKeys(), + $tableDiff->getModifiedColumns(), + $tableDiff->getDroppedForeignKeys(), + ); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 9c3b1c9fb61e..4a07a52ecc11 100755 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -3,6 +3,7 @@ namespace Illuminate\Database\Schema\Grammars; use Doctrine\DBAL\Schema\Index; +use Doctrine\DBAL\Schema\TableDiff; use Illuminate\Database\Connection; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Blueprint; @@ -39,6 +40,8 @@ public function compileTableExists() /** * Compile the query to determine the list of columns. * + * @deprecated Will be removed in a future Laravel version. + * * @param string $table * @return string */ @@ -47,6 +50,21 @@ public function compileColumnListing($table) return 'pragma table_info('.$this->wrap(str_replace('.', '__', $table)).')'; } + /** + * Compile the query to determine the columns. + * + * @param string $table + * @return string + */ + public function compileColumns($table) + { + return sprintf( + "select name, type, not 'notnull' as 'nullable', dflt_value as 'default', pk as 'primary' " + .'from pragma_table_info(%s) order by cid asc', + $this->wrap(str_replace('.', '__', $table)) + ); + } + /** * Compile a create table command. * @@ -318,13 +336,32 @@ public function compileDropColumn(Blueprint $blueprint, Fluent $command, Connect $blueprint, $schema = $connection->getDoctrineSchemaManager() ); + $droppedColumns = []; + foreach ($command->columns as $name) { - $tableDiff->removedColumns[$name] = $connection->getDoctrineColumn( + $droppedColumns[$name] = $connection->getDoctrineColumn( $this->getTablePrefix().$blueprint->getTable(), $name ); } - return (array) $schema->getDatabasePlatform()->getAlterTableSQL($tableDiff); + $platform = $connection->getDoctrineConnection()->getDatabasePlatform(); + + return (array) $platform->getAlterTableSQL( + new TableDiff( + $tableDiff->getOldTable(), + $tableDiff->getAddedColumns(), + $tableDiff->getModifiedColumns(), + $droppedColumns, + $tableDiff->getRenamedColumns(), + $tableDiff->getAddedIndexes(), + $tableDiff->getModifiedIndexes(), + $tableDiff->getDroppedIndexes(), + $tableDiff->getRenamedIndexes(), + $tableDiff->getAddedForeignKeys(), + $tableDiff->getModifiedColumns(), + $tableDiff->getDroppedForeignKeys(), + ) + ); } } diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index a2b56afacf61..d70aedadc0e9 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -79,6 +79,8 @@ public function compileTableExists() /** * Compile the query to determine the list of columns. * + * @deprecated Will be removed in a future Laravel version. + * * @param string $table * @return string */ @@ -87,6 +89,31 @@ public function compileColumnListing($table) return "select name from sys.columns where object_id = object_id('$table')"; } + /** + * Compile the query to determine the columns. + * + * @param string $table + * @return string + */ + public function compileColumns($table) + { + return sprintf( + 'select col.name, type.name as type_name, ' + .'col.max_length as length, col.precision as precision, col.scale as places, ' + .'col.is_nullable as nullable, def.definition as [default], ' + .'col.is_identity as autoincrement, col.collation_name as collation, ' + .'cast(prop.value as nvarchar(max)) as comment ' + .'from sys.columns as col ' + .'join sys.types as type on col.user_type_id = type.user_type_id ' + .'join sys.objects as obj on col.object_id = obj.object_id ' + .'join sys.schemas as scm on obj.schema_id = scm.schema_id ' + .'left join sys.default_constraints def on col.default_object_id = def.object_id and col.object_id = def.parent_object_id ' + ."left join sys.extended_properties as prop on obj.object_id = prop.major_id and col.column_id = prop.minor_id and prop.name = 'MS_Description' " + ."where obj.type = 'U' and obj.name = %s and scm.name = SCHEMA_NAME()", + $this->quoteString($table), + ); + } + /** * Compile a create table command. * diff --git a/src/Illuminate/Database/Schema/MySqlBuilder.php b/src/Illuminate/Database/Schema/MySqlBuilder.php index bbb0763a721d..3fcb73764fbe 100755 --- a/src/Illuminate/Database/Schema/MySqlBuilder.php +++ b/src/Illuminate/Database/Schema/MySqlBuilder.php @@ -46,20 +46,20 @@ public function hasTable($table) } /** - * Get the column listing for a given table. + * Get the columns for a given table. * * @param string $table * @return array */ - public function getColumnListing($table) + public function getColumns($table) { $table = $this->connection->getTablePrefix().$table; $results = $this->connection->selectFromWriteConnection( - $this->grammar->compileColumnListing(), [$this->connection->getDatabaseName(), $table] + $this->grammar->compileColumns($this->connection->getDatabaseName(), $table) ); - return $this->connection->getPostProcessor()->processColumnListing($results); + return $this->connection->getPostProcessor()->processColumns($results); } /** diff --git a/src/Illuminate/Database/Schema/PostgresBuilder.php b/src/Illuminate/Database/Schema/PostgresBuilder.php index adfbd688ee28..34a75ceb5fd5 100755 --- a/src/Illuminate/Database/Schema/PostgresBuilder.php +++ b/src/Illuminate/Database/Schema/PostgresBuilder.php @@ -176,22 +176,22 @@ public function getAllTypes() } /** - * Get the column listing for a given table. + * Get the columns for a given table. * * @param string $table * @return array */ - public function getColumnListing($table) + public function getColumns($table) { [$database, $schema, $table] = $this->parseSchemaAndTable($table); $table = $this->connection->getTablePrefix().$table; $results = $this->connection->selectFromWriteConnection( - $this->grammar->compileColumnListing(), [$database, $schema, $table] + $this->grammar->compileColumns($database, $schema, $table) ); - return $this->connection->getPostProcessor()->processColumnListing($results); + return $this->connection->getPostProcessor()->processColumns($results); } /** diff --git a/src/Illuminate/Database/composer.json b/src/Illuminate/Database/composer.json index 3c76a0174268..c840493bd621 100644 --- a/src/Illuminate/Database/composer.json +++ b/src/Illuminate/Database/composer.json @@ -36,13 +36,13 @@ }, "suggest": { "ext-filter": "Required to use the Postgres database driver.", - "doctrine/dbal": "Required to rename columns and drop SQLite columns (^3.5.1).", + "doctrine/dbal": "Required to rename columns and drop SQLite columns (^4.0).", "fakerphp/faker": "Required to use the eloquent factory builder (^1.21).", "illuminate/console": "Required to use the database commands (^11.0).", "illuminate/events": "Required to use the observers with Eloquent (^11.0).", "illuminate/filesystem": "Required to use the migrations (^11.0).", "illuminate/pagination": "Required to paginate the result set (^11.0).", - "symfony/finder": "Required to use Eloquent model factories (^6.3)." + "symfony/finder": "Required to use Eloquent model factories (^7.0)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Filesystem/composer.json b/src/Illuminate/Filesystem/composer.json index b2657b376d75..b46be7d136d2 100644 --- a/src/Illuminate/Filesystem/composer.json +++ b/src/Illuminate/Filesystem/composer.json @@ -19,7 +19,7 @@ "illuminate/contracts": "^11.0", "illuminate/macroable": "^11.0", "illuminate/support": "^11.0", - "symfony/finder": "^6.3" + "symfony/finder": "^7.0" }, "autoload": { "psr-4": { @@ -41,8 +41,8 @@ "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", - "symfony/filesystem": "Required to enable support for relative symbolic links (^6.3).", - "symfony/mime": "Required to enable support for guessing extensions (^6.3)." + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.0).", + "symfony/mime": "Required to enable support for guessing extensions (^7.0)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 57d4f23f6751..b95ccc31e476 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -709,6 +709,24 @@ public function runningInConsole() return $this->isRunningInConsole; } + /** + * Determine if the application is running any of the given console commands. + * + * @param string|array ...$commands + * @return bool + */ + public function runningConsoleCommand(...$commands) + { + if (! $this->runningInConsole()) { + return false; + } + + return in_array( + $_SERVER['argv'][1] ?? null, + is_array($commands[0]) ? $commands[0] : $commands + ); + } + /** * Determine if the application is running unit tests. * diff --git a/src/Illuminate/Foundation/Console/CliDumper.php b/src/Illuminate/Foundation/Console/CliDumper.php index beed2f2af9f4..304dfcb0c351 100644 --- a/src/Illuminate/Foundation/Console/CliDumper.php +++ b/src/Illuminate/Foundation/Console/CliDumper.php @@ -94,7 +94,7 @@ public function dumpWithSource(Data $data) $output = (string) $this->dump($data, true); $lines = explode("\n", $output); - $lines[0] .= $this->getDumpSourceContent(); + $lines[array_key_last($lines) - 1] .= $this->getDumpSourceContent(); $this->output->write(implode("\n", $lines)); diff --git a/src/Illuminate/Foundation/Console/ClosureCommand.php b/src/Illuminate/Foundation/Console/ClosureCommand.php index 4cd54e8e4a79..c5be75ecc103 100644 --- a/src/Illuminate/Foundation/Console/ClosureCommand.php +++ b/src/Illuminate/Foundation/Console/ClosureCommand.php @@ -39,7 +39,7 @@ public function __construct($signature, Closure $callback) * @param \Symfony\Component\Console\Output\OutputInterface $output * @return int */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $inputs = array_merge($input->getArguments(), $input->getOptions()); diff --git a/src/Illuminate/Foundation/Console/QueuedCommand.php b/src/Illuminate/Foundation/Console/QueuedCommand.php index fb3d027b4b0a..43848cc263e3 100644 --- a/src/Illuminate/Foundation/Console/QueuedCommand.php +++ b/src/Illuminate/Foundation/Console/QueuedCommand.php @@ -39,4 +39,14 @@ public function handle(KernelContract $kernel) { $kernel->call(...array_values($this->data)); } + + /** + * Get the display name for the queued job. + * + * @return string + */ + public function displayName() + { + return array_values($this->data)[0]; + } } diff --git a/src/Illuminate/Foundation/Console/RouteListCommand.php b/src/Illuminate/Foundation/Console/RouteListCommand.php index 7cd48f5d761c..11a8c7247cbe 100644 --- a/src/Illuminate/Foundation/Console/RouteListCommand.php +++ b/src/Illuminate/Foundation/Console/RouteListCommand.php @@ -90,7 +90,9 @@ public function __construct(Router $router) */ public function handle() { - $this->router->flushMiddlewareGroups(); + if (! $this->output->isVeryVerbose()) { + $this->router->flushMiddlewareGroups(); + } if (! $this->router->getRoutes()->count()) { return $this->components->error("Your application doesn't have any routes."); diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 301918317dd7..c877e772df3c 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -302,7 +302,7 @@ protected function reportThrowable(Throwable $e): void } try { - $logger = $this->container->make(LoggerInterface::class); + $logger = $this->newLogger(); } catch (Exception) { throw $e; } @@ -360,7 +360,7 @@ protected function shouldntReport(Throwable $e) with($throttle->key ?: 'illuminate:foundation:exceptions:'.$e::class, fn ($key) => $this->hashThrottleKeys ? md5($key) : $key), $throttle->maxAttempts, fn () => true, - 60 * $throttle->decayMinutes + $throttle->decaySeconds ); }), rescue: false, report: false); } @@ -719,10 +719,16 @@ protected function renderHttpException(HttpExceptionInterface $e) $this->registerErrorViewPaths(); if ($view = $this->getHttpExceptionView($e)) { - return response()->view($view, [ - 'errors' => new ViewErrorBag, - 'exception' => $e, - ], $e->getStatusCode(), $e->getHeaders()); + try { + return response()->view($view, [ + 'errors' => new ViewErrorBag, + 'exception' => $e, + ], $e->getStatusCode(), $e->getHeaders()); + } catch (Throwable $t) { + config('app.debug') && throw $t; + + $this->report($t); + } } return $this->convertExceptionToResponse($e); @@ -872,4 +878,14 @@ protected function isHttpException(Throwable $e) { return $e instanceof HttpExceptionInterface; } + + /** + * Create a new logger instance. + * + * @return \Psr\Log\LoggerInterface + */ + protected function newLogger() + { + return $this->container->make(LoggerInterface::class); + } } diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 2a88d9c5c4a4..772a2635a3b4 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -11,7 +11,6 @@ use Illuminate\Http\Request; use Illuminate\Routing\Redirector; use Illuminate\Validation\ValidatesWhenResolvedTrait; -use Illuminate\Validation\ValidationException; class FormRequest extends Request implements ValidatesWhenResolved { @@ -152,7 +151,9 @@ public function validationData() */ protected function failedValidation(Validator $validator) { - throw (new ValidationException($validator)) + $exception = $validator->getException(); + + throw (new $exception($validator)) ->errorBag($this->errorBag) ->redirectTo($this->getRedirectUrl()); } diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php b/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php index b2e2de142e99..08c1635443a6 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactionsManager.php @@ -42,6 +42,6 @@ public function callbackApplicableTransactions() */ public function afterCommitCallbacksShouldBeExecuted($level) { - return $level === 2; + return $level === 1; } } diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 8d8f23e57254..a42ce7ff98bc 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -162,21 +162,15 @@ protected function setUpTraits() /** * {@inheritdoc} */ - protected function runTest(): mixed + protected function transformException(Throwable $error): Throwable { - $result = null; + $response = static::$latestResponse ?? null; - try { - $result = parent::runTest(); - } catch (Throwable $e) { - if (! is_null(static::$latestResponse)) { - static::$latestResponse->transformNotSuccessfulException($e); - } - - throw $e; + if (! is_null($response)) { + $response->transformNotSuccessfulException($error); } - return $result; + return $error; } /** diff --git a/src/Illuminate/Hashing/Argon2IdHasher.php b/src/Illuminate/Hashing/Argon2IdHasher.php index 9aca47ac9c71..f12f806e0654 100644 --- a/src/Illuminate/Hashing/Argon2IdHasher.php +++ b/src/Illuminate/Hashing/Argon2IdHasher.php @@ -18,7 +18,7 @@ class Argon2IdHasher extends ArgonHasher */ public function check($value, $hashedValue, array $options = []) { - if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'argon2id') { + if ($this->verifyAlgorithm && ! $this->isUsingCorrectAlgorithm($hashedValue)) { throw new RuntimeException('This password does not use the Argon2id algorithm.'); } @@ -29,6 +29,17 @@ public function check($value, $hashedValue, array $options = []) return password_verify($value, $hashedValue); } + /** + * Verify the hashed value's algorithm. + * + * @param string $hashedValue + * @return bool + */ + protected function isUsingCorrectAlgorithm($hashedValue) + { + return $this->info($hashedValue)['algoName'] === 'argon2id'; + } + /** * Get the algorithm that should be used for hashing. * diff --git a/src/Illuminate/Hashing/ArgonHasher.php b/src/Illuminate/Hashing/ArgonHasher.php index b999257f4b52..4cbb38b465ea 100644 --- a/src/Illuminate/Hashing/ArgonHasher.php +++ b/src/Illuminate/Hashing/ArgonHasher.php @@ -95,7 +95,7 @@ protected function algorithm() */ public function check($value, $hashedValue, array $options = []) { - if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'argon2i') { + if ($this->verifyAlgorithm && ! $this->isUsingCorrectAlgorithm($hashedValue)) { throw new RuntimeException('This password does not use the Argon2i algorithm.'); } @@ -118,6 +118,56 @@ public function needsRehash($hashedValue, array $options = []) ]); } + /** + * Verifies that the configuration is less than or equal to what is configured. + * + * @internal + */ + public function verifyConfiguration($value) + { + return $this->isUsingCorrectAlgorithm($value) && $this->isUsingValidOptions($value); + } + + /** + * Verify the hashed value's algorithm. + * + * @param string $hashedValue + * @return bool + */ + protected function isUsingCorrectAlgorithm($hashedValue) + { + return $this->info($hashedValue)['algoName'] === 'argon2i'; + } + + /** + * Verify the hashed value's options. + * + * @param string $hashedValue + * @return bool + */ + protected function isUsingValidOptions($hashedValue) + { + ['options' => $options] = $this->info($hashedValue); + + if ( + ! is_int($options['memory_cost'] ?? null) || + ! is_int($options['time_cost'] ?? null) || + ! is_int($options['threads'] ?? null) + ) { + return false; + } + + if ( + $options['memory_cost'] > $this->memory || + $options['time_cost'] > $this->time || + $options['threads'] > $this->threads + ) { + return false; + } + + return true; + } + /** * Set the default password memory factor. * diff --git a/src/Illuminate/Hashing/BcryptHasher.php b/src/Illuminate/Hashing/BcryptHasher.php index f74edab88805..50b3859ed81c 100755 --- a/src/Illuminate/Hashing/BcryptHasher.php +++ b/src/Illuminate/Hashing/BcryptHasher.php @@ -67,7 +67,7 @@ public function make($value, array $options = []) */ public function check($value, $hashedValue, array $options = []) { - if ($this->verifyAlgorithm && $this->info($hashedValue)['algoName'] !== 'bcrypt') { + if ($this->verifyAlgorithm && ! $this->isUsingCorrectAlgorithm($hashedValue)) { throw new RuntimeException('This password does not use the Bcrypt algorithm.'); } @@ -88,6 +88,48 @@ public function needsRehash($hashedValue, array $options = []) ]); } + /** + * Verifies that the configuration is less than or equal to what is configured. + * + * @internal + */ + public function verifyConfiguration($value) + { + return $this->isUsingCorrectAlgorithm($value) && $this->isUsingValidOptions($value); + } + + /** + * Verify the hashed value's algorithm. + * + * @param string $hashedValue + * @return bool + */ + protected function isUsingCorrectAlgorithm($hashedValue) + { + return $this->info($hashedValue)['algoName'] === 'bcrypt'; + } + + /** + * Verify the hashed value's options. + * + * @param string $hashedValue + * @return bool + */ + protected function isUsingValidOptions($hashedValue) + { + ['options' => $options] = $this->info($hashedValue); + + if (! is_int($options['cost'] ?? null)) { + return false; + } + + if ($options['cost'] > $this->rounds) { + return false; + } + + return true; + } + /** * Set the default password work factor. * diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index 251bb2c7c940..4707dbe1fd4a 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -533,7 +533,7 @@ protected function filterFiles($files) */ public function hasSession(bool $skipIfUninitialized = false): bool { - return ! is_null($this->session); + return $this->session instanceof SymfonySessionDecorator; } /** @@ -542,7 +542,7 @@ public function hasSession(bool $skipIfUninitialized = false): bool public function getSession(): SessionInterface { return $this->hasSession() - ? new SymfonySessionDecorator($this->session()) + ? $this->session : throw new SessionNotFoundException; } @@ -559,7 +559,7 @@ public function session() throw new RuntimeException('Session store not set on request.'); } - return $this->session; + return $this->session->store; } /** @@ -570,7 +570,7 @@ public function session() */ public function setLaravelSession($session) { - $this->session = $session; + $this->session = new SymfonySessionDecorator($session); } /** diff --git a/src/Illuminate/Http/Resources/Json/ResourceResponse.php b/src/Illuminate/Http/Resources/Json/ResourceResponse.php index 51f36576f0ae..430e41a72950 100644 --- a/src/Illuminate/Http/Resources/Json/ResourceResponse.php +++ b/src/Illuminate/Http/Resources/Json/ResourceResponse.php @@ -53,7 +53,7 @@ public function toResponse($request) /** * Wrap the given data if necessary. * - * @param array $data + * @param \Illuminate\Support\Collection|array $data * @param array $with * @param array $additional * @return array diff --git a/src/Illuminate/Http/composer.json b/src/Illuminate/Http/composer.json index 05c6b9d00eba..768d72bc6637 100755 --- a/src/Illuminate/Http/composer.json +++ b/src/Illuminate/Http/composer.json @@ -16,15 +16,15 @@ "require": { "php": "^8.2", "ext-filter": "*", - "fruitcake/php-cors": "^1.2", + "fruitcake/php-cors": "^1.3", "guzzlehttp/uri-template": "^1.0", "illuminate/collections": "^11.0", "illuminate/macroable": "^11.0", "illuminate/session": "^11.0", "illuminate/support": "^11.0", - "symfony/http-foundation": "^6.3", - "symfony/http-kernel": "^6.3", - "symfony/mime": "^6.3" + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0", + "symfony/mime": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Mail/Transport/SesTransport.php b/src/Illuminate/Mail/Transport/SesTransport.php index 7929bc13803d..7ef78eb582fa 100644 --- a/src/Illuminate/Mail/Transport/SesTransport.php +++ b/src/Illuminate/Mail/Transport/SesTransport.php @@ -4,8 +4,8 @@ use Aws\Exception\AwsException; use Aws\Ses\SesClient; -use Exception; use Stringable; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Header\MetadataHeader; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; @@ -76,7 +76,7 @@ protected function doSend(SentMessage $message): void } catch (AwsException $e) { $reason = $e->getAwsErrorMessage() ?? $e->getMessage(); - throw new Exception( + throw new TransportException( sprintf('Request to AWS SES API failed. Reason: %s.', $reason), is_int($e->getCode()) ? $e->getCode() : 0, $e diff --git a/src/Illuminate/Mail/Transport/SesV2Transport.php b/src/Illuminate/Mail/Transport/SesV2Transport.php index bbeaa1bd5911..e874e583abd5 100644 --- a/src/Illuminate/Mail/Transport/SesV2Transport.php +++ b/src/Illuminate/Mail/Transport/SesV2Transport.php @@ -4,8 +4,8 @@ use Aws\Exception\AwsException; use Aws\SesV2\SesV2Client; -use Exception; use Stringable; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Header\MetadataHeader; use Symfony\Component\Mailer\SentMessage; use Symfony\Component\Mailer\Transport\AbstractTransport; @@ -80,7 +80,7 @@ protected function doSend(SentMessage $message): void } catch (AwsException $e) { $reason = $e->getAwsErrorMessage() ?? $e->getMessage(); - throw new Exception( + throw new TransportException( sprintf('Request to AWS SES V2 API failed. Reason: %s.', $reason), is_int($e->getCode()) ? $e->getCode() : 0, $e diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index 42a64ccdac78..ad0211e96d7c 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -22,7 +22,7 @@ "illuminate/support": "^11.0", "league/commonmark": "^2.2", "psr/log": "^1.0|^2.0|^3.0", - "symfony/mailer": "^6.3", + "symfony/mailer": "^7.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5" }, "autoload": { @@ -37,9 +37,9 @@ }, "suggest": { "aws/aws-sdk-php": "Required to use the SES mail driver (^3.235.5).", - "symfony/http-client": "Required to use the Symfony API mail transports (^6.3).", - "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^6.3).", - "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^6.3)." + "symfony/http-client": "Required to use the Symfony API mail transports (^7.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.0)." }, "config": { "sort-packages": true diff --git a/src/Illuminate/Process/composer.json b/src/Illuminate/Process/composer.json index c056b4aac494..76eca7284827 100644 --- a/src/Illuminate/Process/composer.json +++ b/src/Illuminate/Process/composer.json @@ -19,7 +19,7 @@ "illuminate/contracts": "^10.0", "illuminate/macroable": "^10.0", "illuminate/support": "^10.0", - "symfony/process": "^6.3" + "symfony/process": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Queue/Console/RetryCommand.php b/src/Illuminate/Queue/Console/RetryCommand.php index 106a8ec4ede8..37827dd01683 100644 --- a/src/Illuminate/Queue/Console/RetryCommand.php +++ b/src/Illuminate/Queue/Console/RetryCommand.php @@ -21,7 +21,7 @@ class RetryCommand extends Command protected $signature = 'queue:retry {id?* : The ID of the failed job or "all" to retry all jobs} {--queue= : Retry all of the failed jobs for the specified queue} - {--range=* : Range of job IDs (numeric) to be retried}'; + {--range=* : Range of job IDs (numeric) to be retried (e.g. 1-5)}'; /** * The console command description. diff --git a/src/Illuminate/Queue/Middleware/RateLimited.php b/src/Illuminate/Queue/Middleware/RateLimited.php index c8546338386e..4ebdc2677e93 100644 --- a/src/Illuminate/Queue/Middleware/RateLimited.php +++ b/src/Illuminate/Queue/Middleware/RateLimited.php @@ -69,7 +69,7 @@ public function handle($job, $next) return (object) [ 'key' => md5($this->limiterName.$limit->key), 'maxAttempts' => $limit->maxAttempts, - 'decayMinutes' => $limit->decayMinutes, + 'decaySeconds' => $limit->decaySeconds, ]; })->all() ); @@ -92,7 +92,7 @@ protected function handleJob($job, $next, array $limits) : false; } - $this->limiter->hit($limit->key, $limit->decayMinutes * 60); + $this->limiter->hit($limit->key, $limit->decaySeconds); } return $next($job); diff --git a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php index e919786f27c6..cbc809549333 100644 --- a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php +++ b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php @@ -49,7 +49,7 @@ public function __construct($limiterName) protected function handleJob($job, $next, array $limits) { foreach ($limits as $limit) { - if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) { + if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) { return $this->shouldRelease ? $job->release($this->getTimeUntilNextRetry($limit->key)) : false; @@ -64,13 +64,13 @@ protected function handleJob($job, $next, array $limits) * * @param string $key * @param int $maxAttempts - * @param int $decayMinutes + * @param int $decaySeconds * @return bool */ - protected function tooManyAttempts($key, $maxAttempts, $decayMinutes) + protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) { $limiter = new DurationLimiter( - $this->redis, $key, $maxAttempts, $decayMinutes * 60 + $this->redis, $key, $maxAttempts, $decaySeconds ); return tap(! $limiter->acquire(), function () use ($key, $limiter) { diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 61ddf343e499..60ba1801c7d8 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -30,11 +30,11 @@ class ThrottlesExceptions protected $maxAttempts; /** - * The number of minutes until the maximum attempts are reset. + * The number of seconds until the maximum attempts are reset. * * @var int */ - protected $decayMinutes; + protected $decaySeconds; /** * The number of minutes to wait before retrying the job after an exception. @@ -68,13 +68,13 @@ class ThrottlesExceptions * Create a new middleware instance. * * @param int $maxAttempts - * @param int $decayMinutes + * @param int $decaySeconds * @return void */ - public function __construct($maxAttempts = 10, $decayMinutes = 10) + public function __construct($maxAttempts = 10, $decaySeconds = 600) { $this->maxAttempts = $maxAttempts; - $this->decayMinutes = $decayMinutes; + $this->decaySeconds = $decaySeconds; } /** @@ -101,7 +101,7 @@ public function handle($job, $next) throw $throwable; } - $this->limiter->hit($jobKey, $this->decayMinutes * 60); + $this->limiter->hit($jobKey, $this->decaySeconds); return $job->release($this->retryAfterMinutes * 60); } diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php index 38790e353e2d..0dbc9443b4b0 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -38,7 +38,7 @@ public function handle($job, $next) $this->redis = Container::getInstance()->make(Redis::class); $this->limiter = new DurationLimiter( - $this->redis, $this->getKey($job), $this->maxAttempts, $this->decayMinutes * 60 + $this->redis, $this->getKey($job), $this->maxAttempts, $this->decaySeconds ); if ($this->limiter->tooManyAttempts()) { diff --git a/src/Illuminate/Queue/composer.json b/src/Illuminate/Queue/composer.json index f341b6768392..66bc9c0f4afd 100644 --- a/src/Illuminate/Queue/composer.json +++ b/src/Illuminate/Queue/composer.json @@ -25,7 +25,7 @@ "illuminate/support": "^11.0", "laravel/serializable-closure": "^1.2.2", "ramsey/uuid": "^4.7", - "symfony/process": "^6.3" + "symfony/process": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Routing/Middleware/ThrottleRequests.php b/src/Illuminate/Routing/Middleware/ThrottleRequests.php index bf6cb92aefa2..227a7579c5ab 100644 --- a/src/Illuminate/Routing/Middleware/ThrottleRequests.php +++ b/src/Illuminate/Routing/Middleware/ThrottleRequests.php @@ -94,7 +94,7 @@ public function handle($request, Closure $next, $maxAttempts = 60, $decayMinutes (object) [ 'key' => $prefix.$this->resolveRequestSignature($request), 'maxAttempts' => $this->resolveMaxAttempts($request, $maxAttempts), - 'decayMinutes' => $decayMinutes, + 'decaySeconds' => 60 * $decayMinutes, 'responseCallback' => null, ], ] @@ -129,7 +129,7 @@ protected function handleRequestUsingNamedLimiter($request, Closure $next, $limi return (object) [ 'key' => self::$shouldHashKeys ? md5($limiterName.$limit->key) : $limiterName.':'.$limit->key, 'maxAttempts' => $limit->maxAttempts, - 'decayMinutes' => $limit->decayMinutes, + 'decaySeconds' => $limit->decaySeconds, 'responseCallback' => $limit->responseCallback, ]; })->all() @@ -153,7 +153,7 @@ protected function handleRequest($request, Closure $next, array $limits) throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback); } - $this->limiter->hit($limit->key, $limit->decayMinutes * 60); + $this->limiter->hit($limit->key, $limit->decaySeconds); } $response = $next($request); diff --git a/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php b/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php index e818c69f14c7..20afd95dd9b8 100644 --- a/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php +++ b/src/Illuminate/Routing/Middleware/ThrottleRequestsWithRedis.php @@ -57,7 +57,7 @@ public function __construct(RateLimiter $limiter, Redis $redis) protected function handleRequest($request, Closure $next, array $limits) { foreach ($limits as $limit) { - if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decayMinutes)) { + if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) { throw $this->buildException($request, $limit->key, $limit->maxAttempts, $limit->responseCallback); } } @@ -80,13 +80,13 @@ protected function handleRequest($request, Closure $next, array $limits) * * @param string $key * @param int $maxAttempts - * @param int $decayMinutes + * @param int $decaySeconds * @return mixed */ - protected function tooManyAttempts($key, $maxAttempts, $decayMinutes) + protected function tooManyAttempts($key, $maxAttempts, $decaySeconds) { $limiter = new DurationLimiter( - $this->getRedisConnection(), $key, $maxAttempts, $decayMinutes * 60 + $this->getRedisConnection(), $key, $maxAttempts, $decaySeconds ); return tap(! $limiter->acquire(), function () use ($key, $limiter) { diff --git a/src/Illuminate/Routing/RoutingServiceProvider.php b/src/Illuminate/Routing/RoutingServiceProvider.php index 696e4ca4868d..a2ca675da4e2 100755 --- a/src/Illuminate/Routing/RoutingServiceProvider.php +++ b/src/Illuminate/Routing/RoutingServiceProvider.php @@ -137,8 +137,10 @@ protected function registerPsrRequest() if (class_exists(Psr17Factory::class) && class_exists(PsrHttpFactory::class)) { $psr17Factory = new Psr17Factory; - return (new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory)) - ->createRequest($app->make('request')); + return with((new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory)) + ->createRequest($illuminateRequest = $app->make('request')), fn ($request) => $request->withParsedBody( + array_merge($request->getParsedBody() ?? [], $illuminateRequest->getPayload()->all()) + )); } throw new BindingResolutionException('Unable to resolve PSR request. Please install the symfony/psr-http-message-bridge and nyholm/psr7 packages.'); diff --git a/src/Illuminate/Routing/composer.json b/src/Illuminate/Routing/composer.json index 82825c675cab..92d08b0908fd 100644 --- a/src/Illuminate/Routing/composer.json +++ b/src/Illuminate/Routing/composer.json @@ -25,9 +25,9 @@ "illuminate/pipeline": "^11.0", "illuminate/session": "^11.0", "illuminate/support": "^11.0", - "symfony/http-foundation": "^6.3", - "symfony/http-kernel": "^6.3", - "symfony/routing": "^6.3" + "symfony/http-foundation": "^7.0", + "symfony/http-kernel": "^7.0", + "symfony/routing": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Session/Middleware/AuthenticateSession.php b/src/Illuminate/Session/Middleware/AuthenticateSession.php index 7e96f683e0f6..43982374b456 100644 --- a/src/Illuminate/Session/Middleware/AuthenticateSession.php +++ b/src/Illuminate/Session/Middleware/AuthenticateSession.php @@ -111,7 +111,7 @@ protected function guard() } /** - * Get the path the user should be redirected to when their session is not autheneticated. + * Get the path the user should be redirected to when their session is not authenticated. * * @param \Illuminate\Http\Request $request * @return string|null diff --git a/src/Illuminate/Session/Middleware/StartSession.php b/src/Illuminate/Session/Middleware/StartSession.php index 8079ab9c145a..53f3a16c6ba2 100644 --- a/src/Illuminate/Session/Middleware/StartSession.php +++ b/src/Illuminate/Session/Middleware/StartSession.php @@ -80,7 +80,7 @@ protected function handleRequestWhileBlocking(Request $request, $session, Closur $lockFor = $request->route() && $request->route()->locksFor() ? $request->route()->locksFor() - : 10; + : $this->manager->defaultRouteBlockLockSeconds(); $lock = $this->cache($this->manager->blockDriver()) ->lock('session:'.$session->getId(), $lockFor) @@ -90,7 +90,7 @@ protected function handleRequestWhileBlocking(Request $request, $session, Closur $lock->block( ! is_null($request->route()->waitsFor()) ? $request->route()->waitsFor() - : 10 + : $this->manager->defaultRouteBlockWaitSeconds() ); return $this->handleStatefulRequest($request, $session, $next); diff --git a/src/Illuminate/Session/SessionManager.php b/src/Illuminate/Session/SessionManager.php index 01112f01ecbf..6094627e6ef5 100755 --- a/src/Illuminate/Session/SessionManager.php +++ b/src/Illuminate/Session/SessionManager.php @@ -236,6 +236,26 @@ public function blockDriver() return $this->config->get('session.block_store'); } + /** + * Get the maximum number of seconds the session lock should be held for. + * + * @return int + */ + public function defaultRouteBlockLockSeconds() + { + return $this->config->get('session.block_lock_seconds', 10); + } + + /** + * Get the maximum number of seconds to wait while attempting to acquire a route block session lock. + * + * @return int + */ + public function defaultRouteBlockWaitSeconds() + { + return $this->config->get('session.block_wait_seconds', 10); + } + /** * Get the session configuration. * diff --git a/src/Illuminate/Session/SymfonySessionDecorator.php b/src/Illuminate/Session/SymfonySessionDecorator.php index 02034910bb9e..978816683fe7 100644 --- a/src/Illuminate/Session/SymfonySessionDecorator.php +++ b/src/Illuminate/Session/SymfonySessionDecorator.php @@ -13,9 +13,9 @@ class SymfonySessionDecorator implements SessionInterface /** * The underlying Laravel session store. * - * @var \Illuminate\Session\Store + * @var \Illuminate\Contracts\Session\Session */ - protected $store; + public readonly Session $store; /** * Create a new session decorator. @@ -46,10 +46,8 @@ public function getId(): string /** * {@inheritdoc} - * - * @return void */ - public function setId(string $id) + public function setId(string $id): void { $this->store->setId($id); } @@ -64,10 +62,8 @@ public function getName(): string /** * {@inheritdoc} - * - * @return void */ - public function setName(string $name) + public function setName(string $name): void { $this->store->setName($name); } @@ -94,10 +90,8 @@ public function migrate(bool $destroy = false, int $lifetime = null): bool /** * {@inheritdoc} - * - * @return void */ - public function save() + public function save(): void { $this->store->save(); } @@ -120,10 +114,8 @@ public function get(string $name, mixed $default = null): mixed /** * {@inheritdoc} - * - * @return void */ - public function set(string $name, mixed $value) + public function set(string $name, mixed $value): void { $this->store->put($name, $value); } @@ -138,10 +130,8 @@ public function all(): array /** * {@inheritdoc} - * - * @return void */ - public function replace(array $attributes) + public function replace(array $attributes): void { $this->store->replace($attributes); } @@ -156,10 +146,8 @@ public function remove(string $name): mixed /** * {@inheritdoc} - * - * @return void */ - public function clear() + public function clear(): void { $this->store->flush(); } @@ -174,10 +162,8 @@ public function isStarted(): bool /** * {@inheritdoc} - * - * @return void */ - public function registerBag(SessionBagInterface $bag) + public function registerBag(SessionBagInterface $bag): void { throw new BadMethodCallException('Method not implemented by Laravel.'); } diff --git a/src/Illuminate/Session/composer.json b/src/Illuminate/Session/composer.json index 85a08b559167..56789edc0f78 100755 --- a/src/Illuminate/Session/composer.json +++ b/src/Illuminate/Session/composer.json @@ -21,8 +21,8 @@ "illuminate/contracts": "^11.0", "illuminate/filesystem": "^11.0", "illuminate/support": "^11.0", - "symfony/finder": "^6.3", - "symfony/http-foundation": "^6.3" + "symfony/finder": "^7.0", + "symfony/http-foundation": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 80e300664331..a2b23eb49453 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -38,6 +38,7 @@ * @method static bool isProduction() * @method static string detectEnvironment(\Closure $callback) * @method static bool runningInConsole() + * @method static bool runningConsoleCommand(string|array ...$commands) * @method static bool runningUnitTests() * @method static bool hasDebugModeEnabled() * @method static void registerConfiguredProviders() diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 474772583694..d9de5c9a4581 100755 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -42,6 +42,7 @@ * @method static int affectingStatement(string $query, array $bindings = []) * @method static bool unprepared(string $query) * @method static array pretend(\Closure $callback) + * @method static mixed withoutPretending(\Closure $callback) * @method static void bindValues(\PDOStatement $statement, array $bindings) * @method static array prepareBindings(array $bindings) * @method static void logQuery(string $query, array $bindings, float|null $time = null) diff --git a/src/Illuminate/Support/Facades/Facade.php b/src/Illuminate/Support/Facades/Facade.php index c5a01e7ae2ec..80c58bba7b5b 100755 --- a/src/Illuminate/Support/Facades/Facade.php +++ b/src/Illuminate/Support/Facades/Facade.php @@ -46,11 +46,11 @@ public static function resolved(Closure $callback) $accessor = static::getFacadeAccessor(); if (static::$app->resolved($accessor) === true) { - $callback(static::getFacadeRoot()); + $callback(static::getFacadeRoot(), static::$app); } - static::$app->afterResolving($accessor, function ($service) use ($callback) { - $callback($service); + static::$app->afterResolving($accessor, function ($service, $app) use ($callback) { + $callback($service, $app); }); } diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index d4dc8f55dc2f..4a87bcc25cec 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -15,8 +15,9 @@ * @method static bool hasColumns(string $table, array $columns) * @method static void whenTableHasColumn(string $table, string $column, \Closure $callback) * @method static void whenTableDoesntHaveColumn(string $table, string $column, \Closure $callback) - * @method static string getColumnType(string $table, string $column) + * @method static string getColumnType(string $table, string $column, bool $fullDefinition = false) * @method static array getColumnListing(string $table) + * @method static array getColumns(string $table) * @method static void table(string $table, \Closure $callback) * @method static void create(string $table, \Closure $callback) * @method static void drop(string $table) diff --git a/src/Illuminate/Support/Facades/Session.php b/src/Illuminate/Support/Facades/Session.php index e1ea7534b55a..4baf6c93c40b 100755 --- a/src/Illuminate/Support/Facades/Session.php +++ b/src/Illuminate/Support/Facades/Session.php @@ -5,6 +5,8 @@ /** * @method static bool shouldBlock() * @method static string|null blockDriver() + * @method static int defaultRouteBlockLockSeconds() + * @method static int defaultRouteBlockWaitSeconds() * @method static array getSessionConfig() * @method static string getDefaultDriver() * @method static void setDefaultDriver(string $name) diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 28a92ea2bb2e..400123ecb204 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -15,6 +15,7 @@ use Ramsey\Uuid\Uuid; use Ramsey\Uuid\UuidFactory; use Symfony\Component\Uid\Ulid; +use Throwable; use Traversable; use voku\helper\ASCII; @@ -296,7 +297,7 @@ public static function containsAll($haystack, $needles, $ignoreCase = false) * * @param string $string * @param int $mode - * @param string $encoding + * @param string|null $encoding * @return string */ public static function convertCase(string $string, int $mode = MB_CASE_FOLD, ?string $encoding = 'UTF-8') @@ -339,7 +340,7 @@ public static function excerpt($text, $phrase = '', $options = []) $radius = $options['radius'] ?? 100; $omission = $options['omission'] ?? '...'; - preg_match('/^(.*?)('.preg_quote((string) $phrase).')(.*)$/iu', (string) $text, $matches); + preg_match('/^(.*?)('.preg_quote((string) $phrase, '/').')(.*)$/iu', (string) $text, $matches); if (empty($matches)) { return null; @@ -851,25 +852,33 @@ public static function pluralStudly($value, $count = 2) */ public static function password($length = 32, $letters = true, $numbers = true, $symbols = true, $spaces = false) { - return (new Collection) - ->when($letters, fn ($c) => $c->merge([ - 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', - 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', - 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', - 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', - 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', - ])) - ->when($numbers, fn ($c) => $c->merge([ - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - ])) - ->when($symbols, fn ($c) => $c->merge([ - '~', '!', '#', '$', '%', '^', '&', '*', '(', ')', '-', - '_', '.', ',', '<', '>', '?', '/', '\\', '{', '}', '[', - ']', '|', ':', ';', - ])) - ->when($spaces, fn ($c) => $c->merge([' '])) - ->pipe(fn ($c) => Collection::times($length, fn () => $c[random_int(0, $c->count() - 1)])) - ->implode(''); + $password = new Collection(); + + $options = (new Collection([ + 'letters' => $letters === true ? [ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ] : null, + 'numbers' => $numbers === true ? [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + ] : null, + 'symbols' => $symbols === true ? [ + '~', '!', '#', '$', '%', '^', '&', '*', '(', ')', '-', + '_', '.', ',', '<', '>', '?', '/', '\\', '{', '}', '[', + ']', '|', ':', ';', + ] : null, + 'spaces' => $spaces === true ? [' '] : null, + ]))->filter()->each(fn ($c) => $password->push($c[random_int(0, count($c) - 1)]) + )->flatten(); + + $length = $length - $password->count(); + + return $password->merge($options->pipe( + fn ($c) => Collection::times($length, fn () => $c[random_int(0, $c->count() - 1)]) + ))->shuffle()->implode(''); } /** @@ -997,12 +1006,28 @@ public static function replaceArray($search, $replace, $subject) $result = array_shift($segments); foreach ($segments as $segment) { - $result .= (array_shift($replace) ?? $search).$segment; + $result .= self::toStringOr(array_shift($replace) ?? $search, $search).$segment; } return $result; } + /** + * Convert the given value to a string or return the given fallback on failure. + * + * @param mixed $value + * @param string $fallback + * @return string + */ + private static function toStringOr($value, $fallback) + { + try { + return (string) $value; + } catch (Throwable $e) { + return $fallback; + } + } + /** * Replace the given value in the given string. * @@ -1127,6 +1152,24 @@ public static function replaceEnd($search, $replace, $subject) return $subject; } + /** + * Replace the patterns matching the given regular expression. + * + * @param string $pattern + * @param \Closure|string $replace + * @param array|string $subject + * @param int $limit + * @return string|string[]|null + */ + public static function replaceMatches($pattern, $replace, $subject, $limit = -1) + { + if ($replace instanceof Closure) { + return preg_replace_callback($pattern, $replace, $subject, $limit); + } + + return preg_replace($pattern, $replace, $subject, $limit); + } + /** * Remove any occurrence of the given string in the subject. * diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 3062f82161b0..e0b4a7e13119 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -23,6 +23,7 @@ "illuminate/conditionable": "^11.0", "illuminate/contracts": "^11.0", "illuminate/macroable": "^11.0", + "nesbot/carbon": "^2.67", "voku/portable-ascii": "^2.0" }, "conflict": { @@ -45,9 +46,9 @@ "illuminate/filesystem": "Required to use the composer class (^11.0).", "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.0.2).", "ramsey/uuid": "Required to use Str::uuid() (^4.7).", - "symfony/process": "Required to use the composer class (^6.3).", - "symfony/uid": "Required to use Str::ulid() (^6.3).", - "symfony/var-dumper": "Required to use the dd function (^6.3).", + "symfony/process": "Required to use the composer class (^7.0).", + "symfony/uid": "Required to use Str::ulid() (^7.0).", + "symfony/var-dumper": "Required to use the dd function (^7.0).", "vlucas/phpdotenv": "Required to use the Env class and env helper (^5.4.1)." }, "config": { diff --git a/src/Illuminate/Testing/ParallelConsoleOutput.php b/src/Illuminate/Testing/ParallelConsoleOutput.php index 91008dde890d..6df016b037b6 100644 --- a/src/Illuminate/Testing/ParallelConsoleOutput.php +++ b/src/Illuminate/Testing/ParallelConsoleOutput.php @@ -49,7 +49,7 @@ public function __construct($output) * @param int $options * @return void */ - public function write($messages, bool $newline = false, int $options = 0) + public function write($messages, bool $newline = false, int $options = 0): void { $messages = collect($messages)->filter(function ($message) { return ! Str::contains($message, $this->ignore); diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 44545565c2ea..9b131c01dd5e 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -864,6 +864,10 @@ public function validateExists($attribute, $value, $parameters) $expected = is_array($value) ? count(array_unique($value)) : 1; + if ($expected === 0) { + return true; + } + return $this->getExistCount( $connection, $table, $column, $value, $parameters ) >= $expected; diff --git a/src/Illuminate/Validation/ValidatesWhenResolvedTrait.php b/src/Illuminate/Validation/ValidatesWhenResolvedTrait.php index 251f312bedbe..596585551d22 100644 --- a/src/Illuminate/Validation/ValidatesWhenResolvedTrait.php +++ b/src/Illuminate/Validation/ValidatesWhenResolvedTrait.php @@ -75,7 +75,9 @@ protected function passedValidation() */ protected function failedValidation(Validator $validator) { - throw new ValidationException($validator); + $exception = $validator->getException(); + + throw new $exception($validator); } /** diff --git a/src/Illuminate/Validation/Validator.php b/src/Illuminate/Validation/Validator.php index 4a7b5f99fe2d..579e82135282 100755 --- a/src/Illuminate/Validation/Validator.php +++ b/src/Illuminate/Validation/Validator.php @@ -1481,6 +1481,16 @@ public function setPresenceVerifier(PresenceVerifierInterface $presenceVerifier) $this->presenceVerifier = $presenceVerifier; } + /** + * Get the exception to throw upon failed validation. + * + * @return string + */ + public function getException() + { + return $this->exception; + } + /** * Set the exception to throw upon failed validation. * diff --git a/src/Illuminate/Validation/composer.json b/src/Illuminate/Validation/composer.json index 4cf5d733e16b..a1f6fe3e6992 100755 --- a/src/Illuminate/Validation/composer.json +++ b/src/Illuminate/Validation/composer.json @@ -25,8 +25,8 @@ "illuminate/macroable": "^11.0", "illuminate/support": "^11.0", "illuminate/translation": "^11.0", - "symfony/http-foundation": "^6.3", - "symfony/mime": "^6.3" + "symfony/http-foundation": "^7.0", + "symfony/mime": "^7.0" }, "autoload": { "psr-4": { diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 62005da34610..18d1978af176 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -256,12 +256,12 @@ public function compileString($value) { [$this->footer, $result] = [[], '']; - $value = $this->storeUncompiledBlocks($value); - foreach ($this->prepareStringsForCompilationUsing as $callback) { $value = $callback($value); } + $value = $this->storeUncompiledBlocks($value); + // First we will compile the Blade component tags. This is a precompile style // step which compiles the component Blade tags into @component directives // that may be used by Blade. Then we should call any other precompilers. diff --git a/src/Illuminate/View/Component.php b/src/Illuminate/View/Component.php index 150129ca65f9..dcbe764c506b 100644 --- a/src/Illuminate/View/Component.php +++ b/src/Illuminate/View/Component.php @@ -143,6 +143,10 @@ public function resolveView() } $resolver = function ($view) { + if ($view instanceof ViewContract) { + return $view; + } + return $this->extractBladeViewFromString($view); }; diff --git a/tests/Auth/AuthAccessGateTest.php b/tests/Auth/AuthAccessGateTest.php index c6ed6c2cbcc4..3f143f1651e5 100644 --- a/tests/Auth/AuthAccessGateTest.php +++ b/tests/Auth/AuthAccessGateTest.php @@ -8,6 +8,7 @@ use Illuminate\Auth\Access\Response; use Illuminate\Container\Container; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use stdClass; @@ -555,9 +556,7 @@ public function testForUserMethodAttachesANewUserToANewGateInstanceWithGuessCall $this->assertSame(3, $counter); } - /** - * @dataProvider notCallableDataProvider - */ + #[DataProvider('notCallableDataProvider')] public function testDefineSecondParameterShouldBeStringOrCallable($callback) { $this->expectException(InvalidArgumentException::class); @@ -1036,12 +1035,11 @@ public function testEveryAbilityCheckFailsIfNonePass() } /** - * @dataProvider hasAbilitiesTestDataProvider - * * @param array $abilitiesToSet * @param array|string $abilitiesToCheck * @param bool $expectedHasValue */ + #[DataProvider('hasAbilitiesTestDataProvider')] public function testHasAbilities($abilitiesToSet, $abilitiesToCheck, $expectedHasValue) { $gate = $this->getBasicGate(); diff --git a/tests/Auth/AuthPasswordBrokerTest.php b/tests/Auth/AuthPasswordBrokerTest.php index f89971b4c7cf..f37b5e82dc40 100755 --- a/tests/Auth/AuthPasswordBrokerTest.php +++ b/tests/Auth/AuthPasswordBrokerTest.php @@ -22,12 +22,8 @@ protected function tearDown(): void public function testIfUserIsNotFoundErrorRedirectIsReturned() { $mocks = $this->getMocks(); - $broker = $this->getMockBuilder(PasswordBroker::class) - ->onlyMethods(['getUser']) - ->addMethods(['makeErrorRedirect']) - ->setConstructorArgs(array_values($mocks)) - ->getMock(); - $broker->expects($this->once())->method('getUser')->willReturn(null); + $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); + $broker->shouldReceive('getUser')->once()->andReturnNull(); $this->assertSame(PasswordBrokerContract::INVALID_USER, $broker->sendResetLink(['credentials'])); } @@ -35,7 +31,7 @@ public function testIfUserIsNotFoundErrorRedirectIsReturned() public function testIfTokenIsRecentlyCreated() { $mocks = $this->getMocks(); - $broker = $this->getMockBuilder(PasswordBroker::class)->addMethods(['emailResetLink', 'getUri'])->setConstructorArgs(array_values($mocks))->getMock(); + $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(CanResetPassword::class)); $mocks['tokens']->shouldReceive('recentlyCreatedToken')->once()->with($user)->andReturn(true); $user->shouldReceive('sendPasswordResetNotification')->with('token'); @@ -65,7 +61,7 @@ public function testUserIsRetrievedByCredentials() public function testBrokerCreatesTokenAndRedirectsWithoutError() { $mocks = $this->getMocks(); - $broker = $this->getMockBuilder(PasswordBroker::class)->addMethods(['emailResetLink', 'getUri'])->setConstructorArgs(array_values($mocks))->getMock(); + $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(CanResetPassword::class)); $mocks['tokens']->shouldReceive('recentlyCreatedToken')->once()->with($user)->andReturn(false); $mocks['tokens']->shouldReceive('create')->once()->with($user)->andReturn('token'); @@ -99,12 +95,9 @@ public function testRedirectReturnedByRemindWhenRecordDoesntExistInTable() public function testResetRemovesRecordOnReminderTableAndCallsCallback() { unset($_SERVER['__password.reset.test']); - $broker = $this->getMockBuilder(PasswordBroker::class) - ->onlyMethods(['validateReset']) - ->addMethods(['getPassword', 'getToken']) - ->setConstructorArgs(array_values($mocks = $this->getMocks())) - ->getMock(); - $broker->expects($this->once())->method('validateReset')->willReturn($user = m::mock(CanResetPassword::class)); + $mocks = $this->getMocks(); + $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial()->shouldAllowMockingProtectedMethods(); + $broker->shouldReceive('validateReset')->once()->andReturn($user = m::mock(CanResetPassword::class)); $mocks['tokens']->shouldReceive('delete')->once()->with($user); $callback = function ($user, $password) { $_SERVER['__password.reset.test'] = compact('user', 'password'); @@ -125,7 +118,7 @@ public function testExecutesCallbackInsteadOfSendingNotification() }; $mocks = $this->getMocks(); - $broker = $this->getMockBuilder(PasswordBroker::class)->addMethods(['emailResetLink', 'getUri'])->setConstructorArgs(array_values($mocks))->getMock(); + $broker = m::mock(PasswordBroker::class, array_values($mocks))->makePartial(); $mocks['users']->shouldReceive('retrieveByCredentials')->once()->with(['foo'])->andReturn($user = m::mock(CanResetPassword::class)); $mocks['tokens']->shouldReceive('recentlyCreatedToken')->once()->with($user)->andReturn(false); $mocks['tokens']->shouldReceive('create')->once()->with($user)->andReturn('token'); diff --git a/tests/Broadcasting/BroadcasterTest.php b/tests/Broadcasting/BroadcasterTest.php index d19ecfdc186a..eca82ca9754d 100644 --- a/tests/Broadcasting/BroadcasterTest.php +++ b/tests/Broadcasting/BroadcasterTest.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Model; use Illuminate\Http\Request; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -314,9 +315,7 @@ public function testUserAuthenticationWithoutResolve() $this->assertNull($this->broadcaster->resolveAuthenticatedUser(new Request(['socket_id' => '1234.1234']))); } - /** - * @dataProvider channelNameMatchPatternProvider - */ + #[DataProvider('channelNameMatchPatternProvider')] public function testChannelNameMatchPattern($channel, $pattern, $shouldMatch) { $this->assertEquals($shouldMatch, $this->broadcaster->channelNameMatchesPattern($channel, $pattern)); diff --git a/tests/Broadcasting/UsePusherChannelsNamesTest.php b/tests/Broadcasting/UsePusherChannelsNamesTest.php index c2b87d57026b..122e83e8c3f4 100644 --- a/tests/Broadcasting/UsePusherChannelsNamesTest.php +++ b/tests/Broadcasting/UsePusherChannelsNamesTest.php @@ -4,13 +4,12 @@ use Illuminate\Broadcasting\Broadcasters\Broadcaster; use Illuminate\Broadcasting\Broadcasters\UsePusherChannelConventions; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class UsePusherChannelsNamesTest extends TestCase { - /** - * @dataProvider channelsProvider - */ + #[DataProvider('channelsProvider')] public function testChannelNameNormalization($requestChannelName, $normalizedName) { $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; @@ -31,9 +30,7 @@ public function testChannelNameNormalizationSpecialCase() ); } - /** - * @dataProvider channelsProvider - */ + #[DataProvider('channelsProvider')] public function testIsGuardedChannel($requestChannelName, $_, $guarded) { $broadcaster = new FakeBroadcasterUsingPusherChannelsNames; diff --git a/tests/Bus/BusBatchTest.php b/tests/Bus/BusBatchTest.php index 49c394611185..9f870f9adc9d 100644 --- a/tests/Bus/BusBatchTest.php +++ b/tests/Bus/BusBatchTest.php @@ -19,6 +19,7 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\CallQueuedClosure; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; use stdClass; @@ -414,9 +415,7 @@ public function test_options_serialization_on_postgres() $builder->shouldHaveReceived('first'); } - /** - * @dataProvider serializedOptions - */ + #[DataProvider('serializedOptions')] public function test_options_unserialize_on_postgres($serialize, $options) { $factory = m::mock(BatchFactory::class); diff --git a/tests/Cache/CacheMemcachedConnectorTest.php b/tests/Cache/CacheMemcachedConnectorTest.php index 402b210b0dbe..f853afa2ee78 100755 --- a/tests/Cache/CacheMemcachedConnectorTest.php +++ b/tests/Cache/CacheMemcachedConnectorTest.php @@ -5,6 +5,7 @@ use Illuminate\Cache\MemcachedConnector; use Memcached; use Mockery as m; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use stdClass; @@ -48,9 +49,7 @@ public function testServersAreAddedCorrectlyWithPersistentConnection() $this->assertSame($result, $memcached); } - /** - * @requires extension memcached - */ + #[RequiresPhpExtension('memcached')] public function testServersAreAddedCorrectlyWithValidOptions() { $validOptions = [ @@ -71,9 +70,7 @@ public function testServersAreAddedCorrectlyWithValidOptions() $this->assertSame($result, $memcached); } - /** - * @requires extension memcached - */ + #[RequiresPhpExtension('memcached')] public function testServersAreAddedCorrectlyWithSaslCredentials() { $saslCredentials = ['foo', 'bar']; diff --git a/tests/Cache/CacheMemcachedStoreTest.php b/tests/Cache/CacheMemcachedStoreTest.php index a61af86e16f2..cb5dadc8f9bb 100755 --- a/tests/Cache/CacheMemcachedStoreTest.php +++ b/tests/Cache/CacheMemcachedStoreTest.php @@ -6,12 +6,10 @@ use Illuminate\Support\Carbon; use Memcached; use Mockery as m; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; -use stdClass; -/** - * @requires extension memcached - */ +#[RequiresPhpExtension('memcached')] class CacheMemcachedStoreTest extends TestCase { protected function tearDown(): void @@ -23,7 +21,7 @@ protected function tearDown(): void public function testGetReturnsNullWhenNotFound() { - $memcache = $this->getMockBuilder(stdClass::class)->addMethods(['get', 'getResultCode'])->getMock(); + $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['get', 'getResultCode'])->getMock(); $memcache->expects($this->once())->method('get')->with($this->equalTo('foo:bar'))->willReturn(null); $memcache->expects($this->once())->method('getResultCode')->willReturn(1); $store = new MemcachedStore($memcache, 'foo:'); @@ -32,7 +30,7 @@ public function testGetReturnsNullWhenNotFound() public function testMemcacheValueIsReturned() { - $memcache = $this->getMockBuilder(stdClass::class)->addMethods(['get', 'getResultCode'])->getMock(); + $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['get', 'getResultCode'])->getMock(); $memcache->expects($this->once())->method('get')->willReturn('bar'); $memcache->expects($this->once())->method('getResultCode')->willReturn(0); $store = new MemcachedStore($memcache); @@ -41,7 +39,7 @@ public function testMemcacheValueIsReturned() public function testMemcacheGetMultiValuesAreReturnedWithCorrectKeys() { - $memcache = $this->getMockBuilder(stdClass::class)->addMethods(['getMulti', 'getResultCode'])->getMock(); + $memcache = $this->getMockBuilder(Memcached::class)->onlyMethods(['getMulti', 'getResultCode'])->getMock(); $memcache->expects($this->once())->method('getMulti')->with( ['foo:foo', 'foo:bar', 'foo:baz'] )->willReturn([ diff --git a/tests/Cache/CacheRepositoryTest.php b/tests/Cache/CacheRepositoryTest.php index 56f70c58edd4..5c4b77ce070f 100755 --- a/tests/Cache/CacheRepositoryTest.php +++ b/tests/Cache/CacheRepositoryTest.php @@ -19,6 +19,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Carbon; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class CacheRepositoryTest extends TestCase @@ -280,10 +281,9 @@ public static function dataProviderTestGetSeconds() } /** - * @dataProvider dataProviderTestGetSeconds - * * @param mixed $duration */ + #[DataProvider('dataProviderTestGetSeconds')] public function testGetSeconds($duration) { $repo = $this->getRepository(); diff --git a/tests/Cache/LimitTest.php b/tests/Cache/LimitTest.php new file mode 100644 index 000000000000..165ab7799001 --- /dev/null +++ b/tests/Cache/LimitTest.php @@ -0,0 +1,41 @@ +assertSame(1, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + + $limit = Limit::perSecond(3); + $this->assertSame(1, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + + $limit = Limit::perMinute(3); + $this->assertSame(60, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + + $limit = Limit::perMinutes(2, 3); + $this->assertSame(120, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + + $limit = Limit::perHour(3); + $this->assertSame(3600, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + + $limit = Limit::perDay(3); + $this->assertSame(86400, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + + $limit = new GlobalLimit(3); + $this->assertSame(60, $limit->decaySeconds); + $this->assertSame(3, $limit->maxAttempts); + } +} diff --git a/tests/Cache/RedisCacheIntegrationTest.php b/tests/Cache/RedisCacheIntegrationTest.php index 410a02c6f82a..ff99dd0bfd81 100644 --- a/tests/Cache/RedisCacheIntegrationTest.php +++ b/tests/Cache/RedisCacheIntegrationTest.php @@ -5,6 +5,7 @@ use Illuminate\Cache\RedisStore; use Illuminate\Cache\Repository; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class RedisCacheIntegrationTest extends TestCase @@ -24,10 +25,9 @@ protected function tearDown(): void } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testRedisCacheAddTwice($driver) { $store = new RedisStore($this->redis[$driver]); @@ -40,10 +40,9 @@ public function testRedisCacheAddTwice($driver) /** * Breaking change. * - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testRedisCacheAddFalse($driver) { $store = new RedisStore($this->redis[$driver]); @@ -56,10 +55,9 @@ public function testRedisCacheAddFalse($driver) /** * Breaking change. * - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testRedisCacheAddNull($driver) { $store = new RedisStore($this->redis[$driver]); diff --git a/tests/Console/Scheduling/EventTest.php b/tests/Console/Scheduling/EventTest.php index 8d7883b9cb35..4cc6c02d0f5a 100644 --- a/tests/Console/Scheduling/EventTest.php +++ b/tests/Console/Scheduling/EventTest.php @@ -6,6 +6,7 @@ use Illuminate\Console\Scheduling\EventMutex; use Illuminate\Support\Str; use Mockery as m; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; class EventTest extends TestCase @@ -17,9 +18,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @requires OS Linux|Darwin - */ + #[RequiresOperatingSystem('Linux|Darwin')] public function testBuildCommandUsingUnix() { $event = new Event(m::mock(EventMutex::class), 'php -i'); @@ -27,9 +26,7 @@ public function testBuildCommandUsingUnix() $this->assertSame("php -i > '/dev/null' 2>&1", $event->buildCommand()); } - /** - * @requires OS Windows - */ + #[RequiresOperatingSystem('Windows')] public function testBuildCommandUsingWindows() { $event = new Event(m::mock(EventMutex::class), 'php -i'); @@ -37,9 +34,7 @@ public function testBuildCommandUsingWindows() $this->assertSame('php -i > "NUL" 2>&1', $event->buildCommand()); } - /** - * @requires OS Linux|Darwin - */ + #[RequiresOperatingSystem('Linux|Darwin')] public function testBuildCommandInBackgroundUsingUnix() { $event = new Event(m::mock(EventMutex::class), 'php -i'); @@ -50,9 +45,7 @@ public function testBuildCommandInBackgroundUsingUnix() $this->assertSame("(php -i > '/dev/null' 2>&1 ; '".PHP_BINARY."' 'artisan' schedule:finish {$scheduleId} \"$?\") > '/dev/null' 2>&1 &", $event->buildCommand()); } - /** - * @requires OS Windows - */ + #[RequiresOperatingSystem('Windows')] public function testBuildCommandInBackgroundUsingWindows() { $event = new Event(m::mock(EventMutex::class), 'php -i'); diff --git a/tests/Database/DatabaseConcernsBuildsQueriesTraitTest.php b/tests/Database/DatabaseConcernsBuildsQueriesTraitTest.php index 55d03551a542..730b91855ff1 100644 --- a/tests/Database/DatabaseConcernsBuildsQueriesTraitTest.php +++ b/tests/Database/DatabaseConcernsBuildsQueriesTraitTest.php @@ -9,7 +9,11 @@ class DatabaseConcernsBuildsQueriesTraitTest extends TestCase { public function testTapCallbackInstance() { - $mock = $this->getMockForTrait(BuildsQueries::class); + $mock = new class + { + use BuildsQueries; + }; + $mock->tap(function ($builder) use ($mock) { $this->assertEquals($mock, $builder); }); diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index 0df367eed634..b91db617a9b5 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -7,6 +7,7 @@ use Exception; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Connection; +use Illuminate\Database\DatabaseTransactionsManager; use Illuminate\Database\Events\QueryExecuted; use Illuminate\Database\Events\TransactionBeginning; use Illuminate\Database\Events\TransactionCommitted; @@ -116,11 +117,11 @@ public function testSelectResultsetsReturnsMultipleRowset() $statement->expects($this->once())->method('bindValue')->with(1, 'foo', 2); $statement->expects($this->once())->method('execute'); $statement->expects($this->atLeastOnce())->method('fetchAll')->willReturn(['boom']); - $statement->expects($this->atLeastOnce())->method('nextRowset')->will($this->returnCallback(function () { + $statement->expects($this->atLeastOnce())->method('nextRowset')->willReturnCallback(function () { static $i = 1; return ++$i <= 2; - })); + }); $pdo->expects($this->once())->method('prepare')->with('CALL a_procedure(?)')->willReturn($statement); $mock = $this->getMockConnection(['prepareBindings'], $writePdo); $mock->setReadPdo($pdo); @@ -289,6 +290,20 @@ public function testCommittingFiresEventsIfSet() $connection->commit(); } + public function testAfterCommitIsExecutedOnFinalCommit() + { + $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit'])->getMock(); + $transactionsManager = $this->getMockBuilder(DatabaseTransactionsManager::class)->onlyMethods(['afterCommitCallbacksShouldBeExecuted'])->getMock(); + $transactionsManager->expects($this->once())->method('afterCommitCallbacksShouldBeExecuted')->with(0)->willReturn(true); + + $connection = $this->getMockConnection([], $pdo); + $connection->setTransactionManager($transactionsManager); + + $connection->transaction(function () { + // do nothing + }); + } + public function testRollBackedFiresEventsIfSet() { $pdo = $this->createMock(DatabaseConnectionTestMockPDO::class); diff --git a/tests/Database/DatabaseConnectorTest.php b/tests/Database/DatabaseConnectorTest.php index 79ee6d0606c4..7ada98829e8a 100755 --- a/tests/Database/DatabaseConnectorTest.php +++ b/tests/Database/DatabaseConnectorTest.php @@ -10,6 +10,8 @@ use Mockery as m; use PDO; use PDOStatement; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use stdClass; @@ -27,9 +29,7 @@ public function testOptionResolution() $this->assertEquals([0 => 'baz', 1 => 'bar', 2 => 'boom'], $connector->getOptions(['options' => [0 => 'baz', 2 => 'boom']])); } - /** - * @dataProvider mySqlConnectProvider - */ + #[DataProvider('mySqlConnectProvider')] public function testMySqlConnectCallsCreateConnectionWithProperArguments($dsn, $config) { $connector = $this->getMockBuilder(MySqlConnector::class)->onlyMethods(['createConnection', 'getOptions'])->getMock(); @@ -90,11 +90,10 @@ public function testPostgresConnectCallsCreateConnectionWithProperArguments() } /** - * @dataProvider provideSearchPaths - * * @param string $searchPath * @param string $expectedSql */ + #[DataProvider('provideSearchPaths')] public function testPostgresSearchPathIsSet($searchPath, $expectedSql) { $dsn = 'pgsql:host=foo;dbname=\'bar\''; @@ -317,9 +316,7 @@ public function testSqlServerConnectCallsCreateConnectionWithOptionalArguments() $this->assertSame($result, $connection); } - /** - * @requires extension odbc - */ + #[RequiresPhpExtension('odbc')] public function testSqlServerConnectCallsCreateConnectionWithPreferredODBC() { $config = ['odbc' => true, 'odbc_datasource_name' => 'server=localhost;database=test;']; diff --git a/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php new file mode 100644 index 000000000000..047dc138a5e9 --- /dev/null +++ b/tests/Database/DatabaseEloquentBelongsToManyCreateOrFirstTest.php @@ -0,0 +1,498 @@ +id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + [456], + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection()->expects('insert')->with( + 'insert into "related_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $source->getConnection()->expects('insert')->with( + 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', + [456, 123], + )->andReturnTrue(); + + $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodAssociatesExistingRelated(): void + { + $source = new BelongsToManyCreateOrFirstTestSourceModel(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "related_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $source->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $source->getConnection() + ->expects('select') + ->with('select * from "related_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $source->getConnection()->expects('insert')->with( + 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', + [456, 123], + )->andReturnTrue(); + + $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + // Pivot is not loaded when related model is newly created. + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRelatedAlreadyAssociated(): void + { + $source = new BelongsToManyCreateOrFirstTestSourceModel(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + 'pivot_source_id' => 123, + 'pivot_related_id' => 456, + ]]); + + $result = $source->related()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRelatedAssociatedJustNow(): void + { + $source = new BelongsToManyCreateOrFirstTestSourceModel(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "related_table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $source->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $source->getConnection() + ->expects('select') + ->with('select * from "related_table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $sql = 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)'; + $bindings = [456, 123]; + + $source->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + false, + ) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + 'pivot_source_id' => 123, + 'pivot_related_id' => 456, + ]]); + + $result = $source->related()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRelatedAndAssociatesIt(): void + { + $source = new BelongsToManyCreateOrFirstTestSourceModel(); + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $source->getConnection() + ->expects('select') + ->with( + 'select * from "related_table" where ("attr" = ?) limit 1', + ['foo'], + true, + ) + ->andReturn([[ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $source->getConnection() + ->expects('insert') + ->with( + 'insert into "pivot_table" ("related_id", "source_id") values (?, ?)', + [456, 123], + ) + ->andReturnTrue(); + + $result = $source->related()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + // Pivot is not loaded when related model is newly created. + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodFallsBackToCreateOrFirst(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = Mockery::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = false; + $instance->syncOriginal(); + $relation + ->expects('createOrFirst') + ->with(['attr' => 'foo'], ['val' => 'bar'], [], true) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('select') + ->with( + 'select "related_table".*, "pivot_table"."source_id" as "pivot_source_id", "pivot_table"."related_id" as "pivot_related_id" from "related_table" inner join "pivot_table" on "related_table"."id" = "pivot_table"."related_id" where "pivot_table"."source_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $source->getConnection() + ->expects('select') + ->with( + 'select * from "related_table" where ("attr" = ?) limit 1', + ['foo'], + true, + ) + ->andReturn([]); + + $result = $source->related()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + 'pivot' => [ + 'source_id' => 123, + 'related_id' => 456, + ], + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRelated(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = Mockery::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = true; + $instance->syncOriginal(); + $relation + ->expects('firstOrCreate') + ->with(['attr' => 'foo'], ['val' => 'baz'], [], true) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + + $result = $source->related()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRelated(): void + { + $source = new class() extends BelongsToManyCreateOrFirstTestSourceModel + { + protected function newBelongsToMany(Builder $query, Model $parent, $table, $foreignPivotKey, $relatedPivotKey, $parentKey, $relatedKey, $relationName = null): BelongsToMany + { + $relation = Mockery::mock(BelongsToMany::class)->makePartial(); + $relation->__construct(...func_get_args()); + $instance = new BelongsToManyCreateOrFirstTestRelatedModel([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]); + $instance->exists = true; + $instance->wasRecentlyCreated = false; + $instance->syncOriginal(); + $relation + ->expects('firstOrCreate') + ->with(['attr' => 'foo'], ['val' => 'baz'], [], true) + ->andReturn($instance); + + return $relation; + } + }; + $source->id = 123; + $this->mockConnectionForModels( + [$source, new BelongsToManyCreateOrFirstTestRelatedModel()], + 'SQLite', + ); + $source->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $source->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $source->getConnection() + ->expects('update') + ->with( + 'update "related_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + ) + ->andReturn(1); + + $result = $source->related()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertEquals([ + 'id' => 456, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModels(array $models, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new BaseBuilder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + foreach ($models as $model) { + /** @var Model $model */ + $class = get_class($model); + $class::setConnectionResolver($resolver); + } + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +/** + * @property int $id + */ +class BelongsToManyCreateOrFirstTestRelatedModel extends Model +{ + protected $table = 'related_table'; + protected $guarded = []; +} + +/** + * @property int $id + */ +class BelongsToManyCreateOrFirstTestSourceModel extends Model +{ + protected $table = 'source_table'; + protected $guarded = []; + + public function related(): BelongsToMany + { + return $this->belongsToMany( + BelongsToManyCreateOrFirstTestRelatedModel::class, + 'pivot_table', + 'source_id', + 'related_id', + ); + } +} diff --git a/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php new file mode 100755 index 000000000000..ea09aa234b91 --- /dev/null +++ b/tests/Database/DatabaseEloquentBuilderCreateOrFirstTest.php @@ -0,0 +1,331 @@ +mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], false) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $sql = 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], false) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->newQuery()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 123], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRecord(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite', [123]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + $model = new EloquentBuilderCreateOrFirstTestModel(); + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], true) + ->andReturn([]); + + $sql = 'insert into "table" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "table" where ("attr" = ?) limit 1', ['foo'], false) + ->andReturn([[ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection() + ->expects('update') + ->with( + 'update "table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 123], + ) + ->andReturn(1); + + $result = $model->newQuery()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + $class = get_class($model); + $class::setConnectionResolver($resolver); + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +class EloquentBuilderCreateOrFirstTestModel extends Model +{ + protected $table = 'table'; + protected $guarded = []; +} diff --git a/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php new file mode 100755 index 000000000000..09faed3f2654 --- /dev/null +++ b/tests/Database/DatabaseEloquentHasManyCreateOrFirstTest.php @@ -0,0 +1,365 @@ +id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], false) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $model->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], false) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $model->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRecord(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite', [456]); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $model->getConnection()->expects('insert')->with( + 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)', + ['foo', 'bar', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + )->andReturn(1); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + $model = new HasManyCreateOrFirstTestParentModel(); + $model->id = 123; + $this->mockConnectionForModel($model, 'SQLite'); + $model->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $model->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], true) + ->andReturn([]); + + $sql = 'insert into "child_table" ("attr", "val", "parent_id", "updated_at", "created_at") values (?, ?, ?, ?, ?)'; + $bindings = ['foo', 'baz', 123, '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $model->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $model->getConnection() + ->expects('select') + ->with('select * from "child_table" where "child_table"."parent_id" = ? and "child_table"."parent_id" is not null and ("attr" = ?) limit 1', [123, 'foo'], false) + ->andReturn([[ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $model->getConnection()->expects('update')->with( + 'update "child_table" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 456], + )->andReturn(1); + + $result = $model->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 456, + 'parent_id' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + $class = get_class($model); + $class::setConnectionResolver($resolver); + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +/** + * @property int $id + */ +class HasManyCreateOrFirstTestParentModel extends Model +{ + protected $table = 'parent_table'; + protected $guarded = []; + + public function children(): HasMany + { + return $this->hasMany(HasManyCreateOrFirstTestChildModel::class, 'parent_id'); + } +} + +/** + * @property int $id + * @property int $parent_id + */ +class HasManyCreateOrFirstTestChildModel extends Model +{ + protected $table = 'child_table'; + protected $guarded = []; +} diff --git a/tests/Database/DatabaseEloquentHasManyTest.php b/tests/Database/DatabaseEloquentHasManyTest.php index 0bec03bc97fa..eca4cefede25 100755 --- a/tests/Database/DatabaseEloquentHasManyTest.php +++ b/tests/Database/DatabaseEloquentHasManyTest.php @@ -7,6 +7,7 @@ use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\UniqueConstraintViolationException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -155,6 +156,7 @@ public function testFirstOrCreateMethodCreatesNewModelWithForeignKeySet() $relation = $this->getRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $model = $this->expectCreatedModel($relation, ['foo']); $this->assertEquals($model, $relation->firstOrCreate(['foo'])); @@ -165,6 +167,7 @@ public function testFirstOrCreateMethodWithValuesCreatesNewModelWithForeignKeySe $relation = $this->getRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo' => 'bar'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $model = $this->expectCreatedModel($relation, ['foo' => 'bar', 'baz' => 'qux']); $this->assertEquals($model, $relation->firstOrCreate(['foo' => 'bar'], ['baz' => 'qux'])); @@ -225,7 +228,9 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn($model = m::mock(stdClass::class)); $relation->getRelated()->shouldReceive('newInstance')->never(); - $model->shouldReceive('fill')->once()->with(['bar']); + + $model->wasRecentlyCreated = false; + $model->shouldReceive('fill')->once()->with(['bar'])->andReturn($model); $model->shouldReceive('save')->once(); $this->assertInstanceOf(stdClass::class, $relation->updateOrCreate(['foo'], ['bar'])); @@ -234,11 +239,15 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() public function testUpdateOrCreateMethodCreatesNewModelWithForeignKeySet() { $relation = $this->getRelation(); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(function ($scope) { + return $scope(); + }); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); - $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class)); + $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo', 'bar'])->andReturn($model = m::mock(Model::class)); + + $model->wasRecentlyCreated = true; $model->shouldReceive('save')->once()->andReturn(true); - $model->shouldReceive('fill')->once()->with(['bar']); $model->shouldReceive('setAttribute')->once()->with('foreign_key', 1); $this->assertInstanceOf(Model::class, $relation->updateOrCreate(['foo'], ['bar'])); @@ -335,7 +344,8 @@ public function testCreateManyCreatesARelatedModelForEachRecord() protected function getRelation() { - $builder = m::mock(Builder::class); + $queryBuilder = m::mock(QueryBuilder::class); + $builder = m::mock(Builder::class, [$queryBuilder]); $builder->shouldReceive('whereNotNull')->with('table.foreign_key'); $builder->shouldReceive('where')->with('table.foreign_key', '=', 1); $related = m::mock(Model::class); diff --git a/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php new file mode 100644 index 000000000000..bb1142027c5a --- /dev/null +++ b/tests/Database/DatabaseEloquentHasManyThroughCreateOrFirstTest.php @@ -0,0 +1,425 @@ +id = 123; + $this->mockConnectionForModel($parent, 'SQLite', [789]); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + $parent->getConnection()->expects('insert')->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testCreateOrFirstMethodRetrievesExistingRecord(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $parent->children()->createOrFirst(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodCreatesNewRecord(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite', [789]); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $parent->getConnection()->expects('insert')->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + )->andReturnTrue(); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesExistingRecord(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01 00:00:00', + 'updated_at' => '2023-01-01 00:00:00', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testFirstOrCreateMethodRetrievesRecordCreatedJustNow(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ? and "val" = ?) limit 1', + [123, 'foo', 'bar'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodCreatesNewRecord(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite', [789]); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $parent->getConnection() + ->expects('insert') + ->with( + 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)', + ['foo', 'baz', '2023-01-01 00:00:00', '2023-01-01 00:00:00'], + ) + ->andReturnTrue(); + + $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertTrue($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesExistingRecord(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $parent->getConnection() + ->expects('update') + ->with( + 'update "child" set "val" = ?, "updated_at" = ? where "id" = ?', + ['baz', '2023-01-01 00:00:00', 789], + ) + ->andReturn(1); + + $result = $parent->children()->updateOrCreate(['attr' => 'foo'], ['val' => 'baz']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'baz', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + public function testUpdateOrCreateMethodUpdatesRecordCreatedJustNow(): void + { + $parent = new HasManyThroughCreateOrFirstTestParentModel(); + $parent->id = 123; + $this->mockConnectionForModel($parent, 'SQLite'); + $parent->getConnection()->shouldReceive('transactionLevel')->andReturn(0); + $parent->getConnection()->shouldReceive('getName')->andReturn('sqlite'); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ?) limit 1', + [123, 'foo'], + true, + ) + ->andReturn([]); + + $sql = 'insert into "child" ("attr", "val", "updated_at", "created_at") values (?, ?, ?, ?)'; + $bindings = ['foo', 'bar', '2023-01-01 00:00:00', '2023-01-01 00:00:00']; + + $parent->getConnection() + ->expects('insert') + ->with($sql, $bindings) + ->andThrow(new UniqueConstraintViolationException('sqlite', $sql, $bindings, new Exception())); + + $parent->getConnection() + ->expects('select') + ->with( + 'select "child".*, "pivot"."parent_id" as "laravel_through_key" from "child" inner join "pivot" on "pivot"."id" = "child"."pivot_id" where "pivot"."parent_id" = ? and ("attr" = ? and "val" = ?) limit 1', + [123, 'foo', 'bar'], + true, + ) + ->andReturn([[ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ]]); + + $result = $parent->children()->firstOrCreate(['attr' => 'foo'], ['val' => 'bar']); + $this->assertFalse($result->wasRecentlyCreated); + $this->assertEquals([ + 'id' => 789, + 'pivot_id' => 456, + 'laravel_through_key' => 123, + 'attr' => 'foo', + 'val' => 'bar', + 'created_at' => '2023-01-01T00:00:00.000000Z', + 'updated_at' => '2023-01-01T00:00:00.000000Z', + ], $result->toArray()); + } + + protected function mockConnectionForModel(Model $model, string $database, array $lastInsertIds = []): void + { + $grammarClass = 'Illuminate\Database\Query\Grammars\\'.$database.'Grammar'; + $processorClass = 'Illuminate\Database\Query\Processors\\'.$database.'Processor'; + $grammar = new $grammarClass; + $processor = new $processorClass; + $connection = Mockery::mock(ConnectionInterface::class, ['getQueryGrammar' => $grammar, 'getPostProcessor' => $processor]); + $connection->shouldReceive('query')->andReturnUsing(function () use ($connection, $grammar, $processor) { + return new Builder($connection, $grammar, $processor); + }); + $connection->shouldReceive('getDatabaseName')->andReturn('database'); + $resolver = Mockery::mock(ConnectionResolverInterface::class, ['connection' => $connection]); + + $class = get_class($model); + $class::setConnectionResolver($resolver); + + $connection->shouldReceive('getPdo')->andReturn($pdo = Mockery::mock(PDO::class)); + + foreach ($lastInsertIds as $id) { + $pdo->expects('lastInsertId')->andReturn($id); + } + } +} + +/** + * @property int $id + * @property int $pivot_id + */ +class HasManyThroughCreateOrFirstTestChildModel extends Model +{ + protected $table = 'child'; + protected $guarded = []; +} + +/** + * @property int $id + * @property int $parent_id + */ +class HasManyThroughCreateOrFirstTestPivotModel extends Model +{ + protected $table = 'pivot'; + protected $guarded = []; +} + +/** + * @property int $id + */ +class HasManyThroughCreateOrFirstTestParentModel extends Model +{ + protected $table = 'parent'; + protected $guarded = []; + + public function children(): HasManyThrough + { + return $this->hasManyThrough( + HasManyThroughCreateOrFirstTestChildModel::class, + HasManyThroughCreateOrFirstTestPivotModel::class, + 'parent_id', + 'pivot_id', + ); + } +} diff --git a/tests/Database/DatabaseEloquentIrregularPluralTest.php b/tests/Database/DatabaseEloquentIrregularPluralTest.php index 7d2ce9490b43..9ca4cedc4aa0 100644 --- a/tests/Database/DatabaseEloquentIrregularPluralTest.php +++ b/tests/Database/DatabaseEloquentIrregularPluralTest.php @@ -71,16 +71,14 @@ protected function schema() return $connection->getSchemaBuilder(); } - /** @test */ - public function itPluralizesTheTableName() + public function testItPluralizesTheTableName() { $model = new IrregularPluralHuman; $this->assertSame('irregular_plural_humans', $model->getTable()); } - /** @test */ - public function itTouchesTheParentWithAnIrregularPlural() + public function testItTouchesTheParentWithAnIrregularPlural() { Carbon::setTestNow('2018-05-01 12:13:14'); @@ -104,8 +102,7 @@ public function itTouchesTheParentWithAnIrregularPlural() $this->assertSame('2018-05-01 15:16:17', (string) $human->updated_at); } - /** @test */ - public function itPluralizesMorphToManyRelationships() + public function testItPluralizesMorphToManyRelationships() { $human = IrregularPluralHuman::create(['email' => 'bobby@example.com']); diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 72eb895c6fee..fc0744794691 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -23,6 +23,8 @@ use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Casts\AsStringable; use Illuminate\Database\Eloquent\Collection; +use Illuminate\Database\Eloquent\Concerns\HasUlids; +use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\JsonEncodingException; use Illuminate\Database\Eloquent\MassAssignmentException; use Illuminate\Database\Eloquent\MissingAttributeException; @@ -1863,6 +1865,98 @@ public function testCloneModelMakesAFreshCopyOfTheModel() $this->assertEquals(['bar'], $clone->foo); } + public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasUuidPrimaryKey() + { + $class = new EloquentPrimaryUuidModelStub(); + $class->uuid = 'ccf55569-bc4a-4450-875f-b5cffb1b34ec'; + $class->exists = true; + $class->first = 'taylor'; + $class->last = 'otwell'; + $class->created_at = $class->freshTimestamp(); + $class->updated_at = $class->freshTimestamp(); + $class->setRelation('foo', ['bar']); + + $clone = $class->replicate(); + + $this->assertNull($clone->uuid); + $this->assertFalse($clone->exists); + $this->assertSame('taylor', $clone->first); + $this->assertSame('otwell', $clone->last); + $this->assertArrayNotHasKey('created_at', $clone->getAttributes()); + $this->assertArrayNotHasKey('updated_at', $clone->getAttributes()); + $this->assertEquals(['bar'], $clone->foo); + } + + public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasUuid() + { + $class = new EloquentNonPrimaryUuidModelStub(); + $class->id = 1; + $class->uuid = 'ccf55569-bc4a-4450-875f-b5cffb1b34ec'; + $class->exists = true; + $class->first = 'taylor'; + $class->last = 'otwell'; + $class->created_at = $class->freshTimestamp(); + $class->updated_at = $class->freshTimestamp(); + $class->setRelation('foo', ['bar']); + + $clone = $class->replicate(); + + $this->assertNull($clone->id); + $this->assertNull($clone->uuid); + $this->assertFalse($clone->exists); + $this->assertSame('taylor', $clone->first); + $this->assertSame('otwell', $clone->last); + $this->assertArrayNotHasKey('created_at', $clone->getAttributes()); + $this->assertArrayNotHasKey('updated_at', $clone->getAttributes()); + $this->assertEquals(['bar'], $clone->foo); + } + + public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasUlidPrimaryKey() + { + $class = new EloquentPrimaryUlidModelStub(); + $class->ulid = '01HBZ975D8606P6CV672KW1AP2'; + $class->exists = true; + $class->first = 'taylor'; + $class->last = 'otwell'; + $class->created_at = $class->freshTimestamp(); + $class->updated_at = $class->freshTimestamp(); + $class->setRelation('foo', ['bar']); + + $clone = $class->replicate(); + + $this->assertNull($clone->ulid); + $this->assertFalse($clone->exists); + $this->assertSame('taylor', $clone->first); + $this->assertSame('otwell', $clone->last); + $this->assertArrayNotHasKey('created_at', $clone->getAttributes()); + $this->assertArrayNotHasKey('updated_at', $clone->getAttributes()); + $this->assertEquals(['bar'], $clone->foo); + } + + public function testCloneModelMakesAFreshCopyOfTheModelWhenModelHasUlid() + { + $class = new EloquentNonPrimaryUlidModelStub(); + $class->id = 1; + $class->ulid = '01HBZ975D8606P6CV672KW1AP2'; + $class->exists = true; + $class->first = 'taylor'; + $class->last = 'otwell'; + $class->created_at = $class->freshTimestamp(); + $class->updated_at = $class->freshTimestamp(); + $class->setRelation('foo', ['bar']); + + $clone = $class->replicate(); + + $this->assertNull($clone->id); + $this->assertNull($clone->ulid); + $this->assertFalse($clone->exists); + $this->assertSame('taylor', $clone->first); + $this->assertSame('otwell', $clone->last); + $this->assertArrayNotHasKey('created_at', $clone->getAttributes()); + $this->assertArrayNotHasKey('updated_at', $clone->getAttributes()); + $this->assertEquals(['bar'], $clone->foo); + } + public function testModelObserversCanBeAttachedToModels() { EloquentModelStub::setEventDispatcher($events = m::mock(Dispatcher::class)); @@ -3335,6 +3429,62 @@ class EloquentDifferentConnectionModelStub extends EloquentModelStub public $connection = 'different_connection'; } +class EloquentPrimaryUuidModelStub extends EloquentModelStub +{ + use HasUuids; + + public $incrementing = false; + protected $keyType = 'string'; + + public function getKeyName() + { + return 'uuid'; + } +} + +class EloquentNonPrimaryUuidModelStub extends EloquentModelStub +{ + use HasUuids; + + public function getKeyName() + { + return 'id'; + } + + public function uniqueIds() + { + return ['uuid']; + } +} + +class EloquentPrimaryUlidModelStub extends EloquentModelStub +{ + use HasUlids; + + public $incrementing = false; + protected $keyType = 'string'; + + public function getKeyName() + { + return 'ulid'; + } +} + +class EloquentNonPrimaryUlidModelStub extends EloquentModelStub +{ + use HasUlids; + + public function getKeyName() + { + return 'id'; + } + + public function uniqueIds() + { + return ['ulid']; + } +} + class EloquentModelSavingEventStub { // diff --git a/tests/Database/DatabaseEloquentMorphTest.php b/tests/Database/DatabaseEloquentMorphTest.php index 924cd17fc97b..7804226dc885 100755 --- a/tests/Database/DatabaseEloquentMorphTest.php +++ b/tests/Database/DatabaseEloquentMorphTest.php @@ -9,6 +9,7 @@ use Illuminate\Database\Eloquent\Relations\MorphMany; use Illuminate\Database\Eloquent\Relations\MorphOne; use Illuminate\Database\Eloquent\Relations\Relation; +use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Database\UniqueConstraintViolationException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -195,6 +196,7 @@ public function testFirstOrCreateMethodCreatesNewMorphModel() $relation = $this->getOneRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class)); $model->shouldReceive('setAttribute')->once()->with('morph_id', 1); $model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent())); @@ -208,6 +210,7 @@ public function testFirstOrCreateMethodWithValuesCreatesNewMorphModel() $relation = $this->getOneRelation(); $relation->getQuery()->shouldReceive('where')->once()->with(['foo' => 'bar'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(fn ($scope) => $scope()); $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo' => 'bar', 'baz' => 'qux'])->andReturn($model = m::mock(Model::class)); $model->shouldReceive('setAttribute')->once()->with('morph_id', 1); $model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent())); @@ -300,8 +303,10 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn($model = m::mock(Model::class)); $relation->getRelated()->shouldReceive('newInstance')->never(); + + $model->wasRecentlyCreated = false; $model->shouldReceive('setAttribute')->never(); - $model->shouldReceive('fill')->once()->with(['bar']); + $model->shouldReceive('fill')->once()->with(['bar'])->andReturn($model); $model->shouldReceive('save')->once(); $this->assertInstanceOf(Model::class, $relation->updateOrCreate(['foo'], ['bar'])); @@ -310,13 +315,17 @@ public function testUpdateOrCreateMethodFindsFirstModelAndUpdates() public function testUpdateOrCreateMethodCreatesNewMorphModel() { $relation = $this->getOneRelation(); + $relation->getQuery()->shouldReceive('withSavepointIfNeeded')->once()->andReturnUsing(function ($scope) { + return $scope(); + }); $relation->getQuery()->shouldReceive('where')->once()->with(['foo'])->andReturn($relation->getQuery()); $relation->getQuery()->shouldReceive('first')->once()->with()->andReturn(null); - $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo'])->andReturn($model = m::mock(Model::class)); + $relation->getRelated()->shouldReceive('newInstance')->once()->with(['foo', 'bar'])->andReturn($model = m::mock(Model::class)); + + $model->wasRecentlyCreated = true; $model->shouldReceive('setAttribute')->once()->with('morph_id', 1); $model->shouldReceive('setAttribute')->once()->with('morph_type', get_class($relation->getParent())); $model->shouldReceive('save')->once()->andReturn(true); - $model->shouldReceive('fill')->once()->with(['bar']); $this->assertInstanceOf(Model::class, $relation->updateOrCreate(['foo'], ['bar'])); } @@ -435,7 +444,8 @@ public function testIsNotModelWithAnotherConnection() protected function getOneRelation() { - $builder = m::mock(Builder::class); + $queryBuilder = m::mock(QueryBuilder::class); + $builder = m::mock(Builder::class, [$queryBuilder]); $builder->shouldReceive('whereNotNull')->once()->with('table.morph_id'); $builder->shouldReceive('where')->once()->with('table.morph_id', '=', 1); $related = m::mock(Model::class); diff --git a/tests/Database/DatabaseMySQLSchemaBuilderTest.php b/tests/Database/DatabaseMySQLSchemaBuilderTest.php index ee9fe903e1b4..2a89d31957c3 100755 --- a/tests/Database/DatabaseMySQLSchemaBuilderTest.php +++ b/tests/Database/DatabaseMySQLSchemaBuilderTest.php @@ -38,11 +38,11 @@ public function testGetColumnListing() $connection->shouldReceive('getDatabaseName')->andReturn('db'); $connection->shouldReceive('getSchemaGrammar')->andReturn($grammar); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $grammar->shouldReceive('compileColumnListing')->once()->andReturn('sql'); - $processor->shouldReceive('processColumnListing')->once()->andReturn(['column']); + $grammar->shouldReceive('compileColumns')->with('db', 'prefix_table')->once()->andReturn('sql'); + $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'column']]); $builder = new MySqlBuilder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); - $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql', ['db', 'prefix_table'])->andReturn(['column']); + $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql')->andReturn([['name' => 'column']]); $this->assertEquals(['column'], $builder->getColumnListing('table')); } diff --git a/tests/Database/DatabaseMySqlProcessorTest.php b/tests/Database/DatabaseMySqlProcessorTest.php index 6f7b4bbdb53d..5ac56b1ce990 100644 --- a/tests/Database/DatabaseMySqlProcessorTest.php +++ b/tests/Database/DatabaseMySqlProcessorTest.php @@ -7,18 +7,26 @@ class DatabaseMySqlProcessorTest extends TestCase { - public function testProcessColumnListing() + public function testProcessColumns() { $processor = new MySqlProcessor; - $listing = [['column_name' => 'id'], ['column_name' => 'name'], ['column_name' => 'email']]; - $expected = ['id', 'name', 'email']; - $this->assertEquals($expected, $processor->processColumnListing($listing)); + $listing = [ + ['name' => 'id', 'type_name' => 'bigint', 'type' => 'bigint', 'collation' => 'collate', 'nullable' => 'YES', 'default' => '', 'extra' => 'auto_increment', 'comment' => 'bar'], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'varchar(100)', 'collation' => 'collate', 'nullable' => 'NO', 'default' => 'foo', 'extra' => '', 'comment' => ''], + ['name' => 'email', 'type_name' => 'varchar', 'type' => 'varchar(100)', 'collation' => 'collate', 'nullable' => 'YES', 'default' => 'NULL', 'extra' => 'on update CURRENT_TIMESTAMP', 'comment' => 'NULL'], + ]; + $expected = [ + ['name' => 'id', 'type_name' => 'bigint', 'type' => 'bigint', 'collation' => 'collate', 'nullable' => true, 'default' => '', 'auto_increment' => true, 'comment' => 'bar'], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'varchar(100)', 'collation' => 'collate', 'nullable' => false, 'default' => 'foo', 'auto_increment' => false, 'comment' => ''], + ['name' => 'email', 'type_name' => 'varchar', 'type' => 'varchar(100)', 'collation' => 'collate', 'nullable' => true, 'default' => 'NULL', 'auto_increment' => false, 'comment' => 'NULL'], + ]; + $this->assertEquals($expected, $processor->processColumns($listing)); // convert listing to objects to simulate PDO::FETCH_CLASS foreach ($listing as &$row) { $row = (object) $row; } - $this->assertEquals($expected, $processor->processColumnListing($listing)); + $this->assertEquals($expected, $processor->processColumns($listing)); } } diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index 7bca44c41a3d..d601b04a6fb0 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -756,7 +756,7 @@ public function testAddingDouble() $statements = $blueprint->toSql($this->getConnection(), $this->getGrammar()); $this->assertCount(1, $statements); - $this->assertSame('alter table `users` add `foo` double not null', $statements[0]); + $this->assertSame('alter table `users` add `foo` double(15, 6) not null', $statements[0]); } public function testAddingDoubleSpecifyingPrecision() diff --git a/tests/Database/DatabaseMySqlSchemaStateTest.php b/tests/Database/DatabaseMySqlSchemaStateTest.php index 63a7107e7caa..0468a0d501d5 100644 --- a/tests/Database/DatabaseMySqlSchemaStateTest.php +++ b/tests/Database/DatabaseMySqlSchemaStateTest.php @@ -5,14 +5,13 @@ use Generator; use Illuminate\Database\MySqlConnection; use Illuminate\Database\Schema\MySqlSchemaState; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionMethod; class DatabaseMySqlSchemaStateTest extends TestCase { - /** - * @dataProvider provider - */ + #[DataProvider('provider')] public function testConnectionString(string $expectedConnectionString, array $expectedVariables, array $dbConfig): void { $connection = $this->createMock(MySqlConnection::class); diff --git a/tests/Database/DatabasePostgresBuilderTest.php b/tests/Database/DatabasePostgresBuilderTest.php index b5e5af603462..11c2b9ca41ac 100644 --- a/tests/Database/DatabasePostgresBuilderTest.php +++ b/tests/Database/DatabasePostgresBuilderTest.php @@ -146,13 +146,13 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathMissing() $connection->shouldReceive('getConfig')->with('schema')->andReturn(null); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumnListing')->andReturn('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'); - $connection->shouldReceive('selectFromWriteConnection')->with('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?', ['laravel', 'public', 'foo'])->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('laravel', 'public', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); $connection->shouldReceive('getTablePrefix'); $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processColumnListing')->andReturn(['some_column']); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); $builder = $this->getBuilder($connection); $builder->getColumnListing('foo'); @@ -164,13 +164,13 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathFilled() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('myapp,public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumnListing')->andReturn('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'); - $connection->shouldReceive('selectFromWriteConnection')->with('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?', ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('laravel', 'myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); $connection->shouldReceive('getTablePrefix'); $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processColumnListing')->andReturn(['some_column']); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); $builder = $this->getBuilder($connection); $builder->getColumnListing('foo'); @@ -183,13 +183,13 @@ public function testGetColumnListingWhenSchemaUnqualifiedAndSearchPathIsUserVari $connection->shouldReceive('getConfig')->with('search_path')->andReturn('$user'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumnListing')->andReturn('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'); - $connection->shouldReceive('selectFromWriteConnection')->with('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?', ['laravel', 'foouser', 'foo'])->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('laravel', 'foouser', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); $connection->shouldReceive('getTablePrefix'); $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processColumnListing')->andReturn(['some_column']); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); $builder = $this->getBuilder($connection); $builder->getColumnListing('foo'); @@ -201,13 +201,13 @@ public function testGetColumnListingWhenSchemaQualifiedAndSearchPathMismatches() $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumnListing')->andReturn('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'); - $connection->shouldReceive('selectFromWriteConnection')->with('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?', ['laravel', 'myapp', 'foo'])->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('laravel', 'myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); $connection->shouldReceive('getTablePrefix'); $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processColumnListing')->andReturn(['some_column']); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); $builder = $this->getBuilder($connection); $builder->getColumnListing('myapp.foo'); @@ -219,13 +219,13 @@ public function testGetColumnWhenDatabaseAndSchemaQualifiedAndSearchPathMismatch $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); $grammar = m::mock(PostgresGrammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); - $grammar->shouldReceive('compileColumnListing')->andReturn('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?'); - $connection->shouldReceive('selectFromWriteConnection')->with('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?', ['mydatabase', 'myapp', 'foo'])->andReturn(['countable_result']); + $grammar->shouldReceive('compileColumns')->with('mydatabase', 'myapp', 'foo')->andReturn('sql'); + $connection->shouldReceive('selectFromWriteConnection')->with('sql')->andReturn(['countable_result']); $connection->shouldReceive('getTablePrefix'); $connection->shouldReceive('getConfig')->with('database')->andReturn('laravel'); $processor = m::mock(PostgresProcessor::class); $connection->shouldReceive('getPostProcessor')->andReturn($processor); - $processor->shouldReceive('processColumnListing')->andReturn(['some_column']); + $processor->shouldReceive('processColumns')->andReturn([['name' => 'some_column']]); $builder = $this->getBuilder($connection); $builder->getColumnListing('mydatabase.myapp.foo'); diff --git a/tests/Database/DatabasePostgresProcessorTest.php b/tests/Database/DatabasePostgresProcessorTest.php index 6ce284f86de8..8213fc68aba7 100644 --- a/tests/Database/DatabasePostgresProcessorTest.php +++ b/tests/Database/DatabasePostgresProcessorTest.php @@ -7,20 +7,30 @@ class DatabasePostgresProcessorTest extends TestCase { - public function testProcessColumnListing() + public function testProcessColumns() { $processor = new PostgresProcessor; - $listing = [['column_name' => 'id'], ['column_name' => 'name'], ['column_name' => 'email']]; - $expected = ['id', 'name', 'email']; + $listing = [ + ['name' => 'id', 'type_name' => 'int4', 'type' => 'integer', 'collation' => '', 'nullable' => true, 'default' => "nextval('employee_id_seq'::regclass)", 'comment' => ''], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'character varying(100)', 'collation' => 'collate', 'nullable' => false, 'default' => '', 'comment' => 'foo'], + ['name' => 'balance', 'type_name' => 'numeric', 'type' => 'numeric(8,2)', 'collation' => '', 'nullable' => true, 'default' => '4', 'comment' => 'NULL'], + ['name' => 'birth_date', 'type_name' => 'timestamp', 'type' => 'timestamp(6) without time zone', 'collation' => '', 'nullable' => false, 'default' => '', 'comment' => ''], + ]; + $expected = [ + ['name' => 'id', 'type_name' => 'int4', 'type' => 'integer', 'collation' => '', 'nullable' => true, 'default' => null, 'auto_increment' => true, 'comment' => ''], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'character varying(100)', 'collation' => 'collate', 'nullable' => false, 'default' => '', 'auto_increment' => false, 'comment' => 'foo'], + ['name' => 'balance', 'type_name' => 'numeric', 'type' => 'numeric(8,2)', 'collation' => '', 'nullable' => true, 'default' => '4', 'auto_increment' => false, 'comment' => 'NULL'], + ['name' => 'birth_date', 'type_name' => 'timestamp', 'type' => 'timestamp(6) without time zone', 'collation' => '', 'nullable' => false, 'default' => '', 'auto_increment' => false, 'comment' => ''], + ]; - $this->assertEquals($expected, $processor->processColumnListing($listing)); + $this->assertEquals($expected, $processor->processColumns($listing)); // convert listing to objects to simulate PDO::FETCH_CLASS foreach ($listing as &$row) { $row = (object) $row; } - $this->assertEquals($expected, $processor->processColumnListing($listing)); + $this->assertEquals($expected, $processor->processColumns($listing)); } } diff --git a/tests/Database/DatabasePostgresSchemaBuilderTest.php b/tests/Database/DatabasePostgresSchemaBuilderTest.php index 7d917c36fe6b..448b41138a84 100755 --- a/tests/Database/DatabasePostgresSchemaBuilderTest.php +++ b/tests/Database/DatabasePostgresSchemaBuilderTest.php @@ -44,11 +44,11 @@ public function testGetColumnListing() $connection->shouldReceive('getConfig')->with('database')->andReturn('db'); $connection->shouldReceive('getConfig')->with('schema')->andReturn('schema'); $connection->shouldReceive('getConfig')->with('search_path')->andReturn('public'); - $grammar->shouldReceive('compileColumnListing')->once()->andReturn('sql'); - $processor->shouldReceive('processColumnListing')->once()->andReturn(['column']); + $grammar->shouldReceive('compileColumns')->with('db', 'public', 'prefix_table')->once()->andReturn('sql'); + $processor->shouldReceive('processColumns')->once()->andReturn([['name' => 'column']]); $builder = new PostgresBuilder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); - $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql', ['db', 'public', 'prefix_table'])->andReturn(['column']); + $connection->shouldReceive('selectFromWriteConnection')->once()->with('sql')->andReturn([['name' => 'column']]); $this->assertEquals(['column'], $builder->getColumnListing('table')); } diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 8ff15e2e14a7..db139e523280 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -1186,11 +1186,11 @@ public function testCompileTableExists() $this->assertSame('select * from information_schema.tables where table_catalog = ? and table_schema = ? and table_name = ? and table_type = \'BASE TABLE\'', $statement); } - public function testCompileColumnListing() + public function testCompileColumns() { - $statement = $this->getGrammar()->compileColumnListing(); + $statement = $this->getGrammar()->compileColumns('db', 'public', 'table'); - $this->assertSame('select column_name from information_schema.columns where table_catalog = ? and table_schema = ? and table_name = ?', $statement); + $this->assertStringContainsString("where c.relname = 'table' and n.nspname = 'public'", $statement); } protected function getConnection() diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 0c22cb687dc7..bba4137f94aa 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -4425,6 +4425,26 @@ public function testChunkPaginatesUsingIdWithAlias() }, 'table.id', 'table_id'); } + public function testChunkPaginatesUsingIdDesc() + { + $builder = $this->getMockQueryBuilder(); + $builder->orders[] = ['column' => 'foobar', 'direction' => 'desc']; + + $chunk1 = collect([(object) ['someIdField' => 10], (object) ['someIdField' => 1]]); + $chunk2 = collect([]); + $builder->shouldReceive('forPageBeforeId')->once()->with(2, 0, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('forPageBeforeId')->once()->with(2, 1, 'someIdField')->andReturnSelf(); + $builder->shouldReceive('get')->times(2)->andReturn($chunk1, $chunk2); + + $callbackAssertor = m::mock(stdClass::class); + $callbackAssertor->shouldReceive('doSomething')->once()->with($chunk1); + $callbackAssertor->shouldReceive('doSomething')->never()->with($chunk2); + + $builder->chunkByIdDesc(2, function ($results) use ($callbackAssertor) { + $callbackAssertor->doSomething($results); + }, 'someIdField'); + } + public function testPaginate() { $perPage = 16; diff --git a/tests/Database/DatabaseQueryGrammarTest.php b/tests/Database/DatabaseQueryGrammarTest.php new file mode 100644 index 000000000000..aee7f822caba --- /dev/null +++ b/tests/Database/DatabaseQueryGrammarTest.php @@ -0,0 +1,44 @@ +getMethod('whereRaw'); + $expressionArray = ['sql' => new Expression('select * from "users"')]; + + $rawQuery = $method->invoke($grammar, $builder, $expressionArray); + + $this->assertSame('select * from "users"', $rawQuery); + } + + public function testWhereRawReturnsStringWhenStringPassed() + { + $builder = m::mock(Builder::class); + $grammar = new Grammar; + $reflection = new ReflectionClass($grammar); + $method = $reflection->getMethod('whereRaw'); + $stringArray = ['sql' => 'select * from "users"']; + + $rawQuery = $method->invoke($grammar, $builder, $stringArray); + + $this->assertSame('select * from "users"', $rawQuery); + } +} diff --git a/tests/Database/DatabaseSQLiteProcessorTest.php b/tests/Database/DatabaseSQLiteProcessorTest.php index 8ab4b2b6520e..4aeb4438f7d7 100644 --- a/tests/Database/DatabaseSQLiteProcessorTest.php +++ b/tests/Database/DatabaseSQLiteProcessorTest.php @@ -7,20 +7,28 @@ class DatabaseSQLiteProcessorTest extends TestCase { - public function testProcessColumnListing() + public function testProcessColumns() { $processor = new SQLiteProcessor; - $listing = [['name' => 'id'], ['name' => 'name'], ['name' => 'email']]; - $expected = ['id', 'name', 'email']; + $listing = [ + ['name' => 'id', 'type' => 'INTEGER', 'nullable' => '0', 'default' => '', 'primary' => '1'], + ['name' => 'name', 'type' => 'varchar', 'nullable' => '1', 'default' => 'foo', 'primary' => '0'], + ['name' => 'is_active', 'type' => 'tinyint(1)', 'nullable' => '0', 'default' => '1', 'primary' => '0'], + ]; + $expected = [ + ['name' => 'id', 'type_name' => 'integer', 'type' => 'integer', 'collation' => null, 'nullable' => false, 'default' => '', 'auto_increment' => true, 'comment' => null], + ['name' => 'name', 'type_name' => 'varchar', 'type' => 'varchar', 'collation' => null, 'nullable' => true, 'default' => 'foo', 'auto_increment' => false, 'comment' => null], + ['name' => 'is_active', 'type_name' => 'tinyint', 'type' => 'tinyint(1)', 'collation' => null, 'nullable' => false, 'default' => '1', 'auto_increment' => false, 'comment' => null], + ]; - $this->assertEquals($expected, $processor->processColumnListing($listing)); + $this->assertEquals($expected, $processor->processColumns($listing)); // convert listing to objects to simulate PDO::FETCH_CLASS foreach ($listing as &$row) { $row = (object) $row; } - $this->assertEquals($expected, $processor->processColumnListing($listing)); + $this->assertEquals($expected, $processor->processColumns($listing)); } } diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index 1a80835d8abe..a414e813a49a 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -164,7 +164,7 @@ public function testRenameIndex() }); $manager = $db->getConnection()->getDoctrineSchemaManager(); - $details = $manager->listTableDetails('prefix_users'); + $details = $manager->introspectTable('prefix_users'); $this->assertTrue($details->hasIndex('index1')); $this->assertFalse($details->hasIndex('index2')); @@ -172,7 +172,7 @@ public function testRenameIndex() $table->renameIndex('index1', 'index2'); }); - $details = $manager->listTableDetails('prefix_users'); + $details = $manager->introspectTable('prefix_users'); $this->assertFalse($details->hasIndex('index1')); $this->assertTrue($details->hasIndex('index2')); diff --git a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php index d469645fc8a1..719540076439 100644 --- a/tests/Database/DatabaseSchemaBuilderIntegrationTest.php +++ b/tests/Database/DatabaseSchemaBuilderIntegrationTest.php @@ -39,28 +39,6 @@ protected function tearDown(): void Facade::setFacadeApplication(null); } - public function testDropAllTablesWorksWithForeignKeys() - { - $this->db->connection()->getSchemaBuilder()->create('table1', function (Blueprint $table) { - $table->integer('id'); - $table->string('name'); - }); - - $this->db->connection()->getSchemaBuilder()->create('table2', function (Blueprint $table) { - $table->integer('id'); - $table->string('user_id'); - $table->foreign('user_id')->references('id')->on('table1'); - }); - - $this->assertTrue($this->db->connection()->getSchemaBuilder()->hasTable('table1')); - $this->assertTrue($this->db->connection()->getSchemaBuilder()->hasTable('table2')); - - $this->db->connection()->getSchemaBuilder()->dropAllTables(); - - $this->assertFalse($this->db->connection()->getSchemaBuilder()->hasTable('table1')); - $this->assertFalse($this->db->connection()->getSchemaBuilder()->hasTable('table2')); - } - public function testHasColumnWithTablePrefix() { $this->db->connection()->setTablePrefix('test_'); @@ -73,40 +51,6 @@ public function testHasColumnWithTablePrefix() $this->assertTrue($this->db->connection()->getSchemaBuilder()->hasColumn('table1', 'name')); } - public function testHasColumnAndIndexWithPrefixIndexDisabled() - { - $this->db->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => 'example_', - 'prefix_indexes' => false, - ]); - - $this->db->connection()->getSchemaBuilder()->create('table1', function (Blueprint $table) { - $table->integer('id'); - $table->string('name')->index(); - }); - - $this->assertArrayHasKey('table1_name_index', $this->db->connection()->getDoctrineSchemaManager()->listTableIndexes('example_table1')); - } - - public function testHasColumnAndIndexWithPrefixIndexEnabled() - { - $this->db->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', - 'prefix' => 'example_', - 'prefix_indexes' => true, - ]); - - $this->db->connection()->getSchemaBuilder()->create('table1', function (Blueprint $table) { - $table->integer('id'); - $table->string('name')->index(); - }); - - $this->assertArrayHasKey('example_table1_name_index', $this->db->connection()->getDoctrineSchemaManager()->listTableIndexes('example_table1')); - } - public function testDropColumnWithTablePrefix() { $this->db->connection()->setTablePrefix('test_'); diff --git a/tests/Database/DatabaseSchemaBuilderTest.php b/tests/Database/DatabaseSchemaBuilderTest.php index b22bfd7dc70e..53bd2693e058 100755 --- a/tests/Database/DatabaseSchemaBuilderTest.php +++ b/tests/Database/DatabaseSchemaBuilderTest.php @@ -2,8 +2,11 @@ namespace Illuminate\Tests\Database; +use Doctrine\DBAL\Schema\Column; +use Doctrine\DBAL\Types\Type; use Illuminate\Database\Connection; use Illuminate\Database\Schema\Builder; +use Illuminate\Database\Schema\Grammars\Grammar; use LogicException; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -70,15 +73,16 @@ public function testTableHasColumns() public function testGetColumnTypeAddsPrefix() { $connection = m::mock(Connection::class); - $column = m::mock(stdClass::class); - $type = m::mock(stdClass::class); - $grammar = m::mock(stdClass::class); + $column = m::mock(Column::class); + $type = m::mock(Type::class); + $grammar = m::mock(Grammar::class); $connection->shouldReceive('getSchemaGrammar')->once()->andReturn($grammar); $builder = new Builder($connection); $connection->shouldReceive('getTablePrefix')->once()->andReturn('prefix_'); $connection->shouldReceive('getDoctrineColumn')->once()->with('prefix_users', 'id')->andReturn($column); + $connection->shouldReceive('usingNativeSchemaOperations')->once()->andReturn(false); $column->shouldReceive('getType')->once()->andReturn($type); - $type->shouldReceive('getName')->once()->andReturn('integer'); + $type->shouldReceive('lookupName')->once()->andReturn('integer'); $this->assertSame('integer', $builder->getColumnType('users', 'id')); } diff --git a/tests/Encryption/EncrypterTest.php b/tests/Encryption/EncrypterTest.php index c6a0f1d28143..64f2e9d39502 100755 --- a/tests/Encryption/EncrypterTest.php +++ b/tests/Encryption/EncrypterTest.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Encryption\DecryptException; use Illuminate\Encryption\Encrypter; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -221,9 +222,7 @@ public static function provideTamperedData() ]; } - /** - * @dataProvider provideTamperedData - */ + #[DataProvider('provideTamperedData')] public function testTamperedPayloadWillGetRejected($payload) { $this->expectException(DecryptException::class); diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index d6cc99c64dcd..3d509e0c7745 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -17,6 +17,7 @@ use League\Flysystem\UnableToRetrieveMetadata; use League\Flysystem\UnableToWriteFile; use Mockery as m; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\StreamedResponse; @@ -426,9 +427,7 @@ public function testPutFileWithoutPath() $this->assertSame('normal file content', $filesystemAdapter->read($storagePath)); } - /** - * @requires extension ftp - */ + #[RequiresPhpExtension('ftp')] public function testCreateFtpDriver() { $filesystem = new FilesystemManager(new Application); diff --git a/tests/Filesystem/FilesystemManagerTest.php b/tests/Filesystem/FilesystemManagerTest.php index ef1eb632f92a..8850c5557a06 100644 --- a/tests/Filesystem/FilesystemManagerTest.php +++ b/tests/Filesystem/FilesystemManagerTest.php @@ -6,6 +6,7 @@ use Illuminate\Filesystem\FilesystemManager; use Illuminate\Foundation\Application; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use PHPUnit\Framework\TestCase; class FilesystemManagerTest extends TestCase @@ -98,9 +99,7 @@ public function testCanBuildScopedDisks() } } - /** - * @requires OS Linux|Darwin - */ + #[RequiresOperatingSystem('Linux|Darwin')] public function testCanBuildScopedDisksWithVisibility() { try { diff --git a/tests/Filesystem/FilesystemTest.php b/tests/Filesystem/FilesystemTest.php index f9cd9cd239a6..54fc3b0b2bc2 100755 --- a/tests/Filesystem/FilesystemTest.php +++ b/tests/Filesystem/FilesystemTest.php @@ -7,6 +7,10 @@ use Illuminate\Support\LazyCollection; use Illuminate\Testing\Assert; use Mockery as m; +use PHPUnit\Framework\Attributes\AfterClass; +use PHPUnit\Framework\Attributes\BeforeClass; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use SplFileInfo; @@ -14,18 +18,14 @@ class FilesystemTest extends TestCase { private static $tempDir; - /** - * @beforeClass - */ + #[BeforeClass] public static function setUpTempDir() { self::$tempDir = sys_get_temp_dir().'/tmp'; mkdir(self::$tempDir); } - /** - * @afterClass - */ + #[AfterClass] public static function tearDownTempDir() { $files = new Filesystem; @@ -97,9 +97,7 @@ public function testReplaceInFileCorrectlyReplaces() $this->assertStringEqualsFile($tempFile, 'Hello Taylor'); } - /** - * @requires OS Linux|Darwin - */ + #[RequiresOperatingSystem('Linux|Darwin')] public function testReplaceWhenUnixSymlinkExists() { $tempFile = self::$tempDir.'/file.txt'; @@ -428,9 +426,7 @@ public function testSizeOutputsSize() $this->assertEquals($size, $files->size(self::$tempDir.'/foo.txt')); } - /** - * @requires extension fileinfo - */ + #[RequiresPhpExtension('fileinfo')] public function testMimeTypeOutputsMimeType() { file_put_contents(self::$tempDir.'/foo.txt', 'foo'); @@ -527,10 +523,8 @@ public function testMakeDirectory() $this->assertFileExists(self::$tempDir.'/created'); } - /** - * @requires extension pcntl - * @requires OS Linux|Darwin - */ + #[RequiresOperatingSystem('Linux|Darwin')] + #[RequiresPhpExtension('pcntl')] public function testSharedGet() { $content = str_repeat('123456', 1000000); diff --git a/tests/Foundation/Console/CliDumperTest.php b/tests/Foundation/Console/CliDumperTest.php index c8a9281dacde..c92245f9a703 100644 --- a/tests/Foundation/Console/CliDumperTest.php +++ b/tests/Foundation/Console/CliDumperTest.php @@ -55,7 +55,7 @@ public function testArray() $output = $this->dump(['string', 1, 1.1, ['string', 1, 1.1]]); $expected = <<<'EOF' - array:4 [ // app/routes/console.php:18 + array:4 [ 0 => "string" 1 => 1 2 => 1.1 @@ -64,7 +64,7 @@ public function testArray() 1 => 1 2 => 1.1 ] - ] + ] // app/routes/console.php:18 EOF; @@ -90,9 +90,9 @@ public function testObject() $objectId = spl_object_id($user); $expected = <<app = new Application( + $laravel = new \Illuminate\Foundation\Application(__DIR__), + m::mock(Dispatcher::class, ['dispatch' => null, 'fire' => null]), + 'testing', + ); + + $router = new Router(m::mock('Illuminate\Events\Dispatcher')); + + $kernel = new class($laravel, $router) extends Kernel + { + protected $middlewareGroups = [ + 'web' => ['Middleware 1', 'Middleware 2', 'Middleware 5'], + 'auth' => ['Middleware 3', 'Middleware 4'], + ]; + + protected $middlewarePriority = [ + 'Middleware 1', + 'Middleware 4', + 'Middleware 2', + 'Middleware 3', + ]; + }; + + $kernel->prependToMiddlewarePriority('Middleware 5'); + + $laravel->singleton(Kernel::class, function () use ($kernel) { + return $kernel; + }); + + $router->get('/example', function () { + return 'Hello World'; + })->middleware('exampleMiddleware'); + + $router->get('/example-group', function () { + return 'Hello Group'; + })->middleware(['web', 'auth']); + + $command = new RouteListCommand($router); + $command->setLaravel($laravel); + + $this->app->addCommands([$command]); + } + + public function testNoMiddlewareIfNotVerbose() + { + $this->app->call('route:list'); + $output = $this->app->output(); + + $this->assertStringNotContainsString('exampleMiddleware', $output); + } + + public function testMiddlewareGroupsAssignmentInCli() + { + $this->app->call('route:list', ['-v' => true]); + $output = $this->app->output(); + + $this->assertStringContainsString('exampleMiddleware', $output); + $this->assertStringContainsString('web', $output); + $this->assertStringContainsString('auth', $output); + + $this->assertStringNotContainsString('Middleware 1', $output); + $this->assertStringNotContainsString('Middleware 2', $output); + $this->assertStringNotContainsString('Middleware 3', $output); + $this->assertStringNotContainsString('Middleware 4', $output); + $this->assertStringNotContainsString('Middleware 5', $output); + } + + public function testMiddlewareGroupsExpandInCliIfVeryVerbose() + { + $this->app->call('route:list', ['-vv' => true]); + $output = $this->app->output(); + + $this->assertStringContainsString('exampleMiddleware', $output); + $this->assertStringContainsString('Middleware 1', $output); + $this->assertStringContainsString('Middleware 2', $output); + $this->assertStringContainsString('Middleware 3', $output); + $this->assertStringContainsString('Middleware 4', $output); + $this->assertStringContainsString('Middleware 5', $output); + + $this->assertStringNotContainsString('web', $output); + $this->assertStringNotContainsString('auth', $output); + } + + public function testMiddlewareGroupsAssignmentInJson() + { + $this->app->call('route:list', ['--json' => true, '-v' => true]); + $output = $this->app->output(); + + $this->assertStringContainsString('exampleMiddleware', $output); + $this->assertStringContainsString('web', $output); + $this->assertStringContainsString('auth', $output); + + $this->assertStringNotContainsString('Middleware 1', $output); + $this->assertStringNotContainsString('Middleware 2', $output); + $this->assertStringNotContainsString('Middleware 3', $output); + $this->assertStringNotContainsString('Middleware 4', $output); + $this->assertStringNotContainsString('Middleware 5', $output); + } + + public function testMiddlewareGroupsExpandInJsonIfVeryVerbose() + { + $this->app->call('route:list', ['--json' => true, '-vv' => true]); + $output = $this->app->output(); + + $this->assertStringContainsString('exampleMiddleware', $output); + $this->assertStringContainsString('Middleware 1', $output); + $this->assertStringContainsString('Middleware 2', $output); + $this->assertStringContainsString('Middleware 3', $output); + $this->assertStringContainsString('Middleware 4', $output); + $this->assertStringContainsString('Middleware 5', $output); + + $this->assertStringNotContainsString('web', $output); + $this->assertStringNotContainsString('auth', $output); + } + + public function testMiddlewareGroupsExpandCorrectlySortedIfVeryVerbose() + { + $this->app->call('route:list', ['--json' => true, '-vv' => true]); + $output = $this->app->output(); + + $expectedOrder = '[{"domain":null,"method":"GET|HEAD","uri":"example","name":null,"action":"Closure","middleware":["exampleMiddleware"]},{"domain":null,"method":"GET|HEAD","uri":"example-group","name":null,"action":"Closure","middleware":["Middleware 5","Middleware 1","Middleware 4","Middleware 2","Middleware 3"]}]'; + + $this->assertJsonStringEqualsJsonString($expectedOrder, $output); + } +} diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php index 8f1ce849d6a8..f23dc801f33d 100755 --- a/tests/Foundation/FoundationApplicationTest.php +++ b/tests/Foundation/FoundationApplicationTest.php @@ -500,7 +500,6 @@ public function testEnvPathsAreAbsoluteInWindows() ); } - /** @test */ public function testMacroable(): void { $app = new Application; @@ -517,7 +516,6 @@ public function testMacroable(): void $this->assertFalse($app->foo()); } - /** @test */ public function testUseConfigPath(): void { $app = new Application; diff --git a/tests/Foundation/FoundationExceptionsHandlerTest.php b/tests/Foundation/FoundationExceptionsHandlerTest.php index a804ecab2f10..ee4d42da088b 100644 --- a/tests/Foundation/FoundationExceptionsHandlerTest.php +++ b/tests/Foundation/FoundationExceptionsHandlerTest.php @@ -49,6 +49,8 @@ class FoundationExceptionsHandlerTest extends TestCase protected $config; + protected $viewFactory; + protected $container; protected $handler; @@ -59,14 +61,18 @@ protected function setUp(): void { $this->config = m::mock(Config::class); + $this->viewFactory = m::mock(ViewFactory::class); + $this->request = m::mock(stdClass::class); $this->container = Container::setInstance(new Container); $this->container->instance('config', $this->config); + $this->container->instance(ViewFactory::class, $this->viewFactory); + $this->container->instance(ResponseFactoryContract::class, new ResponseFactory( - m::mock(ViewFactory::class), + $this->viewFactory, m::mock(Redirector::class) )); @@ -397,6 +403,44 @@ public function getErrorView($e) $this->assertNull($handler->getErrorView(new HttpException(404))); } + private function executeScenarioWhereErrorViewThrowsWhileRenderingAndDebugIs($debug) + { + $this->viewFactory->shouldReceive('exists')->once()->with('errors::404')->andReturn(true); + $this->viewFactory->shouldReceive('make')->once()->withAnyArgs()->andThrow(new Exception('Rendering this view throws an exception')); + + $this->config->shouldReceive('get')->with('app.debug', null)->andReturn($debug); + + $handler = new class($this->container) extends Handler + { + protected function registerErrorViewPaths() + { + } + + public function getErrorView($e) + { + return $this->renderHttpException($e); + } + }; + + $this->assertInstanceOf(SymfonyResponse::class, $handler->getErrorView(new HttpException(404))); + } + + public function testItDoesNotCrashIfErrorViewThrowsWhileRenderingAndDebugFalse() + { + // When debug is false, the exception thrown while rendering the error view + // should not bubble as this may trigger an infinite loop. + } + + public function testItDoesNotCrashIfErrorViewThrowsWhileRenderingAndDebugTrue() + { + // When debug is true, it is OK to bubble the exception thrown while rendering + // the error view as the debug handler should handle this gracefully. + + $this->expectException(\Exception::class); + $this->expectExceptionMessage('Rendering this view throws an exception'); + $this->executeScenarioWhereErrorViewThrowsWhileRenderingAndDebugIs(true); + } + public function testAssertExceptionIsThrown() { $this->assertThrows(function () { diff --git a/tests/Foundation/Testing/DatabaseMigrationsTest.php b/tests/Foundation/Testing/DatabaseMigrationsTest.php index 133a7f5fdd06..0d14f7909c22 100644 --- a/tests/Foundation/Testing/DatabaseMigrationsTest.php +++ b/tests/Foundation/Testing/DatabaseMigrationsTest.php @@ -2,33 +2,36 @@ namespace Illuminate\Tests\Foundation\Testing; -use Illuminate\Contracts\Console\Kernel; +use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; use Illuminate\Foundation\Testing\DatabaseMigrations; use Illuminate\Foundation\Testing\RefreshDatabaseState; use Mockery as m; +use Orchestra\Testbench\Concerns\Testing; +use Orchestra\Testbench\Foundation\Application as Testbench; use PHPUnit\Framework\TestCase; use ReflectionMethod; +use function Orchestra\Testbench\package_path; + class DatabaseMigrationsTest extends TestCase { protected $traitObject; protected function setUp(): void { - RefreshDatabaseState::$migrated = false; + $this->traitObject = m::mock(DatabaseMigrationsTestMockClass::class)->makePartial(); + $this->traitObject->setUp(); + } - $this->traitObject = $this->getMockForAbstractClass(DatabaseMigrationsTestMockClass::class, [], '', true, true, true, [ - 'artisan', - 'beforeApplicationDestroyed', - ]); + protected function tearDown(): void + { + $this->traitObject->tearDown(); - $kernelObj = m::mock(); - $kernelObj->shouldReceive('setArtisan') - ->with(null); + if ($container = m::getContainer()) { + $this->addToAssertionCount($container->mockery_getExpectationCount()); + } - $this->traitObject->app = [ - Kernel::class => $kernelObj, - ]; + m::close(); } private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) @@ -44,8 +47,8 @@ private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) public function testRefreshTestDatabaseDefault() { $this->traitObject - ->expects($this->once()) - ->method('artisan') + ->shouldReceive('artisan') + ->once() ->with('migrate:fresh', [ '--drop-views' => false, '--drop-types' => false, @@ -62,8 +65,8 @@ public function testRefreshTestDatabaseWithDropViewsOption() $this->traitObject->dropViews = true; $this->traitObject - ->expects($this->once()) - ->method('artisan') + ->shouldReceive('artisan') + ->once() ->with('migrate:fresh', [ '--drop-views' => true, '--drop-types' => false, @@ -80,8 +83,8 @@ public function testRefreshTestDatabaseWithDropTypesOption() $this->traitObject->dropTypes = true; $this->traitObject - ->expects($this->once()) - ->method('artisan') + ->shouldReceive('artisan') + ->once() ->with('migrate:fresh', [ '--drop-views' => false, '--drop-types' => true, @@ -97,10 +100,43 @@ public function testRefreshTestDatabaseWithDropTypesOption() class DatabaseMigrationsTestMockClass { use DatabaseMigrations; - - public $app; + use InteractsWithConsole; + use Testing; public $dropViews = false; public $dropTypes = false; + + public function setUp() + { + RefreshDatabaseState::$migrated = false; + + $this->app = $this->refreshApplication(); + $this->withoutMockingConsoleOutput(); + } + + public function tearDown() + { + RefreshDatabaseState::$migrated = false; + + $this->callBeforeApplicationDestroyedCallbacks(); + $this->app?->flush(); + } + + protected function setUpTraits() + { + return []; + } + + protected function setUpTheTestEnvironmentTraitToBeIgnored(string $use): bool + { + return true; + } + + protected function refreshApplication() + { + return Testbench::create( + basePath: package_path('vendor/orchestra/testbench-core/laravel') + ); + } } diff --git a/tests/Foundation/Testing/RefreshDatabaseTest.php b/tests/Foundation/Testing/RefreshDatabaseTest.php index a5acc8275c85..5a15a5d4958e 100644 --- a/tests/Foundation/Testing/RefreshDatabaseTest.php +++ b/tests/Foundation/Testing/RefreshDatabaseTest.php @@ -2,13 +2,17 @@ namespace Illuminate\Tests\Foundation\Testing; -use Illuminate\Contracts\Console\Kernel; +use Illuminate\Foundation\Testing\Concerns\InteractsWithConsole; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabaseState; use Mockery as m; +use Orchestra\Testbench\Concerns\Testing; +use Orchestra\Testbench\Foundation\Application as Testbench; use PHPUnit\Framework\TestCase; use ReflectionMethod; +use function Orchestra\Testbench\package_path; + class RefreshDatabaseTest extends TestCase { protected $traitObject; @@ -17,18 +21,19 @@ protected function setUp(): void { RefreshDatabaseState::$migrated = false; - $this->traitObject = $this->getMockForAbstractClass(RefreshDatabaseTestMockClass::class, [], '', true, true, true, [ - 'artisan', - 'beginDatabaseTransaction', - ]); + $this->traitObject = m::mock(RefreshDatabaseTestMockClass::class)->makePartial(); + $this->traitObject->setUp(); + } + + protected function tearDown(): void + { + $this->traitObject->tearDown(); - $kernelObj = m::mock(); - $kernelObj->shouldReceive('setArtisan') - ->with(null); + if ($container = m::getContainer()) { + $this->addToAssertionCount($container->mockery_getExpectationCount()); + } - $this->traitObject->app = [ - Kernel::class => $kernelObj, - ]; + m::close(); } private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) @@ -44,8 +49,8 @@ private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) public function testRefreshTestDatabaseDefault() { $this->traitObject - ->expects($this->once()) - ->method('artisan') + ->shouldReceive('artisan') + ->once() ->with('migrate:fresh', [ '--drop-views' => false, '--drop-types' => false, @@ -62,8 +67,8 @@ public function testRefreshTestDatabaseWithDropViewsOption() $this->traitObject->dropViews = true; $this->traitObject - ->expects($this->once()) - ->method('artisan') + ->shouldReceive('artisan') + ->once() ->with('migrate:fresh', [ '--drop-views' => true, '--drop-types' => false, @@ -80,8 +85,8 @@ public function testRefreshTestDatabaseWithDropTypesOption() $this->traitObject->dropTypes = true; $this->traitObject - ->expects($this->once()) - ->method('artisan') + ->shouldReceive('artisan') + ->once() ->with('migrate:fresh', [ '--drop-views' => false, '--drop-types' => true, @@ -96,11 +101,44 @@ public function testRefreshTestDatabaseWithDropTypesOption() class RefreshDatabaseTestMockClass { + use InteractsWithConsole; use RefreshDatabase; - - public $app; + use Testing; public $dropViews = false; public $dropTypes = false; + + public function setUp() + { + RefreshDatabaseState::$migrated = false; + + $this->app = $this->refreshApplication(); + $this->withoutMockingConsoleOutput(); + } + + public function tearDown() + { + RefreshDatabaseState::$migrated = false; + + $this->callBeforeApplicationDestroyedCallbacks(); + $this->app?->flush(); + } + + protected function setUpTraits() + { + return []; + } + + protected function setUpTheTestEnvironmentTraitToBeIgnored(string $use): bool + { + return true; + } + + public function refreshApplication() + { + return Testbench::create( + basePath: package_path('vendor/orchestra/testbench-core/laravel') + ); + } } diff --git a/tests/Foundation/Testing/Traits/CanConfigureMigrationCommandsTest.php b/tests/Foundation/Testing/Traits/CanConfigureMigrationCommandsTest.php index 4a5a49696f1e..58f54a633931 100644 --- a/tests/Foundation/Testing/Traits/CanConfigureMigrationCommandsTest.php +++ b/tests/Foundation/Testing/Traits/CanConfigureMigrationCommandsTest.php @@ -12,7 +12,7 @@ class CanConfigureMigrationCommandsTest extends TestCase protected function setUp(): void { - $this->traitObject = $this->getMockForAbstractClass(CanConfigureMigrationCommandsTestMockClass::class); + $this->traitObject = new CanConfigureMigrationCommandsTestMockClass(); } private function __reflectAndSetupAccessibleForProtectedTraitMethod($methodName) diff --git a/tests/Hashing/HasherTest.php b/tests/Hashing/HasherTest.php index 3559cc0303d3..70c6c3d175b0 100755 --- a/tests/Hashing/HasherTest.php +++ b/tests/Hashing/HasherTest.php @@ -8,6 +8,7 @@ use Illuminate\Hashing\ArgonHasher; use Illuminate\Hashing\BcryptHasher; use Illuminate\Hashing\HashManager; +use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\TestCase; use RuntimeException; @@ -82,9 +83,7 @@ public function testBasicArgon2idHashing() $this->assertTrue($this->hashManager->isHashed($value)); } - /** - * @depends testBasicBcryptHashing - */ + #[Depends('testBasicBcryptHashing')] public function testBasicBcryptVerification() { $this->expectException(RuntimeException::class); @@ -94,9 +93,7 @@ public function testBasicBcryptVerification() (new BcryptHasher(['verify' => true]))->check('password', $argonHashed); } - /** - * @depends testBasicArgon2iHashing - */ + #[Depends('testBasicArgon2iHashing')] public function testBasicArgon2iVerification() { $this->expectException(RuntimeException::class); @@ -106,9 +103,7 @@ public function testBasicArgon2iVerification() (new ArgonHasher(['verify' => true]))->check('password', $bcryptHashed); } - /** - * @depends testBasicArgon2idHashing - */ + #[Depends('testBasicArgon2idHashing')] public function testBasicArgon2idVerification() { $this->expectException(RuntimeException::class); diff --git a/tests/Http/HttpJsonResponseTest.php b/tests/Http/HttpJsonResponseTest.php index 48b5aef73267..a2b296da59b7 100644 --- a/tests/Http/HttpJsonResponseTest.php +++ b/tests/Http/HttpJsonResponseTest.php @@ -7,14 +7,13 @@ use Illuminate\Http\JsonResponse; use InvalidArgumentException; use JsonSerializable; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use stdClass; class HttpJsonResponseTest extends TestCase { - /** - * @dataProvider setAndRetrieveDataProvider - */ + #[DataProvider('setAndRetrieveDataProvider')] public function testSetAndRetrieveData($data) { $response = new JsonResponse($data); @@ -67,9 +66,7 @@ public function testSetAndRetrieveStatusCode() $this->assertSame(404, $response->getStatusCode()); } - /** - * @dataProvider jsonErrorDataProvider - */ + #[DataProvider('jsonErrorDataProvider')] public function testInvalidArgumentExceptionOnJsonError($data) { $this->expectException(InvalidArgumentException::class); @@ -77,9 +74,7 @@ public function testInvalidArgumentExceptionOnJsonError($data) new JsonResponse(['data' => $data]); } - /** - * @dataProvider jsonErrorDataProvider - */ + #[DataProvider('jsonErrorDataProvider')] public function testGracefullyHandledSomeJsonErrorsWithPartialOutputOnError($data) { new JsonResponse(['data' => $data], 200, [], JSON_PARTIAL_OUTPUT_ON_ERROR); diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index 124f0bdbd33a..10db3c4701a5 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -12,6 +12,7 @@ use Illuminate\Tests\Database\Fixtures\Models\Money\Price; use InvalidArgumentException; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use RuntimeException; use Symfony\Component\HttpFoundation\Exception\SessionNotFoundException; @@ -80,9 +81,7 @@ public function testDecodedPathMethod() $this->assertSame('foo bar', $request->decodedPath()); } - /** - * @dataProvider segmentProvider - */ + #[DataProvider('segmentProvider')] public function testSegmentMethod($path, $segment, $expected) { $request = Request::create($path); @@ -99,9 +98,7 @@ public static function segmentProvider() ]; } - /** - * @dataProvider segmentsProvider - */ + #[DataProvider('segmentsProvider')] public function testSegmentsMethod($path, $expected) { $request = Request::create($path); @@ -1068,9 +1065,7 @@ public static function getPrefersCases() ]; } - /** - * @dataProvider getPrefersCases - */ + #[DataProvider('getPrefersCases')] public function testPrefersMethod($accept, $prefers, $expected) { $this->assertSame( diff --git a/tests/Http/HttpTestingFileFactoryTest.php b/tests/Http/HttpTestingFileFactoryTest.php index c2824b6fe327..5ee649cce689 100644 --- a/tests/Http/HttpTestingFileFactoryTest.php +++ b/tests/Http/HttpTestingFileFactoryTest.php @@ -3,13 +3,14 @@ namespace Illuminate\Tests\Http; use Illuminate\Http\Testing\FileFactory; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; /** - * @requires extension gd - * * @link https://www.php.net/manual/en/function.gd-info.php */ +#[RequiresPhpExtension('gd')] class HttpTestingFileFactoryTest extends TestCase { public function testImagePng() @@ -118,7 +119,7 @@ public function testCreateWithoutMimeType() ); } - /** @dataProvider generateImageDataProvider */ + #[DataProvider('generateImageDataProvider')] public function testCallingCreateWithoutGDLoadedThrowsAnException(string $fileExtension, string $driver) { if ($this->isGDSupported($driver)) { diff --git a/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php b/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php index c5fc77aec88d..df78219bb62a 100644 --- a/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php +++ b/tests/Integration/Auth/ApiAuthenticationWithEloquentTest.php @@ -7,10 +7,9 @@ use Illuminate\Support\Facades\Route; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_mysql - */ +#[RequiresPhpExtension('pdo_mysql')] class ApiAuthenticationWithEloquentTest extends TestCase { protected function getEnvironmentSetUp($app) diff --git a/tests/Integration/Auth/ForgotPasswordTest.php b/tests/Integration/Auth/ForgotPasswordTest.php index 94766105ae78..46a682c229d8 100644 --- a/tests/Integration/Auth/ForgotPasswordTest.php +++ b/tests/Integration/Auth/ForgotPasswordTest.php @@ -43,8 +43,7 @@ protected function defineRoutes($router) })->name('custom.password.reset'); } - /** @test */ - public function it_can_send_forgot_password_email() + public function testItCanSendForgotPasswordEmail() { Notification::fake(); @@ -67,8 +66,7 @@ function (ResetPassword $notification, $channels) use ($user) { ); } - /** @test */ - public function it_can_send_forgot_password_email_via_create_url_using() + public function testItCanSendForgotPasswordEmailViaCreateUrlUsing() { Notification::fake(); @@ -95,8 +93,7 @@ function (ResetPassword $notification, $channels) use ($user) { ); } - /** @test */ - public function it_can_send_forgot_password_email_via_to_mail_using() + public function testItCanSendForgotPasswordEmailViaToMailUsing() { Notification::fake(); diff --git a/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php b/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php index 221601ac76f6..e34dcd78b82d 100644 --- a/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php +++ b/tests/Integration/Auth/ForgotPasswordWithoutDefaultRoutesTest.php @@ -39,8 +39,7 @@ protected function defineRoutes($router) })->name('custom.password.reset'); } - /** @test */ - public function it_cannot_send_forgot_password_email() + public function testItCannotSendForgotPasswordEmail() { $this->expectException('Symfony\Component\Routing\Exception\RouteNotFoundException'); $this->expectExceptionMessage('Route [password.reset] not defined.'); @@ -66,8 +65,7 @@ function (ResetPassword $notification, $channels) use ($user) { ); } - /** @test */ - public function it_can_send_forgot_password_email_via_create_url_using() + public function testItCanSendForgotPasswordEmailViaCreateUrlUsing() { Notification::fake(); @@ -94,8 +92,7 @@ function (ResetPassword $notification, $channels) use ($user) { ); } - /** @test */ - public function it_can_send_forgot_password_email_via_to_mail_using() + public function testItCanSendForgotPasswordEmailViaToMailUsing() { Notification::fake(); diff --git a/tests/Integration/Cache/MemcachedCacheLockTestCase.php b/tests/Integration/Cache/MemcachedCacheLockTestCase.php index 7f3e93f8c7c0..706bb8b3ae37 100644 --- a/tests/Integration/Cache/MemcachedCacheLockTestCase.php +++ b/tests/Integration/Cache/MemcachedCacheLockTestCase.php @@ -4,10 +4,9 @@ use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Support\Facades\Cache; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension memcached - */ +#[RequiresPhpExtension('memcached')] class MemcachedCacheLockTestCase extends MemcachedIntegrationTestCase { public function testMemcachedLocksCanBeAcquiredAndReleased() diff --git a/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php b/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php index 4aab9422a8fd..ee7787aa407e 100644 --- a/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php +++ b/tests/Integration/Cache/MemcachedTaggedCacheTestCase.php @@ -3,10 +3,9 @@ namespace Illuminate\Tests\Integration\Cache; use Illuminate\Support\Facades\Cache; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension memcached - */ +#[RequiresPhpExtension('memcached')] class MemcachedTaggedCacheTestCase extends MemcachedIntegrationTestCase { public function testMemcachedCanStoreAndRetrieveTaggedCacheItems() diff --git a/tests/Integration/Cache/PhpRedisCacheLockTest.php b/tests/Integration/Cache/PhpRedisCacheLockTest.php index 0ca4ea85f960..fe40f2ac3fe3 100644 --- a/tests/Integration/Cache/PhpRedisCacheLockTest.php +++ b/tests/Integration/Cache/PhpRedisCacheLockTest.php @@ -5,6 +5,7 @@ use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Support\Facades\Cache; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use Redis; class PhpRedisCacheLockTest extends TestCase @@ -138,9 +139,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithMsgpackSerialization() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension lzf - */ + #[RequiresPhpExtension('lzf')] public function testRedisLockCanBeAcquiredAndReleasedWithLzfCompression() { if (! defined('Redis::COMPRESSION_LZF')) { @@ -167,9 +166,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithLzfCompression() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension zstd - */ + #[RequiresPhpExtension('zstd')] public function testRedisLockCanBeAcquiredAndReleasedWithZstdCompression() { if (! defined('Redis::COMPRESSION_ZSTD')) { @@ -215,9 +212,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithZstdCompression() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension lz4 - */ + #[RequiresPhpExtension('lz4')] public function testRedisLockCanBeAcquiredAndReleasedWithLz4Compression() { if (! defined('Redis::COMPRESSION_LZ4')) { @@ -263,9 +258,7 @@ public function testRedisLockCanBeAcquiredAndReleasedWithLz4Compression() $this->assertNull($store->lockConnection()->get($store->getPrefix().'foo')); } - /** - * @requires extension Lzf - */ + #[RequiresPhpExtension('lzf')] public function testRedisLockCanBeAcquiredAndReleasedWithSerializationAndCompression() { if (! defined('Redis::COMPRESSION_LZF')) { diff --git a/tests/Integration/Cache/Psr6RedisTest.php b/tests/Integration/Cache/Psr6RedisTest.php index a242a2f77e29..062d5ec4dbfb 100644 --- a/tests/Integration/Cache/Psr6RedisTest.php +++ b/tests/Integration/Cache/Psr6RedisTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Tests\Integration\Cache\Fixtures\Unserializable; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class Psr6RedisTest extends TestCase { @@ -25,9 +26,7 @@ protected function tearDown(): void $this->tearDownRedis(); } - /** - * @dataProvider redisClientDataProvider - */ + #[DataProvider('redisClientDataProvider')] public function testTransactionIsNotOpenedWhenSerializationFails($redisClient): void { $this->app['config']['cache.default'] = 'redis'; diff --git a/tests/Integration/Console/CommandEventsTest.php b/tests/Integration/Console/CommandEventsTest.php index 167835eb9b7f..cc63e22dc4fc 100644 --- a/tests/Integration/Console/CommandEventsTest.php +++ b/tests/Integration/Console/CommandEventsTest.php @@ -12,6 +12,7 @@ use Illuminate\Support\Str; use Orchestra\Testbench\Foundation\Application as Testbench; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class CommandEventsTest extends TestCase { @@ -48,9 +49,7 @@ protected function setUp(): void parent::setUp(); } - /** - * @dataProvider foregroundCommandEventsProvider - */ + #[DataProvider('foregroundCommandEventsProvider')] public function testCommandEventsReceiveParsedInput($callback) { $this->app[ConsoleKernel::class]->registerCommand(new CommandEventsTestCommand); diff --git a/tests/Integration/Console/CommandSchedulingTest.php b/tests/Integration/Console/CommandSchedulingTest.php index 1e02c3b3d2fa..1a4c918aa5e0 100644 --- a/tests/Integration/Console/CommandSchedulingTest.php +++ b/tests/Integration/Console/CommandSchedulingTest.php @@ -6,6 +6,7 @@ use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Str; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class CommandSchedulingTest extends TestCase { @@ -62,9 +63,7 @@ protected function tearDown(): void parent::tearDown(); } - /** - * @dataProvider executionProvider - */ + #[DataProvider('executionProvider')] public function testExecutionOrder($background, $expected) { $schedule = $this->app->make(Schedule::class); diff --git a/tests/Integration/Console/ConsoleApplicationTest.php b/tests/Integration/Console/ConsoleApplicationTest.php index 8d6a607aa46f..9daafb57c89a 100644 --- a/tests/Integration/Console/ConsoleApplicationTest.php +++ b/tests/Integration/Console/ConsoleApplicationTest.php @@ -5,6 +5,8 @@ use Illuminate\Console\Command; use Illuminate\Console\Scheduling\Schedule; use Illuminate\Contracts\Console\Kernel; +use Illuminate\Foundation\Console\QueuedCommand; +use Illuminate\Support\Facades\Queue; use Orchestra\Testbench\TestCase; class ConsoleApplicationTest extends TestCase @@ -65,6 +67,19 @@ public function testArtisanInstantiateScheduleWhenNeed() $this->assertTrue($this->app->resolved(Schedule::class)); } + + public function testArtisanQueue() + { + Queue::fake(); + + $this->app[Kernel::class]->queue('foo:bar', [ + 'id' => 1, + ]); + + Queue::assertPushed(QueuedCommand::class, function ($job) { + return $job->displayName() === 'foo:bar'; + }); + } } class FooCommandStub extends Command diff --git a/tests/Integration/Console/GeneratorCommandTest.php b/tests/Integration/Console/GeneratorCommandTest.php new file mode 100644 index 000000000000..cb8ce75a6818 --- /dev/null +++ b/tests/Integration/Console/GeneratorCommandTest.php @@ -0,0 +1,27 @@ +artisan('make:command', ['name' => $given]) + ->expectsOutputToContain('The name "'.$given.'" is reserved by PHP.') + ->assertExitCode(0); + } + + public static function reservedNamesDataProvider() + { + yield ['__halt_compiler']; + yield ['__HALT_COMPILER']; + yield ['array']; + yield ['ARRAY']; + yield ['__class__']; + yield ['__CLASS__']; + } +} diff --git a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php index bc7b98d5b1b4..f40626a9dbe5 100644 --- a/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php +++ b/tests/Integration/Console/Scheduling/SubMinuteSchedulingTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Sleep; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class SubMinuteSchedulingTest extends TestCase { @@ -45,7 +46,7 @@ public function test_it_doesnt_wait_for_sub_minute_events_when_none_are_schedule Sleep::assertNeverSlept(); } - /** @dataProvider frequencyProvider */ + #[DataProvider('frequencyProvider')] public function test_it_runs_sub_minute_callbacks($frequency, $expectedRuns) { $runs = 0; diff --git a/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php b/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php index f0f36deb4e36..6cf2c07af602 100644 --- a/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php +++ b/tests/Integration/Database/ConfigureCustomDoctrineTypeTest.php @@ -25,7 +25,7 @@ public function testRegisterCustomDoctrineTypesWithNonDefaultDatabaseConnections { $this->assertTrue( DB::connection() - ->getDoctrineSchemaManager() + ->getDoctrineConnection() ->getDatabasePlatform() ->hasDoctrineTypeMappingFor('xml') ); @@ -34,7 +34,7 @@ public function testRegisterCustomDoctrineTypesWithNonDefaultDatabaseConnections // this is not the default connection but it has the custom type mappings $this->assertTrue( DB::connection('sqlite') - ->getDoctrineSchemaManager() + ->getDoctrineConnection() ->getDatabasePlatform() ->hasDoctrineTypeMappingFor('xml') ); @@ -87,7 +87,7 @@ public function testRenameConfiguredCustomDoctrineColumnTypeWithMysql() class PostgresXmlType extends Type { - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string { return 'xml'; } @@ -100,7 +100,7 @@ public function getName() class MySQLBitType extends Type { - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string { return 'bit'; } diff --git a/tests/Integration/Database/DBAL/TimestampTypeTest.php b/tests/Integration/Database/DBAL/TimestampTypeTest.php index d7cec38c53b0..f53265cf8431 100644 --- a/tests/Integration/Database/DBAL/TimestampTypeTest.php +++ b/tests/Integration/Database/DBAL/TimestampTypeTest.php @@ -20,7 +20,7 @@ public function testRegisterTimestampTypeOnConnection() { $this->assertTrue( $this->app['db']->connection() - ->getDoctrineSchemaManager() + ->getDoctrineConnection() ->getDatabasePlatform() ->hasDoctrineTypeMappingFor('timestamp') ); diff --git a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php b/tests/Integration/Database/DatabaseSchemaBlueprintTest.php similarity index 82% rename from tests/Database/DatabaseSchemaBlueprintIntegrationTest.php rename to tests/Integration/Database/DatabaseSchemaBlueprintTest.php index 5fd34340606a..66a217a02f15 100644 --- a/tests/Database/DatabaseSchemaBlueprintIntegrationTest.php +++ b/tests/Integration/Database/DatabaseSchemaBlueprintTest.php @@ -1,53 +1,38 @@ db = $db = new DB; - - $db->addConnection([ - 'driver' => 'sqlite', - 'database' => ':memory:', - ]); - - $db->setAsGlobal(); + $this->beforeApplicationDestroyed(function () { + Schema::useNativeSchemaOperationsIfPossible(false); + }); - $container = new Container; - $container->instance('db', $db->getDatabaseManager()); - Facade::setFacadeApplication($container); + parent::setUp(); } - protected function tearDown(): void + protected function defineEnvironment($app) { - Facade::clearResolvedInstances(); - Facade::setFacadeApplication(null); - $this->db->connection()->getSchemaBuilder()->useNativeSchemaOperationsIfPossible(false); + $app['config']->set([ + 'database.default' => 'testing', + ]); } public function testRenamingAndChangingColumnsWork() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->string('name'); $table->string('age'); }); @@ -57,31 +42,19 @@ public function testRenamingAndChangingColumnsWork() $table->integer('age')->change(); }); - $queries = $blueprint->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint->toSql(DB::connection(), new SQLiteGrammar); // Expect one of the following two query sequences to be present... $expected = [ [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name, age FROM users', 'DROP TABLE users', - 'CREATE TABLE users (name VARCHAR(255) NOT NULL, age INTEGER NOT NULL)', + 'CREATE TABLE users (name VARCHAR NOT NULL, age INTEGER NOT NULL)', 'INSERT INTO users (name, age) SELECT name, age FROM __temp__users', 'DROP TABLE __temp__users', 'CREATE TEMPORARY TABLE __temp__users AS SELECT name, age FROM users', 'DROP TABLE users', - 'CREATE TABLE users (first_name VARCHAR(255) NOT NULL, age VARCHAR(255) NOT NULL COLLATE "BINARY")', - 'INSERT INTO users (first_name, age) SELECT name, age FROM __temp__users', - 'DROP TABLE __temp__users', - ], - [ - 'CREATE TEMPORARY TABLE __temp__users AS SELECT name, age FROM users', - 'DROP TABLE users', - 'CREATE TABLE users (name VARCHAR(255) NOT NULL COLLATE "BINARY", age INTEGER NOT NULL)', - 'INSERT INTO users (name, age) SELECT name, age FROM __temp__users', - 'DROP TABLE __temp__users', - 'CREATE TEMPORARY TABLE __temp__users AS SELECT name, age FROM users', - 'DROP TABLE users', - 'CREATE TABLE users (first_name VARCHAR(255) NOT NULL, age VARCHAR(255) NOT NULL COLLATE "BINARY")', + 'CREATE TABLE users (first_name VARCHAR NOT NULL, age VARCHAR NOT NULL)', 'INSERT INTO users (first_name, age) SELECT name, age FROM __temp__users', 'DROP TABLE __temp__users', ], @@ -92,7 +65,7 @@ public function testRenamingAndChangingColumnsWork() public function testRenamingColumnsWithoutDoctrineWorks() { - $connection = $this->db->connection(); + $connection = DB::connection(); $schema = $connection->getSchemaBuilder(); $schema->useNativeSchemaOperationsIfPossible(); @@ -130,7 +103,7 @@ public function testRenamingColumnsWithoutDoctrineWorks() public function testDroppingColumnsWithoutDoctrineWorks() { - $connection = $this->db->connection(); + $connection = DB::connection(); $schema = $connection->getSchemaBuilder(); $schema->useNativeSchemaOperationsIfPossible(); @@ -144,7 +117,7 @@ public function testDroppingColumnsWithoutDoctrineWorks() public function testNativeColumnModifyingOnMySql() { - $connection = $this->db->connection(); + $connection = DB::connection(); $schema = $connection->getSchemaBuilder(); $schema->useNativeSchemaOperationsIfPossible(); @@ -172,7 +145,7 @@ public function testNativeColumnModifyingOnMySql() public function testNativeColumnModifyingOnPostgreSql() { - $connection = $this->db->connection(); + $connection = DB::connection(); $schema = $connection->getSchemaBuilder(); $schema->useNativeSchemaOperationsIfPossible(); @@ -248,7 +221,7 @@ public function testNativeColumnModifyingOnPostgreSql() public function testNativeColumnModifyingOnSqlServer() { - $connection = $this->db->connection(); + $connection = DB::connection(); $schema = $connection->getSchemaBuilder(); $schema->useNativeSchemaOperationsIfPossible(); @@ -285,7 +258,7 @@ public function testNativeColumnModifyingOnSqlServer() public function testChangingColumnWithCollationWorks() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->string('age'); }); @@ -297,13 +270,13 @@ public function testChangingColumnWithCollationWorks() $table->integer('age')->collation('NOCASE')->change(); }); - $queries = $blueprint->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint->toSql(DB::connection(), new SQLiteGrammar); $expected = [ [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT age FROM users', 'DROP TABLE users', - 'CREATE TABLE users (age INTEGER NOT NULL)', + 'CREATE TABLE users (age INTEGER NOT NULL COLLATE "RTRIM")', 'INSERT INTO users (age) SELECT age FROM __temp__users', 'DROP TABLE __temp__users', ], @@ -311,13 +284,13 @@ public function testChangingColumnWithCollationWorks() $this->assertContains($queries, $expected); - $queries = $blueprint2->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint2->toSql(DB::connection(), new SQLiteGrammar); $expected = [ [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT age FROM users', 'DROP TABLE users', - 'CREATE TABLE users (age INTEGER NOT NULL)', + 'CREATE TABLE users (age INTEGER NOT NULL COLLATE "NOCASE")', 'INSERT INTO users (age) SELECT age FROM __temp__users', 'DROP TABLE __temp__users', ], @@ -328,7 +301,7 @@ public function testChangingColumnWithCollationWorks() public function testChangingCharColumnsWork() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->string('name'); }); @@ -336,7 +309,7 @@ public function testChangingCharColumnsWork() $table->char('name', 50)->change(); }); - $queries = $blueprint->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint->toSql(DB::connection(), new SQLiteGrammar); $expected = [ [ @@ -360,7 +333,7 @@ public function testChangingCharColumnsWork() public function testChangingPrimaryAutoincrementColumnsToNonAutoincrementColumnsWork() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->increments('id'); }); @@ -368,7 +341,7 @@ public function testChangingPrimaryAutoincrementColumnsToNonAutoincrementColumns $table->binary('id')->change(); }); - $queries = $blueprint->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint->toSql(DB::connection(), new SQLiteGrammar); $expected = [ [ @@ -385,7 +358,7 @@ public function testChangingPrimaryAutoincrementColumnsToNonAutoincrementColumns public function testChangingDoubleColumnsWork() { - $this->db->connection()->getSchemaBuilder()->create('products', function ($table) { + DB::connection()->getSchemaBuilder()->create('products', function ($table) { $table->integer('price'); }); @@ -393,7 +366,7 @@ public function testChangingDoubleColumnsWork() $table->double('price')->change(); }); - $queries = $blueprint->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint->toSql(DB::connection(), new SQLiteGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__products AS SELECT price FROM products', @@ -408,12 +381,12 @@ public function testChangingDoubleColumnsWork() public function testRenameIndexWorks() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->string('name'); $table->string('age'); }); - $this->db->connection()->getSchemaBuilder()->table('users', function ($table) { + DB::connection()->getSchemaBuilder()->table('users', function ($table) { $table->index(['name'], 'index1'); }); @@ -421,7 +394,7 @@ public function testRenameIndexWorks() $table->renameIndex('index1', 'index2'); }); - $queries = $blueprint->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprint->toSql(DB::connection(), new SQLiteGrammar); $expected = [ 'DROP INDEX index1', @@ -430,7 +403,7 @@ public function testRenameIndexWorks() $this->assertEquals($expected, $queries); - $queries = $blueprint->toSql($this->db->connection(), new SqlServerGrammar); + $queries = $blueprint->toSql(DB::connection(), new SqlServerGrammar); $expected = [ 'sp_rename N\'"users"."index1"\', "index2", N\'INDEX\'', @@ -438,7 +411,7 @@ public function testRenameIndexWorks() $this->assertEquals($expected, $queries); - $queries = $blueprint->toSql($this->db->connection(), new MySqlGrammar); + $queries = $blueprint->toSql(DB::connection(), new MySqlGrammar); $expected = [ 'alter table `users` rename index `index1` to `index2`', @@ -446,7 +419,7 @@ public function testRenameIndexWorks() $this->assertEquals($expected, $queries); - $queries = $blueprint->toSql($this->db->connection(), new PostgresGrammar); + $queries = $blueprint->toSql(DB::connection(), new PostgresGrammar); $expected = [ 'alter index "index1" rename to "index2"', @@ -457,7 +430,7 @@ public function testRenameIndexWorks() public function testAddUniqueIndexWithoutNameWorks() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->string('name')->nullable(); }); @@ -465,10 +438,15 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintMySql->toSql($this->db->connection(), new MySqlGrammar); + $queries = $blueprintMySql->toSql(DB::connection(), new MySqlGrammar); $expected = [ [ + 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', + 'DROP TABLE users', + 'CREATE TABLE users (name VARCHAR(255) DEFAULT NULL)', + 'INSERT INTO users (name) SELECT name FROM __temp__users', + 'DROP TABLE __temp__users', 'alter table `users` add unique `users_name_unique`(`name`)', ], ]; @@ -479,10 +457,16 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintPostgres->toSql($this->db->connection(), new PostgresGrammar); + $queries = $blueprintPostgres->toSql(DB::connection(), new PostgresGrammar); $expected = [ [ + + 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', + 'DROP TABLE users', + 'CREATE TABLE users (name VARCHAR(255) DEFAULT NULL)', + 'INSERT INTO users (name) SELECT name FROM __temp__users', + 'DROP TABLE __temp__users', 'alter table "users" add constraint "users_name_unique" unique ("name")', ], ]; @@ -493,10 +477,15 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintSQLite->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprintSQLite->toSql(DB::connection(), new SQLiteGrammar); $expected = [ [ + 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', + 'DROP TABLE users', + 'CREATE TABLE users (name VARCHAR(255) DEFAULT NULL)', + 'INSERT INTO users (name) SELECT name FROM __temp__users', + 'DROP TABLE __temp__users', 'create unique index "users_name_unique" on "users" ("name")', ], ]; @@ -507,10 +496,15 @@ public function testAddUniqueIndexWithoutNameWorks() $table->string('name')->nullable()->unique()->change(); }); - $queries = $blueprintSqlServer->toSql($this->db->connection(), new SqlServerGrammar); + $queries = $blueprintSqlServer->toSql(DB::connection(), new SqlServerGrammar); $expected = [ [ + 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', + 'DROP TABLE users', + 'CREATE TABLE users (name VARCHAR(255) DEFAULT NULL)', + 'INSERT INTO users (name) SELECT name FROM __temp__users', + 'DROP TABLE __temp__users', 'create unique index "users_name_unique" on "users" ("name")', ], ]; @@ -520,7 +514,7 @@ public function testAddUniqueIndexWithoutNameWorks() public function testAddUniqueIndexWithNameWorks() { - $this->db->connection()->getSchemaBuilder()->create('users', function ($table) { + DB::connection()->getSchemaBuilder()->create('users', function ($table) { $table->string('name')->nullable(); }); @@ -528,10 +522,15 @@ public function testAddUniqueIndexWithNameWorks() $table->string('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintMySql->toSql($this->db->connection(), new MySqlGrammar); + $queries = $blueprintMySql->toSql(DB::connection(), new MySqlGrammar); $expected = [ [ + 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', + 'DROP TABLE users', + 'CREATE TABLE users (name VARCHAR(255) DEFAULT NULL)', + 'INSERT INTO users (name) SELECT name FROM __temp__users', + 'DROP TABLE __temp__users', 'alter table `users` add unique `index1`(`name`)', ], ]; @@ -542,7 +541,7 @@ public function testAddUniqueIndexWithNameWorks() $table->unsignedInteger('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintPostgres->toSql($this->db->connection(), new PostgresGrammar); + $queries = $blueprintPostgres->toSql(DB::connection(), new PostgresGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -559,7 +558,7 @@ public function testAddUniqueIndexWithNameWorks() $table->unsignedInteger('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintSQLite->toSql($this->db->connection(), new SQLiteGrammar); + $queries = $blueprintSQLite->toSql(DB::connection(), new SQLiteGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -576,7 +575,7 @@ public function testAddUniqueIndexWithNameWorks() $table->unsignedInteger('name')->nullable()->unique('index1')->change(); }); - $queries = $blueprintSqlServer->toSql($this->db->connection(), new SqlServerGrammar); + $queries = $blueprintSqlServer->toSql(DB::connection(), new SqlServerGrammar); $expected = [ 'CREATE TEMPORARY TABLE __temp__users AS SELECT name FROM users', @@ -592,7 +591,7 @@ public function testAddUniqueIndexWithNameWorks() public function testDropIndexOnColumnChangeWorks() { - $connection = $this->db->connection(); + $connection = DB::connection(); $connection->getSchemaBuilder()->create('users', function ($table) { $table->string('name')->nullable(); @@ -640,7 +639,7 @@ public function testItEnsuresDroppingMultipleColumnsIsAvailable() $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage("SQLite doesn't support multiple calls to dropColumn / renameColumn in a single modification."); - $this->db->connection()->getSchemaBuilder()->table('users', function (Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->dropColumn('name'); $table->dropColumn('email'); }); @@ -651,7 +650,7 @@ public function testItEnsuresRenamingMultipleColumnsIsAvailable() $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage("SQLite doesn't support multiple calls to dropColumn / renameColumn in a single modification."); - $this->db->connection()->getSchemaBuilder()->table('users', function (Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->renameColumn('name', 'first_name'); $table->renameColumn('name2', 'last_name'); }); @@ -662,7 +661,7 @@ public function testItEnsuresRenamingAndDroppingMultipleColumnsIsAvailable() $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage("SQLite doesn't support multiple calls to dropColumn / renameColumn in a single modification."); - $this->db->connection()->getSchemaBuilder()->table('users', function (Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->dropColumn('name'); $table->renameColumn('name2', 'last_name'); }); @@ -670,15 +669,16 @@ public function testItEnsuresRenamingAndDroppingMultipleColumnsIsAvailable() public function testItDoesNotSetPrecisionHigherThanSupportedWhenRenamingTimestamps() { - $this->db->connection()->getSchemaBuilder()->create('users', function (Blueprint $table) { + Schema::create('users', function (Blueprint $table) { $table->timestamp('created_at'); }); try { // this would only fail in mysql, postgres and sql server - $this->db->connection()->getSchemaBuilder()->table('users', function (Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->renameColumn('created_at', 'new_created_at'); }); + $this->addToAssertionCount(1); // it did not throw } catch (\Exception $e) { // Expecting something similar to: @@ -693,7 +693,7 @@ public function testItEnsuresDroppingForeignKeyIsAvailable() $this->expectException(BadMethodCallException::class); $this->expectExceptionMessage("SQLite doesn't support dropping foreign keys (you would need to re-create the table)."); - $this->db->connection()->getSchemaBuilder()->table('users', function (Blueprint $table) { + Schema::table('users', function (Blueprint $table) { $table->dropForeign('something'); }); } diff --git a/tests/Integration/Database/DatabaseSchemaBuilderTest.php b/tests/Integration/Database/DatabaseSchemaBuilderTest.php new file mode 100644 index 000000000000..b016a0124eee --- /dev/null +++ b/tests/Integration/Database/DatabaseSchemaBuilderTest.php @@ -0,0 +1,76 @@ +set([ + 'database.default' => 'testing', + 'database.connections.sqlite-with-prefix' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => 'example_', + 'prefix_indexes' => false, + ], + 'database.connections.sqlite-with-indexed-prefix' => [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => 'example_', + 'prefix_indexes' => true, + ], + ]); + } + + public function testDropAllTablesWorksWithForeignKeys() + { + Schema::create('table1', function (Blueprint $table) { + $table->integer('id'); + $table->string('name'); + }); + + Schema::create('table2', function (Blueprint $table) { + $table->integer('id'); + $table->string('user_id'); + $table->foreign('user_id')->references('id')->on('table1'); + }); + + $this->assertTrue(Schema::hasTable('table1')); + $this->assertTrue(Schema::hasTable('table2')); + + Schema::dropAllTables(); + + $this->assertFalse(Schema::hasTable('table1')); + $this->assertFalse(Schema::hasTable('table2')); + } + + public function testHasColumnAndIndexWithPrefixIndexDisabled() + { + $connection = DB::connection('sqlite-with-prefix'); + + Schema::connection('sqlite-with-prefix')->create('table1', function (Blueprint $table) { + $table->integer('id'); + $table->string('name')->index(); + }); + + $this->assertArrayHasKey('table1_name_index', $connection->getDoctrineSchemaManager()->listTableIndexes('example_table1')); + } + + public function testHasColumnAndIndexWithPrefixIndexEnabled() + { + $connection = DB::connection('sqlite-with-indexed-prefix'); + + Schema::connection('sqlite-with-indexed-prefix')->create('table1', function (Blueprint $table) { + $table->integer('id'); + $table->string('name')->index(); + }); + + $this->assertArrayHasKey('example_table1_name_index', $connection->getDoctrineSchemaManager()->listTableIndexes('example_table1')); + } +} diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php index b27149129fd1..0d2e15c54f41 100644 --- a/tests/Integration/Database/EloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -12,11 +12,10 @@ use Illuminate\Database\Eloquent\Model as Eloquent; use Illuminate\Database\Schema\Blueprint; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\Group; use PHPUnit\Framework\TestCase; -/** - * @group integration - */ +#[Group('integration')] class EloquentModelCustomCastingTest extends TestCase { protected function setUp(): void @@ -67,9 +66,7 @@ protected function tearDown(): void $this->schema()->drop('members'); } - /** - * @requires extension gmp - */ + #[RequiresPhpExtension('gmp')] public function testSavingCastedAttributesToDatabase() { /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ @@ -106,9 +103,7 @@ public function testSavingCastedAttributesToDatabase() $this->assertInstanceOf(GMP::class, $model->amount); } - /** - * @requires extension gmp - */ + #[RequiresPhpExtension('gmp')] public function testInvalidArgumentExceptionOnInvalidValue() { /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ @@ -127,9 +122,7 @@ public function testInvalidArgumentExceptionOnInvalidValue() $this->assertSame('address_line_two_value', $model->address->lineTwo); } - /** - * @requires extension gmp - */ + #[RequiresPhpExtension('gmp')] public function testInvalidArgumentExceptionOnNull() { /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ @@ -148,9 +141,7 @@ public function testInvalidArgumentExceptionOnNull() $this->assertSame('address_line_two_value', $model->address->lineTwo); } - /** - * @requires extension gmp - */ + #[RequiresPhpExtension('gmp')] public function testModelsWithCustomCastsCanBeConvertedToArrays() { /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ diff --git a/tests/Integration/Database/EloquentModelHashedCastingTest.php b/tests/Integration/Database/EloquentModelHashedCastingTest.php index df3402f8e46f..754d66b9a399 100644 --- a/tests/Integration/Database/EloquentModelHashedCastingTest.php +++ b/tests/Integration/Database/EloquentModelHashedCastingTest.php @@ -2,24 +2,14 @@ namespace Illuminate\Tests\Integration\Database; -use Illuminate\Contracts\Hashing\Hasher; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Facades\Hash; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Schema; +use RuntimeException; class EloquentModelHashedCastingTest extends DatabaseTestCase { - protected $hasher; - - protected function setUp(): void - { - parent::setUp(); - - $this->hasher = $this->mock(Hasher::class); - Hash::swap($this->hasher); - } - protected function defineDatabaseMigrationsAfterDatabaseRefreshed() { Schema::create('hashed_casts', function (Blueprint $table) { @@ -28,46 +18,150 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() }); } - public function testHashed() + public function testHashedWithBcrypt() + { + Config::set('hashing.driver', 'bcrypt'); + Config::set('hashing.bcrypt.rounds', 13); + + $subject = HashedCast::create([ + 'password' => 'password', + ]); + + $this->assertTrue(password_verify('password', $subject->password)); + $this->assertSame('2y', password_get_info($subject->password)['algo']); + $this->assertSame(13, password_get_info($subject->password)['options']['cost']); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => $subject->password, + ]); + } + + public function testNotHashedIfAlreadyHashedWithBcrypt() + { + Config::set('hashing.driver', 'bcrypt'); + Config::set('hashing.bcrypt.rounds', 13); + + $subject = HashedCast::create([ + // "password"; 13 rounds; bcrypt; + 'password' => '$2y$13$Hdxlvi7OZqK3/fKVNypJs.vJqQcmOo3HnnT6w7fec9FRTRYxAhuCO', + ]); + + $this->assertSame('$2y$13$Hdxlvi7OZqK3/fKVNypJs.vJqQcmOo3HnnT6w7fec9FRTRYxAhuCO', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => '$2y$13$Hdxlvi7OZqK3/fKVNypJs.vJqQcmOo3HnnT6w7fec9FRTRYxAhuCO', + ]); + } + + public function testNotHashedIfNullWithBrcypt() + { + Config::set('hashing.driver', 'bcrypt'); + Config::set('hashing.bcrypt.rounds', 13); + + $subject = HashedCast::create([ + 'password' => null, + ]); + + $this->assertNull($subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => null, + ]); + } + + public function testPassingHashWithHigherCostThrowsExceptionWithBcrypt() + { + Config::set('hashing.driver', 'bcrypt'); + Config::set('hashing.bcrypt.rounds', 10); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); + + $subject = HashedCast::create([ + // "password"; 13 rounds; bcrypt; + 'password' => '$2y$13$Hdxlvi7OZqK3/fKVNypJs.vJqQcmOo3HnnT6w7fec9FRTRYxAhuCO', + ]); + } + + public function testPassingHashWithLowerCostDoesNotThrowExceptionWithBcrypt() + { + Config::set('hashing.driver', 'bcrypt'); + Config::set('hashing.bcrypt.rounds', 13); + + $subject = HashedCast::create([ + // "password"; 7 rounds; bcrypt; + 'password' => '$2y$07$Ivc2VnUOUFtfdbXFc/Ysu.PgiwAHkDmbZQNR1OpIjKCxTxEfWLP5y', + ]); + + $this->assertSame('$2y$07$Ivc2VnUOUFtfdbXFc/Ysu.PgiwAHkDmbZQNR1OpIjKCxTxEfWLP5y', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => '$2y$07$Ivc2VnUOUFtfdbXFc/Ysu.PgiwAHkDmbZQNR1OpIjKCxTxEfWLP5y', + ]); + } + + public function testPassingDifferentHashAlgorithmThrowsExceptionWithBcrypt() { - $this->hasher->expects('isHashed') - ->with('this is a password') - ->andReturnFalse(); + Config::set('hashing.driver', 'bcrypt'); + Config::set('hashing.bcrypt.rounds', 13); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); - $this->hasher->expects('make') - ->with('this is a password') - ->andReturn('hashed-password'); + $subject = HashedCast::create([ + // "password"; argon2id; + 'password' => '$argon2i$v=19$m=1024,t=2,p=2$OENON0I5bXo2WDQyQnM2bg$3ma8cKHITsmAjyIYKDLdSvtkMCiEz/s6qWnLAf+Ehek', + ]); + } + + public function testHashedWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 1234); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); $subject = HashedCast::create([ - 'password' => 'this is a password', + 'password' => 'password', ]); - $this->assertSame('hashed-password', $subject->password); + $this->assertTrue(password_verify('password', $subject->password)); + $this->assertSame('argon2i', password_get_info($subject->password)['algo']); + $this->assertSame(1234, password_get_info($subject->password)['options']['memory_cost']); + $this->assertSame(2, password_get_info($subject->password)['options']['threads']); + $this->assertSame(7, password_get_info($subject->password)['options']['time_cost']); $this->assertDatabaseHas('hashed_casts', [ 'id' => $subject->id, - 'password' => 'hashed-password', + 'password' => $subject->password, ]); } - public function testNotHashedIfAlreadyHashed() + public function testNotHashedIfAlreadyHashedWithArgon() { - $this->hasher->expects('isHashed') - ->with('already-hashed-password') - ->andReturnTrue(); + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 1234); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); $subject = HashedCast::create([ - 'password' => 'already-hashed-password', + // "password"; 1234 memory; 2 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=1234,t=7,p=2$Lm9vSkJuU3M1SllaaTNwZA$5izrDfbWtpkSBH9EczQ8U1yjSOvAkhE4AuYrbBHwi5k', ]); - $this->assertSame('already-hashed-password', $subject->password); + $this->assertSame('$argon2i$v=19$m=1234,t=7,p=2$Lm9vSkJuU3M1SllaaTNwZA$5izrDfbWtpkSBH9EczQ8U1yjSOvAkhE4AuYrbBHwi5k', $subject->password); $this->assertDatabaseHas('hashed_casts', [ 'id' => $subject->id, - 'password' => 'already-hashed-password', + 'password' => '$argon2i$v=19$m=1234,t=7,p=2$Lm9vSkJuU3M1SllaaTNwZA$5izrDfbWtpkSBH9EczQ8U1yjSOvAkhE4AuYrbBHwi5k', ]); } - public function testNotHashedIfNull() + public function testNotHashedIfNullWithArgon() { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 1234); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); + $subject = HashedCast::create([ 'password' => null, ]); @@ -78,6 +172,141 @@ public function testNotHashedIfNull() 'password' => null, ]); } + + public function testPassingHashWithHigherMemoryThrowsExceptionWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 1234); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); + + $subject = HashedCast::create([ + // "password"; 2345 memory; 2 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + } + + public function testPassingHashWithHigherTimeThrowsExceptionWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 1234); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); + + $subject = HashedCast::create([ + // "password"; 1234 memory; 2 threads; 8 time; argon2i; + 'password' => '$argon2i$v=19$m=1234,t=8,p=2$LmszcGVHd0t6b3JweUxqTQ$sdY25X0Qe86fezr1cEjYQxAHI2SdN67yVs5x0ovffag', + ]); + } + + public function testPassingHashWithHigherThreadsThrowsExceptionWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 1234); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); + + $subject = HashedCast::create([ + // "password"; 1234 memory; 3 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=1234,t=7,p=3$OFludXF6bzFpRmdpSHdwSA$J1P4dCGJde6mYe2RZEOFWaztBbDWfxQAM09ZQRMjsw8', + ]); + } + + public function testPassingHashWithLowerMemoryThrowsExceptionWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 3456); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); + + $subject = HashedCast::create([ + // "password"; 2345 memory; 2 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + + $this->assertSame('$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + } + + public function testPassingHashWithLowerTimeThrowsExceptionWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 2345); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 8); + + $subject = HashedCast::create([ + // "password"; 2345 memory; 2 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + + $this->assertSame('$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + } + + public function testPassingHashWithLowerThreadsThrowsExceptionWithArgon() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.argon.memory', 2345); + Config::set('hashing.argon.threads', 3); + Config::set('hashing.argon.time', 7); + + $subject = HashedCast::create([ + // "password"; 2345 memory; 2 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + + $this->assertSame('$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + } + + public function testPassingDifferentHashAlgorithmThrowsExceptionWithArgonAndBcrypt() + { + Config::set('hashing.driver', 'argon'); + Config::set('hashing.bcrypt.rounds', 13); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); + + $subject = HashedCast::create([ + // "password"; bcrypt; + 'password' => '$2y$13$Hdxlvi7OZqK3/fKVNypJs.vJqQcmOo3HnnT6w7fec9FRTRYxAhuCO', + ]); + } + + public function testPassingDifferentHashAlgorithmThrowsExceptionWithArgon2idAndBcrypt() + { + Config::set('hashing.driver', 'argon2id'); + Config::set('hashing.argon.memory', 2345); + Config::set('hashing.argon.threads', 2); + Config::set('hashing.argon.time', 7); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage("Could not verify the hashed value's configuration."); + + $subject = HashedCast::create([ + // "password"; 2345 memory; 2 threads; 7 time; argon2i; + 'password' => '$argon2i$v=19$m=2345,t=7,p=2$MWVVZnpiZHl5RkcveHovcA$QECQzuQ2aAKvUpD25cTUJaAyPFxlOIsCRu+5nbDsU3k', + ]); + } } class HashedCast extends Model diff --git a/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php b/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php index 41a2eed8a0da..262ef43d9c5d 100644 --- a/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php +++ b/tests/Integration/Database/EloquentTransactionWithAfterCommitTests.php @@ -2,7 +2,11 @@ namespace Illuminate\Tests\Integration\Database; +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Auth\User; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; use Illuminate\Support\Facades\DB; use Orchestra\Testbench\Concerns\WithLaravelMigrations; use Orchestra\Testbench\Factories\UserFactory; @@ -41,6 +45,21 @@ public function testObserverCalledWithAfterCommitWhenInsideTransaction() $this->assertEquals(1, $observer::$calledTimes, 'Failed to assert the observer was called once.'); } + public function testObserverCalledWithAfterCommitWhenInsideTransactionWithDispatchSync() + { + User::observe($observer = EloquentTransactionWithAfterCommitTestsUserObserverUsingDispatchSync::resetting()); + + $user1 = DB::transaction(fn () => User::create(UserFactory::new()->raw())); + + $this->assertTrue($user1->exists); + $this->assertEquals(1, $observer::$calledTimes, 'Failed to assert the observer was called once.'); + + $this->assertDatabaseHas('password_reset_tokens', [ + 'email' => $user1->email, + 'token' => sha1($user1->email), + ]); + } + public function testObserverIsCalledOnTestsWithAfterCommitWhenUsingSavepoint() { User::observe($observer = EloquentTransactionWithAfterCommitTestsUserObserver::resetting()); @@ -102,3 +121,32 @@ public function created($user) static::$calledTimes++; } } + +class EloquentTransactionWithAfterCommitTestsUserObserverUsingDispatchSync extends EloquentTransactionWithAfterCommitTestsUserObserver +{ + public function created($user) + { + dispatch_sync(new EloquentTransactionWithAfterCommitTestsJob($user->email)); + + parent::created($user); + } +} + +class EloquentTransactionWithAfterCommitTestsJob implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable; + + public function __construct(public string $email) + { + // ... + } + + public function handle(): void + { + DB::transaction(function () { + DB::table('password_reset_tokens')->insert([ + ['email' => $this->email, 'token' => sha1($this->email), 'created_at' => now()], + ]); + }); + } +} diff --git a/tests/Integration/Database/EloquentWhereHasTest.php b/tests/Integration/Database/EloquentWhereHasTest.php index 22cfaf5d5a75..26a52c0e1651 100644 --- a/tests/Integration/Database/EloquentWhereHasTest.php +++ b/tests/Integration/Database/EloquentWhereHasTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Tests\Integration\Database\DatabaseTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class EloquentWhereHasTest extends DatabaseTestCase { @@ -49,9 +50,8 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() /** * Check that the 'whereRelation' callback function works. - * - * @dataProvider dataProviderWhereRelationCallback */ + #[DataProvider('dataProviderWhereRelationCallback')] public function testWhereRelationCallback($callbackEloquent, $callbackQuery) { $userWhereRelation = User::whereRelation('posts', $callbackEloquent); @@ -69,9 +69,8 @@ public function testWhereRelationCallback($callbackEloquent, $callbackQuery) /** * Check that the 'orWhereRelation' callback function works. - * - * @dataProvider dataProviderWhereRelationCallback */ + #[DataProvider('dataProviderWhereRelationCallback')] public function testOrWhereRelationCallback($callbackEloquent, $callbackQuery) { $userOrWhereRelation = User::orWhereRelation('posts', $callbackEloquent); diff --git a/tests/Integration/Database/Fixtures/TinyInteger.php b/tests/Integration/Database/Fixtures/TinyInteger.php index 8eefbabdd264..6b94dff3e869 100644 --- a/tests/Integration/Database/Fixtures/TinyInteger.php +++ b/tests/Integration/Database/Fixtures/TinyInteger.php @@ -21,7 +21,7 @@ class TinyInteger extends Type * @param \Doctrine\DBAL\Platforms\AbstractPlatform $platform * @return string */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string { return 'TINYINT'; } diff --git a/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php b/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php index 9848da4a642b..62592a40486f 100755 --- a/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php +++ b/tests/Integration/Database/MySql/DatabaseEmulatePreparesMySqlConnectionTest.php @@ -3,11 +3,11 @@ namespace Illuminate\Tests\Integration\Database\MySql; use PDO; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_mysql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_mysql')] class DatabaseEmulatePreparesMySqlConnectionTest extends DatabaseMySqlConnectionTest { protected function getEnvironmentSetUp($app) diff --git a/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php b/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php index a365157cf924..b1cb6a67f2c6 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlConnectionTest.php @@ -5,11 +5,12 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_mysql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_mysql')] class DatabaseMySqlConnectionTest extends MySqlTestCase { const TABLE = 'player'; @@ -32,9 +33,7 @@ protected function destroyDatabaseMigrations() Schema::drop(self::TABLE); } - /** - * @dataProvider floatComparisonsDataProvider - */ + #[DataProvider('floatComparisonsDataProvider')] public function testJsonFloatComparison($value, $operator, $shouldMatch) { DB::table(self::TABLE)->insert([self::JSON_COL => '{"rank":'.self::FLOAT_VAL.'}']); @@ -68,9 +67,7 @@ public function testFloatValueStoredCorrectly() $this->assertEquals(self::FLOAT_VAL, DB::table(self::TABLE)->value(self::FLOAT_COL)); } - /** - * @dataProvider jsonWhereNullDataProvider - */ + #[DataProvider('jsonWhereNullDataProvider')] public function testJsonWhereNull($expected, $key, array $value = ['value' => 123]) { DB::table(self::TABLE)->insert([self::JSON_COL => json_encode($value)]); @@ -78,9 +75,7 @@ public function testJsonWhereNull($expected, $key, array $value = ['value' => 12 $this->assertSame($expected, DB::table(self::TABLE)->whereNull(self::JSON_COL.'->'.$key)->exists()); } - /** - * @dataProvider jsonWhereNullDataProvider - */ + #[DataProvider('jsonWhereNullDataProvider')] public function testJsonWhereNotNull($expected, $key, array $value = ['value' => 123]) { DB::table(self::TABLE)->insert([self::JSON_COL => json_encode($value)]); @@ -122,9 +117,7 @@ public function testJsonPathUpdate() $this->assertSame(1, $updatedCount); } - /** - * @dataProvider jsonContainsKeyDataProvider - */ + #[DataProvider('jsonContainsKeyDataProvider')] public function testWhereJsonContainsKey($count, $column) { DB::table(self::TABLE)->insert([ diff --git a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php index 18b156dc98da..ad1290f384f8 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderAlterTableWithEnumTest.php @@ -4,12 +4,12 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use stdClass; -/** - * @requires extension pdo_mysql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_mysql')] class DatabaseMySqlSchemaBuilderAlterTableWithEnumTest extends MySqlTestCase { protected function defineDatabaseMigrationsAfterDatabaseRefreshed() diff --git a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php index bcbcd6d12bdd..f493b687e522 100644 --- a/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php +++ b/tests/Integration/Database/MySql/DatabaseMySqlSchemaBuilderTest.php @@ -5,11 +5,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_mysql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_mysql')] class DatabaseMySqlSchemaBuilderTest extends MySqlTestCase { public function testAddCommentToTable() diff --git a/tests/Integration/Database/MySql/EscapeTest.php b/tests/Integration/Database/MySql/EscapeTest.php index 9ad6d6e8a41f..3f50939d49a0 100644 --- a/tests/Integration/Database/MySql/EscapeTest.php +++ b/tests/Integration/Database/MySql/EscapeTest.php @@ -2,12 +2,12 @@ namespace Illuminate\Tests\Integration\Database\MySql; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use RuntimeException; -/** - * @requires extension pdo_mysql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_mysql')] class EscapeTest extends MySqlTestCase { public function testEscapeInt() diff --git a/tests/Integration/Database/MySql/FulltextTest.php b/tests/Integration/Database/MySql/FulltextTest.php index a98d0c74b48a..69b7e33b8a1c 100644 --- a/tests/Integration/Database/MySql/FulltextTest.php +++ b/tests/Integration/Database/MySql/FulltextTest.php @@ -5,11 +5,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_mysql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_mysql')] class FulltextTest extends MySqlTestCase { protected function defineDatabaseMigrationsAfterDatabaseRefreshed() diff --git a/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php b/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php index 41c238787712..7b4aab0fb019 100644 --- a/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php +++ b/tests/Integration/Database/Postgres/DatabasePostgresConnectionTest.php @@ -5,11 +5,12 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_pgsql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_pgsql')] class DatabasePostgresConnectionTest extends PostgresTestCase { protected function defineDatabaseMigrationsAfterDatabaseRefreshed() @@ -26,9 +27,7 @@ protected function destroyDatabaseMigrations() Schema::drop('json_table'); } - /** - * @dataProvider jsonWhereNullDataProvider - */ + #[DataProvider('jsonWhereNullDataProvider')] public function testJsonWhereNull($expected, $key, array $value = ['value' => 123]) { DB::table('json_table')->insert(['json_col' => json_encode($value)]); @@ -36,9 +35,7 @@ public function testJsonWhereNull($expected, $key, array $value = ['value' => 12 $this->assertSame($expected, DB::table('json_table')->whereNull("json_col->$key")->exists()); } - /** - * @dataProvider jsonWhereNullDataProvider - */ + #[DataProvider('jsonWhereNullDataProvider')] public function testJsonWhereNotNull($expected, $key, array $value = ['value' => 123]) { DB::table('json_table')->insert(['json_col' => json_encode($value)]); @@ -91,9 +88,7 @@ public function testJsonPathUpdate() $this->assertSame(1, $updatedCount); } - /** - * @dataProvider jsonContainsKeyDataProvider - */ + #[DataProvider('jsonContainsKeyDataProvider')] public function testWhereJsonContainsKey($count, $column) { DB::table('json_table')->insert([ diff --git a/tests/Integration/Database/Postgres/EscapeTest.php b/tests/Integration/Database/Postgres/EscapeTest.php index dc382b4a1143..9feaedb2fd9f 100644 --- a/tests/Integration/Database/Postgres/EscapeTest.php +++ b/tests/Integration/Database/Postgres/EscapeTest.php @@ -2,12 +2,12 @@ namespace Illuminate\Tests\Integration\Database\Postgres; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use RuntimeException; -/** - * @requires extension pdo_pgsql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_pgsql')] class EscapeTest extends PostgresTestCase { public function testEscapeInt() diff --git a/tests/Integration/Database/Postgres/FulltextTest.php b/tests/Integration/Database/Postgres/FulltextTest.php index 39ddb6837022..81befb9fcbbe 100644 --- a/tests/Integration/Database/Postgres/FulltextTest.php +++ b/tests/Integration/Database/Postgres/FulltextTest.php @@ -5,11 +5,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_pgsql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_pgsql')] class FulltextTest extends PostgresTestCase { protected function defineDatabaseMigrationsAfterDatabaseRefreshed() diff --git a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php index 8efa387060ca..3aa249c2aece 100644 --- a/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php +++ b/tests/Integration/Database/Postgres/PostgresSchemaBuilderTest.php @@ -5,11 +5,11 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_pgsql - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_pgsql')] class PostgresSchemaBuilderTest extends PostgresTestCase { protected function getEnvironmentSetUp($app) diff --git a/tests/Integration/Database/QueryBuilderTest.php b/tests/Integration/Database/QueryBuilderTest.php index 6ec6930b13db..a52b03d45c22 100644 --- a/tests/Integration/Database/QueryBuilderTest.php +++ b/tests/Integration/Database/QueryBuilderTest.php @@ -407,11 +407,13 @@ public function testPluck() 'Bar Post', ], DB::table('posts')->select(['content', 'id', 'title'])->pluck('title')->toArray()); + // Test without SELECT override. $this->assertSame([ 'Foo Post', 'Bar Post', ], DB::table('posts')->pluck('title')->toArray()); + // Test specific key. $this->assertSame([ 1 => 'Foo Post', 2 => 'Bar Post', diff --git a/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php b/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php index 28b27f89f80f..975eb8c67954 100644 --- a/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php +++ b/tests/Integration/Database/SqlServer/DatabaseSqlServerConnectionTest.php @@ -5,11 +5,12 @@ use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; -/** - * @requires extension pdo_sqlsrv - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] +#[RequiresPhpExtension('pdo_sqlsrv')] class DatabaseSqlServerConnectionTest extends SqlServerTestCase { protected function defineDatabaseMigrationsAfterDatabaseRefreshed() @@ -26,9 +27,7 @@ protected function destroyDatabaseMigrations() Schema::drop('json_table'); } - /** - * @dataProvider jsonContainsKeyDataProvider - */ + #[DataProvider('jsonContainsKeyDataProvider')] public function testWhereJsonContainsKey($count, $column) { DB::table('json_table')->insert([ diff --git a/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php b/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php index 212ee52be7d8..f62ee3d22f09 100644 --- a/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php +++ b/tests/Integration/Database/Sqlite/DatabaseSqliteConnectionTest.php @@ -6,6 +6,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; use Illuminate\Tests\Integration\Database\DatabaseTestCase; +use PHPUnit\Framework\Attributes\DataProvider; class DatabaseSqliteConnectionTest extends DatabaseTestCase { @@ -38,9 +39,7 @@ protected function destroyDatabaseMigrations() Schema::drop('json_table'); } - /** - * @dataProvider jsonContainsKeyDataProvider - */ + #[DataProvider('jsonContainsKeyDataProvider')] public function testWhereJsonContainsKey($count, $column) { DB::table('json_table')->insert([ diff --git a/tests/Integration/Filesystem/FilesystemTest.php b/tests/Integration/Filesystem/FilesystemTest.php index 8bd38fe309fe..2bde297d09f9 100644 --- a/tests/Integration/Filesystem/FilesystemTest.php +++ b/tests/Integration/Filesystem/FilesystemTest.php @@ -4,11 +4,10 @@ use Illuminate\Support\Facades\File; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use Symfony\Component\Process\Process; -/** - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] class FilesystemTest extends TestCase { protected $stubFile; diff --git a/tests/Integration/Filesystem/StorageTest.php b/tests/Integration/Filesystem/StorageTest.php index 4193a629e178..95b4990bda04 100644 --- a/tests/Integration/Filesystem/StorageTest.php +++ b/tests/Integration/Filesystem/StorageTest.php @@ -5,11 +5,10 @@ use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Storage; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\RequiresOperatingSystem; use Symfony\Component\Process\Process; -/** - * @requires OS Linux|Darwin - */ +#[RequiresOperatingSystem('Linux|Darwin')] class StorageTest extends TestCase { protected $stubFile; diff --git a/tests/Integration/Foundation/ExceptionHandlerTest.php b/tests/Integration/Foundation/ExceptionHandlerTest.php index 622b4348d2e8..d13a74f4122b 100644 --- a/tests/Integration/Foundation/ExceptionHandlerTest.php +++ b/tests/Integration/Foundation/ExceptionHandlerTest.php @@ -4,9 +4,13 @@ use Illuminate\Auth\Access\AuthorizationException; use Illuminate\Auth\Access\Response; +use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Route; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Process\PhpProcess; +use Throwable; class ExceptionHandlerTest extends TestCase { @@ -120,9 +124,7 @@ public function testItHasFallbackErrorMessageForUnknownStatusCodes() ]); } - /** - * @dataProvider exitCodesProvider - */ + #[DataProvider('exitCodesProvider')] public function testItReturnsNonZeroExitCodesForUncaughtExceptions($providers, $successful) { $basePath = static::applicationBasePath(); @@ -151,4 +153,46 @@ public static function exitCodesProvider() yield 'Throw exception' => [[Fixtures\Providers\ThrowUncaughtExceptionServiceProvider::class], false]; yield 'Do not throw exception' => [[Fixtures\Providers\ThrowExceptionServiceProvider::class], true]; } + + public function test_it_handles_malformed_error_views_in_production() + { + Config::set('view.paths', [__DIR__.'/Fixtures/MalformedErrorViews']); + Config::set('app.debug', false); + $reported = []; + $this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) { + $reported[] = $e; + }); + + try { + $response = $this->get('foo'); + } catch (Throwable) { + $response ??= null; + } + + $this->assertCount(1, $reported); + $this->assertSame('Undefined variable $foo (View: '.__DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MalformedErrorViews'.DIRECTORY_SEPARATOR.'errors'.DIRECTORY_SEPARATOR.'404.blade.php)', $reported[0]->getMessage()); + $this->assertNotNull($response); + $response->assertStatus(404); + } + + public function test_it_handles_malformed_error_views_in_development() + { + Config::set('view.paths', [__DIR__.'/Fixtures/MalformedErrorViews']); + Config::set('app.debug', true); + $reported = []; + $this->app[ExceptionHandler::class]->reportable(function (Throwable $e) use (&$reported) { + $reported[] = $e; + }); + + try { + $response = $this->get('foo'); + } catch (Throwable) { + $response ??= null; + } + + $this->assertCount(1, $reported); + $this->assertSame('Undefined variable $foo (View: '.__DIR__.DIRECTORY_SEPARATOR.'Fixtures'.DIRECTORY_SEPARATOR.'MalformedErrorViews'.DIRECTORY_SEPARATOR.'errors'.DIRECTORY_SEPARATOR.'404.blade.php)', $reported[0]->getMessage()); + $this->assertNotNull($response); + $response->assertStatus(500); + } } diff --git a/tests/Integration/Foundation/Fixtures/MalformedErrorViews/errors/404.blade.php b/tests/Integration/Foundation/Fixtures/MalformedErrorViews/errors/404.blade.php new file mode 100644 index 000000000000..26b405f9717d --- /dev/null +++ b/tests/Integration/Foundation/Fixtures/MalformedErrorViews/errors/404.blade.php @@ -0,0 +1,2 @@ +My custom 404 +assertTrue($this->app['tail.registered']); $this->assertTrue($this->app['tail.booted']); diff --git a/tests/Integration/Foundation/RoutingServiceProviderTest.php b/tests/Integration/Foundation/RoutingServiceProviderTest.php new file mode 100644 index 000000000000..4753f03eda02 --- /dev/null +++ b/tests/Integration/Foundation/RoutingServiceProviderTest.php @@ -0,0 +1,145 @@ +getParsedBody(); + })->middleware(MergeDataMiddleware::class); + + $response = $this->withoutExceptionHandling()->get('test-route?'.http_build_query([ + 'sent' => 'sent-data', + 'overridden' => 'overriden-sent-data', + ])); + + $response->assertOk(); + $response->assertExactJson([ + 'request-data' => 'request-data', + ]); + } + + public function testItWorksNormallyWithoutMergeDataMiddlewareWithEmptyRequests() + { + Route::get('test-route', function (ServerRequestInterface $request) { + return $request->getParsedBody(); + }); + + $response = $this->withoutExceptionHandling()->get('test-route', [ + 'content-type' => 'application/json', + ]); + + $response->assertOk(); + $response->assertExactJson([]); + } + + public function testItIncludesMergedDataInServerRequestInterfaceInstancesUsingGetJsonRequestsWithContentTypeHeader() + { + Route::get('test-route', function (ServerRequestInterface $request) { + return $request->getParsedBody(); + })->middleware(MergeDataMiddleware::class); + + $response = $this->getJson('test-route?'.http_build_query([ + 'sent' => 'sent-data', + 'overridden' => 'overriden-sent-data', + ]), [ + 'content-type' => 'application/json', + ]); + + $response->assertOk(); + $response->assertExactJson([ + 'json-data' => 'json-data', + 'merged' => 'replaced-merged-data', + 'overridden' => 'overriden-merged-data', + 'request-data' => 'request-data', + ]); + } + + public function testItIncludesMergedDataInServerRequestInterfaceInstancesUsingGetJsonRequests() + { + Route::get('test-route', function (ServerRequestInterface $request) { + return $request->getParsedBody(); + })->middleware(MergeDataMiddleware::class); + + $response = $this->getJson('test-route?'.http_build_query([ + 'sent' => 'sent-data', + 'overridden' => 'overriden-sent-data', + ])); + + $response->assertOk(); + $response->assertExactJson([ + 'json-data' => 'json-data', + 'merged' => 'replaced-merged-data', + 'overridden' => 'overriden-merged-data', + 'request-data' => 'request-data', + ]); + } + + public function testItIncludesMergedDataInServerRequestInterfaceInstancesUsingPostRequests() + { + Route::post('test-route', function (ServerRequestInterface $request) { + return $request->getParsedBody(); + })->middleware(MergeDataMiddleware::class); + + $response = $this->post('test-route', [ + 'sent' => 'sent-data', + 'overridden' => 'overriden-sent-data', + ]); + + $response->assertOk(); + $response->assertExactJson([ + 'sent' => 'sent-data', + 'merged' => 'replaced-merged-data', + 'overridden' => 'overriden-merged-data', + 'request-data' => 'request-data', + ]); + } + + public function testItIncludesMergedDataInServerRequestInterfaceInstancesUsingPostJsonRequests() + { + Route::post('test-route', function (ServerRequestInterface $request) { + return $request->getParsedBody(); + })->middleware(MergeDataMiddleware::class); + + $response = $this->postJson('test-route', [ + 'sent' => 'sent-data', + 'overridden' => 'overriden-sent-data', + ]); + + $response->assertOk(); + $response->assertExactJson([ + 'json-data' => 'json-data', + 'sent' => 'sent-data', + 'merged' => 'replaced-merged-data', + 'overridden' => 'overriden-merged-data', + 'request-data' => 'request-data', + ]); + } +} + +class MergeDataMiddleware +{ + public function handle(Request $request, $next) + { + $request->merge(['merged' => 'first-merged-data']); + + $request->merge(['merged' => 'replaced-merged-data']); + + $request->merge(['overridden' => 'overriden-merged-data']); + + $request->request->set('request-data', 'request-data'); + + $request->query->set('query-data', 'query-data'); + + $request->json()->set('json-data', 'json-data'); + + return $next($request); + } +} diff --git a/tests/Integration/Generators/FactoryMakeCommandTest.php b/tests/Integration/Generators/FactoryMakeCommandTest.php index 02ec76943c99..fccbe1fb0282 100644 --- a/tests/Integration/Generators/FactoryMakeCommandTest.php +++ b/tests/Integration/Generators/FactoryMakeCommandTest.php @@ -8,7 +8,6 @@ class FactoryMakeCommandTest extends TestCase 'database/factories/FooFactory.php', ]; - /** @test */ public function testItCanGenerateFactoryFile() { $this->artisan('make:factory', ['name' => 'FooFactory']) diff --git a/tests/Integration/Generators/ResourceMakeCommandTest.php b/tests/Integration/Generators/ResourceMakeCommandTest.php index b50b40872a85..eb1457a9b650 100644 --- a/tests/Integration/Generators/ResourceMakeCommandTest.php +++ b/tests/Integration/Generators/ResourceMakeCommandTest.php @@ -9,8 +9,7 @@ class ResourceMakeCommandTest extends TestCase 'app/Http/Resources/FooResourceCollection.php', ]; - /** @test */ - public function it_can_generate_resource_file() + public function testItCanGenerateResourceFile() { $this->artisan('make:resource', ['name' => 'FooResource']) ->assertExitCode(0); @@ -22,8 +21,7 @@ public function it_can_generate_resource_file() ], 'app/Http/Resources/FooResource.php'); } - /** @test */ - public function it_can_generate_resource_collection_file() + public function testItCanGenerateResourceCollectionFile() { $this->artisan('make:resource', ['name' => 'FooResourceCollection', '--collection' => true]) ->assertExitCode(0); diff --git a/tests/Integration/Http/ThrottleRequestsTest.php b/tests/Integration/Http/ThrottleRequestsTest.php index 6ddd2a0ef51e..d0f43260eaa2 100644 --- a/tests/Integration/Http/ThrottleRequestsTest.php +++ b/tests/Integration/Http/ThrottleRequestsTest.php @@ -4,6 +4,7 @@ use Illuminate\Cache\RateLimiter; use Illuminate\Cache\RateLimiting\GlobalLimit; +use Illuminate\Cache\RateLimiting\Limit; use Illuminate\Container\Container; use Illuminate\Http\Exceptions\ThrottleRequestsException; use Illuminate\Routing\Middleware\ThrottleRequests; @@ -116,4 +117,129 @@ public function testItCanGenerateDefinitionViaStaticMethod() $signature = (string) ThrottleRequests::with(prefix: 'foo'); $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:60,1,foo', $signature); } + + public static function perMinuteThrottlingDataSet() + { + return [ + [ThrottleRequests::using('test')], + [ThrottleRequests::with(maxAttempts: 3, decayMinutes: 1)], + ['throttle:3,1'], + ]; + } + + /** @dataProvider perMinuteThrottlingDataSet */ + public function testItCanThrottlePerMinute(string $middleware) + { + $rateLimiter = Container::getInstance()->make(RateLimiter::class); + $rateLimiter->for('test', fn () => Limit::perMinute(3)); + Route::get('/', fn () => 'ok')->middleware($middleware); + + Carbon::setTestNow('2000-01-01 00:00:00.000'); + + // Make 3 requests, each a second apart, that should all be successful. + + for ($i = 0; $i < 3; $i++) { + match ($i) { + 0 => $this->assertSame('2000-01-01 00:00:00.000', now()->toDateTimeString('m')), + 1 => $this->assertSame('2000-01-01 00:00:01.000', now()->toDateTimeString('m')), + 2 => $this->assertSame('2000-01-01 00:00:02.000', now()->toDateTimeString('m')), + }; + + $response = $this->get('/'); + $response->assertOk(); + $response->assertContent('ok'); + $response->assertHeader('X-RateLimit-Limit', 3); + $response->assertHeader('X-RateLimit-Remaining', 3 - ($i + 1)); + + Carbon::setTestNow(now()->addSecond()); + } + + // It is now 3 seconds past and we will make another request that + // should be rate limited. + + $this->assertSame('2000-01-01 00:00:03.000', now()->toDateTimeString('m')); + + $response = $this->get('/'); + $response->assertStatus(429); + $response->assertHeader('Retry-After', 57); + $response->assertHeader('X-RateLimit-Reset', now()->addSeconds(57)->timestamp); + $response->assertHeader('X-RateLimit-Limit', 3); + $response->assertHeader('X-RateLimit-Remaining', 0); + + // We will now make it the very end of the minute, to check boundaries, + // and make another request that should be rate limited and tell us to + // try again in 1 second. + Carbon::setTestNow('2000-01-01 00:00:59.999'); + + $response = $this->get('/'); + $response->assertStatus(429); + $response->assertHeader('Retry-After', 1); + $response->assertHeader('X-RateLimit-Reset', now()->addSeconds(1)->timestamp); + $response->assertHeader('X-RateLimit-Limit', 3); + $response->assertHeader('X-RateLimit-Remaining', 0); + + // We now tick over into the next second. We should now be able to make + // requests again. + Carbon::setTestNow('2000-01-01 00:01:00.000'); + + $response = $this->get('/'); + $response->assertOk(); + } + + public function testItCanThrottlePerSecond() + { + $rateLimiter = Container::getInstance()->make(RateLimiter::class); + $rateLimiter->for('test', fn () => Limit::perSecond(3)); + Route::get('/', fn () => 'ok')->middleware(ThrottleRequests::using('test')); + + Carbon::setTestNow('2000-01-01 00:00:00.000'); + + // Make 3 requests, each a 100ms apart, that should all be successful. + + for ($i = 0; $i < 3; $i++) { + match ($i) { + 0 => $this->assertSame('2000-01-01 00:00:00.000', now()->toDateTimeString('m')), + 1 => $this->assertSame('2000-01-01 00:00:00.100', now()->toDateTimeString('m')), + 2 => $this->assertSame('2000-01-01 00:00:00.200', now()->toDateTimeString('m')), + }; + + $response = $this->get('/'); + $response->assertOk(); + $response->assertContent('ok'); + $response->assertHeader('X-RateLimit-Limit', 3); + $response->assertHeader('X-RateLimit-Remaining', 3 - ($i + 1)); + + Carbon::setTestNow(now()->addMilliseconds(100)); + } + + // It is now 300 milliseconds past and we will make another request + // that should be rate limited. + + $this->assertSame('2000-01-01 00:00:00.300', now()->toDateTimeString('m')); + + $response = $this->get('/'); + $response->assertStatus(429); + $response->assertHeader('Retry-After', 1); + $response->assertHeader('X-RateLimit-Reset', now()->addSecond()->timestamp); + $response->assertHeader('X-RateLimit-Limit', 3); + $response->assertHeader('X-RateLimit-Remaining', 0); + + // We will now make it the very end of the minute, to check boundaries, + // and make another request that should be rate limited and tell us to + // try again in 1 second. + Carbon::setTestNow('2000-01-01 00:00:00.999'); + + $response = $this->get('/'); + $response->assertHeader('Retry-After', 1); + $response->assertHeader('X-RateLimit-Reset', now()->addSecond()->timestamp); + $response->assertHeader('X-RateLimit-Limit', 3); + $response->assertHeader('X-RateLimit-Remaining', 0); + + // We now tick over into the next second. We should now be able to make + // requests again. + Carbon::setTestNow('2000-01-01 00:00:01.000'); + + $response = $this->get('/'); + $response->assertOk(); + } } diff --git a/tests/Integration/Migration/MigratorTest.php b/tests/Integration/Migration/MigratorTest.php index f4210789ada5..a57e469c4bcc 100644 --- a/tests/Integration/Migration/MigratorTest.php +++ b/tests/Integration/Migration/MigratorTest.php @@ -2,7 +2,9 @@ namespace Illuminate\Tests\Integration\Migration; +use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Schema; use Mockery as m; use Orchestra\Testbench\TestCase; use Symfony\Component\Console\Output\OutputInterface; @@ -98,6 +100,137 @@ public function testPretendMigrate() $this->assertFalse(DB::getSchemaBuilder()->hasTable('people')); } + public function testIgnorePretendModeForCallbackData() + { + // Create two tables with different columns so that we can query it later + // with the new method DB::withoutPretending(). + + Schema::create('table_1', function (Blueprint $table) { + $table->increments('id'); + $table->string('column_1'); + }); + + Schema::create('table_2', function (Blueprint $table) { + $table->increments('id'); + $table->string('column_2')->default('default_value'); + }); + + // From here on we simulate to be in pretend mode. This normally is done by + // running the migration with the option --pretend. + + DB::pretend(function () { + // Returns an empty array because we are in pretend mode. + $tablesEmpty = DB::select("SELECT name FROM sqlite_master WHERE type='table'"); + + $this->assertTrue([] === $tablesEmpty); + + // Returns an array with two tables because we ignore pretend mode. + $tablesList = DB::withoutPretending(function (): array { + return DB::select("SELECT name FROM sqlite_master WHERE type='table'"); + }); + + $this->assertTrue([] !== $tablesList); + + // The following would not be possible in pretend mode, if the + // method DB::withoutPretending() would not exists, + // because nothing is executed in pretend mode. + foreach ($tablesList as $table) { + if (in_array($table->name, ['sqlite_sequence', 'migrations'])) { + continue; + } + + $columnsEmpty = DB::select("PRAGMA table_info($table->name)"); + + $this->assertTrue([] === $columnsEmpty); + + $columnsList = DB::withoutPretending(function () use ($table): array { + return DB::select("PRAGMA table_info($table->name)"); + }); + + $this->assertTrue([] !== $columnsList); + $this->assertCount(2, $columnsList); + + // Confirm that we are still in pretend mode. This column should + // not be added. We query the table columns again to ensure the + // count is still two. + DB::statement("ALTER TABLE $table->name ADD COLUMN column_3 varchar(255) DEFAULT 'default_value' NOT NULL"); + + $columnsList = DB::withoutPretending(function () use ($table): array { + return DB::select("PRAGMA table_info($table->name)"); + }); + + $this->assertCount(2, $columnsList); + } + }); + + Schema::dropIfExists('table_1'); + Schema::dropIfExists('table_2'); + } + + public function testIgnorePretendModeForCallbackOutputDynamicContentIsShown() + { + // Persist data to table we can work with. + $this->expectInfo('Running migrations.'); + $this->expectTask('2014_10_12_000000_create_people_is_dynamic_table', 'DONE'); + + $this->output->shouldReceive('writeln')->once(); + + $this->subject->run([__DIR__.'/pretending/2014_10_12_000000_create_people_is_dynamic_table.php'], ['pretend' => false]); + + $this->assertTrue(DB::getSchemaBuilder()->hasTable('people')); + + // Test the actual functionality. + $this->expectInfo('Running migrations.'); + $this->expectTwoColumnDetail('DynamicContentIsShown'); + $this->expectBulletList([ + 'create table "blogs" ("id" integer primary key autoincrement not null, "url" varchar, "name" varchar)', + 'insert into "blogs" ("url") values (\'www.janedoe.com\'), (\'www.johndoe.com\')', + 'ALTER TABLE \'pseudo_table_name\' MODIFY \'column_name\' VARCHAR(191)', + 'select * from "people"', + 'insert into "blogs" ("id", "name") values (1, \'Jane Doe Blog\')', + 'insert into "blogs" ("id", "name") values (2, \'John Doe Blog\')', + ]); + + $this->output->shouldReceive('writeln')->once(); + + $this->subject->run([__DIR__.'/pretending/2023_10_17_000000_dynamic_content_is_shown.php'], ['pretend' => true]); + + $this->assertFalse(DB::getSchemaBuilder()->hasTable('blogs')); + + Schema::dropIfExists('people'); + } + + public function testIgnorePretendModeForCallbackOutputDynamicContentNotShown() + { + // Persist data to table we can work with. + $this->expectInfo('Running migrations.'); + $this->expectTask('2014_10_12_000000_create_people_non_dynamic_table', 'DONE'); + + $this->output->shouldReceive('writeln')->once(); + + $this->subject->run([__DIR__.'/pretending/2014_10_12_000000_create_people_non_dynamic_table.php'], ['pretend' => false]); + + $this->assertTrue(DB::getSchemaBuilder()->hasTable('people')); + + // Test the actual functionality. + $this->expectInfo('Running migrations.'); + $this->expectTwoColumnDetail('DynamicContentNotShown'); + $this->expectBulletList([ + 'create table "blogs" ("id" integer primary key autoincrement not null, "url" varchar, "name" varchar)', + 'insert into "blogs" ("url") values (\'www.janedoe.com\'), (\'www.johndoe.com\')', + 'ALTER TABLE \'pseudo_table_name\' MODIFY \'column_name\' VARCHAR(191)', + 'select * from "people"', + ]); + + $this->output->shouldReceive('writeln')->once(); + + $this->subject->run([__DIR__.'/pretending/2023_10_17_000000_dynamic_content_not_shown.php'], ['pretend' => true]); + + $this->assertFalse(DB::getSchemaBuilder()->hasTable('blogs')); + + Schema::dropIfExists('people'); + } + protected function expectInfo($message): void { $this->output->shouldReceive('writeln')->once()->with(m::on( diff --git a/tests/Integration/Migration/pretending/2014_10_12_000000_create_people_is_dynamic_table.php b/tests/Integration/Migration/pretending/2014_10_12_000000_create_people_is_dynamic_table.php new file mode 100644 index 000000000000..c8fdadaa4c9e --- /dev/null +++ b/tests/Integration/Migration/pretending/2014_10_12_000000_create_people_is_dynamic_table.php @@ -0,0 +1,27 @@ +increments('id'); + $table->string('blog_id')->nullable(); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + DB::table('people')->insert([ + ['email' => 'jane@example.com', 'name' => 'Jane Doe', 'password' => 'secret'], + ['email' => 'john@example.com', 'name' => 'John Doe', 'password' => 'secret'], + ]); + } +} diff --git a/tests/Integration/Migration/pretending/2014_10_12_000000_create_people_non_dynamic_table.php b/tests/Integration/Migration/pretending/2014_10_12_000000_create_people_non_dynamic_table.php new file mode 100644 index 000000000000..91341141484b --- /dev/null +++ b/tests/Integration/Migration/pretending/2014_10_12_000000_create_people_non_dynamic_table.php @@ -0,0 +1,26 @@ +increments('id'); + $table->string('name'); + $table->string('email')->unique(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + DB::table('people')->insert([ + ['email' => 'jane@example.com', 'name' => 'Jane Doe', 'password' => 'secret'], + ['email' => 'john@example.com', 'name' => 'John Doe', 'password' => 'secret'], + ]); + } +} diff --git a/tests/Integration/Migration/pretending/2023_10_17_000000_dynamic_content_is_shown.php b/tests/Integration/Migration/pretending/2023_10_17_000000_dynamic_content_is_shown.php new file mode 100644 index 000000000000..f5304710f422 --- /dev/null +++ b/tests/Integration/Migration/pretending/2023_10_17_000000_dynamic_content_is_shown.php @@ -0,0 +1,37 @@ +increments('id'); + $table->string('url')->nullable(); + $table->string('name')->nullable(); + }); + + DB::table('blogs')->insert([ + ['url' => 'www.janedoe.com'], + ['url' => 'www.johndoe.com'], + ]); + + DB::statement("ALTER TABLE 'pseudo_table_name' MODIFY 'column_name' VARCHAR(191)"); + + /** @var \Illuminate\Support\Collection $tablesList */ + $tablesList = DB::withoutPretending(function () { + return DB::table('people')->get(); + }); + + $tablesList->each(function ($person, $key) { + DB::table('blogs')->where('blog_id', '=', $person->blog_id)->insert([ + 'id' => $key + 1, + 'name' => "{$person->name} Blog", + ]); + }); + } +} diff --git a/tests/Integration/Migration/pretending/2023_10_17_000000_dynamic_content_not_shown.php b/tests/Integration/Migration/pretending/2023_10_17_000000_dynamic_content_not_shown.php new file mode 100644 index 000000000000..78fcb97aead4 --- /dev/null +++ b/tests/Integration/Migration/pretending/2023_10_17_000000_dynamic_content_not_shown.php @@ -0,0 +1,32 @@ +increments('id'); + $table->string('url')->nullable(); + $table->string('name')->nullable(); + }); + + DB::table('blogs')->insert([ + ['url' => 'www.janedoe.com'], + ['url' => 'www.johndoe.com'], + ]); + + DB::statement("ALTER TABLE 'pseudo_table_name' MODIFY 'column_name' VARCHAR(191)"); + + DB::table('people')->get()->each(function ($person, $key) { + DB::table('blogs')->where('blog_id', '=', $person->blog_id)->insert([ + 'id' => $key + 1, + 'name' => "{$person->name} Blog", + ]); + }); + } +} diff --git a/tests/Integration/Queue/CustomPayloadTest.php b/tests/Integration/Queue/CustomPayloadTest.php index 5ae3a8f43017..e3f7cf28b9a8 100644 --- a/tests/Integration/Queue/CustomPayloadTest.php +++ b/tests/Integration/Queue/CustomPayloadTest.php @@ -8,6 +8,7 @@ use Illuminate\Queue\Queue; use Illuminate\Support\ServiceProvider; use Orchestra\Testbench\Concerns\CreatesApplication; +use PHPUnit\Framework\Attributes\DataProvider; class CustomPayloadTest extends TestCase { @@ -25,9 +26,7 @@ public static function websites() yield ['blog.laravel.com']; } - /** - * @dataProvider websites - */ + #[DataProvider('websites')] public function test_custom_payload_gets_cleared_for_each_data_provider(string $websites) { $dispatcher = $this->app->make(QueueingDispatcher::class); diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index 80fc594fcd91..c199a0096292 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -4,13 +4,17 @@ use Illuminate\Bus\Dispatcher; use Illuminate\Bus\Queueable; +use Illuminate\Cache\ArrayStore; use Illuminate\Cache\RateLimiter; use Illuminate\Cache\RateLimiting\Limit; +use Illuminate\Cache\Repository; +use Illuminate\Container\Container; use Illuminate\Contracts\Cache\Repository as Cache; use Illuminate\Contracts\Queue\Job; use Illuminate\Queue\CallQueuedHandler; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\RateLimited; +use Illuminate\Support\Carbon; use Mockery as m; use Orchestra\Testbench\TestCase; @@ -188,6 +192,94 @@ protected function assertJobWasSkipped($class) $this->assertFalse($class::$handled); } + + public function testItCanLimitPerMinute() + { + Container::getInstance()->instance(RateLimiter::class, $limiter = new RateLimiter(new Repository(new ArrayStore))); + $limiter->for('test', fn () => Limit::perMinute(3)); + $jobFactory = fn () => new class + { + public $released = false; + + public function release() + { + $this->released = true; + } + }; + $next = fn ($job) => $job; + + $middleware = new RateLimited('test'); + + Carbon::setTestNow('2000-00-00 00:00:00.000'); + + for ($i = 0; $i < 3; $i++) { + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertFalse($job->released); + + Carbon::setTestNow(now()->addSeconds(1)); + } + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertNull($result); + $this->assertTrue($job->released); + + Carbon::setTestNow('2000-00-00 00:00:59.999'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertNull($result); + $this->assertTrue($job->released); + + Carbon::setTestNow('2000-00-00 00:01:00.000'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertFalse($job->released); + } + + public function testItCanLimitPerSecond() + { + Container::getInstance()->instance(RateLimiter::class, $limiter = new RateLimiter(new Repository(new ArrayStore))); + $limiter->for('test', fn () => Limit::perSecond(3)); + $jobFactory = fn () => new class + { + public $released = false; + + public function release() + { + $this->released = true; + } + }; + $next = fn ($job) => $job; + + $middleware = new RateLimited('test'); + + Carbon::setTestNow('2000-00-00 00:00:00.000'); + + for ($i = 0; $i < 3; $i++) { + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertFalse($job->released); + + Carbon::setTestNow(now()->addMilliseconds(100)); + } + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertNull($result); + $this->assertTrue($job->released); + + Carbon::setTestNow('2000-00-00 00:00:00.999'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertNull($result); + $this->assertTrue($job->released); + + Carbon::setTestNow('2000-00-00 00:00:01.000'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertFalse($job->released); + } } class RateLimitedTestJob diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index 6eff31a6aabd..1bb75e50619a 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -9,8 +9,10 @@ use Illuminate\Queue\CallQueuedHandler; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\ThrottlesExceptions; +use Illuminate\Support\Carbon; use Mockery as m; use Orchestra\Testbench\TestCase; +use RuntimeException; class ThrottlesExceptionsTest extends TestCase { @@ -105,6 +107,168 @@ protected function assertJobRanSuccessfully($class) $this->assertTrue($class::$handled); } + + public function testItCanLimitPerMinute() + { + $jobFactory = fn () => new class + { + public $released = false; + + public $handled = false; + + public function release() + { + $this->released = true; + + return $this; + } + }; + $next = function ($job) { + $job->handled = true; + + throw new RuntimeException('Whoops!'); + }; + + $middleware = new ThrottlesExceptions(3, 60); + + Carbon::setTestNow('2000-00-00 00:00:00.000'); + + for ($i = 0; $i < 3; $i++) { + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertTrue($job->handled); + + Carbon::setTestNow(now()->addSeconds(1)); + } + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertFalse($job->handled); + + Carbon::setTestNow('2000-00-00 00:00:59.999'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertFalse($job->handled); + + Carbon::setTestNow('2000-00-00 00:01:00.000'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertTrue($job->handled); + } + + public function testItCanLimitPerSecond() + { + $jobFactory = fn () => new class + { + public $released = false; + + public $handled = false; + + public function release() + { + $this->released = true; + + return $this; + } + }; + $next = function ($job) { + $job->handled = true; + + throw new RuntimeException('Whoops!'); + }; + + $middleware = new ThrottlesExceptions(3, 1); + + Carbon::setTestNow('2000-00-00 00:00:00.000'); + + for ($i = 0; $i < 3; $i++) { + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertTrue($job->handled); + + Carbon::setTestNow(now()->addMilliseconds(100)); + } + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertFalse($job->handled); + + Carbon::setTestNow('2000-00-00 00:00:00.999'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertFalse($job->handled); + + Carbon::setTestNow('2000-00-00 00:00:01.000'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertTrue($job->handled); + } + + public function testLimitingWithDefaultValues() + { + $jobFactory = fn () => new class + { + public $released = false; + + public $handled = false; + + public function release() + { + $this->released = true; + + return $this; + } + }; + $next = function ($job) { + $job->handled = true; + + throw new RuntimeException('Whoops!'); + }; + + $middleware = new ThrottlesExceptions(); + + Carbon::setTestNow('2000-00-00 00:00:00.000'); + + for ($i = 0; $i < 10; $i++) { + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertTrue($job->handled); + + Carbon::setTestNow(now()->addSeconds(1)); + } + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertFalse($job->handled); + + Carbon::setTestNow('2000-00-00 00:09:59.999'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertFalse($job->handled); + + Carbon::setTestNow('2000-00-00 00:10:00.000'); + + $result = $middleware->handle($job = $jobFactory(), $next); + $this->assertSame($job, $result); + $this->assertTrue($job->released); + $this->assertTrue($job->handled); + } } class CircuitBreakerTestJob @@ -122,7 +286,7 @@ public function handle() public function middleware() { - return [(new ThrottlesExceptions(2, 10))->by('test')]; + return [(new ThrottlesExceptions(2, 10 * 60))->by('test')]; } } @@ -139,6 +303,6 @@ public function handle() public function middleware() { - return [(new ThrottlesExceptions(2, 10))->by('test')]; + return [(new ThrottlesExceptions(2, 10 * 60))->by('test')]; } } diff --git a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php index 87a7af68a22c..6cb102c48ffe 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsWithRedisTest.php @@ -10,6 +10,7 @@ use Illuminate\Queue\CallQueuedHandler; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis; +use Illuminate\Support\Carbon; use Illuminate\Support\Str; use Mockery as m; use Orchestra\Testbench\TestCase; @@ -23,6 +24,8 @@ protected function setUp(): void parent::setUp(); $this->setUpRedis(); + + Carbon::setTestNow(now()); } protected function tearDown(): void @@ -31,6 +34,8 @@ protected function tearDown(): void $this->tearDownRedis(); + Carbon::setTestNow(); + m::close(); } @@ -38,10 +43,7 @@ public function testCircuitIsOpenedForJobErrors() { $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key = Str::random()); $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); - - retry(2, function () use ($key) { - $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); - }); + $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); } public function testCircuitStaysClosedForSuccessfulJobs() @@ -57,10 +59,7 @@ public function testCircuitResetsAfterSuccess() $this->assertJobRanSuccessfully(CircuitBreakerWithRedisSuccessfulJob::class, $key); $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); $this->assertJobWasReleasedImmediately(CircuitBreakerWithRedisTestJob::class, $key); - - retry(2, function () use ($key) { - $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); - }); + $this->assertJobWasReleasedWithDelay(CircuitBreakerWithRedisTestJob::class, $key); } protected function assertJobWasReleasedImmediately($class, $key) @@ -145,7 +144,7 @@ public function handle() public function middleware() { - return [(new ThrottlesExceptionsWithRedis(2, 10))->by($this->key)]; + return [(new ThrottlesExceptionsWithRedis(2, 10 * 60))->by($this->key)]; } } @@ -169,6 +168,6 @@ public function handle() public function middleware() { - return [(new ThrottlesExceptionsWithRedis(2, 10))->by($this->key)]; + return [(new ThrottlesExceptionsWithRedis(2, 10 * 60))->by($this->key)]; } } diff --git a/tests/Integration/Routing/RouteRedirectTest.php b/tests/Integration/Routing/RouteRedirectTest.php index edf0882963f1..e46e8e7b4717 100644 --- a/tests/Integration/Routing/RouteRedirectTest.php +++ b/tests/Integration/Routing/RouteRedirectTest.php @@ -6,12 +6,11 @@ use Illuminate\Routing\Middleware\SubstituteBindings; use Illuminate\Support\Facades\Route; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class RouteRedirectTest extends TestCase { - /** - * @dataProvider routeRedirectDataSets - */ + #[DataProvider('routeRedirectDataSets')] public function testRouteRedirect($redirectFrom, $redirectTo, $requestUri, $redirectUri) { $this->withoutExceptionHandling(); diff --git a/tests/Integration/Support/FacadesTest.php b/tests/Integration/Support/FacadesTest.php index 6d067539bf1e..7a1c833c2be3 100644 --- a/tests/Integration/Support/FacadesTest.php +++ b/tests/Integration/Support/FacadesTest.php @@ -2,6 +2,8 @@ namespace Illuminate\Tests\Integration\Support; +use Illuminate\Auth\AuthManager; +use Illuminate\Foundation\Application; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Facade; @@ -19,7 +21,7 @@ protected function tearDown(): void public function testFacadeResolvedCanResolveCallback() { - Auth::resolved(function () { + Auth::resolved(function (AuthManager $auth, Application $app) { $_SERVER['__laravel.authResolved'] = true; }); @@ -36,7 +38,7 @@ public function testFacadeResolvedCanResolveCallbackAfterAccessRootHasBeenResolv $this->assertFalse(isset($_SERVER['__laravel.authResolved'])); - Auth::resolved(function () { + Auth::resolved(function (AuthManager $auth, Application $app) { $_SERVER['__laravel.authResolved'] = true; }); diff --git a/tests/Mail/MailManagerTest.php b/tests/Mail/MailManagerTest.php index deb824da5ec0..4ae036416be9 100644 --- a/tests/Mail/MailManagerTest.php +++ b/tests/Mail/MailManagerTest.php @@ -4,13 +4,12 @@ use InvalidArgumentException; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; class MailManagerTest extends TestCase { - /** - * @dataProvider emptyTransportConfigDataProvider - */ + #[DataProvider('emptyTransportConfigDataProvider')] public function testEmptyTransportConfig($transport) { $this->app['config']->set('mail.mailers.custom_smtp', [ diff --git a/tests/Mail/MailSesTransportTest.php b/tests/Mail/MailSesTransportTest.php index f21b7256474f..8d3fd4c6b83d 100755 --- a/tests/Mail/MailSesTransportTest.php +++ b/tests/Mail/MailSesTransportTest.php @@ -2,6 +2,8 @@ namespace Illuminate\Tests\Mail; +use Aws\Command; +use Aws\Exception\AwsException; use Aws\Ses\SesClient; use Illuminate\Config\Repository; use Illuminate\Container\Container; @@ -10,6 +12,7 @@ use Illuminate\View\Factory; use Mockery as m; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Header\MetadataHeader; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; @@ -78,6 +81,23 @@ public function testSend() (new SesTransport($client))->send($message); } + public function testSendError() + { + $message = new Email(); + $message->subject('Foo subject'); + $message->text('Bar body'); + $message->sender('myself@example.com'); + $message->to('me@example.com'); + + $client = m::mock(SesClient::class); + $client->shouldReceive('sendRawEmail')->once() + ->andThrow(new AwsException('Email address is not verified.', new Command('sendRawEmail'))); + + $this->expectException(TransportException::class); + + (new SesTransport($client))->send($message); + } + public function testSesLocalConfiguration() { $container = new Container; diff --git a/tests/Mail/MailSesV2TransportTest.php b/tests/Mail/MailSesV2TransportTest.php index c95a01440ecf..7b7821558ac7 100755 --- a/tests/Mail/MailSesV2TransportTest.php +++ b/tests/Mail/MailSesV2TransportTest.php @@ -2,6 +2,8 @@ namespace Illuminate\Tests\Mail; +use Aws\Command; +use Aws\Exception\AwsException; use Aws\SesV2\SesV2Client; use Illuminate\Config\Repository; use Illuminate\Container\Container; @@ -10,6 +12,7 @@ use Illuminate\View\Factory; use Mockery as m; use PHPUnit\Framework\TestCase; +use Symfony\Component\Mailer\Exception\TransportException; use Symfony\Component\Mailer\Header\MetadataHeader; use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; @@ -78,6 +81,23 @@ public function testSend() (new SesV2Transport($client))->send($message); } + public function testSendError() + { + $message = new Email(); + $message->subject('Foo subject'); + $message->text('Bar body'); + $message->sender('myself@example.com'); + $message->to('me@example.com'); + + $client = m::mock(SesV2Client::class); + $client->shouldReceive('sendEmail')->once() + ->andThrow(new AwsException('Email address is not verified.', new Command('sendRawEmail'))); + + $this->expectException(TransportException::class); + + (new SesV2Transport($client))->send($message); + } + public function testSesV2LocalConfiguration() { $container = new Container; diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index e4bfe062c018..fee6daff4ac1 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -8,6 +8,7 @@ use Illuminate\Queue\Queue; use Illuminate\Support\Str; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; @@ -19,9 +20,7 @@ protected function tearDown(): void m::close(); } - /** - * @dataProvider pushJobsDataProvider - */ + #[DataProvider('pushJobsDataProvider')] public function testPushProperlyPushesJobOntoDatabase($uuid, $job, $displayNameStartsWith, $jobStartsWith) { Str::createUuidsUsing(function () use ($uuid) { @@ -99,7 +98,7 @@ public function testFailureToCreatePayloadFromObject() $job = new stdClass; $job->invalid = "\xc3\x28"; - $queue = $this->getMockForAbstractClass(Queue::class); + $queue = m::mock(Queue::class)->makePartial(); $class = new ReflectionClass(Queue::class); $createPayload = $class->getMethod('createPayload'); @@ -113,7 +112,7 @@ public function testFailureToCreatePayloadFromArray() { $this->expectException('InvalidArgumentException'); - $queue = $this->getMockForAbstractClass(Queue::class); + $queue = m::mock(Queue::class)->makePartial(); $class = new ReflectionClass(Queue::class); $createPayload = $class->getMethod('createPayload'); diff --git a/tests/Queue/QueueSqsJobTest.php b/tests/Queue/QueueSqsJobTest.php index bebe16b8209a..83706d769585 100644 --- a/tests/Queue/QueueSqsJobTest.php +++ b/tests/Queue/QueueSqsJobTest.php @@ -45,10 +45,7 @@ protected function setUp(): void $this->queueUrl = $this->baseUrl.'/'.$this->account.'/'.$this->queueName; // Get a mock of the SqsClient - $this->mockedSqsClient = $this->getMockBuilder(SqsClient::class) - ->addMethods(['deleteMessage']) - ->disableOriginalConstructor() - ->getMock(); + $this->mockedSqsClient = m::mock(SqsClient::class)->makePartial(); // Use Mockery to mock the IoC Container $this->mockedContainer = m::mock(Container::class); @@ -83,27 +80,21 @@ public function testFireProperlyCallsTheJobHandler() public function testDeleteRemovesTheJobFromSqs() { - $this->mockedSqsClient = $this->getMockBuilder(SqsClient::class) - ->addMethods(['deleteMessage']) - ->disableOriginalConstructor() - ->getMock(); - $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['getQueue'])->setConstructorArgs([$this->mockedSqsClient, $this->queueName, $this->account])->getMock(); + $this->mockedSqsClient = m::mock(SqsClient::class)->makePartial(); + $queue = m::mock(SqsQueue::class, [$this->mockedSqsClient, $this->queueName, $this->account])->makePartial(); $queue->setContainer($this->mockedContainer); $job = $this->getJob(); - $job->getSqs()->expects($this->once())->method('deleteMessage')->with(['QueueUrl' => $this->queueUrl, 'ReceiptHandle' => $this->mockedReceiptHandle]); + $job->getSqs()->shouldReceive('deleteMessage')->once()->with(['QueueUrl' => $this->queueUrl, 'ReceiptHandle' => $this->mockedReceiptHandle]); $job->delete(); } public function testReleaseProperlyReleasesTheJobOntoSqs() { - $this->mockedSqsClient = $this->getMockBuilder(SqsClient::class) - ->addMethods(['changeMessageVisibility']) - ->disableOriginalConstructor() - ->getMock(); - $queue = $this->getMockBuilder(SqsQueue::class)->onlyMethods(['getQueue'])->setConstructorArgs([$this->mockedSqsClient, $this->queueName, $this->account])->getMock(); + $this->mockedSqsClient = m::mock(SqsClient::class)->makePartial(); + $queue = m::mock(SqsQueue::class, [$this->mockedSqsClient, $this->queueName, $this->account])->makePartial(); $queue->setContainer($this->mockedContainer); $job = $this->getJob(); - $job->getSqs()->expects($this->once())->method('changeMessageVisibility')->with(['QueueUrl' => $this->queueUrl, 'ReceiptHandle' => $this->mockedReceiptHandle, 'VisibilityTimeout' => $this->releaseDelay]); + $job->getSqs()->shouldReceive('changeMessageVisibility')->once()->with(['QueueUrl' => $this->queueUrl, 'ReceiptHandle' => $this->mockedReceiptHandle, 'VisibilityTimeout' => $this->releaseDelay]); $job->release($this->releaseDelay); $this->assertTrue($job->isReleased()); } diff --git a/tests/Queue/RedisQueueIntegrationTest.php b/tests/Queue/RedisQueueIntegrationTest.php index e7d26522f1e4..a195775cf82b 100644 --- a/tests/Queue/RedisQueueIntegrationTest.php +++ b/tests/Queue/RedisQueueIntegrationTest.php @@ -12,6 +12,8 @@ use Illuminate\Support\InteractsWithTime; use Illuminate\Support\Str; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; class RedisQueueIntegrationTest extends TestCase @@ -45,10 +47,9 @@ protected function tearDown(): void } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testExpiredJobsArePopped($driver) { $this->setQueue($driver); @@ -77,14 +78,12 @@ public function testExpiredJobsArePopped($driver) } /** - * @dataProvider redisDriverProvider - * - * @requires extension pcntl - * * @param mixed $driver * * @throws \Exception */ + #[DataProvider('redisDriverProvider')] + #[RequiresPhpExtension('pcntl')] public function testBlockingPop($driver) { $this->tearDownRedis(); @@ -105,10 +104,9 @@ public function testBlockingPop($driver) } // /** - // * @dataProvider redisDriverProvider - // * // * @param string $driver // */ + // #[DataProvider('redisDriverProvider')] // public function testMigrateMoreThan100Jobs($driver) // { // $this->setQueue($driver); @@ -122,10 +120,9 @@ public function testBlockingPop($driver) // } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPopProperlyPopsJobOffOfRedis($driver) { $this->setQueue($driver); @@ -157,10 +154,9 @@ public function testPopProperlyPopsJobOffOfRedis($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPopProperlyPopsDelayedJobOffOfRedis($driver) { $this->setQueue($driver); @@ -184,10 +180,9 @@ public function testPopProperlyPopsDelayedJobOffOfRedis($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) { $this->setQueue($driver, 'default', null, null); @@ -214,10 +209,9 @@ public function testPopPopsDelayedJobOffOfRedisWhenExpireNull($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testBlockingPopProperlyPopsJobOffOfRedis($driver) { $this->setQueue($driver, 'default', null, 60, 5); @@ -235,10 +229,9 @@ public function testBlockingPopProperlyPopsJobOffOfRedis($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testBlockingPopProperlyPopsExpiredJobs($driver) { Str::createUuidsUsing(function () { @@ -266,10 +259,9 @@ public function testBlockingPopProperlyPopsExpiredJobs($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testNotExpireJobsWhenExpireNull($driver) { $this->setQueue($driver, 'default', null, null); @@ -315,10 +307,9 @@ public function testNotExpireJobsWhenExpireNull($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testExpireJobsWhenExpireSet($driver) { $this->setQueue($driver, 'default', null, 30); @@ -344,10 +335,9 @@ public function testExpireJobsWhenExpireSet($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testRelease($driver) { $this->setQueue($driver); @@ -385,10 +375,9 @@ public function testRelease($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testReleaseInThePast($driver) { $this->setQueue($driver); @@ -403,10 +392,9 @@ public function testReleaseInThePast($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testDelete($driver) { $this->setQueue($driver); @@ -427,10 +415,9 @@ public function testDelete($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testClear($driver) { $this->setQueue($driver); @@ -447,10 +434,9 @@ public function testClear($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testSize($driver) { $this->setQueue($driver); @@ -468,10 +454,9 @@ public function testSize($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testPushJobQueuedEvent($driver) { $events = m::mock(Dispatcher::class); @@ -493,10 +478,9 @@ public function testPushJobQueuedEvent($driver) } /** - * @dataProvider redisDriverProvider - * * @param string $driver */ + #[DataProvider('redisDriverProvider')] public function testBulkJobQueuedEvent($driver) { $events = m::mock(Dispatcher::class); diff --git a/tests/Routing/RouteUriTest.php b/tests/Routing/RouteUriTest.php index b9e0688b1be1..076d5deb7a72 100644 --- a/tests/Routing/RouteUriTest.php +++ b/tests/Routing/RouteUriTest.php @@ -3,13 +3,12 @@ namespace Illuminate\Tests\Routing; use Illuminate\Routing\RouteUri; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class RouteUriTest extends TestCase { - /** - * @dataProvider uriProvider - */ + #[DataProvider('uriProvider')] public function testRouteUrisAreProperlyParsed($uri, $expectedParsedUri, $expectedBindingFields) { $parsed = RouteUri::parse($uri); diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index 2c6079adf36b..6fdd50f2ce2a 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -10,6 +10,7 @@ use Illuminate\Routing\RouteCollection; use Illuminate\Routing\UrlGenerator; use InvalidArgumentException; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; use Symfony\Component\Routing\Exception\RouteNotFoundException; @@ -587,9 +588,7 @@ public static function providerRouteParameters() ]; } - /** - * @dataProvider providerRouteParameters - */ + #[DataProvider('providerRouteParameters')] public function testUrlGenerationForControllersRequiresPassingOfRequiredParameters($parameters) { $this->expectException(UrlGenerationException::class); @@ -641,9 +640,7 @@ public static function provideParametersAndExpectedMeaningfulExceptionMessages() ]; } - /** - * @dataProvider provideParametersAndExpectedMeaningfulExceptionMessages - */ + #[DataProvider('provideParametersAndExpectedMeaningfulExceptionMessages')] public function testUrlGenerationThrowsExceptionForMissingParametersWithMeaningfulMessage($parameters, $expectedMeaningfulExceptionMessage) { $this->expectException(UrlGenerationException::class); diff --git a/tests/Support/ConfigurationUrlParserTest.php b/tests/Support/ConfigurationUrlParserTest.php index 9035d08a45b6..55434eefa156 100644 --- a/tests/Support/ConfigurationUrlParserTest.php +++ b/tests/Support/ConfigurationUrlParserTest.php @@ -3,13 +3,12 @@ namespace Illuminate\Tests\Support; use Illuminate\Support\ConfigurationUrlParser; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ConfigurationUrlParserTest extends TestCase { - /** - * @dataProvider databaseUrls - */ + #[DataProvider('databaseUrls')] public function testDatabaseUrlsAreParsed($config, $expectedOutput) { $this->assertEquals($expectedOutput, (new ConfigurationUrlParser)->parseConfiguration($config)); diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 60d982ea8848..0057989b1cf8 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -19,6 +19,7 @@ use IteratorAggregate; use JsonSerializable; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionClass; use stdClass; @@ -30,18 +31,14 @@ class SupportCollectionTest extends TestCase { - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstReturnsFirstItemInCollection($collection) { $c = new $collection(['foo', 'bar']); $this->assertSame('foo', $c->first()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstWithCallback($collection) { $data = new $collection(['foo', 'bar', 'baz']); @@ -51,9 +48,7 @@ public function testFirstWithCallback($collection) $this->assertSame('bar', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstWithCallbackAndDefault($collection) { $data = new $collection(['foo', 'bar']); @@ -63,9 +58,7 @@ public function testFirstWithCallbackAndDefault($collection) $this->assertSame('default', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstWithDefaultAndWithoutCallback($collection) { $data = new $collection; @@ -77,9 +70,7 @@ public function testFirstWithDefaultAndWithoutCallback($collection) $this->assertSame('foo', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSoleReturnsFirstItemInCollectionIfOnlyOneExists($collection) { $collection = new $collection([ @@ -92,9 +83,7 @@ public function testSoleReturnsFirstItemInCollectionIfOnlyOneExists($collection) $this->assertSame(['name' => 'foo'], $collection->sole('name', 'foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSoleThrowsExceptionIfNoItemsExist($collection) { $this->expectException(ItemNotFoundException::class); @@ -107,9 +96,7 @@ public function testSoleThrowsExceptionIfNoItemsExist($collection) $collection->where('name', 'INVALID')->sole(); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSoleThrowsExceptionIfMoreThanOneItemExists($collection) { $this->expectExceptionObject(new MultipleItemsFoundException(2)); @@ -123,9 +110,7 @@ public function testSoleThrowsExceptionIfMoreThanOneItemExists($collection) $collection->where('name', 'foo')->sole(); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSoleReturnsFirstItemInCollectionIfOnlyOneExistsWithCallback($collection) { $data = new $collection(['foo', 'bar', 'baz']); @@ -135,9 +120,7 @@ public function testSoleReturnsFirstItemInCollectionIfOnlyOneExistsWithCallback( $this->assertSame('bar', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSoleThrowsExceptionIfNoItemsExistWithCallback($collection) { $this->expectException(ItemNotFoundException::class); @@ -149,9 +132,7 @@ public function testSoleThrowsExceptionIfNoItemsExistWithCallback($collection) }); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSoleThrowsExceptionIfMoreThanOneItemExistsWithCallback($collection) { $this->expectExceptionObject(new MultipleItemsFoundException(2)); @@ -163,9 +144,7 @@ public function testSoleThrowsExceptionIfMoreThanOneItemExistsWithCallback($coll }); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailReturnsFirstItemInCollection($collection) { $collection = new $collection([ @@ -178,9 +157,7 @@ public function testFirstOrFailReturnsFirstItemInCollection($collection) $this->assertSame(['name' => 'foo'], $collection->firstOrFail('name', 'foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailThrowsExceptionIfNoItemsExist($collection) { $this->expectException(ItemNotFoundException::class); @@ -193,9 +170,7 @@ public function testFirstOrFailThrowsExceptionIfNoItemsExist($collection) $collection->where('name', 'INVALID')->firstOrFail(); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailDoesntThrowExceptionIfMoreThanOneItemExists($collection) { $collection = new $collection([ @@ -207,9 +182,7 @@ public function testFirstOrFailDoesntThrowExceptionIfMoreThanOneItemExists($coll $this->assertSame(['name' => 'foo'], $collection->where('name', 'foo')->firstOrFail()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailReturnsFirstItemInCollectionIfOnlyOneExistsWithCallback($collection) { $data = new $collection(['foo', 'bar', 'baz']); @@ -219,9 +192,7 @@ public function testFirstOrFailReturnsFirstItemInCollectionIfOnlyOneExistsWithCa $this->assertSame('bar', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailThrowsExceptionIfNoItemsExistWithCallback($collection) { $this->expectException(ItemNotFoundException::class); @@ -233,9 +204,7 @@ public function testFirstOrFailThrowsExceptionIfNoItemsExistWithCallback($collec }); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailDoesntThrowExceptionIfMoreThanOneItemExistsWithCallback($collection) { $data = new $collection(['foo', 'bar', 'bar']); @@ -248,9 +217,7 @@ public function testFirstOrFailDoesntThrowExceptionIfMoreThanOneItemExistsWithCa ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstOrFailStopsIteratingAtFirstMatch($collection) { $data = new $collection([ @@ -270,9 +237,7 @@ function () { })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFirstWhere($collection) { $data = new $collection([ @@ -291,9 +256,7 @@ public function testFirstWhere($collection) $this->assertNull($data->firstWhere(fn ($value) => ($value['nonexistent'] ?? null) === 'key')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testLastReturnsLastItemInCollection($collection) { $c = new $collection(['foo', 'bar']); @@ -303,9 +266,7 @@ public function testLastReturnsLastItemInCollection($collection) $this->assertNull($c->last()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testLastWithCallback($collection) { $data = new $collection([100, 200, 300]); @@ -325,9 +286,7 @@ public function testLastWithCallback($collection) $this->assertNull($result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testLastWithCallbackAndDefault($collection) { $data = new $collection(['foo', 'bar']); @@ -343,9 +302,7 @@ public function testLastWithCallbackAndDefault($collection) $this->assertSame('bar', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testLastWithDefaultAndWithoutCallback($collection) { $data = new $collection; @@ -391,9 +348,7 @@ public function testShiftReturnsAndRemovesFirstXItemsInCollection() $this->assertEquals(new Collection(['foo', 'bar', 'baz']), (new Collection(['foo', 'bar', 'baz']))->shift(6)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliding($collection) { // Default parameters: $size = 2, $step = 1 @@ -450,9 +405,7 @@ public function testSliding($collection) $this->assertInstanceOf($collection, $chunks->skip(1)->first()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEmptyCollectionIsEmpty($collection) { $c = new $collection; @@ -460,9 +413,7 @@ public function testEmptyCollectionIsEmpty($collection) $this->assertTrue($c->isEmpty()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEmptyCollectionIsNotEmpty($collection) { $c = new $collection(['foo', 'bar']); @@ -471,9 +422,7 @@ public function testEmptyCollectionIsNotEmpty($collection) $this->assertTrue($c->isNotEmpty()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollectionIsConstructed($collection) { $data = new $collection('foo'); @@ -492,9 +441,7 @@ public function testCollectionIsConstructed($collection) $this->assertEmpty($data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollectionShuffleWithSeed($collection) { $data = new $collection(range(0, 100, 10)); @@ -505,9 +452,7 @@ public function testCollectionShuffleWithSeed($collection) $this->assertEquals($firstRandom, $secondRandom); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSkipMethod($collection) { $data = new $collection([1, 2, 3, 4, 5, 6]); @@ -519,9 +464,7 @@ public function testSkipMethod($collection) $this->assertSame([], $data->skip(10)->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSkipUntil($collection) { $data = new $collection([1, 1, 2, 2, 3, 3, 4, 4]); @@ -557,9 +500,7 @@ public function testSkipUntil($collection) $this->assertSame([], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSkipWhile($collection) { $data = new $collection([1, 1, 2, 2, 3, 3, 4, 4]); @@ -595,9 +536,7 @@ public function testSkipWhile($collection) $this->assertSame([3, 3, 4, 4], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGetArrayableItems($collection) { $data = new $collection; @@ -635,9 +574,7 @@ public function testGetArrayableItems($collection) $this->assertSame(['foo' => 'bar'], $array); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testToArrayCallsToArrayOnEachItemInCollection($collection) { $item1 = m::mock(Arrayable::class); @@ -662,9 +599,7 @@ public function testLazyReturnsLazyCollection() $this->assertSame([1, 2, 3, 4, 5], $lazy->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testJsonSerializeCallsToArrayOrJsonSerializeOnEachItemInCollection($collection) { $item1 = m::mock(JsonSerializable::class); @@ -677,9 +612,7 @@ public function testJsonSerializeCallsToArrayOrJsonSerializeOnEachItemInCollecti $this->assertEquals(['foo.json', 'bar.array'], $results); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testToJsonEncodesTheJsonSerializeResult($collection) { $c = $this->getMockBuilder($collection)->onlyMethods(['jsonSerialize'])->getMock(); @@ -688,9 +621,7 @@ public function testToJsonEncodesTheJsonSerializeResult($collection) $this->assertJsonStringEqualsJsonString(json_encode(['foo']), $results); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCastingToStringJsonEncodesTheToArrayResult($collection) { $c = $this->getMockBuilder($collection)->onlyMethods(['jsonSerialize'])->getMock(); @@ -811,18 +742,14 @@ public function testForgetCollectionOfKeys() $this->assertTrue(isset($c['name'])); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCountable($collection) { $c = new $collection(['foo', 'bar']); $this->assertCount(2, $c); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCountByStandalone($collection) { $c = new $collection(['foo', 'foo', 'foo', 'bar', 'bar', 'foobar']); @@ -835,9 +762,7 @@ public function testCountByStandalone($collection) $this->assertEquals([1 => 3, 5 => 3], $c->countBy()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCountByWithKey($collection) { $c = new $collection([ @@ -847,9 +772,7 @@ public function testCountByWithKey($collection) $this->assertEquals(['a' => 4, 'b' => 3], $c->countBy('key')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCountableByWithCallback($collection) { $c = new $collection(['alice', 'aaron', 'bob', 'carla']); @@ -875,9 +798,7 @@ public function testAdd() $this->assertEquals([1, 2, '', null, false, [], 'name'], $c->add('name')->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testContainsOneItem($collection) { $this->assertFalse((new $collection([]))->containsOneItem()); @@ -892,18 +813,14 @@ public function testIterable() $this->assertEquals(['foo'], $c->getIterator()->getArrayCopy()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCachingIterator($collection) { $c = new $collection(['foo']); $this->assertInstanceOf(CachingIterator::class, $c->getCachingIterator()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFilter($collection) { $c = new $collection([['id' => 1, 'name' => 'Hello'], ['id' => 2, 'name' => 'World']]); @@ -923,9 +840,7 @@ public function testFilter($collection) $this->assertEquals([1, 2, 3], $c->filter()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderKeyBy($collection) { $c = new $collection([ @@ -936,9 +851,7 @@ public function testHigherOrderKeyBy($collection) $this->assertEquals(['id1' => 'first', 'id2' => 'second'], $c->keyBy->id->map->name->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderUnique($collection) { $c = new $collection([ @@ -949,9 +862,7 @@ public function testHigherOrderUnique($collection) $this->assertCount(1, $c->unique->id); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderFilter($collection) { $c = new $collection([ @@ -978,9 +889,7 @@ public function active() $this->assertCount(1, $c->filter->active()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhere($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); @@ -1118,9 +1027,7 @@ public function testWhere($collection) $this->assertEquals([['v' => 2, 'g' => null]], $c->where('v', 2)->whereNull('g')->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereStrict($collection) { $c = new $collection([['v' => 3], ['v' => '3']]); @@ -1131,9 +1038,7 @@ public function testWhereStrict($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereInstanceOf($collection) { $c = new $collection([new stdClass, new stdClass, new $collection, new stdClass, new Str]); @@ -1142,9 +1047,7 @@ public function testWhereInstanceOf($collection) $this->assertCount(4, $c->whereInstanceOf([stdClass::class, Str::class])); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereIn($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); @@ -1153,18 +1056,14 @@ public function testWhereIn($collection) $this->assertEquals([['v' => 1]], $c->whereIn('v', [1])->whereIn('v', [1, 3])->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereInStrict($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); $this->assertEquals([['v' => 1], ['v' => 3]], $c->whereInStrict('v', [1, 3])->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNotIn($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); @@ -1172,18 +1071,14 @@ public function testWhereNotIn($collection) $this->assertEquals([['v' => 4]], $c->whereNotIn('v', [2])->whereNotIn('v', [1, 3])->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNotInStrict($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); $this->assertEquals([['v' => 2], ['v' => '3'], ['v' => 4]], $c->whereNotInStrict('v', [1, 3])->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testValues($collection) { $c = new $collection([['id' => 1, 'name' => 'Hello'], ['id' => 2, 'name' => 'World']]); @@ -1192,18 +1087,14 @@ public function testValues($collection) })->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testValuesResetKey($collection) { $data = new $collection([1 => 'a', 2 => 'b', 3 => 'c']); $this->assertEquals([0 => 'a', 1 => 'b', 2 => 'c'], $data->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testValue($collection) { $c = new $collection([['id' => 1, 'name' => 'Hello'], ['id' => 2, 'name' => 'World']]); @@ -1221,9 +1112,7 @@ public function testValue($collection) $this->assertEquals('bar', $c->where('id', 2)->value('pivot.value')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testBetween($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); @@ -1234,9 +1123,7 @@ public function testBetween($collection) $this->assertEquals([['v' => 3], ['v' => '3']], $c->whereBetween('v', [3, 3])->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNotBetween($collection) { $c = new $collection([['v' => 1], ['v' => 2], ['v' => 3], ['v' => '3'], ['v' => 4]]); @@ -1246,9 +1133,7 @@ public function testWhereNotBetween($collection) $this->assertEquals([['v' => 1], ['v' => '2'], ['v' => '4']], $c->whereNotBetween('v', [3, 3])->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFlatten($collection) { // Flat arrays are unaffected @@ -1284,9 +1169,7 @@ public function testFlatten($collection) $this->assertEquals(['#foo', '#bar', '#zap', '#baz'], $c->flatten()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFlattenWithDepth($collection) { // No depth flattens recursively @@ -1301,9 +1184,7 @@ public function testFlattenWithDepth($collection) $this->assertEquals(['#foo', '#bar', ['#baz'], '#zap'], $c->flatten(2)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFlattenIgnoresKeys($collection) { // No depth ignores keys @@ -1315,54 +1196,42 @@ public function testFlattenIgnoresKeys($collection) $this->assertEquals(['#foo', '#bar', '#baz', '#zap'], $c->flatten(1)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMergeNull($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'Hello'], $c->merge(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMergeArray($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'Hello', 'id' => 1], $c->merge(['id' => 1])->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMergeCollection($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'World', 'id' => 1], $c->merge(new $collection(['name' => 'World', 'id' => 1]))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMergeRecursiveNull($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'Hello'], $c->mergeRecursive(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMergeRecursiveArray($collection) { $c = new $collection(['name' => 'Hello', 'id' => 1]); $this->assertEquals(['name' => 'Hello', 'id' => [1, 2]], $c->mergeRecursive(['id' => 2])->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMergeRecursiveCollection($collection) { $c = new $collection(['name' => 'Hello', 'id' => 1, 'meta' => ['tags' => ['a', 'b'], 'roles' => 'admin']]); @@ -1372,18 +1241,14 @@ public function testMergeRecursiveCollection($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReplaceNull($collection) { $c = new $collection(['a', 'b', 'c']); $this->assertEquals(['a', 'b', 'c'], $c->replace(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReplaceArray($collection) { $c = new $collection(['a', 'b', 'c']); @@ -1396,9 +1261,7 @@ public function testReplaceArray($collection) $this->assertEquals(['name' => 'taylor', 'family' => 'otwell', 'age' => 26], $c->replace(['name' => 'taylor', 'age' => 26])->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReplaceCollection($collection) { $c = new $collection(['a', 'b', 'c']); @@ -1420,18 +1283,14 @@ public function testReplaceCollection($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReplaceRecursiveNull($collection) { $c = new $collection(['a', 'b', ['c', 'd']]); $this->assertEquals(['a', 'b', ['c', 'd']], $c->replaceRecursive(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReplaceRecursiveArray($collection) { $c = new $collection(['a', 'b', ['c', 'd']]); @@ -1441,9 +1300,7 @@ public function testReplaceRecursiveArray($collection) $this->assertEquals(['z', 'b', ['c', 'e'], 'f'], $c->replaceRecursive(['z', 2 => [1 => 'e'], 'f'])->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReplaceRecursiveCollection($collection) { $c = new $collection(['a', 'b', ['c', 'd']]); @@ -1453,45 +1310,35 @@ public function testReplaceRecursiveCollection($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnionNull($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'Hello'], $c->union(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnionArray($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'Hello', 'id' => 1], $c->union(['id' => 1])->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnionCollection($collection) { $c = new $collection(['name' => 'Hello']); $this->assertEquals(['name' => 'Hello', 'id' => 1], $c->union(new $collection(['name' => 'World', 'id' => 1]))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffCollection($collection) { $c = new $collection(['id' => 1, 'first_word' => 'Hello']); $this->assertEquals(['id' => 1], $c->diff(new $collection(['first_word' => 'Hello', 'last_word' => 'World']))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffUsingWithCollection($collection) { $c = new $collection(['en_GB', 'fr', 'HR']); @@ -1501,27 +1348,21 @@ public function testDiffUsingWithCollection($collection) $this->assertEquals(['fr'], $c->diffUsing(new $collection(['en_gb', 'hr']), 'strcasecmp')->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffUsingWithNull($collection) { $c = new $collection(['en_GB', 'fr', 'HR']); $this->assertEquals(['en_GB', 'fr', 'HR'], $c->diffUsing(null, 'strcasecmp')->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffNull($collection) { $c = new $collection(['id' => 1, 'first_word' => 'Hello']); $this->assertEquals(['id' => 1, 'first_word' => 'Hello'], $c->diff(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffKeys($collection) { $c1 = new $collection(['id' => 1, 'first_word' => 'Hello']); @@ -1529,9 +1370,7 @@ public function testDiffKeys($collection) $this->assertEquals(['first_word' => 'Hello'], $c1->diffKeys($c2)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffKeysUsing($collection) { $c1 = new $collection(['id' => 1, 'first_word' => 'Hello']); @@ -1542,9 +1381,7 @@ public function testDiffKeysUsing($collection) $this->assertEquals(['first_word' => 'Hello'], $c1->diffKeysUsing($c2, 'strcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffAssoc($collection) { $c1 = new $collection(['id' => 1, 'first_word' => 'Hello', 'not_affected' => 'value']); @@ -1552,9 +1389,7 @@ public function testDiffAssoc($collection) $this->assertEquals(['id' => 1, 'first_word' => 'Hello'], $c1->diffAssoc($c2)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDiffAssocUsing($collection) { $c1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); @@ -1565,9 +1400,7 @@ public function testDiffAssocUsing($collection) $this->assertEquals(['b' => 'brown', 'c' => 'blue', 'red'], $c1->diffAssocUsing($c2, 'strcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDuplicates($collection) { $duplicates = $collection::make([1, 2, 1, 'laravel', null, 'laravel', 'php', null])->duplicates()->all(); @@ -1587,9 +1420,7 @@ public function testDuplicates($collection) $this->assertSame([1 => $expected, 2 => $expected, 5 => '2'], $duplicates); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDuplicatesWithKey($collection) { $items = [['framework' => 'vue'], ['framework' => 'laravel'], ['framework' => 'laravel']]; @@ -1602,9 +1433,7 @@ public function testDuplicatesWithKey($collection) $this->assertSame([2 => 'vue'], $duplicates); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDuplicatesWithCallback($collection) { $items = [['framework' => 'vue'], ['framework' => 'laravel'], ['framework' => 'laravel']]; @@ -1614,9 +1443,7 @@ public function testDuplicatesWithCallback($collection) $this->assertSame([2 => 'laravel'], $duplicates); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDuplicatesWithStrict($collection) { $duplicates = $collection::make([1, 2, 1, 'laravel', null, 'laravel', 'php', null])->duplicatesStrict()->all(); @@ -1636,9 +1463,7 @@ public function testDuplicatesWithStrict($collection) $this->assertSame([2 => $expected, 5 => '2'], $duplicates); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEach($collection) { $c = new $collection($original = [1, 2, 'foo' => 'bar', 'bam' => 'baz']); @@ -1659,9 +1484,7 @@ public function testEach($collection) $this->assertEquals([1, 2, 'foo' => 'bar'], $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEachSpread($collection) { $c = new $collection([[1, 'a'], [2, 'b']]); @@ -1694,27 +1517,21 @@ public function testEachSpread($collection) $this->assertEquals([[1, 'a', 0], [2, 'b', 1]], $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectNull($collection) { $c = new $collection(['id' => 1, 'first_word' => 'Hello']); $this->assertEquals([], $c->intersect(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectCollection($collection) { $c = new $collection(['id' => 1, 'first_word' => 'Hello']); $this->assertEquals(['first_word' => 'Hello'], $c->intersect(new $collection(['first_world' => 'Hello', 'last_word' => 'World']))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectUsingWithNull($collection) { $collect = new $collection(['green', 'brown', 'blue']); @@ -1722,9 +1539,7 @@ public function testIntersectUsingWithNull($collection) $this->assertEquals([], $collect->intersectUsing(null, 'strcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectUsingCollection($collection) { $collect = new $collection(['green', 'brown', 'blue']); @@ -1732,9 +1547,7 @@ public function testIntersectUsingCollection($collection) $this->assertEquals(['green', 'brown'], $collect->intersectUsing(new $collection(['GREEN', 'brown', 'yellow']), 'strcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectAssocWithNull($collection) { $array1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); @@ -1742,9 +1555,7 @@ public function testIntersectAssocWithNull($collection) $this->assertEquals([], $array1->intersectAssoc(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectAssocCollection($collection) { $array1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); @@ -1753,9 +1564,7 @@ public function testIntersectAssocCollection($collection) $this->assertEquals(['a' => 'green'], $array1->intersectAssoc($array2)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectAssocUsingWithNull($collection) { $array1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); @@ -1763,9 +1572,7 @@ public function testIntersectAssocUsingWithNull($collection) $this->assertEquals([], $array1->intersectAssocUsing(null, 'strcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectAssocUsingCollection($collection) { $array1 = new $collection(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); @@ -1774,18 +1581,14 @@ public function testIntersectAssocUsingCollection($collection) $this->assertEquals(['b' => 'brown'], $array1->intersectAssocUsing($array2, 'strcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectByKeysNull($collection) { $c = new $collection(['name' => 'Mateus', 'age' => 18]); $this->assertEquals([], $c->intersectByKeys(null)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testIntersectByKeys($collection) { $c = new $collection(['name' => 'Mateus', 'age' => 18]); @@ -1795,9 +1598,7 @@ public function testIntersectByKeys($collection) $this->assertEquals(['name' => 'taylor', 'family' => 'otwell'], $c->intersectByKeys(new $collection(['height' => 180, 'name' => 'amir', 'family' => 'moharami']))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnique($collection) { $c = new $collection(['Hello', 'World', 'World']); @@ -1807,9 +1608,7 @@ public function testUnique($collection) $this->assertEquals([[1, 2], [2, 3], [3, 4]], $c->unique()->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUniqueWithCallback($collection) { $c = new $collection([ @@ -1842,9 +1641,7 @@ public function testUniqueWithCallback($collection) })->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUniqueStrict($collection) { $c = new $collection([ @@ -1874,27 +1671,21 @@ public function testUniqueStrict($collection) ], $c->uniqueStrict('id')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollapse($collection) { $data = new $collection([[$object1 = new stdClass], [$object2 = new stdClass]]); $this->assertEquals([$object1, $object2], $data->collapse()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollapseWithNestedCollections($collection) { $data = new $collection([new $collection([1, 2, 3]), new $collection([4, 5, 6])]); $this->assertEquals([1, 2, 3, 4, 5, 6], $data->collapse()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testJoin($collection) { $this->assertSame('a, b, c', (new $collection(['a', 'b', 'c']))->join(', ')); @@ -1908,9 +1699,7 @@ public function testJoin($collection) $this->assertSame('', (new $collection([]))->join(', ', ' and ')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCrossJoin($collection) { // Cross join with an array @@ -1940,9 +1729,7 @@ public function testCrossJoin($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSort($collection) { $data = (new $collection([5, 3, 1, 2, 4]))->sort(); @@ -1961,9 +1748,7 @@ public function testSort($collection) $this->assertEquals(['T1', 'T2', 'T10'], $data->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortDesc($collection) { $data = (new $collection([5, 3, 1, 2, 4]))->sortDesc(); @@ -1982,9 +1767,7 @@ public function testSortDesc($collection) $this->assertEquals(['T10', 'T2', 'T1'], $data->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortWithCallback($collection) { $data = (new $collection([5, 3, 1, 2, 4]))->sort(function ($a, $b) { @@ -1998,9 +1781,7 @@ public function testSortWithCallback($collection) $this->assertEquals(range(1, 5), array_values($data->all())); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortBy($collection) { $data = new $collection(['taylor', 'dayle']); @@ -2018,9 +1799,7 @@ public function testSortBy($collection) $this->assertEquals(['taylor', 'dayle'], array_values($data->all())); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortByString($collection) { $data = new $collection([['name' => 'taylor'], ['name' => 'dayle']]); @@ -2034,9 +1813,7 @@ public function testSortByString($collection) $this->assertEquals([['name' => 'dayle'], ['name' => 'taylor']], array_values($data->all())); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortByCallableString($collection) { $data = new $collection([['sort' => 2], ['sort' => 1]]); @@ -2045,9 +1822,7 @@ public function testSortByCallableString($collection) $this->assertEquals([['sort' => 1], ['sort' => 2]], array_values($data->all())); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortByAlwaysReturnsAssoc($collection) { $data = new $collection(['a' => 'taylor', 'b' => 'dayle']); @@ -2075,9 +1850,7 @@ public function testSortByAlwaysReturnsAssoc($collection) $this->assertEquals([1 => ['sort' => 1], 0 => ['sort' => 2]], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortKeys($collection) { $data = new $collection(['b' => 'dayle', 'a' => 'taylor']); @@ -2085,9 +1858,7 @@ public function testSortKeys($collection) $this->assertSame(['a' => 'taylor', 'b' => 'dayle'], $data->sortKeys()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortKeysDesc($collection) { $data = new $collection(['a' => 'taylor', 'b' => 'dayle']); @@ -2095,9 +1866,7 @@ public function testSortKeysDesc($collection) $this->assertSame(['b' => 'dayle', 'a' => 'taylor'], $data->sortKeysDesc()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSortKeysUsing($collection) { $data = new $collection(['B' => 'dayle', 'a' => 'taylor']); @@ -2105,9 +1874,7 @@ public function testSortKeysUsing($collection) $this->assertSame(['a' => 'taylor', 'B' => 'dayle'], $data->sortKeysUsing('strnatcasecmp')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReverse($collection) { $data = new $collection(['zaeed', 'alan']); @@ -2121,18 +1888,14 @@ public function testReverse($collection) $this->assertSame(['framework' => 'laravel', 'name' => 'taylor'], $reversed->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFlip($collection) { $data = new $collection(['name' => 'taylor', 'framework' => 'laravel']); $this->assertEquals(['taylor' => 'name', 'laravel' => 'framework'], $data->flip()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testChunk($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -2145,9 +1908,7 @@ public function testChunk($collection) $this->assertEquals([9 => 10], $data->get(3)->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testChunkWhenGivenZeroAsSize($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -2158,9 +1919,7 @@ public function testChunkWhenGivenZeroAsSize($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testChunkWhenGivenLessThanZero($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -2171,9 +1930,7 @@ public function testChunkWhenGivenLessThanZero($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitIn($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); @@ -2187,9 +1944,7 @@ public function testSplitIn($collection) $this->assertEquals([9, 10], $data->get(2)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testChunkWhileOnEqualElements($collection) { $data = (new $collection(['A', 'A', 'B', 'B', 'C', 'C', 'C'])) @@ -2204,9 +1959,7 @@ public function testChunkWhileOnEqualElements($collection) $this->assertEquals([4 => 'C', 5 => 'C', 6 => 'C'], $data->last()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testChunkWhileOnContiguouslyIncreasingIntegers($collection) { $data = (new $collection([1, 4, 9, 10, 11, 12, 15, 16, 19, 20, 21])) @@ -2223,9 +1976,7 @@ public function testChunkWhileOnContiguouslyIncreasingIntegers($collection) $this->assertEquals([8 => 19, 9 => 20, 10 => 21], $data->last()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEvery($collection) { $c = new $collection([]); @@ -2255,9 +2006,7 @@ public function testEvery($collection) $this->assertFalse($c->concat([['active' => false]])->every->active); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testExcept($collection) { $data = new $collection(['first' => 'Taylor', 'last' => 'Otwell', 'email' => 'taylorotwell@gmail.com']); @@ -2272,18 +2021,14 @@ public function testExcept($collection) $this->assertEquals(['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], $data->except(collect(['last']))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testExceptSelf($collection) { $data = new $collection(['first' => 'Taylor', 'last' => 'Otwell']); $this->assertEquals(['first' => 'Taylor', 'last' => 'Otwell'], $data->except($data)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPluckWithArrayAndObjectValues($collection) { $data = new $collection([(object) ['name' => 'taylor', 'email' => 'foo'], ['name' => 'dayle', 'email' => 'bar']]); @@ -2291,9 +2036,7 @@ public function testPluckWithArrayAndObjectValues($collection) $this->assertEquals(['foo', 'bar'], $data->pluck('email')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPluckWithArrayAccessValues($collection) { $data = new $collection([ @@ -2305,9 +2048,7 @@ public function testPluckWithArrayAccessValues($collection) $this->assertEquals(['foo', 'bar'], $data->pluck('email')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPluckWithDotNotation($collection) { $data = new $collection([ @@ -2328,9 +2069,7 @@ public function testPluckWithDotNotation($collection) $this->assertEquals([['php', 'python'], ['php', 'asp', 'java']], $data->pluck('skill.backend')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPluckDuplicateKeysExist($collection) { $data = new collection([ @@ -2343,9 +2082,7 @@ public function testPluckDuplicateKeysExist($collection) $this->assertEquals(['Tesla' => 'black', 'Pagani' => 'orange'], $data->pluck('color', 'brand')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHas($collection) { $data = new $collection(['id' => 1, 'first' => 'Hello', 'second' => 'World']); @@ -2356,9 +2093,7 @@ public function testHas($collection) $this->assertTrue($data->has('first', 'second')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHasAny($collection) { $data = new $collection(['id' => 1, 'first' => 'Hello', 'second' => 'World']); @@ -2372,9 +2107,7 @@ public function testHasAny($collection) $this->assertFalse($data->hasAny([])); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testImplode($collection) { $data = new $collection([['name' => 'taylor', 'email' => 'foo'], ['name' => 'dayle', 'email' => 'bar']]); @@ -2402,9 +2135,7 @@ public function testImplode($collection) $this->assertSame('taylor-foo,dayle-bar', $data->implode(fn ($user) => $user['name'].'-'.$user['email'], ',')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTake($collection) { $data = new $collection(['taylor', 'dayle', 'shawn']); @@ -2457,9 +2188,7 @@ public function testPutWithNoKey() $this->assertEquals(['taylor', 'shawn', 'dayle'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testRandom($collection) { $data = new $collection([1, 2, 3, 4, 5, 6]); @@ -2507,9 +2236,7 @@ public function testRandom($collection) $this->assertCount(5, array_intersect_assoc($random->all(), $data->all())); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testRandomOnEmptyCollection($collection) { $data = new $collection; @@ -2523,9 +2250,7 @@ public function testRandomOnEmptyCollection($collection) $this->assertCount(0, $random); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeLast($collection) { $data = new $collection(['taylor', 'dayle', 'shawn']); @@ -2533,9 +2258,7 @@ public function testTakeLast($collection) $this->assertEquals([1 => 'dayle', 2 => 'shawn'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeUntilUsingValue($collection) { $data = new $collection([1, 2, 3, 4]); @@ -2545,9 +2268,7 @@ public function testTakeUntilUsingValue($collection) $this->assertSame([1, 2], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeUntilUsingCallback($collection) { $data = new $collection([1, 2, 3, 4]); @@ -2559,9 +2280,7 @@ public function testTakeUntilUsingCallback($collection) $this->assertSame([1, 2], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeUntilReturnsAllItemsForUnmetValue($collection) { $data = new $collection([1, 2, 3, 4]); @@ -2577,9 +2296,7 @@ public function testTakeUntilReturnsAllItemsForUnmetValue($collection) $this->assertSame($data->toArray(), $actual->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeUntilCanBeProxied($collection) { $data = new $collection([ @@ -2595,9 +2312,7 @@ public function testTakeUntilCanBeProxied($collection) $this->assertSame('Taylor', $actual->get(1)->name); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeWhileUsingValue($collection) { $data = new $collection([1, 1, 2, 2, 3, 3]); @@ -2607,9 +2322,7 @@ public function testTakeWhileUsingValue($collection) $this->assertSame([1, 1], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeWhileUsingCallback($collection) { $data = new $collection([1, 2, 3, 4]); @@ -2621,9 +2334,7 @@ public function testTakeWhileUsingCallback($collection) $this->assertSame([1, 2], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeWhileReturnsNoItemsForUnmetValue($collection) { $data = new $collection([1, 2, 3, 4]); @@ -2639,9 +2350,7 @@ public function testTakeWhileReturnsNoItemsForUnmetValue($collection) $this->assertSame([], $actual->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTakeWhileCanBeProxied($collection) { $data = new $collection([ @@ -2658,9 +2367,7 @@ public function testTakeWhileCanBeProxied($collection) $this->assertSame('Adam', $actual->get(1)->name); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMacroable($collection) { // Foo() macro : unique values starting with A @@ -2677,9 +2384,7 @@ public function testMacroable($collection) $this->assertSame(['a', 'aa', 'aaa'], $c->foo()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCanAddMethodsToProxy($collection) { $collection::macro('adults', function ($callback) { @@ -2695,18 +2400,14 @@ public function testCanAddMethodsToProxy($collection) $this->assertSame([['age' => 18], ['age' => 56]], $c->adults->age->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMakeMethod($collection) { $data = $collection::make('foo'); $this->assertEquals(['foo'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMakeMethodFromNull($collection) { $data = $collection::make(null); @@ -2716,9 +2417,7 @@ public function testMakeMethodFromNull($collection) $this->assertEquals([], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMakeMethodFromCollection($collection) { $firstCollection = $collection::make(['foo' => 'bar']); @@ -2726,72 +2425,56 @@ public function testMakeMethodFromCollection($collection) $this->assertEquals(['foo' => 'bar'], $secondCollection->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMakeMethodFromArray($collection) { $data = $collection::make(['foo' => 'bar']); $this->assertEquals(['foo' => 'bar'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithScalar($collection) { $data = $collection::wrap('foo'); $this->assertEquals(['foo'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithArray($collection) { $data = $collection::wrap(['foo']); $this->assertEquals(['foo'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithArrayable($collection) { $data = $collection::wrap($o = new TestArrayableObject); $this->assertEquals([$o], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithJsonable($collection) { $data = $collection::wrap($o = new TestJsonableObject); $this->assertEquals([$o], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithJsonSerialize($collection) { $data = $collection::wrap($o = new TestJsonSerializeObject); $this->assertEquals([$o], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithCollectionClass($collection) { $data = $collection::wrap($collection::make(['foo'])); $this->assertEquals(['foo'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWrapWithCollectionSubclass($collection) { $data = TestCollectionSubclass::wrap($collection::make(['foo'])); @@ -2799,34 +2482,26 @@ public function testWrapWithCollectionSubclass($collection) $this->assertInstanceOf(TestCollectionSubclass::class, $data); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnwrapCollection($collection) { $data = new $collection(['foo']); $this->assertEquals(['foo'], $collection::unwrap($data)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnwrapCollectionWithArray($collection) { $this->assertEquals(['foo'], $collection::unwrap(['foo'])); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnwrapCollectionWithScalar($collection) { $this->assertSame('foo', $collection::unwrap('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEmptyMethod($collection) { $collection = $collection::empty(); @@ -2834,9 +2509,7 @@ public function testEmptyMethod($collection) $this->assertCount(0, $collection->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTimesMethod($collection) { $two = $collection::times(2, function ($number) { @@ -2859,9 +2532,7 @@ public function testTimesMethod($collection) $this->assertEquals(range(1, 5), $range->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testRangeMethod($collection) { $this->assertSame( @@ -2895,9 +2566,7 @@ public function testRangeMethod($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConstructMakeFromObject($collection) { $object = new stdClass; @@ -2906,18 +2575,14 @@ public function testConstructMakeFromObject($collection) $this->assertEquals(['foo' => 'bar'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConstructMethod($collection) { $data = new $collection('foo'); $this->assertEquals(['foo'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConstructMethodFromNull($collection) { $data = new $collection(null); @@ -2927,9 +2592,7 @@ public function testConstructMethodFromNull($collection) $this->assertEquals([], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConstructMethodFromCollection($collection) { $firstCollection = new $collection(['foo' => 'bar']); @@ -2937,18 +2600,14 @@ public function testConstructMethodFromCollection($collection) $this->assertEquals(['foo' => 'bar'], $secondCollection->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConstructMethodFromArray($collection) { $data = new $collection(['foo' => 'bar']); $this->assertEquals(['foo' => 'bar'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConstructMethodFromObject($collection) { $object = new stdClass; @@ -2985,9 +2644,7 @@ public function testSplice() $this->assertEquals(['foo', 'bar', 'baz'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGetPluckValueWithAccessors($collection) { $model = new TestAccessorEloquentTestStub(['some' => 'foo']); @@ -2997,9 +2654,7 @@ public function testGetPluckValueWithAccessors($collection) $this->assertEquals(['foo', 'bar'], $data->pluck('some')->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMap($collection) { $data = new $collection([1, 2, 3]); @@ -3016,9 +2671,7 @@ public function testMap($collection) $this->assertEquals(['first' => 'first-rolyat', 'last' => 'last-llewto'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapSpread($collection) { $c = new $collection([[1, 'a'], [2, 'b']]); @@ -3040,9 +2693,7 @@ public function testMapSpread($collection) $this->assertEquals(['1-a-0', '2-b-1'], $result->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testFlatMap($collection) { $data = new $collection([ @@ -3055,9 +2706,7 @@ public function testFlatMap($collection) $this->assertEquals(['programming', 'basketball', 'music', 'powerlifting'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapToDictionary($collection) { $data = new $collection([ @@ -3076,9 +2725,7 @@ public function testMapToDictionary($collection) $this->assertIsArray($groups->get('A')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapToDictionaryWithNumericKeys($collection) { $data = new $collection([1, 2, 3, 2, 1]); @@ -3090,9 +2737,7 @@ public function testMapToDictionaryWithNumericKeys($collection) $this->assertEquals([1 => [0, 4], 2 => [1, 3], 3 => [2]], $groups->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapToGroups($collection) { $data = new $collection([ @@ -3111,9 +2756,7 @@ public function testMapToGroups($collection) $this->assertInstanceOf($collection, $groups->get('A')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapToGroupsWithNumericKeys($collection) { $data = new $collection([1, 2, 3, 2, 1]); @@ -3126,9 +2769,7 @@ public function testMapToGroupsWithNumericKeys($collection) $this->assertEquals([1, 2, 3, 2, 1], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapWithKeys($collection) { $data = new $collection([ @@ -3145,9 +2786,7 @@ public function testMapWithKeys($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapWithKeysIntegerKeys($collection) { $data = new $collection([ @@ -3164,9 +2803,7 @@ public function testMapWithKeysIntegerKeys($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapWithKeysMultipleRows($collection) { $data = new $collection([ @@ -3190,9 +2827,7 @@ public function testMapWithKeysMultipleRows($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapWithKeysCallbackKey($collection) { $data = new $collection([ @@ -3209,9 +2844,7 @@ public function testMapWithKeysCallbackKey($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapInto($collection) { $data = new $collection([ @@ -3224,9 +2857,7 @@ public function testMapInto($collection) $this->assertSame('second', $data->get(1)->value); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testNth($collection) { $data = new $collection([ @@ -3251,9 +2882,7 @@ public function testNth($collection) $this->assertEquals(['e'], $data->nth(2, -2)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMapWithKeysOverwritingKeys($collection) { $data = new $collection([ @@ -3282,9 +2911,7 @@ public function testTransform() $this->assertEquals(['first' => 'first-rolyat', 'last' => 'last-llewto'], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByAttribute($collection) { $data = new $collection([['rating' => 1, 'url' => '1'], ['rating' => 1, 'url' => '1'], ['rating' => 2, 'url' => '2']]); @@ -3296,9 +2923,7 @@ public function testGroupByAttribute($collection) $this->assertEquals([1 => [['rating' => 1, 'url' => '1'], ['rating' => 1, 'url' => '1']], 2 => [['rating' => 2, 'url' => '2']]], $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByAttributeWithStringableKey($collection) { $data = new $collection($payload = [ @@ -3320,9 +2945,7 @@ public function __toString() $this->assertEquals(['1' => [$payload[0], $payload[1]], '2' => [$payload[2]]], $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByCallable($collection) { $data = new $collection([['rating' => 1, 'url' => '1'], ['rating' => 1, 'url' => '1'], ['rating' => 2, 'url' => '2']]); @@ -3344,9 +2967,7 @@ public function sortByUrl(array $value) return $value['url']; } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByAttributeWithBackedEnumKey($collection) { $data = new $collection([ @@ -3358,9 +2979,7 @@ public function testGroupByAttributeWithBackedEnumKey($collection) $this->assertEquals([TestBackedEnum::A->value => [['rating' => TestBackedEnum::A, 'url' => '1']], TestBackedEnum::B->value => [['rating' => TestBackedEnum::B, 'url' => '1']]], $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByAttributePreservingKeys($collection) { $data = new $collection([10 => ['rating' => 1, 'url' => '1'], 20 => ['rating' => 1, 'url' => '1'], 30 => ['rating' => 2, 'url' => '2']]); @@ -3375,9 +2994,7 @@ public function testGroupByAttributePreservingKeys($collection) $this->assertEquals($expected_result, $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByClosureWhereItemsHaveSingleGroup($collection) { $data = new $collection([['rating' => 1, 'url' => '1'], ['rating' => 1, 'url' => '1'], ['rating' => 2, 'url' => '2']]); @@ -3389,9 +3006,7 @@ public function testGroupByClosureWhereItemsHaveSingleGroup($collection) $this->assertEquals([1 => [['rating' => 1, 'url' => '1'], ['rating' => 1, 'url' => '1']], 2 => [['rating' => 2, 'url' => '2']]], $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByClosureWhereItemsHaveSingleGroupPreservingKeys($collection) { $data = new $collection([10 => ['rating' => 1, 'url' => '1'], 20 => ['rating' => 1, 'url' => '1'], 30 => ['rating' => 2, 'url' => '2']]); @@ -3408,9 +3023,7 @@ public function testGroupByClosureWhereItemsHaveSingleGroupPreservingKeys($colle $this->assertEquals($expected_result, $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByClosureWhereItemsHaveMultipleGroups($collection) { $data = new $collection([ @@ -3440,9 +3053,7 @@ public function testGroupByClosureWhereItemsHaveMultipleGroups($collection) $this->assertEquals($expected_result, $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByClosureWhereItemsHaveMultipleGroupsPreservingKeys($collection) { $data = new $collection([ @@ -3472,9 +3083,7 @@ public function testGroupByClosureWhereItemsHaveMultipleGroupsPreservingKeys($co $this->assertEquals($expected_result, $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGroupByMultiLevelAndClosurePreservingKeys($collection) { $data = new $collection([ @@ -3517,9 +3126,7 @@ function ($item) { $this->assertEquals($expected_result, $result->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testKeyByAttribute($collection) { $data = new $collection([['rating' => 1, 'name' => '1'], ['rating' => 2, 'name' => '2'], ['rating' => 3, 'name' => '3']]); @@ -3533,9 +3140,7 @@ public function testKeyByAttribute($collection) $this->assertEquals([2 => ['rating' => 1, 'name' => '1'], 4 => ['rating' => 2, 'name' => '2'], 6 => ['rating' => 3, 'name' => '3']], $result->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testKeyByClosure($collection) { $data = new $collection([ @@ -3551,9 +3156,7 @@ public function testKeyByClosure($collection) ], $result->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testKeyByObject($collection) { $data = new $collection([ @@ -3569,9 +3172,7 @@ public function testKeyByObject($collection) ], $result->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testContains($collection) { $c = new $collection([1, 3, 5]); @@ -3630,9 +3231,7 @@ public function testContains($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDoesntContain($collection) { $c = new $collection([1, 3, 5]); @@ -3691,9 +3290,7 @@ public function testDoesntContain($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSome($collection) { $c = new $collection([1, 3, 5]); @@ -3732,9 +3329,7 @@ public function testSome($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testContainsStrict($collection) { $c = new $collection([1, 3, 5, '02']); @@ -3780,9 +3375,7 @@ public function testContainsStrict($collection) $this->assertTrue($c->containsStrict('')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testContainsWithOperator($collection) { $c = new $collection([['v' => 1], ['v' => 3], ['v' => '4'], ['v' => 5]]); @@ -3793,9 +3386,7 @@ public function testContainsWithOperator($collection) $this->assertTrue($c->contains('v', '>', 4)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGettingSumFromCollection($collection) { $c = new $collection([(object) ['foo' => 50], (object) ['foo' => 50]]); @@ -3807,27 +3398,21 @@ public function testGettingSumFromCollection($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCanSumValuesWithoutACallback($collection) { $c = new $collection([1, 2, 3, 4, 5]); $this->assertEquals(15, $c->sum()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGettingSumFromEmptyCollection($collection) { $c = new $collection; $this->assertEquals(0, $c->sum('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testValueRetrieverAcceptsDotNotation($collection) { $c = new $collection([ @@ -3894,9 +3479,7 @@ public function testPullReturnsDefault() $this->assertSame('foo', $value); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testRejectRemovesElementsPassingTruthTest($collection) { $c = new $collection(['foo', 'bar']); @@ -3924,9 +3507,7 @@ public function testRejectRemovesElementsPassingTruthTest($collection) })->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testRejectWithoutAnArgumentRemovesTruthyValues($collection) { $data1 = new $collection([ @@ -3947,9 +3528,7 @@ public function testRejectWithoutAnArgumentRemovesTruthyValues($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSearchReturnsIndexOfFirstFoundItem($collection) { $c = new $collection([1, 2, 3, 4, 5, 2, 5, 'foo' => 'bar']); @@ -3965,9 +3544,7 @@ public function testSearchReturnsIndexOfFirstFoundItem($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSearchInStrictMode($collection) { $c = new $collection([false, 0, 1, [], '']); @@ -3980,9 +3557,7 @@ public function testSearchInStrictMode($collection) $this->assertEquals(4, $c->search('', true)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSearchReturnsFalseWhenItemIsNotFound($collection) { $c = new $collection([1, 2, 3, 4, 5, 'foo' => 'bar']); @@ -3997,9 +3572,7 @@ public function testSearchReturnsFalseWhenItemIsNotFound($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testKeys($collection) { $c = new $collection(['name' => 'taylor', 'framework' => 'laravel']); @@ -4009,9 +3582,7 @@ public function testKeys($collection) $this->assertEquals([0, 1], $c->keys()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPaginate($collection) { $c = new $collection(['one', 'two', 'three', 'four']); @@ -4087,9 +3658,7 @@ public function testPushWithMultipleItems() $this->assertSame($expected, $actual); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testZip($collection) { $c = new $collection([1, 2, 3]); @@ -4118,9 +3687,7 @@ public function testZip($collection) $this->assertEquals([3, 6, null], $c->get(2)->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPadPadsArrayWithValue($collection) { $c = new $collection([1, 2, 3]); @@ -4140,9 +3707,7 @@ public function testPadPadsArrayWithValue($collection) $this->assertEquals([1, 2, 3, 4, 5], $c->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGettingMaxItemsFromCollection($collection) { $c = new $collection([(object) ['foo' => 10], (object) ['foo' => 20]]); @@ -4163,9 +3728,7 @@ public function testGettingMaxItemsFromCollection($collection) $this->assertNull($c->max()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGettingMinItemsFromCollection($collection) { $c = new $collection([(object) ['foo' => 10], (object) ['foo' => 20]]); @@ -4196,9 +3759,7 @@ public function testGettingMinItemsFromCollection($collection) $this->assertNull($c->min()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testOnly($collection) { $data = new $collection(['first' => 'Taylor', 'last' => 'Otwell', 'email' => 'taylorotwell@gmail.com']); @@ -4213,9 +3774,7 @@ public function testOnly($collection) $this->assertEquals(['first' => 'Taylor', 'email' => 'taylorotwell@gmail.com'], $data->only(collect(['first', 'email']))->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGettingAvgItemsFromCollection($collection) { $c = new $collection([(object) ['foo' => 10], (object) ['foo' => 20]]); @@ -4257,9 +3816,7 @@ public function testGettingAvgItemsFromCollection($collection) $this->assertEquals(3, $c->avg('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testJsonSerialize($collection) { $c = new $collection([ @@ -4279,9 +3836,7 @@ public function testJsonSerialize($collection) ], $c->jsonSerialize()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCombineWithArray($collection) { $c = new $collection([1, 2, 3]); @@ -4322,9 +3877,7 @@ public function testCombineWithArray($collection) $this->assertSame($expected, $actual); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCombineWithCollection($collection) { $expected = [ @@ -4340,9 +3893,7 @@ public function testCombineWithCollection($collection) $this->assertSame($expected, $actual); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConcatWithArray($collection) { $expected = [ @@ -4368,9 +3919,7 @@ public function testConcatWithArray($collection) $this->assertSame($expected, $actual); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testConcatWithCollection($collection) { $expected = [ @@ -4398,9 +3947,7 @@ public function testConcatWithCollection($collection) $this->assertSame($expected, $actual); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDump($collection) { $log = new Collection; @@ -4416,9 +3963,7 @@ public function testDump($collection) VarDumper::setHandler(null); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReduce($collection) { $data = new $collection([1, 2, 3]); @@ -4435,9 +3980,7 @@ public function testReduce($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReduceSpread($collection) { $data = new $collection([-1, 0, 1, 2, 3, 4, 5]); @@ -4455,9 +3998,7 @@ public function testReduceSpread($collection) $this->assertEquals(-1, $min); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testReduceSpreadThrowsAnExceptionIfReducerDoesNotReturnAnArray($collection) { $data = new $collection([1]); @@ -4469,9 +4010,7 @@ public function testReduceSpreadThrowsAnExceptionIfReducerDoesNotReturnAnArray($ }, null); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testRandomThrowsAnExceptionUsingAmountBiggerThanCollectionSize($collection) { $this->expectException(InvalidArgumentException::class); @@ -4480,9 +4019,7 @@ public function testRandomThrowsAnExceptionUsingAmountBiggerThanCollectionSize($ $data->random(4); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPipe($collection) { $data = new $collection([1, 2, 3]); @@ -4492,9 +4029,7 @@ public function testPipe($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPipeInto($collection) { $data = new $collection([ @@ -4506,9 +4041,7 @@ public function testPipeInto($collection) $this->assertSame($data, $instance->value); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPipeThrough($collection) { $data = new $collection([1, 2, 3]); @@ -4525,9 +4058,7 @@ function ($data) { $this->assertEquals(15, $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMedianValueWithArrayCollection($collection) { $data = new $collection([1, 2, 2, 4]); @@ -4535,9 +4066,7 @@ public function testMedianValueWithArrayCollection($collection) $this->assertEquals(2, $data->median()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMedianValueByKey($collection) { $data = new $collection([ @@ -4549,9 +4078,7 @@ public function testMedianValueByKey($collection) $this->assertEquals(2, $data->median('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMedianOnCollectionWithNull($collection) { $data = new $collection([ @@ -4563,9 +4090,7 @@ public function testMedianOnCollectionWithNull($collection) $this->assertEquals(2, $data->median('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEvenMedianCollection($collection) { $data = new $collection([ @@ -4575,9 +4100,7 @@ public function testEvenMedianCollection($collection) $this->assertEquals(1.5, $data->median('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMedianOutOfOrderCollection($collection) { $data = new $collection([ @@ -4588,27 +4111,21 @@ public function testMedianOutOfOrderCollection($collection) $this->assertEquals(3, $data->median('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMedianOnEmptyCollectionReturnsNull($collection) { $data = new $collection; $this->assertNull($data->median()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testModeOnNullCollection($collection) { $data = new $collection; $this->assertNull($data->mode()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testMode($collection) { $data = new $collection([1, 2, 3, 4, 4, 5]); @@ -4616,9 +4133,7 @@ public function testMode($collection) $this->assertEquals([4], $data->mode()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testModeValueByKey($collection) { $data = new $collection([ @@ -4637,108 +4152,84 @@ public function testModeValueByKey($collection) $this->assertEquals($data2->mode('foo'), $data->mode('foo')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWithMultipleModeValues($collection) { $data = new $collection([1, 2, 2, 1]); $this->assertEquals([1, 2], $data->mode()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliceOffset($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8]); $this->assertEquals([4, 5, 6, 7, 8], $data->slice(3)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliceNegativeOffset($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8]); $this->assertEquals([6, 7, 8], $data->slice(-3)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliceOffsetAndLength($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8]); $this->assertEquals([4, 5, 6], $data->slice(3, 3)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliceOffsetAndNegativeLength($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8]); $this->assertEquals([4, 5, 6, 7], $data->slice(3, -1)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliceNegativeOffsetAndLength($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8]); $this->assertEquals([4, 5, 6], $data->slice(-5, 3)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSliceNegativeOffsetAndNegativeLength($collection) { $data = new $collection([1, 2, 3, 4, 5, 6, 7, 8]); $this->assertEquals([3, 4, 5, 6], $data->slice(-6, -2)->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollectionFromTraversable($collection) { $data = new $collection(new ArrayObject([1, 2, 3])); $this->assertEquals([1, 2, 3], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollectionFromTraversableWithKeys($collection) { $data = new $collection(new ArrayObject(['foo' => 1, 'bar' => 2, 'baz' => 3])); $this->assertEquals(['foo' => 1, 'bar' => 2, 'baz' => 3], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollectionFromEnum($collection) { $data = new $collection(TestEnum::A); $this->assertEquals([TestEnum::A], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollectionFromBackedEnum($collection) { $data = new $collection(TestBackedEnum::A); $this->assertEquals([TestBackedEnum::A], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitCollectionWithADivisibleCount($collection) { $data = new $collection(['a', 'b', 'c', 'd']); @@ -4769,9 +4260,7 @@ public function testSplitCollectionWithADivisibleCount($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitCollectionWithAnUndivisableCount($collection) { $data = new $collection(['a', 'b', 'c']); @@ -4788,9 +4277,7 @@ public function testSplitCollectionWithAnUndivisableCount($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitCollectionWithCountLessThenDivisor($collection) { $data = new $collection(['a']); @@ -4807,9 +4294,7 @@ public function testSplitCollectionWithCountLessThenDivisor($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitCollectionIntoThreeWithCountOfFour($collection) { $data = new $collection(['a', 'b', 'c', 'd']); @@ -4827,9 +4312,7 @@ public function testSplitCollectionIntoThreeWithCountOfFour($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitCollectionIntoThreeWithCountOfFive($collection) { $data = new $collection(['a', 'b', 'c', 'd', 'e']); @@ -4847,9 +4330,7 @@ public function testSplitCollectionIntoThreeWithCountOfFive($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitCollectionIntoSixWithCountOfTen($collection) { $data = new $collection(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']); @@ -4870,9 +4351,7 @@ public function testSplitCollectionIntoSixWithCountOfTen($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testSplitEmptyCollection($collection) { $data = new $collection; @@ -4889,9 +4368,7 @@ public function testSplitEmptyCollection($collection) ); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderCollectionGroupBy($collection) { $data = new $collection([ @@ -4912,9 +4389,7 @@ public function testHigherOrderCollectionGroupBy($collection) ], $data->groupBy->uppercase()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderCollectionMap($collection) { $person1 = (object) ['name' => 'Taylor']; @@ -4929,9 +4404,7 @@ public function testHigherOrderCollectionMap($collection) $this->assertEquals(['TAYLOR', 'TAYLOR'], $data->each->uppercase()->map->name->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderCollectionMapFromArrays($collection) { $person1 = ['name' => 'Taylor']; @@ -4946,9 +4419,7 @@ public function testHigherOrderCollectionMapFromArrays($collection) $this->assertEquals(['TAYLOR', 'TAYLOR'], $data->each->uppercase()->map->name->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPartition($collection) { $data = new $collection(range(1, 10)); @@ -4961,9 +4432,7 @@ public function testPartition($collection) $this->assertEquals([6, 7, 8, 9, 10], $secondPartition->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPartitionCallbackWithKey($collection) { $data = new $collection(['zero', 'one', 'two', 'three']); @@ -4976,9 +4445,7 @@ public function testPartitionCallbackWithKey($collection) $this->assertEquals(['one', 'three'], $odd->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPartitionByKey($collection) { $courses = new $collection([ @@ -4991,9 +4458,7 @@ public function testPartitionByKey($collection) $this->assertSame([['free' => false, 'title' => 'Premium']], $premium->values()->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPartitionWithOperators($collection) { $data = new $collection([ @@ -5028,9 +4493,7 @@ public function testPartitionWithOperators($collection) ], $minors->values()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPartitionPreservesKeys($collection) { $courses = new $collection([ @@ -5043,9 +4506,7 @@ public function testPartitionPreservesKeys($collection) $this->assertSame(['b' => ['free' => false]], $premium->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPartitionEmptyCollection($collection) { $data = new $collection; @@ -5055,9 +4516,7 @@ public function testPartitionEmptyCollection($collection) })); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderPartition($collection) { $courses = new $collection([ @@ -5071,9 +4530,7 @@ public function testHigherOrderPartition($collection) $this->assertSame(['b' => ['free' => false]], $premium->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testTap($collection) { $data = new $collection([1, 2, 3]); @@ -5090,9 +4547,7 @@ public function testTap($collection) $this->assertSame([1, 2, 3], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhen($collection) { $data = new $collection(['michael', 'tom']); @@ -5112,9 +4567,7 @@ public function testWhen($collection) $this->assertSame(['michael', 'tom'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhenDefault($collection) { $data = new $collection(['michael', 'tom']); @@ -5128,9 +4581,7 @@ public function testWhenDefault($collection) $this->assertSame(['michael', 'tom', 'taylor'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhenEmpty($collection) { $data = new $collection(['michael', 'tom']); @@ -5150,9 +4601,7 @@ public function testWhenEmpty($collection) $this->assertSame(['adam'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhenEmptyDefault($collection) { $data = new $collection(['michael', 'tom']); @@ -5166,9 +4615,7 @@ public function testWhenEmptyDefault($collection) $this->assertSame(['michael', 'tom', 'taylor'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhenNotEmpty($collection) { $data = new $collection(['michael', 'tom']); @@ -5188,9 +4635,7 @@ public function testWhenNotEmpty($collection) $this->assertSame([], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhenNotEmptyDefault($collection) { $data = new $collection(['michael', 'tom']); @@ -5204,9 +4649,7 @@ public function testWhenNotEmptyDefault($collection) $this->assertSame(['michael', 'tom', 'adam'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderWhenAndUnless($collection) { $data = new $collection(['michael', 'tom']); @@ -5228,9 +4671,7 @@ public function testHigherOrderWhenAndUnless($collection) $this->assertSame(['michael', 'tom', 'chris', 'adam'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHigherOrderWhenAndUnlessWithProxy($collection) { $data = new $collection(['michael', 'tom']); @@ -5252,9 +4693,7 @@ public function testHigherOrderWhenAndUnlessWithProxy($collection) $this->assertSame(['michael', 'tom', 'chris', 'adam'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnless($collection) { $data = new $collection(['michael', 'tom']); @@ -5274,9 +4713,7 @@ public function testUnless($collection) $this->assertSame(['michael', 'tom'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnlessDefault($collection) { $data = new $collection(['michael', 'tom']); @@ -5290,9 +4727,7 @@ public function testUnlessDefault($collection) $this->assertSame(['michael', 'tom', 'taylor'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnlessEmpty($collection) { $data = new $collection(['michael', 'tom']); @@ -5312,9 +4747,7 @@ public function testUnlessEmpty($collection) $this->assertSame([], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnlessEmptyDefault($collection) { $data = new $collection(['michael', 'tom']); @@ -5328,9 +4761,7 @@ public function testUnlessEmptyDefault($collection) $this->assertSame(['michael', 'tom', 'adam'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnlessNotEmpty($collection) { $data = new $collection(['michael', 'tom']); @@ -5350,9 +4781,7 @@ public function testUnlessNotEmpty($collection) $this->assertSame(['adam'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUnlessNotEmptyDefault($collection) { $data = new $collection(['michael', 'tom']); @@ -5366,9 +4795,7 @@ public function testUnlessNotEmptyDefault($collection) $this->assertSame(['michael', 'tom', 'taylor'], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testHasReturnsValidResults($collection) { $data = new $collection(['foo' => 'one', 'bar' => 'two', 1 => 'three']); @@ -5390,9 +4817,7 @@ public function testPutAddsItemToCollection() $this->assertSame(['foo' => 3, 'bar' => ['nested' => 'two']], $data->toArray()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testItThrowsExceptionWhenTryingToAccessNoProxyProperty($collection) { $data = new $collection; @@ -5401,27 +4826,21 @@ public function testItThrowsExceptionWhenTryingToAccessNoProxyProperty($collecti $data->foo; } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGetWithNullReturnsNull($collection) { $data = new $collection([1, 2, 3]); $this->assertNull($data->get(null)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGetWithDefaultValue($collection) { $data = new $collection(['name' => 'taylor', 'framework' => 'laravel']); $this->assertEquals('34', $data->get('age', 34)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testGetWithCallbackAsDefaultValue($collection) { $data = new $collection(['name' => 'taylor', 'framework' => 'laravel']); @@ -5431,9 +4850,7 @@ public function testGetWithCallbackAsDefaultValue($collection) $this->assertEquals('taylor@example.com', $result); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNull($collection) { $data = new $collection([ @@ -5451,9 +4868,7 @@ public function testWhereNull($collection) $this->assertSame([], $data->whereNull()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNullWithoutKey($collection) { $collection = new $collection([1, null, 3, 'null', false, true]); @@ -5462,9 +4877,7 @@ public function testWhereNullWithoutKey($collection) ], $collection->whereNull()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNotNull($collection) { $data = new $collection($originalData = [ @@ -5485,9 +4898,7 @@ public function testWhereNotNull($collection) $this->assertSame($originalData, $data->whereNotNull()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testWhereNotNullWithoutKey($collection) { $data = new $collection([1, null, 3, 'null', false, true]); @@ -5501,9 +4912,7 @@ public function testWhereNotNullWithoutKey($collection) ], $data->whereNotNull()->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testCollect($collection) { $data = $collection::make([ @@ -5521,9 +4930,7 @@ public function testCollect($collection) ], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testUndot($collection) { $data = $collection::make([ @@ -5557,9 +4964,7 @@ public function testUndot($collection) ], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testDot($collection) { $data = $collection::make([ @@ -5593,9 +4998,7 @@ public function testDot($collection) ], $data->all()); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEnsureForScalar($collection) { $data = $collection::make([1, 2, 3]); @@ -5606,9 +5009,7 @@ public function testEnsureForScalar($collection) $data->ensure('int'); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEnsureForObjects($collection) { $data = $collection::make([new stdClass, new stdClass, new stdClass]); @@ -5619,9 +5020,7 @@ public function testEnsureForObjects($collection) $data->ensure(stdClass::class); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testEnsureForInheritance($collection) { $data = $collection::make([new \Error, new \Error]); @@ -5632,9 +5031,7 @@ public function testEnsureForInheritance($collection) $data->ensure(\Throwable::class); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPercentageWithFlatCollection($collection) { $collection = new $collection([1, 1, 2, 2, 2, 3]); @@ -5645,9 +5042,7 @@ public function testPercentageWithFlatCollection($collection) $this->assertSame(0.0, $collection->percentage(fn ($value) => $value === 5)); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] public function testPercentageWithNestedCollection($collection) { $collection = new $collection([ @@ -5663,9 +5058,20 @@ public function testPercentageWithNestedCollection($collection) $this->assertSame(0.0, $collection->percentage(fn ($value) => $value['foo'] === 'test')); } - /** - * @dataProvider collectionClassProvider - */ + #[DataProvider('collectionClassProvider')] + public function testHighOrderPercentage($collection) + { + $collection = new $collection([ + ['name' => 'Taylor', 'active' => true], + ['name' => 'Nuno', 'active' => true], + ['name' => 'Dries', 'active' => false], + ['name' => 'Jess', 'active' => true], + ]); + + $this->assertSame(75.00, $collection->percentage->active); + } + + #[DataProvider('collectionClassProvider')] public function testPercentageReturnsNullForEmptyCollections($collection) { $collection = new $collection([]); diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index b91fa854e43b..c0fddec3ec76 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -15,6 +15,7 @@ use IteratorAggregate; use LogicException; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionClass; use RuntimeException; @@ -1045,9 +1046,7 @@ public static function providesPregReplaceArrayData() ]; } - /** - * @dataProvider providesPregReplaceArrayData - */ + #[DataProvider('providesPregReplaceArrayData')] public function testPregReplaceArray($pattern, $replacements, $subject, $expectedOutput) { $this->assertSame( diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index 9e676b1ba229..a2f6e47457de 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -4,6 +4,7 @@ use Exception; use Illuminate\Support\Str; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use Ramsey\Uuid\UuidInterface; use ReflectionClass; @@ -203,6 +204,7 @@ public function testStrExcerpt() $this->assertSame('...ocê e...', Str::excerpt('Como você está', 'Ê', ['radius' => 2])); $this->assertSame('João...', Str::excerpt('João Antônio ', 'jo', ['radius' => 2])); $this->assertSame('João Antô...', Str::excerpt('João Antônio', 'JOÃO', ['radius' => 5])); + $this->assertNull(Str::excerpt('', '/')); } public function testStrBefore() @@ -286,22 +288,41 @@ public function testStrAfterLast() $this->assertSame('foo', Str::afterLast('----foo', '---')); } - /** - * @dataProvider strContainsProvider - */ + #[DataProvider('strContainsProvider')] public function testStrContains($haystack, $needles, $expected, $ignoreCase = false) { $this->assertEquals($expected, Str::contains($haystack, $needles, $ignoreCase)); } - /** - * @dataProvider strContainsAllProvider - */ + #[DataProvider('strContainsAllProvider')] public function testStrContainsAll($haystack, $needles, $expected, $ignoreCase = false) { $this->assertEquals($expected, Str::containsAll($haystack, $needles, $ignoreCase)); } + public function testConvertCase() + { + // Upper Case Conversion + $this->assertSame('HELLO', Str::convertCase('hello', MB_CASE_UPPER)); + $this->assertSame('WORLD', Str::convertCase('WORLD', MB_CASE_UPPER)); + + // Lower Case Conversion + $this->assertSame('hello', Str::convertCase('HELLO', MB_CASE_LOWER)); + $this->assertSame('world', Str::convertCase('WORLD', MB_CASE_LOWER)); + + // Case Folding + $this->assertSame('hello', Str::convertCase('HeLLo', MB_CASE_FOLD)); + $this->assertSame('world', Str::convertCase('WoRLD', MB_CASE_FOLD)); + + // Multi-byte String + $this->assertSame('ÜÖÄ', Str::convertCase('üöä', MB_CASE_UPPER, 'UTF-8')); + $this->assertSame('üöä', Str::convertCase('ÜÖÄ', MB_CASE_LOWER, 'UTF-8')); + + // Unsupported Mode + $this->expectException(\ValueError::class); + Str::convertCase('Hello', -1); + } + public function testParseCallback() { $this->assertEquals(['Class', 'method'], Str::parseCallback('Class@method', 'foo')); @@ -411,17 +432,13 @@ public function testIsUrl() $this->assertFalse(Str::isUrl('invalid url')); } - /** - * @dataProvider validUuidList - */ + #[DataProvider('validUuidList')] public function testIsUuidWithValidUuid($uuid) { $this->assertTrue(Str::isUuid($uuid)); } - /** - * @dataProvider invalidUuidList - */ + #[DataProvider('invalidUuidList')] public function testIsUuidWithInvalidUuid($uuid) { $this->assertFalse(Str::isUuid($uuid)); @@ -520,8 +537,7 @@ public function testRandom() $this->assertIsString(Str::random()); } - /** @test */ - public function TestWhetherTheNumberOfGeneratedCharactersIsEquallyDistributed() + public function testWhetherTheNumberOfGeneratedCharactersIsEquallyDistributed() { $results = []; // take 6.200.000 samples, because there are 62 different characters @@ -605,6 +621,8 @@ public function testReplaceArray() // Test for associative array support $this->assertSame('foo/bar', Str::replaceArray('?', [1 => 'foo', 2 => 'bar'], '?/?')); $this->assertSame('foo/bar', Str::replaceArray('?', ['x' => 'foo', 'y' => 'bar'], '?/?')); + // Test does not crash on bad input + $this->assertSame('?', Str::replaceArray('?', [(object) ['foo' => 'bar']], '?')); } public function testReplaceFirst() @@ -862,6 +880,11 @@ public function testTake() { $this->assertSame('ab', Str::take('abcdef', 2)); $this->assertSame('ef', Str::take('abcdef', -2)); + $this->assertSame('', Str::take('abcdef', 0)); + $this->assertSame('', Str::take('', 2)); + $this->assertSame('abcdef', Str::take('abcdef', 10)); + $this->assertSame('abcdef', Str::take('abcdef', 6)); + $this->assertSame('ü', Str::take('üöä', 1)); } public function testLcfirst() @@ -1051,9 +1074,7 @@ public function testRepeat() $this->assertSame('', Str::repeat('', 5)); } - /** - * @dataProvider specialCharacterProvider - */ + #[DataProvider('specialCharacterProvider')] public function testTransliterate(string $value, string $expected): void { $this->assertSame($expected, Str::transliterate($value)); @@ -1079,9 +1100,7 @@ public function testTransliterateOverrideUnknown(): void $this->assertSame('Hello', Str::transliterate('🎂', 'Hello')); } - /** - * @dataProvider specialCharacterProvider - */ + #[DataProvider('specialCharacterProvider')] public function testTransliterateStrict(string $value, string $expected): void { $this->assertSame($expected, Str::transliterate($value, '?', true)); @@ -1297,6 +1316,13 @@ public function testItCanSpecifyAFallbackForAUlidSequence() public function testPasswordCreation() { $this->assertTrue(strlen(Str::password()) === 32); + + $this->assertStringNotContainsString(' ', Str::password()); + $this->assertStringContainsString(' ', Str::password(spaces: true)); + + $this->assertTrue( + Str::of(Str::password())->contains(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']) + ); } } diff --git a/tests/Support/SupportStringableTest.php b/tests/Support/SupportStringableTest.php index 512be61a0dc7..6dd9b3b5b97c 100644 --- a/tests/Support/SupportStringableTest.php +++ b/tests/Support/SupportStringableTest.php @@ -1187,6 +1187,21 @@ public function testStripTags() $this->assertSame('before
after', (string) $this->stringable('before
after')->stripTags('
')); } + public function testReplaceMatches() + { + $stringable = $this->stringable('Hello world!'); + $result = $stringable->replaceMatches('/world/', function ($match) { + return strtoupper($match[0]); + }); + + $this->assertSame('Hello WORLD!', $result->value); + + $stringable = $this->stringable('apple orange apple'); + $result = $stringable->replaceMatches('/apple/', 'fruit', 1); + + $this->assertSame('fruit orange apple', $result->value); + } + public function testScan() { $this->assertSame([123456], $this->stringable('SN/123456')->scan('SN/%d')->toArray()); diff --git a/tests/Testing/Concerns/TestDatabasesTest.php b/tests/Testing/Concerns/TestDatabasesTest.php index 5e6e9e19956f..e5e1f110d884 100644 --- a/tests/Testing/Concerns/TestDatabasesTest.php +++ b/tests/Testing/Concerns/TestDatabasesTest.php @@ -7,6 +7,7 @@ use Illuminate\Support\Facades\DB; use Illuminate\Testing\Concerns\TestDatabases; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; use ReflectionMethod; @@ -46,9 +47,7 @@ public function testSwitchToDatabaseWithoutUrl() $this->switchToDatabase('my_database_test_1'); } - /** - * @dataProvider databaseUrls - */ + #[DataProvider('databaseUrls')] public function testSwitchToDatabaseWithUrl($testDatabase, $url, $testUrl) { DB::shouldReceive('purge')->once(); diff --git a/tests/Testing/ParallelTestingTest.php b/tests/Testing/ParallelTestingTest.php index 521cf16cfb15..ba3853d67ddc 100644 --- a/tests/Testing/ParallelTestingTest.php +++ b/tests/Testing/ParallelTestingTest.php @@ -4,6 +4,7 @@ use Illuminate\Container\Container; use Illuminate\Testing\ParallelTesting; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ParallelTestingTest extends TestCase @@ -17,9 +18,7 @@ protected function setUp(): void $_SERVER['LARAVEL_PARALLEL_TESTING'] = 1; } - /** - * @dataProvider callbacks - */ + #[DataProvider('callbacks')] public function testCallbacks($callback) { $parallelTesting = new ParallelTesting(Container::getInstance()); diff --git a/tests/Translation/TranslationMessageSelectorTest.php b/tests/Translation/TranslationMessageSelectorTest.php index cd53a7de17f0..5aae067445a9 100755 --- a/tests/Translation/TranslationMessageSelectorTest.php +++ b/tests/Translation/TranslationMessageSelectorTest.php @@ -3,13 +3,12 @@ namespace Illuminate\Tests\Translation; use Illuminate\Translation\MessageSelector; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class TranslationMessageSelectorTest extends TestCase { - /** - * @dataProvider chooseTestData - */ + #[DataProvider('chooseTestData')] public function testChoose($expected, $id, $number) { $selector = new MessageSelector; diff --git a/tests/Validation/ValidationExceptionTest.php b/tests/Validation/ValidationExceptionTest.php index 7fcf266ae3d0..2fbec79f5a53 100755 --- a/tests/Validation/ValidationExceptionTest.php +++ b/tests/Validation/ValidationExceptionTest.php @@ -87,11 +87,26 @@ public function testExceptionGetResponseOneError() $this->assertNull($exception->getResponse()); } + public function testGetExceptionClassFromValidator() + { + $validator = $this->getValidator(); + + $exception = $validator->getException(); + + $this->assertEquals(ValidationException::class, $exception); + } + protected function getException($data = [], $rules = []) { - $translator = new Translator(new ArrayLoader, 'en'); - $validator = new Validator($translator, $data, $rules); + $validator = $this->getValidator($data, $rules); return new ValidationException($validator); } + + protected function getValidator($data = [], $rules = []) + { + $translator = new Translator(new ArrayLoader, 'en'); + + return new Validator($translator, $data, $rules); + } } diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 51133741082d..a57688f81baf 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -31,6 +31,8 @@ use InvalidArgumentException; use Mockery as m; use Mockery\MockInterface; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\TestCase; use RuntimeException; use stdClass; @@ -1663,7 +1665,7 @@ public function testProhibits() $this->assertFalse($v->messages()->has('foo.1.email')); } - /** @dataProvider prohibitedRulesData */ + #[DataProvider('prohibitedRulesData')] public function testProhibitedRulesAreConsistent($rules, $data, $result) { $trans = $this->getIlluminateArrayTranslator(); @@ -3226,9 +3228,8 @@ public function testValidateMax() * @param mixed $input * @param mixed $allowed * @param bool $passes - * - * @dataProvider multipleOfDataProvider */ + #[DataProvider('multipleOfDataProvider')] public function testValidateMultipleOf($input, $allowed, $passes) { $trans = $this->getIlluminateArrayTranslator(); @@ -3964,9 +3965,7 @@ public function testValidateUrlWithProtocols() $this->assertFalse($v->passes()); } - /** - * @dataProvider validUrls - */ + #[DataProvider('validUrls')] public function testValidateUrlWithValidUrls($validUrl) { $trans = $this->getIlluminateArrayTranslator(); @@ -3974,9 +3973,7 @@ public function testValidateUrlWithValidUrls($validUrl) $this->assertTrue($v->passes()); } - /** - * @dataProvider invalidUrls - */ + #[DataProvider('invalidUrls')] public function testValidateUrlWithInvalidUrls($invalidUrl) { $trans = $this->getIlluminateArrayTranslator(); @@ -4248,9 +4245,7 @@ public static function invalidUrls() ]; } - /** - * @dataProvider activeUrlDataProvider - */ + #[DataProvider('activeUrlDataProvider')] public function testValidateActiveUrl($data, $outcome) { $trans = $this->getIlluminateArrayTranslator(); @@ -4556,9 +4551,7 @@ public function testValidateMimeEnforcesPhpCheck() $this->assertTrue($v->passes()); } - /** - * @requires extension fileinfo - */ + #[RequiresPhpExtension('fileinfo')] public function testValidateFile() { $trans = $this->getIlluminateArrayTranslator(); @@ -7807,9 +7800,7 @@ public function testMultiplePassesCalls() $this->assertFalse($v->passes()); } - /** - * @dataProvider validUuidList - */ + #[DataProvider('validUuidList')] public function testValidateWithValidUuid($uuid) { $trans = $this->getIlluminateArrayTranslator(); @@ -7817,9 +7808,7 @@ public function testValidateWithValidUuid($uuid) $this->assertTrue($v->passes()); } - /** - * @dataProvider invalidUuidList - */ + #[DataProvider('invalidUuidList')] public function testValidateWithInvalidUuid($uuid) { $trans = $this->getIlluminateArrayTranslator(); @@ -8097,9 +8086,7 @@ public static function providesPassingExcludeIfData() ]; } - /** - * @dataProvider providesPassingExcludeIfData - */ + #[DataProvider('providesPassingExcludeIfData')] public function testExcludeIf($rules, $data, $expectedValidatedData) { $validator = new Validator( @@ -8212,9 +8199,7 @@ public static function providesFailingExcludeIfData() ]; } - /** - * @dataProvider providesFailingExcludeIfData - */ + #[DataProvider('providesFailingExcludeIfData')] public function testExcludeIfWhenValidationFails($rules, $data, $expectedMessages) { $validator = new Validator( @@ -8254,9 +8239,7 @@ public static function providesPassingExcludeData() ]; } - /** - * @dataProvider providesPassingExcludeData - */ + #[DataProvider('providesPassingExcludeData')] public function testExclude($rules, $data, $expectedValidatedData) { $validator = new Validator( @@ -8824,7 +8807,7 @@ public function testItTrimsSpaceFromParameters() ], $validator->messages()->keys()); } - /** @dataProvider outsideRangeExponents */ + #[DataProvider('outsideRangeExponents')] public function testItLimitsLengthOfScientificNotationExponent($value) { $trans = $this->getIlluminateArrayTranslator(); @@ -8848,7 +8831,7 @@ public static function outsideRangeExponents() ]; } - /** @dataProvider withinRangeExponents */ + #[DataProvider('withinRangeExponents')] public function testItAllowsScientificNotationWithinRange($value, $rule) { $trans = $this->getIlluminateArrayTranslator(); diff --git a/tests/View/Blade/BladeEchoHandlerTest.php b/tests/View/Blade/BladeEchoHandlerTest.php index 16ac1aaea354..55e48b005c6a 100644 --- a/tests/View/Blade/BladeEchoHandlerTest.php +++ b/tests/View/Blade/BladeEchoHandlerTest.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Support\Fluent; use Illuminate\Support\Str; +use PHPUnit\Framework\Attributes\DataProvider; class BladeEchoHandlerTest extends AbstractBladeTestCase { @@ -49,9 +50,7 @@ public function testWhitespaceIsPreservedCorrectly() ); } - /** - * @dataProvider handlerLogicDataProvider - */ + #[DataProvider('handlerLogicDataProvider')] public function testHandlerLogicWorksCorrectly($blade) { $this->expectException(Exception::class); @@ -80,9 +79,7 @@ public static function handlerLogicDataProvider() ]; } - /** - * @dataProvider nonStringableDataProvider - */ + #[DataProvider('nonStringableDataProvider')] public function testHandlerWorksWithNonStringables($blade, $expectedOutput) { app()->singleton('blade.compiler', function () { diff --git a/tests/View/Blade/BladeForeachStatementsTest.php b/tests/View/Blade/BladeForeachStatementsTest.php index 1b4ce8ada8f3..80ba9736767b 100644 --- a/tests/View/Blade/BladeForeachStatementsTest.php +++ b/tests/View/Blade/BladeForeachStatementsTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\View\Blade; use Illuminate\Contracts\View\ViewCompilationException; +use PHPUnit\Framework\Attributes\DataProvider; class BladeForeachStatementsTest extends AbstractBladeTestCase { @@ -94,9 +95,7 @@ public function testLoopContentHolderIsExtractedFromForeachStatements() $this->assertEquals($expected, $this->compiler->compileString($string)); } - /** - * @dataProvider invalidForeachStatementsDataProvider - */ + #[DataProvider('invalidForeachStatementsDataProvider')] public function testForeachStatementsThrowHumanizedMessageWhenInvalidStatement($initialStatement) { $this->expectException(ViewCompilationException::class); diff --git a/tests/View/Blade/BladeForelseStatementsTest.php b/tests/View/Blade/BladeForelseStatementsTest.php index 3fb033b4e964..67e0cd38f355 100644 --- a/tests/View/Blade/BladeForelseStatementsTest.php +++ b/tests/View/Blade/BladeForelseStatementsTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\View\Blade; use Illuminate\Contracts\View\ViewCompilationException; +use PHPUnit\Framework\Attributes\DataProvider; class BladeForelseStatementsTest extends AbstractBladeTestCase { @@ -80,9 +81,7 @@ public function testNestedForelseStatementsAreCompiled() $this->assertEquals($expected, $this->compiler->compileString($string)); } - /** - * @dataProvider invalidForelseStatementsDataProvider - */ + #[DataProvider('invalidForelseStatementsDataProvider')] public function testForelseStatementsThrowHumanizedMessageWhenInvalidStatement($initialStatement) { $this->expectException(ViewCompilationException::class); diff --git a/tests/View/ComponentTest.php b/tests/View/ComponentTest.php index 18318f0038a5..a5ff36d82531 100644 --- a/tests/View/ComponentTest.php +++ b/tests/View/ComponentTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\View; +use Closure; use Illuminate\Config\Repository as Config; use Illuminate\Container\Container; use Illuminate\Contracts\Container\BindingResolutionException; @@ -72,6 +73,42 @@ public function testRegularViewsGetReturnedUsingViewHelper() $this->assertSame($view, $component->resolveView()); } + public function testRenderingStringClosureFromComponent() + { + $this->config->shouldReceive('get')->once()->with('view.compiled')->andReturn('/tmp'); + $this->viewFactory->shouldReceive('exists')->once()->andReturn(false); + $this->viewFactory->shouldReceive('addNamespace')->once()->with('__components', '/tmp'); + + $component = new class() extends Component + { + protected $title; + + public function __construct($title = 'World') + { + $this->title = $title; + } + + public function render() + { + return function (array $data) { + return "

Hello {$this->title}

"; + }; + } + }; + + $closure = $component->resolveView(); + + $viewPath = $closure([]); + + $this->viewFactory->shouldReceive('make')->with($viewPath, [], [])->andReturn('

Hello World

'); + + $this->assertInstanceOf(Closure::class, $closure); + $this->assertSame('__components::9cc08f5001b343c093ee1a396da820dc', $viewPath); + + $hash = str_replace('__components::', '', $viewPath); + $this->assertSame('

Hello World

', file_get_contents("/tmp/{$hash}.blade.php")); + } + public function testRegularViewsGetReturnedUsingViewMethod() { $view = m::mock(View::class); diff --git a/tests/View/ViewBladeCompilerTest.php b/tests/View/ViewBladeCompilerTest.php index b7ef8a4ea75c..c3955945e8ff 100644 --- a/tests/View/ViewBladeCompilerTest.php +++ b/tests/View/ViewBladeCompilerTest.php @@ -6,6 +6,7 @@ use Illuminate\View\Compilers\BladeCompiler; use InvalidArgumentException; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ViewBladeCompilerTest extends TestCase @@ -122,11 +123,10 @@ public function testRawTagsCanBeSetToLegacyValues() } /** - * @dataProvider appendViewPathDataProvider - * * @param string $content * @param string $compiled */ + #[DataProvider('appendViewPathDataProvider')] public function testIncludePathToTemplate($content, $compiled) { $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__); diff --git a/tests/View/ViewFileViewFinderTest.php b/tests/View/ViewFileViewFinderTest.php index 80d0ad3f90f8..92f8e954bf9d 100755 --- a/tests/View/ViewFileViewFinderTest.php +++ b/tests/View/ViewFileViewFinderTest.php @@ -6,6 +6,7 @@ use Illuminate\View\FileViewFinder; use InvalidArgumentException; use Mockery as m; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; class ViewFileViewFinderTest extends TestCase @@ -153,9 +154,7 @@ public static function pathsProvider() ]; } - /** - * @dataProvider pathsProvider - */ + #[DataProvider('pathsProvider')] public function testNormalizedPaths($originalPath, $exceptedPath) { $finder = $this->getFinder();