diff --git a/bin/release.sh b/bin/release.sh index e5ea612d3ab6..b629084461d7 100755 --- a/bin/release.sh +++ b/bin/release.sh @@ -52,7 +52,7 @@ git tag $VERSION git push origin --tags # Tag Components -for REMOTE in auth broadcasting bus cache collections conditionable config console container contracts cookie database encryption events filesystem hashing http log macroable mail notifications pagination pipeline queue redis routing session support testing translation validation view +for REMOTE in auth broadcasting bus cache collections conditionable config console container contracts cookie database encryption events filesystem hashing http log macroable mail notifications pagination pipeline process queue redis routing session support testing translation validation view do echo "" echo "" diff --git a/bin/split.sh b/bin/split.sh index 806fee08bf74..9536ec7a4f31 100755 --- a/bin/split.sh +++ b/bin/split.sh @@ -41,6 +41,7 @@ remote mail git@github.com:illuminate/mail.git remote notifications git@github.com:illuminate/notifications.git remote pagination git@github.com:illuminate/pagination.git remote pipeline git@github.com:illuminate/pipeline.git +remote process git@github.com:illuminate/process.git remote queue git@github.com:illuminate/queue.git remote redis git@github.com:illuminate/redis.git remote routing git@github.com:illuminate/routing.git @@ -74,6 +75,7 @@ split 'src/Illuminate/Mail' mail split 'src/Illuminate/Notifications' notifications split 'src/Illuminate/Pagination' pagination split 'src/Illuminate/Pipeline' pipeline +split 'src/Illuminate/Process' process split 'src/Illuminate/Queue' queue split 'src/Illuminate/Redis' redis split 'src/Illuminate/Routing' routing diff --git a/composer.json b/composer.json index abd78ec1deb2..806a8df93c77 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "ext-openssl": "*", "ext-session": "*", "ext-tokenizer": "*", + "composer-runtime-api": "^2.2", "brick/math": "^0.9.3|^0.10.2|^0.11", "doctrine/inflector": "^2.0.5", "dragonmantank/cron-expression": "^3.3.2", @@ -78,6 +79,7 @@ "illuminate/notifications": "self.version", "illuminate/pagination": "self.version", "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", "illuminate/queue": "self.version", "illuminate/redis": "self.version", "illuminate/routing": "self.version", @@ -105,7 +107,7 @@ "pda/pheanstalk": "^4.0", "phpstan/phpdoc-parser": "^1.15", "phpstan/phpstan": "^1.4.7", - "phpunit/phpunit": "^9.6.0 || ^10.0.1", + "phpunit/phpunit": "^9.6.0 || ^10.0.7", "predis/predis": "^2.0.2", "symfony/cache": "^6.2", "symfony/http-client": "^6.2.4" @@ -189,6 +191,6 @@ "composer/package-versions-deprecated": true } }, - "minimum-stability": "dev", + "minimum-stability": "stable", "prefer-stable": true } diff --git a/src/Illuminate/Cache/FileLock.php b/src/Illuminate/Cache/FileLock.php new file mode 100644 index 000000000000..a5638b6832f4 --- /dev/null +++ b/src/Illuminate/Cache/FileLock.php @@ -0,0 +1,16 @@ +store->add($this->name, $this->owner, $this->seconds); + } +} diff --git a/src/Illuminate/Cache/FileStore.php b/src/Illuminate/Cache/FileStore.php index 42292295f0ce..6a6feb8a545f 100755 --- a/src/Illuminate/Cache/FileStore.php +++ b/src/Illuminate/Cache/FileStore.php @@ -12,7 +12,7 @@ class FileStore implements Store, LockProvider { - use InteractsWithTime, HasCacheLock, RetrievesMultipleKeys; + use InteractsWithTime, RetrievesMultipleKeys; /** * The Illuminate Filesystem instance. @@ -200,6 +200,31 @@ public function forever($key, $value) return $this->put($key, $value, 0); } + /** + * Get a lock instance. + * + * @param string $name + * @param int $seconds + * @param string|null $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function lock($name, $seconds = 0, $owner = null) + { + return new FileLock($this, $name, $seconds, $owner); + } + + /** + * Restore a lock instance using the owner identifier. + * + * @param string $name + * @param string $owner + * @return \Illuminate\Contracts\Cache\Lock + */ + public function restoreLock($name, $owner) + { + return $this->lock($name, 0, $owner); + } + /** * Remove an item from the cache. * diff --git a/src/Illuminate/Cache/Repository.php b/src/Illuminate/Cache/Repository.php index f6cd1c264ee1..85e7b826d50f 100755 --- a/src/Illuminate/Cache/Repository.php +++ b/src/Illuminate/Cache/Repository.php @@ -394,7 +394,9 @@ public function remember($key, $ttl, Closure $callback) return $value; } - $this->put($key, $value = $callback(), value($ttl)); + $value = $callback(); + + $this->put($key, $value, value($ttl, $value)); return $value; } diff --git a/src/Illuminate/Collections/Arr.php b/src/Illuminate/Collections/Arr.php index e11b061954c8..5ca863a6790f 100644 --- a/src/Illuminate/Collections/Arr.php +++ b/src/Illuminate/Collections/Arr.php @@ -836,9 +836,7 @@ public static function where($array, callable $callback) */ public static function whereNotNull($array) { - return static::where($array, function ($value) { - return ! is_null($value); - }); + return static::where($array, fn ($value) => ! is_null($value)); } /** diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index b094f4c151ca..aa12217dafb1 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -84,11 +84,9 @@ public function avg($callback = null) { $callback = $this->valueRetriever($callback); - $items = $this->map(function ($value) use ($callback) { - return $callback($value); - })->filter(function ($value) { - return ! is_null($value); - }); + $items = $this + ->map(fn ($value) => $callback($value)) + ->filter(fn ($value) => ! is_null($value)); if ($count = $items->count()) { return $items->sum() / $count; @@ -349,14 +347,10 @@ public function duplicatesStrict($callback = null) protected function duplicateComparator($strict) { if ($strict) { - return function ($a, $b) { - return $a === $b; - }; + return fn ($a, $b) => $a === $b; } - return function ($a, $b) { - return $a == $b; - }; + return fn ($a, $b) => $a == $b; } /** @@ -1151,9 +1145,7 @@ public function sliding($size = 2, $step = 1) { $chunks = floor(($this->count() - $size) / $step) + 1; - return static::times($chunks, function ($number) use ($size, $step) { - return $this->slice(($number - 1) * $step, $size); - }); + return static::times($chunks, fn ($number) => $this->slice(($number - 1) * $step, $size)); } /** @@ -1634,13 +1626,9 @@ public function values() */ public function zip($items) { - $arrayableItems = array_map(function ($items) { - return $this->getArrayableItems($items); - }, func_get_args()); + $arrayableItems = array_map(fn ($items) => $this->getArrayableItems($items), func_get_args()); - $params = array_merge([function () { - return new static(func_get_args()); - }, $this->items], $arrayableItems); + $params = array_merge([fn () => new static(func_get_args()), $this->items], $arrayableItems); return new static(array_map(...$params)); } diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index e5aa7978ca51..8119b3af230c 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -430,9 +430,7 @@ public function except($keys) public function filter(callable $callback = null) { if (is_null($callback)) { - $callback = function ($value) { - return (bool) $value; - }; + $callback = fn ($value) => (bool) $value; } return new static(function () use ($callback) { @@ -1500,9 +1498,7 @@ public function takeWhile($value) /** @var callable(TValue, TKey): bool $callback */ $callback = $this->useAsCallable($value) ? $value : $this->equality($value); - return $this->takeUntil(function ($item, $key) use ($callback) { - return ! $callback($item, $key); - }); + return $this->takeUntil(fn ($item, $key) => ! $callback($item, $key)); } /** diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index 37607c99bee7..c4b17d9bef41 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -92,6 +92,7 @@ abstract class GeneratorCommand extends Command implements PromptsForMissingInpu 'require', 'require_once', 'return', + 'self', 'static', 'switch', 'throw', diff --git a/src/Illuminate/Console/Scheduling/CacheEventMutex.php b/src/Illuminate/Console/Scheduling/CacheEventMutex.php index 1f6b15eacbea..76036835310c 100644 --- a/src/Illuminate/Console/Scheduling/CacheEventMutex.php +++ b/src/Illuminate/Console/Scheduling/CacheEventMutex.php @@ -3,6 +3,7 @@ namespace Illuminate\Console\Scheduling; use Illuminate\Contracts\Cache\Factory as Cache; +use Illuminate\Contracts\Cache\LockProvider; class CacheEventMutex implements EventMutex, CacheAware { @@ -39,6 +40,12 @@ public function __construct(Cache $cache) */ public function create(Event $event) { + if ($this->cache->store($this->store)->getStore() instanceof LockProvider) { + return $this->cache->store($this->store)->getStore() + ->lock($event->mutexName(), $event->expiresAt * 60) + ->acquire(); + } + return $this->cache->store($this->store)->add( $event->mutexName(), true, $event->expiresAt * 60 ); @@ -52,6 +59,12 @@ public function create(Event $event) */ public function exists(Event $event) { + if ($this->cache->store($this->store)->getStore() instanceof LockProvider) { + return ! $this->cache->store($this->store)->getStore() + ->lock($event->mutexName(), $event->expiresAt * 60) + ->get(fn () => true); + } + return $this->cache->store($this->store)->has($event->mutexName()); } @@ -63,6 +76,14 @@ public function exists(Event $event) */ public function forget(Event $event) { + if ($this->cache->store($this->store)->getStore() instanceof LockProvider) { + $this->cache->store($this->store)->getStore() + ->lock($event->mutexName(), $event->expiresAt * 60) + ->forceRelease(); + + return; + } + $this->cache->store($this->store)->forget($event->mutexName()); } diff --git a/src/Illuminate/Console/Scheduling/Event.php b/src/Illuminate/Console/Scheduling/Event.php index 7b1ca1de033b..0ff10188b251 100644 --- a/src/Illuminate/Console/Scheduling/Event.php +++ b/src/Illuminate/Console/Scheduling/Event.php @@ -80,7 +80,7 @@ class Event public $onOneServer = false; /** - * The amount of time the mutex should be valid. + * The number of minutes the mutex should be valid. * * @var int */ diff --git a/src/Illuminate/Contracts/Cookie/QueueingFactory.php b/src/Illuminate/Contracts/Cookie/QueueingFactory.php index b73f3becc846..2d5f51acd464 100644 --- a/src/Illuminate/Contracts/Cookie/QueueingFactory.php +++ b/src/Illuminate/Contracts/Cookie/QueueingFactory.php @@ -7,7 +7,7 @@ interface QueueingFactory extends Factory /** * Queue a cookie to send with the next response. * - * @param array ...$parameters + * @param mixed ...$parameters * @return void */ public function queue(...$parameters); diff --git a/src/Illuminate/Contracts/Foundation/Application.php b/src/Illuminate/Contracts/Foundation/Application.php index fd90428f46a0..f768eb963392 100644 --- a/src/Illuminate/Contracts/Foundation/Application.php +++ b/src/Illuminate/Contracts/Foundation/Application.php @@ -53,6 +53,14 @@ public function databasePath($path = ''); */ public function langPath($path = ''); + /** + * Get the path to the public directory. + * + * @param string $path + * @return string + */ + public function publicPath($path = ''); + /** * Get the path to the resources directory. * @@ -91,6 +99,13 @@ public function runningInConsole(); */ public function runningUnitTests(); + /** + * Determine if the application is running with debug mode enabled. + * + * @return bool + */ + public function hasDebugModeEnabled(); + /** * Get an instance of the maintenance mode manager implementation. * diff --git a/src/Illuminate/Contracts/Console/Process/InvokedProcess.php b/src/Illuminate/Contracts/Process/InvokedProcess.php similarity index 96% rename from src/Illuminate/Contracts/Console/Process/InvokedProcess.php rename to src/Illuminate/Contracts/Process/InvokedProcess.php index 5b017d40a82e..d272d377296b 100644 --- a/src/Illuminate/Contracts/Console/Process/InvokedProcess.php +++ b/src/Illuminate/Contracts/Process/InvokedProcess.php @@ -1,6 +1,6 @@ getPdo()->rollBack(); + $pdo = $this->getPdo(); + + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } } elseif ($this->queryGrammar->supportsSavepoints()) { $this->getPdo()->exec( $this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1)) diff --git a/src/Illuminate/Database/Console/Migrations/StatusCommand.php b/src/Illuminate/Database/Console/Migrations/StatusCommand.php index 60ad9dc19a96..aa01f07823de 100644 --- a/src/Illuminate/Database/Console/Migrations/StatusCommand.php +++ b/src/Illuminate/Database/Console/Migrations/StatusCommand.php @@ -65,9 +65,13 @@ public function handle() $this->components->twoColumnDetail('Migration name', 'Batch / Status'); - $migrations->each( - fn ($migration) => $this->components->twoColumnDetail($migration[0], $migration[1]) - ); + $migrations + ->when($this->option('pending'), fn ($collection) => $collection->filter(function ($migration) { + return str($migration[1])->contains('Pending'); + })) + ->each( + fn ($migration) => $this->components->twoColumnDetail($migration[0], $migration[1]) + ); $this->newLine(); } else { @@ -120,9 +124,8 @@ protected function getOptions() { return [ ['database', null, InputOption::VALUE_OPTIONAL, 'The database connection to use'], - + ['pending', null, InputOption::VALUE_NONE, 'Only list pending migrations'], ['path', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'The path(s) to the migrations files to use'], - ['realpath', null, InputOption::VALUE_NONE, 'Indicate any provided migration file paths are pre-resolved absolute paths'], ]; } diff --git a/src/Illuminate/Database/Console/MonitorCommand.php b/src/Illuminate/Database/Console/MonitorCommand.php index 5d0f3edcbdb2..3dff3158268c 100644 --- a/src/Illuminate/Database/Console/MonitorCommand.php +++ b/src/Illuminate/Database/Console/MonitorCommand.php @@ -20,17 +20,6 @@ class MonitorCommand extends DatabaseInspectionCommand {--databases= : The database connections to monitor} {--max= : The maximum number of connections that can be open before an event is dispatched}'; - /** - * The name of the console command. - * - * This name is used to identify the command during lazy loading. - * - * @var string|null - * - * @deprecated - */ - protected static $defaultName = 'db:monitor'; - /** * The console command description. * diff --git a/src/Illuminate/Database/Console/ShowModelCommand.php b/src/Illuminate/Database/Console/ShowModelCommand.php index 150f725c206b..4075a3cf3d9d 100644 --- a/src/Illuminate/Database/Console/ShowModelCommand.php +++ b/src/Illuminate/Database/Console/ShowModelCommand.php @@ -27,17 +27,6 @@ class ShowModelCommand extends DatabaseInspectionCommand */ protected $name = 'model:show {model}'; - /** - * The name of the console command. - * - * This name is used to identify the command during lazy loading. - * - * @var string|null - * - * @deprecated - */ - protected static $defaultName = 'model:show'; - /** * The console command description. * diff --git a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php index 23543baf95ce..29e9cefc0769 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsArrayObject.php @@ -11,7 +11,7 @@ class AsArrayObject implements Castable * Get the caster class to use when casting from / to this cast target. * * @param array $arguments - * @return CastsAttributes, iterable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject, iterable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php index 1a0dd83e08b0..b6644da18cca 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsCollection.php @@ -12,7 +12,7 @@ class AsCollection implements Castable * Get the caster class to use when casting from / to this cast target. * * @param array $arguments - * @return CastsAttributes<\Illuminate\Support\Collection, iterable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php index ce2b6639eeb3..2835f5473837 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedArrayObject.php @@ -12,7 +12,7 @@ class AsEncryptedArrayObject implements Castable * Get the caster class to use when casting from / to this cast target. * * @param array $arguments - * @return CastsAttributes, iterable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject, iterable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php index 64cdf003bab0..a4ed9faac504 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEncryptedCollection.php @@ -13,7 +13,7 @@ class AsEncryptedCollection implements Castable * Get the caster class to use when casting from / to this cast target. * * @param array $arguments - * @return CastsAttributes<\Illuminate\Support\Collection, iterable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php index 5b477853769a..d8c91875d560 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php @@ -15,7 +15,7 @@ class AsEnumArrayObject implements Castable * @template TEnum * * @param array{class-string} $arguments - * @return CastsAttributes, iterable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Database\Eloquent\Casts\ArrayObject, iterable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php index ca1feb5a981e..2cdce9d622aa 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php @@ -15,7 +15,7 @@ class AsEnumCollection implements Castable * @template TEnum of \UnitEnum|\BackedEnum * * @param array{class-string} $arguments - * @return CastsAttributes, iterable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Collection, iterable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Casts/AsStringable.php b/src/Illuminate/Database/Eloquent/Casts/AsStringable.php index c2927d2ecca8..c30e1a560b6b 100644 --- a/src/Illuminate/Database/Eloquent/Casts/AsStringable.php +++ b/src/Illuminate/Database/Eloquent/Casts/AsStringable.php @@ -12,7 +12,7 @@ class AsStringable implements Castable * Get the caster class to use when casting from / to this cast target. * * @param array $arguments - * @return CastsAttributes<\Illuminate\Support\Stringable, string|\Stringable> + * @return \Illuminate\Contracts\Database\Eloquent\CastsAttributes<\Illuminate\Support\Stringable, string|\Stringable> */ public static function castUsing(array $arguments) { diff --git a/src/Illuminate/Database/Eloquent/Factories/Factory.php b/src/Illuminate/Database/Eloquent/Factories/Factory.php index 65489814c7cf..4a416b86b1d6 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Factory.php +++ b/src/Illuminate/Database/Eloquent/Factories/Factory.php @@ -525,7 +525,7 @@ public function set($key, $value) /** * Add a new sequenced state transformation to the model definition. * - * @param array ...$sequence + * @param mixed ...$sequence * @return static */ public function sequence(...$sequence) diff --git a/src/Illuminate/Database/Eloquent/Factories/Sequence.php b/src/Illuminate/Database/Eloquent/Factories/Sequence.php index 7a4688bfaf17..e523fb3eebd0 100644 --- a/src/Illuminate/Database/Eloquent/Factories/Sequence.php +++ b/src/Illuminate/Database/Eloquent/Factories/Sequence.php @@ -30,7 +30,7 @@ class Sequence implements Countable /** * Create a new sequence instance. * - * @param array ...$sequence + * @param mixed ...$sequence * @return void */ public function __construct(...$sequence) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 01eaa94351af..ae460d371140 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -1083,6 +1083,16 @@ public function push() return true; } + /** + * Save the model and all of its relationships without raising any events to the parent model. + * + * @return bool + */ + public function pushQuietly() + { + return static::withoutEvents(fn () => $this->push()); + } + /** * Save the model to the database without raising any events. * diff --git a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php index c0462994a94a..64827357f44e 100755 --- a/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php +++ b/src/Illuminate/Database/Eloquent/Relations/HasOneOrMany.php @@ -367,6 +367,17 @@ public function createMany(iterable $records) return $instances; } + /** + * Create a Collection of new instances of the related model without raising any events to the parent model. + * + * @param iterable $records + * @return \Illuminate\Database\Eloquent\Collection + */ + public function createManyQuietly(iterable $records) + { + return Model::withoutEvents(fn () => $this->createMany($records)); + } + /** * Set the foreign ID for creating a related model. * diff --git a/src/Illuminate/Database/Migrations/Migrator.php b/src/Illuminate/Database/Migrations/Migrator.php index 87ede7ed5184..6df4a2024470 100755 --- a/src/Illuminate/Database/Migrations/Migrator.php +++ b/src/Illuminate/Database/Migrations/Migrator.php @@ -66,6 +66,13 @@ class Migrator */ protected $paths = []; + /** + * The paths that have already been required. + * + * @var array + */ + protected static $requiredPathCache = []; + /** * The output interface implementation. * @@ -515,9 +522,13 @@ protected function resolvePath(string $path) return new $class; } - $migration = $this->files->getRequire($path); + $migration = static::$requiredPathCache[$path] ??= $this->files->getRequire($path); - return is_object($migration) ? $migration : new $class; + if (is_object($migration)) { + return clone $migration; + } + + return new $class; } /** diff --git a/src/Illuminate/Database/PDO/Connection.php b/src/Illuminate/Database/PDO/Connection.php index 7bae4cc04948..66f5d9e32ae1 100644 --- a/src/Illuminate/Database/PDO/Connection.php +++ b/src/Illuminate/Database/PDO/Connection.php @@ -57,6 +57,8 @@ public function exec(string $statement): int * * @param string $sql * @return \Doctrine\DBAL\Driver\Statement + * + * @throws \Doctrine\DBAL\Driver\PDO\Exception */ public function prepare(string $sql): StatementInterface { @@ -93,6 +95,8 @@ public function query(string $sql): ResultInterface * * @param string|null $name * @return mixed + * + * @throws \Doctrine\DBAL\Driver\PDO\Exception */ public function lastInsertId($name = null) { diff --git a/src/Illuminate/Database/Query/Builder.php b/src/Illuminate/Database/Query/Builder.php index c21123862f1b..3cc3cf027535 100755 --- a/src/Illuminate/Database/Query/Builder.php +++ b/src/Illuminate/Database/Query/Builder.php @@ -101,6 +101,13 @@ class Builder implements BuilderContract */ public $from; + /** + * The index hint for the query. + * + * @var \Illuminate\Database\Query\IndexHint + */ + public $indexHint; + /** * The table joins for the query. * @@ -412,6 +419,10 @@ public function addSelect($column) $this->selectSub($column, $as); } else { + if (is_array($this->columns) && in_array($column, $this->columns, true)) { + continue; + } + $this->columns[] = $column; } } @@ -455,6 +466,45 @@ public function from($table, $as = null) return $this; } + /** + * Add an index hint to suggest a query index. + * + * @param string $index + * @return $this + */ + public function useIndex($index) + { + $this->indexHint = new IndexHint('hint', $index); + + return $this; + } + + /** + * Add an index hint to force a query index. + * + * @param string $index + * @return $this + */ + public function forceIndex($index) + { + $this->indexHint = new IndexHint('force', $index); + + return $this; + } + + /** + * Add an index hint to ignore a query index. + * + * @param string $index + * @return $this + */ + public function ignoreIndex($index) + { + $this->indexHint = new IndexHint('ignore', $index); + + return $this; + } + /** * Add a join clause to the query. * diff --git a/src/Illuminate/Database/Query/Grammars/Grammar.php b/src/Illuminate/Database/Query/Grammars/Grammar.php index 5540c8650d22..e15c55644070 100755 --- a/src/Illuminate/Database/Query/Grammars/Grammar.php +++ b/src/Illuminate/Database/Query/Grammars/Grammar.php @@ -36,6 +36,7 @@ class Grammar extends BaseGrammar 'aggregate', 'columns', 'from', + 'indexHint', 'joins', 'wheres', 'groups', diff --git a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php index f9733492df10..131f8afb4767 100755 --- a/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/MySqlGrammar.php @@ -74,6 +74,22 @@ public function whereFullText(Builder $query, $where) return "match ({$columns}) against (".$value."{$mode}{$expanded})"; } + /** + * Compile the index hints for the query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Database\Query\IndexHint $indexHint + * @return string + */ + protected function compileIndexHint(Builder $query, $indexHint) + { + return match ($indexHint->type) { + 'hint' => "use index ({$indexHint->index})", + 'force' => "force index ({$indexHint->index})", + default => "ignore index ({$indexHint->index})", + }; + } + /** * Compile an insert ignore statement into SQL. * diff --git a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php index 81b890b8b717..8bf7d39f6935 100755 --- a/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SQLiteGrammar.php @@ -117,6 +117,20 @@ protected function dateBasedWhere($type, Builder $query, $where) return "strftime('{$type}', {$this->wrap($where['column'])}) {$where['operator']} cast({$value} as text)"; } + /** + * Compile the index hints for the query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Database\Query\IndexHint $indexHint + * @return string + */ + protected function compileIndexHint(Builder $query, $indexHint) + { + return $indexHint->type === 'force' + ? "indexed by {$indexHint->index}" + : ''; + } + /** * Compile a "JSON length" statement into SQL. * diff --git a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php index ebf68df00303..9de923e091da 100755 --- a/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php +++ b/src/Illuminate/Database/Query/Grammars/SqlServerGrammar.php @@ -28,6 +28,7 @@ class SqlServerGrammar extends Grammar 'aggregate', 'columns', 'from', + 'indexHint', 'joins', 'wheres', 'groups', @@ -101,6 +102,20 @@ protected function compileFrom(Builder $query, $table) return $from; } + /** + * Compile the index hints for the query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param \Illuminate\Database\Query\IndexHint $indexHint + * @return string + */ + protected function compileIndexHint(Builder $query, $indexHint) + { + return $indexHint->type === 'force' + ? "with (index({$indexHint->index}))" + : ''; + } + /** * {@inheritdoc} * diff --git a/src/Illuminate/Database/Query/IndexHint.php b/src/Illuminate/Database/Query/IndexHint.php new file mode 100755 index 000000000000..2a720a2dee2b --- /dev/null +++ b/src/Illuminate/Database/Query/IndexHint.php @@ -0,0 +1,33 @@ +type = $type; + $this->index = $index; + } +} diff --git a/src/Illuminate/Database/Schema/SchemaState.php b/src/Illuminate/Database/Schema/SchemaState.php index 8f998ca2603a..58d9c3a438aa 100644 --- a/src/Illuminate/Database/Schema/SchemaState.php +++ b/src/Illuminate/Database/Schema/SchemaState.php @@ -86,7 +86,7 @@ abstract public function load($path); /** * Create a new process instance. * - * @param array ...$arguments + * @param mixed ...$arguments * @return \Symfony\Component\Process\Process */ public function makeProcess(...$arguments) diff --git a/src/Illuminate/Filesystem/AwsS3V3Adapter.php b/src/Illuminate/Filesystem/AwsS3V3Adapter.php index 9c210c8fbc15..8e908e81aa59 100644 --- a/src/Illuminate/Filesystem/AwsS3V3Adapter.php +++ b/src/Illuminate/Filesystem/AwsS3V3Adapter.php @@ -95,6 +95,40 @@ public function temporaryUrl($path, $expiration, array $options = []) return (string) $uri; } + /** + * Get a temporary upload URL for the file at the given path. + * + * @param string $path + * @param \DateTimeInterface $expiration + * @param array $options + * @return array + */ + public function temporaryUploadUrl($path, $expiration, array $options = []) + { + $command = $this->client->getCommand('PutObject', array_merge([ + 'Bucket' => $this->config['bucket'], + 'Key' => $this->prefixer->prefixPath($path), + ], $options)); + + $signedRequest = $this->client->createPresignedRequest( + $command, $expiration, $options + ); + + $uri = $signedRequest->getUri(); + + // If an explicit base URL has been set on the disk configuration then we will use + // it as the base URL instead of the default path. This allows the developer to + // have full control over the base path for this filesystem's generated URLs. + if (isset($this->config['temporary_url'])) { + $uri = $this->replaceBaseUrl($uri, $this->config['temporary_url']); + } + + return [ + 'url' => (string) $uri, + 'headers' => $signedRequest->getHeaders(), + ]; + } + /** * Get the underlying S3 client. * diff --git a/src/Illuminate/Filesystem/FilesystemAdapter.php b/src/Illuminate/Filesystem/FilesystemAdapter.php index 2c6aabfcac74..471ae1d1d9cf 100644 --- a/src/Illuminate/Filesystem/FilesystemAdapter.php +++ b/src/Illuminate/Filesystem/FilesystemAdapter.php @@ -741,6 +741,25 @@ public function temporaryUrl($path, $expiration, array $options = []) throw new RuntimeException('This driver does not support creating temporary URLs.'); } + /** + * Get a temporary upload URL for the file at the given path. + * + * @param string $path + * @param \DateTimeInterface $expiration + * @param array $options + * @return array + * + * @throws \RuntimeException + */ + public function temporaryUploadUrl($path, $expiration, array $options = []) + { + if (method_exists($this->adapter, 'temporaryUploadUrl')) { + return $this->adapter->temporaryUploadUrl($path, $expiration, $options); + } + + throw new RuntimeException('This driver does not support creating temporary upload URLs.'); + } + /** * Concatenate a path to a URL. * diff --git a/src/Illuminate/Foundation/Application.php b/src/Illuminate/Foundation/Application.php index 283ae65b421c..12e7431985fc 100755 --- a/src/Illuminate/Foundation/Application.php +++ b/src/Illuminate/Foundation/Application.php @@ -117,6 +117,13 @@ class Application extends Container implements ApplicationContract, CachesConfig */ protected $appPath; + /** + * The custom configuration path defined by the developer. + * + * @var string + */ + protected $configPath; + /** * The custom database path defined by the developer. * @@ -131,6 +138,13 @@ class Application extends Container implements ApplicationContract, CachesConfig */ protected $langPath; + /** + * The custom public / web path defined by the developer. + * + * @var string + */ + protected $publicPath; + /** * The custom storage path defined by the developer. * @@ -324,11 +338,10 @@ protected function bindPathsInContainer() $this->instance('path', $this->path()); $this->instance('path.base', $this->basePath()); $this->instance('path.config', $this->configPath()); - $this->instance('path.public', $this->publicPath()); - $this->instance('path.storage', $this->storagePath()); $this->instance('path.database', $this->databasePath()); + $this->instance('path.public', $this->publicPath()); $this->instance('path.resources', $this->resourcePath()); - $this->instance('path.bootstrap', $this->bootstrapPath()); + $this->instance('path.storage', $this->storagePath()); $this->useBootstrapPath(value(function () { return is_dir($directory = $this->basePath('.laravel')) @@ -351,9 +364,7 @@ protected function bindPathsInContainer() */ public function path($path = '') { - $appPath = $this->appPath ?: $this->basePath('app'); - - return $this->joinPaths($appPath, $path); + return $this->joinPaths($this->appPath ?: $this->basePath('app'), $path); } /** @@ -416,7 +427,22 @@ public function useBootstrapPath($path) */ public function configPath($path = '') { - return $this->joinPaths($this->basePath('config'), $path); + return $this->joinPaths($this->configPath ?: $this->basePath('config'), $path); + } + + /** + * Set the configuration directory. + * + * @param string $path + * @return $this + */ + public function useConfigPath($path) + { + $this->configPath = $path; + + $this->instance('path.config', $path); + + return $this; } /** @@ -427,9 +453,7 @@ public function configPath($path = '') */ public function databasePath($path = '') { - $databasePath = $this->databasePath ?: $this->basePath('database'); - - return $this->joinPaths($databasePath, $path); + return $this->joinPaths($this->databasePath ?: $this->basePath('database'), $path); } /** @@ -481,7 +505,22 @@ public function useLangPath($path) */ public function publicPath($path = '') { - return $this->joinPaths($this->basePath('public'), $path); + return $this->joinPaths($this->publicPath ?: $this->basePath('public'), $path); + } + + /** + * Set the public / web directory. + * + * @param string $path + * @return $this + */ + public function usePublicPath($path) + { + $this->publicPath = $path; + + $this->instance('path.public', $path); + + return $this; } /** @@ -492,9 +531,7 @@ public function publicPath($path = '') */ public function storagePath($path = '') { - $storagePath = $this->storagePath ?: $this->basePath('storage'); - - return $this->joinPaths($storagePath, $path); + return $this->joinPaths($this->storagePath ?: $this->basePath('storage'), $path); } /** diff --git a/src/Illuminate/Foundation/Console/AboutCommand.php b/src/Illuminate/Foundation/Console/AboutCommand.php index c6a38ce9cb4d..1d1b47e96863 100644 --- a/src/Illuminate/Foundation/Console/AboutCommand.php +++ b/src/Illuminate/Foundation/Console/AboutCommand.php @@ -18,17 +18,6 @@ class AboutCommand extends Command protected $signature = 'about {--only= : The section to display} {--json : Output the information as JSON}'; - /** - * The name of the console command. - * - * This name is used to identify the command during lazy loading. - * - * @var string|null - * - * @deprecated - */ - protected static $defaultName = 'about'; - /** * The console command description. * diff --git a/src/Illuminate/Foundation/Console/DocsCommand.php b/src/Illuminate/Foundation/Console/DocsCommand.php index 95afeb001677..279c55d06e65 100644 --- a/src/Illuminate/Foundation/Console/DocsCommand.php +++ b/src/Illuminate/Foundation/Console/DocsCommand.php @@ -26,17 +26,6 @@ class DocsCommand extends Command */ protected $signature = 'docs {page? : The documentation page to open} {section? : The section of the page to open}'; - /** - * The name of the console command. - * - * This name is used to identify the command during lazy loading. - * - * @var string|null - * - * @deprecated - */ - protected static $defaultName = 'docs'; - /** * The console command description. * diff --git a/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php b/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php index 1e11e1100f6d..f3c3fa2c5a39 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentDecryptCommand.php @@ -26,17 +26,6 @@ class EnvironmentDecryptCommand extends Command {--path= : Path to write the decrypted file} {--filename= : Filename of the decrypted file}'; - /** - * The name of the console command. - * - * This name is used to identify the command during lazy loading. - * - * @var string|null - * - * @deprecated - */ - protected static $defaultName = 'env:decrypt'; - /** * The console command description. * diff --git a/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php b/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php index ef9082629087..b0fb424e1d25 100644 --- a/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php +++ b/src/Illuminate/Foundation/Console/EnvironmentEncryptCommand.php @@ -23,17 +23,6 @@ class EnvironmentEncryptCommand extends Command {--env= : The environment to be encrypted} {--force : Overwrite the existing encrypted environment file}'; - /** - * The name of the console command. - * - * This name is used to identify the command during lazy loading. - * - * @var string|null - * - * @deprecated - */ - protected static $defaultName = 'env:encrypt'; - /** * The console command description. * diff --git a/src/Illuminate/Foundation/Console/ListenerMakeCommand.php b/src/Illuminate/Foundation/Console/ListenerMakeCommand.php index ee28b04d5129..6d815bbd7d31 100644 --- a/src/Illuminate/Foundation/Console/ListenerMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ListenerMakeCommand.php @@ -126,7 +126,7 @@ protected function getOptions() */ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) { - if ($this->didReceiveOptions($input)) { + if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) { return; } diff --git a/src/Illuminate/Foundation/Console/ModelMakeCommand.php b/src/Illuminate/Foundation/Console/ModelMakeCommand.php index bbff62eb9120..4a4412a008d9 100644 --- a/src/Illuminate/Foundation/Console/ModelMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ModelMakeCommand.php @@ -234,7 +234,7 @@ protected function getOptions() */ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) { - if ($this->didReceiveOptions($input)) { + if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) { return; } diff --git a/src/Illuminate/Foundation/Console/ObserverMakeCommand.php b/src/Illuminate/Foundation/Console/ObserverMakeCommand.php index dd2c039ef19a..1a6988d7d455 100644 --- a/src/Illuminate/Foundation/Console/ObserverMakeCommand.php +++ b/src/Illuminate/Foundation/Console/ObserverMakeCommand.php @@ -151,7 +151,7 @@ protected function getOptions() */ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) { - if ($this->didReceiveOptions($input)) { + if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) { return; } diff --git a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php index 2785392612bd..97672b684671 100644 --- a/src/Illuminate/Foundation/Console/PolicyMakeCommand.php +++ b/src/Illuminate/Foundation/Console/PolicyMakeCommand.php @@ -210,7 +210,7 @@ protected function getOptions() */ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) { - if ($this->didReceiveOptions($input)) { + if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) { return; } diff --git a/src/Illuminate/Foundation/Console/TestMakeCommand.php b/src/Illuminate/Foundation/Console/TestMakeCommand.php index 61b5e6e869b7..7ee08a160fb3 100644 --- a/src/Illuminate/Foundation/Console/TestMakeCommand.php +++ b/src/Illuminate/Foundation/Console/TestMakeCommand.php @@ -121,7 +121,7 @@ protected function getOptions() */ protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output) { - if ($this->didReceiveOptions($input)) { + if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) { return; } diff --git a/src/Illuminate/Foundation/Exceptions/Handler.php b/src/Illuminate/Foundation/Exceptions/Handler.php index 40a4dc4cbda0..3626d82408d7 100644 --- a/src/Illuminate/Foundation/Exceptions/Handler.php +++ b/src/Illuminate/Foundation/Exceptions/Handler.php @@ -299,6 +299,23 @@ protected function shouldntReport(Throwable $e) return ! is_null(Arr::first($dontReport, fn ($type) => $e instanceof $type)); } + /** + * Remove the given exception class from the list of exceptions that should be ignored. + * + * @param string $exception + * @return $this + */ + public function stopIgnoring(string $exception) + { + $this->dontReport = collect($this->dontReport) + ->reject(fn ($ignored) => $ignored === $exception)->values()->all(); + + $this->internalDontReport = collect($this->internalDontReport) + ->reject(fn ($ignored) => $ignored === $exception)->values()->all(); + + return $this; + } + /** * Create the context array for logging the given exception. * diff --git a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php index 16141c0a55af..b3f1a14f9cce 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTruncation.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTruncation.php @@ -80,7 +80,11 @@ protected function truncateTablesForConnection(ConnectionInterface $connection, $connection->unsetEventDispatcher(); collect(static::$allTables[$name] ??= $connection->getDoctrineSchemaManager()->listTableNames()) - ->diff($this->exceptTables($name)) + ->when( + property_exists($this, 'tablesToTruncate'), + fn ($tables) => $tables->intersect($this->tablesToTruncate), + fn ($tables) => $tables->diff($this->exceptTables($name)) + ) ->filter(fn ($table) => $connection->table($table)->exists()) ->each(fn ($table) => $connection->table($table)->truncate()); diff --git a/src/Illuminate/Foundation/helpers.php b/src/Illuminate/Foundation/helpers.php index 85ce8aca806c..61a6e4e815c3 100644 --- a/src/Illuminate/Foundation/helpers.php +++ b/src/Illuminate/Foundation/helpers.php @@ -628,7 +628,7 @@ function precognitive($callable = null) */ function public_path($path = '') { - return app()->joinPaths(app()->make('path.public'), $path); + return app()->publicPath($path); } } diff --git a/src/Illuminate/Mail/MailManager.php b/src/Illuminate/Mail/MailManager.php index 779c84c76d77..daa5a032209c 100644 --- a/src/Illuminate/Mail/MailManager.php +++ b/src/Illuminate/Mail/MailManager.php @@ -3,12 +3,14 @@ namespace Illuminate\Mail; use Aws\Ses\SesClient; +use Aws\SesV2\SesV2Client; use Closure; use Illuminate\Contracts\Mail\Factory as FactoryContract; use Illuminate\Log\LogManager; use Illuminate\Mail\Transport\ArrayTransport; use Illuminate\Mail\Transport\LogTransport; use Illuminate\Mail\Transport\SesTransport; +use Illuminate\Mail\Transport\SesV2Transport; use Illuminate\Support\Arr; use Illuminate\Support\Str; use InvalidArgumentException; @@ -154,7 +156,8 @@ public function createSymfonyTransport(array $config) return call_user_func($this->customCreators[$transport], $config); } - if (trim($transport ?? '') === '' || ! method_exists($this, $method = 'create'.ucfirst($transport).'Transport')) { + if (trim($transport ?? '') === '' || + ! method_exists($this, $method = 'create'.ucfirst(Str::camel($transport)).'Transport')) { throw new InvalidArgumentException("Unsupported mail transport [{$transport}]."); } @@ -250,6 +253,28 @@ protected function createSesTransport(array $config) ); } + /** + * Create an instance of the Symfony Amazon SES V2 Transport driver. + * + * @param array $config + * @return \Illuminate\Mail\Transport\Se2VwTransport + */ + protected function createSesV2Transport(array $config) + { + $config = array_merge( + $this->app['config']->get('services.ses', []), + ['version' => 'latest'], + $config + ); + + $config = Arr::except($config, ['transport']); + + return new SesV2Transport( + new SesV2Client($this->addSesCredentials($config)), + $config['options'] ?? [] + ); + } + /** * Add the SES credentials to the configuration array. * diff --git a/src/Illuminate/Mail/Mailer.php b/src/Illuminate/Mail/Mailer.php index 0c7f1ee6d593..48ea74cf32eb 100755 --- a/src/Illuminate/Mail/Mailer.php +++ b/src/Illuminate/Mail/Mailer.php @@ -269,6 +269,8 @@ public function send($view, array $data = [], $callback = null) return $this->sendMailable($view); } + $data['mailer'] = $this->name; + // First we need to parse the view, which could either be a string or an array // containing both an HTML and plain text versions of the view which should // be used when sending an e-mail. We will extract both of them out here. diff --git a/src/Illuminate/Mail/Transport/SesTransport.php b/src/Illuminate/Mail/Transport/SesTransport.php index d6a64da89d67..9db7734c62ad 100644 --- a/src/Illuminate/Mail/Transport/SesTransport.php +++ b/src/Illuminate/Mail/Transport/SesTransport.php @@ -88,16 +88,6 @@ protected function doSend(SentMessage $message): void $message->getOriginalMessage()->getHeaders()->addHeader('X-SES-Message-ID', $messageId); } - /** - * Get the string representation of the transport. - * - * @return string - */ - public function __toString(): string - { - return 'ses'; - } - /** * Get the Amazon SES client for the SesTransport instance. * @@ -128,4 +118,14 @@ public function setOptions(array $options) { return $this->options = $options; } + + /** + * Get the string representation of the transport. + * + * @return string + */ + public function __toString(): string + { + return 'ses'; + } } diff --git a/src/Illuminate/Mail/Transport/SesV2Transport.php b/src/Illuminate/Mail/Transport/SesV2Transport.php new file mode 100644 index 000000000000..4157570e5bd0 --- /dev/null +++ b/src/Illuminate/Mail/Transport/SesV2Transport.php @@ -0,0 +1,135 @@ +ses = $ses; + $this->options = $options; + + parent::__construct(); + } + + /** + * {@inheritDoc} + */ + protected function doSend(SentMessage $message): void + { + $options = $this->options; + + if ($message->getOriginalMessage() instanceof Message) { + foreach ($message->getOriginalMessage()->getHeaders()->all() as $header) { + if ($header instanceof MetadataHeader) { + $options['Tags'][] = ['Name' => $header->getKey(), 'Value' => $header->getValue()]; + } + } + } + + try { + $result = $this->ses->sendEmail( + array_merge( + $options, [ + 'ReplyToAddresses' => [$message->getEnvelope()->getSender()->toString()], + 'Destination' => [ + 'ToAddresses' => collect($message->getEnvelope()->getRecipients()) + ->map + ->toString() + ->values() + ->all(), + ], + 'Content' => [ + 'Raw' => [ + 'Data' => $message->toString(), + ], + ], + ] + ) + ); + } catch (AwsException $e) { + $reason = $e->getAwsErrorMessage() ?? $e->getMessage(); + + throw new Exception( + sprintf('Request to AWS SES V2 API failed. Reason: %s.', $reason), + is_int($e->getCode()) ? $e->getCode() : 0, + $e + ); + } + + $messageId = $result->get('MessageId'); + + $message->getOriginalMessage()->getHeaders()->addHeader('X-Message-ID', $messageId); + $message->getOriginalMessage()->getHeaders()->addHeader('X-SES-Message-ID', $messageId); + } + + /** + * Get the Amazon SES V2 client for the SesV2Transport instance. + * + * @return \Aws\SesV2\SesV2Client + */ + public function ses() + { + return $this->ses; + } + + /** + * Get the transmission options being used by the transport. + * + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * Set the transmission options being used by the transport. + * + * @param array $options + * @return array + */ + public function setOptions(array $options) + { + return $this->options = $options; + } + + /** + * Get the string representation of the transport. + * + * @return string + */ + public function __toString(): string + { + return 'ses-v2'; + } +} diff --git a/src/Illuminate/Process/.gitattributes b/src/Illuminate/Process/.gitattributes new file mode 100644 index 000000000000..7e54581c2a32 --- /dev/null +++ b/src/Illuminate/Process/.gitattributes @@ -0,0 +1,2 @@ +/.github export-ignore +.gitattributes export-ignore diff --git a/src/Illuminate/Process/.github/workflows/close-pull-request.yml b/src/Illuminate/Process/.github/workflows/close-pull-request.yml new file mode 100644 index 000000000000..226152e7f1eb --- /dev/null +++ b/src/Illuminate/Process/.github/workflows/close-pull-request.yml @@ -0,0 +1,13 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: "Thank you for your pull request. However, you have submitted this PR on the Illuminate organization which is a read-only sub split of `laravel/framework`. Please submit your PR on the https://github.com/laravel/framework repository.

