diff --git a/.github/workflows/update-assets.yml b/.github/workflows/update-assets.yml index 8ecd63efce0a..0563172549a1 100644 --- a/.github/workflows/update-assets.yml +++ b/.github/workflows/update-assets.yml @@ -8,6 +8,9 @@ on: - '/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json' workflow_dispatch: +permissions: + contents: write + jobs: update: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ca39ba4620a..55abba5ea7ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.11.1...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.12.0...12.x) + +## [v12.12.0](https://github.com/laravel/framework/compare/v12.11.1...v12.12.0) - 2025-05-01 + +* [12.x] Make Blueprint Resolver Statically by [@finagin](https://github.com/finagin) in https://github.com/laravel/framework/pull/55607 +* [12.x] Allow limiting number of assets to preload by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55618 +* [12.x] Set job instance on "failed" command instance by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55617 ## [v12.11.1](https://github.com/laravel/framework/compare/v12.11.0...v12.11.1) - 2025-04-30 diff --git a/composer.json b/composer.json index 708b0c12c893..8c76eb8b7c06 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ "guzzlehttp/uri-template": "^1.0", "laravel/prompts": "^0.3.0", "laravel/serializable-closure": "^1.3|^2.0", - "league/commonmark": "^2.6", + "league/commonmark": "^2.7", "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", @@ -116,7 +116,7 @@ "php-http/discovery": "^1.15", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", - "predis/predis": "^2.3", + "predis/predis": "^2.3|^3.0", "resend/resend-php": "^0.10.0", "symfony/cache": "^7.2.0", "symfony/http-client": "^7.2.0", @@ -189,7 +189,7 @@ "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", - "predis/predis": "Required to use the predis connector (^2.3).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", diff --git a/src/Illuminate/Bus/Queueable.php b/src/Illuminate/Bus/Queueable.php index c42614d9e65d..b8a439dad2ac 100644 --- a/src/Illuminate/Bus/Queueable.php +++ b/src/Illuminate/Bus/Queueable.php @@ -221,9 +221,11 @@ public function chain($chain) */ public function prependToChain($job) { - $jobs = ChainedBatch::prepareNestedBatches(new Collection([$job])); + $jobs = ChainedBatch::prepareNestedBatches(Collection::wrap($job)); - $this->chained = Arr::prepend($this->chained, $this->serializeJob($jobs->first())); + foreach ($jobs->reverse() as $job) { + $this->chained = Arr::prepend($this->chained, $this->serializeJob($job)); + } return $this; } @@ -236,9 +238,11 @@ public function prependToChain($job) */ public function appendToChain($job) { - $jobs = ChainedBatch::prepareNestedBatches(new Collection([$job])); + $jobs = ChainedBatch::prepareNestedBatches(Collection::wrap($job)); - $this->chained = array_merge($this->chained, [$this->serializeJob($jobs->first())]); + foreach ($jobs as $job) { + $this->chained = array_merge($this->chained, [$this->serializeJob($job)]); + } return $this; } diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 23e1af7bbfee..95faa17a7121 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -712,12 +712,17 @@ public function isEmpty() } /** - * Determine if the collection contains a single item. + * Determine if the collection contains exactly one item. If a callback is provided, determine if exactly one item matches the condition. * + * @param (callable(TValue, TKey): bool)|null $callback * @return bool */ - public function containsOneItem() + public function containsOneItem(?callable $callback = null): bool { + if ($callback) { + return $this->filter($callback)->count() === 1; + } + return $this->count() === 1; } diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index f9f21536d1fc..b7955bd111a9 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -803,7 +803,7 @@ public function whereAttachedTo($related, $relationshipName = null, $boolean = ' $this->has( $relationshipName, boolean: $boolean, - callback: fn (Builder $query) => $query->whereKey($relatedCollection), + callback: fn (Builder $query) => $query->whereKey($relatedCollection->pluck($related->getKeyName())), ); return $this; diff --git a/src/Illuminate/Database/Grammar.php b/src/Illuminate/Database/Grammar.php index d56482dc889b..1d437f0566ce 100755 --- a/src/Illuminate/Database/Grammar.php +++ b/src/Illuminate/Database/Grammar.php @@ -31,8 +31,8 @@ public function __construct(Connection $connection) /** * Wrap an array of values. * - * @param array $values - * @return array + * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $values + * @return array */ public function wrapArray(array $values) { @@ -136,7 +136,7 @@ protected function wrapAliasedTable($value, $prefix = null) /** * Wrap the given value segments. * - * @param array $segments + * @param list $segments * @return string */ protected function wrapSegments($segments) @@ -190,7 +190,7 @@ protected function isJsonSelector($value) /** * Convert an array of column names into a delimited string. * - * @param array $columns + * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns * @return string */ public function columnize(array $columns) @@ -201,7 +201,7 @@ public function columnize(array $columns) /** * Create query parameter place-holders for an array. * - * @param array $values + * @param array $values * @return string */ public function parameterize(array $values) @@ -223,7 +223,7 @@ public function parameter($value) /** * Quote the given string literal. * - * @param string|array $value + * @param string|array $value * @return string */ public function quoteString($value) diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index 1f00b7bd655f..ed13026725ce 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -90,14 +90,17 @@ class Builder implements BuilderContract /** * An aggregate function and column to be run. * - * @var array|null + * @var array{ + * function: string, + * columns: array<\Illuminate\Contracts\Database\Query\Expression|string> + * }|null */ public $aggregate; /** * The columns that should be returned. * - * @var array|null + * @var array|null */ public $columns; @@ -275,7 +278,7 @@ public function __construct( /** * Set the columns to be selected. * - * @param array|mixed $columns + * @param mixed $columns * @return $this */ public function select($columns = ['*']) @@ -429,7 +432,7 @@ protected function prependDatabaseNameIfCrossDatabaseQuery($query) /** * Add a new select column to the query. * - * @param array|mixed $column + * @param mixed $column * @return $this */ public function addSelect($column) @@ -3016,7 +3019,7 @@ public function toRawSql() * Execute a query for a single record by ID. * * @param int|string $id - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @return object|null */ public function find($id, $columns = ['*']) @@ -3030,7 +3033,7 @@ public function find($id, $columns = ['*']) * @template TValue * * @param mixed $id - * @param (\Closure(): TValue)|list|string $columns + * @param (\Closure(): TValue)|string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param (\Closure(): TValue)|null $callback * @return object|TValue */ @@ -3093,7 +3096,7 @@ public function soleValue($column) /** * Execute the query as a "select" statement. * - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @return \Illuminate\Support\Collection */ public function get($columns = ['*']) @@ -3149,7 +3152,7 @@ protected function withoutGroupLimitKeys($items) * Paginate the given query into a simple paginator. * * @param int|\Closure $perPage - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param string $pageName * @param int|null $page * @param \Closure|int|null $total @@ -3177,7 +3180,7 @@ public function paginate($perPage = 15, $columns = ['*'], $pageName = 'page', $p * This is more efficient on larger data-sets, etc. * * @param int $perPage - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param string $pageName * @param int|null $page * @return \Illuminate\Contracts\Pagination\Paginator @@ -3200,7 +3203,7 @@ public function simplePaginate($perPage = 15, $columns = ['*'], $pageName = 'pag * This is more efficient on larger data-sets, etc. * * @param int|null $perPage - * @param array|string $columns + * @param string|\Illuminate\Contracts\Database\Query\Expression|array $columns * @param string $cursorName * @param \Illuminate\Pagination\Cursor|string|null $cursor * @return \Illuminate\Contracts\Pagination\CursorPaginator @@ -3247,7 +3250,7 @@ protected function ensureOrderForCursorPagination($shouldReverse = false) /** * Get the count of the total records for the paginator. * - * @param array $columns + * @param array $columns * @return int */ public function getCountForPagination($columns = ['*']) @@ -3269,8 +3272,8 @@ public function getCountForPagination($columns = ['*']) /** * Run a pagination count query. * - * @param array $columns - * @return array + * @param array $columns + * @return array */ protected function runPaginationCountQuery($columns = ['*']) { @@ -3310,7 +3313,8 @@ protected function cloneForPaginationCount() /** * Remove the column aliases since they will break count queries. * - * @return array + * @param array $columns + * @return array */ protected function withoutSelectAliases(array $columns) { @@ -3653,7 +3657,7 @@ public function numericAggregate($function, $columns = ['*']) * Set the aggregate property without running the query. * * @param string $function - * @param array $columns + * @param array<\Illuminate\Contracts\Database\Query\Expression|string> $columns * @return $this */ protected function setAggregate($function, $columns) @@ -3674,9 +3678,11 @@ protected function setAggregate($function, $columns) * * After running the callback, the columns are reset to the original value. * - * @param array $columns - * @param callable $callback - * @return mixed + * @template TResult + * + * @param array $columns + * @param callable(): TResult $callback + * @return TResult */ protected function onceWithColumns($columns, $callback) { @@ -4078,7 +4084,7 @@ protected function forSubQuery() /** * Get all of the query builder's columns in a text-only array with all expressions evaluated. * - * @return array + * @return list */ public function getColumns() { @@ -4168,7 +4174,7 @@ public function getRawBindings() * Set the bindings on the query builder. * * @param list $bindings - * @param string $type + * @param "select"|"from"|"join"|"where"|"groupBy"|"having"|"order"|"union"|"unionOrder" $type * @return $this * * @throws \InvalidArgumentException @@ -4188,7 +4194,7 @@ public function setBindings(array $bindings, $type = 'where') * Add a binding to the query. * * @param mixed $value - * @param string $type + * @param "select"|"from"|"join"|"where"|"groupBy"|"having"|"order"|"union"|"unionOrder" $type * @return $this * * @throws \InvalidArgumentException diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 9b35083af603..ea1c44e00866 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -123,7 +123,7 @@ protected function compileComponents(Builder $query) * Compile an aggregated select clause. * * @param \Illuminate\Database\Query\Builder $query - * @param array $aggregate + * @param array{function: string, columns: array<\Illuminate\Contracts\Database\Query\Expression|string>} $aggregate * @return string */ protected function compileAggregate(Builder $query, $aggregate) diff --git a/src/Illuminate/Events/Dispatcher.php b/src/Illuminate/Events/Dispatcher.php index ee409586efc8..c49a49d30ad7 100755 --- a/src/Illuminate/Events/Dispatcher.php +++ b/src/Illuminate/Events/Dispatcher.php @@ -21,6 +21,8 @@ use Illuminate\Support\Traits\ReflectsClosures; use ReflectionClass; +use function Illuminate\Support\enum_value; + class Dispatcher implements DispatcherContract { use Macroable, ReflectsClosures; @@ -631,8 +633,8 @@ protected function queueHandler($class, $method, $arguments) : $listener->delay ?? null; is_null($delay) - ? $connection->pushOn($queue, $job) - : $connection->laterOn($queue, $delay, $job); + ? $connection->pushOn(enum_value($queue), $job) + : $connection->laterOn(enum_value($queue), $delay, $job); } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 0fed290349c5..91897ea0dd52 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -45,7 +45,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '12.12.0'; + const VERSION = '12.13.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/.gitignore b/src/Illuminate/Foundation/resources/exceptions/renderer/.gitignore new file mode 100644 index 000000000000..07e6e472cc75 --- /dev/null +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json index 45eee2341a84..1935db7d4b58 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package-lock.json @@ -11,7 +11,7 @@ "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "tippy.js": "^6.3.7", - "vite": "^5.4.18", + "vite": "^5.4.19", "vite-require": "^0.2.3" } }, @@ -2106,9 +2106,9 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, "node_modules/vite": { - "version": "5.4.18", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz", - "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==", + "version": "5.4.19", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz", + "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==", "license": "MIT", "dependencies": { "esbuild": "^0.21.3", diff --git a/src/Illuminate/Foundation/resources/exceptions/renderer/package.json b/src/Illuminate/Foundation/resources/exceptions/renderer/package.json index efa13c77fc74..d9f2b42b685e 100644 --- a/src/Illuminate/Foundation/resources/exceptions/renderer/package.json +++ b/src/Illuminate/Foundation/resources/exceptions/renderer/package.json @@ -12,7 +12,7 @@ "postcss": "^8.4.38", "tailwindcss": "^3.4.3", "tippy.js": "^6.3.7", - "vite": "^5.4.18", + "vite": "^5.4.19", "vite-require": "^0.2.3" } } diff --git a/src/Illuminate/Http/Request.php b/src/Illuminate/Http/Request.php index bee09601ef03..d64082868105 100644 --- a/src/Illuminate/Http/Request.php +++ b/src/Illuminate/Http/Request.php @@ -417,7 +417,7 @@ public function get(string $key, mixed $default = null): mixed * * @param string|null $key * @param mixed $default - * @return \Symfony\Component\HttpFoundation\InputBag|mixed + * @return ($key is null ? \Symfony\Component\HttpFoundation\InputBag : mixed) */ public function json($key = null, $default = null) { @@ -633,7 +633,7 @@ public function user($guard = null) * * @param string|null $param * @param mixed $default - * @return \Illuminate\Routing\Route|object|string|null + * @return ($param is null ? \Illuminate\Routing\Route : object|string|null) */ public function route($param = null, $default = null) { diff --git a/src/Illuminate/Log/LogManager.php b/src/Illuminate/Log/LogManager.php index da69039adcb1..270b716f9d8d 100644 --- a/src/Illuminate/Log/LogManager.php +++ b/src/Illuminate/Log/LogManager.php @@ -628,6 +628,10 @@ protected function parseDriver($driver) $driver ??= 'null'; } + if ($driver === null) { + return null; + } + return trim($driver); } diff --git a/src/Illuminate/Mail/composer.json b/src/Illuminate/Mail/composer.json index cf28958fdcad..6f976a9e45dc 100755 --- a/src/Illuminate/Mail/composer.json +++ b/src/Illuminate/Mail/composer.json @@ -20,7 +20,7 @@ "illuminate/contracts": "^12.0", "illuminate/macroable": "^12.0", "illuminate/support": "^12.0", - "league/commonmark": "^2.6", + "league/commonmark": "^2.7", "psr/log": "^1.0|^2.0|^3.0", "symfony/mailer": "^7.2.0", "tijsverkoyen/css-to-inline-styles": "^2.2.5" diff --git a/src/Illuminate/Queue/CallQueuedClosure.php b/src/Illuminate/Queue/CallQueuedClosure.php index 732600ccfea1..34bdf3b85796 100644 --- a/src/Illuminate/Queue/CallQueuedClosure.php +++ b/src/Illuminate/Queue/CallQueuedClosure.php @@ -22,6 +22,13 @@ class CallQueuedClosure implements ShouldQueue */ public $closure; + /** + * The name assigned to the job. + * + * @var string|null + */ + public $name = null; + /** * The callbacks that should be executed on failure. * @@ -105,6 +112,21 @@ public function displayName() { $reflection = new ReflectionFunction($this->closure->getClosure()); - return 'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; + $prefix = is_null($this->name) ? '' : "{$this->name} - "; + + return $prefix.'Closure ('.basename($reflection->getFileName()).':'.$reflection->getStartLine().')'; + } + + /** + * Assign a name to the job. + * + * @param string $name + * @return $this + */ + public function name($name) + { + $this->name = $name; + + return $this; } } diff --git a/src/Illuminate/Queue/Middleware/RateLimited.php b/src/Illuminate/Queue/Middleware/RateLimited.php index 1b4b2b78d941..a2b5343e59db 100644 --- a/src/Illuminate/Queue/Middleware/RateLimited.php +++ b/src/Illuminate/Queue/Middleware/RateLimited.php @@ -25,6 +25,13 @@ class RateLimited */ protected $limiterName; + /** + * The number of seconds before a job should be available again if the limit is exceeded. + * + * @var \DateTimeInterface|int|null + */ + public $releaseAfter; + /** * Indicates if the job should be released if the limit is exceeded. * @@ -89,7 +96,7 @@ protected function handleJob($job, $next, array $limits) foreach ($limits as $limit) { if ($this->limiter->tooManyAttempts($limit->key, $limit->maxAttempts)) { return $this->shouldRelease - ? $job->release($this->getTimeUntilNextRetry($limit->key)) + ? $job->release($this->releaseAfter ?: $this->getTimeUntilNextRetry($limit->key)) : false; } @@ -99,6 +106,19 @@ protected function handleJob($job, $next, array $limits) return $next($job); } + /** + * Set the delay (in seconds) to release the job back to the queue. + * + * @param \DateTimeInterface|int $releaseAfter + * @return $this + */ + public function releaseAfter($releaseAfter) + { + $this->releaseAfter = $releaseAfter; + + return $this; + } + /** * Do not release the job back to the queue if the limit is exceeded. * diff --git a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php index bb87b101d404..25870e08f034 100644 --- a/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php +++ b/src/Illuminate/Queue/Middleware/RateLimitedWithRedis.php @@ -50,7 +50,7 @@ protected function handleJob($job, $next, array $limits) foreach ($limits as $limit) { if ($this->tooManyAttempts($limit->key, $limit->maxAttempts, $limit->decaySeconds)) { return $this->shouldRelease - ? $job->release($this->getTimeUntilNextRetry($limit->key)) + ? $job->release($this->releaseAfter ?: $this->getTimeUntilNextRetry($limit->key)) : false; } } diff --git a/src/Illuminate/Redis/composer.json b/src/Illuminate/Redis/composer.json index 3addbb07ff68..05535a2460b2 100755 --- a/src/Illuminate/Redis/composer.json +++ b/src/Illuminate/Redis/composer.json @@ -27,7 +27,7 @@ }, "suggest": { "ext-redis": "Required to use the phpredis connector (^4.0|^5.0|^6.0).", - "predis/predis": "Required to use the predis connector (^2.3)." + "predis/predis": "Required to use the predis connector (^2.3|^3.0)." }, "extra": { "branch-alias": { diff --git a/src/Illuminate/Support/Facades/Request.php b/src/Illuminate/Support/Facades/Request.php index 8461fa41b1ee..8200fcec7b34 100755 --- a/src/Illuminate/Support/Facades/Request.php +++ b/src/Illuminate/Support/Facades/Request.php @@ -151,7 +151,7 @@ * @method static string|array|null cookie(string|null $key = null, string|array|null $default = null) * @method static array allFiles() * @method static bool hasFile(string $key) - * @method static array|(\Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|null file(string|null $key = null, mixed $default = null) + * @method static array|\Illuminate\Http\UploadedFile|\Illuminate\Http\UploadedFile[]|null file(string|null $key = null, mixed $default = null) * @method static \Illuminate\Http\Request dump(mixed $keys = []) * @method static never dd(mixed ...$args) * @method static bool exists(string|array $key) diff --git a/src/Illuminate/Support/composer.json b/src/Illuminate/Support/composer.json index 4b53abef7e57..404eff091464 100644 --- a/src/Illuminate/Support/composer.json +++ b/src/Illuminate/Support/composer.json @@ -49,7 +49,7 @@ "suggest": { "illuminate/filesystem": "Required to use the Composer class (^12.0).", "laravel/serializable-closure": "Required to use the once function (^1.3|^2.0).", - "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.6).", + "league/commonmark": "Required to use Str::markdown() and Stringable::markdown() (^2.7).", "league/uri": "Required to use the Uri class (^7.5.1).", "ramsey/uuid": "Required to use Str::uuid() (^4.7).", "symfony/process": "Required to use the Composer class (^7.2).", diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 3f6f59a36730..2f3d2ea32f45 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -213,6 +213,23 @@ public function assertRedirectContains($uri) return $this; } + /** + * Assert whether the response is redirecting back to the previous location. + * + * @return $this + */ + public function assertRedirectBack() + { + PHPUnit::withResponse($this)->assertTrue( + $this->isRedirect(), + $this->statusMessageWithDetails('201, 301, 302, 303, 307, 308', $this->getStatusCode()), + ); + + $this->assertLocation(app('url')->previous()); + + return $this; + } + /** * Assert whether the response is redirecting to a given route. * diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 96a6beed7e20..09146044a349 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1282,6 +1282,7 @@ public function testWhereAttachedTo() { $related = new EloquentBuilderTestModelFarRelatedStub; $related->id = 49; + $related->name = 'test'; $builder = EloquentBuilderTestModelParentStub::whereAttachedTo($related, 'roles'); @@ -1292,9 +1293,11 @@ public function testWhereAttachedToCollection() { $model1 = new EloquentBuilderTestModelParentStub; $model1->id = 3; + $model1->name = 'test3'; $model2 = new EloquentBuilderTestModelParentStub; $model2->id = 4; + $model2->name = 'test4'; $builder = EloquentBuilderTestModelFarRelatedStub::whereAttachedTo(new Collection([$model1, $model2]), 'roles'); diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 1d176afa00f1..c27cbfe476b1 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -173,6 +173,7 @@ protected function createSchema() $this->schema($connection)->create('achievements', function ($table) { $table->increments('id'); + $table->integer('status')->nullable(); }); $this->schema($connection)->create('eloquent_test_achievement_eloquent_test_user', function ($table) { @@ -1497,7 +1498,7 @@ public function testWhereAttachedTo() $user1 = EloquentTestUser::create(['email' => 'user1@gmail.com']); $user2 = EloquentTestUser::create(['email' => 'user2@gmail.com']); $user3 = EloquentTestUser::create(['email' => 'user3@gmail.com']); - $achievement1 = EloquentTestAchievement::create(); + $achievement1 = EloquentTestAchievement::create(['status' => 3]); $achievement2 = EloquentTestAchievement::create(); $achievement3 = EloquentTestAchievement::create(); @@ -2988,6 +2989,7 @@ class EloquentTestAchievement extends Eloquent public $timestamps = false; protected $table = 'achievements'; + protected $guarded = []; public function eloquentTestUsers() { diff --git a/tests/Events/QueuedEventsTest.php b/tests/Events/QueuedEventsTest.php index 9724a36bdbd2..4f9ec170ba1f 100644 --- a/tests/Events/QueuedEventsTest.php +++ b/tests/Events/QueuedEventsTest.php @@ -199,6 +199,23 @@ public function testQueuePropagateMiddleware() && $job->middleware[0]->b === 'bar'; }); } + + public function testDispatchesOnQueueDefinedWithEnum() + { + $d = new Dispatcher; + $queue = m::mock(Queue::class); + + $fakeQueue = new QueueFake(new Container); + + $d->setQueueResolver(function () use ($fakeQueue) { + return $fakeQueue; + }); + + $d->listen('some.event', TestDispatcherViaQueueSupportsEnum::class.'@handle'); + $d->dispatch('some.event', ['foo', 'bar']); + + $fakeQueue->assertPushedOn('enumerated-queue', CallQueuedListener::class); + } } class TestDispatcherQueuedHandler implements ShouldQueue @@ -367,3 +384,16 @@ public function withDelay($event) return 20; } } + +enum TestQueueType: string +{ + case EnumeratedQueue = 'enumerated-queue'; +} + +class TestDispatcherViaQueueSupportsEnum implements ShouldQueue +{ + public function viaQueue() + { + return TestQueueType::EnumeratedQueue; + } +} diff --git a/tests/Integration/Cache/FileCacheLockTest.php b/tests/Integration/Cache/FileCacheLockTest.php index c9eff5ced0e9..575f1220b852 100644 --- a/tests/Integration/Cache/FileCacheLockTest.php +++ b/tests/Integration/Cache/FileCacheLockTest.php @@ -3,17 +3,25 @@ namespace Illuminate\Tests\Integration\Cache; use Exception; +use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Sleep; use Orchestra\Testbench\Attributes\WithConfig; use Orchestra\Testbench\TestCase; #[WithConfig('cache.default', 'file')] class FileCacheLockTest extends TestCase { - public function testLocksCanBeAcquiredAndReleased() + protected function setUp(): void { + parent::setUp(); + + // flush lock from previous tests Cache::lock('foo')->forceRelease(); + } + public function testLocksCanBeAcquiredAndReleased() + { $lock = Cache::lock('foo', 10); $this->assertTrue($lock->get()); $this->assertFalse(Cache::lock('foo', 10)->get()); @@ -27,7 +35,6 @@ public function testLocksCanBeAcquiredAndReleased() public function testLocksCanBlockForSeconds() { - Cache::lock('foo')->forceRelease(); $this->assertSame('taylor', Cache::lock('foo', 10)->block(1, function () { return 'taylor'; })); @@ -38,11 +45,11 @@ public function testLocksCanBlockForSeconds() public function testConcurrentLocksAreReleasedSafely() { - Cache::lock('foo')->forceRelease(); + Sleep::fake(syncWithCarbon: true); $firstLock = Cache::lock('foo', 1); $this->assertTrue($firstLock->get()); - sleep(2); + Sleep::for(2)->seconds(); $secondLock = Cache::lock('foo', 10); $this->assertTrue($secondLock->get()); @@ -54,8 +61,6 @@ public function testConcurrentLocksAreReleasedSafely() public function testLocksWithFailedBlockCallbackAreReleased() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); try { @@ -75,8 +80,6 @@ public function testLocksWithFailedBlockCallbackAreReleased() public function testLocksCanBeReleasedUsingOwnerToken() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); $this->assertTrue($firstLock->get()); $owner = $firstLock->owner(); @@ -89,8 +92,6 @@ public function testLocksCanBeReleasedUsingOwnerToken() public function testOwnerStatusCanBeCheckedAfterRestoringLock() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); $this->assertTrue($firstLock->get()); $owner = $firstLock->owner(); @@ -101,8 +102,6 @@ public function testOwnerStatusCanBeCheckedAfterRestoringLock() public function testOtherOwnerDoesNotOwnLockAfterRestore() { - Cache::lock('foo')->forceRelease(); - $firstLock = Cache::lock('foo', 10); $this->assertTrue($firstLock->isOwnedBy(null)); $this->assertTrue($firstLock->get()); @@ -112,4 +111,16 @@ public function testOtherOwnerDoesNotOwnLockAfterRestore() $this->assertTrue($secondLock->isOwnedBy($firstLock->owner())); $this->assertFalse($secondLock->isOwnedByCurrentProcess()); } + + public function testExceptionIfBlockCanNotAcquireLock() + { + Sleep::fake(syncWithCarbon: true); + + // acquire and not release lock + Cache::lock('foo', 10)->get(); + + // try to get lock and hit block timeout + $this->expectException(LockTimeoutException::class); + Cache::lock('foo', 10)->block(5); + } } diff --git a/tests/Integration/Cache/MemoizedStoreTest.php b/tests/Integration/Cache/MemoizedStoreTest.php index 028688b262e2..8ca80eda5135 100644 --- a/tests/Integration/Cache/MemoizedStoreTest.php +++ b/tests/Integration/Cache/MemoizedStoreTest.php @@ -70,7 +70,7 @@ public function test_null_values_are_memoized_when_retrieving_single_value() $this->assertNull($memoized); } - public function test_it_can_memoize_when_retrieving_mulitple_values() + public function test_it_can_memoize_when_retrieving_multiple_values() { Cache::put('name.0', 'Tim', 60); Cache::put('name.1', 'Taylor', 60); @@ -116,7 +116,7 @@ public function test_it_uses_correct_keys_for_getMultiple() $this->assertSame($cacheValue, $memoValue); } - public function test_null_values_are_memoized_when_retrieving_mulitple_values() + public function test_null_values_are_memoized_when_retrieving_multiple_values() { $live = Cache::getMultiple(['name.0', 'name.1']); $memoized = Cache::memo()->getMultiple(['name.0', 'name.1']); @@ -132,7 +132,7 @@ public function test_null_values_are_memoized_when_retrieving_mulitple_values() $this->assertSame($memoized, ['name.0' => null, 'name.1' => null]); } - public function test_it_can_retrieve_already_memoized_and_not_yet_memoized_values_when_retrieving_mulitple_values() + public function test_it_can_retrieve_already_memoized_and_not_yet_memoized_values_when_retrieving_multiple_values() { Cache::put('name.0', 'Tim', 60); Cache::put('name.1', 'Taylor', 60); diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index eddfd83c23c1..441cb59dea97 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -166,6 +166,24 @@ public function testQueueMayBeNullForJobQueueingAndJobQueuedEvent() $this->assertNull($events[3]->queue); } + public function testQueuedClosureCanBeNamed() + { + Config::set('queue.default', 'database'); + $events = []; + $this->app['events']->listen(function (JobQueued $e) use (&$events) { + $events[] = $e; + }); + + dispatch(function () { + // + })->name('custom name'); + + $this->assertCount(1, $events); + $this->assertInstanceOf(JobQueued::class, $events[0]); + $this->assertSame('custom name', $events[0]->job->name); + $this->assertStringContainsString('custom name', $events[0]->job->displayName()); + } + public function testCanDisableDispatchingAfterResponse() { Job::dispatchAfterResponse('test'); diff --git a/tests/Integration/Queue/RateLimitedTest.php b/tests/Integration/Queue/RateLimitedTest.php index 334ec657536e..45116af9e3e8 100644 --- a/tests/Integration/Queue/RateLimitedTest.php +++ b/tests/Integration/Queue/RateLimitedTest.php @@ -138,6 +138,18 @@ public function testJobsCanHaveConditionalRateLimits() $this->assertJobWasReleased(NonAdminTestJob::class); } + public function testRateLimitedJobsCanBeSkippedOnLimitReachedAndReleasedAfter() + { + $rateLimiter = $this->app->make(RateLimiter::class); + + $rateLimiter->for('test', function ($job) { + return Limit::perHour(1); + }); + + $this->assertJobRanSuccessfully(RateLimitedReleaseAfterTestJob::class); + $this->assertJobWasReleasedAfter(RateLimitedReleaseAfterTestJob::class, 60); + } + public function testMiddlewareSerialization() { $rateLimited = new RateLimited('limiterName'); @@ -192,6 +204,25 @@ protected function assertJobWasReleased($class) $this->assertFalse($class::$handled); } + protected function assertJobWasReleasedAfter($class, $releaseAfter) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('release')->once()->withArgs([$releaseAfter]); + $job->shouldReceive('isReleased')->andReturn(true); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertFalse($class::$handled); + } + protected function assertJobWasSkipped($class) { $class::$handled = false; @@ -341,6 +372,14 @@ public function middleware() } } +class RateLimitedReleaseAfterTestJob extends RateLimitedTestJob +{ + public function middleware() + { + return [(new RateLimited('test'))->releaseAfter(60)]; + } +} + enum BackedEnumNamedRateLimited: string { case FOO = 'bar'; diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 2edd11ca6718..7c8104b570ae 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -838,6 +838,10 @@ public function testContainsOneItem($collection) $this->assertFalse((new $collection([]))->containsOneItem()); $this->assertTrue((new $collection([1]))->containsOneItem()); $this->assertFalse((new $collection([1, 2]))->containsOneItem()); + + $this->assertFalse(collect([1, 2, 2])->containsOneItem(fn ($number) => $number === 2)); + $this->assertTrue(collect(['ant', 'bear', 'cat'])->containsOneItem(fn ($word) => strlen($word) === 4)); + $this->assertFalse(collect(['ant', 'bear', 'cat'])->containsOneItem(fn ($word) => strlen($word) > 4)); } public function testIterable() diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index cae497bdd133..32642d0df55c 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -2652,6 +2652,27 @@ public function testAssertRedirect() $response->assertRedirect(); } + public function testAssertRedirectBack() + { + app()->instance('session.store', $store = new Store('test-session', new ArraySessionHandler(1))); + + $store->setPreviousUrl('https://url.com'); + + app('url')->setSessionResolver(fn () => app('session.store')); + + $response = TestResponse::fromBaseResponse( + (new Response('', 302))->withHeaders(['Location' => 'https://url.com']) + ); + + $response->assertRedirectBack(); + + $this->expectException(ExpectationFailedException::class); + + $store->setPreviousUrl('https://url.net'); + + $response->assertRedirectBack(); + } + public function testGetDecryptedCookie() { $response = TestResponse::fromBaseResponse( diff --git a/types/Http/Request.php b/types/Http/Request.php index 340b77bb973f..c95bed31cf70 100644 --- a/types/Http/Request.php +++ b/types/Http/Request.php @@ -14,3 +14,9 @@ enum TestEnum: string ]); assertType('TestEnum|null', $request->enum('key', TestEnum::class)); + +assertType('Illuminate\Routing\Route', $request->route()); +assertType('object|string|null', $request->route('key')); + +assertType('Symfony\Component\HttpFoundation\InputBag', $request->json()); +assertType('mixed', $request->json('key'));