diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c196a99d749..9ca39ba4620a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,37 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.10.1...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.11.1...12.x) + +## [v12.11.1](https://github.com/laravel/framework/compare/v12.11.0...v12.11.1) - 2025-04-30 + +* Revert "[12.x]`ScheduledTaskFailed` not dispatched on scheduled task failing" by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55612 +* [12.x] Resolve issue with BelongsToManyRelationship factory by [@jackbayliss](https://github.com/jackbayliss) in https://github.com/laravel/framework/pull/55608 + +## [v12.11.0](https://github.com/laravel/framework/compare/v12.10.2...v12.11.0) - 2025-04-29 + +* Add payload creation and original delay info to job payload by [@taylorotwell](https://github.com/taylorotwell) in https://github.com/laravel/framework/pull/55529 +* Add config option to ignore view cache timestamps by [@pizkaz](https://github.com/pizkaz) in https://github.com/laravel/framework/pull/55536 +* [12.x] Dispatch NotificationFailed when sending fails by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55507 +* [12.x] Option to disable dispatchAfterResponse in a test by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55456 +* [12.x] Pass flags to custom Json::$encoder by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55548 +* [12.x] Use pendingAttributes of relationships when creating relationship models via model factories by [@gdebrauwer](https://github.com/gdebrauwer) in https://github.com/laravel/framework/pull/55558 +* [12.x] Fix double query in model relation serialization by [@AndrewMast](https://github.com/AndrewMast) in https://github.com/laravel/framework/pull/55547 +* [12.x] Improve circular relation check in Automatic Relation Loading by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55542 +* [12.x] Prevent relation autoload context from being serialized by [@litvinchuk](https://github.com/litvinchuk) in https://github.com/laravel/framework/pull/55582 +* Remove `@internal` Annotation from `$components` Property in `InteractsWithIO` by [@michaelnabil230](https://github.com/michaelnabil230) in https://github.com/laravel/framework/pull/55580 +* Ensure fake job implements job contract by [@timacdonald](https://github.com/timacdonald) in https://github.com/laravel/framework/pull/55574 +* [12.x] Fix `AnyOf` constructor parameter type by [@axlon](https://github.com/axlon) in https://github.com/laravel/framework/pull/55577 +* Sync changes to Illuminate components before release by [@driesvints](https://github.com/driesvints) in https://github.com/laravel/framework/pull/55591 +* [12.x] Set class-string generics on `Enum` rule by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55588 +* [12.x] added detailed doc types to bindings related methods by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55576 +* [12.x] Improve [@use](https://github.com/use) directive to support function and const modifiers by [@rodolfosrg](https://github.com/rodolfosrg) in https://github.com/laravel/framework/pull/55583 +* 12.x scheduled task failed not dispatched on scheduled task failing by [@achrafAa](https://github.com/achrafAa) in https://github.com/laravel/framework/pull/55572 +* [12.x] Introduce Reflector methods for accessing class attributes by [@daniser](https://github.com/daniser) in https://github.com/laravel/framework/pull/55568 +* [12.x] Typed getters for Arr helper by [@tibbsa](https://github.com/tibbsa) in https://github.com/laravel/framework/pull/55567 + +## [v12.10.2](https://github.com/laravel/framework/compare/v12.10.1...v12.10.2) - 2025-04-24 + +* [12.x] Address Model@relationLoaded when relation is null by [@rodrigopedra](https://github.com/rodrigopedra) in https://github.com/laravel/framework/pull/55531 ## [v12.10.1](https://github.com/laravel/framework/compare/v12.10.0...v12.10.1) - 2025-04-23 diff --git a/src/Illuminate/Bus/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index 32f917d796a6..0107b9e5acd4 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -51,6 +51,13 @@ class Dispatcher implements QueueingDispatcher */ protected $queueResolver; + /** + * Indicates if dispatching after response is disabled. + * + * @var bool + */ + protected $allowsDispatchingAfterResponses = true; + /** * Create a new command dispatcher instance. * @@ -252,6 +259,12 @@ protected function pushCommandToQueue($queue, $command) */ public function dispatchAfterResponse($command, $handler = null) { + if (! $this->allowsDispatchingAfterResponses) { + $this->dispatchSync($command); + + return; + } + $this->container->terminating(function () use ($command, $handler) { $this->dispatchSync($command, $handler); }); @@ -282,4 +295,28 @@ public function map(array $map) return $this; } + + /** + * Allow dispatching after responses. + * + * @return $this + */ + public function withDispatchingAfterResponses() + { + $this->allowsDispatchingAfterResponses = true; + + return $this; + } + + /** + * Disable dispatching after responses. + * + * @return $this + */ + public function withoutDispatchingAfterResponses() + { + $this->allowsDispatchingAfterResponses = false; + + return $this; + } } diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index e4ef759c8f61..d9b7561db2cf 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -40,6 +40,38 @@ public static function add($array, $key, $value) return $array; } + /** + * Get an array item from an array using "dot" notation. + */ + public static function array(ArrayAccess|array $array, string|int|null $key, ?array $default = null): array + { + $value = Arr::get($array, $key, $default); + + if (! is_array($value)) { + throw new InvalidArgumentException( + sprintf('Array value for key [%s] must be an array, %s found.', $key, gettype($value)) + ); + } + + return $value; + } + + /** + * Get a boolean item from an array using "dot" notation. + */ + public static function boolean(ArrayAccess|array $array, string|int|null $key, ?bool $default = null): bool + { + $value = Arr::get($array, $key, $default); + + if (! is_bool($value)) { + throw new InvalidArgumentException( + sprintf('Array value for key [%s] must be a boolean, %s found.', $key, gettype($value)) + ); + } + + return $value; + } + /** * Collapse an array of arrays into a single array. * @@ -286,6 +318,22 @@ public static function flatten($array, $depth = INF) return $result; } + /** + * Get a float item from an array using "dot" notation. + */ + public static function float(ArrayAccess|array $array, string|int|null $key, ?float $default = null): float + { + $value = Arr::get($array, $key, $default); + + if (! is_float($value)) { + throw new InvalidArgumentException( + sprintf('Array value for key [%s] must be a float, %s found.', $key, gettype($value)) + ); + } + + return $value; + } + /** * Remove one or many array items from a given array using "dot" notation. * @@ -433,6 +481,22 @@ public static function hasAny($array, $keys) return false; } + /** + * Get an integer item from an array using "dot" notation. + */ + public static function integer(ArrayAccess|array $array, string|int|null $key, ?int $default = null): int + { + $value = Arr::get($array, $key, $default); + + if (! is_integer($value)) { + throw new InvalidArgumentException( + sprintf('Array value for key [%s] must be an integer, %s found.', $key, gettype($value)) + ); + } + + return $value; + } + /** * Determines if an array is associative. * @@ -906,6 +970,22 @@ public static function sortRecursiveDesc($array, $options = SORT_REGULAR) return static::sortRecursive($array, $options, true); } + /** + * Get a string item from an array using "dot" notation. + */ + public static function string(ArrayAccess|array $array, string|int|null $key, ?string $default = null): string + { + $value = Arr::get($array, $key, $default); + + if (! is_string($value)) { + throw new InvalidArgumentException( + sprintf('Array value for key [%s] must be a string, %s found.', $key, gettype($value)) + ); + } + + return $value; + } + /** * Conditionally compile classes from an array into a CSS class list. * diff --git a/src/Illuminate/Console/Concerns/InteractsWithIO.php b/src/Illuminate/Console/Concerns/InteractsWithIO.php index f839ce463499..33a4b726377b 100644 --- a/src/Illuminate/Console/Concerns/InteractsWithIO.php +++ b/src/Illuminate/Console/Concerns/InteractsWithIO.php @@ -19,8 +19,6 @@ trait InteractsWithIO * The console components factory. * * @var \Illuminate\Console\View\Components\Factory - * - * @internal This property is not meant to be used or overwritten outside the framework. */ protected $components; diff --git a/src/Illuminate/Database/Eloquent/Casts/Json.php b/src/Illuminate/Database/Eloquent/Casts/Json.php index 970b309dbb94..783d5b9986f6 100644 --- a/src/Illuminate/Database/Eloquent/Casts/Json.php +++ b/src/Illuminate/Database/Eloquent/Casts/Json.php @@ -23,7 +23,9 @@ class Json */ public static function encode(mixed $value, int $flags = 0): mixed { - return isset(static::$encoder) ? (static::$encoder)($value) : json_encode($value, $flags); + return isset(static::$encoder) + ? (static::$encoder)($value, $flags) + : json_encode($value, $flags); } /** diff --git a/src/Illuminate/Database/Eloquent/Collection.php b/src/Illuminate/Database/Eloquent/Collection.php index 05ec0d345a1a..1c9dad35263f 100755 --- a/src/Illuminate/Database/Eloquent/Collection.php +++ b/src/Illuminate/Database/Eloquent/Collection.php @@ -761,7 +761,7 @@ public function withRelationshipAutoloading() foreach ($this as $model) { if (! $model->hasRelationAutoloadCallback()) { - $model->autoloadRelationsUsing($callback); + $model->autoloadRelationsUsing($callback, $this); } } diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php index 79a2f5d98cd0..8382cc183f4a 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasRelationships.php @@ -46,6 +46,13 @@ trait HasRelationships */ protected $relationAutoloadCallback = null; + /** + * The relationship autoloader callback context. + * + * @var mixed + */ + protected $relationAutoloadContext = null; + /** * The many to many relationship methods. * @@ -118,10 +125,16 @@ public function hasRelationAutoloadCallback() */ public function autoloadRelationsUsing(Closure $callback, $context = null) { + // Prevent circular relation autoloading... + if ($context && $this->relationAutoloadContext === $context) { + return $this; + } + $this->relationAutoloadCallback = $callback; + $this->relationAutoloadContext = $context; foreach ($this->relations as $key => $value) { - $this->propagateRelationAutoloadCallbackToRelation($key, $value, $context); + $this->propagateRelationAutoloadCallbackToRelation($key, $value); } return $this; @@ -163,10 +176,9 @@ protected function invokeRelationAutoloadCallbackFor($key, $tuples) * * @param string $key * @param mixed $models - * @param mixed $context * @return void */ - protected function propagateRelationAutoloadCallbackToRelation($key, $models, $context = null) + protected function propagateRelationAutoloadCallbackToRelation($key, $models) { if (! $this->hasRelationAutoloadCallback() || ! $models) { return; @@ -183,10 +195,7 @@ protected function propagateRelationAutoloadCallbackToRelation($key, $models, $c $callback = fn (array $tuples) => $this->invokeRelationAutoloadCallbackFor($key, $tuples); foreach ($models as $model) { - // Check if relation autoload contexts are different to avoid circular relation autoload... - if ((is_null($context) || $context !== $model) && is_object($model) && method_exists($model, 'autoloadRelationsUsing')) { - $model->autoloadRelationsUsing($callback, $context); - } + $model->autoloadRelationsUsing($callback, $this->relationAutoloadContext); } } @@ -1086,7 +1095,7 @@ public function setRelation($relation, $value) { $this->relations[$relation] = $value; - $this->propagateRelationAutoloadCallbackToRelation($relation, $value, $this); + $this->propagateRelationAutoloadCallbackToRelation($relation, $value); return $this; } diff --git a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php index da004c83bc74..5498dc856516 100644 --- a/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/BelongsToManyRelationship.php @@ -50,7 +50,13 @@ public function __construct($factory, $pivot, $relationship) */ public function createFor(Model $model) { - Collection::wrap($this->factory instanceof Factory ? $this->factory->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { + $factoryInstance = $this->factory instanceof Factory; + + if ($factoryInstance) { + $relationship = $model->{$this->relationship}(); + } + + Collection::wrap($factoryInstance ? $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $model) : $this->factory)->each(function ($attachable) use ($model) { $model->{$this->relationship}()->attach( $attachable, is_callable($this->pivot) ? call_user_func($this->pivot, $model) : $this->pivot diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index ffe9018e67f0..a52d840f421e 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -530,6 +530,21 @@ public function state($state) ]); } + /** + * Prepend a new state transformation to the model definition. + * + * @param (callable(array, TModel|null): array)|array $state + * @return static + */ + public function prependState($state) + { + return $this->newInstance([ + 'states' => $this->states->prepend( + is_callable($state) ? $state : fn () => $state, + ), + ]); + } + /** * Set a single model attribute. * diff --git a/src/Illuminate/Database/Eloquent/Factories/Relationship.php b/src/Illuminate/Database/Eloquent/Factories/Relationship.php index 4024f1c929c0..e23bc99d78b0 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Relationship.php +++ b/src/Illuminate/Database/Eloquent/Factories/Relationship.php @@ -49,13 +49,15 @@ public function createFor(Model $parent) $this->factory->state([ $relationship->getMorphType() => $relationship->getMorphClass(), $relationship->getForeignKeyName() => $relationship->getParentKey(), - ])->create([], $parent); + ])->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent); } elseif ($relationship instanceof HasOneOrMany) { $this->factory->state([ $relationship->getForeignKeyName() => $relationship->getParentKey(), - ])->create([], $parent); + ])->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent); } elseif ($relationship instanceof BelongsToMany) { - $relationship->attach($this->factory->create([], $parent)); + $relationship->attach( + $this->factory->prependState($relationship->getQuery()->pendingAttributes)->create([], $parent) + ); } } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index be5a2b3f1dbe..72d7e3315e36 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -2502,6 +2502,7 @@ public function __sleep() $this->classCastCache = []; $this->attributeCastCache = []; $this->relationAutoloadCallback = null; + $this->relationAutoloadContext = null; return array_keys(get_object_vars($this)); } diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index c1fa22a9d7f8..1f00b7bd655f 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -63,7 +63,17 @@ class Builder implements BuilderContract /** * The current query value bindings. * - * @var array + * @var array{ + * select: list, + * from: list, + * join: list, + * where: list, + * groupBy: list, + * having: list, + * order: list, + * union: list, + * unionOrder: list, + * } */ public $bindings = [ 'select' => [], @@ -4127,7 +4137,7 @@ public function getOffset() /** * Get the current query value bindings in a flattened array. * - * @return array + * @return list */ public function getBindings() { @@ -4137,7 +4147,17 @@ public function getBindings() /** * Get the raw array of bindings. * - * @return array + * @return array{ + * select: list, + * from: list, + * join: list, + * where: list, + * groupBy: list, + * having: list, + * order: list, + * union: list, + * unionOrder: list, + * } */ public function getRawBindings() { @@ -4147,6 +4167,7 @@ public function getRawBindings() /** * Set the bindings on the query builder. * + * @param list $bindings * @param string $type * @return $this * @@ -4208,6 +4229,7 @@ public function castBinding($value) /** * Merge an array of bindings into our bindings. * + * @param self $query * @return $this */ public function mergeBindings(self $query) @@ -4220,7 +4242,8 @@ public function mergeBindings(self $query) /** * Remove all of the expressions from a list of bindings. * - * @return array + * @param array $bindings + * @return list */ public function cleanBindings(array $bindings) { diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index 109932a27d12..d70cb9314231 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -9,6 +9,9 @@ use InvalidArgumentException; use LogicException; +/** + * @template TResolver of \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint + */ class Builder { use Macroable; @@ -30,9 +33,9 @@ class Builder /** * The Blueprint resolver callback. * - * @var \Closure + * @var TResolver|null */ - protected $resolver; + protected static $resolver = null; /** * The default string length for migrations. @@ -629,8 +632,8 @@ protected function createBlueprint($table, ?Closure $callback = null) { $connection = $this->connection; - if (isset($this->resolver)) { - return call_user_func($this->resolver, $connection, $table, $callback); + if (static::$resolver !== null) { + return call_user_func(static::$resolver, $connection, $table, $callback); } return Container::getInstance()->make(Blueprint::class, compact('connection', 'table', 'callback')); @@ -698,11 +701,11 @@ public function getConnection() /** * Set the Schema Blueprint resolver callback. * - * @param \Closure $resolver + * @param TResolver|null $resolver * @return void */ - public function blueprintResolver(Closure $resolver) + public function blueprintResolver(?Closure $resolver) { - $this->resolver = $resolver; + static::$resolver = $resolver; } } diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 3c491b0544b2..0fed290349c5 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.10.2'; + const VERSION = '12.12.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Console/ApiInstallCommand.php b/src/Illuminate/Foundation/Console/ApiInstallCommand.php index 9c460555e80a..f8e19c4853f8 100644 --- a/src/Illuminate/Foundation/Console/ApiInstallCommand.php +++ b/src/Illuminate/Foundation/Console/ApiInstallCommand.php @@ -37,7 +37,7 @@ class ApiInstallCommand extends Command /** * Execute the console command. * - * @return int + * @return void */ public function handle() { @@ -67,12 +67,11 @@ public function handle() } if ($this->option('passport')) { - Process::run(array_filter([ + Process::run([ php_binary(), artisan_binary(), 'passport:install', - $this->confirm('Would you like to use UUIDs for all client IDs?') ? '--uuids' : null, - ])); + ]); $this->components->info('API scaffolding installed. Please add the [Laravel\Passport\HasApiTokens] trait to your User model.'); } else { @@ -150,7 +149,7 @@ protected function installSanctum() protected function installPassport() { $this->requireComposerPackages($this->option('composer'), [ - 'laravel/passport:^12.0', + 'laravel/passport:^13.0', ]); } } diff --git a/src/Illuminate/Http/Middleware/AddLinkHeadersForPreloadedAssets.php b/src/Illuminate/Http/Middleware/AddLinkHeadersForPreloadedAssets.php index 7c1de1dae942..247c1507c506 100644 --- a/src/Illuminate/Http/Middleware/AddLinkHeadersForPreloadedAssets.php +++ b/src/Illuminate/Http/Middleware/AddLinkHeadersForPreloadedAssets.php @@ -8,18 +8,31 @@ class AddLinkHeadersForPreloadedAssets { + /** + * Configure the middleware. + * + * @param int $limit + * @return string + */ + public static function using($limit) + { + return static::class.':'.$limit; + } + /** * Handle the incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next + * @param int $limit * @return \Illuminate\Http\Response */ - public function handle($request, $next) + public function handle($request, $next, $limit = null) { - return tap($next($request), function ($response) { + return tap($next($request), function ($response) use ($limit) { if ($response instanceof Response && Vite::preloadedAssets() !== []) { $response->header('Link', (new Collection(Vite::preloadedAssets())) + ->when($limit, fn ($assets, $limit) => $assets->take($limit)) ->map(fn ($attributes, $url) => "<{$url}>; ".implode('; ', $attributes)) ->join(', '), false); } diff --git a/src/Illuminate/Notifications/NotificationSender.php b/src/Illuminate/Notifications/NotificationSender.php index c7e75fd851b2..46ef9e88cf15 100644 --- a/src/Illuminate/Notifications/NotificationSender.php +++ b/src/Illuminate/Notifications/NotificationSender.php @@ -6,11 +6,13 @@ use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Database\Eloquent\Model; +use Illuminate\Notifications\Events\NotificationFailed; use Illuminate\Notifications\Events\NotificationSending; use Illuminate\Notifications\Events\NotificationSent; use Illuminate\Support\Collection; use Illuminate\Support\Str; use Illuminate\Support\Traits\Localizable; +use Throwable; class NotificationSender { @@ -44,6 +46,13 @@ class NotificationSender */ protected $locale; + /** + * Indicates whether a NotificationFailed event has been dispatched. + * + * @var bool + */ + protected $failedEventWasDispatched = false; + /** * Create a new notification sender instance. * @@ -58,6 +67,8 @@ public function __construct($manager, $bus, $events, $locale = null) $this->events = $events; $this->locale = $locale; $this->manager = $manager; + + $this->events->listen(NotificationFailed::class, fn () => $this->failedEventWasDispatched = true); } /** @@ -144,7 +155,19 @@ protected function sendToNotifiable($notifiable, $id, $notification, $channel) return; } - $response = $this->manager->driver($channel)->send($notifiable, $notification); + try { + $response = $this->manager->driver($channel)->send($notifiable, $notification); + } catch (Throwable $exception) { + if (! $this->failedEventWasDispatched) { + $this->events->dispatch( + new NotificationFailed($notifiable, $notification, $channel, ['exception' => $exception]) + ); + } + + $this->failedEventWasDispatched = false; + + throw $exception; + } $this->events->dispatch( new NotificationSent($notifiable, $notification, $channel, $response) diff --git a/src/Illuminate/Queue/BeanstalkdQueue.php b/src/Illuminate/Queue/BeanstalkdQueue.php index d1f64e982492..56e9c4e0664b 100755 --- a/src/Illuminate/Queue/BeanstalkdQueue.php +++ b/src/Illuminate/Queue/BeanstalkdQueue.php @@ -125,7 +125,7 @@ public function later($delay, $job, $data = '', $queue = null) { return $this->enqueueUsing( $job, - $this->createPayload($job, $this->getQueue($queue), $data), + $this->createPayload($job, $this->getQueue($queue), $data, $delay), $queue, $delay, function ($payload, $queue, $delay) { diff --git a/src/Illuminate/Queue/CallQueuedHandler.php b/src/Illuminate/Queue/CallQueuedHandler.php index 0da7e735ca39..c7b887a4d953 100644 --- a/src/Illuminate/Queue/CallQueuedHandler.php +++ b/src/Illuminate/Queue/CallQueuedHandler.php @@ -274,12 +274,17 @@ protected function ensureUniqueJobLockIsReleasedViaContext() * @param array $data * @param \Throwable|null $e * @param string $uuid + * @param \Illuminate\Contracts\Queue\Job|null $job * @return void */ - public function failed(array $data, $e, string $uuid) + public function failed(array $data, $e, string $uuid, ?Job $job = null) { $command = $this->getCommand($data); + if (! is_null($job)) { + $command = $this->setJobInstanceIfNecessary($job, $command); + } + if (! $command instanceof ShouldBeUniqueUntilProcessing) { $this->ensureUniqueJobLockIsReleased($command); } diff --git a/src/Illuminate/Queue/DatabaseQueue.php b/src/Illuminate/Queue/DatabaseQueue.php index 0e6163a2c265..41d04e2b001c 100644 --- a/src/Illuminate/Queue/DatabaseQueue.php +++ b/src/Illuminate/Queue/DatabaseQueue.php @@ -126,7 +126,7 @@ public function later($delay, $job, $data = '', $queue = null) { return $this->enqueueUsing( $job, - $this->createPayload($job, $this->getQueue($queue), $data), + $this->createPayload($job, $this->getQueue($queue), $data, $delay), $queue, $delay, function ($payload, $queue, $delay) { diff --git a/src/Illuminate/Queue/Jobs/FakeJob.php b/src/Illuminate/Queue/Jobs/FakeJob.php index ef1d4e8bc04a..e3567d1f70e2 100644 --- a/src/Illuminate/Queue/Jobs/FakeJob.php +++ b/src/Illuminate/Queue/Jobs/FakeJob.php @@ -2,9 +2,10 @@ namespace Illuminate\Queue\Jobs; +use Illuminate\Contracts\Queue\Job as JobContract; use Illuminate\Support\Str; -class FakeJob extends Job +class FakeJob extends Job implements JobContract { /** * The number of seconds the released job was delayed. diff --git a/src/Illuminate/Queue/Jobs/Job.php b/src/Illuminate/Queue/Jobs/Job.php index 8ec6ac54f805..112501b26580 100755 --- a/src/Illuminate/Queue/Jobs/Job.php +++ b/src/Illuminate/Queue/Jobs/Job.php @@ -251,7 +251,7 @@ protected function failed($e) [$class, $method] = JobName::parse($payload['job']); if (method_exists($this->instance = $this->resolve($class), 'failed')) { - $this->instance->failed($payload['data'], $e, $payload['uuid'] ?? ''); + $this->instance->failed($payload['data'], $e, $payload['uuid'] ?? '', $this); } } diff --git a/src/Illuminate/Queue/Queue.php b/src/Illuminate/Queue/Queue.php index a5f831957b09..49b3cdda6f2c 100755 --- a/src/Illuminate/Queue/Queue.php +++ b/src/Illuminate/Queue/Queue.php @@ -2,6 +2,7 @@ namespace Illuminate\Queue; +use Carbon\Carbon; use Closure; use DateTimeInterface; use Illuminate\Bus\UniqueLock; @@ -97,17 +98,24 @@ public function bulk($jobs, $data = '', $queue = null) * @param \Closure|string|object $job * @param string $queue * @param mixed $data + * @param \DateTimeInterface|\DateInterval|int|null $delay * @return string * * @throws \Illuminate\Queue\InvalidPayloadException */ - protected function createPayload($job, $queue, $data = '') + protected function createPayload($job, $queue, $data = '', $delay = null) { if ($job instanceof Closure) { $job = CallQueuedClosure::create($job); } - $payload = json_encode($value = $this->createPayloadArray($job, $queue, $data), \JSON_UNESCAPED_UNICODE); + $value = $this->createPayloadArray($job, $queue, $data); + + $value['delay'] = isset($delay) + ? $this->secondsUntil($delay) + : null; + + $payload = json_encode($value, \JSON_UNESCAPED_UNICODE); if (json_last_error() !== JSON_ERROR_NONE) { throw new InvalidPayloadException( @@ -156,6 +164,7 @@ protected function createObjectPayload($job, $queue) 'commandName' => $job, 'command' => $job, ], + 'createdAt' => Carbon::now()->getTimestamp(), ]); $command = $this->jobShouldBeEncrypted($job) && $this->container->bound(Encrypter::class) @@ -277,6 +286,7 @@ protected function createStringPayload($job, $queue, $data) 'backoff' => null, 'timeout' => null, 'data' => $data, + 'createdAt' => Carbon::now()->getTimestamp(), ]); } diff --git a/src/Illuminate/Queue/RedisQueue.php b/src/Illuminate/Queue/RedisQueue.php index e8d6d77c7a51..84cfbde358cf 100644 --- a/src/Illuminate/Queue/RedisQueue.php +++ b/src/Illuminate/Queue/RedisQueue.php @@ -192,7 +192,7 @@ public function later($delay, $job, $data = '', $queue = null) { return $this->enqueueUsing( $job, - $this->createPayload($job, $this->getQueue($queue), $data), + $this->createPayload($job, $this->getQueue($queue), $data, $delay), $queue, $delay, function ($payload, $queue, $delay) { diff --git a/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php b/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php index 09fbf4829e7c..25549425b7c0 100644 --- a/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php +++ b/src/Illuminate/Queue/SerializesAndRestoresModelIdentifiers.php @@ -107,7 +107,7 @@ public function restoreModel($value) { return $this->getQueryForModelRestoration( (new $value->class)->setConnection($value->connection), $value->id - )->useWritePdo()->firstOrFail()->load($value->relations ?? []); + )->useWritePdo()->firstOrFail()->loadMissing($value->relations ?? []); } /** diff --git a/src/Illuminate/Queue/SqsQueue.php b/src/Illuminate/Queue/SqsQueue.php index 0d74b8dda43f..a128be81109f 100755 --- a/src/Illuminate/Queue/SqsQueue.php +++ b/src/Illuminate/Queue/SqsQueue.php @@ -128,7 +128,7 @@ public function later($delay, $job, $data = '', $queue = null) { return $this->enqueueUsing( $job, - $this->createPayload($job, $queue ?: $this->default, $data), + $this->createPayload($job, $queue ?: $this->default, $data, $delay), $queue, $delay, function ($payload, $queue, $delay) { diff --git a/src/Illuminate/Support/Facades/Bus.php b/src/Illuminate/Support/Facades/Bus.php index 90e64ac9cf0e..affeb6076985 100644 --- a/src/Illuminate/Support/Facades/Bus.php +++ b/src/Illuminate/Support/Facades/Bus.php @@ -20,6 +20,8 @@ * @method static void dispatchAfterResponse(mixed $command, mixed $handler = null) * @method static \Illuminate\Bus\Dispatcher pipeThrough(array $pipes) * @method static \Illuminate\Bus\Dispatcher map(array $map) + * @method static \Illuminate\Bus\Dispatcher withDispatchingAfterResponses() + * @method static \Illuminate\Bus\Dispatcher withoutDispatchingAfterResponses() * @method static \Illuminate\Support\Testing\Fakes\BusFake except(array|string $jobsToDispatch) * @method static void assertDispatched(string|\Closure $command, callable|int|null $callback = null) * @method static void assertDispatchedTimes(string|\Closure $command, int $times = 1) diff --git a/src/Illuminate/Support/Facades/Schema.php b/src/Illuminate/Support/Facades/Schema.php index 09d0844c8610..d0e3e5f84bf1 100755 --- a/src/Illuminate/Support/Facades/Schema.php +++ b/src/Illuminate/Support/Facades/Schema.php @@ -44,7 +44,7 @@ * @method static string|null getCurrentSchemaName() * @method static array parseSchemaAndTable(string $reference, string|bool|null $withDefaultSchema = null) * @method static \Illuminate\Database\Connection getConnection() - * @method static void blueprintResolver(\Closure $resolver) + * @method static void blueprintResolver(\Closure|null $resolver) * @method static void macro(string $name, object|callable $macro) * @method static void mixin(object $mixin, bool $replace = true) * @method static bool hasMacro(string $name) diff --git a/src/Illuminate/Support/Reflector.php b/src/Illuminate/Support/Reflector.php index a767d5ea7073..f5eb72f0fcdd 100644 --- a/src/Illuminate/Support/Reflector.php +++ b/src/Illuminate/Support/Reflector.php @@ -2,6 +2,7 @@ namespace Illuminate\Support; +use ReflectionAttribute; use ReflectionClass; use ReflectionEnum; use ReflectionMethod; @@ -56,6 +57,46 @@ public static function isCallable($var, $syntaxOnly = false) return false; } + /** + * Get the specified class attribute, optionally following an inheritance chain. + * + * @template TAttribute of object + * + * @param object|class-string $objectOrClass + * @param class-string $attribute + * @return TAttribute|null + */ + public static function getClassAttribute($objectOrClass, $attribute, $ascend = false) + { + return static::getClassAttributes($objectOrClass, $attribute, $ascend)->flatten()->first(); + } + + /** + * Get the specified class attribute(s), optionally following an inheritance chain. + * + * @template TTarget of object + * @template TAttribute of object + * + * @param TTarget|class-string $objectOrClass + * @param class-string $attribute + * @return ($includeParents is true ? Collection, Collection> : Collection) + */ + public static function getClassAttributes($objectOrClass, $attribute, $includeParents = false) + { + $reflectionClass = new ReflectionClass($objectOrClass); + + $attributes = []; + + do { + $attributes[$reflectionClass->name] = new Collection(array_map( + fn (ReflectionAttribute $reflectionAttribute) => $reflectionAttribute->newInstance(), + $reflectionClass->getAttributes($attribute) + )); + } while ($includeParents && false !== $reflectionClass = $reflectionClass->getParentClass()); + + return $includeParents ? new Collection($attributes) : reset($attributes); + } + /** * Get the class name of the given parameter's type, if possible. * diff --git a/src/Illuminate/Validation/Rules/AnyOf.php b/src/Illuminate/Validation/Rules/AnyOf.php index 9d653bdaadbe..a27b20c98100 100644 --- a/src/Illuminate/Validation/Rules/AnyOf.php +++ b/src/Illuminate/Validation/Rules/AnyOf.php @@ -27,7 +27,7 @@ class AnyOf implements Rule, ValidatorAwareRule /** * Sets the validation rules to match against. * - * @param Illuminate\Contracts\Validation\ValidationRule[][] $rules + * @param array $rules * * @throws \InvalidArgumentException */ diff --git a/src/Illuminate/Validation/Rules/Enum.php b/src/Illuminate/Validation/Rules/Enum.php index d100dca4d8ff..4ffd6ec70efe 100644 --- a/src/Illuminate/Validation/Rules/Enum.php +++ b/src/Illuminate/Validation/Rules/Enum.php @@ -16,7 +16,7 @@ class Enum implements Rule, ValidatorAwareRule /** * The type of the enum. * - * @var class-string + * @var class-string<\UnitEnum> */ protected $type; @@ -44,7 +44,7 @@ class Enum implements Rule, ValidatorAwareRule /** * Create a new rule instance. * - * @param class-string $type + * @param class-string<\UnitEnum> $type */ public function __construct($type) { diff --git a/src/Illuminate/View/Compilers/Compiler.php b/src/Illuminate/View/Compilers/Compiler.php index dbd349e69e2a..1e61eae95932 100755 --- a/src/Illuminate/View/Compilers/Compiler.php +++ b/src/Illuminate/View/Compilers/Compiler.php @@ -44,6 +44,13 @@ abstract class Compiler */ protected $compiledExtension = 'php'; + /** + * Indicates if view cache timestamps should be checked. + * + * @var bool + */ + protected $shouldCheckTimestamps; + /** * Create a new compiler instance. * @@ -51,6 +58,7 @@ abstract class Compiler * @param string $cachePath * @param string $basePath * @param bool $shouldCache + * @param bool $shouldCheckTimestamps * @param string $compiledExtension * * @throws \InvalidArgumentException @@ -61,6 +69,7 @@ public function __construct( $basePath = '', $shouldCache = true, $compiledExtension = 'php', + $shouldCheckTimestamps = true, ) { if (! $cachePath) { throw new InvalidArgumentException('Please provide a valid cache path.'); @@ -71,6 +80,7 @@ public function __construct( $this->basePath = $basePath; $this->shouldCache = $shouldCache; $this->compiledExtension = $compiledExtension; + $this->shouldCheckTimestamps = $shouldCheckTimestamps; } /** @@ -107,6 +117,10 @@ public function isExpired($path) return true; } + if (! $this->shouldCheckTimestamps) { + return false; + } + try { return $this->files->lastModified($path) >= $this->files->lastModified($compiled); diff --git a/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php b/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php index 49a524a5fd9f..cefd5fd6eb94 100644 --- a/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php +++ b/src/Illuminate/View/Compilers/Concerns/CompilesUseStatements.php @@ -12,20 +12,35 @@ trait CompilesUseStatements */ protected function compileUse($expression) { - $expression = preg_replace('/[()]/', '', $expression); + $expression = trim(preg_replace('/[()]/', '', $expression), " '\""); - // Preserve grouped imports as-is... + // Isolate alias... if (str_contains($expression, '{')) { - $use = ltrim(trim($expression, " '\""), '\\'); + $pathWithOptionalModifier = $expression; + $aliasWithLeadingSpace = ''; + } else { + $segments = explode(',', $expression); + $pathWithOptionalModifier = trim($segments[0], " '\""); - return ""; + $aliasWithLeadingSpace = isset($segments[1]) + ? ' as '.trim($segments[1], " '\"") + : ''; } - $segments = explode(',', $expression); + // Split modifier and path... + if (str_starts_with($pathWithOptionalModifier, 'function ')) { + $modifierWithTrailingSpace = 'function '; + $path = explode(' ', $pathWithOptionalModifier, 2)[1] ?? $pathWithOptionalModifier; + } elseif (str_starts_with($pathWithOptionalModifier, 'const ')) { + $modifierWithTrailingSpace = 'const '; + $path = explode(' ', $pathWithOptionalModifier, 2)[1] ?? $pathWithOptionalModifier; + } else { + $modifierWithTrailingSpace = ''; + $path = $pathWithOptionalModifier; + } - $use = ltrim(trim($segments[0], " '\""), '\\'); - $as = isset($segments[1]) ? ' as '.trim($segments[1], " '\"") : ''; + $path = ltrim($path, '\\'); - return ""; + return ""; } } diff --git a/src/Illuminate/View/ViewServiceProvider.php b/src/Illuminate/View/ViewServiceProvider.php index 41cd8b93c9aa..2ef3d31177b4 100755 --- a/src/Illuminate/View/ViewServiceProvider.php +++ b/src/Illuminate/View/ViewServiceProvider.php @@ -100,6 +100,7 @@ public function registerBladeCompiler() $app['config']->get('view.relative_hash', false) ? $app->basePath() : '', $app['config']->get('view.cache', true), $app['config']->get('view.compiled_extension', 'php'), + $app['config']->get('view.check_cache_timestamps', true), ), function ($blade) { $blade->component('dynamic-component', DynamicComponent::class); }); diff --git a/tests/Database/DatabaseEloquentFactoryTest.php b/tests/Database/DatabaseEloquentFactoryTest.php index b0860d236b03..da5467794d77 100644 --- a/tests/Database/DatabaseEloquentFactoryTest.php +++ b/tests/Database/DatabaseEloquentFactoryTest.php @@ -850,6 +850,62 @@ public function test_factory_global_model_resolver() $this->assertEquals(FactoryTestGuessModelFactory::new()->modelName(), FactoryTestGuessModel::class); } + public function test_factory_model_has_many_relationship_has_pending_attributes() + { + FactoryTestUser::factory()->has(new FactoryTestPostFactory(), 'postsWithFooBarBazAsTitle')->create(); + + $this->assertEquals('foo bar baz', FactoryTestPost::first()->title); + } + + public function test_factory_model_has_many_relationship_has_pending_attributes_override() + { + FactoryTestUser::factory()->has((new FactoryTestPostFactory())->state(['title' => 'other title']), 'postsWithFooBarBazAsTitle')->create(); + + $this->assertEquals('other title', FactoryTestPost::first()->title); + } + + public function test_factory_model_has_one_relationship_has_pending_attributes() + { + FactoryTestUser::factory()->has(new FactoryTestPostFactory(), 'postWithFooBarBazAsTitle')->create(); + + $this->assertEquals('foo bar baz', FactoryTestPost::first()->title); + } + + public function test_factory_model_has_one_relationship_has_pending_attributes_override() + { + FactoryTestUser::factory()->has((new FactoryTestPostFactory())->state(['title' => 'other title']), 'postWithFooBarBazAsTitle')->create(); + + $this->assertEquals('other title', FactoryTestPost::first()->title); + } + + public function test_factory_model_belongs_to_many_relationship_has_pending_attributes() + { + FactoryTestUser::factory()->has(new FactoryTestRoleFactory(), 'rolesWithFooBarBazAsName')->create(); + + $this->assertEquals('foo bar baz', FactoryTestRole::first()->name); + } + + public function test_factory_model_belongs_to_many_relationship_has_pending_attributes_override() + { + FactoryTestUser::factory()->has((new FactoryTestRoleFactory())->state(['name' => 'other name']), 'rolesWithFooBarBazAsName')->create(); + + $this->assertEquals('other name', FactoryTestRole::first()->name); + } + + public function test_factory_model_morph_many_relationship_has_pending_attributes() + { + (new FactoryTestPostFactory())->has(new FactoryTestCommentFactory(), 'commentsWithFooBarBazAsBody')->create(); + + $this->assertEquals('foo bar baz', FactoryTestComment::first()->body); + } + + public function test_factory_model_morph_many_relationship_has_pending_attributes_override() + { + (new FactoryTestPostFactory())->has((new FactoryTestCommentFactory())->state(['body' => 'other body']), 'commentsWithFooBarBazAsBody')->create(); + + $this->assertEquals('other body', FactoryTestComment::first()->body); + } + /** * Get a database connection instance. * @@ -895,11 +951,26 @@ public function posts() return $this->hasMany(FactoryTestPost::class, 'user_id'); } + public function postsWithFooBarBazAsTitle() + { + return $this->hasMany(FactoryTestPost::class, 'user_id')->withAttributes(['title' => 'foo bar baz']); + } + + public function postWithFooBarBazAsTitle() + { + return $this->hasOne(FactoryTestPost::class, 'user_id')->withAttributes(['title' => 'foo bar baz']); + } + public function roles() { return $this->belongsToMany(FactoryTestRole::class, 'role_user', 'user_id', 'role_id')->withPivot('admin'); } + public function rolesWithFooBarBazAsName() + { + return $this->belongsToMany(FactoryTestRole::class, 'role_user', 'user_id', 'role_id')->withPivot('admin')->withAttributes(['name' => 'foo bar baz']); + } + public function factoryTestRoles() { return $this->belongsToMany(FactoryTestRole::class, 'role_user', 'user_id', 'role_id')->withPivot('admin'); @@ -944,6 +1015,11 @@ public function comments() { return $this->morphMany(FactoryTestComment::class, 'commentable'); } + + public function commentsWithFooBarBazAsBody() + { + return $this->morphMany(FactoryTestComment::class, 'commentable')->withAttributes(['body' => 'foo bar baz']); + } } class FactoryTestCommentFactory extends Factory diff --git a/tests/Http/Middleware/VitePreloadingTest.php b/tests/Http/Middleware/VitePreloadingTest.php index 4fb11f8b0b37..765ee029590a 100644 --- a/tests/Http/Middleware/VitePreloadingTest.php +++ b/tests/Http/Middleware/VitePreloadingTest.php @@ -106,4 +106,47 @@ public function testItDoesNotOverwriteOtherLinkHeaders() $response->headers->all('Link'), ); } + + public function testItCanLimitNumberOfAssetsPreloaded() + { + $app = new Container; + $app->instance(Vite::class, new class extends Vite + { + protected $preloadedAssets = [ + 'https://laravel.com/first.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + 'https://laravel.com/second.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + 'https://laravel.com/third.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + 'https://laravel.com/fourth.js' => [ + 'rel="modulepreload"', + 'foo="bar"', + ], + ]; + }); + Facade::setFacadeApplication($app); + + $response = (new AddLinkHeadersForPreloadedAssets)->handle(new Request, fn () => new Response('ok'), 2); + + $this->assertSame( + [ + '; rel="modulepreload"; foo="bar", ; rel="modulepreload"; foo="bar"', + ], + $response->headers->all('Link'), + ); + } + + public function test_it_can_configure_the_middleware() + { + $definition = AddLinkHeadersForPreloadedAssets::using(limit: 5); + + $this->assertSame('Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets:5', $definition); + } } diff --git a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php index a3f8a5f882f7..03f1d7ca611f 100644 --- a/tests/Integration/Database/EloquentModelRelationAutoloadTest.php +++ b/tests/Integration/Database/EloquentModelRelationAutoloadTest.php @@ -61,6 +61,8 @@ public function testRelationAutoloadForCollection() $this->assertCount(2, DB::getQueryLog()); $this->assertCount(3, $likes); $this->assertTrue($posts[0]->comments[0]->relationLoaded('likes')); + + DB::disableQueryLog(); } public function testRelationAutoloadForSingleModel() @@ -84,6 +86,8 @@ public function testRelationAutoloadForSingleModel() $this->assertCount(2, DB::getQueryLog()); $this->assertCount(2, $likes); $this->assertTrue($post->comments[0]->relationLoaded('likes')); + + DB::disableQueryLog(); } public function testRelationAutoloadWithSerialization() @@ -109,6 +113,50 @@ public function testRelationAutoloadWithSerialization() $this->assertCount(2, DB::getQueryLog()); Model::automaticallyEagerLoadRelationships(false); + + DB::disableQueryLog(); + } + + public function testRelationAutoloadWithCircularRelations() + { + $post = Post::create(); + $comment1 = $post->comments()->create(['parent_id' => null]); + $comment2 = $post->comments()->create(['parent_id' => $comment1->id]); + $post->likes()->create(); + + DB::enableQueryLog(); + + $post->withRelationshipAutoloading(); + $comment = $post->comments->first(); + $comment->setRelation('post', $post); + + $this->assertCount(1, $post->likes); + + $this->assertCount(2, DB::getQueryLog()); + + DB::disableQueryLog(); + } + + public function testRelationAutoloadWithChaperoneRelations() + { + Model::automaticallyEagerLoadRelationships(); + + $post = Post::create(); + $comment1 = $post->comments()->create(['parent_id' => null]); + $comment2 = $post->comments()->create(['parent_id' => $comment1->id]); + $post->likes()->create(); + + DB::enableQueryLog(); + + $post->load('commentsWithChaperone'); + + $this->assertCount(1, $post->likes); + + $this->assertCount(2, DB::getQueryLog()); + + Model::automaticallyEagerLoadRelationships(false); + + DB::disableQueryLog(); } public function testRelationAutoloadVariousNestedMorphRelations() @@ -163,6 +211,8 @@ public function testRelationAutoloadVariousNestedMorphRelations() $this->assertCount(2, $videos); $this->assertTrue($videoLike->relationLoaded('likeable')); $this->assertTrue($videoLike->likeable->relationLoaded('commentable')); + + DB::disableQueryLog(); } } @@ -197,6 +247,11 @@ public function comments() return $this->morphMany(Comment::class, 'commentable'); } + public function commentsWithChaperone() + { + return $this->morphMany(Comment::class, 'commentable')->chaperone(); + } + public function likes() { return $this->morphMany(Like::class, 'likeable'); diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index ebef0f344318..eddfd83c23c1 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -10,6 +10,7 @@ use Illuminate\Queue\Events\JobQueued; use Illuminate\Queue\Events\JobQueueing; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Support\Facades\Bus; use Illuminate\Support\Facades\Config; use Orchestra\Testbench\Attributes\WithMigration; @@ -165,6 +166,37 @@ public function testQueueMayBeNullForJobQueueingAndJobQueuedEvent() $this->assertNull($events[3]->queue); } + public function testCanDisableDispatchingAfterResponse() + { + Job::dispatchAfterResponse('test'); + + $this->assertFalse(Job::$ran); + + $this->app->terminate(); + + $this->assertTrue(Job::$ran); + + Bus::withoutDispatchingAfterResponses(); + + Job::$ran = false; + Job::dispatchAfterResponse('test'); + + $this->assertTrue(Job::$ran); + + $this->app->terminate(); + + Bus::withDispatchingAfterResponses(); + + Job::$ran = false; + Job::dispatchAfterResponse('test'); + + $this->assertFalse(Job::$ran); + + $this->app->terminate(); + + $this->assertTrue(Job::$ran); + } + /** * Helpers. */ diff --git a/tests/Integration/Queue/ModelSerializationTest.php b/tests/Integration/Queue/ModelSerializationTest.php index bd3b401c6576..e13cbb4ec293 100644 --- a/tests/Integration/Queue/ModelSerializationTest.php +++ b/tests/Integration/Queue/ModelSerializationTest.php @@ -31,6 +31,8 @@ protected function setUp(): void { parent::setUp(); + Model::preventLazyLoading(false); + Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email'); @@ -161,6 +163,28 @@ public function testItReloadsRelationships() $this->assertEquals($unSerialized->order->getRelations(), $order->getRelations()); } + public function testItReloadsRelationshipsOnlyOnce() + { + $order = tap(ModelSerializationTestCustomOrder::create(), function (ModelSerializationTestCustomOrder $order) { + $order->wasRecentlyCreated = false; + }); + + $product1 = Product::create(); + $product2 = Product::create(); + + Line::create(['order_id' => $order->id, 'product_id' => $product1->id]); + Line::create(['order_id' => $order->id, 'product_id' => $product2->id]); + + $order->load('line', 'lines', 'products'); + + $this->expectsDatabaseQueryCount(4); + + $serialized = serialize(new ModelRelationSerializationTestClass($order)); + $unSerialized = unserialize($serialized); + + $this->assertEquals($unSerialized->order->getRelations(), $order->getRelations()); + } + public function testItReloadsNestedRelationships() { $order = tap(Order::create(), function (Order $order) { @@ -433,6 +457,29 @@ public function newCollection(array $models = []) } } +class ModelSerializationTestCustomOrder extends Model +{ + public $table = 'orders'; + public $guarded = []; + public $timestamps = false; + public $with = ['line', 'lines', 'products']; + + public function line() + { + return $this->hasOne(Line::class, 'order_id'); + } + + public function lines() + { + return $this->hasMany(Line::class, 'order_id'); + } + + public function products() + { + return $this->belongsToMany(Product::class, 'lines', 'order_id'); + } +} + class Order extends Model { public $guarded = []; diff --git a/tests/Notifications/NotificationChannelManagerTest.php b/tests/Notifications/NotificationChannelManagerTest.php index efcce081aeee..4b272db8399f 100644 --- a/tests/Notifications/NotificationChannelManagerTest.php +++ b/tests/Notifications/NotificationChannelManagerTest.php @@ -2,17 +2,20 @@ namespace Illuminate\Tests\Notifications; +use Exception; use Illuminate\Bus\Queueable; use Illuminate\Container\Container; use Illuminate\Contracts\Bus\Dispatcher as Bus; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\ChannelManager; +use Illuminate\Notifications\Events\NotificationFailed; use Illuminate\Notifications\Events\NotificationSending; use Illuminate\Notifications\Events\NotificationSent; use Illuminate\Notifications\Notifiable; use Illuminate\Notifications\Notification; use Illuminate\Notifications\SendQueuedNotifications; +use Illuminate\Support\Collection; use Mockery as m; use PHPUnit\Framework\TestCase; @@ -34,6 +37,7 @@ public function testNotificationCanBeDispatchedToDriver() Container::setInstance($container); $manager = m::mock(ChannelManager::class.'[driver]', [$container]); $manager->shouldReceive('driver')->andReturn($driver = m::mock()); + $events->shouldReceive('listen')->once(); $events->shouldReceive('until')->with(m::type(NotificationSending::class))->andReturn(true); $driver->shouldReceive('send')->once(); $events->shouldReceive('dispatch')->with(m::type(NotificationSent::class)); @@ -49,6 +53,7 @@ public function testNotificationNotSentOnHalt() $container->instance(Dispatcher::class, $events = m::mock()); Container::setInstance($container); $manager = m::mock(ChannelManager::class.'[driver]', [$container]); + $events->shouldReceive('listen')->once(); $events->shouldReceive('until')->once()->with(m::type(NotificationSending::class))->andReturn(false); $events->shouldReceive('until')->with(m::type(NotificationSending::class))->andReturn(true); $manager->shouldReceive('driver')->once()->andReturn($driver = m::mock()); @@ -66,6 +71,7 @@ public function testNotificationNotSentWhenCancelled() $container->instance(Dispatcher::class, $events = m::mock()); Container::setInstance($container); $manager = m::mock(ChannelManager::class.'[driver]', [$container]); + $events->shouldReceive('listen')->once(); $events->shouldReceive('until')->with(m::type(NotificationSending::class))->andReturn(true); $manager->shouldNotReceive('driver'); $events->shouldNotReceive('dispatch'); @@ -81,6 +87,7 @@ public function testNotificationSentWhenNotCancelled() $container->instance(Dispatcher::class, $events = m::mock()); Container::setInstance($container); $manager = m::mock(ChannelManager::class.'[driver]', [$container]); + $events->shouldReceive('listen')->once(); $events->shouldReceive('until')->with(m::type(NotificationSending::class))->andReturn(true); $manager->shouldReceive('driver')->once()->andReturn($driver = m::mock()); $driver->shouldReceive('send')->once(); @@ -89,6 +96,56 @@ public function testNotificationSentWhenNotCancelled() $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestNotCancelledNotification); } + public function testNotificationNotSentWhenFailed() + { + $this->expectException(Exception::class); + + $container = new Container; + $container->instance('config', ['app.name' => 'Name', 'app.logo' => 'Logo']); + $container->instance(Bus::class, $bus = m::mock()); + $container->instance(Dispatcher::class, $events = m::mock()); + Container::setInstance($container); + $manager = m::mock(ChannelManager::class.'[driver]', [$container]); + $manager->shouldReceive('driver')->andReturn($driver = m::mock()); + $driver->shouldReceive('send')->andThrow(new Exception()); + $events->shouldReceive('listen')->once(); + $events->shouldReceive('until')->with(m::type(NotificationSending::class))->andReturn(true); + $events->shouldReceive('dispatch')->once()->with(m::type(NotificationFailed::class)); + $events->shouldReceive('dispatch')->never()->with(m::type(NotificationSent::class)); + + $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + } + + public function testNotificationFailedDispatchedOnlyOnceWhenFailed() + { + $this->expectException(Exception::class); + + $container = new Container; + $container->instance('config', ['app.name' => 'Name', 'app.logo' => 'Logo']); + $container->instance(Bus::class, $bus = m::mock()); + $container->instance(Dispatcher::class, $events = m::mock(Dispatcher::class)); + Container::setInstance($container); + $manager = m::mock(ChannelManager::class.'[driver]', [$container]); + $manager->shouldReceive('driver')->andReturn($driver = m::mock()); + $driver->shouldReceive('send')->andReturnUsing(function ($notifiable, $notification) use ($events) { + $events->dispatch(new NotificationFailed($notifiable, $notification, 'test')); + throw new Exception(); + }); + $listeners = new Collection(); + $events->shouldReceive('until')->with(m::type(NotificationSending::class))->andReturn(true); + $events->shouldReceive('listen')->once()->andReturnUsing(function ($event, $callback) use ($listeners) { + $listeners->push($callback); + }); + $events->shouldReceive('dispatch')->once()->with(m::type(NotificationFailed::class))->andReturnUsing(function ($event) use ($listeners) { + foreach ($listeners as $listener) { + $listener($event); + } + }); + $events->shouldReceive('dispatch')->never()->with(m::type(NotificationSent::class)); + + $manager->send(new NotificationChannelManagerTestNotifiable, new NotificationChannelManagerTestNotification); + } + public function testNotificationCanBeQueued() { $container = new Container; @@ -98,6 +155,7 @@ public function testNotificationCanBeQueued() $bus->shouldReceive('dispatch')->with(m::type(SendQueuedNotifications::class)); Container::setInstance($container); $manager = m::mock(ChannelManager::class.'[driver]', [$container]); + $events->shouldReceive('listen')->once(); $manager->send([new NotificationChannelManagerTestNotifiable], new NotificationChannelManagerTestQueuedNotification); } diff --git a/tests/Notifications/NotificationSenderTest.php b/tests/Notifications/NotificationSenderTest.php index 4770fc96cd17..6e3a9954938c 100644 --- a/tests/Notifications/NotificationSenderTest.php +++ b/tests/Notifications/NotificationSenderTest.php @@ -30,6 +30,7 @@ public function testItCanSendQueuedNotificationsWithAStringVia() $bus = m::mock(BusDispatcher::class); $bus->shouldReceive('dispatch'); $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen')->once(); $sender = new NotificationSender($manager, $bus, $events); @@ -43,6 +44,7 @@ public function testItCanSendNotificationsWithAnEmptyStringVia() $bus = m::mock(BusDispatcher::class); $bus->shouldNotReceive('dispatch'); $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen')->once(); $sender = new NotificationSender($manager, $bus, $events); @@ -56,6 +58,7 @@ public function testItCannotSendNotificationsViaDatabaseForAnonymousNotifiables( $bus = m::mock(BusDispatcher::class); $bus->shouldNotReceive('dispatch'); $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen')->once(); $sender = new NotificationSender($manager, $bus, $events); @@ -72,6 +75,7 @@ public function testItCanSendQueuedNotificationsThroughMiddleware() return $job->middleware[0] instanceof TestNotificationMiddleware; }); $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen')->once(); $sender = new NotificationSender($manager, $bus, $events); @@ -99,6 +103,7 @@ public function testItCanSendQueuedMultiChannelNotificationsThroughDifferentMidd return empty($job->middleware); }); $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen')->once(); $sender = new NotificationSender($manager, $bus, $events); @@ -122,6 +127,7 @@ public function testItCanSendQueuedWithViaConnectionsNotifications() }); $events = m::mock(EventDispatcher::class); + $events->shouldReceive('listen')->once(); $sender = new NotificationSender($manager, $bus, $events); diff --git a/tests/Queue/QueueBeanstalkdJobTest.php b/tests/Queue/QueueBeanstalkdJobTest.php index 514555001e67..1405cb3f0712 100755 --- a/tests/Queue/QueueBeanstalkdJobTest.php +++ b/tests/Queue/QueueBeanstalkdJobTest.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Queue\Events\JobFailed; use Illuminate\Queue\Jobs\BeanstalkdJob; +use Illuminate\Queue\Jobs\Job; use Mockery as m; use Pheanstalk\Contract\JobIdInterface; use Pheanstalk\Contract\PheanstalkManagerInterface; @@ -39,7 +40,7 @@ public function testFailProperlyCallsTheJobHandler() $job->getPheanstalkJob()->shouldReceive('getData')->andReturn(json_encode(['job' => 'foo', 'uuid' => 'test-uuid', 'data' => ['data']])); $job->getContainer()->shouldReceive('make')->once()->with('foo')->andReturn($handler = m::mock(BeanstalkdJobTestFailedTest::class)); $job->getPheanstalk()->shouldReceive('delete')->once()->with($job->getPheanstalkJob())->andReturnSelf(); - $handler->shouldReceive('failed')->once()->with(['data'], m::type(Exception::class), 'test-uuid'); + $handler->shouldReceive('failed')->once()->with(['data'], m::type(Exception::class), 'test-uuid', m::type(Job::class)); $job->getContainer()->shouldReceive('make')->once()->with(Dispatcher::class)->andReturn($events = m::mock(Dispatcher::class)); $events->shouldReceive('dispatch')->once()->with(m::type(JobFailed::class))->andReturnNull(); diff --git a/tests/Queue/QueueBeanstalkdQueueTest.php b/tests/Queue/QueueBeanstalkdQueueTest.php index ba34a7069305..dfa4eace4a16 100755 --- a/tests/Queue/QueueBeanstalkdQueueTest.php +++ b/tests/Queue/QueueBeanstalkdQueueTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Queue; +use Carbon\Carbon; use Illuminate\Container\Container; use Illuminate\Queue\BeanstalkdQueue; use Illuminate\Queue\Jobs\BeanstalkdJob; @@ -38,6 +39,9 @@ public function testPushProperlyPushesJobOntoBeanstalkd() { $uuid = Str::uuid(); + $time = Carbon::now(); + Carbon::setTestNow($time); + Str::createUuidsUsing(function () use ($uuid) { return $uuid; }); @@ -46,13 +50,14 @@ public function testPushProperlyPushesJobOntoBeanstalkd() $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); - $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), 1024, 0, 60); + $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'delay' => null]), 1024, 0, 60); $this->queue->push('foo', ['data'], 'stack'); $this->queue->push('foo', ['data']); $this->container->shouldHaveReceived('bound')->with('events')->times(4); + Carbon::setTestNow(); Str::createUuidsNormally(); } @@ -64,17 +69,21 @@ public function testDelayedPushProperlyPushesJobOntoBeanstalkd() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $this->setQueue('default', 60); $pheanstalk = $this->queue->getPheanstalk(); $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); $pheanstalk->shouldReceive('useTube')->once()->with(m::type(TubeName::class)); - $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR); + $pheanstalk->shouldReceive('put')->twice()->with(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'delay' => 5]), Pheanstalk::DEFAULT_PRIORITY, 5, Pheanstalk::DEFAULT_TTR); $this->queue->later(5, 'foo', ['data'], 'stack'); $this->queue->later(5, 'foo', ['data']); $this->container->shouldHaveReceived('bound')->with('events')->times(4); + Carbon::setTestNow(); Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueDatabaseQueueUnitTest.php b/tests/Queue/QueueDatabaseQueueUnitTest.php index 3714b99a80b5..9702a4d6695e 100644 --- a/tests/Queue/QueueDatabaseQueueUnitTest.php +++ b/tests/Queue/QueueDatabaseQueueUnitTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Queue; +use Carbon\Carbon; use Illuminate\Container\Container; use Illuminate\Database\Connection; use Illuminate\Queue\DatabaseQueue; @@ -69,6 +70,9 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $queue = $this->getMockBuilder(DatabaseQueue::class) ->onlyMethods(['currentTime']) ->setConstructorArgs([$database = m::mock(Connection::class), 'table', 'default']) @@ -76,9 +80,9 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() $queue->expects($this->any())->method('currentTime')->willReturn('time'); $queue->setContainer($container = m::spy(Container::class)); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); - $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) use ($uuid) { + $query->shouldReceive('insertGetId')->once()->andReturnUsing(function ($array) use ($uuid, $time) { $this->assertSame('default', $array['queue']); - $this->assertSame(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), $array['payload']); + $this->assertSame(json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'delay' => 10]), $array['payload']); $this->assertEquals(0, $array['attempts']); $this->assertNull($array['reserved_at']); $this->assertIsInt($array['available_at']); @@ -88,6 +92,7 @@ public function testDelayedPushProperlyPushesJobOntoDatabase() $container->shouldHaveReceived('bound')->with('events')->twice(); + Carbon::setTestNow(); Str::createUuidsNormally(); } @@ -130,22 +135,25 @@ public function testBulkBatchPushesOntoDatabase() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $database = m::mock(Connection::class); $queue = $this->getMockBuilder(DatabaseQueue::class)->onlyMethods(['currentTime', 'availableAt'])->setConstructorArgs([$database, 'table', 'default'])->getMock(); $queue->expects($this->any())->method('currentTime')->willReturn('created'); $queue->expects($this->any())->method('availableAt')->willReturn('available'); $database->shouldReceive('table')->with('table')->andReturn($query = m::mock(stdClass::class)); - $query->shouldReceive('insert')->once()->andReturnUsing(function ($records) use ($uuid) { + $query->shouldReceive('insert')->once()->andReturnUsing(function ($records) use ($uuid, $time) { $this->assertEquals([[ 'queue' => 'queue', - 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), + 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'delay' => null]), 'attempts' => 0, 'reserved_at' => null, 'available_at' => 'available', 'created_at' => 'created', ], [ 'queue' => 'queue', - 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'bar', 'job' => 'bar', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data']]), + 'payload' => json_encode(['uuid' => $uuid, 'displayName' => 'bar', 'job' => 'bar', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'delay' => null]), 'attempts' => 0, 'reserved_at' => null, 'available_at' => 'available', @@ -155,6 +163,7 @@ public function testBulkBatchPushesOntoDatabase() $queue->bulk(['foo', 'bar'], ['data'], 'queue'); + Carbon::setTestNow(); Str::createUuidsNormally(); } diff --git a/tests/Queue/QueueRedisQueueTest.php b/tests/Queue/QueueRedisQueueTest.php index 007f743653d8..7ea0bfdbe076 100644 --- a/tests/Queue/QueueRedisQueueTest.php +++ b/tests/Queue/QueueRedisQueueTest.php @@ -27,16 +27,20 @@ public function testPushProperlyPushesJobOntoRedis() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->setContainer($container = m::spy(Container::class)); $redis->shouldReceive('connection')->once()->andReturn($redis); - $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'id' => 'foo', 'attempts' => 0])); + $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => null])); $id = $queue->push('foo', ['data']); $this->assertSame('foo', $id); $container->shouldHaveReceived('bound')->with('events')->twice(); + Carbon::setTestNow(); Str::createUuidsNormally(); } @@ -48,11 +52,14 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->setContainer($container = m::spy(Container::class)); $redis->shouldReceive('connection')->once()->andReturn($redis); - $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'custom' => 'taylor', 'id' => 'foo', 'attempts' => 0])); + $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'custom' => 'taylor', 'id' => 'foo', 'attempts' => 0, 'delay' => null])); Queue::createPayloadUsing(function ($connection, $queue, $payload) { return ['custom' => 'taylor']; @@ -64,6 +71,7 @@ public function testPushProperlyPushesJobOntoRedisWithCustomPayloadHook() Queue::createPayloadUsing(null); + Carbon::setTestNow(); Str::createUuidsNormally(); } @@ -75,11 +83,14 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); $queue->setContainer($container = m::spy(Container::class)); $redis->shouldReceive('connection')->once()->andReturn($redis); - $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'custom' => 'taylor', 'bar' => 'foo', 'id' => 'foo', 'attempts' => 0])); + $redis->shouldReceive('eval')->once()->with(LuaScripts::push(), 2, 'queues:default', 'queues:default:notify', json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'custom' => 'taylor', 'bar' => 'foo', 'id' => 'foo', 'attempts' => 0, 'delay' => null])); Queue::createPayloadUsing(function ($connection, $queue, $payload) { return ['custom' => 'taylor']; @@ -95,6 +106,7 @@ public function testPushProperlyPushesJobOntoRedisWithTwoCustomPayloadHook() Queue::createPayloadUsing(null); + Carbon::setTestNow(); Str::createUuidsNormally(); } @@ -106,6 +118,9 @@ public function testDelayedPushProperlyPushesJobOntoRedis() return $uuid; }); + $time = Carbon::now(); + Carbon::setTestNow($time); + $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['availableAt', 'getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); @@ -115,13 +130,14 @@ public function testDelayedPushProperlyPushesJobOntoRedis() $redis->shouldReceive('zadd')->once()->with( 'queues:default:delayed', 2, - json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'id' => 'foo', 'attempts' => 0]) + json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 1]) ); $id = $queue->later(1, 'foo', ['data']); $this->assertSame('foo', $id); $container->shouldHaveReceived('bound')->with('events')->twice(); + Carbon::setTestNow(); Str::createUuidsNormally(); } @@ -133,22 +149,24 @@ public function testDelayedPushWithDateTimeProperlyPushesJobOntoRedis() return $uuid; }); - $date = Carbon::now(); + $time = $date = Carbon::now(); + Carbon::setTestNow($time); $queue = $this->getMockBuilder(RedisQueue::class)->onlyMethods(['availableAt', 'getRandomId'])->setConstructorArgs([$redis = m::mock(Factory::class), 'default'])->getMock(); $queue->setContainer($container = m::spy(Container::class)); $queue->expects($this->once())->method('getRandomId')->willReturn('foo'); - $queue->expects($this->once())->method('availableAt')->with($date)->willReturn(2); + $queue->expects($this->once())->method('availableAt')->with($date)->willReturn(5); $redis->shouldReceive('connection')->once()->andReturn($redis); $redis->shouldReceive('zadd')->once()->with( 'queues:default:delayed', - 2, - json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'id' => 'foo', 'attempts' => 0]) + 5, + json_encode(['uuid' => $uuid, 'displayName' => 'foo', 'job' => 'foo', 'maxTries' => null, 'maxExceptions' => null, 'failOnTimeout' => false, 'backoff' => null, 'timeout' => null, 'data' => ['data'], 'createdAt' => $time->getTimestamp(), 'id' => 'foo', 'attempts' => 0, 'delay' => 5]) ); - $queue->later($date, 'foo', ['data']); + $queue->later($date->addSeconds(5), 'foo', ['data']); $container->shouldHaveReceived('bound')->with('events')->twice(); + Carbon::setTestNow(); Str::createUuidsNormally(); } } diff --git a/tests/Queue/QueueSyncQueueTest.php b/tests/Queue/QueueSyncQueueTest.php index e7aab86ad779..901f66c2d75e 100755 --- a/tests/Queue/QueueSyncQueueTest.php +++ b/tests/Queue/QueueSyncQueueTest.php @@ -61,6 +61,28 @@ public function testFailedJobGetsHandledWhenAnExceptionIsThrown() Container::setInstance(); } + public function testFailedJobHasAccessToJobInstance() + { + unset($_SERVER['__sync.failed']); + + $sync = new SyncQueue; + $container = new Container; + $container->bind(\Illuminate\Contracts\Events\Dispatcher::class, \Illuminate\Events\Dispatcher::class); + $container->bind(\Illuminate\Contracts\Bus\Dispatcher::class, \Illuminate\Bus\Dispatcher::class); + $container->bind(\Illuminate\Contracts\Container\Container::class, \Illuminate\Container\Container::class); + $sync->setContainer($container); + + SyncQueue::createPayloadUsing(function ($connection, $queue, $payload) { + return ['data' => ['extra' => 'extraValue']]; + }); + + try { + $sync->push(new FailingSyncQueueJob()); + } catch (LogicException $e) { + $this->assertSame('extraValue', $_SERVER['__sync.failed']); + } + } + public function testCreatesPayloadObject() { $sync = new SyncQueue; @@ -177,6 +199,23 @@ public function failed() } } +class FailingSyncQueueJob implements ShouldQueue +{ + use InteractsWithQueue; + + public function handle() + { + throw new LogicException(); + } + + public function failed() + { + $payload = $this->job->payload(); + + $_SERVER['__sync.failed'] = $payload['data']['extra']; + } +} + class SyncQueueJob implements ShouldQueue { use InteractsWithQueue; diff --git a/tests/Support/Fixtures/ClassesWithAttributes.php b/tests/Support/Fixtures/ClassesWithAttributes.php new file mode 100644 index 000000000000..baed29f97fd3 --- /dev/null +++ b/tests/Support/Fixtures/ClassesWithAttributes.php @@ -0,0 +1,41 @@ +assertSame('bar', Arr::get(['' => ['' => 'bar']], '.')); } + public function testItGetsAString() + { + $test_array = ['string' => 'foo bar', 'integer' => 1234]; + + // Test string values are returned as strings + $this->assertSame( + 'foo bar', Arr::string($test_array, 'string') + ); + + // Test that default string values are returned for missing keys + $this->assertSame( + 'default', Arr::string($test_array, 'missing_key', 'default') + ); + + // Test that an exception is raised if the value is not a string + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('#^Array value for key \[integer\] must be a string, (.*) found.#'); + Arr::string($test_array, 'integer'); + } + + public function testItGetsAnInteger() + { + $test_array = ['string' => 'foo bar', 'integer' => 1234]; + + // Test integer values are returned as integers + $this->assertSame( + 1234, Arr::integer($test_array, 'integer') + ); + + // Test that default integer values are returned for missing keys + $this->assertSame( + 999, Arr::integer($test_array, 'missing_key', 999) + ); + + // Test that an exception is raised if the value is not an integer + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('#^Array value for key \[string\] must be an integer, (.*) found.#'); + Arr::integer($test_array, 'string'); + } + + public function testItGetsAFloat() + { + $test_array = ['string' => 'foo bar', 'float' => 12.34]; + + // Test float values are returned as floats + $this->assertSame( + 12.34, Arr::float($test_array, 'float') + ); + + // Test that default float values are returned for missing keys + $this->assertSame( + 56.78, Arr::float($test_array, 'missing_key', 56.78) + ); + + // Test that an exception is raised if the value is not a float + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('#^Array value for key \[string\] must be a float, (.*) found.#'); + Arr::float($test_array, 'string'); + } + + public function testItGetsABoolean() + { + $test_array = ['string' => 'foo bar', 'boolean' => true]; + + // Test boolean values are returned as booleans + $this->assertSame( + true, Arr::boolean($test_array, 'boolean') + ); + + // Test that default boolean values are returned for missing keys + $this->assertSame( + true, Arr::boolean($test_array, 'missing_key', true) + ); + + // Test that an exception is raised if the value is not a boolean + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('#^Array value for key \[string\] must be a boolean, (.*) found.#'); + Arr::boolean($test_array, 'string'); + } + + public function testItGetsAnArray() + { + $test_array = ['string' => 'foo bar', 'array' => ['foo', 'bar']]; + + // Test array values are returned as arrays + $this->assertSame( + ['foo', 'bar'], Arr::array($test_array, 'array') + ); + + // Test that default array values are returned for missing keys + $this->assertSame( + [1, 'two'], Arr::array($test_array, 'missing_key', [1, 'two']) + ); + + // Test that an exception is raised if the value is not an array + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessageMatches('#^Array value for key \[string\] must be an array, (.*) found.#'); + Arr::array($test_array, 'string'); + } + public function testHas() { $array = ['products.desk' => ['price' => 100]]; diff --git a/tests/Support/SupportReflectorTest.php b/tests/Support/SupportReflectorTest.php index 2933ff090795..67e438361d8f 100644 --- a/tests/Support/SupportReflectorTest.php +++ b/tests/Support/SupportReflectorTest.php @@ -75,6 +75,63 @@ public function testIsCallable() $this->assertFalse(Reflector::isCallable(['TotallyMissingClass', 'foo'])); $this->assertTrue(Reflector::isCallable(['TotallyMissingClass', 'foo'], true)); } + + public function testGetClassAttributes() + { + require_once __DIR__.'/Fixtures/ClassesWithAttributes.php'; + + $this->assertSame([], Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\UnusedAttr::class)->toArray()); + + $this->assertSame( + [Fixtures\ChildClass::class => [], Fixtures\ParentClass::class => []], + Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\UnusedAttr::class, true)->toArray() + ); + + $this->assertSame( + ['quick', 'brown', 'fox'], + Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\StrAttr::class)->map->string->all() + ); + + $this->assertSame( + ['quick', 'brown', 'fox', 'lazy', 'dog'], + Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\StrAttr::class, true)->flatten()->map->string->all() + ); + + $this->assertSame(7, Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\NumAttr::class)->sum->number); + $this->assertSame(12, Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\NumAttr::class, true)->flatten()->sum->number); + $this->assertSame(5, Reflector::getClassAttributes(Fixtures\ParentClass::class, Fixtures\NumAttr::class)->sum->number); + $this->assertSame(5, Reflector::getClassAttributes(Fixtures\ParentClass::class, Fixtures\NumAttr::class, true)->flatten()->sum->number); + + $this->assertSame( + [Fixtures\ChildClass::class, Fixtures\ParentClass::class], + Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\StrAttr::class, true)->keys()->all() + ); + + $this->assertContainsOnlyInstancesOf( + Fixtures\StrAttr::class, + Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\StrAttr::class)->all() + ); + + $this->assertContainsOnlyInstancesOf( + Fixtures\StrAttr::class, + Reflector::getClassAttributes(Fixtures\ChildClass::class, Fixtures\StrAttr::class, true)->flatten()->all() + ); + } + + public function testGetClassAttribute() + { + require_once __DIR__.'/Fixtures/ClassesWithAttributes.php'; + + $this->assertNull(Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\UnusedAttr::class)); + $this->assertNull(Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\UnusedAttr::class, true)); + $this->assertNull(Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\ParentOnlyAttr::class)); + $this->assertInstanceOf(Fixtures\ParentOnlyAttr::class, Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\ParentOnlyAttr::class, true)); + $this->assertInstanceOf(Fixtures\StrAttr::class, Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\StrAttr::class)); + $this->assertInstanceOf(Fixtures\StrAttr::class, Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\StrAttr::class, true)); + $this->assertSame('quick', Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\StrAttr::class)->string); + $this->assertSame('quick', Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\StrAttr::class, true)->string); + $this->assertSame('lazy', Reflector::getClassAttribute(Fixtures\ParentClass::class, Fixtures\StrAttr::class)->string); + } } class A diff --git a/tests/View/Blade/BladeUseTest.php b/tests/View/Blade/BladeUseTest.php index 980a266128a6..28072b64559c 100644 --- a/tests/View/Blade/BladeUseTest.php +++ b/tests/View/Blade/BladeUseTest.php @@ -69,4 +69,70 @@ public function testUseStatementWithBracesAndBackslashAreCompiledCorrectly() $string = "Foo @use(\SomeNamespace\{Foo, Bar}) bar"; $this->assertEquals($expected, $this->compiler->compileString($string)); } + + public function testUseStatementsWithModifiersAreCompiled() + { + $expected = 'Foo bar'; + + $string = "Foo @use('function SomeNamespace\SomeFunction', 'Foo') bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + + $string = 'Foo @use(function SomeNamespace\SomeFunction, Foo) bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testUseStatementsWithModifiersWithoutAliasAreCompiled() + { + $expected = 'Foo bar'; + + $string = "Foo @use('const SomeNamespace\SOME_CONST') bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + + $string = 'Foo @use(const SomeNamespace\SOME_CONST) bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testUseStatementsWithModifiersAndBackslashAtBeginningAreCompiled() + { + $expected = 'Foo bar'; + + $string = "Foo @use('function \SomeNamespace\SomeFunction') bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + + $string = 'Foo @use(function \SomeNamespace\SomeFunction) bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testUseStatementsWithModifiersBackslashAtBeginningAndAliasedAreCompiled() + { + $expected = 'Foo bar'; + + $string = "Foo @use('const \SomeNamespace\SOME_CONST', 'Foo') bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + + $string = 'Foo @use(const \SomeNamespace\SOME_CONST, Foo) bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testUseStatementsWithModifiersWithBracesAreCompiledCorrectly() + { + $expected = 'Foo bar'; + + $string = "Foo @use('function SomeNamespace\{Foo, Bar}') bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + + $string = 'Foo @use(function SomeNamespace\{Foo, Bar}) bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } + + public function testUseFunctionStatementWithBracesAndBackslashAreCompiledCorrectly() + { + $expected = 'Foo bar'; + + $string = "Foo @use('const \SomeNamespace\{FOO, BAR}') bar"; + $this->assertEquals($expected, $this->compiler->compileString($string)); + + $string = 'Foo @use(const \SomeNamespace\{FOO, BAR}) bar'; + $this->assertEquals($expected, $this->compiler->compileString($string)); + } } diff --git a/tests/View/ViewBladeCompilerTest.php b/tests/View/ViewBladeCompilerTest.php index ed59a62fbbe7..b26693489c81 100644 --- a/tests/View/ViewBladeCompilerTest.php +++ b/tests/View/ViewBladeCompilerTest.php @@ -51,10 +51,17 @@ public function testIsExpiredReturnsFalseWhenUseCacheIsTrueAndNoFileModification public function testIsExpiredReturnsTrueWhenUseCacheIsFalse() { - $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__, $basePath = '', $useCache = false); + $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__, shouldCache: false); $this->assertTrue($compiler->isExpired('foo')); } + public function testIsExpiredReturnsFalseWhenIgnoreCacheTimestampsIsTrue() + { + $compiler = new BladeCompiler($files = $this->getFiles(), __DIR__, shouldCheckTimestamps: false); + $files->shouldReceive('exists')->once()->with(__DIR__.'/'.hash('xxh128', 'v2foo').'.php')->andReturn(true); + $this->assertFalse($compiler->isExpired('foo')); + } + public function testCompilePathIsProperlyCreated() { $compiler = new BladeCompiler($this->getFiles(), __DIR__);