diff --git a/CHANGELOG.md b/CHANGELOG.md index 55abba5ea7ce..0198d02e6505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,29 @@ # Release Notes for 12.x -## [Unreleased](https://github.com/laravel/framework/compare/v12.12.0...12.x) +## [Unreleased](https://github.com/laravel/framework/compare/v12.13.0...12.x) + +## [v12.13.0](https://github.com/laravel/framework/compare/v12.12.0...v12.13.0) - 2025-05-07 + +* [12.x] fix no arguments return type in request class by [@olivernybroe](https://github.com/olivernybroe) in https://github.com/laravel/framework/pull/55631 +* [12.x] Add support for callback evaluation in containsOneItem method by [@fernandokbs](https://github.com/fernandokbs) in https://github.com/laravel/framework/pull/55622 +* [12.x] add generics to aggregate related methods and properties by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55628 +* [12.x] Fix typo in PHPDoc by [@AhmedAlaa4611](https://github.com/AhmedAlaa4611) in https://github.com/laravel/framework/pull/55636 +* [12.x] Allow naming queued closures by [@willrowe](https://github.com/willrowe) in https://github.com/laravel/framework/pull/55634 +* [12.x] Add `assertRedirectBack` assertion method by [@ryangjchandler](https://github.com/ryangjchandler) in https://github.com/laravel/framework/pull/55635 +* [12.x] Typehints for bindings by [@cosmastech](https://github.com/cosmastech) in https://github.com/laravel/framework/pull/55633 +* [12.x] add PHP Doc types to arrays for methods in Database\Grammar by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55629 +* fix trim null arg deprecation by [@apreiml](https://github.com/apreiml) in https://github.com/laravel/framework/pull/55649 +* [12.x] Support predis/predis 3.x by [@gabrielrbarbosa](https://github.com/gabrielrbarbosa) in https://github.com/laravel/framework/pull/55641 +* Bump vite from 5.4.18 to 5.4.19 in /src/Illuminate/Foundation/resources/exceptions/renderer by [@dependabot](https://github.com/dependabot) in https://github.com/laravel/framework/pull/55655 +* [12.x] Fix predis versions by [@GrahamCampbell](https://github.com/GrahamCampbell) in https://github.com/laravel/framework/pull/55654 +* [12.x] Bump minimum league/commonmark by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55659 +* [12.x] Fix typo in MemoizedStoreTest by [@szepeviktor](https://github.com/szepeviktor) in https://github.com/laravel/framework/pull/55662 +* [12.x] Queue event listeners with enum values by [@wgriffioen](https://github.com/wgriffioen) in https://github.com/laravel/framework/pull/55656 +* [12.x] Implement releaseAfter method in RateLimited middleware by [@adamjgriffith](https://github.com/adamjgriffith) in https://github.com/laravel/framework/pull/55671 +* [12.x] Improve Cache Tests by [@nuernbergerA](https://github.com/nuernbergerA) in https://github.com/laravel/framework/pull/55670 +* [12.x] Only pass model IDs to Eloquent `whereAttachedTo` method by [@ashleyshenton](https://github.com/ashleyshenton) in https://github.com/laravel/framework/pull/55666 +* feat(bus): allow adding multiple jobs to chain by [@dallyger](https://github.com/dallyger) in https://github.com/laravel/framework/pull/55668 +* [12.x] add generics to QueryBuilder’s column related methods by [@taka-oyama](https://github.com/taka-oyama) in https://github.com/laravel/framework/pull/55663 ## [v12.12.0](https://github.com/laravel/framework/compare/v12.11.1...v12.12.0) - 2025-05-01 diff --git a/src/Illuminate/Auth/AuthManager.php b/src/Illuminate/Auth/AuthManager.php index 70723558886e..131959148b06 100755 --- a/src/Illuminate/Auth/AuthManager.php +++ b/src/Illuminate/Auth/AuthManager.php @@ -126,6 +126,7 @@ public function createSessionDriver($name, $config) $this->createUserProvider($config['provider'] ?? null), $this->app['session.store'], rehashOnLogin: $this->app['config']->get('hashing.rehash_on_login', true), + timeboxDuration: $this->app['config']->get('auth.timebox_duration', 200000), ); // When using the remember me functionality of the authentication services we diff --git a/src/Illuminate/Auth/Passwords/PasswordBroker.php b/src/Illuminate/Auth/Passwords/PasswordBroker.php index 89565eb77b3a..d955f3e42e1d 100755 --- a/src/Illuminate/Auth/Passwords/PasswordBroker.php +++ b/src/Illuminate/Auth/Passwords/PasswordBroker.php @@ -9,6 +9,7 @@ use Illuminate\Contracts\Auth\UserProvider; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Arr; +use Illuminate\Support\Timebox; use UnexpectedValueException; class PasswordBroker implements PasswordBrokerContract @@ -34,18 +35,41 @@ class PasswordBroker implements PasswordBrokerContract */ protected $events; + /** + * The timebox instance. + * + * @var \Illuminate\Support\Timebox + */ + protected $timebox; + + /** + * The number of microseconds that the timebox should wait for. + * + * @var int + */ + protected $timeboxDuration; + /** * Create a new password broker instance. * * @param \Illuminate\Auth\Passwords\TokenRepositoryInterface $tokens * @param \Illuminate\Contracts\Auth\UserProvider $users * @param \Illuminate\Contracts\Events\Dispatcher|null $dispatcher + * @param \Illuminate\Support\Timebox|null $timebox + * @param int $timeboxDuration */ - public function __construct(#[\SensitiveParameter] TokenRepositoryInterface $tokens, UserProvider $users, ?Dispatcher $dispatcher = null) - { + public function __construct( + #[\SensitiveParameter] TokenRepositoryInterface $tokens, + UserProvider $users, + ?Dispatcher $dispatcher = null, + ?Timebox $timebox = null, + int $timeboxDuration = 200000, + ) { $this->users = $users; $this->tokens = $tokens; $this->events = $dispatcher; + $this->timebox = $timebox ?: new Timebox; + $this->timeboxDuration = $timeboxDuration; } /** @@ -57,33 +81,35 @@ public function __construct(#[\SensitiveParameter] TokenRepositoryInterface $tok */ public function sendResetLink(#[\SensitiveParameter] array $credentials, ?Closure $callback = null) { - // First we will check to see if we found a user at the given credentials and - // if we did not we will redirect back to this current URI with a piece of - // "flash" data in the session to indicate to the developers the errors. - $user = $this->getUser($credentials); + return $this->timebox->call(function () use ($credentials, $callback) { + // First we will check to see if we found a user at the given credentials and + // if we did not we will redirect back to this current URI with a piece of + // "flash" data in the session to indicate to the developers the errors. + $user = $this->getUser($credentials); - if (is_null($user)) { - return static::INVALID_USER; - } + if (is_null($user)) { + return static::INVALID_USER; + } - if ($this->tokens->recentlyCreatedToken($user)) { - return static::RESET_THROTTLED; - } + if ($this->tokens->recentlyCreatedToken($user)) { + return static::RESET_THROTTLED; + } - $token = $this->tokens->create($user); + $token = $this->tokens->create($user); - if ($callback) { - return $callback($user, $token) ?? static::RESET_LINK_SENT; - } + if ($callback) { + return $callback($user, $token) ?? static::RESET_LINK_SENT; + } - // Once we have the reset token, we are ready to send the message out to this - // user with a link to reset their password. We will then redirect back to - // the current URI having nothing set in the session to indicate errors. - $user->sendPasswordResetNotification($token); + // Once we have the reset token, we are ready to send the message out to this + // user with a link to reset their password. We will then redirect back to + // the current URI having nothing set in the session to indicate errors. + $user->sendPasswordResetNotification($token); - $this->events?->dispatch(new PasswordResetLinkSent($user)); + $this->events?->dispatch(new PasswordResetLinkSent($user)); - return static::RESET_LINK_SENT; + return static::RESET_LINK_SENT; + }, $this->timeboxDuration); } /** @@ -95,25 +121,29 @@ public function sendResetLink(#[\SensitiveParameter] array $credentials, ?Closur */ public function reset(#[\SensitiveParameter] array $credentials, Closure $callback) { - $user = $this->validateReset($credentials); + return $this->timebox->call(function ($timebox) use ($credentials, $callback) { + $user = $this->validateReset($credentials); - // If the responses from the validate method is not a user instance, we will - // assume that it is a redirect and simply return it from this method and - // the user is properly redirected having an error message on the post. - if (! $user instanceof CanResetPasswordContract) { - return $user; - } + // If the responses from the validate method is not a user instance, we will + // assume that it is a redirect and simply return it from this method and + // the user is properly redirected having an error message on the post. + if (! $user instanceof CanResetPasswordContract) { + return $user; + } - $password = $credentials['password']; + $password = $credentials['password']; - // Once the reset has been validated, we'll call the given callback with the - // new password. This gives the user an opportunity to store the password - // in their persistent storage. Then we'll delete the token and return. - $callback($user, $password); + // Once the reset has been validated, we'll call the given callback with the + // new password. This gives the user an opportunity to store the password + // in their persistent storage. Then we'll delete the token and return. + $callback($user, $password); - $this->tokens->delete($user); + $this->tokens->delete($user); + + $timebox->returnEarly(); - return static::PASSWORD_RESET; + return static::PASSWORD_RESET; + }, $this->timeboxDuration); } /** @@ -199,4 +229,14 @@ public function getRepository() { return $this->tokens; } + + /** + * Get the timebox instance used by the guard. + * + * @return \Illuminate\Support\Timebox + */ + public function getTimebox() + { + return $this->timebox; + } } diff --git a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php index 516638b17f5f..3946e596ef9f 100644 --- a/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php +++ b/src/Illuminate/Auth/Passwords/PasswordBrokerManager.php @@ -70,6 +70,7 @@ protected function resolve($name) $this->createTokenRepository($config), $this->app['auth']->createUserProvider($config['provider'] ?? null), $this->app['events'] ?? null, + timeboxDuration: $this->app['config']->get('auth.timebox_duration', 200000), ); } diff --git a/src/Illuminate/Auth/SessionGuard.php b/src/Illuminate/Auth/SessionGuard.php index 13bd15f46c5a..985b0bb4407c 100644 --- a/src/Illuminate/Auth/SessionGuard.php +++ b/src/Illuminate/Auth/SessionGuard.php @@ -96,6 +96,13 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth */ protected $timebox; + /** + * The number of microseconds that the timebox should wait for. + * + * @var int + */ + protected $timeboxDuration; + /** * Indicates if passwords should be rehashed on login if needed. * @@ -126,6 +133,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth * @param \Symfony\Component\HttpFoundation\Request|null $request * @param \Illuminate\Support\Timebox|null $timebox * @param bool $rehashOnLogin + * @param int $timeboxDuration */ public function __construct( $name, @@ -134,6 +142,7 @@ public function __construct( ?Request $request = null, ?Timebox $timebox = null, bool $rehashOnLogin = true, + int $timeboxDuration = 200000, ) { $this->name = $name; $this->session = $session; @@ -141,6 +150,7 @@ public function __construct( $this->provider = $provider; $this->timebox = $timebox ?: new Timebox; $this->rehashOnLogin = $rehashOnLogin; + $this->timeboxDuration = $timeboxDuration; } /** @@ -290,9 +300,17 @@ public function onceUsingId($id) */ public function validate(array $credentials = []) { - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + return $this->timebox->call(function ($timebox) use ($credentials) { + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - return $this->hasValidCredentials($user, $credentials); + $validated = $this->hasValidCredentials($user, $credentials); + + if ($validated) { + $timebox->returnEarly(); + } + + return $validated; + }, $this->timeboxDuration); } /** @@ -390,27 +408,31 @@ protected function failedBasicResponse() */ public function attempt(array $credentials = [], $remember = false) { - $this->fireAttemptEvent($credentials, $remember); + return $this->timebox->call(function ($timebox) use ($credentials, $remember) { + $this->fireAttemptEvent($credentials, $remember); - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - // If an implementation of UserInterface was returned, we'll ask the provider - // to validate the user against the given credentials, and if they are in - // fact valid we'll log the users into the application and return true. - if ($this->hasValidCredentials($user, $credentials)) { - $this->rehashPasswordIfRequired($user, $credentials); + // If an implementation of UserInterface was returned, we'll ask the provider + // to validate the user against the given credentials, and if they are in + // fact valid we'll log the users into the application and return true. + if ($this->hasValidCredentials($user, $credentials)) { + $this->rehashPasswordIfRequired($user, $credentials); - $this->login($user, $remember); + $this->login($user, $remember); - return true; - } + $timebox->returnEarly(); - // If the authentication attempt fails we will fire an event so that the user - // may be notified of any suspicious attempts to access their account from - // an unrecognized user. A developer may listen to this event as needed. - $this->fireFailedEvent($user, $credentials); + return true; + } - return false; + // If the authentication attempt fails we will fire an event so that the user + // may be notified of any suspicious attempts to access their account from + // an unrecognized user. A developer may listen to this event as needed. + $this->fireFailedEvent($user, $credentials); + + return false; + }, $this->timeboxDuration); } /** @@ -423,24 +445,28 @@ public function attempt(array $credentials = [], $remember = false) */ public function attemptWhen(array $credentials = [], $callbacks = null, $remember = false) { - $this->fireAttemptEvent($credentials, $remember); + return $this->timebox->call(function ($timebox) use ($credentials, $callbacks, $remember) { + $this->fireAttemptEvent($credentials, $remember); - $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); + $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials); - // This method does the exact same thing as attempt, but also executes callbacks after - // the user is retrieved and validated. If one of the callbacks returns falsy we do - // not login the user. Instead, we will fail the specific authentication attempt. - if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) { - $this->rehashPasswordIfRequired($user, $credentials); + // This method does the exact same thing as attempt, but also executes callbacks after + // the user is retrieved and validated. If one of the callbacks returns falsy we do + // not login the user. Instead, we will fail the specific authentication attempt. + if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) { + $this->rehashPasswordIfRequired($user, $credentials); - $this->login($user, $remember); + $this->login($user, $remember); - return true; - } + $timebox->returnEarly(); - $this->fireFailedEvent($user, $credentials); + return true; + } - return false; + $this->fireFailedEvent($user, $credentials); + + return false; + }, $this->timeboxDuration); } /** @@ -452,17 +478,13 @@ public function attemptWhen(array $credentials = [], $callbacks = null, $remembe */ protected function hasValidCredentials($user, $credentials) { - return $this->timebox->call(function ($timebox) use ($user, $credentials) { - $validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials); - - if ($validated) { - $timebox->returnEarly(); + $validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials); - $this->fireValidatedEvent($user); - } + if ($validated) { + $this->fireValidatedEvent($user); + } - return $validated; - }, 200 * 1000); + return $validated; } /** diff --git a/src/Illuminate/Cache/MemoizedStore.php b/src/Illuminate/Cache/MemoizedStore.php index d899ef09d609..fc6313db2a1a 100644 --- a/src/Illuminate/Cache/MemoizedStore.php +++ b/src/Illuminate/Cache/MemoizedStore.php @@ -2,9 +2,11 @@ namespace Illuminate\Cache; +use BadMethodCallException; +use Illuminate\Contracts\Cache\LockProvider; use Illuminate\Contracts\Cache\Store; -class MemoizedStore implements Store +class MemoizedStore implements LockProvider, Store { /** * The memoized cache values. @@ -160,6 +162,39 @@ public function forever($key, $value) return $this->repository->forever($key, $value); } + /** + * Get a lock instance. + * + * @param string $name + * @param int $seconds + * @param string|null $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function lock($name, $seconds = 0, $owner = null) + { + if (! $this->repository->getStore() instanceof LockProvider) { + throw new BadMethodCallException('This cache store does not support locks.'); + } + + return $this->repository->getStore()->lock(...func_get_args()); + } + + /** + * Restore a lock instance using the owner identifier. + * + * @param string $name + * @param string $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function restoreLock($name, $owner) + { + if (! $this->repository instanceof LockProvider) { + throw new BadMethodCallException('This cache store does not support locks.'); + } + + return $this->repository->resoreLock(...func_get_args()); + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index d9b7561db2cf..bea43ce76c26 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -4,9 +4,14 @@ use ArgumentCountError; use ArrayAccess; +use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Contracts\Support\Jsonable; use Illuminate\Support\Traits\Macroable; use InvalidArgumentException; +use JsonSerializable; use Random\Randomizer; +use Traversable; +use WeakMap; class Arr { @@ -23,6 +28,21 @@ public static function accessible($value) return is_array($value) || $value instanceof ArrayAccess; } + /** + * Determine whether the given value is arrayable. + * + * @param mixed $value + * @return bool + */ + public static function arrayable($value) + { + return is_array($value) + || $value instanceof Arrayable + || $value instanceof Traversable + || $value instanceof Jsonable + || $value instanceof JsonSerializable; + } + /** * Add an element to an array using "dot" notation if it doesn't exist. * @@ -378,6 +398,32 @@ public static function forget(&$array, $keys) } } + /** + * Get the underlying array of items from the given argument. + * + * @template TKey of array-key = array-key + * @template TValue = mixed + * + * @param array|Enumerable|Arrayable|WeakMap|Traversable|Jsonable|JsonSerializable|object $items + * @return ($items is WeakMap ? list : array) + * + * @throws \InvalidArgumentException + */ + public static function from($items) + { + return match (true) { + is_array($items) => $items, + $items instanceof Enumerable => $items->all(), + $items instanceof Arrayable => $items->toArray(), + $items instanceof WeakMap => iterator_to_array($items, false), + $items instanceof Traversable => iterator_to_array($items), + $items instanceof Jsonable => json_decode($items->toJson(), true), + $items instanceof JsonSerializable => (array) $items->jsonSerialize(), + is_object($items) => (array) $items, + default => throw new InvalidArgumentException('Items cannot be represented by a scalar value.'), + }; + } + /** * Get an item from an array using "dot" notation. * diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index d2894529ed6e..c11c9c434d89 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -12,12 +12,9 @@ use Illuminate\Support\Collection; use Illuminate\Support\Enumerable; use Illuminate\Support\HigherOrderCollectionProxy; -use InvalidArgumentException; use JsonSerializable; -use Traversable; use UnexpectedValueException; use UnitEnum; -use WeakMap; use function Illuminate\Support\enum_value; @@ -1059,17 +1056,9 @@ public function __get($key) */ protected function getArrayableItems($items) { - return match (true) { - is_array($items) => $items, - $items instanceof WeakMap => throw new InvalidArgumentException('Collections can not be created using instances of WeakMap.'), - $items instanceof Enumerable => $items->all(), - $items instanceof Arrayable => $items->toArray(), - $items instanceof Traversable => iterator_to_array($items), - $items instanceof Jsonable => json_decode($items->toJson(), true), - $items instanceof JsonSerializable => (array) $items->jsonSerialize(), - $items instanceof UnitEnum => [$items], - default => (array) $items, - }; + return is_null($items) || is_scalar($items) || $items instanceof UnitEnum + ? Arr::wrap($items) + : Arr::from($items); } /** diff --git a/src/Illuminate/Collections/helpers.php b/src/Illuminate/Collections/helpers.php index 55844559e711..16c8f0118993 100644 --- a/src/Illuminate/Collections/helpers.php +++ b/src/Illuminate/Collections/helpers.php @@ -77,9 +77,9 @@ function data_get($target, $key, $default = null) $segment = match ($segment) { '\*' => '*', '\{first}' => '{first}', - '{first}' => array_key_first(is_array($target) ? $target : (new Collection($target))->all()), + '{first}' => array_key_first(Arr::from($target)), '\{last}' => '{last}', - '{last}' => array_key_last(is_array($target) ? $target : (new Collection($target))->all()), + '{last}' => array_key_last(Arr::from($target)), default => $segment, }; diff --git a/src/Illuminate/Container/Container.php b/src/Illuminate/Container/Container.php index 32ecaeabe998..5c401d465e7f 100755 --- a/src/Illuminate/Container/Container.php +++ b/src/Illuminate/Container/Container.php @@ -1496,6 +1496,16 @@ protected function fireCallbackArray($object, array $callbacks) } } + /** + * Get the name of the binding the container is currently resolving. + * + * @return class-string|string|null + */ + public function currentlyResolving() + { + return end($this->buildStack) ?: null; + } + /** * Get the container's bindings. * diff --git a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php index 162ebec1777b..91bbb4b72d3d 100644 --- a/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/MorphToMany.php @@ -121,13 +121,14 @@ public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, } /** - * Get the pivot models that are currently attached. + * Get the pivot models that are currently attached, filtered by related model keys. * + * @param mixed $ids * @return \Illuminate\Support\Collection */ - protected function getCurrentlyAttachedPivots() + protected function getCurrentlyAttachedPivotsForIds($ids = null) { - return parent::getCurrentlyAttachedPivots()->map(function ($record) { + return parent::getCurrentlyAttachedPivotsForIds($ids)->map(function ($record) { return $record instanceof MorphPivot ? $record->setMorphType($this->morphType) ->setMorphClass($this->morphClass) diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index ed13026725ce..d2b97d5d121e 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -1101,7 +1101,7 @@ public function orWhereColumn($first, $operator = null, $second = null) /** * Add a raw where clause to the query. * - * @param string $sql + * @param \Illuminate\Contracts\Database\Query\Expression|string $sql * @param mixed $bindings * @param string $boolean * @return $this diff --git a/src/Illuminate/Database/Schema/Builder.php b/src/Illuminate/Database/Schema/Builder.php index d70cb9314231..c22019536e7c 100755 --- a/src/Illuminate/Database/Schema/Builder.php +++ b/src/Illuminate/Database/Schema/Builder.php @@ -9,9 +9,6 @@ use InvalidArgumentException; use LogicException; -/** - * @template TResolver of \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint - */ class Builder { use Macroable; @@ -33,9 +30,9 @@ class Builder /** * The Blueprint resolver callback. * - * @var TResolver|null + * @var \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint|null */ - protected static $resolver = null; + protected $resolver; /** * The default string length for migrations. @@ -632,8 +629,8 @@ protected function createBlueprint($table, ?Closure $callback = null) { $connection = $this->connection; - if (static::$resolver !== null) { - return call_user_func(static::$resolver, $connection, $table, $callback); + if (isset($this->resolver)) { + return call_user_func($this->resolver, $connection, $table, $callback); } return Container::getInstance()->make(Blueprint::class, compact('connection', 'table', 'callback')); @@ -701,11 +698,11 @@ public function getConnection() /** * Set the Schema Blueprint resolver callback. * - * @param TResolver|null $resolver + * @param \Closure(string, \Closure, string): \Illuminate\Database\Schema\Blueprint|null $resolver * @return void */ - public function blueprintResolver(?Closure $resolver) + public function blueprintResolver(Closure $resolver) { - static::$resolver = $resolver; + $this->resolver = $resolver; } } diff --git a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php index db992eac43ab..16e8634d3e6b 100755 --- a/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/MySqlGrammar.php @@ -927,6 +927,16 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + $isMaria = $this->connection->isMaria(); + $version = $this->connection->getServerVersion(); + + if ($isMaria || + (! $isMaria && version_compare($version, '8.0.13', '>='))) { + if ($column->useCurrent) { + $column->default(new Expression('(CURDATE())')); + } + } + return 'date'; } @@ -1024,6 +1034,16 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + $isMaria = $this->connection->isMaria(); + $version = $this->connection->getServerVersion(); + + if ($isMaria || + (! $isMaria && version_compare($version, '8.0.13', '>='))) { + if ($column->useCurrent) { + $column->default(new Expression('(YEAR(CURDATE()))')); + } + } + return 'year'; } diff --git a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php index a40f7a62e153..7e1e6a1d2fa8 100755 --- a/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/PostgresGrammar.php @@ -922,6 +922,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CURRENT_DATE')); + } + return 'date'; } @@ -1007,6 +1011,10 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('EXTRACT(YEAR FROM CURRENT_DATE)')); + } + return $this->typeInteger($column); } diff --git a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php index 91222b7e83eb..8908836dd9c7 100644 --- a/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SQLiteGrammar.php @@ -907,6 +907,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CURRENT_DATE')); + } + return 'date'; } @@ -992,6 +996,10 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression("(CAST(strftime('%Y', 'now') AS INTEGER))")); + } + return $this->typeInteger($column); } diff --git a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php index 5e183a5dce76..f0608eb2e4dc 100755 --- a/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Schema/Grammars/SqlServerGrammar.php @@ -769,6 +769,10 @@ protected function typeJsonb(Fluent $column) */ protected function typeDate(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CAST(GETDATE() AS DATE)')); + } + return 'date'; } @@ -856,6 +860,10 @@ protected function typeTimestampTz(Fluent $column) */ protected function typeYear(Fluent $column) { + if ($column->useCurrent) { + $column->default(new Expression('CAST(YEAR(GETDATE()) AS INTEGER)')); + } + return $this->typeInteger($column); } diff --git a/src/Illuminate/Database/Schema/MySqlSchemaState.php b/src/Illuminate/Database/Schema/MySqlSchemaState.php index 30729f1ef2e8..1635de7742e5 100644 --- a/src/Illuminate/Database/Schema/MySqlSchemaState.php +++ b/src/Illuminate/Database/Schema/MySqlSchemaState.php @@ -115,6 +115,11 @@ protected function connectionString() $value .= ' --ssl-ca="${:LARAVEL_LOAD_SSL_CA}"'; } + if (isset($config['options'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT]) && + $config['options'][\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] === false) { + $value .= ' --ssl=off'; + } + return $value; } diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 91897ea0dd52..b9faeb152595 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.13.0'; + const VERSION = '12.14.0'; /** * The base path for the Laravel installation. diff --git a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php index adabdcec0576..8574401b5d3e 100644 --- a/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php +++ b/src/Illuminate/Foundation/Auth/Access/AuthorizesRequests.php @@ -108,7 +108,7 @@ public function authorizeResource($model, $parameter = null, array $options = [] /** * Get the map of resource methods to ability names. * - * @return array + * @return array */ protected function resourceAbilityMap() { @@ -126,7 +126,7 @@ protected function resourceAbilityMap() /** * Get the list of resource methods which do not have model parameters. * - * @return array + * @return list */ protected function resourceMethodsWithoutModels() { diff --git a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php index f8506bc07dd4..e0ffdd534495 100644 --- a/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php +++ b/src/Illuminate/Foundation/Configuration/ApplicationBuilder.php @@ -92,12 +92,12 @@ public function withProviders(array $providers = [], bool $withBootstrapProvider /** * Register the core event service provider for the application. * - * @param array|bool $discover + * @param iterable|bool $discover * @return $this */ - public function withEvents(array|bool $discover = []) + public function withEvents(iterable|bool $discover = true) { - if (is_array($discover) && count($discover) > 0) { + if (is_iterable($discover)) { AppEventServiceProvider::setEventDiscoveryPaths($discover); } diff --git a/src/Illuminate/Foundation/Events/DiscoverEvents.php b/src/Illuminate/Foundation/Events/DiscoverEvents.php index e2933f937872..35f244837bce 100644 --- a/src/Illuminate/Foundation/Events/DiscoverEvents.php +++ b/src/Illuminate/Foundation/Events/DiscoverEvents.php @@ -2,6 +2,7 @@ namespace Illuminate\Foundation\Events; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Reflector; use Illuminate\Support\Str; @@ -16,19 +17,23 @@ class DiscoverEvents /** * The callback to be used to guess class names. * - * @var callable(SplFileInfo, string): string|null + * @var (callable(SplFileInfo, string): class-string)|null */ public static $guessClassNamesUsingCallback; /** * Get all of the events and listeners by searching the given listener directory. * - * @param string $listenerPath + * @param array|string $listenerPath * @param string $basePath * @return array */ public static function within($listenerPath, $basePath) { + if (Arr::wrap($listenerPath) === []) { + return []; + } + $listeners = new Collection(static::getListenerEvents( Finder::create()->files()->in($listenerPath), $basePath )); @@ -51,7 +56,7 @@ public static function within($listenerPath, $basePath) /** * Get all of the listeners and their corresponding events. * - * @param iterable $listeners + * @param iterable $listeners * @param string $basePath * @return array */ @@ -91,7 +96,7 @@ protected static function getListenerEvents($listeners, $basePath) * * @param \SplFileInfo $file * @param string $basePath - * @return string + * @return class-string */ protected static function classFromFile(SplFileInfo $file, $basePath) { @@ -111,7 +116,7 @@ protected static function classFromFile(SplFileInfo $file, $basePath) /** * Specify a callback to be used to guess class names. * - * @param callable(SplFileInfo, string): string $callback + * @param callable(SplFileInfo, string): class-string $callback * @return void */ public static function guessClassNamesUsing(callable $callback) diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index d5074505302d..ac7b3ec7838b 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -6,8 +6,8 @@ use Illuminate\Auth\Listeners\SendEmailVerificationNotification; use Illuminate\Foundation\Events\DiscoverEvents; use Illuminate\Support\Arr; -use Illuminate\Support\Collection; use Illuminate\Support\Facades\Event; +use Illuminate\Support\LazyCollection; use Illuminate\Support\ServiceProvider; class EventServiceProvider extends ServiceProvider @@ -43,7 +43,7 @@ class EventServiceProvider extends ServiceProvider /** * The configured event discovery paths. * - * @var array|null + * @var iterable|null */ protected static $eventDiscoveryPaths; @@ -145,25 +145,23 @@ public function shouldDiscoverEvents() */ public function discoverEvents() { - return (new Collection($this->discoverEventsWithin())) + return (new LazyCollection($this->discoverEventsWithin())) ->flatMap(function ($directory) { return glob($directory, GLOB_ONLYDIR); }) ->reject(function ($directory) { return ! is_dir($directory); }) - ->reduce(function ($discovered, $directory) { - return array_merge_recursive( - $discovered, - DiscoverEvents::within($directory, $this->eventDiscoveryBasePath()) - ); - }, []); + ->pipe(fn ($directories) => DiscoverEvents::within( + $directories->all(), + $this->eventDiscoveryBasePath(), + )); } /** * Get the listener directories that should be used to discover events. * - * @return array + * @return iterable */ protected function discoverEventsWithin() { @@ -175,23 +173,24 @@ protected function discoverEventsWithin() /** * Add the given event discovery paths to the application's event discovery paths. * - * @param string|array $paths + * @param string|iterable $paths * @return void */ - public static function addEventDiscoveryPaths(array|string $paths) + public static function addEventDiscoveryPaths(iterable|string $paths) { - static::$eventDiscoveryPaths = array_values(array_unique( - array_merge(static::$eventDiscoveryPaths, Arr::wrap($paths)) - )); + static::$eventDiscoveryPaths = (new LazyCollection(static::$eventDiscoveryPaths)) + ->merge(is_string($paths) ? [$paths] : $paths) + ->unique() + ->values(); } /** * Set the globally configured event discovery paths. * - * @param array $paths + * @param iterable $paths * @return void */ - public static function setEventDiscoveryPaths(array $paths) + public static function setEventDiscoveryPaths(iterable $paths) { static::$eventDiscoveryPaths = $paths; } diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index 9ed063241a8f..a84c082343b7 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Database\ConnectionInterface; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; trait DatabaseTruncation @@ -120,7 +121,7 @@ protected function getAllTablesForConnection(ConnectionInterface $connection, ?s $schema = $connection->getSchemaBuilder(); - return static::$allTables[$name] = (new Collection($schema->getTables($schema->getCurrentSchemaListing())))->all(); + return static::$allTables[$name] = Arr::from($schema->getTables($schema->getCurrentSchemaListing())); } /** diff --git a/src/Illuminate/Http/Client/Factory.php b/src/Illuminate/Http/Client/Factory.php index c38932ef33d5..49391a4fa1bb 100644 --- a/src/Illuminate/Http/Client/Factory.php +++ b/src/Illuminate/Http/Client/Factory.php @@ -62,14 +62,14 @@ class Factory /** * The recorded response array. * - * @var array + * @var list */ protected $recorded = []; /** * All created response sequences. * - * @var array + * @var list<\Illuminate\Http\Client\ResponseSequence> */ protected $responseSequences = []; @@ -195,7 +195,7 @@ public static function failedRequest($body = null, $status = 200, $headers = []) * Create a new connection exception for use during stubbing. * * @param string|null $message - * @return \GuzzleHttp\Promise\PromiseInterface + * @return \Closure(\Illuminate\Http\Client\Request): \GuzzleHttp\Promise\PromiseInterface */ public static function failedConnection($message = null) { @@ -221,7 +221,7 @@ public function sequence(array $responses = []) /** * Register a stub callable that will intercept requests and be able to return stub responses. * - * @param callable|array|null $callback + * @param callable|array|null $callback * @return $this */ public function fake($callback = null) @@ -283,7 +283,7 @@ public function fakeSequence($url = '*') * Stub the given URL using the given callback. * * @param string $url - * @param \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|callable|int|string|array $callback + * @param \Illuminate\Http\Client\Response|\GuzzleHttp\Promise\PromiseInterface|callable|int|string|array|\Illuminate\Http\Client\ResponseSequence $callback * @return $this */ public function stubUrl($url, $callback) @@ -371,7 +371,7 @@ public function recordRequestResponsePair($request, $response) /** * Assert that a request / response pair was recorded matching a given truth test. * - * @param callable $callback + * @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback * @return void */ public function assertSent($callback) @@ -385,7 +385,7 @@ public function assertSent($callback) /** * Assert that the given request was sent in the given order. * - * @param array $callbacks + * @param list $callbacks * @return void */ public function assertSentInOrder($callbacks) @@ -407,7 +407,7 @@ public function assertSentInOrder($callbacks) /** * Assert that a request / response pair was not recorded matching a given truth test. * - * @param callable $callback + * @param callable|(\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool) $callback * @return void */ public function assertNotSent($callback) @@ -460,8 +460,8 @@ public function assertSequencesAreEmpty() /** * Get a collection of the request / response pairs matching the given truth test. * - * @param callable $callback - * @return \Illuminate\Support\Collection + * @param (\Closure(\Illuminate\Http\Client\Request, \Illuminate\Http\Client\Response|null): bool)|callable $callback + * @return \Illuminate\Support\Collection */ public function recorded($callback = null) { diff --git a/src/Illuminate/Http/Client/PendingRequest.php b/src/Illuminate/Http/Client/PendingRequest.php index 117319de5029..7abcb3a459b8 100644 --- a/src/Illuminate/Http/Client/PendingRequest.php +++ b/src/Illuminate/Http/Client/PendingRequest.php @@ -26,7 +26,6 @@ use OutOfBoundsException; use Psr\Http\Message\MessageInterface; use Psr\Http\Message\RequestInterface; -use RuntimeException; use Symfony\Component\VarDumper\VarDumper; class PendingRequest @@ -1033,7 +1032,11 @@ protected function makePromise(string $method, string $url, array $options = [], $this->dispatchResponseReceivedEvent($response); }); }) - ->otherwise(function (OutOfBoundsException|TransferException $e) { + ->otherwise(function (OutOfBoundsException|TransferException|StrayRequestException $e) { + if ($e instanceof StrayRequestException) { + throw $e; + } + if ($e instanceof ConnectException || ($e instanceof RequestException && ! $e->hasResponse())) { $exception = new ConnectionException($e->getMessage(), 0, $e); @@ -1334,7 +1337,7 @@ public function buildStubHandler() if (is_null($response)) { if ($this->preventStrayRequests) { - throw new RuntimeException('Attempted request to ['.(string) $request->getUri().'] without a matching fake.'); + throw new StrayRequestException((string) $request->getUri()); } return $handler($request, $options); diff --git a/src/Illuminate/Http/Client/Pool.php b/src/Illuminate/Http/Client/Pool.php index aba741f4bf75..e9716be08571 100644 --- a/src/Illuminate/Http/Client/Pool.php +++ b/src/Illuminate/Http/Client/Pool.php @@ -26,7 +26,7 @@ class Pool /** * The pool of requests. * - * @var array + * @var array */ protected $pool = []; @@ -65,7 +65,7 @@ protected function asyncRequest() /** * Retrieve the requests in the pool. * - * @return array + * @return array */ public function getRequests() { diff --git a/src/Illuminate/Http/Client/Response.php b/src/Illuminate/Http/Client/Response.php index c2352bb01f81..e69647bfb2bc 100644 --- a/src/Illuminate/Http/Client/Response.php +++ b/src/Illuminate/Http/Client/Response.php @@ -235,7 +235,7 @@ public function serverError() /** * Execute the given callback if there was a server or client error. * - * @param callable $callback + * @param callable|(\Closure(\Illuminate\Http\Client\Response): mixed) $callback * @return $this */ public function onError(callable $callback) @@ -339,7 +339,7 @@ public function throwIf($condition) /** * Throw an exception if the response status code matches the given code. * - * @param callable|int $statusCode + * @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode * @return $this * * @throws \Illuminate\Http\Client\RequestException @@ -357,7 +357,7 @@ public function throwIfStatus($statusCode) /** * Throw an exception unless the response status code matches the given code. * - * @param callable|int $statusCode + * @param int|(\Closure(int, \Illuminate\Http\Client\Response): bool)|callable $statusCode * @return $this * * @throws \Illuminate\Http\Client\RequestException diff --git a/src/Illuminate/Http/Client/StrayRequestException.php b/src/Illuminate/Http/Client/StrayRequestException.php new file mode 100644 index 000000000000..1392233bbadc --- /dev/null +++ b/src/Illuminate/Http/Client/StrayRequestException.php @@ -0,0 +1,13 @@ +hidden, array_flip($keys)); } + /** + * Retrieve all values except those with the given keys. + * + * @param array $keys + * @return array + */ + public function except($keys) + { + return array_diff_key($this->data, array_flip($keys)); + } + + /** + * Retrieve all hidden values except those with the given keys. + * + * @param array $keys + * @return array + */ + public function exceptHidden($keys) + { + return array_diff_key($this->hidden, array_flip($keys)); + } + /** * Add a context value. * diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php index 68017795655c..5c5c7d04a7ed 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptions.php @@ -57,6 +57,13 @@ class ThrottlesExceptions */ protected $whenCallback; + /** + * The callbacks that determine if the job should be deleted. + * + * @var callable[] + */ + protected array $deleteWhenCallbacks = []; + /** * The prefix of the rate limiter key. * @@ -111,6 +118,10 @@ public function handle($job, $next) report($throwable); } + if ($this->shouldDelete($throwable)) { + return $job->delete(); + } + $this->limiter->hit($jobKey, $this->decaySeconds); return $job->release($this->retryAfterMinutes * 60); @@ -130,6 +141,38 @@ public function when(callable $callback) return $this; } + /** + * Add a callback that should determine if the job should be deleted. + * + * @param callable|string $callback + * @return $this + */ + public function deleteWhen(callable|string $callback) + { + $this->deleteWhenCallbacks[] = is_string($callback) + ? fn (Throwable $e) => $e instanceof $callback + : $callback; + + return $this; + } + + /** + * Run the skip / delete callbacks to determine if the job should be deleted for the given exception. + * + * @param Throwable $throwable + * @return bool + */ + protected function shouldDelete(Throwable $throwable): bool + { + foreach ($this->deleteWhenCallbacks as $callback) { + if (call_user_func($callback, $throwable)) { + return true; + } + } + + return false; + } + /** * Set the prefix of the rate limiter key. * diff --git a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php index e5b79a7d67ed..8c6b78912b0d 100644 --- a/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php +++ b/src/Illuminate/Queue/Middleware/ThrottlesExceptionsWithRedis.php @@ -58,6 +58,10 @@ public function handle($job, $next) report($throwable); } + if ($this->shouldDelete($throwable)) { + return $job->delete(); + } + $this->limiter->acquire(); return $job->release($this->retryAfterMinutes * 60); diff --git a/src/Illuminate/Queue/SerializesModels.php b/src/Illuminate/Queue/SerializesModels.php index db17b98755af..388c1d745561 100644 --- a/src/Illuminate/Queue/SerializesModels.php +++ b/src/Illuminate/Queue/SerializesModels.php @@ -36,6 +36,10 @@ public function __serialize() continue; } + if (method_exists($property, 'isVirtual') && $property->isVirtual()) { + continue; + } + $value = $this->getPropertyValue($property); if ($property->hasDefaultValue() && $value === $property->getDefaultValue()) { diff --git a/src/Illuminate/Routing/RouteUrlGenerator.php b/src/Illuminate/Routing/RouteUrlGenerator.php index cd23162c015f..52fcec1e4237 100644 --- a/src/Illuminate/Routing/RouteUrlGenerator.php +++ b/src/Illuminate/Routing/RouteUrlGenerator.php @@ -197,9 +197,14 @@ protected function formatParameters(Route $route, $parameters) unset($parameters[$name]); continue; - } elseif (! isset($this->defaultParameters[$name]) && ! isset($optionalParameters[$name])) { - // No named parameter or default value for a required parameter, try to match to positional parameter below... - array_push($requiredRouteParametersWithoutDefaultsOrNamedParameters, $name); + } else { + $bindingField = $route->bindingFieldFor($name); + $defaultParameterKey = $bindingField ? "$name:$bindingField" : $name; + + if (! isset($this->defaultParameters[$defaultParameterKey]) && ! isset($optionalParameters[$name])) { + // No named parameter or default value for a required parameter, try to match to positional parameter below... + array_push($requiredRouteParametersWithoutDefaultsOrNamedParameters, $name); + } } $namedParameters[$name] = ''; diff --git a/src/Illuminate/Support/Facades/App.php b/src/Illuminate/Support/Facades/App.php index ed8a9d6dce10..9dd93084dad4 100755 --- a/src/Illuminate/Support/Facades/App.php +++ b/src/Illuminate/Support/Facades/App.php @@ -130,6 +130,7 @@ * @method static void afterResolving(\Closure|string $abstract, \Closure|null $callback = null) * @method static void afterResolvingAttribute(string $attribute, \Closure $callback) * @method static void fireAfterResolvingAttributeCallbacks(\ReflectionAttribute[] $attributes, mixed $object) + * @method static string|null currentlyResolving() * @method static array getBindings() * @method static string getAlias(string $abstract) * @method static void forgetExtenders(string $abstract) diff --git a/src/Illuminate/Support/Facades/Context.php b/src/Illuminate/Support/Facades/Context.php index 6b894fad39b7..aaac04d8ed00 100644 --- a/src/Illuminate/Support/Facades/Context.php +++ b/src/Illuminate/Support/Facades/Context.php @@ -15,6 +15,8 @@ * @method static mixed pullHidden(string $key, mixed $default = null) * @method static array only(array $keys) * @method static array onlyHidden(array $keys) + * @method static array except(array $keys) + * @method static array exceptHidden(array $keys) * @method static \Illuminate\Log\Context\Repository add(string|array $key, mixed $value = null) * @method static \Illuminate\Log\Context\Repository addHidden(string|array $key, mixed $value = null) * @method static \Illuminate\Log\Context\Repository forget(string|array $key) diff --git a/src/Illuminate/Support/Facades/Http.php b/src/Illuminate/Support/Facades/Http.php index ecbcca5ba49c..823056859572 100644 --- a/src/Illuminate/Support/Facades/Http.php +++ b/src/Illuminate/Support/Facades/Http.php @@ -12,19 +12,19 @@ * @method static \GuzzleHttp\Promise\PromiseInterface response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \GuzzleHttp\Psr7\Response psr7Response(array|string|null $body = null, int $status = 200, array $headers = []) * @method static \Illuminate\Http\Client\RequestException failedRequest(array|string|null $body = null, int $status = 200, array $headers = []) - * @method static \GuzzleHttp\Promise\PromiseInterface failedConnection(string|null $message = null) + * @method static \Closure failedConnection(string|null $message = null) * @method static \Illuminate\Http\Client\ResponseSequence sequence(array $responses = []) * @method static bool preventingStrayRequests() * @method static \Illuminate\Http\Client\Factory allowStrayRequests() * @method static \Illuminate\Http\Client\Factory record() * @method static void recordRequestResponsePair(\Illuminate\Http\Client\Request $request, \Illuminate\Http\Client\Response|null $response) - * @method static void assertSent(callable $callback) + * @method static void assertSent(callable|\Closure $callback) * @method static void assertSentInOrder(array $callbacks) - * @method static void assertNotSent(callable $callback) + * @method static void assertNotSent(callable|\Closure $callback) * @method static void assertNothingSent() * @method static void assertSentCount(int $count) * @method static void assertSequencesAreEmpty() - * @method static \Illuminate\Support\Collection recorded(callable $callback = null) + * @method static \Illuminate\Support\Collection recorded(\Closure|callable $callback = null) * @method static \Illuminate\Http\Client\PendingRequest createPendingRequest() * @method static \Illuminate\Contracts\Events\Dispatcher|null getDispatcher() * @method static array getGlobalMiddleware() diff --git a/src/Illuminate/Support/Facades/Password.php b/src/Illuminate/Support/Facades/Password.php index 7099e3971fd8..ac6f226aa251 100755 --- a/src/Illuminate/Support/Facades/Password.php +++ b/src/Illuminate/Support/Facades/Password.php @@ -15,6 +15,7 @@ * @method static void deleteToken(\Illuminate\Contracts\Auth\CanResetPassword $user) * @method static bool tokenExists(\Illuminate\Contracts\Auth\CanResetPassword $user, string $token) * @method static \Illuminate\Auth\Passwords\TokenRepositoryInterface getRepository() + * @method static \Illuminate\Support\Timebox getTimebox() * * @see \Illuminate\Auth\Passwords\PasswordBrokerManager * @see \Illuminate\Auth\Passwords\PasswordBroker diff --git a/src/Illuminate/Support/Number.php b/src/Illuminate/Support/Number.php index ec5d6342d416..7ebf7916cb32 100644 --- a/src/Illuminate/Support/Number.php +++ b/src/Illuminate/Support/Number.php @@ -160,14 +160,19 @@ public static function currency(int|float $number, string $in = '', ?string $loc * @param int|float $bytes * @param int $precision * @param int|null $maxPrecision + * @param bool $useBinaryPrefix * @return string */ - public static function fileSize(int|float $bytes, int $precision = 0, ?int $maxPrecision = null) + public static function fileSize(int|float $bytes, int $precision = 0, ?int $maxPrecision = null, bool $useBinaryPrefix = false) { - $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + $base = $useBinaryPrefix ? 1024 : 1000; - for ($i = 0; ($bytes / 1024) > 0.9 && ($i < count($units) - 1); $i++) { - $bytes /= 1024; + $units = $useBinaryPrefix + ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB', 'RiB', 'QiB'] + : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB']; + + for ($i = 0; ($bytes / $base) > 0.9 && ($i < count($units) - 1); $i++) { + $bytes /= $base; } return sprintf('%s %s', static::format($bytes, $precision, $maxPrecision), $units[$i]); diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 3bb47011d654..27bdb6bf0189 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -1180,7 +1180,7 @@ public static function repeat(string $string, int $times) public static function replaceArray($search, $replace, $subject) { if ($replace instanceof Traversable) { - $replace = (new Collection($replace))->all(); + $replace = Arr::from($replace); } $segments = explode($search, $subject); @@ -1222,15 +1222,15 @@ private static function toStringOr($value, $fallback) public static function replace($search, $replace, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = (new Collection($search))->all(); + $search = Arr::from($search); } if ($replace instanceof Traversable) { - $replace = (new Collection($replace))->all(); + $replace = Arr::from($replace); } if ($subject instanceof Traversable) { - $subject = (new Collection($subject))->all(); + $subject = Arr::from($subject); } return $caseSensitive @@ -1363,7 +1363,7 @@ public static function replaceMatches($pattern, $replace, $subject, $limit = -1) public static function remove($search, $subject, $caseSensitive = true) { if ($search instanceof Traversable) { - $search = (new Collection($search))->all(); + $search = Arr::from($search); } return $caseSensitive diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index 883eb16c4582..0f8b6722fa6c 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -232,11 +232,15 @@ protected function appendFilePath($contents) */ protected function getOpenAndClosingPhpTokens($contents) { - return (new Collection(token_get_all($contents))) - ->pluck(0) - ->filter(function ($token) { - return in_array($token, [T_OPEN_TAG, T_OPEN_TAG_WITH_ECHO, T_CLOSE_TAG]); - }); + $tokens = []; + + foreach (token_get_all($contents) as $token) { + if ($token[0] === T_OPEN_TAG || $token[0] === T_OPEN_TAG_WITH_ECHO || $token[0] === T_CLOSE_TAG) { + $tokens[] = $token[0]; + } + } + + return new Collection($tokens); } /** diff --git a/tests/Container/ContainerTest.php b/tests/Container/ContainerTest.php index 5522b8af9b2f..0302729bc970 100755 --- a/tests/Container/ContainerTest.php +++ b/tests/Container/ContainerTest.php @@ -2,9 +2,11 @@ namespace Illuminate\Tests\Container; +use Attribute; use Illuminate\Container\Container; use Illuminate\Container\EntryNotFoundException; use Illuminate\Contracts\Container\BindingResolutionException; +use Illuminate\Contracts\Container\ContextualAttribute; use PHPUnit\Framework\TestCase; use Psr\Container\ContainerExceptionInterface; use stdClass; @@ -522,6 +524,23 @@ public function testGetAlias() $this->assertSame('ConcreteStub', $container->getAlias('foo')); } + public function testCurrentlyResolving() + { + $container = new Container; + + $container->afterResolvingAttribute(ContainerCurrentResolvingAttribute::class, function ($attr, $instance, $container) { + $this->assertEquals(ContainerCurrentResolvingConcrete::class, $container->currentlyResolving()); + }); + + $container->when(ContainerCurrentResolvingConcrete::class) + ->needs('$currentlyResolving') + ->give(fn ($container) => $container->currentlyResolving()); + + $resolved = $container->make(ContainerCurrentResolvingConcrete::class); + + $this->assertEquals(ContainerCurrentResolvingConcrete::class, $resolved->currentlyResolving); + } + public function testGetAliasRecursive() { $container = new Container; @@ -860,3 +879,23 @@ public function work(IContainerContractStub $stub) return $stub; } } + +#[Attribute(Attribute::TARGET_PARAMETER)] +class ContainerCurrentResolvingAttribute implements ContextualAttribute +{ + public function resolve() + { + } +} + +class ContainerCurrentResolvingConcrete +{ + public $currentlyResolving; + + public function __construct( + #[ContainerCurrentResolvingAttribute] + string $currentlyResolving + ) { + $this->currentlyResolving = $currentlyResolving; + } +} diff --git a/tests/Database/DatabaseMariaDbSchemaGrammarTest.php b/tests/Database/DatabaseMariaDbSchemaGrammarTest.php index 1224247e20de..dff05674d9ff 100755 --- a/tests/Database/DatabaseMariaDbSchemaGrammarTest.php +++ b/tests/Database/DatabaseMariaDbSchemaGrammarTest.php @@ -873,7 +873,11 @@ public function testAddingJsonb() public function testAddingDate() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->date('foo'); $statements = $blueprint->toSql(); @@ -881,15 +885,47 @@ public function testAddingDate() $this->assertSame('alter table `users` add `foo` date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `foo` date not null default (CURDATE())', $statements[0]); + } + public function testAddingYear() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->year('birth_year'); $statements = $blueprint->toSql(); $this->assertCount(1, $statements); $this->assertSame('alter table `users` add `birth_year` year not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(true); + $conn->shouldReceive('getServerVersion')->andReturn('10.3.0'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `birth_year` year not null default (YEAR(CURDATE()))', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseMySqlSchemaGrammarTest.php b/tests/Database/DatabaseMySqlSchemaGrammarTest.php index 09082dab62df..cfc6bb02df3b 100755 --- a/tests/Database/DatabaseMySqlSchemaGrammarTest.php +++ b/tests/Database/DatabaseMySqlSchemaGrammarTest.php @@ -872,7 +872,11 @@ public function testAddingJsonb() public function testAddingDate() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->date('foo'); $statements = $blueprint->toSql(); @@ -880,15 +884,75 @@ public function testAddingDate() $this->assertSame('alter table `users` add `foo` date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `foo` date not null default (CURDATE())', $statements[0]); + } + + public function testAddingDateWithDefaultCurrentOn57() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('5.7'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `foo` date not null', $statements[0]); + } + public function testAddingYear() { - $blueprint = new Blueprint($this->getConnection(), 'users'); + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); $blueprint->year('birth_year'); $statements = $blueprint->toSql(); $this->assertCount(1, $statements); $this->assertSame('alter table `users` add `birth_year` year not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('8.0.13'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `birth_year` year not null default (YEAR(CURDATE()))', $statements[0]); + } + + public function testAddingYearWithDefaultCurrentOn57() + { + $conn = $this->getConnection(); + $conn->shouldReceive('isMaria')->andReturn(false); + $conn->shouldReceive('getServerVersion')->andReturn('5.7'); + + $blueprint = new Blueprint($conn, 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table `users` add `birth_year` year not null', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseMySqlSchemaStateTest.php b/tests/Database/DatabaseMySqlSchemaStateTest.php index 3a9c7db3896c..18985114e509 100644 --- a/tests/Database/DatabaseMySqlSchemaStateTest.php +++ b/tests/Database/DatabaseMySqlSchemaStateTest.php @@ -70,6 +70,24 @@ public static function provider(): Generator ], ]; + yield 'no_ssl' => [ + ' --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --ssl=off', [ + 'LARAVEL_LOAD_SOCKET' => '', + 'LARAVEL_LOAD_HOST' => '', + 'LARAVEL_LOAD_PORT' => '', + 'LARAVEL_LOAD_USER' => 'root', + 'LARAVEL_LOAD_PASSWORD' => '', + 'LARAVEL_LOAD_DATABASE' => 'forge', + 'LARAVEL_LOAD_SSL_CA' => '', + ], [ + 'username' => 'root', + 'database' => 'forge', + 'options' => [ + \PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => false, + ], + ], + ]; + yield 'unix socket' => [ ' --user="${:LARAVEL_LOAD_USER}" --password="${:LARAVEL_LOAD_PASSWORD}" --socket="${:LARAVEL_LOAD_SOCKET}"', [ 'LARAVEL_LOAD_SOCKET' => '/tmp/mysql.sock', diff --git a/tests/Database/DatabasePostgresSchemaGrammarTest.php b/tests/Database/DatabasePostgresSchemaGrammarTest.php index 0c31a6d2d06a..f3402250245f 100755 --- a/tests/Database/DatabasePostgresSchemaGrammarTest.php +++ b/tests/Database/DatabasePostgresSchemaGrammarTest.php @@ -714,6 +714,16 @@ public function testAddingDate() $this->assertSame('alter table "users" add column "foo" date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" date not null default CURRENT_DATE', $statements[0]); + } + public function testAddingYear() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -723,6 +733,15 @@ public function testAddingYear() $this->assertSame('alter table "users" add column "birth_year" integer not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "birth_year" integer not null default EXTRACT(YEAR FROM CURRENT_DATE)', $statements[0]); + } + public function testAddingJson() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php index d9233f548fa9..26de4bcf0bf8 100755 --- a/tests/Database/DatabaseSQLiteSchemaGrammarTest.php +++ b/tests/Database/DatabaseSQLiteSchemaGrammarTest.php @@ -640,6 +640,16 @@ public function testAddingDate() $this->assertSame('alter table "users" add column "foo" date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "foo" date not null default CURRENT_DATE', $statements[0]); + } + public function testAddingYear() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -649,6 +659,15 @@ public function testAddingYear() $this->assertSame('alter table "users" add column "birth_year" integer not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add column "birth_year" integer not null default (CAST(strftime(\'%Y\', \'now\') AS INTEGER))', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index e8546270597b..e5f26379caa6 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -102,6 +102,31 @@ public function testDropIndexDefaultNamesWhenPrefixSupplied() $this->assertSame('prefix_geo_coordinates_spatialindex', $commands[0]->index); } + public function testDefaultCurrentDate() + { + $getSql = function ($grammar, $mysql57 = false) { + if ($grammar == 'MySql') { + $connection = $this->getConnection($grammar); + $mysql57 ? $connection->shouldReceive('getServerVersion')->andReturn('5.7') : $connection->shouldReceive('getServerVersion')->andReturn('8.0.13'); + $connection->shouldReceive('isMaria')->andReturn(false); + + return (new Blueprint($connection, 'users', function ($table) { + $table->date('created')->useCurrent(); + }))->toSql(); + } else { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->date('created')->useCurrent(); + })->toSql(); + } + }; + + $this->assertEquals(['alter table `users` add `created` date not null default (CURDATE())'], $getSql('MySql')); + $this->assertEquals(['alter table `users` add `created` date not null'], $getSql('MySql', mysql57: true)); + $this->assertEquals(['alter table "users" add column "created" date not null default CURRENT_DATE'], $getSql('Postgres')); + $this->assertEquals(['alter table "users" add column "created" date not null default CURRENT_DATE'], $getSql('SQLite')); + $this->assertEquals(['alter table "users" add "created" date not null default CAST(GETDATE() AS DATE)'], $getSql('SqlServer')); + } + public function testDefaultCurrentDateTime() { $getSql = function ($grammar) { @@ -130,6 +155,31 @@ public function testDefaultCurrentTimestamp() $this->assertEquals(['alter table "users" add "created" datetime not null default CURRENT_TIMESTAMP'], $getSql('SqlServer')); } + public function testDefaultCurrentYear() + { + $getSql = function ($grammar, $mysql57 = false) { + if ($grammar == 'MySql') { + $connection = $this->getConnection($grammar); + $mysql57 ? $connection->shouldReceive('getServerVersion')->andReturn('5.7') : $connection->shouldReceive('getServerVersion')->andReturn('8.0.13'); + $connection->shouldReceive('isMaria')->andReturn(false); + + return (new Blueprint($connection, 'users', function ($table) { + $table->year('birth_year')->useCurrent(); + }))->toSql(); + } else { + return $this->getBlueprint($grammar, 'users', function ($table) { + $table->year('birth_year')->useCurrent(); + })->toSql(); + } + }; + + $this->assertEquals(['alter table `users` add `birth_year` year not null default (YEAR(CURDATE()))'], $getSql('MySql')); + $this->assertEquals(['alter table `users` add `birth_year` year not null'], $getSql('MySql', mysql57: true)); + $this->assertEquals(['alter table "users" add column "birth_year" integer not null default EXTRACT(YEAR FROM CURRENT_DATE)'], $getSql('Postgres')); + $this->assertEquals(['alter table "users" add column "birth_year" integer not null default (CAST(strftime(\'%Y\', \'now\') AS INTEGER))'], $getSql('SQLite')); + $this->assertEquals(['alter table "users" add "birth_year" int not null default CAST(YEAR(GETDATE()) AS INTEGER)'], $getSql('SqlServer')); + } + public function testRemoveColumn() { $getSql = function ($grammar) { diff --git a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php index d06003473498..c31cf83f5576 100755 --- a/tests/Database/DatabaseSqlServerSchemaGrammarTest.php +++ b/tests/Database/DatabaseSqlServerSchemaGrammarTest.php @@ -614,6 +614,16 @@ public function testAddingDate() $this->assertSame('alter table "users" add "foo" date not null', $statements[0]); } + public function testAddingDateWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->date('foo')->useCurrent(); + $statements = $blueprint->toSql(); + + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add "foo" date not null default CAST(GETDATE() AS DATE)', $statements[0]); + } + public function testAddingYear() { $blueprint = new Blueprint($this->getConnection(), 'users'); @@ -623,6 +633,15 @@ public function testAddingYear() $this->assertSame('alter table "users" add "birth_year" int not null', $statements[0]); } + public function testAddingYearWithDefaultCurrent() + { + $blueprint = new Blueprint($this->getConnection(), 'users'); + $blueprint->year('birth_year')->useCurrent(); + $statements = $blueprint->toSql(); + $this->assertCount(1, $statements); + $this->assertSame('alter table "users" add "birth_year" int not null default CAST(YEAR(GETDATE()) AS INTEGER)', $statements[0]); + } + public function testAddingDateTime() { $blueprint = new Blueprint($this->getConnection(), 'users'); diff --git a/tests/Http/HttpClientTest.php b/tests/Http/HttpClientTest.php index 56ee25932b0f..af70017090ef 100644 --- a/tests/Http/HttpClientTest.php +++ b/tests/Http/HttpClientTest.php @@ -21,6 +21,7 @@ use Illuminate\Http\Client\RequestException; use Illuminate\Http\Client\Response; use Illuminate\Http\Client\ResponseSequence; +use Illuminate\Http\Client\StrayRequestException; use Illuminate\Http\Response as HttpResponse; use Illuminate\Support\Arr; use Illuminate\Support\Carbon; @@ -3178,12 +3179,38 @@ public function testItCanEnforceFaking() $responses[] = $this->factory->get('https://forge.laravel.com')->body(); $this->assertSame(['ok', 'ok'], $responses); - $this->expectException(RuntimeException::class); + $this->expectException(StrayRequestException::class); $this->expectExceptionMessage('Attempted request to [https://laravel.com] without a matching fake.'); $this->factory->get('https://laravel.com'); } + public function testItCanEnforceFakingInThePool() + { + $this->factory->preventStrayRequests(); + $this->factory->fake(['https://vapor.laravel.com' => Factory::response('ok', 200)]); + $this->factory->fake(['https://forge.laravel.com' => Factory::response('ok', 200)]); + + $responses = $this->factory->pool(function (Pool $pool) { + return [ + $pool->get('https://vapor.laravel.com'), + $pool->get('https://forge.laravel.com'), + ]; + }); + + $this->assertSame(200, $responses[0]->status()); + $this->assertSame(200, $responses[1]->status()); + + $this->expectException(StrayRequestException::class); + $this->expectExceptionMessage('Attempted request to [https://laravel.com] without a matching fake.'); + + $this->factory->pool(function (Pool $pool) { + return [ + $pool->get('https://laravel.com'), + ]; + }); + } + public function testPreventingStrayRequests() { $this->assertFalse($this->factory->preventingStrayRequests()); diff --git a/tests/Integration/Cache/MemoizedStoreTest.php b/tests/Integration/Cache/MemoizedStoreTest.php index 8ca80eda5135..009906f1555f 100644 --- a/tests/Integration/Cache/MemoizedStoreTest.php +++ b/tests/Integration/Cache/MemoizedStoreTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Integration\Cache; +use BadMethodCallException; use Illuminate\Cache\Events\CacheEvent; use Illuminate\Cache\Events\CacheMissed; use Illuminate\Cache\Events\ForgettingKey; @@ -10,12 +11,15 @@ use Illuminate\Cache\Events\RetrievingKey; use Illuminate\Cache\Events\RetrievingManyKeys; use Illuminate\Cache\Events\WritingKey; +use Illuminate\Contracts\Cache\Store; use Illuminate\Foundation\Testing\Concerns\InteractsWithRedis; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Exceptions; use Illuminate\Support\Facades\Redis; use Orchestra\Testbench\TestCase; +use Throwable; class MemoizedStoreTest extends TestCase { @@ -406,4 +410,89 @@ public function test_it_resets_cache_store_with_scoped_instances() $this->assertSame('Taylor', $live); $this->assertSame('Taylor', $memoized); } + + public function test_it_throws_when_underlying_store_does_not_support_locks() + { + $this->freezeTime(); + $exceptions = []; + Exceptions::reportable(function (Throwable $e) use (&$exceptions) { + $exceptions[] = $e; + }); + Config::set('cache.stores.no-lock', ['driver' => 'no-lock']); + Cache::extend('no-lock', fn () => Cache::repository(new class implements Store + { + public function get($key) + { + return Cache::get(...func_get_args()); + } + + public function many(array $keys) + { + return Cache::many(...func_get_args()); + } + + public function put($key, $value, $seconds) + { + return Cache::put(...func_get_args()); + } + + public function putMany(array $values, $seconds) + { + return Cache::putMany(...func_get_args()); + } + + public function increment($key, $value = 1) + { + return Cache::increment(...func_get_args()); + } + + public function decrement($key, $value = 1) + { + return Cache::decrement(...func_get_args()); + } + + public function forever($key, $value) + { + return Cache::forever(...func_get_args()); + } + + public function forget($key) + { + return Cache::forget(...func_get_args()); + } + + public function flush() + { + return Cache::flush(...func_get_args()); + } + + public function getPrefix() + { + return Cache::getPrefix(...func_get_args()); + } + })); + Cache::flexible('key', [10, 20], 'value-1'); + + $this->travel(11)->seconds(); + Cache::memo('no-lock')->flexible('key', [10, 20], 'value-2'); + defer()->invoke(); + $value = Cache::get('key'); + + $this->assertCount(1, $exceptions); + $this->assertInstanceOf(BadMethodCallException::class, $exceptions[0]); + $this->assertSame('This cache store does not support locks.', $exceptions[0]->getMessage()); + } + + public function test_it_supports_with_flexible() + { + $this->freezeTime(); + Cache::flexible('key', [10, 20], 'value-1'); + + $this->travel(11)->seconds(); + Cache::memo()->flexible('key', [10, 20], 'value-2'); + defer()->invoke(); + $value = Cache::get('key'); + + $this->assertSame('value-2', $value); + } } diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index e94fe5cce005..8521b948e8e9 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -179,6 +179,16 @@ public function testCustomMorphPivotClassDetachAttributes() $project->equipments()->save($equipment); $equipment->projects()->sync([]); + + $this->assertEquals( + [PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class, PivotEventsTestProject::class], + PivotEventsTestModelEquipment::$eventsMorphClasses + ); + + $this->assertEquals( + ['equipmentable_type', 'equipmentable_type', 'equipmentable_type', 'equipmentable_type', 'equipmentable_type', 'equipmentable_type'], + PivotEventsTestModelEquipment::$eventsMorphTypes + ); } } @@ -237,6 +247,55 @@ class PivotEventsTestModelEquipment extends MorphPivot { public $table = 'equipmentables'; + public static $eventsMorphClasses = []; + + public static $eventsMorphTypes = []; + + public static function boot() + { + parent::boot(); + + static::creating(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::created(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::updating(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::updated(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::saving(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::saved(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::deleting(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + + static::deleted(function ($model) { + static::$eventsMorphClasses[] = $model->morphClass; + static::$eventsMorphTypes[] = $model->morphType; + }); + } + public function equipment() { return $this->belongsTo(PivotEventsTestEquipment::class); diff --git a/tests/Integration/Foundation/DiscoverEventsTest.php b/tests/Integration/Foundation/DiscoverEventsTest.php index 116f2374afba..417358539e24 100644 --- a/tests/Integration/Foundation/DiscoverEventsTest.php +++ b/tests/Integration/Foundation/DiscoverEventsTest.php @@ -57,6 +57,33 @@ class_alias(UnionListener::class, 'Tests\Integration\Foundation\Fixtures\EventDi ], $events); } + public function testMultipleDirectoriesCanBeDiscovered(): void + { + $events = DiscoverEvents::within([ + __DIR__.'/Fixtures/EventDiscovery/Listeners', + __DIR__.'/Fixtures/EventDiscovery/UnionListeners', + ], getcwd()); + + $this->assertEquals([ + EventOne::class => [ + Listener::class.'@handle', + Listener::class.'@handleEventOne', + UnionListener::class.'@handle', + ], + EventTwo::class => [ + Listener::class.'@handleEventTwo', + UnionListener::class.'@handle', + ], + ], $events); + } + + public function testNoExceptionForEmptyDirectories(): void + { + $events = DiscoverEvents::within([], getcwd()); + + $this->assertEquals([], $events); + } + public function testEventsCanBeDiscoveredUsingCustomClassNameGuessing() { DiscoverEvents::guessClassNamesUsing(function (SplFileInfo $file, $basePath) { diff --git a/tests/Integration/Queue/ThrottlesExceptionsTest.php b/tests/Integration/Queue/ThrottlesExceptionsTest.php index e559c41b7f8f..19f525afcc8d 100644 --- a/tests/Integration/Queue/ThrottlesExceptionsTest.php +++ b/tests/Integration/Queue/ThrottlesExceptionsTest.php @@ -40,6 +40,11 @@ public function testCircuitResetsAfterSuccess() $this->assertJobWasReleasedWithDelay(CircuitBreakerTestJob::class); } + public function testCircuitCanSkipJob() + { + $this->assertJobWasDeleted(CircuitBreakerSkipJob::class); + } + protected function assertJobWasReleasedImmediately($class) { $class::$handled = false; @@ -82,6 +87,27 @@ protected function assertJobWasReleasedWithDelay($class) $this->assertFalse($class::$handled); } + protected function assertJobWasDeleted($class) + { + $class::$handled = false; + $instance = new CallQueuedHandler(new Dispatcher($this->app), $this->app); + + $job = m::mock(Job::class); + + $job->shouldReceive('hasFailed')->once()->andReturn(false); + $job->shouldReceive('delete')->once(); + $job->shouldReceive('isDeleted')->andReturn(true); + $job->shouldReceive('isReleased')->twice()->andReturn(false); + $job->shouldReceive('isDeletedOrReleased')->once()->andReturn(true); + $job->shouldReceive('uuid')->andReturn('simple-test-uuid'); + + $instance->call($job, [ + 'command' => serialize($command = new $class), + ]); + + $this->assertTrue($class::$handled); + } + protected function assertJobRanSuccessfully($class) { $class::$handled = false; @@ -314,6 +340,25 @@ public function middleware() } } +class CircuitBreakerSkipJob +{ + use InteractsWithQueue, Queueable; + + public static $handled = false; + + public function handle() + { + static::$handled = true; + + throw new Exception; + } + + public function middleware() + { + return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(Exception::class)]; + } +} + class CircuitBreakerSuccessfulJob { use InteractsWithQueue, Queueable; diff --git a/tests/Log/ContextTest.php b/tests/Log/ContextTest.php index dfb76df9194b..ef9d61f0bccc 100644 --- a/tests/Log/ContextTest.php +++ b/tests/Log/ContextTest.php @@ -364,6 +364,34 @@ public function test_it_can_retrieve_subset_of_context() ])); } + public function test_it_can_exclude_subset_of_context() + { + Context::add('parent.child.1', 5); + Context::add('parent.child.2', 6); + Context::add('another', 7); + + $this->assertSame([ + 'another' => 7, + ], Context::except([ + 'parent.child.1', + 'parent.child.2', + ])); + } + + public function test_it_can_exclude_subset_of_hidden_context() + { + Context::addHidden('parent.child.1', 5); + Context::addHidden('parent.child.2', 6); + Context::addHidden('another', 7); + + $this->assertSame([ + 'another' => 7, + ], Context::exceptHidden([ + 'parent.child.1', + 'parent.child.2', + ])); + } + public function test_it_adds_context_to_logging() { $path = storage_path('logs/laravel.log'); diff --git a/tests/Routing/RoutingUrlGeneratorTest.php b/tests/Routing/RoutingUrlGeneratorTest.php index f03e4b10bb8c..54eab01a1cc8 100755 --- a/tests/Routing/RoutingUrlGeneratorTest.php +++ b/tests/Routing/RoutingUrlGeneratorTest.php @@ -1066,6 +1066,24 @@ public function testComplexRouteGenerationWithDefaultsAndBindingFields() $url->route('tenantSlugPost', ['post' => $keyParam('concretePost')]), ); + // Repeat the two assertions above without the 'tenant' default (without slug) + $url->defaults(['tenant' => null]); + + // tenantSlugPost: Tenant (with default) omitted, post passed positionally, with the default value for 'tenant' (without slug) removed + $this->assertSame( + 'https://www.foo.com/tenantSlugPost/defaultTenantSlug/concretePost', + $url->route('tenantSlugPost', [$keyParam('concretePost')]), + ); + + // tenantSlugPost: Tenant (with default) omitted, post passed using key, with the default value for 'tenant' (without slug) removed + $this->assertSame( + 'https://www.foo.com/tenantSlugPost/defaultTenantSlug/concretePost', + $url->route('tenantSlugPost', ['post' => $keyParam('concretePost')]), + ); + + // Revert the default value for the tenant parameter back + $url->defaults(['tenant' => 'defaultTenant']); + /** * One parameter with a default value, one without a default value. * diff --git a/tests/Support/Common.php b/tests/Support/Common.php new file mode 100644 index 000000000000..7928b8705624 --- /dev/null +++ b/tests/Support/Common.php @@ -0,0 +1,62 @@ + 'bar']; + } +} + +class TestJsonableObject implements Jsonable +{ + public function toJson($options = 0) + { + return '{"foo":"bar"}'; + } +} + +class TestJsonSerializeObject implements JsonSerializable +{ + public function jsonSerialize(): array + { + return ['foo' => 'bar']; + } +} + +class TestJsonSerializeWithScalarValueObject implements JsonSerializable +{ + public function jsonSerialize(): string + { + return 'foo'; + } +} + +class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable +{ + public $items; + + public function __construct($items = []) + { + $this->items = $items; + } + + public function getIterator(): Traversable + { + return new ArrayIterator($this->items); + } + + public function jsonSerialize(): array + { + return json_decode(json_encode($this->items), true); + } +} diff --git a/tests/Support/SupportArrTest.php b/tests/Support/SupportArrTest.php index e5d15a06362f..a81e6eb79bee 100644 --- a/tests/Support/SupportArrTest.php +++ b/tests/Support/SupportArrTest.php @@ -11,6 +11,10 @@ use InvalidArgumentException; use PHPUnit\Framework\TestCase; use stdClass; +use WeakMap; + +include_once 'Common.php'; +include_once 'Enums.php'; class SupportArrTest extends TestCase { @@ -32,6 +36,25 @@ public function testAccessible(): void $this->assertFalse(Arr::accessible(static fn () => null)); } + public function testArrayable(): void + { + $this->assertTrue(Arr::arrayable([])); + $this->assertTrue(Arr::arrayable(new TestArrayableObject)); + $this->assertTrue(Arr::arrayable(new TestJsonableObject)); + $this->assertTrue(Arr::arrayable(new TestJsonSerializeObject)); + $this->assertTrue(Arr::arrayable(new TestTraversableAndJsonSerializableObject)); + + $this->assertFalse(Arr::arrayable(null)); + $this->assertFalse(Arr::arrayable('abc')); + $this->assertFalse(Arr::arrayable(new stdClass)); + $this->assertFalse(Arr::arrayable((object) ['a' => 1, 'b' => 2])); + $this->assertFalse(Arr::arrayable(123)); + $this->assertFalse(Arr::arrayable(12.34)); + $this->assertFalse(Arr::arrayable(true)); + $this->assertFalse(Arr::arrayable(new \DateTime)); + $this->assertFalse(Arr::arrayable(static fn () => null)); + } + public function testAdd() { $array = Arr::add(['name' => 'Desk'], 'price', 100); @@ -1485,6 +1508,32 @@ public function testForget() $this->assertEquals([2 => [1 => 'products']], $array); } + public function testFrom() + { + $this->assertSame(['foo' => 'bar'], Arr::from(['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], Arr::from((object) ['foo' => 'bar'])); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestArrayableObject)); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonableObject)); + $this->assertSame(['foo' => 'bar'], Arr::from(new TestJsonSerializeObject)); + $this->assertSame(['foo'], Arr::from(new TestJsonSerializeWithScalarValueObject)); + + $this->assertSame(['name' => 'A'], Arr::from(TestEnum::A)); + $this->assertSame(['name' => 'A', 'value' => 1], Arr::from(TestBackedEnum::A)); + $this->assertSame(['name' => 'A', 'value' => 'A'], Arr::from(TestStringBackedEnum::A)); + + $subject = [new stdClass, new stdClass]; + $items = new TestTraversableAndJsonSerializableObject($subject); + $this->assertSame($subject, Arr::from($items)); + + $items = new WeakMap; + $items[$temp = new class {}] = 'bar'; + $this->assertSame(['bar'], Arr::from($items)); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Items cannot be represented by a scalar value.'); + Arr::from(123); + } + public function testWrap() { $string = 'a'; diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 7c8104b570ae..513e1fa4187a 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -8,7 +8,6 @@ use CachingIterator; use Exception; use Illuminate\Contracts\Support\Arrayable; -use Illuminate\Contracts\Support\Jsonable; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Collection; use Illuminate\Support\HtmlString; @@ -18,7 +17,6 @@ use Illuminate\Support\Str; use Illuminate\Support\Stringable; use InvalidArgumentException; -use IteratorAggregate; use JsonSerializable; use Mockery as m; use PHPUnit\Framework\Attributes\DataProvider; @@ -26,10 +24,10 @@ use ReflectionClass; use stdClass; use Symfony\Component\VarDumper\VarDumper; -use Traversable; use UnexpectedValueException; use WeakMap; +include_once 'Common.php'; include_once 'Enums.php'; class SupportCollectionTest extends TestCase @@ -2915,14 +2913,12 @@ public function testConstructMethodFromObject($collection) #[DataProvider('collectionClassProvider')] public function testConstructMethodFromWeakMap($collection) { - $this->expectException('InvalidArgumentException'); - $map = new WeakMap(); $object = new stdClass; $object->foo = 'bar'; $map[$object] = 3; - $data = new $collection($map); + $this->assertEquals([3], $data->all()); } public function testSplice() @@ -5787,30 +5783,6 @@ public function offsetUnset($offset): void } } -class TestArrayableObject implements Arrayable -{ - public function toArray() - { - return ['foo' => 'bar']; - } -} - -class TestJsonableObject implements Jsonable -{ - public function toJson($options = 0) - { - return '{"foo":"bar"}'; - } -} - -class TestJsonSerializeObject implements JsonSerializable -{ - public function jsonSerialize(): array - { - return ['foo' => 'bar']; - } -} - class TestJsonSerializeToStringObject implements JsonSerializable { public function jsonSerialize(): string @@ -5819,34 +5791,6 @@ public function jsonSerialize(): string } } -class TestJsonSerializeWithScalarValueObject implements JsonSerializable -{ - public function jsonSerialize(): string - { - return 'foo'; - } -} - -class TestTraversableAndJsonSerializableObject implements IteratorAggregate, JsonSerializable -{ - public $items; - - public function __construct($items) - { - $this->items = $items; - } - - public function getIterator(): Traversable - { - return new ArrayIterator($this->items); - } - - public function jsonSerialize(): array - { - return json_decode(json_encode($this->items), true); - } -} - class TestCollectionMapIntoObject { public $value; diff --git a/tests/Support/SupportNumberTest.php b/tests/Support/SupportNumberTest.php index 8f7567433baa..8d7ba346cf99 100644 --- a/tests/Support/SupportNumberTest.php +++ b/tests/Support/SupportNumberTest.php @@ -174,18 +174,38 @@ public function testBytesToHuman() $this->assertSame('0 B', Number::fileSize(0)); $this->assertSame('0.00 B', Number::fileSize(0, precision: 2)); $this->assertSame('1 B', Number::fileSize(1)); - $this->assertSame('1 KB', Number::fileSize(1024)); - $this->assertSame('2 KB', Number::fileSize(2048)); - $this->assertSame('2.00 KB', Number::fileSize(2048, precision: 2)); - $this->assertSame('1.23 KB', Number::fileSize(1264, precision: 2)); - $this->assertSame('1.234 KB', Number::fileSize(1264.12345, maxPrecision: 3)); - $this->assertSame('1.234 KB', Number::fileSize(1264, 3)); - $this->assertSame('5 GB', Number::fileSize(1024 * 1024 * 1024 * 5)); - $this->assertSame('10 TB', Number::fileSize((1024 ** 4) * 10)); - $this->assertSame('10 PB', Number::fileSize((1024 ** 5) * 10)); - $this->assertSame('1 ZB', Number::fileSize(1024 ** 7)); - $this->assertSame('1 YB', Number::fileSize(1024 ** 8)); - $this->assertSame('1,024 YB', Number::fileSize(1024 ** 9)); + $this->assertSame('1 KB', Number::fileSize(1000)); + $this->assertSame('2 KB', Number::fileSize(2000)); + $this->assertSame('2.00 KB', Number::fileSize(2000, precision: 2)); + $this->assertSame('1.23 KB', Number::fileSize(1234, precision: 2)); + $this->assertSame('1.234 KB', Number::fileSize(1234, maxPrecision: 3)); + $this->assertSame('1.234 KB', Number::fileSize(1234, 3)); + $this->assertSame('5 GB', Number::fileSize(1000 * 1000 * 1000 * 5)); + $this->assertSame('10 TB', Number::fileSize((1000 ** 4) * 10)); + $this->assertSame('10 PB', Number::fileSize((1000 ** 5) * 10)); + $this->assertSame('1 ZB', Number::fileSize(1000 ** 7)); + $this->assertSame('1 YB', Number::fileSize(1000 ** 8)); + $this->assertSame('1 RB', Number::fileSize(1000 ** 9)); + $this->assertSame('1 QB', Number::fileSize(1000 ** 10)); + $this->assertSame('1,000 QB', Number::fileSize(1000 ** 11)); + + $this->assertSame('0 B', Number::fileSize(0, useBinaryPrefix: true)); + $this->assertSame('0.00 B', Number::fileSize(0, precision: 2, useBinaryPrefix: true)); + $this->assertSame('1 B', Number::fileSize(1, useBinaryPrefix: true)); + $this->assertSame('1 KiB', Number::fileSize(1024, useBinaryPrefix: true)); + $this->assertSame('2 KiB', Number::fileSize(2048, useBinaryPrefix: true)); + $this->assertSame('2.00 KiB', Number::fileSize(2048, precision: 2, useBinaryPrefix: true)); + $this->assertSame('1.23 KiB', Number::fileSize(1264, precision: 2, useBinaryPrefix: true)); + $this->assertSame('1.234 KiB', Number::fileSize(1264.12345, maxPrecision: 3, useBinaryPrefix: true)); + $this->assertSame('1.234 KiB', Number::fileSize(1264, 3, useBinaryPrefix: true)); + $this->assertSame('5 GiB', Number::fileSize(1024 * 1024 * 1024 * 5, useBinaryPrefix: true)); + $this->assertSame('10 TiB', Number::fileSize((1024 ** 4) * 10, useBinaryPrefix: true)); + $this->assertSame('10 PiB', Number::fileSize((1024 ** 5) * 10, useBinaryPrefix: true)); + $this->assertSame('1 ZiB', Number::fileSize(1024 ** 7, useBinaryPrefix: true)); + $this->assertSame('1 YiB', Number::fileSize(1024 ** 8, useBinaryPrefix: true)); + $this->assertSame('1 RiB', Number::fileSize(1024 ** 9, useBinaryPrefix: true)); + $this->assertSame('1 QiB', Number::fileSize(1024 ** 10, useBinaryPrefix: true)); + $this->assertSame('1,024 QiB', Number::fileSize(1024 ** 11, useBinaryPrefix: true)); } public function testClamp()