diff --git a/CHANGELOG.md b/CHANGELOG.md index 08670c2ba505..42ef61c3e274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,25 @@ # Release Notes for 11.x -## [Unreleased](https://github.com/laravel/framework/compare/v11.29.0...11.x) +## [Unreleased](https://github.com/laravel/framework/compare/v11.30.0...11.x) + +## [v11.30.0](https://github.com/laravel/framework/compare/v11.29.0...v11.30.0) - 2024-10-30 + +* Add `$bind` parameter to `Blade::directive` by [@hossein-zare](https://github.com/hossein-zare) in https://github.com/laravel/framework/pull/53279 +* [11.x] Fix `trans_choice()` when translation replacement include `|` separator by [@crynobone](https://github.com/crynobone) in https://github.com/laravel/framework/pull/53331 +* [11.x] Allow the authorize method to accept Backed Enums directly by [@johanvanhelden](https://github.com/johanvanhelden) in https://github.com/laravel/framework/pull/53330 +* [11.x] use `exists()` instead of `count()` by [@browner12](https://github.com/browner12) in https://github.com/laravel/framework/pull/53328 +* [11.x] Docblock Improvements by [@mtlukaszczyk](https://github.com/mtlukaszczyk) in https://github.com/laravel/framework/pull/53325 +* Allow for custom Postgres operators to be added by [@boris-glumpler](https://github.com/boris-glumpler) in https://github.com/laravel/framework/pull/53324 +* [11.x] Support Optional Dimensions for `vector` Column Type by [@akr4m](https://github.com/akr4m) in https://github.com/laravel/framework/pull/53316 +* [11.x] Test Improvements by [@saMahmoudzadeh](https://github.com/saMahmoudzadeh) in https://github.com/laravel/framework/pull/53306 +* [11.x] Added `dropColumnsIfExists`, `dropColumnIfExists` and `dropForeignIfExists` by [@eusonlito](https://github.com/eusonlito) in https://github.com/laravel/framework/pull/53305 +* [11.x] Provide an error message for PostTooLargeException by [@patrickomeara](https://github.com/patrickomeara) in https://github.com/laravel/framework/pull/53301 +* [11.x] Fix integrity constraint violation on failed_jobs_uuid_unique by [@bytestream](https://github.com/bytestream) in https://github.com/laravel/framework/pull/53264 +* Revert "[11.x] Added `dropColumnsIfExists`, `dropColumnIfExists` and `dropForeignIfExists`" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/53338 +* [11.x] Introduce `HasUniqueStringIds` by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/53280 +* [11.x] Refactor: check for contextual attribute before getting parameter class name by [@korkoshko](https://github.com/korkoshko) in https://github.com/laravel/framework/pull/53339 +* [11.x] Pick up existing views and markdowns when creating mails by [@kevinb1989](https://github.com/kevinb1989) in https://github.com/laravel/framework/pull/53308 +* [11.x] Add withoutDefer and withDefer testing helpers by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/53340 ## [v11.29.0](https://github.com/laravel/framework/compare/v11.28.1...v11.29.0) - 2024-10-22 diff --git a/src/Illuminate/Bus/PendingBatch.php b/src/Illuminate/Bus/PendingBatch.php index ad39b41923eb..53ebdd65296c 100644 --- a/src/Illuminate/Bus/PendingBatch.php +++ b/src/Illuminate/Bus/PendingBatch.php @@ -12,6 +12,8 @@ use Laravel\SerializableClosure\SerializableClosure; use Throwable; +use function Illuminate\Support\enum_value; + class PendingBatch { use Conditionable; @@ -261,12 +263,12 @@ public function connection() /** * Specify the queue that the batched jobs should run on. * - * @param string $queue + * @param \BackedEnum|string|null $queue * @return $this */ - public function onQueue(string $queue) + public function onQueue($queue) { - $this->options['queue'] = $queue; + $this->options['queue'] = enum_value($queue); return $this; } diff --git a/src/Illuminate/Database/Concerns/ManagesTransactions.php b/src/Illuminate/Database/Concerns/ManagesTransactions.php index ce0342ec5010..e7ac09423d16 100644 --- a/src/Illuminate/Database/Concerns/ManagesTransactions.php +++ b/src/Illuminate/Database/Concerns/ManagesTransactions.php @@ -10,11 +10,13 @@ trait ManagesTransactions { /** + * @template TReturn of mixed + * * Execute a Closure within a transaction. * - * @param \Closure $callback + * @param (\Closure(static): TReturn) $callback * @param int $attempts - * @return mixed + * @return TReturn * * @throws \Throwable */ diff --git a/src/Illuminate/Database/Console/Migrations/RefreshCommand.php b/src/Illuminate/Database/Console/Migrations/RefreshCommand.php index 3fd1083a7524..7d74f5b38c0c 100755 --- a/src/Illuminate/Database/Console/Migrations/RefreshCommand.php +++ b/src/Illuminate/Database/Console/Migrations/RefreshCommand.php @@ -38,7 +38,7 @@ public function handle() { if ($this->isProhibited() || ! $this->confirmToProceed()) { - return 1; + return Command::FAILURE; } // Next we'll gather some of the options so that we can have the right options diff --git a/src/Illuminate/Database/Console/Migrations/ResetCommand.php b/src/Illuminate/Database/Console/Migrations/ResetCommand.php index 695da444b1d1..85ccae9734e0 100755 --- a/src/Illuminate/Database/Console/Migrations/ResetCommand.php +++ b/src/Illuminate/Database/Console/Migrations/ResetCommand.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Console\Migrations; +use Illuminate\Console\Command; use Illuminate\Console\ConfirmableTrait; use Illuminate\Console\Prohibitable; use Illuminate\Database\Migrations\Migrator; @@ -56,7 +57,7 @@ public function handle() { if ($this->isProhibited() || ! $this->confirmToProceed()) { - return 1; + return Command::FAILURE; } return $this->migrator->usingConnection($this->option('database'), function () { diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasUlids.php b/src/Illuminate/Database/Eloquent/Concerns/HasUlids.php index 85a810db5ee1..344f97338aa1 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasUlids.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasUlids.php @@ -2,33 +2,14 @@ namespace Illuminate\Database\Eloquent\Concerns; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Str; trait HasUlids { - /** - * Initialize the trait. - * - * @return void - */ - public function initializeHasUlids() - { - $this->usesUniqueIds = true; - } + use HasUniqueStringIds; /** - * Get the columns that should receive a unique identifier. - * - * @return array - */ - public function uniqueIds() - { - return [$this->getKeyName()]; - } - - /** - * Generate a new ULID for the model. + * Generate a new unique key for the model. * * @return string */ @@ -38,53 +19,13 @@ public function newUniqueId() } /** - * Retrieve the model for a bound value. + * Determine if given key is valid. * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $query * @param mixed $value - * @param string|null $field - * @return \Illuminate\Contracts\Database\Eloquent\Builder - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function resolveRouteBindingQuery($query, $value, $field = null) - { - if ($field && in_array($field, $this->uniqueIds()) && ! Str::isUlid($value)) { - throw (new ModelNotFoundException)->setModel(get_class($this), $value); - } - - if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! Str::isUlid($value)) { - throw (new ModelNotFoundException)->setModel(get_class($this), $value); - } - - return parent::resolveRouteBindingQuery($query, $value, $field); - } - - /** - * Get the auto-incrementing key type. - * - * @return string - */ - public function getKeyType() - { - if (in_array($this->getKeyName(), $this->uniqueIds())) { - return 'string'; - } - - return $this->keyType; - } - - /** - * Get the value indicating whether the IDs are incrementing. - * * @return bool */ - public function getIncrementing() + protected function isValidUniqueId($value): bool { - if (in_array($this->getKeyName(), $this->uniqueIds())) { - return false; - } - - return $this->incrementing; + return Str::isUlid($value); } } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasUniqueStringIds.php b/src/Illuminate/Database/Eloquent/Concerns/HasUniqueStringIds.php new file mode 100644 index 000000000000..ae86c43042d5 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Concerns/HasUniqueStringIds.php @@ -0,0 +1,94 @@ +usesUniqueIds = true; + } + + /** + * Get the columns that should receive a unique identifier. + * + * @return array + */ + public function uniqueIds() + { + return [$this->getKeyName()]; + } + + /** + * Retrieve the model for a bound value. + * + * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $query + * @param mixed $value + * @param string|null $field + * @return \Illuminate\Contracts\Database\Eloquent\Builder + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function resolveRouteBindingQuery($query, $value, $field = null) + { + if ($field && in_array($field, $this->uniqueIds()) && ! $this->isValidUniqueId($value)) { + throw (new ModelNotFoundException)->setModel(get_class($this), $value); + } + + if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! $this->isValidUniqueId($value)) { + throw (new ModelNotFoundException)->setModel(get_class($this), $value); + } + + return parent::resolveRouteBindingQuery($query, $value, $field); + } + + /** + * Get the auto-incrementing key type. + * + * @return string + */ + public function getKeyType() + { + if (in_array($this->getKeyName(), $this->uniqueIds())) { + return 'string'; + } + + return $this->keyType; + } + + /** + * Get the value indicating whether the IDs are incrementing. + * + * @return bool + */ + public function getIncrementing() + { + if (in_array($this->getKeyName(), $this->uniqueIds())) { + return false; + } + + return $this->incrementing; + } +} diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasUuids.php b/src/Illuminate/Database/Eloquent/Concerns/HasUuids.php index 55d1acfe770e..8d6c35ad89af 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasUuids.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasUuids.php @@ -2,33 +2,14 @@ namespace Illuminate\Database\Eloquent\Concerns; -use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Support\Str; trait HasUuids { - /** - * Initialize the trait. - * - * @return void - */ - public function initializeHasUuids() - { - $this->usesUniqueIds = true; - } + use HasUniqueStringIds; /** - * Get the columns that should receive a unique identifier. - * - * @return array - */ - public function uniqueIds() - { - return [$this->getKeyName()]; - } - - /** - * Generate a new UUID for the model. + * Generate a new unique key for the model. * * @return string */ @@ -38,53 +19,13 @@ public function newUniqueId() } /** - * Retrieve the model for a bound value. + * Determine if given key is valid. * - * @param \Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Relations\Relation<*, *, *> $query * @param mixed $value - * @param string|null $field - * @return \Illuminate\Contracts\Database\Eloquent\Builder - * - * @throws \Illuminate\Database\Eloquent\ModelNotFoundException - */ - public function resolveRouteBindingQuery($query, $value, $field = null) - { - if ($field && in_array($field, $this->uniqueIds()) && ! Str::isUuid($value)) { - throw (new ModelNotFoundException)->setModel(get_class($this), $value); - } - - if (! $field && in_array($this->getRouteKeyName(), $this->uniqueIds()) && ! Str::isUuid($value)) { - throw (new ModelNotFoundException)->setModel(get_class($this), $value); - } - - return parent::resolveRouteBindingQuery($query, $value, $field); - } - - /** - * Get the auto-incrementing key type. - * - * @return string - */ - public function getKeyType() - { - if (in_array($this->getKeyName(), $this->uniqueIds())) { - return 'string'; - } - - return $this->keyType; - } - - /** - * Get the value indicating whether the IDs are incrementing. - * * @return bool */ - public function getIncrementing() + protected function isValidUniqueId($value): bool { - if (in_array($this->getKeyName(), $this->uniqueIds())) { - return false; - } - - return $this->incrementing; + return Str::isUuid($value); } } diff --git a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php index 232c824d1b1f..6e8b64e9b89c 100755 --- a/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/PostgresGrammar.php @@ -22,6 +22,13 @@ class PostgresGrammar extends Grammar 'is distinct from', 'is not distinct from', ]; + /** + * The Postgres grammar specific custom operators. + * + * @var array + */ + protected static $customOperators = []; + /** * The grammar specific bitwise operators. * @@ -31,6 +38,13 @@ class PostgresGrammar extends Grammar '~', '&', '|', '#', '<<', '>>', '<<=', '>>=', ]; + /** + * Indicates if the cascade option should be used when truncating. + * + * @var bool + */ + protected static $cascadeTruncate = true; + /** * Compile a basic where clause. * @@ -646,7 +660,7 @@ protected function compileDeleteWithJoinsOrLimit(Builder $query) */ public function compileTruncate(Builder $query) { - return ['truncate '.$this->wrapTable($query->from).' restart identity cascade' => []]; + return ['truncate '.$this->wrapTable($query->from).' restart identity'.(static::$cascadeTruncate ? ' cascade' : '') => []]; } /** @@ -772,4 +786,38 @@ public function substituteBindingsIntoRawSql($sql, $bindings) return $query; } + + /** + * Get the Postgres grammar specific operators. + * + * @return array + */ + public function getOperators() + { + return array_values(array_unique(array_merge(parent::getOperators(), static::$customOperators))); + } + + /** + * Set any Postgres grammar specific custom operators. + * + * @param array $operators + * @return void + */ + public static function customOperators(array $operators) + { + static::$customOperators = array_values( + array_merge(static::$customOperators, array_filter(array_filter($operators, 'is_string'))) + ); + } + + /** + * Enable or disable the "cascade" option when compiling the truncate statement. + * + * @param bool $value + * @return void + */ + public static function cascadeOnTrucate(bool $value = true) + { + static::$cascadeTruncate = $value; + } } diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 28dc472cf252..0d34270ceffe 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -1456,12 +1456,14 @@ public function computed($column, $expression) * Create a new vector column on the table. * * @param string $column - * @param int $dimensions + * @param int|null $dimensions * @return \Illuminate\Database\Schema\ColumnDefinition */ - public function vector($column, $dimensions) + public function vector($column, $dimensions = null) { - return $this->addColumn('vector', $column, compact('dimensions')); + $options = $dimensions ? compact('dimensions') : []; + + return $this->addColumn('vector', $column, $options); } /** diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index 667a06ebe9f4..0bd6a4b22af0 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -1128,7 +1128,9 @@ protected function typeComputed(Fluent $column) */ protected function typeVector(Fluent $column) { - return "vector($column->dimensions)"; + return isset($column->dimensions) && $column->dimensions !== '' + ? "vector({$column->dimensions})" + : 'vector'; } /** diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index 85b57df62f50..6ec9221ff3d8 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -1078,7 +1078,9 @@ protected function typeGeography(Fluent $column) */ protected function typeVector(Fluent $column) { - return "vector($column->dimensions)"; + return isset($column->dimensions) && $column->dimensions !== '' + ? "vector({$column->dimensions})" + : 'vector'; } /** diff --git a/src/Illuminate/Events/QueuedClosure.php b/src/Illuminate/Events/QueuedClosure.php index 31a462ace41f..8ec63a84711b 100644 --- a/src/Illuminate/Events/QueuedClosure.php +++ b/src/Illuminate/Events/QueuedClosure.php @@ -5,6 +5,8 @@ use Closure; use Laravel\SerializableClosure\SerializableClosure; +use function Illuminate\Support\enum_value; + class QueuedClosure { /** @@ -69,12 +71,12 @@ public function onConnection($connection) /** * Set the desired queue for the job. * - * @param string|null $queue + * @param \BackedEnum|string|null $queue * @return $this */ public function onQueue($queue) { - $this->queue = $queue; + $this->queue = enum_value($queue); return $this; } diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index b8cd8d7c33e7..1a6ecc195856 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 = '11.29.0'; + const VERSION = '11.30.0'; /** * The base path for the Laravel installation. @@ -1465,6 +1465,17 @@ public function setDeferredServices(array $services) $this->deferredServices = $services; } + /** + * Determine if the given service is a deferred service. + * + * @param string $service + * @return bool + */ + public function isDeferredService($service) + { + return isset($this->deferredServices[$service]); + } + /** * Add an array of services to the application's deferred services. * @@ -1477,14 +1488,16 @@ public function addDeferredServices(array $services) } /** - * Determine if the given service is a deferred service. + * Remove an array of services from the application's deferred services. * - * @param string $service - * @return bool + * @param array $services + * @return void */ - public function isDeferredService($service) + public function removeDeferredServices(array $services) { - return isset($this->deferredServices[$service]); + foreach ($services as $service) { + unset($this->deferredServices[$service]); + } } /** diff --git a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php index 19dc16416746..adabdcec0576 100644 --- a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php +++ b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php @@ -5,6 +5,8 @@ use Illuminate\Contracts\Auth\Access\Gate; use Illuminate\Support\Str; +use function Illuminate\Support\enum_value; + trait AuthorizesRequests { /** @@ -49,6 +51,8 @@ public function authorizeForUser($user, $ability, $arguments = []) */ protected function parseAbilityAndArguments($ability, $arguments) { + $ability = enum_value($ability); + if (is_string($ability) && ! str_contains($ability, '\\')) { return [$ability, $arguments]; } diff --git a/src/Illuminate/Foundation/Bus/PendingChain.php b/src/Illuminate/Foundation/Bus/PendingChain.php index 2fb14990c56a..3e6f8729ee2f 100644 --- a/src/Illuminate/Foundation/Bus/PendingChain.php +++ b/src/Illuminate/Foundation/Bus/PendingChain.php @@ -8,6 +8,8 @@ use Illuminate\Support\Traits\Conditionable; use Laravel\SerializableClosure\SerializableClosure; +use function Illuminate\Support\enum_value; + class PendingChain { use Conditionable; @@ -83,12 +85,12 @@ public function onConnection($connection) /** * Set the desired queue for the job. * - * @param string|null $queue + * @param \BackedEnum|string|null $queue * @return $this */ public function onQueue($queue) { - $this->queue = $queue; + $this->queue = enum_value($queue); return $this; } diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index 89ec3b9cc50f..dde1af231d43 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -269,6 +269,18 @@ public function withMiddleware(?callable $callback = null) if ($priorities = $middleware->getMiddlewarePriority()) { $kernel->setMiddlewarePriority($priorities); } + + if ($priorityAppends = $middleware->getMiddlewarePriorityAppends()) { + foreach ($priorityAppends as $middleware => $after) { + $kernel->addToMiddlewarePriorityAfter($after, $middleware); + } + } + + if ($priorityPrepends = $middleware->getMiddlewarePriorityPrepends()) { + foreach ($priorityPrepends as $middleware => $before) { + $kernel->addToMiddlewarePriorityBefore($before, $middleware); + } + } }); return $this; diff --git a/src/Illuminate/Foundation/Configuration/Middleware.php b/src/Illuminate/Foundation/Configuration/Middleware.php index 52b83acf518b..52cf908d61d9 100644 --- a/src/Illuminate/Foundation/Configuration/Middleware.php +++ b/src/Illuminate/Foundation/Configuration/Middleware.php @@ -145,6 +145,20 @@ class Middleware */ protected $priority = []; + /** + * The middleware to prepend to the middleware priority definition. + * + * @var array + */ + protected $prependPriority = []; + + /** + * The middleware to append to the middleware priority definition. + * + * @var array + */ + protected $appendPriority = []; + /** * Prepend middleware to the application's global middleware stack. * @@ -400,6 +414,34 @@ public function priority(array $priority) return $this; } + /** + * Prepend middleware to the priority middleware. + * + * @param array|string $before + * @param string $prepend + * @return $this + */ + public function prependToPriorityList($before, $prepend) + { + $this->prependPriority[$prepend] = $before; + + return $this; + } + + /** + * Append middleware to the priority middleware. + * + * @param array|string $after + * @param string $append + * @return $this + */ + public function appendToPriorityList($after, $append) + { + $this->appendPriority[$append] = $after; + + return $this; + } + /** * Get the global middleware. * @@ -766,4 +808,24 @@ public function getMiddlewarePriority() { return $this->priority; } + + /** + * Get the middleware to prepend to the middleware priority definition. + * + * @return array + */ + public function getMiddlewarePriorityPrepends() + { + return $this->prependPriority; + } + + /** + * Get the middleware to append to the middleware priority definition. + * + * @return array + */ + public function getMiddlewarePriorityAppends() + { + return $this->appendPriority; + } } diff --git a/src/Illuminate/Foundation/Console/MailMakeCommand.php b/src/Illuminate/Foundation/Console/MailMakeCommand.php index 2ec59a15d5a1..d6bf70f799ff 100644 --- a/src/Illuminate/Foundation/Console/MailMakeCommand.php +++ b/src/Illuminate/Foundation/Console/MailMakeCommand.php @@ -70,6 +70,10 @@ protected function writeMarkdownTemplate() str_replace('.', '/', $this->getView()).'.blade.php' ); + if ($this->files->exists($path)) { + return $this->components->error(sprintf('%s [%s] already exists.', 'Markdown view', $path)); + } + $this->files->ensureDirectoryExists(dirname($path)); $this->files->put($path, file_get_contents(__DIR__.'/stubs/markdown.stub')); @@ -88,6 +92,10 @@ protected function writeView() str_replace('.', '/', $this->getView()).'.blade.php' ); + if ($this->files->exists($path)) { + return $this->components->error(sprintf('%s [%s] already exists.', 'View', $path)); + } + $this->files->ensureDirectoryExists(dirname($path)); $stub = str_replace( diff --git a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php index be497edaf88e..c137442a9b12 100644 --- a/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php +++ b/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php @@ -239,6 +239,8 @@ protected function registerExceptionTracking() */ protected function registerExceptionRenderer() { + $this->loadViewsFrom(__DIR__.'/../Exceptions/views', 'laravel-exceptions'); + if (! $this->app->hasDebugModeEnabled()) { return; } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php index b4aad547cc38..24afbda1ba81 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithContainer.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Foundation\Mix; use Illuminate\Foundation\Vite; +use Illuminate\Support\Defer\DeferredCallbackCollection; use Illuminate\Support\Facades\Facade; use Illuminate\Support\HtmlString; use Mockery; @@ -25,6 +26,13 @@ trait InteractsWithContainer */ protected $originalMix; + /** + * The original deferred callbacks collection. + * + * @var \Illuminate\Support\Defer\DeferredCallbackCollection|null + */ + protected $originalDeferredCallbacksCollection; + /** * Register an instance of an object in the container. * @@ -234,4 +242,38 @@ protected function withMix() return $this; } + + /** + * Execute deferred functions immediately. + * + * @return $this + */ + protected function withoutDefer() + { + if ($this->originalDeferredCallbacksCollection == null) { + $this->originalDeferredCallbacksCollection = $this->app->make(DeferredCallbackCollection::class); + } + + $this->swap(DeferredCallbackCollection::class, new class extends DeferredCallbackCollection + { + public function offsetSet(mixed $offset, mixed $value): void + { + $value(); + } + }); + } + + /** + * Restore deferred functions. + * + * @return $this + */ + protected function withDefer() + { + if ($this->originalDeferredCallbacksCollection) { + $this->app->instance(DeferredCallbackCollection::class, $this->originalDeferredCallbacksCollection); + } + + return $this; + } } diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php index 0b47714d1414..c4744b170d07 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase.php @@ -231,7 +231,7 @@ protected function isSoftDeletableModel($model) * Cast a JSON string to a database compatible type. * * @param array|object|string $value - * @param string|null $collection + * @param string|null $connection * @return \Illuminate\Contracts\Database\Query\Expression */ public function castAsJson($value, $connection = null) diff --git a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php index 3d210cd7888d..ed058498d86f 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php +++ b/src/Illuminate/Foundation/Testing/Concerns/InteractsWithTestCaseLifecycle.php @@ -21,6 +21,7 @@ use Illuminate\Foundation\Testing\WithoutMiddleware; use Illuminate\Http\Middleware\TrustHosts; use Illuminate\Http\Middleware\TrustProxies; +use Illuminate\Queue\Console\WorkCommand; use Illuminate\Queue\Queue; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Facade; @@ -178,6 +179,7 @@ protected function tearDownTheTestEnvironment(): void TrustProxies::flushState(); TrustHosts::flushState(); ValidateCsrfToken::flushState(); + WorkCommand::flushState(); if ($this->callbackException) { throw $this->callbackException; diff --git a/src/Illuminate/Http/Middleware/ValidatePostSize.php b/src/Illuminate/Http/Middleware/ValidatePostSize.php index bfa620a85f46..8b3519c92842 100644 --- a/src/Illuminate/Http/Middleware/ValidatePostSize.php +++ b/src/Illuminate/Http/Middleware/ValidatePostSize.php @@ -21,7 +21,7 @@ public function handle($request, Closure $next) $max = $this->getPostMaxSize(); if ($max > 0 && $request->server('CONTENT_LENGTH') > $max) { - throw new PostTooLargeException; + throw new PostTooLargeException('The POST data is too large.'); } return $next($request); diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 93179f701591..bd6484f3ea67 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -465,7 +465,7 @@ protected function setGlobalToAndRemoveCcAndBcc($message) * Queue a new mail message for sending. * * @param \Illuminate\Contracts\Mail\Mailable|string|array $view - * @param string|null $queue + * @param \BackedEnum|string|null $queue * @return mixed * * @throws \InvalidArgumentException @@ -486,7 +486,7 @@ public function queue($view, $queue = null) /** * Queue a new mail message for sending on the given queue. * - * @param string $queue + * @param \BackedEnum|string|null $queue * @param \Illuminate\Contracts\Mail\Mailable $view * @return mixed */ diff --git a/src/Illuminate/Queue/Console/WorkCommand.php b/src/Illuminate/Queue/Console/WorkCommand.php index e36e352904b9..4e3b94202f94 100644 --- a/src/Illuminate/Queue/Console/WorkCommand.php +++ b/src/Illuminate/Queue/Console/WorkCommand.php @@ -76,6 +76,13 @@ class WorkCommand extends Command */ protected $latestStartedAt; + /** + * Indicates if the worker's event listeners have been registered. + * + * @var bool + */ + private static $hasRegisteredListeners = false; + /** * Create a new queue work command. * @@ -172,6 +179,10 @@ protected function gatherWorkerOptions() */ protected function listenForEvents() { + if (static::$hasRegisteredListeners) { + return; + } + $this->laravel['events']->listen(JobProcessing::class, function ($event) { $this->writeOutput($event->job, 'starting'); }); @@ -189,6 +200,8 @@ protected function listenForEvents() $this->logFailedJob($event); }); + + static::$hasRegisteredListeners = true; } /** @@ -360,4 +373,14 @@ protected function outputUsingJson() return $this->option('json'); } + + /** + * Reset static variables. + * + * @return void + */ + public static function flushState() + { + static::$hasRegisteredListeners = false; + } } diff --git a/src/Illuminate/Routing/ResolvesRouteDependencies.php b/src/Illuminate/Routing/ResolvesRouteDependencies.php index 9bf2f2523691..135a94c03894 100644 --- a/src/Illuminate/Routing/ResolvesRouteDependencies.php +++ b/src/Illuminate/Routing/ResolvesRouteDependencies.php @@ -96,6 +96,9 @@ protected function transformDependency(ReflectionParameter $parameter, $paramete return $this->container->make($className); } + // If the parameter has a type-hinted class, we will check to see if it is already in + // the list of parameters. If it is we will just skip it as it is probably a model + // binding and we do not want to mess with those; otherwise, we resolve it here. if ($className && (! $this->alreadyInParameters($className, $parameters))) { $isEnum = (new ReflectionClass($className))->isEnum(); diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index 66f7f8aff318..2c01b3ad1d65 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -83,8 +83,9 @@ * @method static bool providerIsLoaded(string $provider) * @method static array getDeferredServices() * @method static void setDeferredServices(array $services) - * @method static void addDeferredServices(array $services) * @method static bool isDeferredService(string $service) + * @method static void addDeferredServices(array $services) + * @method static void removeDeferredServices(array $services) * @method static void provideFacades(string $namespace) * @method static string getLocale() * @method static string currentLocale() diff --git a/src/Illuminate/Support/Facades/Blade.php b/src/Illuminate/Support/Facades/Blade.php index 3d32cf8621c7..01dc7ae76723 100755 --- a/src/Illuminate/Support/Facades/Blade.php +++ b/src/Illuminate/Support/Facades/Blade.php @@ -26,7 +26,8 @@ * @method static void aliasComponent(string $path, string|null $alias = null) * @method static void include(string $path, string|null $alias = null) * @method static void aliasInclude(string $path, string|null $alias = null) - * @method static void directive(string $name, callable $handler) + * @method static void bindDirective(string $name, callable $handler) + * @method static void directive(string $name, callable $handler, bool $bind = false) * @method static array getCustomDirectives() * @method static \Illuminate\View\Compilers\BladeCompiler prepareStringsForCompilationUsing(callable $callback) * @method static void precompiler(callable $precompiler) diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index e4967fab47fc..80e14dcf9a5d 100644 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -105,7 +105,7 @@ * @method static string getServerVersion() * @method static void resolverFor(string $driver, \Closure $callback) * @method static \Closure|null getResolver(string $driver) - * @method static mixed transaction(\Closure $callback, int $attempts = 1) + * @method static void transaction(\Closure $callback, int $attempts = 1) * @method static void beginTransaction() * @method static void commit() * @method static void rollBack(int|null $toLevel = null) diff --git a/src/Illuminate/Support/Facades/Mail.php b/src/Illuminate/Support/Facades/Mail.php index 8987aa04cc3a..7223a76e87cc 100755 --- a/src/Illuminate/Support/Facades/Mail.php +++ b/src/Illuminate/Support/Facades/Mail.php @@ -28,8 +28,8 @@ * @method static string render(string|array $view, array $data = []) * @method static \Illuminate\Mail\SentMessage|null send(\Illuminate\Contracts\Mail\Mailable|string|array $view, array $data = [], \Closure|string|null $callback = null) * @method static \Illuminate\Mail\SentMessage|null sendNow(\Illuminate\Contracts\Mail\Mailable|string|array $mailable, array $data = [], \Closure|string|null $callback = null) - * @method static mixed queue(\Illuminate\Contracts\Mail\Mailable|string|array $view, string|null $queue = null) - * @method static mixed onQueue(string $queue, \Illuminate\Contracts\Mail\Mailable $view) + * @method static mixed queue(\Illuminate\Contracts\Mail\Mailable|string|array $view, \BackedEnum|string|null $queue = null) + * @method static mixed onQueue(\BackedEnum|string|null $queue, \Illuminate\Contracts\Mail\Mailable $view) * @method static mixed queueOn(string $queue, \Illuminate\Contracts\Mail\Mailable $view) * @method static mixed later(\DateTimeInterface|\DateInterval|int $delay, \Illuminate\Contracts\Mail\Mailable $view, string|null $queue = null) * @method static mixed laterOn(string $queue, \DateTimeInterface|\DateInterval|int $delay, \Illuminate\Contracts\Mail\Mailable $view) diff --git a/src/Illuminate/Testing/Constraints/HasInDatabase.php b/src/Illuminate/Testing/Constraints/HasInDatabase.php index f17ce8c51afe..f6cd338e9587 100644 --- a/src/Illuminate/Testing/Constraints/HasInDatabase.php +++ b/src/Illuminate/Testing/Constraints/HasInDatabase.php @@ -51,7 +51,9 @@ public function __construct(Connection $database, array $data) */ public function matches($table): bool { - return $this->database->table($table)->where($this->data)->count() > 0; + return $this->database->table($table) + ->where($this->data) + ->exists(); } /** diff --git a/src/Illuminate/Testing/Constraints/NotSoftDeletedInDatabase.php b/src/Illuminate/Testing/Constraints/NotSoftDeletedInDatabase.php index ff8195829f9f..87cef8b6d02d 100644 --- a/src/Illuminate/Testing/Constraints/NotSoftDeletedInDatabase.php +++ b/src/Illuminate/Testing/Constraints/NotSoftDeletedInDatabase.php @@ -61,7 +61,7 @@ public function matches($table): bool return $this->database->table($table) ->where($this->data) ->whereNull($this->deletedAtColumn) - ->count() > 0; + ->exists(); } /** diff --git a/src/Illuminate/Testing/Constraints/SoftDeletedInDatabase.php b/src/Illuminate/Testing/Constraints/SoftDeletedInDatabase.php index baaeee27a181..0d14f83b6c67 100644 --- a/src/Illuminate/Testing/Constraints/SoftDeletedInDatabase.php +++ b/src/Illuminate/Testing/Constraints/SoftDeletedInDatabase.php @@ -63,7 +63,7 @@ public function matches($table): bool return $this->database->table($table) ->where($this->data) ->whereNotNull($this->deletedAtColumn) - ->count() > 0; + ->exists(); } /** diff --git a/src/Illuminate/Translation/Translator.php b/src/Illuminate/Translation/Translator.php index 5f9b1e299896..aff3291e3267 100755 --- a/src/Illuminate/Translation/Translator.php +++ b/src/Illuminate/Translation/Translator.php @@ -118,7 +118,7 @@ public function has($key, $locale = null, $fallback = true) $locale = $locale ?: $this->locale; // We should temporarily disable the handling of missing translation keys - // while perfroming the existence check. After the check, we will turn + // while performing the existence check. After the check, we will turn // the missing translation keys handling back to its original value. $handleMissingTranslationKeys = $this->handleMissingTranslationKeys; @@ -200,7 +200,7 @@ public function get($key, array $replace = [], $locale = null, $fallback = true) public function choice($key, $number, array $replace = [], $locale = null) { $line = $this->get( - $key, $replace, $locale = $this->localeForChoice($key, $locale) + $key, [], $locale = $this->localeForChoice($key, $locale) ); // If the given "number" is actually an array or countable we will simply count the diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 7911462ebf32..396c21a45099 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -934,22 +934,37 @@ public function aliasInclude($path, $alias = null) }); } + /** + * Register a handler for custom directives, binding the handler to the compiler. + * + * @param string $name + * @param callable $handler + * @return void + * + * @throws \InvalidArgumentException + */ + public function bindDirective($name, callable $handler) + { + $this->directive($name, $handler, bind: true); + } + /** * Register a handler for custom directives. * * @param string $name * @param callable $handler + * @param bool $bind * @return void * * @throws \InvalidArgumentException */ - public function directive($name, callable $handler) + public function directive($name, callable $handler, bool $bind = false) { if (! preg_match('/^\w+(?:::\w+)?$/x', $name)) { throw new InvalidArgumentException("The directive name [{$name}] is not valid. Directive names must only contain alphanumeric characters and underscores."); } - $this->customDirectives[$name] = $handler; + $this->customDirectives[$name] = $bind ? $handler->bindTo($this, BladeCompiler::class) : $handler; } /** diff --git a/tests/Database/DatabasePostgresQueryGrammarTest.php b/tests/Database/DatabasePostgresQueryGrammarTest.php index 6b2e2826ac0d..2fc526946ef5 100755 --- a/tests/Database/DatabasePostgresQueryGrammarTest.php +++ b/tests/Database/DatabasePostgresQueryGrammarTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Database; use Illuminate\Database\Connection; +use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Grammars\PostgresGrammar; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -28,4 +29,46 @@ public function testToRawSql() $this->assertSame('select * from "users" where \'{}\' ? \'Hello\\\'\\\'World?\' AND "email" = \'foo\'', $query); } + + public function testCustomOperators() + { + PostgresGrammar::customOperators(['@@@', '@>', '']); + PostgresGrammar::customOperators(['@@>', 1]); + + $connection = m::mock(Connection::class); + $grammar = new PostgresGrammar; + $grammar->setConnection($connection); + + $operators = $grammar->getOperators(); + + $this->assertIsList($operators); + $this->assertContains('@@@', $operators); + $this->assertContains('@@>', $operators); + $this->assertNotContains('', $operators); + $this->assertNotContains(1, $operators); + $this->assertSame(array_unique($operators), $operators); + } + + public function testCompileTruncate() + { + $postgres = new PostgresGrammar; + $builder = m::mock(Builder::class); + $builder->from = 'users'; + + $this->assertEquals([ + 'truncate "users" restart identity cascade' => [], + ], $postgres->compileTruncate($builder)); + + PostgresGrammar::cascadeOnTrucate(false); + + $this->assertEquals([ + 'truncate "users" restart identity' => [], + ], $postgres->compileTruncate($builder)); + + PostgresGrammar::cascadeOnTrucate(); + + $this->assertEquals([ + 'truncate "users" restart identity cascade' => [], + ], $postgres->compileTruncate($builder)); + } } diff --git a/tests/Foundation/FoundationAuthorizesRequestsTraitTest.php b/tests/Foundation/FoundationAuthorizesRequestsTraitTest.php index 1005103aaf66..a628f00169ed 100644 --- a/tests/Foundation/FoundationAuthorizesRequestsTraitTest.php +++ b/tests/Foundation/FoundationAuthorizesRequestsTraitTest.php @@ -35,6 +35,24 @@ public function testBasicGateCheck() $this->assertTrue($_SERVER['_test.authorizes.trait']); } + public function testAcceptsBackedEnumAsAbility() + { + unset($_SERVER['_test.authorizes.trait.enum']); + + $gate = $this->getBasicGate(); + + $gate->define('baz', function () { + $_SERVER['_test.authorizes.trait.enum'] = true; + + return true; + }); + + $response = (new FoundationTestAuthorizeTraitClass)->authorize(TestAbility::BAZ); + + $this->assertInstanceOf(Response::class, $response); + $this->assertTrue($_SERVER['_test.authorizes.trait.enum']); + } + public function testExceptionIsThrownIfGateCheckFails() { $this->expectException(AuthorizationException::class); @@ -163,3 +181,8 @@ public function store($object) $this->authorize($object); } } + +enum TestAbility: string +{ + case BAZ = 'baz'; +} diff --git a/tests/Foundation/FoundationInteractsWithDatabaseTest.php b/tests/Foundation/FoundationInteractsWithDatabaseTest.php index 898334c7ab56..24b02bc180b1 100644 --- a/tests/Foundation/FoundationInteractsWithDatabaseTest.php +++ b/tests/Foundation/FoundationInteractsWithDatabaseTest.php @@ -8,6 +8,7 @@ use Illuminate\Database\Query\Builder; use Illuminate\Foundation\Testing\Concerns\InteractsWithDatabase; use Illuminate\Foundation\Testing\TestCase as TestingTestCase; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; use Mockery as m; use Orchestra\Testbench\Concerns\CreatesApplication; @@ -39,14 +40,14 @@ protected function tearDown(): void public function testSeeInDatabaseFindsResults() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertDatabaseHas($this->table, $this->data); } public function testAssertDatabaseHasSupportsModelClass() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertDatabaseHas(ProductStub::class, $this->data); } @@ -60,7 +61,7 @@ public function testAssertDatabaseHasConstrainsToModel() ...$this->data, ]; - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertDatabaseHas(new ProductStub(['id' => 1]), $data); } @@ -70,7 +71,7 @@ public function testSeeInDatabaseDoesNotFindResults() $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The table is empty.'); - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect()); @@ -83,7 +84,7 @@ public function testSeeInDatabaseFindsNotMatchingResults() $this->expectExceptionMessage('Found similar results: '.json_encode([['title' => 'Forge']], JSON_PRETTY_PRINT)); - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('take')->andReturnSelf(); $builder->shouldReceive('get')->andReturn(collect([['title' => 'Forge']])); @@ -97,8 +98,7 @@ public function testSeeInDatabaseFindsManyNotMatchingResults() $this->expectExceptionMessage('Found similar results: '.json_encode(['data', 'data', 'data'], JSON_PRETTY_PRINT).' and 2 others.'); - $builder = $this->mockCountBuilder(0); - $builder->shouldReceive('count')->andReturn(0, 5); + $builder = $this->mockCountBuilder(false, countResult: [5, 5]); $builder->shouldReceive('take')->andReturnSelf(); $builder->shouldReceive('get')->andReturn( @@ -110,14 +110,14 @@ public function testSeeInDatabaseFindsManyNotMatchingResults() public function testDontSeeInDatabaseDoesNotFindResults() { - $this->mockCountBuilder(0); + $this->mockCountBuilder(false); $this->assertDatabaseMissing($this->table, $this->data); } public function testAssertDatabaseMissingSupportsModelClass() { - $this->mockCountBuilder(0); + $this->mockCountBuilder(false); $this->assertDatabaseMissing(ProductStub::class, $this->data); } @@ -131,7 +131,7 @@ public function testAssertDatabaseMissingConstrainsToModel() ...$this->data, ]; - $this->mockCountBuilder(0); + $this->mockCountBuilder(false); $this->assertDatabaseMissing(new ProductStub(['id' => 1]), $data); } @@ -140,7 +140,7 @@ public function testDontSeeInDatabaseFindsResults() { $this->expectException(ExpectationFailedException::class); - $builder = $this->mockCountBuilder(1); + $builder = $this->mockCountBuilder(true); $builder->shouldReceive('take')->andReturnSelf(); $builder->shouldReceive('get')->andReturn(collect([$this->data])); @@ -150,14 +150,14 @@ public function testDontSeeInDatabaseFindsResults() public function testAssertTableEntriesCount() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertDatabaseCount($this->table, 1); } public function testAssertDatabaseCountSupportModels() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertDatabaseCount(ProductStub::class, 1); $this->assertDatabaseCount(new ProductStub, 1); @@ -165,7 +165,7 @@ public function testAssertDatabaseCountSupportModels() public function testAssertDatabaseEmpty() { - $this->mockCountBuilder(0); + $this->mockCountBuilder(false); $this->assertDatabaseEmpty(ProductStub::class); $this->assertDatabaseEmpty(new ProductStub); @@ -175,14 +175,14 @@ public function testAssertTableEntriesCountWrong() { $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('Failed asserting that table [products] matches expected entries count of 3. Entries found: 1.'); - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertDatabaseCount($this->table, 3); } public function testAssertDatabaseMissingPassesWhenDoesNotFindResults() { - $this->mockCountBuilder(0); + $this->mockCountBuilder(false); $this->assertDatabaseMissing($this->table, $this->data); } @@ -191,7 +191,7 @@ public function testAssertDatabaseMissingFailsWhenFindsResults() { $this->expectException(ExpectationFailedException::class); - $builder = $this->mockCountBuilder(1); + $builder = $this->mockCountBuilder(true); $builder->shouldReceive('get')->andReturn(collect([$this->data])); @@ -202,7 +202,7 @@ public function testAssertModelMissingPassesWhenDoesNotFindModelResults() { $this->data = ['id' => 1]; - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect()); @@ -211,14 +211,14 @@ public function testAssertModelMissingPassesWhenDoesNotFindModelResults() public function testAssertSoftDeletedInDatabaseFindsResults() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertSoftDeleted($this->table, $this->data); } public function testAssertSoftDeletedSupportModelStrings() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertSoftDeleted(ProductStub::class, $this->data); } @@ -228,7 +228,7 @@ public function testAssertSoftDeletedInDatabaseDoesNotFindResults() $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The table is empty.'); - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect()); @@ -242,7 +242,7 @@ public function testAssertSoftDeletedInDatabaseDoesNotFindModelResults() $this->data = ['id' => 1]; - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect()); @@ -257,7 +257,7 @@ public function testAssertSoftDeletedInDatabaseDoesNotFindModelWithCustomColumnR $model = new CustomProductStub(['id' => 1, 'name' => 'Laravel']); $this->data = ['id' => 1, 'name' => 'Tailwind']; - $builder = $this->mockCountBuilder(0, 'trashed_at'); + $builder = $this->mockCountBuilder(false, 'trashed_at'); $builder->shouldReceive('get')->andReturn(collect()); @@ -272,7 +272,7 @@ public function testAssertSoftDeletedInDatabaseDoesNotFindModePassedViaFcnWithCu $model = new CustomProductStub(['id' => 1, 'name' => 'Laravel']); $this->data = ['id' => 1]; - $builder = $this->mockCountBuilder(0, 'trashed_at'); + $builder = $this->mockCountBuilder(false, 'trashed_at'); $builder->shouldReceive('get')->andReturn(collect()); @@ -281,14 +281,14 @@ public function testAssertSoftDeletedInDatabaseDoesNotFindModePassedViaFcnWithCu public function testAssertNotSoftDeletedInDatabaseFindsResults() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertNotSoftDeleted($this->table, $this->data); } public function testAssertNotSoftDeletedSupportModelStrings() { - $this->mockCountBuilder(1); + $this->mockCountBuilder(true); $this->assertNotSoftDeleted(ProductStub::class, $this->data); } @@ -298,7 +298,7 @@ public function testAssertNotSoftDeletedOnlyFindsMatchingModels() $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('Failed asserting that any existing row'); - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect(), collect(1)); @@ -310,7 +310,7 @@ public function testAssertNotSoftDeletedInDatabaseDoesNotFindResults() $this->expectException(ExpectationFailedException::class); $this->expectExceptionMessage('The table is empty.'); - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect()); @@ -324,7 +324,7 @@ public function testAssertNotSoftDeletedInDatabaseDoesNotFindModelResults() $this->data = ['id' => 1]; - $builder = $this->mockCountBuilder(0); + $builder = $this->mockCountBuilder(false); $builder->shouldReceive('get')->andReturn(collect()); @@ -339,7 +339,7 @@ public function testAssertNotSoftDeletedInDatabaseDoesNotFindModelWithCustomColu $model = new CustomProductStub(['id' => 1, 'name' => 'Laravel']); $this->data = ['id' => 1, 'name' => 'Tailwind']; - $builder = $this->mockCountBuilder(0, 'trashed_at'); + $builder = $this->mockCountBuilder(false, 'trashed_at'); $builder->shouldReceive('get')->andReturn(collect()); @@ -354,7 +354,7 @@ public function testAssertNotSoftDeletedInDatabaseDoesNotFindModelPassedViaFcnWi $model = new CustomProductStub(['id' => 1, 'name' => 'Laravel']); $this->data = ['id' => 1]; - $builder = $this->mockCountBuilder(0, 'trashed_at'); + $builder = $this->mockCountBuilder(false, 'trashed_at'); $builder->shouldReceive('get')->andReturn(collect()); @@ -365,7 +365,7 @@ public function testAssertExistsPassesWhenFindsResults() { $this->data = ['id' => 1]; - $builder = $this->mockCountBuilder(1); + $builder = $this->mockCountBuilder(true); $builder->shouldReceive('get')->andReturn(collect($this->data)); @@ -482,10 +482,13 @@ public function testExpectsDatabaseQueryCount() $case->tearDown(); } - protected function mockCountBuilder($countResult, $deletedAtColumn = 'deleted_at') + protected function mockCountBuilder($existsResult, $deletedAtColumn = 'deleted_at', $countResult = null) { $builder = m::mock(Builder::class); + $countResult = Arr::wrap($countResult); + $countResult = ! empty($countResult) ? $countResult : [$existsResult ? 1 : 0]; + $key = array_key_first($this->data); $value = $this->data[$key]; @@ -501,7 +504,9 @@ protected function mockCountBuilder($countResult, $deletedAtColumn = 'deleted_at $builder->shouldReceive('whereNull')->with($deletedAtColumn)->andReturnSelf(); - $builder->shouldReceive('count')->andReturn($countResult)->byDefault(); + $builder->shouldReceive('exists')->andReturn($existsResult)->byDefault(); + + $builder->shouldReceive('count')->andReturn(...$countResult)->byDefault(); $this->connection->shouldReceive('table') ->with($this->table) diff --git a/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php b/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php index 7b1c279c8659..ed7ad1bc84be 100644 --- a/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php +++ b/tests/Foundation/Testing/Concerns/InteractsWithContainerTest.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Mix; use Illuminate\Foundation\Vite; +use Illuminate\Support\Defer\DeferredCallbackCollection; use Orchestra\Testbench\TestCase; use stdClass; @@ -73,6 +74,26 @@ public function testWithMixRestoresOriginalHandlerAndReturnsInstance() $this->assertSame($this, $instance); } + public function testWithoutDefer() + { + $called = []; + defer(function () use (&$called) { + $called[] = 1; + }); + $this->assertSame([], $called); + + $this->withoutDefer(); + defer(function () use (&$called) { + $called[] = 2; + }); + $this->assertSame([2], $called); + + $this->withDefer(); + $this->assertSame([2], $called); + $this->app[DeferredCallbackCollection::class]->invoke(); + $this->assertSame([2, 1], $called); + } + public function testForgetMock() { $this->mock(InstanceStub::class) diff --git a/tests/Integration/Concurrency/ConcurrencyTest.php b/tests/Integration/Concurrency/ConcurrencyTest.php index 76012712c375..c62295255013 100644 --- a/tests/Integration/Concurrency/ConcurrencyTest.php +++ b/tests/Integration/Concurrency/ConcurrencyTest.php @@ -1,6 +1,6 @@ app['files'] + ->put( + $this->app->basePath($existingMarkdownPath), + 'My existing markdown' + ); + $this->artisan('make:mail', ['name' => 'FooMail', '--markdown' => 'existing-markdown']) + ->expectsOutputToContain('already exists.') + ->assertExitCode(0); + + $this->assertFileContains([ + 'namespace App\Mail;', + 'use Illuminate\Mail\Mailable;', + 'class FooMail extends Mailable', + 'return new Content(', + "markdown: 'existing-markdown',", + ], 'app/Mail/FooMail.php'); + $this->assertFileContains([ + '', + 'My existing markdown', + '', + ], $existingMarkdownPath); + } + public function testItCanGenerateMailFileWithViewOption() { $this->artisan('make:mail', ['name' => 'FooMail', '--view' => 'foo-mail']) @@ -63,6 +89,31 @@ public function testItCanGenerateMailFileWithViewOption() $this->assertFilenameExists('resources/views/foo-mail.blade.php'); } + public function testErrorsWillBeDisplayedWhenViewsAlreadyExist() + { + $existingViewPath = 'resources/views/existing-template.blade.php'; + $this->app['files'] + ->put( + $this->app->basePath($existingViewPath), + '
My existing template
' + ); + $this->artisan('make:mail', ['name' => 'FooMail', '--view' => 'existing-template']) + ->expectsOutputToContain('already exists.') + ->assertExitCode(0); + + $this->assertFileContains([ + 'namespace App\Mail;', + 'use Illuminate\Mail\Mailable;', + 'class FooMail extends Mailable', + 'return new Content(', + "view: 'existing-template',", + ], 'app/Mail/FooMail.php'); + $this->assertFilenameExists($existingViewPath); + $this->assertFileContains([ + '
My existing template
', + ], $existingViewPath); + } + public function testItCanGenerateMailFileWithTest() { $this->artisan('make:mail', ['name' => 'FooMail', '--test' => true]) diff --git a/tests/Integration/Http/ResourceTest.php b/tests/Integration/Http/ResourceTest.php index 56d2e5fd3aab..a0ec135b26b7 100644 --- a/tests/Integration/Http/ResourceTest.php +++ b/tests/Integration/Http/ResourceTest.php @@ -46,7 +46,6 @@ use Illuminate\Tests\Integration\Http\Fixtures\SerializablePostResource; use Illuminate\Tests\Integration\Http\Fixtures\Subscription; use LogicException; -use Mockery as m; use Orchestra\Testbench\TestCase; class ResourceTest extends TestCase @@ -1503,12 +1502,16 @@ public function work() public function testPostTooLargeException() { + $request = new Request(server: ['CONTENT_LENGTH' => '4']); + $post = new ValidatePostSize; + $post->handle($request, fn () => null); + $this->expectException(PostTooLargeException::class); + $this->expectExceptionMessage('The POST data is too large.'); - $request = m::mock(Request::class, ['server' => ['CONTENT_LENGTH' => '2147483640']]); + $request = new Request(server: ['CONTENT_LENGTH' => '2147483640']); $post = new ValidatePostSize; - $post->handle($request, function () { - }); + $post->handle($request, fn () => null); } public function testLeadingMergeKeyedValueIsMergedCorrectlyWhenFirstValueIsMissing() diff --git a/tests/Integration/Queue/WorkCommandTest.php b/tests/Integration/Queue/WorkCommandTest.php index 7d1d5ef12121..6d3796e01491 100644 --- a/tests/Integration/Queue/WorkCommandTest.php +++ b/tests/Integration/Queue/WorkCommandTest.php @@ -4,11 +4,16 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Foundation\Testing\DatabaseMigrations; +use Illuminate\Queue\Console\WorkCommand; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Exceptions; use Illuminate\Support\Facades\Queue; use Orchestra\Testbench\Attributes\WithMigration; +use RuntimeException; #[WithMigration] #[WithMigration('queue')] @@ -29,6 +34,13 @@ protected function setUp(): void $this->markTestSkippedWhenUsingSyncQueueDriver(); } + protected function tearDown(): void + { + WorkCommand::flushState(); + + parent::tearDown(); + } + public function testRunningOneJob() { Queue::push(new FirstJob); @@ -157,6 +169,21 @@ public function testMaxTimeExceeded() $this->assertFalse(FirstJob::$ran); $this->assertFalse(SecondJob::$ran); } + + public function testFailedJobListenerOnlyRunsOnce() + { + $this->markTestSkippedWhenUsingQueueDrivers(['redis', 'beanstalkd']); + + Exceptions::fake(); + + Queue::push(new FirstJob); + $this->withoutMockingConsoleOutput()->artisan('queue:work', ['--once' => true, '--sleep' => 0]); + + Queue::push(new JobWillFail); + $this->withoutMockingConsoleOutput()->artisan('queue:work', ['--once' => true]); + Exceptions::assertNotReported(UniqueConstraintViolationException::class); + $this->assertSame(2, substr_count(Artisan::output(), JobWillFail::class)); + } } class FirstJob implements ShouldQueue @@ -196,3 +223,13 @@ public function handle() static::$ran = true; } } + +class JobWillFail implements ShouldQueue +{ + use Dispatchable, Queueable; + + public function handle() + { + throw new RuntimeException; + } +} diff --git a/tests/Integration/Translation/TranslatorTest.php b/tests/Integration/Translation/TranslatorTest.php index a75950335f72..2756d75ad8e6 100644 --- a/tests/Integration/Translation/TranslatorTest.php +++ b/tests/Integration/Translation/TranslatorTest.php @@ -3,6 +3,7 @@ namespace Illuminate\Tests\Integration\Translation; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; class TranslatorTest extends TestCase { @@ -14,6 +15,7 @@ class TranslatorTest extends TestCase */ protected function defineEnvironment($app) { + $app['translator']->addNamespace('tests', __DIR__.'/lang'); $app['translator']->addJsonPath(__DIR__.'/lang'); parent::defineEnvironment($app); @@ -96,4 +98,42 @@ public function testItReturnsCorrectLocaleForMissingKeys() $this->app['translator']->handleMissingKeysUsing(null); } + + #[DataProvider('greetingChoiceDataProvider')] + public function testItCanHandleChoice(int $count, string $expected, ?string $locale = null) + { + if (! is_null($locale)) { + $this->app->setLocale($locale); + } + + $name = 'Taylor'; + + $this->assertSame( + strtr($expected, [':name' => $name, ':count' => $count]), + $this->app['translator']->choice('tests::app.greeting', $count, ['name' => $name]) + ); + } + + #[DataProvider('greetingChoiceDataProvider')] + public function testItCanHandleChoiceWithChoiceSeparatorInReplaceString(int $count, string $expected, ?string $locale = null) + { + if (! is_null($locale)) { + $this->app->setLocale($locale); + } + + $name = 'Taylor | Laravel'; + + $this->assertSame( + strtr($expected, [':name' => $name, ':count' => $count]), + $this->app['translator']->choice('tests::app.greeting', $count, ['name' => $name]) + ); + } + + public static function greetingChoiceDataProvider() + { + yield [0, 'Hello :name']; + yield [3, 'Hello :name, you have 3 unread messages']; + yield [0, 'Bonjour :name', 'fr']; + yield [3, 'Bonjour :name, vous avez :count messages non lus', 'fr']; + } } diff --git a/tests/Integration/Translation/lang/en/app.php b/tests/Integration/Translation/lang/en/app.php new file mode 100644 index 000000000000..654ced858808 --- /dev/null +++ b/tests/Integration/Translation/lang/en/app.php @@ -0,0 +1,5 @@ + '{0} Hello :name|[1,*] Hello :name, you have :count unread messages', +]; diff --git a/tests/Integration/Translation/lang/fr/app.php b/tests/Integration/Translation/lang/fr/app.php new file mode 100644 index 000000000000..54bb0c162125 --- /dev/null +++ b/tests/Integration/Translation/lang/fr/app.php @@ -0,0 +1,5 @@ + '{0} Bonjour :name|[1,*] Bonjour :name, vous avez :count messages non lus', +]; diff --git a/tests/Translation/TranslationTranslatorTest.php b/tests/Translation/TranslationTranslatorTest.php index f160b3ae1df9..f3b933a2f650 100755 --- a/tests/Translation/TranslationTranslatorTest.php +++ b/tests/Translation/TranslationTranslatorTest.php @@ -126,7 +126,7 @@ public function testGetMethodProperlyLoadsAndRetrievesItemForGlobalNamespace() public function testChoiceMethodProperlyLoadsAndRetrievesItemForAnInt() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); $t->expects($this->once())->method('localeForChoice')->with($this->equalTo('foo'), $this->equalTo(null))->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('line', 10, 'en')->andReturn('choiced'); @@ -137,7 +137,7 @@ public function testChoiceMethodProperlyLoadsAndRetrievesItemForAnInt() public function testChoiceMethodProperlyLoadsAndRetrievesItemForAFloat() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); $t->expects($this->once())->method('localeForChoice')->with($this->equalTo('foo'), $this->equalTo(null))->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('line', 1.2, 'en')->andReturn('choiced'); @@ -148,7 +148,7 @@ public function testChoiceMethodProperlyLoadsAndRetrievesItemForAFloat() public function testChoiceMethodProperlyCountsCollectionsAndLoadsAndRetrievesItem() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'localeForChoice'])->setConstructorArgs([$this->getLoader(), 'en'])->getMock(); - $t->expects($this->exactly(2))->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->expects($this->exactly(2))->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); $t->expects($this->exactly(2))->method('localeForChoice')->with($this->equalTo('foo'), $this->equalTo(null))->willReturn('en'); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->twice()->with('line', 3, 'en')->andReturn('choiced'); @@ -164,7 +164,7 @@ public function testChoiceMethodProperlySelectsLocaleForChoose() { $t = $this->getMockBuilder(Translator::class)->onlyMethods(['get', 'hasForLocale'])->setConstructorArgs([$this->getLoader(), 'cs'])->getMock(); $t->setFallback('en'); - $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo(['replace']), $this->equalTo('en'))->willReturn('line'); + $t->expects($this->once())->method('get')->with($this->equalTo('foo'), $this->equalTo([]), $this->equalTo('en'))->willReturn('line'); $t->expects($this->once())->method('hasForLocale')->with($this->equalTo('foo'), $this->equalTo('cs'))->willReturn(false); $t->setSelector($selector = m::mock(MessageSelector::class)); $selector->shouldReceive('choose')->once()->with('line', 10, 'en')->andReturn('choiced');