diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8841dc0062..ef608478f8ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,47 @@ # Release Notes for 10.x -## [Unreleased](https://github.com/laravel/framework/compare/v10.7.1...10.x) +## [Unreleased](https://github.com/laravel/framework/compare/v10.9.0...10.x) + + +## [v10.9.0 (2023-04-25)](https://github.com/laravel/framework/compare/v10.8.0...v10.9.0) + +### Added +- Add new HTTP status assertions ([#46841](https://github.com/laravel/framework/pull/46841)) +- Allow pruning all cancelled and unfinished queue batches ([#46833](https://github.com/laravel/framework/pull/46833)) +- Added `IGNITION_LOCAL_SITES_PATH` to `$passthroughVariables` in `ServeCommand.php` ([#46857](https://github.com/laravel/framework/pull/46857)) +- Added named static methods for middleware ([#46362](https://github.com/laravel/framework/pull/46362)) + +### Fixed +- Fix date_format rule throw ValueError ([#46824](https://github.com/laravel/framework/pull/46824)) + +### Changed +- Allow separate directory for locks on filestore ([#46811](https://github.com/laravel/framework/pull/46811)) +- Allow to whereMorphedTo work with null model ([#46821](https://github.com/laravel/framework/pull/46821)) +- Use pivot model fromDateTime instead of assuming Carbon in `Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable::addTimestampsToAttachment()` ([#46822](https://github.com/laravel/framework/pull/46822)) +- Make rules method in FormRequest optional ([#46846](https://github.com/laravel/framework/pull/46846)) +- Throw LogicException when calling FileFactory@image() if mimetype is not supported ([#46859](https://github.com/laravel/framework/pull/46859)) +- Improve job release method to accept date instance ([#46854](https://github.com/laravel/framework/pull/46854)) +- Use foreignUlid if model uses HasUlids trait when call foreignIdFor ([#46876](https://github.com/laravel/framework/pull/46876)) + + +## [v10.8.0 (2023-04-18)](https://github.com/laravel/framework/compare/v10.7.1...v10.8.0) + +### Added +- Added syntax sugar to the Process::pipe method ([#46745](https://github.com/laravel/framework/pull/46745)) +- Allow specifying index name when calling ForeignIdColumnDefinition@constrained() ([#46746](https://github.com/laravel/framework/pull/46746)) +- Allow to customise redirect URL in AuthenticateSession Middleware ([#46752](https://github.com/laravel/framework/pull/46752)) +- Added Class based after validation rules ([#46757](https://github.com/laravel/framework/pull/46757)) +- Added max exceptions to broadcast event ([#46800](https://github.com/laravel/framework/pull/46800)) + +### Fixed +- Fixed compiled view file ends with .php ([#46755](https://github.com/laravel/framework/pull/46755)) +- Fix validation rule names ([#46768](https://github.com/laravel/framework/pull/46768)) +- Fixed validateDecimal() ([#46809](https://github.com/laravel/framework/pull/46809)) + +### Changed +- Add headers to exception in `Illuminate/Foundation/Application::abourd()` ([#46780](https://github.com/laravel/framework/pull/46780)) +- Minor skeleton slimming (framework edition) ([#46786](https://github.com/laravel/framework/pull/46786)) +- Release lock for job implementing ShouldBeUnique that is dispatched afterResponse() ([#46806](https://github.com/laravel/framework/pull/46806)) ## [v10.7.1 (2023-04-11)](https://github.com/laravel/framework/compare/v10.7.0...v10.7.1) @@ -8,7 +49,6 @@ ### Changed - Changed `Illuminate/Process/Factory::pipe()` method. It will be run pipes immediately ([e34ab39](https://github.com/laravel/framework/commit/e34ab392800bfc175334c90e9321caa7261c2d65)) - ## [v10.7.0 (2023-04-11)](https://github.com/laravel/framework/compare/v10.6.2...v10.7.0) ### Added @@ -29,7 +69,6 @@ - Removes unnecessary parameters in `creatable()` / `destroyable()` methods in `Illuminate/Routing/PendingSingletonResourceRegistration` ([#46677](https://github.com/laravel/framework/pull/46677)) - Return non-zero exit code for uncaught exceptions ([#46541](https://github.com/laravel/framework/pull/46541)) - ## [v10.6.2 (2023-04-05)](https://github.com/laravel/framework/compare/v10.6.1...v10.6.2) ### Added @@ -39,13 +78,11 @@ - Added missing ignored methods to `Illuminate/View/Component` ([#46692](https://github.com/laravel/framework/pull/46692)) - console.stub: remove void return type from handle ([#46697](https://github.com/laravel/framework/pull/46697)) - ## [v10.6.1 (2023-04-04)](https://github.com/laravel/framework/compare/v10.6.0...v10.6.1) ### Reverted - Reverted ["Set container instance on session manager"Set container instance on session manager](https://github.com/laravel/framework/pull/46621) ([#46691](https://github.com/laravel/framework/pull/46691)) - ## [v10.6.0 (2023-04-04)](https://github.com/laravel/framework/compare/v10.5.1...v10.6.0) ### Added @@ -57,7 +94,6 @@ - Allow $sleepMilliseconds parameter receive a Closure in retry method from PendingRequest ([#46653](https://github.com/laravel/framework/pull/46653)) - Support contextual binding on first class callables ([de8d515](https://github.com/laravel/framework/commit/de8d515fc6d1fabc8f14450342554e0eb67df725), [e511a3b](https://github.com/laravel/framework/commit/e511a3bdb15c294866428b4fe665a4ad14540038)) - ## [v10.5.1 (2023-03-29)](https://github.com/laravel/framework/compare/v10.5.0...v10.5.1) ### Added @@ -70,7 +106,6 @@ - Make sure pivot model has previously defined values ([#46559](https://github.com/laravel/framework/pull/46559)) - Move SetUniqueIds to run before the creating event ([#46622](https://github.com/laravel/framework/pull/46622)) - ## [v10.5.0 (2023-03-28)](https://github.com/laravel/framework/compare/v10.4.1...v10.5.0) ### Added @@ -91,13 +126,11 @@ - allow override of the Builder paginate() total ([#46415](https://github.com/laravel/framework/pull/46415)) - Add a possibility to set a custom on_stats function for the Http Facade ([#46569](https://github.com/laravel/framework/pull/46569)) - ## [v10.4.1 (2023-03-18)](https://github.com/laravel/framework/compare/v10.4.0...v10.4.1) ### Changed - Move Symfony events dispatcher registration to Console\Kernel ([#46508](https://github.com/laravel/framework/pull/46508)) - ## [v10.4.0 (2023-03-17)](https://github.com/laravel/framework/compare/v10.3.3...v10.4.0) ### Added @@ -119,13 +152,11 @@ - Convert eloquent builder to base builder in whereExists ([#46460](https://github.com/laravel/framework/pull/46460)) - Refactor shared static methodExcludedByOptions method to trait ([#46498](https://github.com/laravel/framework/pull/46498)) - ## [v10.3.3 (2023-03-09)](https://github.com/laravel/framework/compare/v10.3.2...v10.3.3) ### Reverted - Reverted ["Allow override of the Builder paginate() total"](https://github.com/laravel/framework/pull/46336) ([#46406](https://github.com/laravel/framework/pull/46406)) - ## [v10.3.2 (2023-03-08)](https://github.com/laravel/framework/compare/v10.3.1...v10.3.2) ### Reverted @@ -134,13 +165,11 @@ ### Fixed - Fixes Expression no longer implements Stringable ([#46395](https://github.com/laravel/framework/pull/46395)) - ## [v10.3.1 (2023-03-08)](https://github.com/laravel/framework/compare/v10.3.0...v10.3.1) ### Reverted - Reverted ["Use fallback when previous URL is the same as the current in `Illuminate/Routing/UrlGenerator::previous()`"](https://github.com/laravel/framework/pull/46234) ([#46392](https://github.com/laravel/framework/pull/46392)) - ## [v10.3.0 (2023-03-07)](https://github.com/laravel/framework/compare/v10.2.0...v10.3.0) ### Added @@ -158,7 +187,6 @@ - Use fallback when previous URL is the same as the current in `Illuminate/Routing/UrlGenerator::previous()` ([#46234](https://github.com/laravel/framework/pull/46234)) - Allow override of the Builder paginate() total ([#46336](https://github.com/laravel/framework/pull/46336)) - ## [v10.2.0 (2023-03-02)](https://github.com/laravel/framework/compare/v10.1.5...v10.2.0) ### Added @@ -182,7 +210,6 @@ - Remove obsolete function_exists('enum_exists') calls ([#46319](https://github.com/laravel/framework/pull/46319)) - Cast json decoded failed_job_ids to array in DatabaseBatchRepository::toBatch ([#46329](https://github.com/laravel/framework/pull/46329)) - ## [v10.1.5 (2023-02-24)](https://github.com/laravel/framework/compare/v10.1.4...v10.1.5) ### Fixed @@ -192,13 +219,11 @@ ### Changed - Remove autoload dumping from make:migration ([#46215](https://github.com/laravel/framework/pull/46215)) - ## [v10.1.4 (2023-02-23)](https://github.com/laravel/framework/compare/v10.1.3...v10.1.4) ### Changed - Improve Facade Fake Awareness ([#46188](https://github.com/laravel/framework/pull/46188), [#46232](https://github.com/laravel/framework/pull/46232)) - ## [v10.1.3 (2023-02-22)](https://github.com/laravel/framework/compare/v10.1.2...v10.1.3) ### Added @@ -210,13 +235,11 @@ ### Changes - Accept time when generating ULID in `Str::ulid()` ([#46201](https://github.com/laravel/framework/pull/46201)) - ## [v10.1.2 (2023-02-22)](https://github.com/laravel/framework/compare/v10.1.1...v10.1.2) ### Reverted - Revert changes from `Arr::random()` ([cf3eb90](https://github.com/laravel/framework/commit/cf3eb90a6473444bb7a78d1a3af1e9312a62020d)) - ## [v10.1.1 (2023-02-21)](https://github.com/laravel/framework/compare/v10.1.0...v10.1.1) ### Added @@ -225,7 +248,6 @@ ### Fixed - Fixed `Illuminate/Collections/Arr::shuffle()` for empty array ([0c6cae0](https://github.com/laravel/framework/commit/0c6cae0ef647158b9554cad05ff39db7e7ad0d33)) - ## [v10.1.0 (2023-02-21)](https://github.com/laravel/framework/compare/v10.0.3...v10.1.0) ### Fixed @@ -237,19 +259,16 @@ - Use mixed return type on controller stubs ([#46166](https://github.com/laravel/framework/pull/46166)) - Use InteractsWithDictionary in Eloquent collection ([#46196](https://github.com/laravel/framework/pull/46196)) - ## [v10.0.3 (2023-02-17)](https://github.com/laravel/framework/compare/v10.0.2...v10.0.3) ### Added - Added missing expression support for pluck in Builder ([#46146](https://github.com/laravel/framework/pull/46146)) - ## [v10.0.2 (2023-02-16)](https://github.com/laravel/framework/compare/v10.0.1...v10.0.2) ### Added - Register policies automatically to the gate ([#46132](https://github.com/laravel/framework/pull/46132)) - ## [v10.0.1 (2023-02-16)](https://github.com/laravel/framework/compare/v10.0.0...v10.0.1) ### Added @@ -262,7 +281,6 @@ - Add AddQueuedCookiesToResponse to middlewarePriority so it is handled in the right place ([#46130](https://github.com/laravel/framework/pull/46130)) - Show queue connection in MonitorCommand ([#46122](https://github.com/laravel/framework/pull/46122)) - ## [v10.0.0 (2023-02-14)](https://github.com/laravel/framework/compare/v10.0.0...10.x) Please consult the [upgrade guide](https://laravel.com/docs/10.x/upgrade) and [release notes](https://laravel.com/docs/10.x/releases) in the official Laravel documentation. diff --git a/src/Illuminate/Auth/Middleware/Authenticate.php b/src/Illuminate/Auth/Middleware/Authenticate.php index 05869b00a88c..b1b4cf90abc2 100644 --- a/src/Illuminate/Auth/Middleware/Authenticate.php +++ b/src/Illuminate/Auth/Middleware/Authenticate.php @@ -28,6 +28,18 @@ public function __construct(Auth $auth) $this->auth = $auth; } + /** + * Specify the guards for the middleware. + * + * @param string $guard + * @param string $others + * @return string + */ + public static function using($guard, ...$others) + { + return static::class.':'.implode(',', [$guard, ...$others]); + } + /** * Handle an incoming request. * diff --git a/src/Illuminate/Auth/Middleware/AuthenticateWithBasicAuth.php b/src/Illuminate/Auth/Middleware/AuthenticateWithBasicAuth.php index 92c81e68be47..0b4510c0fb66 100644 --- a/src/Illuminate/Auth/Middleware/AuthenticateWithBasicAuth.php +++ b/src/Illuminate/Auth/Middleware/AuthenticateWithBasicAuth.php @@ -25,6 +25,20 @@ public function __construct(AuthFactory $auth) $this->auth = $auth; } + /** + * Specify the guard and field for the middleware. + * + * @param string|null $guard + * @param string|null $field + * @return string + * + * @named-arguments-supported + */ + public static function using($guard = null, $field = null) + { + return static::class.':'.implode(',', func_get_args()); + } + /** * Handle an incoming request. * diff --git a/src/Illuminate/Auth/Middleware/Authorize.php b/src/Illuminate/Auth/Middleware/Authorize.php index 1af05beb2fac..83e6d60ed396 100644 --- a/src/Illuminate/Auth/Middleware/Authorize.php +++ b/src/Illuminate/Auth/Middleware/Authorize.php @@ -26,6 +26,18 @@ public function __construct(Gate $gate) $this->gate = $gate; } + /** + * Specify the ability and models for the middleware. + * + * @param string $ability + * @param string ...$models + * @return string + */ + public static function using($ability, ...$models) + { + return static::class.':'.implode(',', [$ability, ...$models]); + } + /** * Handle an incoming request. * diff --git a/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php b/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php index 8f2b33ae5c72..10a3f7c65538 100644 --- a/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php +++ b/src/Illuminate/Auth/Middleware/EnsureEmailIsVerified.php @@ -9,6 +9,17 @@ class EnsureEmailIsVerified { + /** + * Specify the redirect route for the middleware. + * + * @param string $route + * @return string + */ + public static function redirectTo($route) + { + return static::class.':'.$route; + } + /** * Handle an incoming request. * diff --git a/src/Illuminate/Auth/Middleware/RequirePassword.php b/src/Illuminate/Auth/Middleware/RequirePassword.php index 4ed43954da22..fac82c87ab13 100644 --- a/src/Illuminate/Auth/Middleware/RequirePassword.php +++ b/src/Illuminate/Auth/Middleware/RequirePassword.php @@ -44,13 +44,27 @@ public function __construct(ResponseFactory $responseFactory, UrlGenerator $urlG $this->passwordTimeout = $passwordTimeout ?: 10800; } + /** + * Specify the redirect route and timeout for the middleware. + * + * @param string|null $redirectToRoute + * @param string|null $passwordTimeoutSeconds + * @return string + * + * @named-arguments-supported + */ + public static function using($redirectToRoute = null, $passwordTimeoutSeconds = null) + { + return static::class.':'.implode(',', func_get_args()); + } + /** * Handle an incoming request. * * @param \Illuminate\Http\Request $request * @param \Closure $next * @param string|null $redirectToRoute - * @param int|null $passwordTimeoutSeconds + * @param string|int|null $passwordTimeoutSeconds * @return mixed */ public function handle($request, Closure $next, $redirectToRoute = null, $passwordTimeoutSeconds = null) @@ -63,7 +77,7 @@ public function handle($request, Closure $next, $redirectToRoute = null, $passwo } return $this->responseFactory->redirectGuest( - $this->urlGenerator->route($redirectToRoute ?? 'password.confirm') + $this->urlGenerator->route($redirectToRoute ?: 'password.confirm') ); } diff --git a/src/Illuminate/Broadcasting/BroadcastEvent.php b/src/Illuminate/Broadcasting/BroadcastEvent.php index fef5423d7d18..3ff22b2ddeec 100644 --- a/src/Illuminate/Broadcasting/BroadcastEvent.php +++ b/src/Illuminate/Broadcasting/BroadcastEvent.php @@ -42,6 +42,13 @@ class BroadcastEvent implements ShouldQueue */ public $backoff; + /** + * The maximum number of unhandled exceptions to allow before failing. + * + * @var int + */ + public $maxExceptions; + /** * Create a new job handler instance. * @@ -55,6 +62,7 @@ public function __construct($event) $this->timeout = property_exists($event, 'timeout') ? $event->timeout : null; $this->backoff = property_exists($event, 'backoff') ? $event->backoff : null; $this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null; + $this->maxExceptions = property_exists($event, 'maxExceptions') ? $event->maxExceptions : null; } /** diff --git a/src/Illuminate/Bus/Dispatcher.php b/src/Illuminate/Bus/Dispatcher.php index 4dc390e653fb..8ed3a21b7c4f 100644 --- a/src/Illuminate/Bus/Dispatcher.php +++ b/src/Illuminate/Bus/Dispatcher.php @@ -263,7 +263,7 @@ protected function pushCommandToQueue($queue, $command) public function dispatchAfterResponse($command, $handler = null) { $this->container->terminating(function () use ($command, $handler) { - $this->dispatchNow($command, $handler); + $this->dispatchSync($command, $handler); }); } diff --git a/src/Illuminate/Cache/CacheManager.php b/src/Illuminate/Cache/CacheManager.php index 98babebf91fb..a821f9261e13 100755 --- a/src/Illuminate/Cache/CacheManager.php +++ b/src/Illuminate/Cache/CacheManager.php @@ -144,7 +144,10 @@ protected function createArrayDriver(array $config) */ protected function createFileDriver(array $config) { - return $this->repository(new FileStore($this->app['files'], $config['path'], $config['permission'] ?? null)); + return $this->repository( + (new FileStore($this->app['files'], $config['path'], $config['permission'] ?? null)) + ->setLockDirectory($config['lock_path'] ?? null) + ); } /** diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index 7202ee63bc6a..424ca63d43c6 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -28,6 +28,13 @@ class FileStore implements Store, LockProvider */ protected $directory; + /** + * The file cache lock directory. + * + * @var string|null + */ + protected $lockDirectory; + /** * Octal representation of the cache file permissions. * @@ -210,7 +217,14 @@ public function forever($key, $value) */ public function lock($name, $seconds = 0, $owner = null) { - return new FileLock($this, $name, $seconds, $owner); + $this->ensureCacheDirectoryExists($this->lockDirectory ?? $this->directory); + + return new FileLock( + new static($this->files, $this->lockDirectory ?? $this->directory, $this->filePermission), + $name, + $seconds, + $owner + ); } /** @@ -364,6 +378,19 @@ public function getDirectory() return $this->directory; } + /** + * Set the cache directory where locks should be stored. + * + * @param string|null $lockDirectory + * @return $this + */ + public function setLockDirectory($lockDirectory) + { + $this->lockDirectory = $lockDirectory; + + return $this; + } + /** * Get the cache key prefix. * diff --git a/src/Illuminate/Cache/Lock.php b/src/Illuminate/Cache/Lock.php index bed170507a9a..ccd1c6474a5f 100644 --- a/src/Illuminate/Cache/Lock.php +++ b/src/Illuminate/Cache/Lock.php @@ -5,6 +5,7 @@ use Illuminate\Contracts\Cache\Lock as LockContract; use Illuminate\Contracts\Cache\LockTimeoutException; use Illuminate\Support\InteractsWithTime; +use Illuminate\Support\Sleep; use Illuminate\Support\Str; abstract class Lock implements LockContract @@ -114,7 +115,7 @@ public function block($seconds, $callback = null) $starting = $this->currentTime(); while (! $this->acquire()) { - usleep($this->sleepMilliseconds * 1000); + Sleep::usleep($this->sleepMilliseconds * 1000); if ($this->currentTime() - $seconds >= $starting) { throw new LockTimeoutException; diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index 5ca863a6790f..da46b5b831a1 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -770,6 +770,18 @@ public static function sortRecursive($array, $options = SORT_REGULAR, $descendin return $array; } + /** + * Recursively sort an array by keys and values in descending order. + * + * @param array $array + * @param int $options + * @return array + */ + public function sortRecursiveDesc($array, $options = SORT_REGULAR) + { + return $this->sortRecursive($array, $options, true); + } + /** * Conditionally compile classes from an array into a CSS class list. * diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 390c2b9ceeee..12356da1545a 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -12,7 +12,8 @@ /** * @template TKey of array-key - * @template TValue + * + * @template-covariant TValue * * @implements \ArrayAccess * @implements \Illuminate\Support\Enumerable diff --git a/src/Illuminate/Collections/Enumerable.php b/src/Illuminate/Collections/Enumerable.php index 0bc440f0b4f1..a561488e8a59 100644 --- a/src/Illuminate/Collections/Enumerable.php +++ b/src/Illuminate/Collections/Enumerable.php @@ -12,7 +12,8 @@ /** * @template TKey of array-key - * @template TValue + * + * @template-covariant TValue * * @extends \Illuminate\Contracts\Support\Arrayable * @extends \IteratorAggregate @@ -549,7 +550,7 @@ public function hasAny($key); /** * Concatenate values of a given key as a string. * - * @param string $value + * @param callable|string $value * @param string|null $glue * @return string */ diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 1b7830180df9..39c7ecc4f8a9 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -16,7 +16,8 @@ /** * @template TKey of array-key - * @template TValue + * + * @template-covariant TValue * * @implements \Illuminate\Support\Enumerable */ diff --git a/src/Illuminate/Collections/Traits/EnumeratesValues.php b/src/Illuminate/Collections/Traits/EnumeratesValues.php index 92d4ec15ebc7..f9614963ae36 100644 --- a/src/Illuminate/Collections/Traits/EnumeratesValues.php +++ b/src/Illuminate/Collections/Traits/EnumeratesValues.php @@ -19,7 +19,8 @@ /** * @template TKey of array-key - * @template TValue + * + * @template-covariant TValue * * @property-read HigherOrderCollectionProxy $average * @property-read HigherOrderCollectionProxy $avg diff --git a/src/Illuminate/Console/Command.php b/src/Illuminate/Console/Command.php index 4ae96d68d97a..4d27527b9245 100755 --- a/src/Illuminate/Console/Command.php +++ b/src/Illuminate/Console/Command.php @@ -61,6 +61,20 @@ class Command extends SymfonyCommand */ protected $hidden = false; + /** + * Indicates whether only one instance of the command can run at any given time. + * + * @var bool + */ + protected $isolated = false; + + /** + * The default exit code for isolated commands. + * + * @var int + */ + protected $isolatedExitCode = self::SUCCESS; + /** * The console command name aliases. * @@ -140,7 +154,7 @@ protected function configureIsolation() null, InputOption::VALUE_OPTIONAL, 'Do not run the command if another instance of the command is already running', - false + $this->isolated )); } @@ -185,7 +199,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return (int) (is_numeric($this->option('isolated')) ? $this->option('isolated') - : self::SUCCESS); + : $this->isolatedExitCode); } $method = method_exists($this, 'handle') ? 'handle' : '__invoke'; diff --git a/src/Illuminate/Contracts/Cache/Repository.php b/src/Illuminate/Contracts/Cache/Repository.php index 0547aaa3de60..4bc4638e46fe 100644 --- a/src/Illuminate/Contracts/Cache/Repository.php +++ b/src/Illuminate/Contracts/Cache/Repository.php @@ -71,7 +71,7 @@ public function forever($key, $value); * @template TCacheValue * * @param string $key - * @param \DateTimeInterface|\DateInterval|int|null $ttl + * @param \DateTimeInterface|\DateInterval|\Closure|int|null $ttl * @param \Closure(): TCacheValue $callback * @return TCacheValue */ diff --git a/src/Illuminate/Contracts/Pagination/Paginator.php b/src/Illuminate/Contracts/Pagination/Paginator.php index 09292f4a3143..04201cef4e9a 100644 --- a/src/Illuminate/Contracts/Pagination/Paginator.php +++ b/src/Illuminate/Contracts/Pagination/Paginator.php @@ -53,14 +53,14 @@ public function items(); /** * Get the "index" of the first item being paginated. * - * @return int + * @return int|null */ public function firstItem(); /** * Get the "index" of the last item being paginated. * - * @return int + * @return int|null */ public function lastItem(); diff --git a/src/Illuminate/Database/Connection.php b/src/Illuminate/Database/Connection.php index bb57b17385a4..2e392ad9bfa7 100755 --- a/src/Illuminate/Database/Connection.php +++ b/src/Illuminate/Database/Connection.php @@ -324,7 +324,7 @@ public function getSchemaBuilder() /** * Begin a fluent query against a database table. * - * @param \Closure|\Illuminate\Database\Query\Builder|string $table + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|string $table * @param string|null $as * @return \Illuminate\Database\Query\Builder */ diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 86cef124a568..d2649c28718d 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -32,6 +32,7 @@ use Illuminate\Support\Exceptions\MathException; use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\Date; +use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; use InvalidArgumentException; use LogicException; @@ -104,6 +105,7 @@ trait HasAttributes 'encrypted:json', 'encrypted:object', 'float', + 'hashed', 'immutable_date', 'immutable_datetime', 'immutable_custom_datetime', @@ -985,6 +987,10 @@ public function setAttribute($key, $value) $value = $this->castAttributeAsEncryptedString($key, $value); } + if (! is_null($value) && $this->hasCast($key, 'hashed')) { + $value = $this->castAttributeAsHashedString($key, $value); + } + $this->attributes[$key] = $value; return $this; @@ -1293,6 +1299,18 @@ public static function encryptUsing($encrypter) static::$encrypter = $encrypter; } + /** + * Cast the given attribute to a hashed string. + * + * @param string $key + * @param mixed $value + * @return string + */ + protected function castAttributeAsHashedString($key, $value) + { + return Hash::needsRehash($value) ? Hash::make($value) : $value; + } + /** * Decode the given float. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php index 72afb178897b..5d7047953115 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasGlobalScopes.php @@ -59,6 +59,27 @@ public static function getGlobalScope($scope) ); } + /** + * Get all of the global scopes that are currently registered. + * + * @return array + */ + public static function getAllGlobalScopes() + { + return static::$globalScopes; + } + + /** + * Set the current global scopes. + * + * @param array $scopes + * @return void + */ + public static function setAllGlobalScopes($scopes) + { + static::$globalScopes = $scopes; + } + /** * Get the global scopes for this class instance. * diff --git a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php index 2412bc68fd03..5933f192ebb9 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php +++ b/src/Illuminate/Database/Eloquent/Concerns/QueriesRelationships.php @@ -448,7 +448,7 @@ public function orWhereMorphRelation($relation, $types, $column, $operator = nul * Add a morph-to relationship condition to the query. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param \Illuminate\Database\Eloquent\Model|string $model + * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return \Illuminate\Database\Eloquent\Builder|static */ public function whereMorphedTo($relation, $model, $boolean = 'and') @@ -457,6 +457,10 @@ public function whereMorphedTo($relation, $model, $boolean = 'and') $relation = $this->getRelationWithoutConstraints($relation); } + if (is_null($model)) { + return $this->whereNull($relation->getMorphType(), $boolean); + } + if (is_string($model)) { $morphMap = Relation::morphMap(); @@ -506,7 +510,7 @@ public function whereNotMorphedTo($relation, $model, $boolean = 'and') * Add a morph-to relationship condition to the query with an "or where" clause. * * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation - * @param \Illuminate\Database\Eloquent\Model|string $model + * @param \Illuminate\Database\Eloquent\Model|string|null $model * @return \Illuminate\Database\Eloquent\Builder|static */ public function orWhereMorphedTo($relation, $model) diff --git a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php index ed8a9b7ba8a1..04862e5d80f7 100644 --- a/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php +++ b/src/Illuminate/Database/Eloquent/Relations/Concerns/InteractsWithPivotTable.php @@ -396,7 +396,7 @@ protected function addTimestampsToAttachment(array $record, $exists = false) if ($this->using) { $pivotModel = new $this->using; - $fresh = $fresh->format($pivotModel->getDateFormat()); + $fresh = $pivotModel->fromDateTime($fresh); } if (! $exists && $this->hasPivotColumn($this->createdAt())) { diff --git a/src/Illuminate/Database/Schema/Blueprint.php b/src/Illuminate/Database/Schema/Blueprint.php index 00fe7c55e086..74b584b51a68 100755 --- a/src/Illuminate/Database/Schema/Blueprint.php +++ b/src/Illuminate/Database/Schema/Blueprint.php @@ -5,6 +5,7 @@ use BadMethodCallException; use Closure; use Illuminate\Database\Connection; +use Illuminate\Database\Eloquent\Concerns\HasUlids; use Illuminate\Database\Query\Expression; use Illuminate\Database\Schema\Grammars\Grammar; use Illuminate\Database\SQLiteConnection; @@ -938,9 +939,19 @@ public function foreignIdFor($model, $column = null) $model = new $model; } - return $model->getKeyType() === 'int' && $model->getIncrementing() - ? $this->foreignId($column ?: $model->getForeignKey()) - : $this->foreignUuid($column ?: $model->getForeignKey()); + $column = $column ?: $model->getForeignKey(); + + if ($model->getKeyType() === 'int' && $model->getIncrementing()) { + return $this->foreignId($column); + } + + $modelTraits = class_uses_recursive($model); + + if (in_array(HasUlids::class, $modelTraits, true)) { + return $this->foreignUlid($column); + } + + return $this->foreignUuid($column); } /** diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 799a446de548..210526a35cd7 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig * * @var string */ - const VERSION = '10.7.1'; + const VERSION = '10.9.0'; /** * The base path for the Laravel installation. @@ -1218,7 +1218,7 @@ public function isDownForMaintenance() public function abort($code, $message = '', array $headers = []) { if ($code == 404) { - throw new NotFoundHttpException($message); + throw new NotFoundHttpException($message, null, 0, $headers); } throw new HttpException($code, $message, null, $headers); diff --git a/src/Illuminate/Foundation/Bus/Dispatchable.php b/src/Illuminate/Foundation/Bus/Dispatchable.php index 8a6ab52f3d1b..46b500559ff7 100644 --- a/src/Illuminate/Foundation/Bus/Dispatchable.php +++ b/src/Illuminate/Foundation/Bus/Dispatchable.php @@ -84,7 +84,7 @@ public static function dispatchSync(...$arguments) */ public static function dispatchAfterResponse(...$arguments) { - return app(Dispatcher::class)->dispatchAfterResponse(new static(...$arguments)); + return self::dispatch(...$arguments)->afterResponse(); } /** diff --git a/src/Illuminate/Foundation/Console/ServeCommand.php b/src/Illuminate/Foundation/Console/ServeCommand.php index b966291f3ab0..84bcb05312a7 100644 --- a/src/Illuminate/Foundation/Console/ServeCommand.php +++ b/src/Illuminate/Foundation/Console/ServeCommand.php @@ -57,6 +57,7 @@ class ServeCommand extends Command */ public static $passthroughVariables = [ 'APP_ENV', + 'IGNITION_LOCAL_SITES_PATH', 'LARAVEL_SAIL', 'PATH', 'PHP_CLI_SERVER_WORKERS', diff --git a/src/Illuminate/Foundation/Console/stubs/request.stub b/src/Illuminate/Foundation/Console/stubs/request.stub index a7c76e404e5e..a9a50ae519af 100644 --- a/src/Illuminate/Foundation/Console/stubs/request.stub +++ b/src/Illuminate/Foundation/Console/stubs/request.stub @@ -17,7 +17,7 @@ class {{ class }} extends FormRequest /** * Get the validation rules that apply to the request. * - * @return array + * @return array */ public function rules(): array { diff --git a/src/Illuminate/Foundation/Http/FormRequest.php b/src/Illuminate/Foundation/Http/FormRequest.php index 30412b6430f5..2a88d9c5c4a4 100644 --- a/src/Illuminate/Foundation/Http/FormRequest.php +++ b/src/Illuminate/Foundation/Http/FormRequest.php @@ -116,7 +116,7 @@ protected function getValidatorInstance() */ protected function createDefaultValidator(ValidationFactory $factory) { - $rules = $this->container->call([$this, 'rules']); + $rules = method_exists($this, 'rules') ? $this->container->call([$this, 'rules']) : []; $validator = $factory->make( $this->validationData(), $rules, diff --git a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php index 4fb968bba4b6..ec7ebc914ba7 100644 --- a/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php +++ b/src/Illuminate/Foundation/Support/Providers/EventServiceProvider.php @@ -25,7 +25,7 @@ class EventServiceProvider extends ServiceProvider /** * The model observers to register. * - * @var array + * @var array> */ protected $observers = []; diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 9fae49f6bc2e..a7315a2c5124 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -10,6 +10,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\ParallelTesting; +use Illuminate\Support\Sleep; use Illuminate\Support\Str; use Illuminate\View\Component; use Mockery; @@ -245,6 +246,7 @@ protected function tearDown(): void Component::forgetFactory(); Queue::createPayloadUsing(null); HandleExceptions::forgetApp(); + Sleep::fake(false); if ($this->callbackException) { throw $this->callbackException; diff --git a/src/Illuminate/Http/FileHelpers.php b/src/Illuminate/Http/FileHelpers.php index 36ed55bea213..27af645aa4d2 100644 --- a/src/Illuminate/Http/FileHelpers.php +++ b/src/Illuminate/Http/FileHelpers.php @@ -53,4 +53,14 @@ public function hashName($path = null) return $path.$hash.$extension; } + + /** + * Get the dimensions of the image (if applicable). + * + * @return array|null + */ + public function dimensions() + { + return @getimagesize($this->getRealPath()); + } } diff --git a/src/Illuminate/Http/Middleware/SetCacheHeaders.php b/src/Illuminate/Http/Middleware/SetCacheHeaders.php index 0a1a156674f3..2a7a13baa135 100644 --- a/src/Illuminate/Http/Middleware/SetCacheHeaders.php +++ b/src/Illuminate/Http/Middleware/SetCacheHeaders.php @@ -4,10 +4,29 @@ use Closure; use Illuminate\Support\Carbon; +use Illuminate\Support\Str; use Symfony\Component\HttpFoundation\BinaryFileResponse; class SetCacheHeaders { + /** + * Specify the options for the middleware. + * + * @param array|string $options + * @return string + */ + public static function using($options) + { + if (is_string($options)) { + return static::class.':'.$options; + } + + return collect($options) + ->map(fn ($value, $key) => is_int($key) ? $value : "{$key}={$value}") + ->map(fn ($value) => Str::finish($value, ';')) + ->pipe(fn ($options) => rtrim(static::class.':'.$options->implode(''), ';')); + } + /** * Add cache related HTTP headers. * diff --git a/src/Illuminate/Http/Testing/FileFactory.php b/src/Illuminate/Http/Testing/FileFactory.php index 9e25d72de8f3..aa4b0296b025 100644 --- a/src/Illuminate/Http/Testing/FileFactory.php +++ b/src/Illuminate/Http/Testing/FileFactory.php @@ -2,6 +2,8 @@ namespace Illuminate\Http\Testing; +use LogicException; + class FileFactory { /** @@ -49,6 +51,8 @@ public function createWithContent($name, $content) * @param int $width * @param int $height * @return \Illuminate\Http\Testing\File + * + * @throws \LogicException */ public function image($name, $width = 10, $height = 10) { @@ -64,9 +68,15 @@ public function image($name, $width = 10, $height = 10) * @param int $height * @param string $extension * @return resource + * + * @throws \LogicException */ protected function generateImage($width, $height, $extension) { + if (! function_exists('imagecreatetruecolor')) { + throw new LogicException('GD extension is not installed.'); + } + return tap(tmpfile(), function ($temp) use ($width, $height, $extension) { ob_start(); @@ -76,7 +86,13 @@ protected function generateImage($width, $height, $extension) $image = imagecreatetruecolor($width, $height); - call_user_func("image{$extension}", $image); + if (! function_exists($functionName = "image{$extension}")) { + ob_get_clean(); + + throw new LogicException("{$functionName} function is not defined and image cannot be generated."); + } + + call_user_func($functionName, $image); fwrite($temp, ob_get_clean()); }); diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index daa5a032209c..ecf094af2c15 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -12,6 +12,7 @@ use Illuminate\Mail\Transport\SesTransport; use Illuminate\Mail\Transport\SesV2Transport; use Illuminate\Support\Arr; +use Illuminate\Support\ConfigurationUrlParser; use Illuminate\Support\Str; use InvalidArgumentException; use Psr\Log\LoggerInterface; @@ -446,9 +447,17 @@ protected function getConfig(string $name) // Here we will check if the "driver" key exists and if it does we will use // the entire mail configuration file as the "driver" config in order to // provide "BC" for any Laravel <= 6.x style mail configuration files. - return $this->app['config']['mail.driver'] + $config = $this->app['config']['mail.driver'] ? $this->app['config']['mail'] : $this->app['config']["mail.mailers.{$name}"]; + + if (isset($config['url'])) { + $config = array_merge($config, (new ConfigurationUrlParser)->parseConfiguration($config)); + + $config['transport'] = Arr::pull($config, 'driver'); + } + + return $config; } /** diff --git a/src/Illuminate/Pagination/AbstractPaginator.php b/src/Illuminate/Pagination/AbstractPaginator.php index a4979840f3b3..ed04d5f97234 100644 --- a/src/Illuminate/Pagination/AbstractPaginator.php +++ b/src/Illuminate/Pagination/AbstractPaginator.php @@ -319,7 +319,7 @@ public function items() /** * Get the number of the first item in the slice. * - * @return int + * @return int|null */ public function firstItem() { @@ -329,7 +329,7 @@ public function firstItem() /** * Get the number of the last item in the slice. * - * @return int + * @return int|null */ public function lastItem() { diff --git a/src/Illuminate/Process/Factory.php b/src/Illuminate/Process/Factory.php index 6ec62db777b0..51ed037ec9a9 100644 --- a/src/Illuminate/Process/Factory.php +++ b/src/Illuminate/Process/Factory.php @@ -275,7 +275,7 @@ public function pool(callable $callback) * Start defining a series of piped processes. * * @param callable|array $callback - * @return \Illuminate\Process\Pipe + * @return \Illuminate\Contracts\Process\ProcessResult */ public function pipe(callable|array $callback, ?callable $output = null) { diff --git a/src/Illuminate/Queue/Console/PruneBatchesCommand.php b/src/Illuminate/Queue/Console/PruneBatchesCommand.php index 993ed17ccdd6..586b4dd6bac3 100644 --- a/src/Illuminate/Queue/Console/PruneBatchesCommand.php +++ b/src/Illuminate/Queue/Console/PruneBatchesCommand.php @@ -46,7 +46,7 @@ public function handle() $this->components->info("{$count} entries deleted."); - if ($this->option('unfinished')) { + if ($this->option('unfinished') !== null) { $count = 0; if ($repository instanceof DatabaseBatchRepository) { @@ -56,7 +56,7 @@ public function handle() $this->components->info("{$count} unfinished entries deleted."); } - if ($this->option('cancelled')) { + if ($this->option('cancelled') !== null) { $count = 0; if ($repository instanceof DatabaseBatchRepository) { diff --git a/src/Illuminate/Queue/InteractsWithQueue.php b/src/Illuminate/Queue/InteractsWithQueue.php index 36a86aeacf33..20f77925857d 100644 --- a/src/Illuminate/Queue/InteractsWithQueue.php +++ b/src/Illuminate/Queue/InteractsWithQueue.php @@ -2,12 +2,16 @@ namespace Illuminate\Queue; +use DateTimeInterface; use Illuminate\Contracts\Queue\Job as JobContract; +use Illuminate\Support\InteractsWithTime; use InvalidArgumentException; use Throwable; trait InteractsWithQueue { + use InteractsWithTime; + /** * The underlying queue job instance. * @@ -61,11 +65,15 @@ public function fail($exception = null) /** * Release the job back into the queue after (n) seconds. * - * @param int $delay + * @param \DateTimeInterface|\DateInterval|int $delay * @return void */ public function release($delay = 0) { + $delay = $delay instanceof DateTimeInterface + ? $this->secondsUntil($delay) + : $delay; + if ($this->job) { return $this->job->release($delay); } diff --git a/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php b/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php index e249053e2cc9..62e50b01aad1 100644 --- a/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php +++ b/src/Illuminate/Redis/Limiters/ConcurrencyLimiter.php @@ -3,6 +3,7 @@ namespace Illuminate\Redis\Limiters; use Illuminate\Contracts\Redis\LimiterTimeoutException; +use Illuminate\Support\Sleep; use Illuminate\Support\Str; use Throwable; @@ -75,7 +76,7 @@ public function block($timeout, $callback = null, $sleep = 250) throw new LimiterTimeoutException; } - usleep($sleep * 1000); + Sleep::usleep($sleep * 1000); } if (is_callable($callback)) { diff --git a/src/Illuminate/Redis/Limiters/DurationLimiter.php b/src/Illuminate/Redis/Limiters/DurationLimiter.php index 65297ff9f65f..b0ecdaf9f4b4 100644 --- a/src/Illuminate/Redis/Limiters/DurationLimiter.php +++ b/src/Illuminate/Redis/Limiters/DurationLimiter.php @@ -3,6 +3,7 @@ namespace Illuminate\Redis\Limiters; use Illuminate\Contracts\Redis\LimiterTimeoutException; +use Illuminate\Support\Sleep; class DurationLimiter { @@ -84,7 +85,7 @@ public function block($timeout, $callback = null, $sleep = 750) throw new LimiterTimeoutException; } - usleep($sleep * 1000); + Sleep::usleep($sleep * 1000); } if (is_callable($callback)) { diff --git a/src/Illuminate/Routing/Middleware/ThrottleRequests.php b/src/Illuminate/Routing/Middleware/ThrottleRequests.php index 09675250fa2f..b9529b84e14b 100644 --- a/src/Illuminate/Routing/Middleware/ThrottleRequests.php +++ b/src/Illuminate/Routing/Middleware/ThrottleRequests.php @@ -34,6 +34,32 @@ public function __construct(RateLimiter $limiter) $this->limiter = $limiter; } + /** + * Specify the named rate limiter to use for the middleware. + * + * @param string $name + * @return string + */ + public static function using($name) + { + return static::class.':'.$name; + } + + /** + * Specify the rate limiter configuration for the middleware. + * + * @param int $maxAttempts + * @param int $decayMinutes + * @param string $prefix + * @return string + * + * @named-arguments-supported + */ + public static function with($maxAttempts = 60, $decayMinutes = 1, $prefix = '') + { + return static::class.':'.implode(',', func_get_args()); + } + /** * Handle an incoming request. * diff --git a/src/Illuminate/Routing/Middleware/ValidateSignature.php b/src/Illuminate/Routing/Middleware/ValidateSignature.php index 63841b28ce08..dec2118daa56 100644 --- a/src/Illuminate/Routing/Middleware/ValidateSignature.php +++ b/src/Illuminate/Routing/Middleware/ValidateSignature.php @@ -16,6 +16,26 @@ class ValidateSignature // ]; + /** + * Specify that the URL signature is for a relative URL. + * + * @return string + */ + public static function relative() + { + return static::class.':relative'; + } + + /** + * Specify that the URL signature is for an absolute URL. + * + * @return class-string + */ + public static function absolute() + { + return static::class; + } + /** * Handle an incoming request. * diff --git a/src/Illuminate/Routing/SortedMiddleware.php b/src/Illuminate/Routing/SortedMiddleware.php index 853378cf7e92..3c2c7912d219 100644 --- a/src/Illuminate/Routing/SortedMiddleware.php +++ b/src/Illuminate/Routing/SortedMiddleware.php @@ -101,6 +101,14 @@ protected function middlewareNames($middleware) yield $interface; } } + + $parents = @class_parents($stripped); + + if ($parents !== false) { + foreach ($parents as $parent) { + yield $parent; + } + } } /** diff --git a/src/Illuminate/Support/DefaultProviders.php b/src/Illuminate/Support/DefaultProviders.php new file mode 100644 index 000000000000..441a083fed5c --- /dev/null +++ b/src/Illuminate/Support/DefaultProviders.php @@ -0,0 +1,102 @@ +providers = $providers ?: [ + \Illuminate\Auth\AuthServiceProvider::class, + \Illuminate\Broadcasting\BroadcastServiceProvider::class, + \Illuminate\Bus\BusServiceProvider::class, + \Illuminate\Cache\CacheServiceProvider::class, + \Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + \Illuminate\Cookie\CookieServiceProvider::class, + \Illuminate\Database\DatabaseServiceProvider::class, + \Illuminate\Encryption\EncryptionServiceProvider::class, + \Illuminate\Filesystem\FilesystemServiceProvider::class, + \Illuminate\Foundation\Providers\FoundationServiceProvider::class, + \Illuminate\Hashing\HashServiceProvider::class, + \Illuminate\Mail\MailServiceProvider::class, + \Illuminate\Notifications\NotificationServiceProvider::class, + \Illuminate\Pagination\PaginationServiceProvider::class, + \Illuminate\Pipeline\PipelineServiceProvider::class, + \Illuminate\Queue\QueueServiceProvider::class, + \Illuminate\Redis\RedisServiceProvider::class, + \Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + \Illuminate\Session\SessionServiceProvider::class, + \Illuminate\Translation\TranslationServiceProvider::class, + \Illuminate\Validation\ValidationServiceProvider::class, + \Illuminate\View\ViewServiceProvider::class, + ]; + } + + /** + * Merge the given providers into the provider collection. + * + * @param array $providers + * @return static + */ + public function merge(array $providers) + { + $this->providers = array_merge($this->providers, $providers); + + return new static($this->providers); + } + + /** + * Replace the given providers with other providers. + * + * @param array $items + * @return static + */ + public function replace(array $replacements) + { + $current = collect($this->providers); + + foreach ($replacements as $from => $to) { + $key = $current->search($from); + + $current = $key ? $current->replace([$key => $to]) : $current; + } + + return new static($current->values()->toArray()); + } + + /** + * Disable the given providers. + * + * @param array $providers + * @return static + */ + public function except(array $providers) + { + return new static(collect($this->providers) + ->reject(fn ($p) => in_array($p, $providers)) + ->values() + ->toArray()); + } + + /** + * Convert the provider collection to an array. + * + * @return array + */ + public function toArray() + { + return $this->providers; + } +} diff --git a/src/Illuminate/Support/Facades/DB.php b/src/Illuminate/Support/Facades/DB.php index 5053897798cd..1faf788182f5 100755 --- a/src/Illuminate/Support/Facades/DB.php +++ b/src/Illuminate/Support/Facades/DB.php @@ -27,7 +27,7 @@ * @method static void useDefaultSchemaGrammar() * @method static void useDefaultPostProcessor() * @method static \Illuminate\Database\Schema\Builder getSchemaBuilder() - * @method static \Illuminate\Database\Query\Builder table(\Closure|\Illuminate\Database\Query\Builder|string $table, string|null $as = null) + * @method static \Illuminate\Database\Query\Builder table(\Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|string $table, string|null $as = null) * @method static \Illuminate\Database\Query\Builder query() * @method static mixed selectOne(string $query, array $bindings = [], bool $useReadPdo = true) * @method static mixed scalar(string $query, array $bindings = [], bool $useReadPdo = true) diff --git a/src/Illuminate/Support/Facades/Process.php b/src/Illuminate/Support/Facades/Process.php index dbe03cf8746e..1df17ba7e7a9 100644 --- a/src/Illuminate/Support/Facades/Process.php +++ b/src/Illuminate/Support/Facades/Process.php @@ -35,7 +35,7 @@ * @method static \Illuminate\Process\Factory assertDidntRun(\Closure|string $callback) * @method static \Illuminate\Process\Factory assertNothingRan() * @method static \Illuminate\Process\Pool pool(callable $callback) - * @method static \Illuminate\Process\Pipe pipe(callable|array $callback, callable|null $output = null) + * @method static \Illuminate\Contracts\Process\ProcessResult pipe(callable|array $callback, callable|null $output = null) * @method static \Illuminate\Process\ProcessPoolResults concurrently(callable $callback, callable|null $output = null) * @method static \Illuminate\Process\PendingProcess newPendingProcess() * @method static void macro(string $name, object|callable $macro) diff --git a/src/Illuminate/Support/Js.php b/src/Illuminate/Support/Js.php index 69e9a7626122..c924d1c83262 100644 --- a/src/Illuminate/Support/Js.php +++ b/src/Illuminate/Support/Js.php @@ -74,7 +74,7 @@ protected function convertDataToJavaScriptExpression($data, $flags = 0, $depth = $data = $data->value; } - $json = $this->jsonEncode($data, $flags, $depth); + $json = static::encode($data, $flags, $depth); if (is_string($data)) { return "'".substr($json, 1, -1)."'"; @@ -93,7 +93,7 @@ protected function convertDataToJavaScriptExpression($data, $flags = 0, $depth = * * @throws \JsonException */ - protected function jsonEncode($data, $flags = 0, $depth = 512) + public static function encode($data, $flags = 0, $depth = 512) { if ($data instanceof Jsonable) { return $data->toJson($flags | static::REQUIRED_FLAGS); diff --git a/src/Illuminate/Support/ServiceProvider.php b/src/Illuminate/Support/ServiceProvider.php index 6c530c121d3c..a46ba29690aa 100755 --- a/src/Illuminate/Support/ServiceProvider.php +++ b/src/Illuminate/Support/ServiceProvider.php @@ -434,4 +434,14 @@ public function isDeferred() { return $this instanceof DeferrableProvider; } + + /** + * Get the default providers for a Laravel application. + * + * @return \Illuminate\Support\DefaultProviders + */ + public static function defaultProviders() + { + return new DefaultProviders; + } } diff --git a/src/Illuminate/Support/Sleep.php b/src/Illuminate/Support/Sleep.php new file mode 100644 index 000000000000..041535b9b42f --- /dev/null +++ b/src/Illuminate/Support/Sleep.php @@ -0,0 +1,397 @@ + + */ + protected static $sequence = []; + + /** + * Indicates if the instance should sleep. + * + * @var bool + */ + protected $shouldSleep = true; + + /** + * Create a new class instance. + * + * @param int|float|\DateInterval $duration + * @return void + */ + public function __construct($duration) + { + if (! $duration instanceof DateInterval) { + $this->duration = CarbonInterval::microsecond(0); + + $this->pending = $duration; + } else { + $duration = CarbonInterval::instance($duration); + + if ($duration->totalMicroseconds < 0) { + $duration = CarbonInterval::seconds(0); + } + + $this->duration = $duration; + } + } + + /** + * Sleep for the given duration. + * + * @param \DateInterval|int|float $duration + * @return static + */ + public static function for($duration) + { + return new static($duration); + } + + /** + * Sleep until the given timestamp. + * + * @param \DateTimeInterface|int $timestamp + * @return static + */ + public static function until($timestamp) + { + if (is_int($timestamp)) { + $timestamp = Carbon::createFromTimestamp($timestamp); + } + + return new static(Carbon::now()->diff($timestamp)); + } + + /** + * Sleep for the given number of microseconds. + * + * @param int $duration + * @return static + */ + public static function usleep($duration) + { + return (new static($duration))->microseconds(); + } + + /** + * Sleep for the given number of seconds. + * + * @param int|float $duration + * @return static + */ + public static function sleep($duration) + { + return (new static($duration))->seconds(); + } + + /** + * Sleep for the given number of minutes. + * + * @return $this + */ + public function minutes() + { + $this->duration->add('minutes', $this->pullPending()); + + return $this; + } + + /** + * Sleep for one minute. + * + * @return $this + */ + public function minute() + { + return $this->minutes(); + } + + /** + * Sleep for the given number of seconds. + * + * @return $this + */ + public function seconds() + { + $this->duration->add('seconds', $this->pullPending()); + + return $this; + } + + /** + * Sleep for one second. + * + * @return $this + */ + public function second() + { + return $this->seconds(); + } + + /** + * Sleep for the given number of milliseconds. + * + * @return $this + */ + public function milliseconds() + { + $this->duration->add('milliseconds', $this->pullPending()); + + return $this; + } + + /** + * Sleep for one millisecond. + * + * @return $this + */ + public function millisecond() + { + return $this->milliseconds(); + } + + /** + * Sleep for the given number of microseconds. + * + * @return $this + */ + public function microseconds() + { + $this->duration->add('microseconds', $this->pullPending()); + + return $this; + } + + /** + * Sleep for on microsecond. + * + * @return $this + */ + public function microsecond() + { + return $this->microseconds(); + } + + /** + * Add additional time to sleep for. + * + * @param int|float $duration + * @return $this + */ + public function and($duration) + { + $this->pending = $duration; + + return $this; + } + + /** + * Handle the object's destruction. + * + * @return void + */ + public function __destruct() + { + if (! $this->shouldSleep) { + return; + } + + if ($this->pending !== null) { + throw new RuntimeException('Unknown duration unit.'); + } + + if (static::$fake) { + static::$sequence[] = $this->duration; + + return; + } + + $remaining = $this->duration->copy(); + + $seconds = (int) $remaining->totalSeconds; + + if ($seconds > 0) { + sleep($seconds); + + $remaining = $remaining->subSeconds($seconds); + } + + $microseconds = (int) $remaining->totalMicroseconds; + + if ($microseconds > 0) { + usleep($microseconds); + } + } + + /** + * Resolve the pending duration. + * + * @return int|float + */ + protected function pullPending() + { + if ($this->pending === null) { + $this->shouldNotSleep(); + + throw new RuntimeException('No duration specified.'); + } + + if ($this->pending < 0) { + $this->pending = 0; + } + + return tap($this->pending, function () { + $this->pending = null; + }); + } + + /** + * Stay awake and capture any attempts to sleep. + * + * @param bool $value + * @return void + */ + public static function fake($value = true) + { + static::$fake = $value; + + static::$sequence = []; + } + + /** + * Assert a given amount of sleeping occurred a specific number of times. + * + * @param \Closure $expected + * @param int $times + * @return void + */ + public static function assertSlept($expected, $times = 1) + { + $count = collect(static::$sequence)->filter($expected)->count(); + + PHPUnit::assertSame( + $times, + $count, + "The expected sleep was found [{$count}] times instead of [{$times}]." + ); + } + + /** + * Assert sleeping occurred a given number of times. + * + * @param int $expected + * @return void + */ + public static function assertSleptTimes($expected) + { + PHPUnit::assertSame($expected, $count = count(static::$sequence), "Expected [{$expected}] sleeps but found [{$count}]."); + } + + /** + * Assert the given sleep sequence was encountered. + * + * @param array $sequence + * @return void + */ + public static function assertSequence($sequence) + { + static::assertSleptTimes(count($sequence)); + + collect($sequence) + ->zip(static::$sequence) + ->eachSpread(function (?Sleep $expected, CarbonInterval $actual) { + if ($expected === null) { + return; + } + + PHPUnit::assertTrue( + $expected->shouldNotSleep()->duration->equalTo($actual), + vsprintf('Expected sleep duration of [%s] but actually slept for [%s].', [ + $expected->duration->cascade()->forHumans([ + 'options' => 0, + 'minimumUnit' => 'microsecond', + ]), + $actual->cascade()->forHumans([ + 'options' => 0, + 'minimumUnit' => 'microsecond', + ]), + ]) + ); + }); + } + + /** + * Assert that no sleeping occurred. + * + * @return void + */ + public static function assertNeverSlept() + { + return static::assertSleptTimes(0); + } + + /** + * Assert that no sleeping occurred. + * + * @return void + */ + public static function assertInsomniac() + { + if (static::$sequence === []) { + PHPUnit::assertTrue(true); + } + + foreach (static::$sequence as $duration) { + PHPUnit::assertSame(0, $duration->totalMicroseconds, vsprintf('Unexpected sleep duration of [%s] found.', [ + $duration->cascade()->forHumans([ + 'options' => 0, + 'minimumUnit' => 'microsecond', + ]), + ])); + } + } + + /** + * Indicate that the instance should not sleep. + * + * @return $this + */ + protected function shouldNotSleep() + { + $this->shouldSleep = false; + + return $this; + } +} diff --git a/src/Illuminate/Support/Timebox.php b/src/Illuminate/Support/Timebox.php index 0d41e0ac2d05..cb99a4a8492d 100644 --- a/src/Illuminate/Support/Timebox.php +++ b/src/Illuminate/Support/Timebox.php @@ -79,6 +79,6 @@ public function dontReturnEarly() */ protected function usleep(int $microseconds) { - usleep($microseconds); + Sleep::usleep($microseconds); } } diff --git a/src/Illuminate/Support/helpers.php b/src/Illuminate/Support/helpers.php index 5b3847f4a54f..5f4d0d0aaf63 100755 --- a/src/Illuminate/Support/helpers.php +++ b/src/Illuminate/Support/helpers.php @@ -6,6 +6,7 @@ use Illuminate\Support\Env; use Illuminate\Support\HigherOrderTapProxy; use Illuminate\Support\Optional; +use Illuminate\Support\Sleep; use Illuminate\Support\Str; if (! function_exists('append_config')) { @@ -120,7 +121,7 @@ function e($value, $doubleEncode = true) $value = $value->value; } - return htmlspecialchars($value ?? '', ENT_QUOTES, 'UTF-8', $doubleEncode); + return htmlspecialchars($value ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8', $doubleEncode); } } @@ -253,7 +254,7 @@ function retry($times, callable $callback, $sleepMilliseconds = 0, $when = null) $sleepMilliseconds = $backoff[$attempts - 1] ?? $sleepMilliseconds; if ($sleepMilliseconds) { - usleep(value($sleepMilliseconds, $attempts, $e) * 1000); + Sleep::usleep(value($sleepMilliseconds, $attempts, $e) * 1000); } goto beginning; diff --git a/src/Illuminate/Testing/Concerns/AssertsStatusCodes.php b/src/Illuminate/Testing/Concerns/AssertsStatusCodes.php index d34b43f5cac7..22b91d6e2420 100644 --- a/src/Illuminate/Testing/Concerns/AssertsStatusCodes.php +++ b/src/Illuminate/Testing/Concerns/AssertsStatusCodes.php @@ -141,6 +141,16 @@ public function assertConflict() return $this->assertStatus(409); } + /** + * Assert that the response has a 410 "Gone" status code. + * + * @return $this + */ + public function assertGone() + { + return $this->assertStatus(410); + } + /** * Assert that the response has a 415 "Unsupported Media Type" status code. * @@ -170,4 +180,24 @@ public function assertTooManyRequests() { return $this->assertStatus(429); } + + /** + * Assert that the response has a 500 "Internal Server Error" status code. + * + * @return $this + */ + public function assertInternalServerError() + { + return $this->assertStatus(500); + } + + /** + * Assert that the response has a 503 "Service Unavailable" status code. + * + * @return $this + */ + public function assertServiceUnavailable() + { + return $this->assertStatus(503); + } } diff --git a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php index cb37e462980b..660818ed0d08 100644 --- a/src/Illuminate/Validation/Concerns/ReplacesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ReplacesAttributes.php @@ -217,7 +217,10 @@ protected function replaceMissingIf($message, $attribute, $rule, $parameters) */ protected function replaceMissingUnless($message, $attribute, $rule, $parameters) { - return $this->replaceMissingIf($message, $attribute, $rule, $parameters); + return str_replace([':other', ':value'], [ + $this->getDisplayableAttribute($parameters[0]), + $this->getDisplayableValue($parameters[0], $parameters[1]), + ], $message); } /** diff --git a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php index 3077bf17641c..bbd910ae861a 100644 --- a/src/Illuminate/Validation/Concerns/ValidatesAttributes.php +++ b/src/Illuminate/Validation/Concerns/ValidatesAttributes.php @@ -25,6 +25,7 @@ use InvalidArgumentException; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\File\UploadedFile; +use ValueError; trait ValidatesAttributes { @@ -550,10 +551,14 @@ public function validateDateFormat($attribute, $value, $parameters) } foreach ($parameters as $format) { - $date = DateTime::createFromFormat('!'.$format, $value); + try { + $date = DateTime::createFromFormat('!'.$format, $value); - if ($date && $date->format($format) == $value) { - return true; + if ($date && $date->format($format) == $value) { + return true; + } + } catch (ValueError) { + return false; } } @@ -585,12 +590,12 @@ public function validateDateEquals($attribute, $value, $parameters) */ public function validateDecimal($attribute, $value, $parameters) { + $this->requireParameterCount(1, $parameters, 'decimal'); + if (! $this->validateNumeric($attribute, $value)) { return false; } - $this->requireParameterCount(1, $parameters, 'decimal'); - $matches = []; preg_match('/^[+-]?\d*.(\d*)$/', $value, $matches); @@ -678,13 +683,21 @@ public function validateDimensions($attribute, $value, $parameters) return true; } - if (! $this->isValidFileInstance($value) || ! $sizeDetails = @getimagesize($value->getRealPath())) { + if (! $this->isValidFileInstance($value)) { + return false; + } + + $dimensions = method_exists($value, 'dimensions') + ? $value->dimensions() + : @getimagesize($value->getRealPath()); + + if (! $dimensions) { return false; } $this->requireParameterCount(1, $parameters, 'dimensions'); - [$width, $height] = $sizeDetails; + [$width, $height] = $dimensions; $parameters = $this->parseNamedParameters($parameters); diff --git a/tests/Auth/AuthenticateMiddlewareTest.php b/tests/Auth/AuthenticateMiddlewareTest.php index 837187ec8416..bb9caa101e6b 100644 --- a/tests/Auth/AuthenticateMiddlewareTest.php +++ b/tests/Auth/AuthenticateMiddlewareTest.php @@ -6,6 +6,7 @@ use Illuminate\Auth\AuthManager; use Illuminate\Auth\EloquentUserProvider; use Illuminate\Auth\Middleware\Authenticate; +use Illuminate\Auth\Middleware\AuthenticateWithBasicAuth; use Illuminate\Auth\RequestGuard; use Illuminate\Config\Repository as Config; use Illuminate\Container\Container; @@ -36,6 +37,30 @@ protected function tearDown(): void Container::setInstance(null); } + public function testItCanGenerateDefinitionViaStaticMethod() + { + $signature = (string) Authenticate::using('foo'); + $this->assertSame('Illuminate\Auth\Middleware\Authenticate:foo', $signature); + + $signature = (string) Authenticate::using('foo', 'bar'); + $this->assertSame('Illuminate\Auth\Middleware\Authenticate:foo,bar', $signature); + + $signature = (string) Authenticate::using('foo', 'bar', 'baz'); + $this->assertSame('Illuminate\Auth\Middleware\Authenticate:foo,bar,baz', $signature); + } + + public function testItCanGenerateDefinitionViaStaticMethodForBasic() + { + $signature = (string) AuthenticateWithBasicAuth::using('guard'); + $this->assertSame('Illuminate\Auth\Middleware\AuthenticateWithBasicAuth:guard', $signature); + + $signature = (string) AuthenticateWithBasicAuth::using('guard', 'field'); + $this->assertSame('Illuminate\Auth\Middleware\AuthenticateWithBasicAuth:guard,field', $signature); + + $signature = (string) AuthenticateWithBasicAuth::using(field: 'field'); + $this->assertSame('Illuminate\Auth\Middleware\AuthenticateWithBasicAuth:,field', $signature); + } + public function testDefaultUnauthenticatedThrows() { $this->expectException(AuthenticationException::class); diff --git a/tests/Auth/AuthorizeMiddlewareTest.php b/tests/Auth/AuthorizeMiddlewareTest.php index 076c1277df14..3b221dd97239 100644 --- a/tests/Auth/AuthorizeMiddlewareTest.php +++ b/tests/Auth/AuthorizeMiddlewareTest.php @@ -55,6 +55,18 @@ protected function tearDown(): void Container::setInstance(null); } + public function testItCanGenerateDefinitionViaStaticMethod() + { + $signature = (string) Authorize::using('ability'); + $this->assertSame('Illuminate\Auth\Middleware\Authorize:ability', $signature); + + $signature = (string) Authorize::using('ability', 'model'); + $this->assertSame('Illuminate\Auth\Middleware\Authorize:ability,model', $signature); + + $signature = (string) Authorize::using('ability', 'model', \App\Models\Comment::class); + $this->assertSame('Illuminate\Auth\Middleware\Authorize:ability,model,App\Models\Comment', $signature); + } + public function testSimpleAbilityUnauthorized() { $this->expectException(AuthorizationException::class); diff --git a/tests/Auth/EnsureEmailIsVerifiedTest.php b/tests/Auth/EnsureEmailIsVerifiedTest.php new file mode 100644 index 000000000000..e517257f9ea1 --- /dev/null +++ b/tests/Auth/EnsureEmailIsVerifiedTest.php @@ -0,0 +1,15 @@ +assertSame('Illuminate\Auth\Middleware\EnsureEmailIsVerified:route.name', $signature); + } +} diff --git a/tests/Database/DatabaseEloquentBuilderTest.php b/tests/Database/DatabaseEloquentBuilderTest.php index 2d73e8a7dba6..0584af163d82 100755 --- a/tests/Database/DatabaseEloquentBuilderTest.php +++ b/tests/Database/DatabaseEloquentBuilderTest.php @@ -1660,6 +1660,15 @@ public function testWhereMorphedTo() $this->assertEquals([$relatedModel->getMorphClass(), $relatedModel->getKey()], $builder->getBindings()); } + public function testWhereMorphedToNull() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $builder = $model->whereMorphedTo('morph', null); + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "morph_type" is null', $builder->toSql()); + } + public function testWhereNotMorphedTo() { $model = new EloquentBuilderTestModelParentStub; @@ -1688,6 +1697,17 @@ public function testOrWhereMorphedTo() $this->assertEquals(['baz', $relatedModel->getMorphClass(), $relatedModel->getKey()], $builder->getBindings()); } + public function testOrWhereMorphedToNull() + { + $model = new EloquentBuilderTestModelParentStub; + $this->mockConnectionForModel($model, ''); + + $builder = $model->where('bar', 'baz')->orWhereMorphedTo('morph', null); + + $this->assertSame('select * from "eloquent_builder_test_model_parent_stubs" where "bar" = ? or "morph_type" is null', $builder->toSql()); + $this->assertEquals(['baz'], $builder->getBindings()); + } + public function testOrWhereNotMorphedTo() { $model = new EloquentBuilderTestModelParentStub; diff --git a/tests/Database/DatabaseEloquentIntegrationTest.php b/tests/Database/DatabaseEloquentIntegrationTest.php index 0fe2e76f4ccf..a7225b2bb1ed 100644 --- a/tests/Database/DatabaseEloquentIntegrationTest.php +++ b/tests/Database/DatabaseEloquentIntegrationTest.php @@ -2345,6 +2345,7 @@ class EloquentTestFriendPivot extends Pivot { protected $table = 'friends'; protected $guarded = []; + public $timestamps = false; public function user() { diff --git a/tests/Database/DatabaseSchemaBlueprintTest.php b/tests/Database/DatabaseSchemaBlueprintTest.php index 45a72191faa6..884434ddc642 100755 --- a/tests/Database/DatabaseSchemaBlueprintTest.php +++ b/tests/Database/DatabaseSchemaBlueprintTest.php @@ -372,6 +372,29 @@ public function testGenerateRelationshipColumnWithUuidModel() ], $blueprint->toSql($connection, new MySqlGrammar)); } + public function testGenerateRelationshipColumnWithUlidModel() + { + require_once __DIR__.'/stubs/EloquentModelUlidStub.php'; + + $base = new Blueprint('posts', function (Blueprint $table) { + $table->foreignIdFor('EloquentModelUlidStub'); + }); + + $connection = m::mock(Connection::class); + + $blueprint = clone $base; + + $this->assertEquals([ + 'alter table "posts" add column "eloquent_model_ulid_stub_id" char(26) not null', + ], $blueprint->toSql($connection, new PostgresGrammar)); + + $blueprint = clone $base; + + $this->assertEquals([ + 'alter table `posts` add `eloquent_model_ulid_stub_id` char(26) not null', + ], $blueprint->toSql($connection, new MySqlGrammar())); + } + public function testDropRelationshipColumnWithIncrementalModel() { $base = new Blueprint('posts', function ($table) { diff --git a/tests/Database/stubs/EloquentModelUlidStub.php b/tests/Database/stubs/EloquentModelUlidStub.php new file mode 100644 index 000000000000..f8f54a67d935 --- /dev/null +++ b/tests/Database/stubs/EloquentModelUlidStub.php @@ -0,0 +1,15 @@ +createRequest([], FoundationTestFormRequestWithoutRulesMethod::class); + + $request->validateResolved(); + + $this->assertEquals([], $request->all()); + } + /** * Catch the given exception thrown from the executor, and return it. * @@ -465,3 +474,11 @@ public function __construct(public $value) // } } + +class FoundationTestFormRequestWithoutRulesMethod extends FormRequest +{ + public function authorize() + { + return true; + } +} diff --git a/tests/Http/HttpTestingFileFactoryTest.php b/tests/Http/HttpTestingFileFactoryTest.php index 0e6eb7438053..a3c9a561e1ba 100644 --- a/tests/Http/HttpTestingFileFactoryTest.php +++ b/tests/Http/HttpTestingFileFactoryTest.php @@ -116,6 +116,29 @@ public function testCreateWithoutMimeType() ); } + /** @dataProvider generateImageDataProvider */ + public function testCallingCreateWithoutGDLoadedThrowsAnException(string $fileExtension, string $driver) + { + if ($this->isGDSupported($driver)) { + $this->markTestSkipped("Requires no {$driver}"); + } + + $this->expectException(\LogicException::class); + (new FileFactory)->image("test.{$fileExtension}"); + } + + public static function generateImageDataProvider(): array + { + return [ + 'jpeg' => ['jpeg', 'JPEG Support'], + 'png' => ['png', 'PNG Support'], + 'gif' => ['gif', 'GIF Create Support'], + 'webp' => ['webp', 'WebP Support'], + 'wbmp' => ['wbmp', 'WBMP Support'], + 'bmp' => ['bmp', 'BMP Support'], + ]; + } + /** * @param string $driver * @return bool diff --git a/tests/Http/Middleware/CacheTest.php b/tests/Http/Middleware/CacheTest.php index d11e3b1a33f3..d1f31a74c0a1 100644 --- a/tests/Http/Middleware/CacheTest.php +++ b/tests/Http/Middleware/CacheTest.php @@ -12,6 +12,29 @@ class CacheTest extends TestCase { + public function testItCanGenerateDefinitionViaStaticMethod() + { + $signature = (string) Cache::using('max_age=120;no-transform;s_maxage=60;'); + $this->assertSame('Illuminate\Http\Middleware\SetCacheHeaders:max_age=120;no-transform;s_maxage=60;', $signature); + + $signature = (string) Cache::using('max_age=120;no-transform;s_maxage=60'); + $this->assertSame('Illuminate\Http\Middleware\SetCacheHeaders:max_age=120;no-transform;s_maxage=60', $signature); + + $signature = (string) Cache::using([ + 'max_age=120', + 'no-transform', + 's_maxage=60', + ]); + $this->assertSame('Illuminate\Http\Middleware\SetCacheHeaders:max_age=120;no-transform;s_maxage=60', $signature); + + $signature = (string) Cache::using([ + 'max_age' => 120, + 'no-transform', + 's_maxage' => '60', + ]); + $this->assertSame('Illuminate\Http\Middleware\SetCacheHeaders:max_age=120;no-transform;s_maxage=60', $signature); + } + public function testDoNotSetHeaderWhenMethodNotCacheable() { $request = new Request; diff --git a/tests/Integration/Auth/Middleware/RequirePasswordTest.php b/tests/Integration/Auth/Middleware/RequirePasswordTest.php index 9b63ccbb6f7d..0247a63234c7 100644 --- a/tests/Integration/Auth/Middleware/RequirePasswordTest.php +++ b/tests/Integration/Auth/Middleware/RequirePasswordTest.php @@ -12,6 +12,18 @@ class RequirePasswordTest extends TestCase { + public function testItCanGenerateDefinitionViaStaticMethod() + { + $signature = (string) RequirePassword::using('route.name'); + $this->assertSame('Illuminate\Auth\Middleware\RequirePassword:route.name', $signature); + + $signature = (string) RequirePassword::using('route.name', 100); + $this->assertSame('Illuminate\Auth\Middleware\RequirePassword:route.name,100', $signature); + + $signature = (string) RequirePassword::using(passwordTimeoutSeconds: 100); + $this->assertSame('Illuminate\Auth\Middleware\RequirePassword:,100', $signature); + } + public function testUserSeesTheWantedPageIfThePasswordWasRecentlyConfirmed() { $this->withoutExceptionHandling(); diff --git a/tests/Integration/Database/EloquentCollectionFreshTest.php b/tests/Integration/Database/EloquentCollectionFreshTest.php index e1c59f7cafe5..30acc2597112 100644 --- a/tests/Integration/Database/EloquentCollectionFreshTest.php +++ b/tests/Integration/Database/EloquentCollectionFreshTest.php @@ -14,6 +14,7 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('email'); + $table->timestamps(); }); } diff --git a/tests/Integration/Database/EloquentCustomPivotCastTest.php b/tests/Integration/Database/EloquentCustomPivotCastTest.php index 7a8cdce8c42c..1a116d37eb9d 100644 --- a/tests/Integration/Database/EloquentCustomPivotCastTest.php +++ b/tests/Integration/Database/EloquentCustomPivotCastTest.php @@ -171,6 +171,8 @@ public function collaborators() class CustomPivotCastTestCollaborator extends Pivot { + public $timestamps = false; + protected $attributes = [ 'permissions' => '["create", "update"]', ]; diff --git a/tests/Integration/Database/EloquentModelHashedCastingTest.php b/tests/Integration/Database/EloquentModelHashedCastingTest.php new file mode 100644 index 000000000000..85b700c07f59 --- /dev/null +++ b/tests/Integration/Database/EloquentModelHashedCastingTest.php @@ -0,0 +1,91 @@ +hasher = $this->mock(Hasher::class); + Hash::swap($this->hasher); + } + + protected function defineDatabaseMigrationsAfterDatabaseRefreshed() + { + Schema::create('hashed_casts', function (Blueprint $table) { + $table->increments('id'); + $table->string('password')->nullable(); + }); + } + + public function testHashed() + { + $this->hasher->expects('needsRehash') + ->with('this is a password') + ->andReturnTrue(); + + $this->hasher->expects('make') + ->with('this is a password') + ->andReturn('hashed-password'); + + $subject = HashedCast::create([ + 'password' => 'this is a password', + ]); + + $this->assertSame('hashed-password', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => 'hashed-password', + ]); + } + + public function testNotHashedIfAlreadyHashed() + { + $this->hasher->expects('needsRehash') + ->with('already-hashed-password') + ->andReturnFalse(); + + $subject = HashedCast::create([ + 'password' => 'already-hashed-password', + ]); + + $this->assertSame('already-hashed-password', $subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => 'already-hashed-password', + ]); + } + + public function testNotHashedIfNull() + { + $subject = HashedCast::create([ + 'password' => null, + ]); + + $this->assertNull($subject->password); + $this->assertDatabaseHas('hashed_casts', [ + 'id' => $subject->id, + 'password' => null, + ]); + } +} + +class HashedCast extends Model +{ + public $timestamps = false; + protected $guarded = []; + + public $casts = [ + 'password' => 'hashed', + ]; +} diff --git a/tests/Integration/Database/EloquentPivotEventsTest.php b/tests/Integration/Database/EloquentPivotEventsTest.php index ea72a019d387..36783f38d9b3 100644 --- a/tests/Integration/Database/EloquentPivotEventsTest.php +++ b/tests/Integration/Database/EloquentPivotEventsTest.php @@ -150,6 +150,8 @@ class PivotEventsTestCollaborator extends Pivot { public $table = 'project_users'; + public $timestamps = false; + protected $casts = [ 'permissions' => 'json', ]; diff --git a/tests/Integration/Database/EloquentPivotSerializationTest.php b/tests/Integration/Database/EloquentPivotSerializationTest.php index b3cbeaf0f031..00899762b7a0 100644 --- a/tests/Integration/Database/EloquentPivotSerializationTest.php +++ b/tests/Integration/Database/EloquentPivotSerializationTest.php @@ -182,9 +182,13 @@ public function projects() class PivotSerializationTestCollaborator extends Pivot { public $table = 'project_users'; + + public $timestamps = false; } class PivotSerializationTestTagAttachment extends MorphPivot { public $table = 'taggables'; + + public $timestamps = false; } diff --git a/tests/Integration/Database/EloquentPivotTest.php b/tests/Integration/Database/EloquentPivotTest.php index e673b459000d..72bd8a687fdb 100644 --- a/tests/Integration/Database/EloquentPivotTest.php +++ b/tests/Integration/Database/EloquentPivotTest.php @@ -129,6 +129,8 @@ class PivotTestCollaborator extends Pivot { public $table = 'collaborators'; + public $timestamps = false; + protected $casts = [ 'permissions' => 'json', ]; @@ -138,6 +140,8 @@ class PivotTestContributor extends Pivot { public $table = 'contributors'; + public $timestamps = false; + public $incrementing = true; protected $casts = [ @@ -149,6 +153,8 @@ class PivotTestSubscription extends Pivot { public $table = 'subscriptions'; + public $timestamps = false; + protected $attributes = [ 'status' => 'active', ]; diff --git a/tests/Integration/Http/ThrottleRequestsTest.php b/tests/Integration/Http/ThrottleRequestsTest.php index a23014bd03f0..6ddd2a0ef51e 100644 --- a/tests/Integration/Http/ThrottleRequestsTest.php +++ b/tests/Integration/Http/ThrottleRequestsTest.php @@ -95,4 +95,25 @@ public function testLimitingUsingNamedLimiter() $this->assertEquals(Carbon::now()->addSeconds(2)->getTimestamp(), $e->getHeaders()['X-RateLimit-Reset']); } } + + public function testItCanGenerateDefinitionViaStaticMethod() + { + $signature = (string) ThrottleRequests::using('gold-tier'); + $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:gold-tier', $signature); + + $signature = (string) ThrottleRequests::with(25); + $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:25', $signature); + + $signature = (string) ThrottleRequests::with(25, 2); + $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:25,2', $signature); + + $signature = (string) ThrottleRequests::with(25, 2, 'foo'); + $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:25,2,foo', $signature); + + $signature = (string) ThrottleRequests::with(maxAttempts: 25, decayMinutes: 2, prefix: 'foo'); + $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:25,2,foo', $signature); + + $signature = (string) ThrottleRequests::with(prefix: 'foo'); + $this->assertSame('Illuminate\Routing\Middleware\ThrottleRequests:60,1,foo', $signature); + } } diff --git a/tests/Integration/Queue/JobDispatchingTest.php b/tests/Integration/Queue/JobDispatchingTest.php index 148246208def..f7fe354180a8 100644 --- a/tests/Integration/Queue/JobDispatchingTest.php +++ b/tests/Integration/Queue/JobDispatchingTest.php @@ -3,8 +3,11 @@ namespace Illuminate\Tests\Integration\Queue; use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Cache\Repository; +use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; use Orchestra\Testbench\TestCase; class JobDispatchingTest extends TestCase @@ -70,6 +73,52 @@ public function testDoesNotDispatchConditionallyWithClosure() $this->assertTrue(Job::$ran); } + + public function testUniqueJobLockIsReleasedForJobDispatchedAfterResponse() + { + // get initial terminatingCallbacks + $terminatingCallbacksReflectionProperty = (new \ReflectionObject($this->app))->getProperty('terminatingCallbacks'); + $terminatingCallbacksReflectionProperty->setAccessible(true); + $startTerminatingCallbacks = $terminatingCallbacksReflectionProperty->getValue($this->app); + + UniqueJob::dispatchAfterResponse('test'); + $this->assertFalse( + $this->getJobLock(UniqueJob::class, 'test') + ); + + $this->app->terminate(); + $this->assertTrue(UniqueJob::$ran); + + $terminatingCallbacksReflectionProperty->setValue($this->app, $startTerminatingCallbacks); + + UniqueJob::$ran = false; + UniqueJob::dispatch('test')->afterResponse(); + $this->app->terminate(); + $this->assertTrue(UniqueJob::$ran); + + // acquire job lock and confirm that job is not dispatched after response + $this->assertTrue( + $this->getJobLock(UniqueJob::class, 'test') + ); + $terminatingCallbacksReflectionProperty->setValue($this->app, $startTerminatingCallbacks); + UniqueJob::$ran = false; + UniqueJob::dispatch('test')->afterResponse(); + $this->app->terminate(); + $this->assertFalse(UniqueJob::$ran); + + // confirm that dispatchAfterResponse also does not run + UniqueJob::dispatchAfterResponse('test'); + $this->app->terminate(); + $this->assertFalse(UniqueJob::$ran); + } + + /** + * Helpers. + */ + private function getJobLock($job, $value = null) + { + return $this->app->get(Repository::class)->lock('laravel_unique_job:'.$job.$value, 10)->get(); + } } class Job implements ShouldQueue @@ -96,3 +145,13 @@ public function replaceValue($value) static::$value = $value; } } + +class UniqueJob extends Job implements ShouldBeUnique +{ + use InteractsWithQueue; + + public function uniqueId() + { + return self::$value; + } +} diff --git a/tests/Integration/Routing/UrlSigningTest.php b/tests/Integration/Routing/UrlSigningTest.php index 326004698e4c..39a86661335f 100644 --- a/tests/Integration/Routing/UrlSigningTest.php +++ b/tests/Integration/Routing/UrlSigningTest.php @@ -276,6 +276,15 @@ public function testSignedMiddlewareIgnoringParameter() } } + public function testItCanGenerateMiddlewareDefinitionViaStaticMethod() + { + $signature = (string) ValidateSignature::relative(); + $this->assertSame('Illuminate\Routing\Middleware\ValidateSignature:relative', $signature); + + $signature = (string) ValidateSignature::absolute(); + $this->assertSame('Illuminate\Routing\Middleware\ValidateSignature', $signature); + } + protected function createValidateSignatureMiddleware(array $ignore) { return new class($ignore) extends ValidateSignature diff --git a/tests/Mail/MailManagerTest.php b/tests/Mail/MailManagerTest.php index a3b3d51c55ab..deb824da5ec0 100644 --- a/tests/Mail/MailManagerTest.php +++ b/tests/Mail/MailManagerTest.php @@ -4,6 +4,7 @@ use InvalidArgumentException; use Orchestra\Testbench\TestCase; +use Symfony\Component\Mailer\Transport\Smtp\EsmtpTransport; class MailManagerTest extends TestCase { @@ -27,6 +28,22 @@ public function testEmptyTransportConfig($transport) $this->app['mail.manager']->mailer('custom_smtp'); } + public function testMailUrlConfig() + { + $this->app['config']->set('mail.mailers.smtp_url', [ + 'url' => 'smtp://usr:pwd@127.0.0.2:5876', + ]); + + $mailer = $this->app['mail.manager']->mailer('smtp_url'); + $transport = $mailer->getSymfonyTransport(); + + $this->assertInstanceOf(EsmtpTransport::class, $transport); + $this->assertSame('usr', $transport->getUsername()); + $this->assertSame('pwd', $transport->getPassword()); + $this->assertSame('127.0.0.2', $transport->getStream()->getHost()); + $this->assertSame(5876, $transport->getStream()->getPort()); + } + public static function emptyTransportConfigDataProvider() { return [ diff --git a/tests/Queue/PruneBatchesCommandTest.php b/tests/Queue/PruneBatchesCommandTest.php new file mode 100644 index 000000000000..1878373ea8b8 --- /dev/null +++ b/tests/Queue/PruneBatchesCommandTest.php @@ -0,0 +1,46 @@ +instance(BatchRepository::class, $repo = m::spy(DatabaseBatchRepository::class)); + + $command = new PruneBatchesCommand; + $command->setLaravel($container); + + $command->run(new ArrayInput(['--unfinished' => 0]), new NullOutput()); + + $repo->shouldHaveReceived('pruneUnfinished')->once(); + } + + public function testAllowPruningAllCancelledBatches() + { + $container = new Container; + $container->instance(BatchRepository::class, $repo = m::spy(DatabaseBatchRepository::class)); + + $command = new PruneBatchesCommand; + $command->setLaravel($container); + + $command->run(new ArrayInput(['--cancelled' => 0]), new NullOutput()); + + $repo->shouldHaveReceived('pruneCancelled')->once(); + } +} diff --git a/tests/Routing/RoutingSortedMiddlewareTest.php b/tests/Routing/RoutingSortedMiddlewareTest.php index e5877a844ce7..4416f9be3686 100644 --- a/tests/Routing/RoutingSortedMiddlewareTest.php +++ b/tests/Routing/RoutingSortedMiddlewareTest.php @@ -64,4 +64,59 @@ public function testItDoesNotMoveNonStringValues() $this->assertEquals(['a', $closure, 'b', $closure2, 'foo'], (new SortedMiddleware(['a', 'b'], ['a', $closure, 'b', $closure2, 'foo']))->all()); $this->assertEquals([$closure, $closure2, 'foo', 'a'], (new SortedMiddleware(['a', 'b'], [$closure, $closure2, 'foo', 'a']))->all()); } + + public function testItSortsUsingParentsAndContracts() + { + $priority = [ + FirstContractStub::class, + SecondStub::class, + 'Third', + ]; + + $middleware = [ + 'Something', + 'Something', + 'Something', + 'Something', + SecondChildStub::class, + 'Otherthing', + FirstStub::class.':api', + 'Third:foo', + FirstStub::class.':foo,bar', + 'Third', + SecondChildStub::class, + ]; + + $expected = [ + 'Something', + FirstStub::class.':api', + FirstStub::class.':foo,bar', + SecondChildStub::class, + 'Otherthing', + 'Third:foo', + 'Third', + ]; + + $this->assertEquals($expected, (new SortedMiddleware($priority, $middleware))->all()); + } +} + +interface FirstContractStub +{ + // +} + +class FirstStub implements FirstContractStub +{ + // +} + +class SecondStub +{ + // +} + +class SecondChildStub extends SecondStub +{ + // } diff --git a/tests/Support/SleepTest.php b/tests/Support/SleepTest.php new file mode 100644 index 000000000000..13c7ed4d9d3b --- /dev/null +++ b/tests/Support/SleepTest.php @@ -0,0 +1,417 @@ +seconds(); + $end = microtime(true); + + $this->assertEqualsWithDelta(1, $end - $start, 0.03); + } + + public function testItSleepsForSecondsWithMilliseconds() + { + $start = microtime(true); + Sleep::for(1.5)->seconds(); + $end = microtime(true); + + $this->assertEqualsWithDelta(1.5, $end - $start, 0.03); + } + + public function testItCanFakeSleeping() + { + Sleep::fake(); + + $start = microtime(true); + Sleep::for(1.5)->seconds(); + $end = microtime(true); + + $this->assertEqualsWithDelta(0, $end - $start, 0.03); + } + + public function testItCanSpecifyMinutes() + { + Sleep::fake(); + + $sleep = Sleep::for(1.5)->minutes(); + + $this->assertSame($sleep->duration->totalMicroseconds, 90_000_000); + } + + public function testItCanSpecifyMinute() + { + Sleep::fake(); + + $sleep = Sleep::for(1)->minute(); + + $this->assertSame($sleep->duration->totalMicroseconds, 60_000_000); + } + + public function testItCanSpecifySeconds() + { + Sleep::fake(); + + $sleep = Sleep::for(1.5)->seconds(); + + $this->assertSame($sleep->duration->totalMicroseconds, 1_500_000); + } + + public function testItCanSpecifySecond() + { + Sleep::fake(); + + $sleep = Sleep::for(1)->second(); + + $this->assertSame($sleep->duration->totalMicroseconds, 1_000_000); + } + + public function testItCanSpecifyMilliseconds() + { + Sleep::fake(); + + $sleep = Sleep::for(1.5)->milliseconds(); + + $this->assertSame($sleep->duration->totalMicroseconds, 1_500); + } + + public function testItCanSpecifyMillisecond() + { + Sleep::fake(); + + $sleep = Sleep::for(1)->millisecond(); + + $this->assertSame($sleep->duration->totalMicroseconds, 1_000); + } + + public function testItCanSpecifyMicroseconds() + { + Sleep::fake(); + + $sleep = Sleep::for(1.5)->microseconds(); + + // rounded as microseconds is the smallest unit supported... + $this->assertSame($sleep->duration->totalMicroseconds, 1); + } + + public function testItCanSpecifyMicrosecond() + { + Sleep::fake(); + + $sleep = Sleep::for(1)->microsecond(); + + $this->assertSame($sleep->duration->totalMicroseconds, 1); + } + + public function testItCanChainDurations() + { + Sleep::fake(); + + $sleep = Sleep::for(1)->second() + ->and(500)->microseconds(); + + $this->assertSame($sleep->duration->totalMicroseconds, 1000500); + } + + public function testItCanUseDateInterval() + { + Sleep::fake(); + + $sleep = Sleep::for(CarbonInterval::seconds(1)->addMilliseconds(5)); + + $this->assertSame($sleep->duration->totalMicroseconds, 1_005_000); + } + + public function testItThrowsForUnknownTimeUnit() + { + try { + Sleep::for(5); + $this->fail(); + } catch (RuntimeException $e) { + $this->assertSame('Unknown duration unit.', $e->getMessage()); + } + } + + public function testItCanAssertSequence() + { + Sleep::fake(); + + Sleep::for(5)->seconds(); + Sleep::for(1)->seconds()->and(5)->microsecond(); + + Sleep::assertSequence([ + Sleep::for(5)->seconds(), + Sleep::for(1)->seconds()->and(5)->microsecond(), + ]); + } + + public function testItFailsSequenceAssertion() + { + Sleep::fake(); + + Sleep::for(5)->seconds(); + Sleep::for(1)->seconds()->and(5)->microseconds(); + + try { + Sleep::assertSequence([ + Sleep::for(5)->seconds(), + Sleep::for(9)->seconds()->and(8)->milliseconds(), + ]); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected sleep duration of [9 seconds 8 milliseconds] but actually slept for [1 second 5 microseconds].\nFailed asserting that false is true.", $e->getMessage()); + } + } + + public function testItCanUseSleep() + { + Sleep::fake(); + + Sleep::sleep(3); + + Sleep::assertSequence([ + Sleep::for(3)->seconds(), + ]); + } + + public function testItCanUseUSleep() + { + Sleep::fake(); + + Sleep::usleep(3); + + Sleep::assertSequence([ + Sleep::for(3)->microseconds(), + ]); + } + + public function testItCanSleepTillGivenTime() + { + Sleep::fake(); + Carbon::setTestNow(now()->startOfDay()); + + Sleep::until(now()->addMinute()); + + Sleep::assertSequence([ + Sleep::for(60)->seconds(), + ]); + } + + public function testItCanSleepTillGivenTimestamp() + { + Sleep::fake(); + Carbon::setTestNow(now()->startOfDay()); + + Sleep::until(now()->addMinute()->timestamp); + + Sleep::assertSequence([ + Sleep::for(60)->seconds(), + ]); + } + + public function testItSleepsForZeroTimeWithNegativeDateTime() + { + Sleep::fake(); + Carbon::setTestNow(now()->startOfDay()); + + Sleep::until(now()->subMinutes(100)); + + Sleep::assertSequence([ + Sleep::for(0)->seconds(), + ]); + } + + public function testSleepingForZeroTime() + { + Sleep::fake(); + + Sleep::for(0)->seconds(); + + try { + Sleep::assertSequence([ + Sleep::for(1)->seconds(), + ]); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected sleep duration of [1 second] but actually slept for [0 microseconds].\nFailed asserting that false is true.", $e->getMessage()); + } + } + + public function testItFailsWhenSequenceContainsTooManySleeps() + { + Sleep::fake(); + + Sleep::for(1)->seconds(); + + try { + Sleep::assertSequence([ + Sleep::for(1)->seconds(), + Sleep::for(1)->seconds(), + ]); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected [2] sleeps but found [1].\nFailed asserting that 1 is identical to 2.", $e->getMessage()); + } + } + + public function testSilentlySetsDurationToZeroForNegativeValues() + { + Sleep::fake(); + + Sleep::for(-1)->seconds(); + + Sleep::assertSequence([ + Sleep::for(0)->seconds(), + ]); + } + + public function testItDoesntCaptureAssertionInstances() + { + Sleep::fake(); + + Sleep::for(1)->second(); + + Sleep::assertSequence([ + Sleep::for(1)->second(), + ]); + + try { + Sleep::assertSequence([ + Sleep::for(1)->second(), + Sleep::for(1)->second(), + ]); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected [2] sleeps but found [1].\nFailed asserting that 1 is identical to 2.", $e->getMessage()); + } + } + + public function testAssertNeverSlept() + { + Sleep::fake(); + + Sleep::assertNeverSlept(); + + Sleep::for(1)->seconds(); + + try { + Sleep::assertNeverSlept(); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected [0] sleeps but found [1].\nFailed asserting that 1 is identical to 0.", $e->getMessage()); + } + } + + public function testAssertNeverAgainstZeroSecondSleep() + { + Sleep::fake(); + + Sleep::assertNeverSlept(); + + Sleep::for(0)->seconds(); + + try { + Sleep::assertNeverSlept(); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected [0] sleeps but found [1].\nFailed asserting that 1 is identical to 0.", $e->getMessage()); + } + } + + public function testItCanAssertNoSleepingOccurred() + { + Sleep::fake(); + + Sleep::assertInsomniac(); + + Sleep::for(0)->second(); + + // we still have not slept... + Sleep::assertInsomniac(); + + Sleep::for(1)->second(); + + try { + Sleep::assertInsomniac(); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Unexpected sleep duration of [1 second] found.\nFailed asserting that 1000000 is identical to 0.", $e->getMessage()); + } + } + + public function testItCanAssertSleepCount() + { + Sleep::fake(); + + Sleep::assertSleptTimes(0); + + Sleep::for(1)->second(); + + Sleep::assertSleptTimes(1); + + try { + Sleep::assertSleptTimes(0); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected [0] sleeps but found [1].\nFailed asserting that 1 is identical to 0.", $e->getMessage()); + } + + try { + Sleep::assertSleptTimes(2); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("Expected [2] sleeps but found [1].\nFailed asserting that 1 is identical to 2.", $e->getMessage()); + } + } + + public function testAssertSlept() + { + Sleep::fake(); + + Sleep::assertSlept(fn () => true, 0); + + try { + Sleep::assertSlept(fn () => true); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("The expected sleep was found [0] times instead of [1].\nFailed asserting that 0 is identical to 1.", $e->getMessage()); + } + + Sleep::for(5)->seconds(); + + Sleep::assertSlept(fn (CarbonInterval $duration) => $duration->totalSeconds === 5); + + try { + Sleep::assertSlept(fn (CarbonInterval $duration) => $duration->totalSeconds === 5, 2); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("The expected sleep was found [1] times instead of [2].\nFailed asserting that 1 is identical to 2.", $e->getMessage()); + } + + try { + Sleep::assertSlept(fn (CarbonInterval $duration) => $duration->totalSeconds === 6); + $this->fail(); + } catch (AssertionFailedError $e) { + $this->assertSame("The expected sleep was found [0] times instead of [1].\nFailed asserting that 0 is identical to 1.", $e->getMessage()); + } + } +} diff --git a/tests/Support/SupportHelpersTest.php b/tests/Support/SupportHelpersTest.php index 1dec3cd9ad0e..aa1dda34cd74 100755 --- a/tests/Support/SupportHelpersTest.php +++ b/tests/Support/SupportHelpersTest.php @@ -37,6 +37,12 @@ public function testE() $this->assertEquals($str, e($html)); } + public function testEWithInvalidCodePoints() + { + $str = mb_convert_encoding('føø bar', 'ISO-8859-1', 'UTF-8'); + $this->assertEquals('f�� bar', e($str)); + } + /** * @requires PHP >= 8.1 */ diff --git a/tests/Testing/TestResponseTest.php b/tests/Testing/TestResponseTest.php index 005c1efca231..0cf30888eabb 100644 --- a/tests/Testing/TestResponseTest.php +++ b/tests/Testing/TestResponseTest.php @@ -707,6 +707,24 @@ public function testAssertConflict() $this->fail(); } + public function testAssertGone() + { + $response = TestResponse::fromBaseResponse( + (new Response)->setStatusCode(Response::HTTP_GONE) + ); + + $response->assertGone(); + + $response = TestResponse::fromBaseResponse( + (new Response)->setStatusCode(Response::HTTP_OK) + ); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Expected response status code [410] but received 200.\nFailed asserting that 410 is identical to 200."); + + $response->assertGone(); + } + public function testAssertTooManyRequests() { $response = TestResponse::fromBaseResponse( @@ -773,6 +791,42 @@ public function testAssertServerError() $response->assertServerError(); } + public function testAssertInternalServerError() + { + $response = TestResponse::fromBaseResponse( + (new Response)->setStatusCode(Response::HTTP_INTERNAL_SERVER_ERROR) + ); + + $response->assertInternalServerError(); + + $response = TestResponse::fromBaseResponse( + (new Response)->setStatusCode(Response::HTTP_OK) + ); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Expected response status code [500] but received 200.\nFailed asserting that 500 is identical to 200."); + + $response->assertInternalServerError(); + } + + public function testAssertServiceUnavailable() + { + $response = TestResponse::fromBaseResponse( + (new Response)->setStatusCode(Response::HTTP_SERVICE_UNAVAILABLE) + ); + + $response->assertServiceUnavailable(); + + $response = TestResponse::fromBaseResponse( + (new Response)->setStatusCode(Response::HTTP_OK) + ); + + $this->expectException(AssertionFailedError::class); + $this->expectExceptionMessage("Expected response status code [503] but received 200.\nFailed asserting that 503 is identical to 200."); + + $response->assertServiceUnavailable(); + } + public function testAssertNoContentAsserts204StatusCodeByDefault() { $statusCode = 500; diff --git a/tests/Validation/ValidationImageFileRuleTest.php b/tests/Validation/ValidationImageFileRuleTest.php index 187848b8fa98..7570b9c92155 100644 --- a/tests/Validation/ValidationImageFileRuleTest.php +++ b/tests/Validation/ValidationImageFileRuleTest.php @@ -30,6 +30,20 @@ public function testDimensions() ); } + public function testDimensionsWithCustomImageSizeMethod() + { + $this->fails( + File::image()->dimensions(Rule::dimensions()->width(100)->height(100)), + new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data($tmpFile = tmpfile())['uri'], 'foo.png'), + ['validation.dimensions'], + ); + + $this->passes( + File::image()->dimensions(Rule::dimensions()->width(200)->height(200)), + new UploadedFileWithCustomImageSizeMethod(stream_get_meta_data($tmpFile = tmpfile())['uri'], 'foo.png'), + ); + } + protected function fails($rule, $values, $messages) { $this->assertValidationRules($rule, $values, false, $messages); @@ -84,3 +98,21 @@ protected function tearDown(): void Facade::setFacadeApplication(null); } } + +class UploadedFileWithCustomImageSizeMethod extends UploadedFile +{ + public function isValid(): bool + { + return true; + } + + public function guessExtension(): string + { + return 'png'; + } + + public function dimensions() + { + return [200, 200]; + } +} diff --git a/tests/Validation/ValidationValidatorTest.php b/tests/Validation/ValidationValidatorTest.php index 5a3121c896a7..6a35a0a72b21 100755 --- a/tests/Validation/ValidationValidatorTest.php +++ b/tests/Validation/ValidationValidatorTest.php @@ -2324,23 +2324,23 @@ public function testValidateMissingUnless() $v = new Validator($trans, ['foo' => 'yes', 'bar' => '2'], ['foo' => 'missing_unless:bar,1']); $this->assertFalse($v->passes()); - $this->assertSame('The foo field must be missing unless bar is 2.', $v->errors()->first('foo')); + $this->assertSame('The foo field must be missing unless bar is 1.', $v->errors()->first('foo')); $v = new Validator($trans, ['foo' => '', 'bar' => '2'], ['foo' => 'missing_unless:bar,1']); $this->assertFalse($v->passes()); - $this->assertSame('The foo field must be missing unless bar is 2.', $v->errors()->first('foo')); + $this->assertSame('The foo field must be missing unless bar is 1.', $v->errors()->first('foo')); $v = new Validator($trans, ['foo' => ' ', 'bar' => '2'], ['foo' => 'missing_unless:bar,1']); $this->assertFalse($v->passes()); - $this->assertSame('The foo field must be missing unless bar is 2.', $v->errors()->first('foo')); + $this->assertSame('The foo field must be missing unless bar is 1.', $v->errors()->first('foo')); $v = new Validator($trans, ['foo' => null, 'bar' => '2'], ['foo' => 'missing_unless:bar,1']); $this->assertFalse($v->passes()); - $this->assertSame('The foo field must be missing unless bar is 2.', $v->errors()->first('foo')); + $this->assertSame('The foo field must be missing unless bar is 1.', $v->errors()->first('foo')); $v = new Validator($trans, ['foo' => [], 'bar' => '2'], ['foo' => 'missing_unless:bar,1']); $this->assertFalse($v->passes()); - $this->assertSame('The foo field must be missing unless bar is 2.', $v->errors()->first('foo')); + $this->assertSame('The foo field must be missing unless bar is 1.', $v->errors()->first('foo')); $v = new Validator($trans, ['foo' => new class implements Countable { @@ -2350,7 +2350,7 @@ public function count(): int } }, 'bar' => '2', ], ['foo' => 'missing_unless:bar,1']); $this->assertFalse($v->passes()); - $this->assertSame('The foo field must be missing unless bar is 2.', $v->errors()->first('foo')); + $this->assertSame('The foo field must be missing unless bar is 1.', $v->errors()->first('foo')); $v = new Validator($trans, ['foo' => 'foo', 'bar' => '1'], ['foo' => 'missing_unless:bar,1']); $this->assertTrue($v->passes()); @@ -4802,6 +4802,9 @@ public function testValidateDateAndFormat() $v = new Validator($trans, ['x' => ['Not', 'a', 'date']], ['x' => 'date_format:Y-m-d']); $this->assertTrue($v->fails()); + $v = new Validator($trans, ['x' => "Contain null bytes \0"], ['x' => 'date_format:Y-m-d']); + $this->assertTrue($v->fails()); + // Set current machine date to 31/xx/xxxx $v = new Validator($trans, ['x' => '2013-02'], ['x' => 'date_format:Y-m']); $this->assertTrue($v->passes()); diff --git a/types/Support/Collection.php b/types/Support/Collection.php index eefe74fbe8e1..f2f5311c8c85 100644 --- a/types/Support/Collection.php +++ b/types/Support/Collection.php @@ -1055,3 +1055,45 @@ class CustomCollection extends Collection assertType('int', $int); assertType('User', $user); } + +class Animal +{ +} +class Tiger extends Animal +{ +} +class Lion extends Animal +{ +} +class Zebra extends Animal +{ +} + +class Zoo +{ + /** + * @var \Illuminate\Support\Collection + */ + private Collection $animals; + + public function __construct() + { + $this->animals = collect([ + new Tiger, + new Lion, + new Zebra, + ]); + } + + /** + * @return \Illuminate\Support\Collection + */ + public function getWithoutZebras(): Collection + { + return $this->animals->filter(fn (Animal $animal) => ! $animal instanceof Zebra); + } +} + +$zoo = new Zoo(); + +assertType('Illuminate\Support\Collection', $zoo->getWithoutZebras());