Thanks!" diff --git a/src/Illuminate/Console/Process/Exceptions/ProcessFailedException.php b/src/Illuminate/Process/Exceptions/ProcessFailedException.php similarity index 68% rename from src/Illuminate/Console/Process/Exceptions/ProcessFailedException.php rename to src/Illuminate/Process/Exceptions/ProcessFailedException.php index c04fd57b6b15..227ef29d0cca 100644 --- a/src/Illuminate/Console/Process/Exceptions/ProcessFailedException.php +++ b/src/Illuminate/Process/Exceptions/ProcessFailedException.php @@ -1,8 +1,8 @@ withoutOutput = true; + $this->quietly = true; return $this; } @@ -211,7 +213,7 @@ public function options(array $options) * * @param array|string|null $command * @param callable|null $output - * @return \Illuminate\Contracts\Console\Process\ProcessResult + * @return \Illuminate\Contracts\Process\ProcessResult */ public function run(array|string $command = null, callable $output = null) { @@ -225,7 +227,7 @@ public function run(array|string $command = null, callable $output = null) $this->factory->recordIfRecording($this, $result); }); } elseif ($this->factory->isRecording() && $this->factory->preventingStrayProcesses()) { - throw new RuntimeException('Attempted process ['.(string) $this->command.'] without a matching fake.'); + throw new RuntimeException('Attempted process ['.$command.'] without a matching fake.'); } return new ProcessResult(tap($process)->run($output)); @@ -239,7 +241,7 @@ public function run(array|string $command = null, callable $output = null) * * @param array|string|null $command * @param callable $output - * @return \Illuminate\Console\Process\InvokedProcess + * @return \Illuminate\Process\InvokedProcess */ public function start(array|string $command = null, callable $output = null) { @@ -252,7 +254,7 @@ public function start(array|string $command = null, callable $output = null) $this->factory->recordIfRecording($this, $process->predictProcessResult()); }); } elseif ($this->factory->isRecording() && $this->factory->preventingStrayProcesses()) { - throw new RuntimeException('Attempted process ['.(string) $this->command.'] without a matching fake.'); + throw new RuntimeException('Attempted process ['.$command.'] without a matching fake.'); } return new InvokedProcess(tap($process)->start($output)); @@ -279,7 +281,7 @@ protected function toSymfonyProcess(array|string|null $command) $process->setIdleTimeout($this->idleTimeout); } - if ($this->withoutOutput) { + if ($this->quietly) { $process->disableOutput(); } @@ -351,7 +353,7 @@ protected function resolveSynchronousFake(string $command, Closure $fake) * @param string $command * @param callable|null $output * @param \Closure $fake - * @return \Illuminate\Console\Process\FakeInvokedProcess + * @return \Illuminate\Process\FakeInvokedProcess */ protected function resolveAsynchronousFake(string $command, ?callable $output, Closure $fake) { diff --git a/src/Illuminate/Console/Process/Pool.php b/src/Illuminate/Process/Pool.php similarity index 84% rename from src/Illuminate/Console/Process/Pool.php rename to src/Illuminate/Process/Pool.php index 801b893315c5..042686d8b01c 100644 --- a/src/Illuminate/Console/Process/Pool.php +++ b/src/Illuminate/Process/Pool.php @@ -1,15 +1,19 @@ all($format)); } + /** + * Remove a message from the message bag. + * + * @param string $key + * @return $this + */ + public function forget($key) + { + unset($this->messages[$key]); + + return $this; + } + /** * Format an array of messages. * diff --git a/src/Illuminate/Support/Str.php b/src/Illuminate/Support/Str.php index 2c7912ed9fc1..06c6a2f19f47 100644 --- a/src/Illuminate/Support/Str.php +++ b/src/Illuminate/Support/Str.php @@ -721,6 +721,39 @@ public static function pluralStudly($value, $count = 2) return implode('', $parts).self::plural($lastWord, $count); } + /** + * Generate a random, secure password. + * + * @param int $length + * @param bool $letters + * @param bool $numbers + * @param bool $symbols + * @param bool $spaces + * @return string + */ + public static function password($length = 32, $letters = true, $numbers = true, $symbols = true, $spaces = false) + { + return (new Collection) + ->when($letters, fn ($c) => $c->merge([ + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', + 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', + 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', + 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + ])) + ->when($numbers, fn ($c) => $c->merge([ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + ])) + ->when($symbols, fn ($c) => $c->merge([ + '~', '!', '#', '$', '%', '^', '&', '*', '(', ')', '-', + '_', '.', ',', '<', '>', '?', '/', '\\', '{', '}', '[', + ']', '|', ':', ';', + ])) + ->when($spaces, fn ($c) => $c->merge([' '])) + ->pipe(fn ($c) => Collection::times($length, fn () => $c[random_int(0, $c->count() - 1)])) + ->implode(''); + } + /** * Generate a more truly "random" alpha-numeric string. * diff --git a/src/Illuminate/Support/Testing/Fakes/BusFake.php b/src/Illuminate/Support/Testing/Fakes/BusFake.php index 21bd822dcfe5..9da1fac7ce61 100644 --- a/src/Illuminate/Support/Testing/Fakes/BusFake.php +++ b/src/Illuminate/Support/Testing/Fakes/BusFake.php @@ -127,15 +127,21 @@ public function assertDispatched($command, $callback = null) /** * Assert if a job was pushed a number of times. * - * @param string $command + * @param string|\Closure $command * @param int $times * @return void */ public function assertDispatchedTimes($command, $times = 1) { - $count = $this->dispatched($command)->count() + - $this->dispatchedAfterResponse($command)->count() + - $this->dispatchedSync($command)->count(); + $callback = null; + + if ($command instanceof Closure) { + [$command, $callback] = [$this->firstClosureParameterType($command), $command]; + } + + $count = $this->dispatched($command, $callback)->count() + + $this->dispatchedAfterResponse($command, $callback)->count() + + $this->dispatchedSync($command, $callback)->count(); PHPUnit::assertSame( $times, $count, @@ -200,13 +206,19 @@ public function assertDispatchedSync($command, $callback = null) /** * Assert if a job was pushed synchronously a number of times. * - * @param string $command + * @param string|\Closure $command * @param int $times * @return void */ public function assertDispatchedSyncTimes($command, $times = 1) { - $count = $this->dispatchedSync($command)->count(); + $callback = null; + + if ($command instanceof Closure) { + [$command, $callback] = [$this->firstClosureParameterType($command), $command]; + } + + $count = $this->dispatchedSync($command, $callback)->count(); PHPUnit::assertSame( $times, $count, @@ -259,13 +271,19 @@ public function assertDispatchedAfterResponse($command, $callback = null) /** * Assert if a job was pushed after the response was sent a number of times. * - * @param string $command + * @param string|\Closure $command * @param int $times * @return void */ public function assertDispatchedAfterResponseTimes($command, $times = 1) { - $count = $this->dispatchedAfterResponse($command)->count(); + $callback = null; + + if ($command instanceof Closure) { + [$command, $callback] = [$this->firstClosureParameterType($command), $command]; + } + + $count = $this->dispatchedAfterResponse($command, $callback)->count(); PHPUnit::assertSame( $times, $count, diff --git a/src/Illuminate/Support/Testing/Fakes/EventFake.php b/src/Illuminate/Support/Testing/Fakes/EventFake.php index 2a102afbadde..c8c8cff05a67 100644 --- a/src/Illuminate/Support/Testing/Fakes/EventFake.php +++ b/src/Illuminate/Support/Testing/Fakes/EventFake.php @@ -6,13 +6,14 @@ use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Support\Arr; use Illuminate\Support\Str; +use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\ReflectsClosures; use PHPUnit\Framework\Assert as PHPUnit; use ReflectionFunction; class EventFake implements Dispatcher { - use ReflectsClosures; + use ForwardsCalls, ReflectsClosures; /** * The original event dispatcher. @@ -85,21 +86,23 @@ public function assertListening($expectedEvent, $expectedListener) $actualListener = (new ReflectionFunction($listenerClosure)) ->getStaticVariables()['listener']; + $normalizedListener = $expectedListener; + if (is_string($actualListener) && Str::contains($actualListener, '@')) { $actualListener = Str::parseCallback($actualListener); if (is_string($expectedListener)) { if (Str::contains($expectedListener, '@')) { - $expectedListener = Str::parseCallback($expectedListener); + $normalizedListener = Str::parseCallback($expectedListener); } else { - $expectedListener = [$expectedListener, 'handle']; + $normalizedListener = [$expectedListener, 'handle']; } } } - if ($actualListener === $expectedListener || + if ($actualListener === $normalizedListener || ($actualListener instanceof Closure && - $expectedListener === Closure::class)) { + $normalizedListener === Closure::class)) { PHPUnit::assertTrue(true); return; @@ -377,4 +380,16 @@ public function until($event, $payload = []) { return $this->dispatch($event, $payload, true); } + + /** + * Handle dynamic method calls to the dispatcher. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->forwardCallTo($this->dispatcher, $method, $parameters); + } } diff --git a/src/Illuminate/Support/Testing/Fakes/MailFake.php b/src/Illuminate/Support/Testing/Fakes/MailFake.php index 00879776f102..c60cc21f09c7 100644 --- a/src/Illuminate/Support/Testing/Fakes/MailFake.php +++ b/src/Illuminate/Support/Testing/Fakes/MailFake.php @@ -8,12 +8,21 @@ use Illuminate\Contracts\Mail\Mailer; use Illuminate\Contracts\Mail\MailQueue; use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Mail\MailManager; +use Illuminate\Support\Traits\ForwardsCalls; use Illuminate\Support\Traits\ReflectsClosures; use PHPUnit\Framework\Assert as PHPUnit; class MailFake implements Factory, Mailer, MailQueue { - use ReflectsClosures; + use ForwardsCalls, ReflectsClosures; + + /** + * The mailer instance. + * + * @var MailManager + */ + protected $manager; /** * The mailer currently being used to send a message. @@ -36,6 +45,17 @@ class MailFake implements Factory, Mailer, MailQueue */ protected $queuedMailables = []; + /** + * Create a new mail fake. + * + * @param MailManager $manager + * @return void + */ + public function __construct(MailManager $manager) + { + $this->manager = $manager; + } + /** * Assert if a mailable was sent based on a truth-test callback. * @@ -431,4 +451,16 @@ public function forgetMailers() return $this; } + + /** + * Handle dynamic method calls to the mailer. + * + * @param string $method + * @param array $parameters + * @return mixed + */ + public function __call($method, $parameters) + { + return $this->forwardCallTo($this->manager, $method, $parameters); + } } diff --git a/src/Illuminate/Testing/Concerns/RunsInParallel.php b/src/Illuminate/Testing/Concerns/RunsInParallel.php index 8699a488b873..011bfaf3744d 100644 --- a/src/Illuminate/Testing/Concerns/RunsInParallel.php +++ b/src/Illuminate/Testing/Concerns/RunsInParallel.php @@ -116,14 +116,16 @@ public function execute(): int }); try { - $this->runner->run(); + $potentialExitCode = $this->runner->run(); } finally { $this->forEachProcess(function () { ParallelTesting::callTearDownProcessCallbacks(); }); } - return $this->getExitCode(); + return $potentialExitCode === null + ? $this->getExitCode() + : $potentialExitCode; } /** diff --git a/src/Illuminate/Testing/TestResponse.php b/src/Illuminate/Testing/TestResponse.php index 630fbb87a590..3fa40418d61a 100644 --- a/src/Illuminate/Testing/TestResponse.php +++ b/src/Illuminate/Testing/TestResponse.php @@ -1682,7 +1682,7 @@ public function __get($key) * Proxy isset() checks to the underlying base response. * * @param string $key - * @return mixed + * @return bool */ public function __isset($key) { diff --git a/src/Illuminate/Translation/MessageSelector.php b/src/Illuminate/Translation/MessageSelector.php index 37c9b31fc4fc..9f6b74db78c0 100755 --- a/src/Illuminate/Translation/MessageSelector.php +++ b/src/Illuminate/Translation/MessageSelector.php @@ -89,9 +89,9 @@ private function extractFromString($part, $number) */ private function stripConditions($segments) { - return collect($segments)->map(function ($part) { - return preg_replace('/^[\{\[]([^\[\]\{\}]*)[\}\]]/', '', $part); - })->all(); + return collect($segments) + ->map(fn ($part) => preg_replace('/^[\{\[]([^\[\]\{\}]*)[\}\]]/', '', $part)) + ->all(); } /** diff --git a/src/Illuminate/Translation/lang/en/validation.php b/src/Illuminate/Translation/lang/en/validation.php index ee4c50908509..99f7c611ca4d 100644 --- a/src/Illuminate/Translation/lang/en/validation.php +++ b/src/Illuminate/Translation/lang/en/validation.php @@ -15,11 +15,11 @@ 'accepted' => 'The :attribute field must be accepted.', 'accepted_if' => 'The :attribute field must be accepted when :other is :value.', - 'active_url' => 'The :attribute field is not a valid URL.', + 'active_url' => 'The :attribute field must be a valid URL.', 'after' => 'The :attribute field must be a date after :date.', 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.', 'alpha' => 'The :attribute field must only contain letters.', - 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes and underscores.', + 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.', 'alpha_num' => 'The :attribute field must only contain letters and numbers.', 'array' => 'The :attribute field must be an array.', 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.', @@ -34,9 +34,9 @@ 'boolean' => 'The :attribute field must be true or false.', 'confirmed' => 'The :attribute field confirmation does not match.', 'current_password' => 'The password is incorrect.', - 'date' => 'The :attribute field is not a valid date.', + 'date' => 'The :attribute field must be a valid date.', 'date_equals' => 'The :attribute field must be a date equal to :date.', - 'date_format' => 'The :attribute field does not match the format :format.', + 'date_format' => 'The :attribute field must match the format :format.', 'decimal' => 'The :attribute field must have :decimal decimal places.', 'declined' => 'The :attribute field must be declined.', 'declined_if' => 'The :attribute field must be declined when :other is :value.', @@ -45,8 +45,8 @@ 'digits_between' => 'The :attribute field must be between :min and :max digits.', 'dimensions' => 'The :attribute field has invalid image dimensions.', 'distinct' => 'The :attribute field has a duplicate value.', - 'doesnt_end_with' => 'The :attribute field may not end with one of the following: :values.', - 'doesnt_start_with' => 'The :attribute field may not start with one of the following: :values.', + 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.', + 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.', 'email' => 'The :attribute field must be a valid email address.', 'ends_with' => 'The :attribute field must end with one of the following: :values.', 'enum' => 'The selected :attribute is invalid.', @@ -67,7 +67,7 @@ ], 'image' => 'The :attribute field must be an image.', 'in' => 'The selected :attribute is invalid.', - 'in_array' => 'The :attribute field does not exist in :other.', + 'in_array' => 'The :attribute field must exist in :other.', 'integer' => 'The :attribute field must be an integer.', 'ip' => 'The :attribute field must be a valid IP address.', 'ipv4' => 'The :attribute field must be a valid IPv4 address.', @@ -134,7 +134,7 @@ 'required_with_all' => 'The :attribute field is required when :values are present.', 'required_without' => 'The :attribute field is required when :values is not present.', 'required_without_all' => 'The :attribute field is required when none of :values are present.', - 'same' => 'The :attribute field and :other must match.', + 'same' => 'The :attribute field must match :other.', 'size' => [ 'array' => 'The :attribute field must contain :size items.', 'file' => 'The :attribute field must be :size kilobytes.', diff --git a/src/Illuminate/View/Compilers/BladeCompiler.php b/src/Illuminate/View/Compilers/BladeCompiler.php index bf4f43d73545..9edbc5cecc8a 100644 --- a/src/Illuminate/View/Compilers/BladeCompiler.php +++ b/src/Illuminate/View/Compilers/BladeCompiler.php @@ -721,7 +721,7 @@ public function if($name, callable $callback) * Check the result of a condition. * * @param string $name - * @param array ...$parameters + * @param mixed ...$parameters * @return bool */ public function check($name, ...$parameters) diff --git a/tests/Cache/CacheArrayStoreTest.php b/tests/Cache/CacheArrayStoreTest.php index 76d4fe70a6ea..db7e6e84e58a 100755 --- a/tests/Cache/CacheArrayStoreTest.php +++ b/tests/Cache/CacheArrayStoreTest.php @@ -248,8 +248,10 @@ public function testValuesAreNotStoredByReference() $store->put('object', $object, 10); $object->bar = true; - $this->assertInstanceOf(stdClass::class, $store->get('object')); - $this->assertFalse(isset($store->get('object')->bar)); + $retrievedObject = $store->get('object'); + + $this->assertTrue($retrievedObject->foo); + $this->assertFalse(property_exists($retrievedObject, 'bar')); } public function testValuesAreStoredByReferenceIfSerializationIsDisabled() @@ -261,8 +263,10 @@ public function testValuesAreStoredByReferenceIfSerializationIsDisabled() $store->put('object', $object, 10); $object->bar = true; - $this->assertInstanceOf(stdClass::class, $store->get('object')); - $this->assertTrue($store->get('object')->bar); + $retrievedObject = $store->get('object'); + + $this->assertTrue($retrievedObject->foo); + $this->assertTrue($retrievedObject->bar); } public function testReleasingLockAfterAlreadyForceReleasedByAnotherOwnerFails() diff --git a/tests/Console/Scheduling/CacheEventMutexTest.php b/tests/Console/Scheduling/CacheEventMutexTest.php index 1e770abd3031..acc7eb31b9c0 100644 --- a/tests/Console/Scheduling/CacheEventMutexTest.php +++ b/tests/Console/Scheduling/CacheEventMutexTest.php @@ -2,6 +2,7 @@ namespace Illuminate\Tests\Console\Scheduling; +use Illuminate\Cache\ArrayStore; use Illuminate\Console\Scheduling\CacheEventMutex; use Illuminate\Console\Scheduling\Event; use Illuminate\Contracts\Cache\Factory; @@ -44,6 +45,7 @@ protected function setUp(): void public function testPreventOverlap() { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new \stdClass); $this->cacheRepository->shouldReceive('add')->once(); $this->cacheMutex->create($this->event); @@ -51,6 +53,7 @@ public function testPreventOverlap() public function testCustomConnection() { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new \stdClass); $this->cacheFactory->shouldReceive('store')->with('test')->andReturn($this->cacheRepository); $this->cacheRepository->shouldReceive('add')->once(); $this->cacheMutex->useStore('test'); @@ -60,6 +63,7 @@ public function testCustomConnection() public function testPreventOverlapFails() { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new \stdClass); $this->cacheRepository->shouldReceive('add')->once()->andReturn(false); $this->assertFalse($this->cacheMutex->create($this->event)); @@ -67,6 +71,7 @@ public function testPreventOverlapFails() public function testOverlapsForNonRunningTask() { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new \stdClass); $this->cacheRepository->shouldReceive('has')->once()->andReturn(false); $this->assertFalse($this->cacheMutex->exists($this->event)); @@ -74,6 +79,7 @@ public function testOverlapsForNonRunningTask() public function testOverlapsForRunningTask() { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new \stdClass); $this->cacheRepository->shouldReceive('has')->once()->andReturn(true); $this->assertTrue($this->cacheMutex->exists($this->event)); @@ -81,8 +87,53 @@ public function testOverlapsForRunningTask() public function testResetOverlap() { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new \stdClass); $this->cacheRepository->shouldReceive('forget')->once(); $this->cacheMutex->forget($this->event); } + + public function testPreventOverlapWithLockProvider() + { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new ArrayStore); + + $this->assertTrue($this->cacheMutex->create($this->event)); + } + + public function testPreventOverlapFailsWithLockProvider() + { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new ArrayStore); + + // first create the lock, so we can test that the next call fails. + $this->cacheMutex->create($this->event); + + $this->assertFalse($this->cacheMutex->create($this->event)); + } + + public function testOverlapsForNonRunningTaskWithLockProvider() + { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new ArrayStore); + + $this->assertFalse($this->cacheMutex->exists($this->event)); + } + + public function testOverlapsForRunningTaskWithLockProvider() + { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new ArrayStore); + + $this->cacheMutex->create($this->event); + + $this->assertTrue($this->cacheMutex->exists($this->event)); + } + + public function testResetOverlapWithLockProvider() + { + $this->cacheRepository->shouldReceive('getStore')->andReturn(new ArrayStore); + + $this->cacheMutex->create($this->event); + + $this->cacheMutex->forget($this->event); + + $this->assertFalse($this->cacheMutex->exists($this->event)); + } } diff --git a/tests/Database/DatabaseConnectionTest.php b/tests/Database/DatabaseConnectionTest.php index e096db94b88f..4a4dce528210 100755 --- a/tests/Database/DatabaseConnectionTest.php +++ b/tests/Database/DatabaseConnectionTest.php @@ -312,8 +312,9 @@ public function testTransactionMethodRetriesOnDeadlock() $this->expectException(QueryException::class); $this->expectExceptionMessage('Deadlock found when trying to get lock (Connection: conn, SQL: )'); - $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit', 'rollBack'])->getMock(); + $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['inTransaction', 'beginTransaction', 'commit', 'rollBack'])->getMock(); $mock = $this->getMockConnection([], $pdo); + $pdo->method('inTransaction')->willReturn(true); $pdo->expects($this->exactly(3))->method('beginTransaction'); $pdo->expects($this->exactly(3))->method('rollBack'); $pdo->expects($this->never())->method('commit'); @@ -324,8 +325,10 @@ public function testTransactionMethodRetriesOnDeadlock() public function testTransactionMethodRollsbackAndThrows() { - $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['beginTransaction', 'commit', 'rollBack'])->getMock(); + $pdo = $this->getMockBuilder(DatabaseConnectionTestMockPDO::class)->onlyMethods(['inTransaction', 'beginTransaction', 'commit', 'rollBack'])->getMock(); $mock = $this->getMockConnection([], $pdo); + // $pdo->expects($this->once())->method('inTransaction'); + $pdo->method('inTransaction')->willReturn(true); $pdo->expects($this->once())->method('beginTransaction'); $pdo->expects($this->once())->method('rollBack'); $pdo->expects($this->never())->method('commit'); diff --git a/tests/Database/DatabaseQueryBuilderTest.php b/tests/Database/DatabaseQueryBuilderTest.php index 4bc95fc73ab9..25568d5ec069 100755 --- a/tests/Database/DatabaseQueryBuilderTest.php +++ b/tests/Database/DatabaseQueryBuilderTest.php @@ -107,7 +107,7 @@ public function testAliasWrappingWithSpacesInDatabaseName() public function testAddingSelects() { $builder = $this->getBuilder(); - $builder->select('foo')->addSelect('bar')->addSelect(['baz', 'boom'])->from('users'); + $builder->select('foo')->addSelect('bar')->addSelect(['baz', 'boom'])->addSelect('bar')->from('users'); $this->assertSame('select "foo", "bar", "baz", "boom" from "users"', $builder->toSql()); } @@ -5441,6 +5441,69 @@ public function testFromQuestionMarkOperatorOnPostgres() $this->assertSame('select * from "users" where "roles" ??& ?', $builder->toSql()); } + public function testUseIndexMySql() + { + $builder = $this->getMySqlBuilder(); + $builder->select('foo')->from('users')->useIndex('test_index'); + $this->assertSame('select `foo` from `users` use index (test_index)', $builder->toSql()); + } + + public function testForceIndexMySql() + { + $builder = $this->getMySqlBuilder(); + $builder->select('foo')->from('users')->forceIndex('test_index'); + $this->assertSame('select `foo` from `users` force index (test_index)', $builder->toSql()); + } + + public function testIgnoreIndexMySql() + { + $builder = $this->getMySqlBuilder(); + $builder->select('foo')->from('users')->ignoreIndex('test_index'); + $this->assertSame('select `foo` from `users` ignore index (test_index)', $builder->toSql()); + } + + public function testUseIndexSqlite() + { + $builder = $this->getSQLiteBuilder(); + $builder->select('foo')->from('users')->useIndex('test_index'); + $this->assertSame('select "foo" from "users"', $builder->toSql()); + } + + public function testForceIndexSqlite() + { + $builder = $this->getSQLiteBuilder(); + $builder->select('foo')->from('users')->forceIndex('test_index'); + $this->assertSame('select "foo" from "users" indexed by test_index', $builder->toSql()); + } + + public function testIgnoreIndexSqlite() + { + $builder = $this->getSQLiteBuilder(); + $builder->select('foo')->from('users')->ignoreIndex('test_index'); + $this->assertSame('select "foo" from "users"', $builder->toSql()); + } + + public function testUseIndexSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->select('foo')->from('users')->useIndex('test_index'); + $this->assertSame('select [foo] from [users]', $builder->toSql()); + } + + public function testForceIndexSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->select('foo')->from('users')->forceIndex('test_index'); + $this->assertSame('select [foo] from [users] with (index(test_index))', $builder->toSql()); + } + + public function testIgnoreIndexSqlServer() + { + $builder = $this->getSqlServerBuilder(); + $builder->select('foo')->from('users')->ignoreIndex('test_index'); + $this->assertSame('select [foo] from [users]', $builder->toSql()); + } + public function testClone() { $builder = $this->getBuilder(); diff --git a/tests/Events/EventsDispatcherTest.php b/tests/Events/EventsDispatcherTest.php index 60d78559b7c0..696fc32761ff 100755 --- a/tests/Events/EventsDispatcherTest.php +++ b/tests/Events/EventsDispatcherTest.php @@ -575,7 +575,6 @@ public function testInvokeIsCalled() // It throws an "Error" when there is no method to be called. $d = new Dispatcher; $d->listen('myEvent', TestListenerLean::class); - $e = null; $this->expectException(Error::class); $this->expectExceptionMessage('Call to undefined method '.TestListenerLean::class.'::__invoke()'); diff --git a/tests/Filesystem/FilesystemAdapterTest.php b/tests/Filesystem/FilesystemAdapterTest.php index 47d334cf6b7c..4a9e3a375385 100644 --- a/tests/Filesystem/FilesystemAdapterTest.php +++ b/tests/Filesystem/FilesystemAdapterTest.php @@ -111,18 +111,18 @@ public function testDownloadNonAsciiFilename() { $this->filesystem->write('file.txt', 'Hello World'); $files = new FilesystemAdapter($this->filesystem, $this->adapter); - $response = $files->download('file.txt', 'пиздюк.txt'); + $response = $files->download('file.txt', 'привет.txt'); $this->assertInstanceOf(StreamedResponse::class, $response); - $this->assertSame("attachment; filename=pizdiuk.txt; filename*=utf-8''%D0%BF%D0%B8%D0%B7%D0%B4%D1%8E%D0%BA.txt", $response->headers->get('content-disposition')); + $this->assertSame("attachment; filename=privet.txt; filename*=utf-8''%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82.txt", $response->headers->get('content-disposition')); } public function testDownloadNonAsciiEmptyFilename() { - $this->filesystem->write('пиздюк.txt', 'Hello World'); + $this->filesystem->write('привет.txt', 'Hello World'); $files = new FilesystemAdapter($this->filesystem, $this->adapter); - $response = $files->download('пиздюк.txt'); + $response = $files->download('привет.txt'); $this->assertInstanceOf(StreamedResponse::class, $response); - $this->assertSame('attachment; filename=pizdiuk.txt; filename*=utf-8\'\'%D0%BF%D0%B8%D0%B7%D0%B4%D1%8E%D0%BA.txt', $response->headers->get('content-disposition')); + $this->assertSame('attachment; filename=privet.txt; filename*=utf-8\'\'%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82.txt', $response->headers->get('content-disposition')); } public function testDownloadPercentInFilename() diff --git a/tests/Foundation/FoundationApplicationTest.php b/tests/Foundation/FoundationApplicationTest.php index 5fd22a872fbc..8f1ce849d6a8 100755 --- a/tests/Foundation/FoundationApplicationTest.php +++ b/tests/Foundation/FoundationApplicationTest.php @@ -516,6 +516,16 @@ public function testMacroable(): void $this->assertFalse($app->foo()); } + + /** @test */ + public function testUseConfigPath(): void + { + $app = new Application; + $app->useConfigPath(__DIR__.'/fixtures/config'); + $app->bootstrapWith([\Illuminate\Foundation\Bootstrap\LoadConfiguration::class]); + + $this->assertSame('bar', $app->make('config')->get('app.foo')); + } } class ApplicationBasicServiceProviderStub extends ServiceProvider diff --git a/tests/Foundation/FoundationHelpersTest.php b/tests/Foundation/FoundationHelpersTest.php index b528064414a7..d485b86efe7b 100644 --- a/tests/Foundation/FoundationHelpersTest.php +++ b/tests/Foundation/FoundationHelpersTest.php @@ -205,9 +205,7 @@ public function testMixHotModuleReloadingWithManifestDirectoryUsesLocalhostIfNoH protected function makeHotModuleReloadFile($url, $directory = '') { - app()->singleton('path.public', function () { - return __DIR__; - }); + app()->usePublicPath(__DIR__); $path = public_path(Str::finish($directory, '/').'hot'); @@ -220,9 +218,7 @@ protected function makeHotModuleReloadFile($url, $directory = '') protected function makeManifest($directory = '') { - app()->singleton('path.public', function () { - return __DIR__; - }); + app()->usePublicPath(__DIR__); $path = public_path(Str::finish($directory, '/').'mix-manifest.json'); @@ -265,8 +261,14 @@ public function testFakeUsesLocale() // Should fallback to en_US $this->assertSame('Arkansas', fake()->state()); - $this->assertSame('Australian Capital Territory', fake('en_AU')->state()); - $this->assertContains(fake('fr_FR')->region(), ['Provence-Alpes-Côte d\'Azur', 'Guadeloupe']); + $this->assertContains(fake('de_DE')->state(), [ + 'Baden-Württemberg', 'Bayern', 'Berlin', 'Brandenburg', 'Bremen', 'Hamburg', 'Hessen', 'Mecklenburg-Vorpommern', 'Niedersachsen', 'Nordrhein-Westfalen', 'Rheinland-Pfalz', 'Saarland', 'Sachsen', 'Sachsen-Anhalt', 'Schleswig-Holstein', 'Thüringen', + ]); + $this->assertContains(fake('fr_FR')->region(), [ + 'Auvergne-Rhône-Alpes', 'Bourgogne-Franche-Comté', 'Bretagne', 'Centre-Val de Loire', 'Corse', 'Grand Est', 'Hauts-de-France', + 'Île-de-France', 'Normandie', 'Nouvelle-Aquitaine', 'Occitanie', 'Pays de la Loire', "Provence-Alpes-Côte d'Azur", + 'Guadeloupe', 'Martinique', 'Guyane', 'La Réunion', 'Mayotte', + ]); app()->instance('config', new ConfigRepository(['app' => ['faker_locale' => 'en_AU']])); mt_srand(4, MT_RAND_PHP); diff --git a/tests/Foundation/FoundationViteTest.php b/tests/Foundation/FoundationViteTest.php index ba30d31eb26e..6fc4b854dd4e 100644 --- a/tests/Foundation/FoundationViteTest.php +++ b/tests/Foundation/FoundationViteTest.php @@ -1108,7 +1108,7 @@ public function testCrossoriginAttributeIsInheritedByPreloadTags() public function testItCanConfigureTheManifestFilename() { $buildDir = Str::random(); - app()->singleton('path.public', fn () => __DIR__); + app()->usePublicPath(__DIR__); if (! file_exists(public_path($buildDir))) { mkdir(public_path($buildDir)); } @@ -1186,7 +1186,7 @@ public function testItOnlyOutputsUniquePreloadTags() protected function makeViteManifest($contents = null, $path = 'build') { - app()->singleton('path.public', fn () => __DIR__); + app()->usePublicPath(__DIR__); if (! file_exists(public_path($path))) { mkdir(public_path($path)); @@ -1247,7 +1247,7 @@ protected function cleanViteManifest($path = 'build') protected function makeViteHotFile($path = null) { - app()->singleton('path.public', fn () => __DIR__); + app()->usePublicPath(__DIR__); $path ??= public_path('hot'); diff --git a/tests/Foundation/fixtures/config/app.php b/tests/Foundation/fixtures/config/app.php new file mode 100644 index 000000000000..14d964e8494a --- /dev/null +++ b/tests/Foundation/fixtures/config/app.php @@ -0,0 +1,5 @@ + 'bar', +]; diff --git a/tests/Http/HttpRequestTest.php b/tests/Http/HttpRequestTest.php index e65cfc8c93aa..8f7e983dcfe5 100644 --- a/tests/Http/HttpRequestTest.php +++ b/tests/Http/HttpRequestTest.php @@ -8,6 +8,7 @@ use Illuminate\Session\Store; use Illuminate\Support\Carbon; use Illuminate\Support\Collection; +use Illuminate\Support\Stringable; use Illuminate\Tests\Database\Fixtures\Models\Money\Price; use InvalidArgumentException; use Mockery as m; @@ -585,6 +586,32 @@ public function testInputMethod() $this->assertInstanceOf(SymfonyUploadedFile::class, $request['file']); } + public function testStringMethod() + { + $request = Request::create('/', 'GET', [ + 'int' => 123, + 'int_str' => '456', + 'float' => 123.456, + 'float_str' => '123.456', + 'float_zero' => 0.000, + 'float_str_zero' => '0.000', + 'str' => 'abc', + 'empty_str' => '', + 'null' => null, + ]); + $this->assertTrue($request->string('int') instanceof Stringable); + $this->assertTrue($request->string('unknown_key') instanceof Stringable); + $this->assertSame('123', $request->string('int')->value()); + $this->assertSame('456', $request->string('int_str')->value()); + $this->assertSame('123.456', $request->string('float')->value()); + $this->assertSame('123.456', $request->string('float_str')->value()); + $this->assertSame('0', $request->string('float_zero')->value()); + $this->assertSame('0.000', $request->string('float_str_zero')->value()); + $this->assertSame('', $request->string('empty_str')->value()); + $this->assertSame('', $request->string('null')->value()); + $this->assertSame('', $request->string('unknown_key')->value()); + } + public function testBooleanMethod() { $request = Request::create('/', 'GET', ['with_trashed' => 'false', 'download' => true, 'checked' => 1, 'unchecked' => '0', 'with_on' => 'on', 'with_yes' => 'yes']); @@ -607,6 +634,7 @@ public function testIntegerMethod() 'nan' => 'nan', 'mixed' => '1ab', 'underscore_notation' => '2_000', + 'null' => null, ]); $this->assertSame(123, $request->integer('int')); $this->assertSame(456, $request->integer('raw_int')); @@ -616,6 +644,8 @@ public function testIntegerMethod() $this->assertSame(1, $request->integer('mixed')); $this->assertSame(2, $request->integer('underscore_notation')); $this->assertSame(123456, $request->integer('unknown_key', 123456)); + $this->assertSame(0, $request->integer('null')); + $this->assertSame(0, $request->integer('null', 123456)); } public function testFloatMethod() @@ -629,6 +659,7 @@ public function testFloatMethod() 'nan' => 'nan', 'mixed' => '1.ab', 'scientific_notation' => '1e3', + 'null' => null, ]); $this->assertSame(1.23, $request->float('float')); $this->assertSame(45.6, $request->float('raw_float')); @@ -639,6 +670,8 @@ public function testFloatMethod() $this->assertSame(1.0, $request->float('mixed')); $this->assertSame(1e3, $request->float('scientific_notation')); $this->assertSame(123.456, $request->float('unknown_key', 123.456)); + $this->assertSame(0.0, $request->float('null')); + $this->assertSame(0.0, $request->float('null', 123.456)); } public function testCollectMethod() diff --git a/tests/Integration/Events/EventFakeTest.php b/tests/Integration/Events/EventFakeTest.php index b172803a5891..ba9855ff64ae 100644 --- a/tests/Integration/Events/EventFakeTest.php +++ b/tests/Integration/Events/EventFakeTest.php @@ -5,6 +5,7 @@ use Closure; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Schema; use Orchestra\Testbench\TestCase; @@ -140,18 +141,27 @@ public function testEventsListedInExceptAreProperlyDispatched() public function testAssertListening() { Event::fake(); - Event::listen('event', 'listener'); - Event::listen('event', PostEventSubscriber::class); - Event::listen('event', 'Illuminate\\Tests\\Integration\\Events\\PostAutoEventSubscriber@handle'); - Event::listen('event', [PostEventSubscriber::class, 'foo']); + + $listenersOfSameEventInRandomOrder = Arr::shuffle([ + 'listener', + 'Illuminate\\Tests\\Integration\\Events\\PostAutoEventSubscriber@handle', + PostEventSubscriber::class, + [PostEventSubscriber::class, 'foo'], + ]); + + foreach ($listenersOfSameEventInRandomOrder as $listener) { + Event::listen('event', $listener); + } + Event::subscribe(PostEventSubscriber::class); + Event::listen(function (NonImportantEvent $event) { // do something }); Post::observe(new PostObserver); - ($post = new Post)->save(); + (new Post)->save(); Event::assertListening('event', 'listener'); Event::assertListening('event', PostEventSubscriber::class); @@ -163,6 +173,13 @@ public function testAssertListening() Event::assertListening('eloquent.saving: '.Post::class, PostObserver::class.'@saving'); Event::assertListening('eloquent.saving: '.Post::class, [PostObserver::class, 'saving']); } + + public function testMissingMethodsAreForwarded() + { + Event::macro('foo', fn () => 'bar'); + + $this->assertEquals('bar', Event::fake()->foo()); + } } class Post extends Model diff --git a/tests/Integration/Foundation/FoundationHelpersTest.php b/tests/Integration/Foundation/FoundationHelpersTest.php index 336273fb1a61..150252c581d9 100644 --- a/tests/Integration/Foundation/FoundationHelpersTest.php +++ b/tests/Integration/Foundation/FoundationHelpersTest.php @@ -117,9 +117,7 @@ public function testMixOnlyThrowsAndReportsOneExceptionWhenAssetIsMissingFromMan protected function makeManifest($directory = '') { - $this->app->singleton('path.public', function () { - return __DIR__; - }); + app()->usePublicPath(__DIR__); $path = public_path(Str::finish($directory, '/').'mix-manifest.json'); diff --git a/tests/Integration/Routing/PrecognitionTest.php b/tests/Integration/Routing/PrecognitionTest.php index 102418fce8d3..7c592dcaad50 100644 --- a/tests/Integration/Routing/PrecognitionTest.php +++ b/tests/Integration/Routing/PrecognitionTest.php @@ -111,7 +111,7 @@ public function testItCanExcludeValidationRulesWhenPrecognitiveWithFormRequest() $response->assertStatus(422); $response->assertJsonPath('errors', [ 'required_integer' => [ - 'The required integer must be an integer.', + 'The required integer field must be an integer.', ], ]); } @@ -129,10 +129,10 @@ public function testItRunsExcludedRulesWhenNotPrecognitiveForFormRequest() $response->assertStatus(422); $response->assertJsonPath('errors', [ 'required_integer' => [ - 'The required integer must be an integer.', + 'The required integer field must be an integer.', ], 'required_integer_when_not_precognitive' => [ - 'The required integer when not precognitive must be an integer.', + 'The required integer when not precognitive field must be an integer.', ], ]); } @@ -154,10 +154,10 @@ public function testClientCanSpecifyInputToValidate() $response->assertStatus(422); $response->assertJsonPath('errors', [ 'optional_integer_1' => [ - 'The optional integer 1 must be an integer.', + 'The optional integer 1 field must be an integer.', ], 'optional_integer_2' => [ - 'The optional integer 2 must be an integer.', + 'The optional integer 2 field must be an integer.', ], ]); } @@ -302,10 +302,10 @@ public function testClientCanSpecifyInputsToValidateWhenUsingControllerValidate( $response->assertStatus(422); $response->assertJsonPath('errors', [ 'optional_integer_1' => [ - 'The optional integer 1 must be an integer.', + 'The optional integer 1 field must be an integer.', ], 'optional_integer_2' => [ - 'The optional integer 2 must be an integer.', + 'The optional integer 2 field must be an integer.', ], ]); } @@ -328,10 +328,10 @@ public function testClientCanSpecifyInputsToValidateWhenUsingControllerValidateW $response->assertStatus(422); $response->assertJsonPath('errors', [ 'optional_integer_1' => [ - 'The optional integer 1 must be an integer.', + 'The optional integer 1 field must be an integer.', ], 'optional_integer_2' => [ - 'The optional integer 2 must be an integer.', + 'The optional integer 2 field must be an integer.', ], ]); } @@ -365,10 +365,10 @@ public function testClientCanSpecifyInputsToValidateWhenUsingRequestValidate() $response->assertStatus(422); $response->assertJsonPath('errors', [ 'optional_integer_1' => [ - 'The optional integer 1 must be an integer.', + 'The optional integer 1 field must be an integer.', ], 'optional_integer_2' => [ - 'The optional integer 2 must be an integer.', + 'The optional integer 2 field must be an integer.', ], ]); } @@ -403,10 +403,10 @@ public function testClientCanSpecifyInputsToValidateWhenUsingRequestValidateWith $response->assertStatus(422); $response->assertJsonPath('errors', [ 'optional_integer_1' => [ - 'The optional integer 1 must be an integer.', + 'The optional integer 1 field must be an integer.', ], 'optional_integer_2' => [ - 'The optional integer 2 must be an integer.', + 'The optional integer 2 field must be an integer.', ], ]); } @@ -429,10 +429,10 @@ public function testClientCanSpecifyInputsToValidateWhenUsingControllerValidateW $response->assertStatus(422); $response->assertJsonPath('errors', [ 'optional_integer_1' => [ - 'The optional integer 1 must be an integer.', + 'The optional integer 1 field must be an integer.', ], 'optional_integer_2' => [ - 'The optional integer 2 must be an integer.', + 'The optional integer 2 field must be an integer.', ], ]); } @@ -467,7 +467,7 @@ public function testSpacesAreImportantInValidationFilterLogicForJsonRequests() $response->assertStatus(422); $response->assertJsonPath('errors', [ ' input with spaces ' => [ - 'The input with spaces must be an integer.', + 'The input with spaces field must be an integer.', ], ]); } diff --git a/tests/Mail/MailSesV2TransportTest.php b/tests/Mail/MailSesV2TransportTest.php new file mode 100755 index 000000000000..692ab537917a --- /dev/null +++ b/tests/Mail/MailSesV2TransportTest.php @@ -0,0 +1,130 @@ +singleton('config', function () { + return new Repository([ + 'services.ses' => [ + 'key' => 'foo', + 'secret' => 'bar', + 'region' => 'us-east-1', + ], + ]); + }); + + $manager = new MailManager($container); + + /** @var \Illuminate\Mail\Transport\SesV2Transport $transport */ + $transport = $manager->createSymfonyTransport(['transport' => 'ses-v2']); + + $ses = $transport->ses(); + + $this->assertSame('us-east-1', $ses->getRegion()); + + $this->assertSame('ses-v2', (string) $transport); + } + + public function testSend() + { + $message = new Email(); + $message->subject('Foo subject'); + $message->text('Bar body'); + $message->sender('myself@example.com'); + $message->to('me@example.com'); + $message->bcc('you@example.com'); + $message->getHeaders()->add(new MetadataHeader('FooTag', 'TagValue')); + + $client = m::mock(SesV2Client::class); + $sesResult = m::mock(); + $sesResult->shouldReceive('get') + ->with('MessageId') + ->once() + ->andReturn('ses-message-id'); + $client->shouldReceive('sendEmail')->once() + ->with(m::on(function ($arg) { + return count($arg['ReplyToAddresses']) === 1 && + $arg['ReplyToAddresses'][0] === 'myself@example.com' && + $arg['Destination']['ToAddresses'] === ['me@example.com', 'you@example.com'] && + $arg['Tags'] === [['Name' => 'FooTag', 'Value' => 'TagValue']]; + })) + ->andReturn($sesResult); + + (new SesV2Transport($client))->send($message); + } + + public function testSesV2LocalConfiguration() + { + $container = new Container; + + $container->singleton('config', function () { + return new Repository([ + 'mail' => [ + 'mailers' => [ + 'ses' => [ + 'transport' => 'ses-v2', + 'region' => 'eu-west-1', + 'options' => [ + 'ConfigurationSetName' => 'Laravel', + 'Tags' => [ + ['Name' => 'Laravel', 'Value' => 'Framework'], + ], + ], + ], + ], + ], + 'services' => [ + 'ses' => [ + 'region' => 'us-east-1', + ], + ], + ]); + }); + + $container->instance('view', $this->createMock(Factory::class)); + + $container->bind('events', function () { + return null; + }); + + $manager = new MailManager($container); + + /** @var \Illuminate\Mail\Mailer $mailer */ + $mailer = $manager->mailer('ses'); + + /** @var \Illuminate\Mail\Transport\SesV2Transport $transport */ + $transport = $mailer->getSymfonyTransport(); + + $this->assertSame('eu-west-1', $transport->ses()->getRegion()); + + $this->assertSame([ + 'ConfigurationSetName' => 'Laravel', + 'Tags' => [ + ['Name' => 'Laravel', 'Value' => 'Framework'], + ], + ], $transport->getOptions()); + } +} diff --git a/tests/Console/ProcessTest.php b/tests/Process/ProcessTest.php similarity index 90% rename from tests/Console/ProcessTest.php rename to tests/Process/ProcessTest.php index 2a32994ba8c8..a61eb1ea37cd 100644 --- a/tests/Console/ProcessTest.php +++ b/tests/Process/ProcessTest.php @@ -1,10 +1,11 @@ run('ls -la'); } - public function testStrayProcessesCanBePrevented() + public function testStrayProcessesCanBePreventedWithStringComand() { $this->expectException(RuntimeException::class); - $this->expectExceptionMessage('Attempted process'); + $this->expectExceptionMessage('Attempted process ['); + $this->expectExceptionMessage('cat composer.json'); + $this->expectExceptionMessage('] without a matching fake.'); $factory = new Factory; @@ -327,6 +330,24 @@ public function testStrayProcessesCanBePrevented() $result = $factory->run('cat composer.json'); } + public function testStrayProcessesCanBePreventedWithArrayCommand() + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Attempted process ['); + $this->expectExceptionMessage('cat composer.json'); + $this->expectExceptionMessage('] without a matching fake.'); + + $factory = new Factory; + + $factory->preventStrayProcesses(); + + $factory->fake([ + 'ls *' => 'ls command', + ]); + + $result = $factory->run(['cat composer.json']); + } + public function testStrayProcessesActuallyRunByDefault() { $factory = new Factory; @@ -396,6 +417,7 @@ public function testRealProcessesCanThrow() } $this->expectException(ProcessFailedException::class); + $this->expectExceptionMessage('The process "echo "Hello World" >&2; exit 1;" failed.'); $factory = new Factory; $result = $factory->path(__DIR__)->run('echo "Hello World" >&2; exit 1;'); @@ -403,6 +425,23 @@ public function testRealProcessesCanThrow() $result->throw(); } + public function testRealProcessesCanTimeout() + { + if (windows_os()) { + $this->markTestSkipped('Requires Linux.'); + } + + $this->expectException(ProcessTimedOutException::class); + $this->expectExceptionMessage( + 'The process "sleep 2; exit 1;" exceeded the timeout of 1 seconds.' + ); + + $factory = new Factory; + $result = $factory->timeout(1)->path(__DIR__)->run('sleep 2; exit 1;'); + + $result->throw(); + } + public function testRealProcessesCanThrowIfTrue() { if (windows_os()) { diff --git a/tests/Support/SupportMailTest.php b/tests/Support/SupportMailTest.php new file mode 100644 index 000000000000..20cf09c7783d --- /dev/null +++ b/tests/Support/SupportMailTest.php @@ -0,0 +1,31 @@ + $str === 'foo' + ? 'it works!' + : 'it failed.', + ); + + $this->assertEquals('it works!', Mail::test('foo')); + } + + public function testItRegisterAndCallMacrosWhenFaked() + { + Mail::macro('test', fn (string $str) => $str === 'foo' + ? 'it works!' + : 'it failed.', + ); + + Mail::fake(); + + $this->assertEquals('it works!', Mail::test('foo')); + } +} diff --git a/tests/Support/SupportMessageBagTest.php b/tests/Support/SupportMessageBagTest.php index 7ad8f6a7640d..1f5477124ca9 100755 --- a/tests/Support/SupportMessageBagTest.php +++ b/tests/Support/SupportMessageBagTest.php @@ -126,6 +126,13 @@ public function testAddIf() $this->assertFalse($container->has('bar')); } + public function testForget() + { + $container = new MessageBag(['foo' => 'bar']); + $container->forget('foo'); + $this->assertFalse($container->has('foo')); + } + public function testHasWithKeyNull() { $container = new MessageBag; diff --git a/tests/Support/SupportStrTest.php b/tests/Support/SupportStrTest.php index a17878811bae..cdb7a2f170a3 100755 --- a/tests/Support/SupportStrTest.php +++ b/tests/Support/SupportStrTest.php @@ -1083,6 +1083,11 @@ public function testItCanSpecifyAFallbackForASequence() Str::createUuidsNormally(); } } + + public function testPasswordCreation() + { + $this->assertTrue(strlen(Str::password()) === 32); + } } class StringableObjectStub diff --git a/tests/Support/SupportTestingBusFakeTest.php b/tests/Support/SupportTestingBusFakeTest.php index a2eb9f2ce41a..cc23b62b824f 100644 --- a/tests/Support/SupportTestingBusFakeTest.php +++ b/tests/Support/SupportTestingBusFakeTest.php @@ -224,6 +224,30 @@ public function testAssertDispatchedAfterResponseWithCallbackFunction() }); } + public function testAssertDispatchedAfterResponseTimesWithCallbackFunction() + { + $this->fake->dispatchAfterResponse(new OtherBusJobStub(0)); + $this->fake->dispatchAfterResponse(new OtherBusJobStub(1)); + $this->fake->dispatchAfterResponse(new OtherBusJobStub(1)); + + try { + $this->fake->assertDispatchedAfterResponseTimes(function (OtherBusJobStub $job) { + return $job->id === 0; + }, 2); + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertStringContainsString('The expected [Illuminate\Tests\Support\OtherBusJobStub] job was pushed 1 times instead of 2 times.', $e->getMessage()); + } + + $this->fake->assertDispatchedAfterResponseTimes(function (OtherBusJobStub $job) { + return $job->id === 0; + }); + + $this->fake->assertDispatchedAfterResponseTimes(function (OtherBusJobStub $job) { + return $job->id === 1; + }, 2); + } + public function testAssertDispatchedSyncWithCallbackFunction() { $this->fake->dispatchSync(new OtherBusJobStub); @@ -262,6 +286,30 @@ public function testAssertDispatchedTimes() $this->fake->assertDispatchedTimes(BusJobStub::class, 2); } + public function testAssertDispatchedTimesWithCallbackFunction() + { + $this->fake->dispatch(new OtherBusJobStub(0)); + $this->fake->dispatchNow(new OtherBusJobStub(1)); + $this->fake->dispatchAfterResponse(new OtherBusJobStub(1)); + + try { + $this->fake->assertDispatchedTimes(function (OtherBusJobStub $job) { + return $job->id === 0; + }, 2); + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertStringContainsString('The expected [Illuminate\Tests\Support\OtherBusJobStub] job was pushed 1 times instead of 2 times.', $e->getMessage()); + } + + $this->fake->assertDispatchedTimes(function (OtherBusJobStub $job) { + return $job->id === 0; + }); + + $this->fake->assertDispatchedTimes(function (OtherBusJobStub $job) { + return $job->id === 1; + }, 2); + } + public function testAssertDispatchedAfterResponseTimes() { $this->fake->dispatchAfterResponse(new BusJobStub); @@ -292,6 +340,30 @@ public function testAssertDispatchedSyncTimes() $this->fake->assertDispatchedSyncTimes(BusJobStub::class, 2); } + public function testAssertDispatchedSyncTimesWithCallbackFunction() + { + $this->fake->dispatchSync(new OtherBusJobStub(0)); + $this->fake->dispatchSync(new OtherBusJobStub(1)); + $this->fake->dispatchSync(new OtherBusJobStub(1)); + + try { + $this->fake->assertDispatchedSyncTimes(function (OtherBusJobStub $job) { + return $job->id === 0; + }, 2); + $this->fail(); + } catch (ExpectationFailedException $e) { + $this->assertStringContainsString('The expected [Illuminate\Tests\Support\OtherBusJobStub] job was synchronously pushed 1 times instead of 2 times.', $e->getMessage()); + } + + $this->fake->assertDispatchedSyncTimes(function (OtherBusJobStub $job) { + return $job->id === 0; + }); + + $this->fake->assertDispatchedSyncTimes(function (OtherBusJobStub $job) { + return $job->id === 1; + }, 2); + } + public function testAssertNotDispatched() { $this->fake->assertNotDispatched(BusJobStub::class); diff --git a/tests/Support/SupportTestingMailFakeTest.php b/tests/Support/SupportTestingMailFakeTest.php index 752a30daacd3..72f4e3b323da 100644 --- a/tests/Support/SupportTestingMailFakeTest.php +++ b/tests/Support/SupportTestingMailFakeTest.php @@ -5,12 +5,19 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Contracts\Translation\HasLocalePreference; use Illuminate\Mail\Mailable; +use Illuminate\Mail\MailManager; use Illuminate\Support\Testing\Fakes\MailFake; +use Mockery as m; use PHPUnit\Framework\ExpectationFailedException; use PHPUnit\Framework\TestCase; class SupportTestingMailFakeTest extends TestCase { + /** + * @var \Mockery + */ + private $mailManager; + /** * @var \Illuminate\Support\Testing\Fakes\MailFake */ @@ -24,7 +31,8 @@ class SupportTestingMailFakeTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->fake = new MailFake; + $this->mailManager = m::mock(MailManager::class); + $this->fake = new MailFake($this->mailManager); $this->mailable = new MailableStub; } @@ -213,6 +221,13 @@ public function testAssertSentWithClosure() return $mail->hasTo($user); }); } + + public function testMissingMethodsAreForwarded() + { + $this->mailManager->shouldReceive('foo')->andReturn('bar'); + + $this->assertEquals('bar', $this->fake->foo()); + } } class MailableStub extends Mailable