This commit is contained in:
RafficMohammed
2023-02-17 13:25:50 +05:30
parent 2381fd7cf5
commit 67950fc78f
891 changed files with 102300 additions and 138477 deletions

View File

@@ -1,6 +1,130 @@
# Release Notes for 9.x
## [Unreleased](https://github.com/laravel/framework/compare/v9.47.0...9.x)
## [Unreleased](https://github.com/laravel/framework/compare/v9.51.0...9.x)
## [v9.51.0](https://github.com/laravel/framework/compare/v9.50.2...v9.51.0) - 2023-02-07
### Added
- Added `Illuminate/Foundation/Testing/Concerns/InteractsWithDatabase::expectsDatabaseQueryCount()` ([#45932](https://github.com/laravel/framework/pull/45932))
- Added pending has-many-through and has-one-through builder ([#45894](https://github.com/laravel/framework/pull/45894))
- Added `Illuminate/Http/Client/PendingRequest::withUrlParameters()` ([#45982](https://github.com/laravel/framework/pull/45982))
### Fixed
- Fix: prevent duplicated content-type on HTTP client ([#45960](https://github.com/laravel/framework/pull/45960))
- Add missing php extensions in composer ([#45941](https://github.com/laravel/framework/pull/45941))
### Changed
- Command schedule:work minor features: schedule:run output file & environment specific verbosity ([#45949](https://github.com/laravel/framework/pull/45949))
- Added missing self reserved word to reservedNames array in `Illuminate/Console/GeneratorCommand.php` ([#46001](https://github.com/laravel/framework/pull/46001))
- pass value along to ttl callback in `Illuminate/Cache/Repository::remember()` ([#46006](https://github.com/laravel/framework/pull/46006))
- Make sure the lock_connection is used for schedule's withoutOverlapping() ([#45963](https://github.com/laravel/framework/pull/45963))
## [v9.50.2](https://github.com/laravel/framework/compare/v9.50.1...v9.50.2) - 2023-02-02
### Fixed
- Fixed missing_with and missing_with_all validation ([#45913](https://github.com/laravel/framework/pull/45913))
- Fixes blade escaped tags issue ([#45928](https://github.com/laravel/framework/pull/45928))
### Changed
- Trims numeric validation values / parameters ([#45912](https://github.com/laravel/framework/pull/45912))
- Random function doesn't generate evenly distributed random chars ([#45916](https://github.com/laravel/framework/pull/45916))
## [v9.50.1](https://github.com/laravel/framework/compare/v9.50.0...v9.50.1) - 2023-02-01
### Reverted
- Reverted ["Optimize destroy method"](https://github.com/laravel/framework/pull/45709) ([#45903](https://github.com/laravel/framework/pull/45903))
### Changed
- Allow scheme to be specified in `Illuminate/Mail/MailManager::createSmtpTransport()` ([68a8bfc](https://github.com/laravel/framework/commit/68a8bfc3ab758962c8f050160ec32833dc12e467))
- Accept optional mode in `Illuminate/Filesystem/Filesystem::replace()` ([2664e7f](https://github.com/laravel/framework/commit/2664e7fcdfe3a290462ae8e326ba79a17c747c1e))
## [v9.50.0](https://github.com/laravel/framework/compare/v9.49.0...v9.50.0) - 2023-02-01
### Added
- Added `Illuminate/Translation/Translator::stringable()` ([#45874](https://github.com/laravel/framework/pull/45874))
- Added `Illuminate/Foundation/Testing/DatabaseTruncation` ([#45726](https://github.com/laravel/framework/pull/45726))
- Added @style Blade directive ([#45887](https://github.com/laravel/framework/pull/45887))
### Reverted
- Reverted: ["Fix Illuminate Filesystem replace() leaves file executable"](https://github.com/laravel/framework/pull/45856) ([5ea388d](https://github.com/laravel/framework/commit/5ea388d7fe6f786b6dbcb34e0b52341c0b38ad7e))
### Fixed
- Fixed LazyCollection::makeIterator() to accept non Generator Function ([#45881](https://github.com/laravel/framework/pull/45881))
### Changed
- Solve data to be dumped for separate schemes ([#45805](https://github.com/laravel/framework/pull/45805))
## [v9.49.0](https://github.com/laravel/framework/compare/v9.48.0...v9.49.0) - 2023-01-31
### Added
- Added `Illuminate/Database/Schema/ForeignKeyDefinition::noActionOnDelete()` ([#45712](https://github.com/laravel/framework/pull/45712))
- Added new throw helper methods to the HTTP Client ([#45704](https://github.com/laravel/framework/pull/45704))
- Added configurable timezone support for WorkCommand output timestamps ([#45722](https://github.com/laravel/framework/pull/45722))
- Added support for casting arrays containing enums ([#45621](https://github.com/laravel/framework/pull/45621))
- Added "missing" validation rules ([#45717](https://github.com/laravel/framework/pull/45717))
- Added `/Illuminate/Database/Eloquent/SoftDeletes::forceDeleteQuietly()` ([#45737](https://github.com/laravel/framework/pull/45737))
- Added `Illuminate/Collections/Arr::sortDesc()` ([#45761](https://github.com/laravel/framework/pull/45761))
- Added CLI Prompts ([#45629](https://github.com/laravel/framework/pull/45629), [#45864](https://github.com/laravel/framework/pull/45864))
- Adds assertJsonIsArray and assertJsonIsObject for TestResponse ([#45731](https://github.com/laravel/framework/pull/45731))
- Added `Illuminate/Database/Eloquent/Relations/HasOneOrMany::createQuietly()` ([#45783](https://github.com/laravel/framework/pull/45783))
- Add validation rules: ascii_alpha, ascii_alpha_num, ascii_alpha_dash ([#45769](https://github.com/laravel/framework/pull/45769))
- Extract status methods to traits ([#45789](https://github.com/laravel/framework/pull/45789))
- Add "addRestoreOrCreate" extension to SoftDeletingScope ([#45754](https://github.com/laravel/framework/pull/45754))
- Added connection established event ([f850d99](https://github.com/laravel/framework/commit/f850d99c50d173189ece2bb37b6c7ddcb456f1f9))
- Add forceDeleting event to models ([#45836](https://github.com/laravel/framework/pull/45836))
- Add title tag in mail template ([#45859](https://github.com/laravel/framework/pull/45859))
- Added new methods to Collection ([#45839](https://github.com/laravel/framework/pull/45839))
- Add skip cancelled middleware ([#45869](https://github.com/laravel/framework/pull/45869))
### Fixed
- Fix flushdb on cluster for `PredisClusterConnection.php` ([#45544](https://github.com/laravel/framework/pull/45544))
- Fix blade tag issue with nested calls ([#45764](https://github.com/laravel/framework/pull/45764))
- Fix infinite loop in blade compiler ([#45780](https://github.com/laravel/framework/pull/45780))
- Fix ValidationValidator not to accept terminating newline ([#45790](https://github.com/laravel/framework/pull/45790))
- Fix stubs publish command generating incorrect controller stubs ([#45812](https://github.com/laravel/framework/pull/45812))
- fix: normalize route pipeline exception ([#45817](https://github.com/laravel/framework/pull/45817))
- Fix Illuminate Filesystem replace() leaves file executable ([#45856](https://github.com/laravel/framework/pull/45856))
### Changed
- Ensures channel name matches from start of string ([#45692](https://github.com/laravel/framework/pull/45692))
- Replace raw invisible characters in regex expressions with counterpart Unicode regex notations ([#45680](https://github.com/laravel/framework/pull/45680))
- Optimize destroy method ([#45709](https://github.com/laravel/framework/pull/45709))
- Unify prohibits behavior around prohibits_if ([#45723](https://github.com/laravel/framework/pull/45723))
- Removes dependency on bcmath ([#45729](https://github.com/laravel/framework/pull/45729))
- Allow brick/math 0.11 also ([#45762](https://github.com/laravel/framework/pull/45762))
- Optimize findMany of BelongsToMany ([#45745](https://github.com/laravel/framework/pull/45745))
- Ensure decimal rule handles large values ([#45693](https://github.com/laravel/framework/pull/45693))
- Backed enum support for @js ([#45862](https://github.com/laravel/framework/pull/45862))
- Restart syscalls for SIGALRM when worker times out a job ([#45871](https://github.com/laravel/framework/pull/45871))
- Ensure subsiquent calls to Mailable->to() overwrite previous entries ([#45885](https://github.com/laravel/framework/pull/45885))
## [v9.48.0](https://github.com/laravel/framework/compare/v9.47.0...v9.48.0) - 2023-01-17
### Added
- Added `Illuminate/Database/Schema/Builder::withoutForeignKeyConstraints()` ([#45601](https://github.com/laravel/framework/pull/45601))
- Added `fragments()` \ `fragmentIf()` \ `fragmentsIf()` methods to `Illuminate/View/View.php` class ([#45656](https://github.com/laravel/framework/pull/45656), [#45669](https://github.com/laravel/framework/pull/45669))
- Added `incrementEach()` and `decrementEach()` to `Illuminate/Database/Query/Builder` ([#45577](https://github.com/laravel/framework/pull/45577))
- Added ability to drop an index when modifying a column ([#45513](https://github.com/laravel/framework/pull/45513))
- Allow to set HTTP client for mailers ([#45684](https://github.com/laravel/framework/pull/45684))
- Added 402 exception view ([#45682](https://github.com/laravel/framework/pull/45682))
- Added `notFound()` helper to Http Client response ([#45681](https://github.com/laravel/framework/pull/45681))
### Fixed
- Fixed decimal cast ([#45602](https://github.com/laravel/framework/pull/45602))
### Changed
- Ignore whitespaces/newlines when finding relations in model:show command ([#45608](https://github.com/laravel/framework/pull/45608))
- Fail queued job with a string messag ([#45625](https://github.com/laravel/framework/pull/45625))
- Allow fake() helper in unit tests ([#45624](https://github.com/laravel/framework/pull/45624))
- allow egulias/email-validator v4 ([#45649](https://github.com/laravel/framework/pull/45649))
- Force countBy method in EloquentCollection to return base collection ([#45663](https://github.com/laravel/framework/pull/45663))
- Allow for the collection of stubs to be published ([#45653](https://github.com/laravel/framework/pull/45653))
## [v9.47.0](https://github.com/laravel/framework/compare/v9.46.0...v9.47.0) - 2023-01-10

View File

@@ -16,13 +16,19 @@
],
"require": {
"php": "^8.0.2",
"ext-ctype": "*",
"ext-filter": "*",
"ext-hash": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"brick/math": "^0.10.2",
"doctrine/inflector": "^2.0",
"ext-session": "*",
"ext-tokenizer": "*",
"brick/math": "^0.9.3|^0.10.2|^0.11",
"doctrine/inflector": "^2.0.5",
"dragonmantank/cron-expression": "^3.3.2",
"egulias/email-validator": "^3.2.1|^4.0",
"fruitcake/php-cors": "^1.2",
"guzzlehttp/uri-template": "^1.0",
"laravel/serializable-closure": "^1.2.2",
"league/commonmark": "^2.2.1",
"league/flysystem": "^3.8.0",
@@ -83,6 +89,7 @@
"illuminate/view": "self.version"
},
"require-dev": {
"ext-gmp": "*",
"ably/ably-php": "^1.0",
"aws/aws-sdk-php": "^3.235.5",
"doctrine/dbal": "^2.13.3|^3.1.4",
@@ -140,11 +147,13 @@
}
},
"suggest": {
"ext-bcmath": "Required to use the multiple_of validation rule.",
"ext-apcu": "Required to use the APC cache driver.",
"ext-fileinfo": "Required to use the Filesystem class.",
"ext-ftp": "Required to use the Flysystem FTP driver.",
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
"ext-memcached": "Required to use the memcache cache driver.",
"ext-pcntl": "Required to use all features of the queue worker.",
"ext-pcntl": "Required to use all features of the queue worker and console signal trapping.",
"ext-pdo": "Required to use all database features.",
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).",
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",

View File

@@ -15,6 +15,7 @@
],
"require": {
"php": "^8.0.2",
"ext-hash": "*",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/http": "^9.0",

View File

@@ -371,6 +371,6 @@ abstract class Broadcaster implements BroadcasterContract
*/
protected function channelNameMatchesPattern($channel, $pattern)
{
return preg_match('/'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel);
return preg_match('/^'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel);
}
}

View File

@@ -15,7 +15,6 @@
],
"require": {
"php": "^8.0.2",
"ext-json": "*",
"psr/log": "^1.0|^2.0|^3.0",
"illuminate/bus": "^9.0",
"illuminate/collections": "^9.0",
@@ -35,6 +34,7 @@
}
},
"suggest": {
"ext-hash": "Required to use the Ably and Pusher broadcast drivers.",
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0)."
},

View File

@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Cache;
class FileLock extends CacheLock
{
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
return $this->store->add($this->name, $this->owner, $this->seconds);
}
}

View File

@@ -12,7 +12,7 @@ use Illuminate\Support\InteractsWithTime;
class FileStore implements Store, LockProvider
{
use InteractsWithTime, HasCacheLock, RetrievesMultipleKeys;
use InteractsWithTime, RetrievesMultipleKeys;
/**
* The Illuminate Filesystem instance.
@@ -200,6 +200,31 @@ class FileStore implements Store, LockProvider
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.
*

View File

@@ -394,7 +394,9 @@ class Repository implements ArrayAccess, CacheContract
return $value;
}
$this->put($key, $value = $callback(), value($ttl));
$value = $callback();
$this->put($key, $value, value($ttl, $value));
return $value;
}

View File

@@ -34,6 +34,8 @@
}
},
"suggest": {
"ext-apcu": "Required to use the APC cache driver.",
"ext-filter": "Required to use the DynamoDb cache driver.",
"ext-memcached": "Required to use the memcache cache driver.",
"illuminate/database": "Required to use the database cache driver (^9.0).",
"illuminate/filesystem": "Required to use the file cache driver (^9.0).",

View File

@@ -731,6 +731,18 @@ class Arr
return Collection::make($array)->sortBy($callback)->all();
}
/**
* Sort the array in descending order using the given callback or "dot" notation.
*
* @param array $array
* @param callable|array|string|null $callback
* @return array
*/
public static function sortDesc($array, $callback = null)
{
return Collection::make($array)->sortByDesc($callback)->all();
}
/**
* Recursively sort an array by keys and values.
*
@@ -783,6 +795,29 @@ class Arr
return implode(' ', $classes);
}
/**
* Conditionally compile styles from an array into a style list.
*
* @param array $array
* @return string
*/
public static function toCssStyles($array)
{
$styleList = static::wrap($array);
$styles = [];
foreach ($styleList as $class => $constraint) {
if (is_numeric($class)) {
$styles[] = Str::finish($constraint, ';');
} elseif ($constraint) {
$styles[] = Str::finish($class, ';');
}
}
return implode(' ', $styles);
}
/**
* Filter the array using the given callback.
*
@@ -803,9 +838,7 @@ class Arr
*/
public static function whereNotNull($array)
{
return static::where($array, function ($value) {
return ! is_null($value);
});
return static::where($array, fn ($value) => ! is_null($value));
}
/**

View File

@@ -84,11 +84,9 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
{
$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 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
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;
}
/**
@@ -627,6 +621,41 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
return new static(array_intersect($this->items, $this->getArrayableItems($items)));
}
/**
* Intersect the collection with the given items, using the callback.
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @param callable(TValue, TValue): int $callback
* @return static
*/
public function intersectUsing($items, callable $callback)
{
return new static(array_uintersect($this->items, $this->getArrayableItems($items), $callback));
}
/**
* Intersect the collection with the given items with additional index check.
*
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function intersectAssoc($items)
{
return new static(array_intersect_assoc($this->items, $this->getArrayableItems($items)));
}
/**
* Intersect the collection with the given items with additional index check, using the callback.
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @param callable(TValue, TValue): int $callback
* @return static
*/
public function intersectAssocUsing($items, callable $callback)
{
return new static(array_intersect_uassoc($this->items, $this->getArrayableItems($items), $callback));
}
/**
* Intersect the collection with the given items by key.
*
@@ -872,7 +901,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items with the specified keys.
*
* @param \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey>|string $keys
* @param \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey>|string|null $keys
* @return static
*/
public function only($keys)
@@ -1598,13 +1627,9 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*/
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));
}

View File

@@ -51,11 +51,10 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
/**
* Wrap the given value in a collection if applicable.
*
* @template TWrapKey of array-key
* @template TWrapValue
*
* @param iterable<TWrapKey, TWrapValue> $value
* @return static<TWrapKey, TWrapValue>
* @param iterable<array-key, TWrapValue>|TWrapValue $value
* @return static<array-key, TWrapValue>
*/
public static function wrap($value);

View File

@@ -430,9 +430,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
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) {
@@ -632,6 +630,41 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
return $this->passthru('intersect', func_get_args());
}
/**
* Intersect the collection with the given items, using the callback.
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @param callable(TValue, TValue): int $callback
* @return static
*/
public function intersectUsing()
{
return $this->passthru('intersectUsing', func_get_args());
}
/**
* Intersect the collection with the given items with additional index check.
*
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function intersectAssoc($items)
{
return $this->passthru('intersectAssoc', func_get_args());
}
/**
* Intersect the collection with the given items with additional index check, using the callback.
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @param callable(TValue, TValue): int $callback
* @return static
*/
public function intersectAssocUsing($items, callable $callback)
{
return $this->passthru('intersectAssocUsing', func_get_args());
}
/**
* Intersect the collection with the given items by key.
*
@@ -1465,9 +1498,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/** @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));
}
/**
@@ -1636,7 +1667,15 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
return new ArrayIterator($source);
}
return $source();
if (is_callable($source)) {
$maybeTraversable = $source();
return $maybeTraversable instanceof Traversable
? $maybeTraversable
: new ArrayIterator(Arr::wrap($maybeTraversable));
}
return new ArrayIterator((array) $source);
}
/**

View File

@@ -114,11 +114,10 @@ trait EnumeratesValues
/**
* Wrap the given value in a collection if applicable.
*
* @template TWrapKey of array-key
* @template TWrapValue
*
* @param iterable<TWrapKey, TWrapValue> $value
* @return static<TWrapKey, TWrapValue>
* @param iterable<array-key, TWrapValue>|TWrapValue $value
* @return static<array-key, TWrapValue>
*/
public static function wrap($value)
{

View File

@@ -180,7 +180,7 @@ if (! function_exists('value')) {
* Return the default value of the given value.
*
* @param mixed $value
* @param mixed $args
* @param mixed ...$args
* @return mixed
*/
function value($value, ...$args)

View File

@@ -13,7 +13,7 @@ trait Conditionable
* @template TWhenParameter
* @template TWhenReturnType
*
* @param (\Closure($this): TWhenParameter)|TWhenParameter|null $value
* @param (\Closure($this): TWhenParameter)|TWhenParameter|null $value
* @param (callable($this, TWhenParameter): TWhenReturnType)|null $callback
* @param (callable($this, TWhenParameter): TWhenReturnType)|null $default
* @return $this|TWhenReturnType

View File

@@ -16,6 +16,7 @@ class Command extends SymfonyCommand
Concerns\HasParameters,
Concerns\InteractsWithIO,
Concerns\InteractsWithSignals,
Concerns\PromptsForMissingInput,
Macroable;
/**

View File

@@ -0,0 +1,108 @@
<?php
namespace Illuminate\Console\Concerns;
use Illuminate\Contracts\Console\PromptsForMissingInput as PromptsForMissingInputContract;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
trait PromptsForMissingInput
{
/**
* Interact with the user before validating the input.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
parent::interact($input, $output);
if ($this instanceof PromptsForMissingInputContract) {
$this->promptForMissingArguments($input, $output);
}
}
/**
* Prompt the user for any missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function promptForMissingArguments(InputInterface $input, OutputInterface $output)
{
$prompted = collect($this->getDefinition()->getArguments())
->filter(fn ($argument) => $argument->isRequired() && is_null($input->getArgument($argument->getName())))
->filter(fn ($argument) => $argument->getName() !== 'command')
->each(fn ($argument) => $input->setArgument(
$argument->getName(),
$this->askPersistently(
$this->promptForMissingArgumentsUsing()[$argument->getName()] ??
'What is '.lcfirst($argument->getDescription()).'?'
)
))
->isNotEmpty();
if ($prompted) {
$this->afterPromptingForMissingArguments($input, $output);
}
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [];
}
/**
* Perform actions after the user was prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
//
}
/**
* Whether the input contains any options that differ from the default values.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return bool
*/
protected function didReceiveOptions(InputInterface $input)
{
return collect($this->getDefinition()->getOptions())
->reject(fn ($option) => $input->getOption($option->getName()) === $option->getDefault())
->isNotEmpty();
}
/**
* Continue asking a question until an answer is provided.
*
* @param string $question
* @return string
*/
private function askPersistently($question)
{
$answer = null;
while ($answer === null) {
$answer = $this->components->ask($question);
if ($answer === null) {
$this->components->error('The answer is required.');
}
}
return $answer;
}
}

View File

@@ -3,11 +3,13 @@
namespace Illuminate\Console;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Finder\Finder;
abstract class GeneratorCommand extends Command
abstract class GeneratorCommand extends Command implements PromptsForMissingInput
{
/**
* The filesystem instance.
@@ -90,6 +92,7 @@ abstract class GeneratorCommand extends Command
'require',
'require_once',
'return',
'self',
'static',
'switch',
'throw',
@@ -233,6 +236,40 @@ abstract class GeneratorCommand extends Command
: $rootNamespace.$model;
}
/**
* Get a list of possible model names.
*
* @return array<int, string>
*/
protected function possibleModels()
{
$modelPath = is_dir(app_path('Models')) ? app_path('Models') : app_path();
return collect((new Finder)->files()->depth(0)->in($modelPath))
->map(fn ($file) => $file->getBasename('.php'))
->values()
->all();
}
/**
* Get a list of possible event names.
*
* @return array<int, string>
*/
protected function possibleEvents()
{
$eventPath = app_path('Events');
if (! is_dir($eventPath)) {
return [];
}
return collect((new Finder)->files()->depth(0)->in($eventPath))
->map(fn ($file) => $file->getBasename('.php'))
->values()
->all();
}
/**
* Get the default namespace for the class.
*
@@ -436,7 +473,19 @@ abstract class GeneratorCommand extends Command
protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the class'],
['name', InputArgument::REQUIRED, 'The name of the '.strtolower($this->type)],
];
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => 'What should the '.strtolower($this->type).' be named?',
];
}
}

View File

@@ -46,6 +46,11 @@ class QuestionHelper extends SymfonyQuestionHelper
$choices = $question->getChoices();
$text = sprintf('<info>%s</info> [<comment>%s</comment>]', $text, OutputFormatter::escape($choices[$default] ?? $default));
break;
default:
$text = sprintf('<info>%s</info> [<comment>%s</comment>]', $text, OutputFormatter::escape($default));
break;
}

View File

@@ -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 @@ class CacheEventMutex implements EventMutex, CacheAware
*/
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 @@ class CacheEventMutex implements EventMutex, CacheAware
*/
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 @@ class CacheEventMutex implements EventMutex, CacheAware
*/
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());
}

View File

@@ -129,6 +129,8 @@ class CallbackEvent extends Event
/**
* Do not allow the event to overlap each other.
*
* The expiration time of the underlying cache lock may be specified in minutes.
*
* @param int $expiresAt
* @return $this
*

View File

@@ -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
*/
@@ -659,6 +659,8 @@ class Event
/**
* Do not allow the event to overlap each other.
*
* The expiration time of the underlying cache lock may be specified in minutes.
*
* @param int $expiresAt
* @return $this
*/

View File

@@ -4,18 +4,20 @@ namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\ProcessUtils;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
#[AsCommand(name: 'schedule:work')]
class ScheduleWorkCommand extends Command
{
/**
* The console command name.
* The name and signature of the console command.
*
* @var string
*/
protected $name = 'schedule:work';
protected $signature = 'schedule:work {--run-output-file= : The file to direct <info>schedule:run</info> output to}';
/**
* The name of the console command.
@@ -42,20 +44,29 @@ class ScheduleWorkCommand extends Command
*/
public function handle()
{
$this->components->info('Running schedule tasks every minute.');
$this->components->info(
'Running scheduled tasks every minute.',
$this->getLaravel()->isLocal() ? OutputInterface::VERBOSITY_NORMAL : OutputInterface::VERBOSITY_VERBOSE
);
[$lastExecutionStartedAt, $executions] = [null, []];
$command = implode(' ', array_map(fn ($arg) => ProcessUtils::escapeArgument($arg), [
PHP_BINARY,
defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan',
'schedule:run',
]));
if ($this->option('run-output-file')) {
$command .= ' >> '.ProcessUtils::escapeArgument($this->option('run-output-file')).' 2>&1';
}
while (true) {
usleep(100 * 1000);
if (Carbon::now()->second === 0 &&
! Carbon::now()->startOfMinute()->equalTo($lastExecutionStartedAt)) {
$executions[] = $execution = new Process([
PHP_BINARY,
defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan',
'schedule:run',
]);
$executions[] = $execution = Process::fromShellCommandline($command);
$execution->start();
@@ -64,7 +75,7 @@ class ScheduleWorkCommand extends Command
foreach ($executions as $key => $execution) {
$output = $execution->getIncrementalOutput().
$execution->getIncrementalErrorOutput();
$execution->getIncrementalErrorOutput();
$this->output->write(ltrim($output, "\n"));

View File

@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Console\View\Components;
class Ask extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $question
* @param string $default
* @return mixed
*/
public function render($question, $default = null)
{
return $this->usingQuestionHelper(fn () => $this->output->ask($question, $default));
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Question\Question;
class AskWithCompletion extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $question
* @param array|callable $choices
* @param string $default
* @return mixed
*/
public function render($question, $choices, $default = null)
{
$question = new Question($question, $default);
is_callable($choices)
? $question->setAutocompleterCallback($choices)
: $question->setAutocompleterValues($choices);
return $this->usingQuestionHelper(
fn () => $this->output->askQuestion($question)
);
}
}

View File

@@ -12,13 +12,17 @@ class Choice extends Component
* @param string $question
* @param array<array-key, string> $choices
* @param mixed $default
* @param int $attempts
* @param bool $multiple
* @return mixed
*/
public function render($question, $choices, $default = null)
public function render($question, $choices, $default = null, $attempts = null, $multiple = false)
{
return $this->usingQuestionHelper(
fn () => $this->output->askQuestion(
new ChoiceQuestion($question, $choices, $default)
(new ChoiceQuestion($question, $choices, $default))
->setMaxAttempts($attempts)
->setMultiselect($multiple)
),
);
}

View File

@@ -6,8 +6,10 @@ use InvalidArgumentException;
/**
* @method void alert(string $string, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method mixed ask(string $question, string $default = null)
* @method mixed askWithCompletion(string $question, array|callable $choices, string $default = null)
* @method void bulletList(array $elements, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method mixed choice(string $question, array $choices, $default = null)
* @method mixed choice(string $question, array $choices, $default = null, int $attempts = null, bool $multiple = false)
* @method bool confirm(string $question, bool $default = false)
* @method void error(string $string, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method void info(string $string, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)

View File

@@ -15,6 +15,7 @@
],
"require": {
"php": "^8.0.2",
"ext-mbstring": "*",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0",
@@ -35,6 +36,7 @@
}
},
"suggest": {
"ext-pcntl": "Required to use signal trapping.",
"dragonmantank/cron-expression": "Required to use scheduler (^3.3.2).",
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (^7.5).",
"illuminate/bus": "Required to use the scheduled job dispatcher (^9.0).",

View File

@@ -631,9 +631,7 @@ class Container implements ArrayAccess, ContainerContract
*/
public function wrap(Closure $callback, array $parameters = [])
{
return function () use ($callback, $parameters) {
return $this->call($callback, $parameters);
};
return fn () => $this->call($callback, $parameters);
}
/**
@@ -677,9 +675,7 @@ class Container implements ArrayAccess, ContainerContract
*/
public function factory($abstract)
{
return function () use ($abstract) {
return $this->make($abstract);
};
return fn () => $this->make($abstract);
}
/**
@@ -1083,9 +1079,7 @@ class Container implements ArrayAccess, ContainerContract
return $this->make($className);
}
return array_map(function ($abstract) {
return $this->resolve($abstract);
}, $concrete);
return array_map(fn ($abstract) => $this->resolve($abstract), $concrete);
}
/**
@@ -1446,9 +1440,7 @@ class Container implements ArrayAccess, ContainerContract
*/
public function offsetSet($key, $value): void
{
$this->bind($key, $value instanceof Closure ? $value : function () use ($value) {
return $value;
});
$this->bind($key, $value instanceof Closure ? $value : fn () => $value);
}
/**

View File

@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Contracts\Console;
interface PromptsForMissingInput
{
//
}

View File

@@ -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);

View File

@@ -8,8 +8,7 @@ interface Castable
* Get the name of the caster class to use when casting from / to this cast target.
*
* @param array $arguments
* @return string
* @return string|\Illuminate\Contracts\Database\Eloquent\CastsAttributes|\Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes
* @return class-string<CastsAttributes|CastsInboundAttributes>|CastsAttributes|CastsInboundAttributes
*/
public static function castUsing(array $arguments);
}

View File

@@ -2,6 +2,10 @@
namespace Illuminate\Contracts\Database\Eloquent;
/**
* @template TGet
* @template TSet
*/
interface CastsAttributes
{
/**
@@ -11,7 +15,7 @@ interface CastsAttributes
* @param string $key
* @param mixed $value
* @param array $attributes
* @return mixed
* @return TGet|null
*/
public function get($model, string $key, $value, array $attributes);
@@ -20,7 +24,7 @@ interface CastsAttributes
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param TSet|null $value
* @param array $attributes
* @return mixed
*/

View File

@@ -64,7 +64,7 @@ interface Application extends Container
/**
* Get or check the current application environment.
*
* @param string|array $environments
* @param string|array ...$environments
* @return string|bool
*/
public function environment(...$environments);

View File

@@ -22,7 +22,7 @@ class EncryptCookies
/**
* The names of the cookies that should not be encrypted.
*
* @var array
* @var array<int, string>
*/
protected $except = [];

View File

@@ -15,6 +15,7 @@
],
"require": {
"php": "^8.0.2",
"ext-hash": "*",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0",

View File

@@ -290,7 +290,11 @@ trait ManagesTransactions
protected function performRollBack($toLevel)
{
if ($toLevel == 0) {
$this->getPdo()->rollBack();
$pdo = $this->getPdo();
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
} elseif ($this->queryGrammar->supportsSavepoints()) {
$this->getPdo()->exec(
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))

View File

@@ -94,7 +94,7 @@ class DumpCommand extends Command
*/
protected function path(Connection $connection)
{
return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.dump'), function ($path) {
return tap($this->option('path') ?: database_path('schema/'.$connection->getName().'-schema.sql'), function ($path) {
(new Filesystem)->ensureDirectoryExists(dirname($path));
});
}

View File

@@ -2,11 +2,12 @@
namespace Illuminate\Database\Console\Migrations;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Database\Migrations\MigrationCreator;
use Illuminate\Support\Composer;
use Illuminate\Support\Str;
class MigrateMakeCommand extends BaseCommand
class MigrateMakeCommand extends BaseCommand implements PromptsForMissingInput
{
/**
* The console command signature.
@@ -128,4 +129,16 @@ class MigrateMakeCommand extends BaseCommand
return parent::getMigrationPath();
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => 'What should the migration be named?',
];
}
}

View File

@@ -65,9 +65,13 @@ class StatusCommand extends BaseCommand
$this->components->twoColumnDetail('<fg=gray>Migration name</>', '<fg=gray>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 @@ class StatusCommand extends BaseCommand
{
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'],
];
}

View File

@@ -4,6 +4,7 @@ namespace Illuminate\Database;
use Doctrine\DBAL\Types\Type;
use Illuminate\Database\Connectors\ConnectionFactory;
use Illuminate\Database\Events\ConnectionEstablished;
use Illuminate\Support\Arr;
use Illuminate\Support\ConfigurationUrlParser;
use Illuminate\Support\Str;
@@ -99,6 +100,12 @@ class DatabaseManager implements ConnectionResolverInterface
$this->connections[$name] = $this->configure(
$this->makeConnection($database), $type
);
if ($this->app->bound('events')) {
$this->app['events']->dispatch(
new ConnectionEstablished($this->connections[$name])
);
}
}
return $this->connections[$name];

View File

@@ -849,7 +849,7 @@ class Builder implements BuilderContract
}
/**
* Get an array with the values of a given column.
* Get a collection with the values of a given column.
*
* @param string|\Illuminate\Database\Query\Expression $column
* @param string|null $key

View File

@@ -6,6 +6,11 @@ use ArrayObject as BaseArrayObject;
use Illuminate\Contracts\Support\Arrayable;
use JsonSerializable;
/**
* @template TKey of array-key
* @template TItem
* @extends \ArrayObject<TKey, TItem>
*/
class ArrayObject extends BaseArrayObject implements Arrayable, JsonSerializable
{
/**

View File

@@ -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 object|string
* @return CastsAttributes<ArrayObject<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{

View File

@@ -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 object|string
* @return CastsAttributes<\Illuminate\Support\Collection<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{

View File

@@ -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 object|string
* @return CastsAttributes<ArrayObject<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{

View File

@@ -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 object|string
* @return CastsAttributes<\Illuminate\Support\Collection<array-key, mixed>, iterable>
*/
public static function castUsing(array $arguments)
{

View File

@@ -0,0 +1,84 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class AsEnumArrayObject implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum
*
* @param array{class-string<TEnum>} $arguments
* @return CastsAttributes<ArrayObject<array-key, TEnum>, iterable<TEnum>>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected $arguments;
public function __construct(array $arguments)
{
$this->arguments = $arguments;
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key]) || is_null($attributes[$key])) {
return;
}
$data = json_decode($attributes[$key], true);
if (! is_array($data)) {
return;
}
$enumClass = $this->arguments[0];
return new ArrayObject((new Collection($data))->map(function ($value) use ($enumClass) {
return is_subclass_of($enumClass, BackedEnum::class)
? $enumClass::from($value)
: constant($enumClass.'::'.$value);
})->toArray());
}
public function set($model, $key, $value, $attributes)
{
if ($value === null) {
return [$key => null];
}
$storable = [];
foreach ($value as $enum) {
$storable[] = $this->getStorableEnumValue($enum);
}
return [$key => json_encode($storable)];
}
public function serialize($model, string $key, $value, array $attributes)
{
return (new Collection($value->getArrayCopy()))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return $enum instanceof BackedEnum ? $enum->value : $enum->name;
}
};
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Illuminate\Database\Eloquent\Casts;
use BackedEnum;
use Illuminate\Contracts\Database\Eloquent\Castable;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
use Illuminate\Support\Collection;
class AsEnumCollection implements Castable
{
/**
* Get the caster class to use when casting from / to this cast target.
*
* @template TEnum of \UnitEnum|\BackedEnum
*
* @param array{class-string<TEnum>} $arguments
* @return CastsAttributes<Collection<array-key, TEnum>, iterable<TEnum>>
*/
public static function castUsing(array $arguments)
{
return new class($arguments) implements CastsAttributes
{
protected $arguments;
public function __construct(array $arguments)
{
$this->arguments = $arguments;
}
public function get($model, $key, $value, $attributes)
{
if (! isset($attributes[$key]) || is_null($attributes[$key])) {
return;
}
$data = json_decode($attributes[$key], true);
if (! is_array($data)) {
return;
}
$enumClass = $this->arguments[0];
return (new Collection($data))->map(function ($value) use ($enumClass) {
return is_subclass_of($enumClass, BackedEnum::class)
? $enumClass::from($value)
: constant($enumClass.'::'.$value);
});
}
public function set($model, $key, $value, $attributes)
{
$value = $value !== null
? (new Collection($value))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->toJson()
: null;
return [$key => $value];
}
public function serialize($model, string $key, $value, array $attributes)
{
return (new Collection($value))->map(function ($enum) {
return $this->getStorableEnumValue($enum);
})->toArray();
}
protected function getStorableEnumValue($enum)
{
if (is_string($enum) || is_int($enum)) {
return $enum;
}
return $enum instanceof BackedEnum ? $enum->value : $enum->name;
}
};
}
}

View File

@@ -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 object|string
* @return CastsAttributes<\Illuminate\Support\Stringable, string|\Stringable>
*/
public static function castUsing(array $arguments)
{

View File

@@ -24,7 +24,7 @@ class Collection extends BaseCollection implements QueueableCollection
*
* @param mixed $key
* @param TFindDefault $default
* @return static<TKey|TModel>|TModel|TFindDefault
* @return static<TKey, TModel>|TModel|TFindDefault
*/
public function find($key, $default = null)
{
@@ -44,9 +44,7 @@ class Collection extends BaseCollection implements QueueableCollection
return $this->whereIn($this->first()->getKeyName(), $key);
}
return Arr::first($this->items, function ($model) use ($key) {
return $model->getKey() == $key;
}, $default);
return Arr::first($this->items, fn ($model) => $model->getKey() == $key, $default);
}
/**
@@ -233,9 +231,7 @@ class Collection extends BaseCollection implements QueueableCollection
$relation = reset($relation);
}
$models->filter(function ($model) use ($name) {
return ! is_null($model) && ! $model->relationLoaded($name);
})->load($relation);
$models->filter(fn ($model) => ! is_null($model) && ! $model->relationLoaded($name))->load($relation);
if (empty($path)) {
return;
@@ -261,12 +257,8 @@ class Collection extends BaseCollection implements QueueableCollection
{
$this->pluck($relation)
->filter()
->groupBy(function ($model) {
return get_class($model);
})
->each(function ($models, $className) use ($relations) {
static::make($models)->load($relations[$className] ?? []);
});
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->load($relations[$className] ?? []));
return $this;
}
@@ -282,12 +274,8 @@ class Collection extends BaseCollection implements QueueableCollection
{
$this->pluck($relation)
->filter()
->groupBy(function ($model) {
return get_class($model);
})
->each(function ($models, $className) use ($relations) {
static::make($models)->loadCount($relations[$className] ?? []);
});
->groupBy(fn ($model) => get_class($model))
->each(fn ($models, $className) => static::make($models)->loadCount($relations[$className] ?? []));
return $this;
}
@@ -307,14 +295,10 @@ class Collection extends BaseCollection implements QueueableCollection
}
if ($key instanceof Model) {
return parent::contains(function ($model) use ($key) {
return $model->is($key);
});
return parent::contains(fn ($model) => $model->is($key));
}
return parent::contains(function ($model) use ($key) {
return $model->getKey() == $key;
});
return parent::contains(fn ($model) => $model->getKey() == $key);
}
/**
@@ -324,9 +308,7 @@ class Collection extends BaseCollection implements QueueableCollection
*/
public function modelKeys()
{
return array_map(function ($model) {
return $model->getKey();
}, $this->items);
return array_map(fn ($model) => $model->getKey(), $this->items);
}
/**
@@ -358,9 +340,7 @@ class Collection extends BaseCollection implements QueueableCollection
{
$result = parent::map($callback);
return $result->contains(function ($item) {
return ! $item instanceof Model;
}) ? $result->toBase() : $result;
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
@@ -378,9 +358,7 @@ class Collection extends BaseCollection implements QueueableCollection
{
$result = parent::mapWithKeys($callback);
return $result->contains(function ($item) {
return ! $item instanceof Model;
}) ? $result->toBase() : $result;
return $result->contains(fn ($item) => ! $item instanceof Model) ? $result->toBase() : $result;
}
/**
@@ -403,12 +381,8 @@ class Collection extends BaseCollection implements QueueableCollection
->get()
->getDictionary();
return $this->filter(function ($model) use ($freshModels) {
return $model->exists && isset($freshModels[$model->getKey()]);
})
->map(function ($model) use ($freshModels) {
return $freshModels[$model->getKey()];
});
return $this->filter(fn ($model) => $model->exists && isset($freshModels[$model->getKey()]))
->map(fn ($model) => $freshModels[$model->getKey()]);
}
/**
@@ -584,7 +558,7 @@ class Collection extends BaseCollection implements QueueableCollection
/**
* Count the number of items in the collection by a field or using a callback.
*
* @param (callable(TValue, TKey): array-key)|string|null $countBy
* @param (callable(TModel, TKey): array-key)|string|null $countBy
* @return \Illuminate\Support\Collection<array-key, int>
*/
public function countBy($countBy = null)
@@ -676,13 +650,11 @@ class Collection extends BaseCollection implements QueueableCollection
* Get the comparison function to detect duplicates.
*
* @param bool $strict
* @return callable(TValue, TValue): bool
* @return callable(TModel, TModel): bool
*/
protected function duplicateComparator($strict)
{
return function ($a, $b) {
return $a->is($b);
};
return fn ($a, $b) => $a->is($b);
}
/**
@@ -801,9 +773,7 @@ class Collection extends BaseCollection implements QueueableCollection
$class = get_class($model);
if ($this->filter(function ($model) use ($class) {
return ! $model instanceof $class;
})->isNotEmpty()) {
if ($this->filter(fn ($model) => ! $model instanceof $class)->isNotEmpty()) {
throw new LogicException('Unable to create query for collection with mixed types.');
}

View File

@@ -98,7 +98,7 @@ trait HasEvents
[
'retrieved', 'creating', 'created', 'updating', 'updated',
'saving', 'saved', 'restoring', 'restored', 'replicating',
'deleting', 'deleted', 'forceDeleted',
'deleting', 'deleted', 'forceDeleting', 'forceDeleted',
],
$this->observables
);

View File

@@ -7,6 +7,7 @@ use Illuminate\Database\ClassMorphViolationException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\PendingHasThroughRelationship;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -359,6 +360,21 @@ trait HasRelationships
return $caller['function'];
}
/**
* Create a pending has-many-through or has-one-through relationship.
*
* @param string|\Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\HasOne $relationship
* @return \Illuminate\Database\Eloquent\PendingHasThroughRelationship
*/
public function through($relationship)
{
if (is_string($relationship)) {
$relationship = $this->{$relationship}();
}
return new PendingHasThroughRelationship($this, $relationship);
}
/**
* Define a one-to-many relationship.
*

View File

@@ -9,7 +9,7 @@ class CrossJoinSequence extends Sequence
/**
* Create a new cross join sequence instance.
*
* @param array $sequences
* @param array ...$sequences
* @return void
*/
public function __construct(...$sequences)

View File

@@ -525,7 +525,7 @@ abstract class Factory
/**
* Add a new sequenced state transformation to the model definition.
*
* @param array $sequence
* @param mixed ...$sequence
* @return static
*/
public function sequence(...$sequence)
@@ -536,7 +536,7 @@ abstract class Factory
/**
* Add a new sequenced state transformation to the model definition and update the pending creation count to the size of the sequence.
*
* @param array $sequence
* @param array ...$sequence
* @return static
*/
public function forEachSequence(...$sequence)
@@ -547,7 +547,7 @@ abstract class Factory
/**
* Add a new cross joined sequenced state transformation to the model definition.
*
* @param array $sequence
* @param array ...$sequence
* @return static
*/
public function crossJoinSequence(...$sequence)

View File

@@ -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)

View File

@@ -1083,6 +1083,16 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
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.
*
@@ -2312,6 +2322,11 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt
return $resolver($this);
}
if (Str::startsWith($method, 'through') &&
method_exists($this, $relationMethod = Str::of($method)->after('through')->lcfirst()->toString())) {
return $this->through($relationMethod);
}
return $this->forwardCallTo($this->newQuery(), $method, $parameters);
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Illuminate\Database\Eloquent;
use BadMethodCallException;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class PendingHasThroughRelationship
{
/**
* The root model that the relationship exists on.
*
* @var \Illuminate\Database\Eloquent\Model
*/
protected $rootModel;
/**
* The local relationship.
*
* @var \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\HasOne
*/
protected $localRelationship;
/**
* Create a pending has-many-through or has-one-through relationship.
*
* @param \Illuminate\Database\Eloquent\Model $rootModel
* @param \Illuminate\Database\Eloquent\Relations\HasMany|\Illuminate\Database\Eloquent\Relations\HasOne $localRelationship
*/
public function __construct($rootModel, $localRelationship)
{
$this->rootModel = $rootModel;
$this->localRelationship = $localRelationship;
}
/**
* Define the distant relationship that this model has.
*
* @param string|(callable(\Illuminate\Database\Eloquent\Model): (\Illuminate\Database\Eloquent\Relations\HasOne|\Illuminate\Database\Eloquent\Relations\HasMany)) $callback
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough|\Illuminate\Database\Eloquent\Relations\HasOneThrough
*/
public function has($callback)
{
if (is_string($callback)) {
$callback = fn () => $this->localRelationship->getRelated()->{$callback}();
}
$distantRelation = $callback($this->localRelationship->getRelated());
if ($distantRelation instanceof HasMany) {
return $this->rootModel->hasManyThrough(
$distantRelation->getRelated()::class,
$this->localRelationship->getRelated()::class,
$this->localRelationship->getForeignKeyName(),
$distantRelation->getForeignKeyName(),
$this->localRelationship->getLocalKeyName(),
$distantRelation->getLocalKeyName(),
);
}
return $this->rootModel->hasOneThrough(
$distantRelation->getRelated()::class,
$this->localRelationship->getRelated()::class,
$this->localRelationship->getForeignKeyName(),
$distantRelation->getForeignKeyName(),
$this->localRelationship->getLocalKeyName(),
$distantRelation->getLocalKeyName(),
);
}
/**
* Handle dynamic method calls into the model.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (Str::startsWith($method, 'has')) {
return $this->has(Str::of($method)->after('has')->lcfirst()->toString());
}
throw new BadMethodCallException(sprintf(
'Call to undefined method %s::%s()', static::class, $method
));
}
}

View File

@@ -688,8 +688,8 @@ class BelongsToMany extends Relation
return $this->getRelated()->newCollection();
}
return $this->whereIn(
$this->getRelated()->getQualifiedKeyName(), $this->parseIds($ids)
return $this->whereKey(
$this->parseIds($ids)
)->get($columns);
}
@@ -1153,8 +1153,6 @@ class BelongsToMany extends Relation
*/
public function touch()
{
$key = $this->getRelated()->getKeyName();
$columns = [
$this->related->getUpdatedAtColumn() => $this->related->freshTimestampString(),
];
@@ -1163,7 +1161,7 @@ class BelongsToMany extends Relation
// the related model's timestamps, to make sure these all reflect the changes
// to the parent models. This will help us keep any caching synced up here.
if (count($ids = $this->allRelatedIds()) > 0) {
$this->getRelated()->newQueryWithoutRelationships()->whereIn($key, $ids)->update($columns);
$this->getRelated()->newQueryWithoutRelationships()->whereKey($ids)->update($columns);
}
}

View File

@@ -323,6 +323,17 @@ abstract class HasOneOrMany extends Relation
});
}
/**
* Create a new instance of the related model without raising any events to the parent model.
*
* @param array $attributes
* @return \Illuminate\Database\Eloquent\Model
*/
public function createQuietly(array $attributes = [])
{
return Model::withoutEvents(fn () => $this->create($attributes));
}
/**
* Create a new instance of the related model. Allow mass-assignment.
*
@@ -353,6 +364,17 @@ abstract class HasOneOrMany extends Relation
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.
*

View File

@@ -45,6 +45,10 @@ trait SoftDeletes
*/
public function forceDelete()
{
if ($this->fireModelEvent('forceDeleting') === false) {
return false;
}
$this->forceDeleting = true;
return tap($this->delete(), function ($deleted) {
@@ -56,6 +60,16 @@ trait SoftDeletes
});
}
/**
* Force a hard delete on a soft deleted model without raising any events.
*
* @return bool|null
*/
public function forceDeleteQuietly()
{
return static::withoutEvents(fn () => $this->forceDelete());
}
/**
* Perform the actual delete query on this model instance.
*
@@ -181,6 +195,17 @@ trait SoftDeletes
static::registerModelEvent('restored', $callback);
}
/**
* Register a "forceDeleting" model event callback with the dispatcher.
*
* @param \Closure|string $callback
* @return void
*/
public static function forceDeleting($callback)
{
static::registerModelEvent('forceDeleting', $callback);
}
/**
* Register a "forceDeleted" model event callback with the dispatcher.
*

View File

@@ -9,7 +9,7 @@ class SoftDeletingScope implements Scope
*
* @var string[]
*/
protected $extensions = ['Restore', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed'];
protected $extensions = ['Restore', 'RestoreOrCreate', 'WithTrashed', 'WithoutTrashed', 'OnlyTrashed'];
/**
* Apply the scope to a given Eloquent query builder.
@@ -74,6 +74,23 @@ class SoftDeletingScope implements Scope
});
}
/**
* Add the restore-or-create extension to the builder.
*
* @param \Illuminate\Database\Eloquent\Builder $builder
* @return void
*/
protected function addRestoreOrCreate(Builder $builder)
{
$builder->macro('restoreOrCreate', function (Builder $builder, array $attributes = [], array $values = []) {
$builder->withTrashed();
return tap($builder->firstOrCreate($attributes, $values), function ($instance) {
$instance->restore();
});
});
}
/**
* Add the with-trashed extension to the builder.
*

View File

@@ -0,0 +1,8 @@
<?php
namespace Illuminate\Database\Events;
class ConnectionEstablished extends ConnectionEvent
{
//
}

View File

@@ -66,6 +66,13 @@ class Migrator
*/
protected $paths = [];
/**
* The paths that have already been required.
*
* @var array<string, \Illuminate\Database\Migrations\Migration|null>
*/
protected static $requiredPathCache = [];
/**
* The output interface implementation.
*
@@ -511,9 +518,13 @@ class Migrator
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;
}
/**
@@ -729,7 +740,7 @@ class Migrator
* Write to the console's output.
*
* @param string $component
* @param array<int, string>|string $arguments
* @param array<int, string>|string ...$arguments
* @return void
*/
protected function write($component, ...$arguments)

View File

@@ -100,6 +100,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.
*
@@ -411,6 +418,10 @@ class Builder implements BuilderContract
$this->selectSub($column, $as);
} else {
if (is_array($this->columns) && in_array($column, $this->columns, true)) {
continue;
}
$this->columns[] = $column;
}
}
@@ -454,6 +465,45 @@ class Builder implements BuilderContract
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.
*
@@ -2498,7 +2548,7 @@ class Builder implements BuilderContract
/**
* Lock the selected rows in the table for updating.
*
* @return \Illuminate\Database\Query\Builder
* @return $this
*/
public function lockForUpdate()
{
@@ -2508,7 +2558,7 @@ class Builder implements BuilderContract
/**
* Share lock the selected rows in the table.
*
* @return \Illuminate\Database\Query\Builder
* @return $this
*/
public function sharedLock()
{

View File

@@ -36,6 +36,7 @@ class Grammar extends BaseGrammar
'aggregate',
'columns',
'from',
'indexHint',
'joins',
'wheres',
'groups',

View File

@@ -74,6 +74,22 @@ class MySqlGrammar extends Grammar
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.
*

View File

@@ -117,6 +117,20 @@ class SQLiteGrammar extends Grammar
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.
*

View File

@@ -96,6 +96,20 @@ class SqlServerGrammar extends Grammar
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}
*

View File

@@ -0,0 +1,33 @@
<?php
namespace Illuminate\Database\Query;
class IndexHint
{
/**
* The type of query hint.
*
* @var string
*/
public $type;
/**
* The name of the index.
*
* @var string
*/
public $index;
/**
* Create a new index hint instance.
*
* @param string $type
* @param string $index
* @return void
*/
public function __construct($type, $index)
{
$this->type = $type;
$this->index = $index;
}
}

View File

@@ -63,4 +63,14 @@ class ForeignKeyDefinition extends Fluent
{
return $this->onDelete('set null');
}
/**
* Indicate that deletes should have "no action".
*
* @return $this
*/
public function noActionOnDelete()
{
return $this->onDelete('no action');
}
}

View File

@@ -15,19 +15,16 @@ class PostgresSchemaState extends SchemaState
*/
public function dump(Connection $connection, $path)
{
$excludedTables = collect($connection->getSchemaBuilder()->getAllTables())
->map->tablename
->reject(function ($table) {
return $table === $this->migrationTable;
})->map(function ($table) {
return '--exclude-table-data="*.'.$table.'"';
})->implode(' ');
$commands = collect([
$this->baseDumpCommand().' --schema-only > '.$path,
$this->baseDumpCommand().' -t '.$this->migrationTable.' --data-only >> '.$path,
]);
$this->makeProcess(
$this->baseDumpCommand().' --file="${:LARAVEL_LOAD_PATH}" '.$excludedTables
)->mustRun($this->output, array_merge($this->baseVariables($this->connection->getConfig()), [
'LARAVEL_LOAD_PATH' => $path,
]));
$commands->map(function ($command, $path) {
$this->makeProcess($command)->mustRun($this->output, array_merge($this->baseVariables($this->connection->getConfig()), [
'LARAVEL_LOAD_PATH' => $path,
]));
});
}
/**
@@ -58,7 +55,7 @@ class PostgresSchemaState extends SchemaState
*/
protected function baseDumpCommand()
{
return 'pg_dump --no-owner --no-acl -Fc --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --username="${:LARAVEL_LOAD_USER}" --dbname="${:LARAVEL_LOAD_DATABASE}"';
return 'pg_dump --no-owner --no-acl --host="${:LARAVEL_LOAD_HOST}" --port="${:LARAVEL_LOAD_PORT}" --username="${:LARAVEL_LOAD_USER}" --dbname="${:LARAVEL_LOAD_DATABASE}"';
}
/**

View File

@@ -86,7 +86,7 @@ abstract class SchemaState
/**
* Create a new process instance.
*
* @param array $arguments
* @param mixed ...$arguments
* @return \Symfony\Component\Process\Process
*/
public function makeProcess(...$arguments)

View File

@@ -16,7 +16,8 @@
],
"require": {
"php": "^8.0.2",
"ext-json": "*",
"ext-pdo": "*",
"brick/math": "^0.9.3|^0.10.2|^0.11",
"illuminate/collections": "^9.0",
"illuminate/container": "^9.0",
"illuminate/contracts": "^9.0",
@@ -35,6 +36,7 @@
}
},
"suggest": {
"ext-filter": "Required to use the Postgres database driver.",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.13.3|^3.1.4).",
"fakerphp/faker": "Required to use the eloquent factory builder (^1.21).",
"illuminate/console": "Required to use the database commands (^9.0).",

View File

@@ -112,7 +112,7 @@ class Encrypter implements EncrypterContract, StringEncrypter
$tag = base64_encode($tag ?? '');
$mac = self::$supportedCiphers[strtolower($this->cipher)]['aead']
? '' // For AEAD-algoritms, the tag / MAC is returned by openssl_encrypt...
? '' // For AEAD-algorithms, the tag / MAC is returned by openssl_encrypt...
: $this->hash($iv, $value);
$json = json_encode(compact('iv', 'value', 'mac', 'tag'), JSON_UNESCAPED_SLASHES);

View File

@@ -15,7 +15,7 @@
],
"require": {
"php": "^8.0.2",
"ext-json": "*",
"ext-hash": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"illuminate/contracts": "^9.0",

View File

@@ -95,6 +95,40 @@ class AwsS3V3Adapter extends FilesystemAdapter
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.
*

View File

@@ -195,9 +195,10 @@ class Filesystem
*
* @param string $path
* @param string $content
* @param int|null $mode
* @return void
*/
public function replace($path, $content)
public function replace($path, $content, $mode = null)
{
// If the path already exists and is a symlink, get the real path...
clearstatcache(true, $path);
@@ -207,7 +208,11 @@ class Filesystem
$tempPath = tempnam(dirname($path), basename($path));
// Fix permissions of tempPath because `tempnam()` creates it with permissions set to 0600...
chmod($tempPath, 0777 - umask());
if (! is_null($mode)) {
chmod($tempPath, $mode);
} else {
chmod($tempPath, 0777 - umask());
}
file_put_contents($tempPath, $content);

View File

@@ -733,6 +733,25 @@ class FilesystemAdapter implements CloudFilesystemContract
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.
*

View File

@@ -32,7 +32,9 @@
}
},
"suggest": {
"ext-fileinfo": "Required to use the Filesystem class.",
"ext-ftp": "Required to use the Flysystem FTP driver.",
"ext-hash": "Required to use the Filesystem class.",
"illuminate/http": "Required for handling uploaded files (^7.0).",
"league/flysystem": "Required to use the Flysystem local driver (^3.0.16).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).",

View File

@@ -38,7 +38,7 @@ class Application extends Container implements ApplicationContract, CachesConfig
*
* @var string
*/
const VERSION = '9.48.0';
const VERSION = '9.52.0';
/**
* The base path for the Laravel installation.
@@ -567,7 +567,7 @@ class Application extends Container implements ApplicationContract, CachesConfig
/**
* Get or check the current application environment.
*
* @param string|array $environments
* @param string|array ...$environments
* @return string|bool
*/
public function environment(...$environments)
@@ -656,9 +656,7 @@ class Application extends Container implements ApplicationContract, CachesConfig
public function registerConfiguredProviders()
{
$providers = Collection::make($this->make('config')->get('app.providers'))
->partition(function ($provider) {
return str_starts_with($provider, 'Illuminate\\');
});
->partition(fn ($provider) => str_starts_with($provider, 'Illuminate\\'));
$providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);
@@ -738,9 +736,7 @@ class Application extends Container implements ApplicationContract, CachesConfig
{
$name = is_string($provider) ? $provider : get_class($provider);
return Arr::where($this->serviceProviders, function ($value) use ($name) {
return $value instanceof $name;
});
return Arr::where($this->serviceProviders, fn ($value) => $value instanceof $name);
}
/**

View File

@@ -11,6 +11,7 @@ trait Dispatchable
/**
* Dispatch the job with the given arguments.
*
* @param mixed ...$arguments
* @return \Illuminate\Foundation\Bus\PendingDispatch
*/
public static function dispatch(...$arguments)
@@ -67,6 +68,7 @@ trait Dispatchable
*
* Queueable jobs will be dispatched to the "sync" queue.
*
* @param mixed ...$arguments
* @return mixed
*/
public static function dispatchSync(...$arguments)
@@ -89,6 +91,7 @@ trait Dispatchable
/**
* Dispatch a command to its appropriate handler after the current process.
*
* @param mixed ...$arguments
* @return mixed
*/
public static function dispatchAfterResponse(...$arguments)

View File

@@ -185,7 +185,7 @@ class Kernel implements KernelContract
}
/**
* Register a callback to be invoked when the command lifecyle duration exceeds a given amount of time.
* Register a callback to be invoked when the command lifecycle duration exceeds a given amount of time.
*
* @param \DateTimeInterface|\Carbon\CarbonInterval|float|int $threshold
* @param callable $handler

View File

@@ -6,7 +6,9 @@ use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'make:listener')]
class ListenerMakeCommand extends GeneratorCommand
@@ -125,4 +127,28 @@ class ListenerMakeCommand extends GeneratorCommand
['queued', null, InputOption::VALUE_NONE, 'Indicates the event listener should be queued'],
];
}
/**
* Interact further with the user if they were prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) {
return;
}
$event = $this->components->askWithCompletion(
'What event should be listened for?',
$this->possibleEvents(),
'none'
);
if ($event && $event !== 'none') {
$input->setOption('event', $event);
}
}
}

View File

@@ -43,7 +43,7 @@ class MailMakeCommand extends GeneratorCommand
*
* @var string
*/
protected $type = 'Mail';
protected $type = 'Mailable';
/**
* Execute the console command.

View File

@@ -6,7 +6,9 @@ use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'make:model')]
class ModelMakeCommand extends GeneratorCommand
@@ -233,4 +235,36 @@ class ModelMakeCommand extends GeneratorCommand
['requests', 'R', InputOption::VALUE_NONE, 'Create new form request classes and use them in the resource controller'],
];
}
/**
* Interact further with the user if they were prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) {
return;
}
collect($this->components->choice('Would you like any of the following?', [
'none',
'all',
'factory',
'form requests',
'migration',
'policy',
'resource controller',
'seed',
], default: 0, multiple: true))
->reject('none')
->map(fn ($option) => match ($option) {
'resource controller' => 'resource',
'form requests' => 'requests',
default => $option,
})
->each(fn ($option) => $input->setOption($option, true));
}
}

View File

@@ -5,7 +5,9 @@ namespace Illuminate\Foundation\Console;
use Illuminate\Console\GeneratorCommand;
use InvalidArgumentException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'make:observer')]
class ObserverMakeCommand extends GeneratorCommand
@@ -150,4 +152,28 @@ class ObserverMakeCommand extends GeneratorCommand
['model', 'm', InputOption::VALUE_OPTIONAL, 'The model that the observer applies to'],
];
}
/**
* Interact further with the user if they were prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) {
return;
}
$model = $this->components->askWithCompletion(
'What model should this observer apply to?',
$this->possibleModels(),
'none'
);
if ($model && $model !== 'none') {
$input->setOption('model', $model);
}
}
}

View File

@@ -6,7 +6,9 @@ use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use LogicException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'make:policy')]
class PolicyMakeCommand extends GeneratorCommand
@@ -209,4 +211,28 @@ class PolicyMakeCommand extends GeneratorCommand
['guard', 'g', InputOption::VALUE_OPTIONAL, 'The guard that the policy relies on'],
];
}
/**
* Interact further with the user if they were prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) {
return;
}
$model = $this->components->askWithCompletion(
'What model should this policy apply to?',
$this->possibleModels(),
'none'
);
if ($model && $model !== 'none') {
$input->setOption('model', $model);
}
}
}

View File

@@ -84,12 +84,12 @@ class StubPublishCommand extends Command
realpath(__DIR__.'/../../Routing/Console/stubs/controller.model.api.stub') => 'controller.model.api.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.model.stub') => 'controller.model.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.nested.api.stub') => 'controller.nested.api.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.nested.singleton.api.stub') => 'controller.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.nested.singleton.stub') => 'controller.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.nested.singleton.api.stub') => 'controller.nested.singleton.api.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.nested.singleton.stub') => 'controller.nested.singleton.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.nested.stub') => 'controller.nested.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.plain.stub') => 'controller.plain.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.singleton.api.stub') => 'controller.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.singleton.stub') => 'controller.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.singleton.api.stub') => 'controller.singleton.api.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.singleton.stub') => 'controller.singleton.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/controller.stub') => 'controller.stub',
realpath(__DIR__.'/../../Routing/Console/stubs/middleware.stub') => 'middleware.stub',
];

View File

@@ -5,7 +5,9 @@ namespace Illuminate\Foundation\Console;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Support\Str;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(name: 'make:test')]
class TestMakeCommand extends GeneratorCommand
@@ -120,4 +122,32 @@ class TestMakeCommand extends GeneratorCommand
['pest', 'p', InputOption::VALUE_NONE, 'Create a Pest test'],
];
}
/**
* Interact further with the user if they were prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
if ($this->isReservedName($this->getNameInput()) || $this->didReceiveOptions($input)) {
return;
}
$type = $this->components->choice('Which type of test would you like', [
'feature',
'unit',
'pest feature',
'pest unit',
], default: 0);
match ($type) {
'feature' => null,
'unit' => $input->setOption('unit', true),
'pest feature' => $input->setOption('pest', true),
'pest unit' => tap($input)->setOption('pest', true)->setOption('unit', true),
};
}
}

View File

@@ -51,21 +51,21 @@ class Kernel implements KernelContract
/**
* The application's middleware stack.
*
* @var array
* @var array<int, class-string|string>
*/
protected $middleware = [];
/**
* The application's route middleware groups.
*
* @var array
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [];
/**
* The application's route middleware.
*
* @var array
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [];
@@ -245,7 +245,7 @@ class Kernel implements KernelContract
}
/**
* Register a callback to be invoked when the requests lifecyle duration exceeds a given amount of time.
* Register a callback to be invoked when the requests lifecycle duration exceeds a given amount of time.
*
* @param \DateTimeInterface|\Carbon\CarbonInterval|float|int $threshold
* @param callable $handler

View File

@@ -19,7 +19,7 @@ class PreventRequestsDuringMaintenance
/**
* The URIs that should be accessible while maintenance mode is enabled.
*
* @var array
* @var array<int, string>
*/
protected $except = [];

View File

@@ -16,7 +16,7 @@ class TrimStrings extends TransformsRequest
/**
* The attributes that should not be trimmed.
*
* @var array
* @var array<int, string>
*/
protected $except = [
//
@@ -53,7 +53,7 @@ class TrimStrings extends TransformsRequest
return $value;
}
return preg_replace('~^[\s]+|[\s]+$~u', '', $value) ?? trim($value);
return preg_replace('~^[\s\x{FEFF}\x{200B}]+|[\s\x{FEFF}\x{200B}]+$~u', '', $value) ?? trim($value);
}
/**

View File

@@ -34,7 +34,7 @@ class VerifyCsrfToken
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
* @var array<int, string>
*/
protected $except = [];

View File

@@ -122,9 +122,7 @@ class ProviderRepository
return;
}
$this->app->make('events')->listen($events, function () use ($provider) {
$this->app->register($provider);
});
$this->app->make('events')->listen($events, fn () => $this->app->register($provider));
}
/**

View File

@@ -11,7 +11,7 @@ class EventServiceProvider extends ServiceProvider
/**
* The event handler mappings for the application.
*
* @var array
* @var array<string, array<int, string>>
*/
protected $listen = [];

View File

@@ -168,6 +168,30 @@ trait InteractsWithDatabase
);
}
/**
* Specify the number of database queries that should occur throughout the test.
*
* @param int $expected
* @param string|null $connection
* @return $this
*/
public function expectsDatabaseQueryCount($expected, $connection = null)
{
with($this->getConnection($connection), function ($connection) use ($expected) {
$actual = 0;
$connection->listen(function () use (&$actual) {
$actual++;
});
$this->beforeApplicationDestroyed(function () use (&$actual, $expected, $connection) {
$this->assertSame($actual, $expected, "Expected {$expected} database queries on the [{$connection->getName()}] connection. {$actual} occurred.");
});
});
return $this;
}
/**
* Determine if the argument is a soft deletable model.
*

View File

@@ -0,0 +1,122 @@
<?php
namespace Illuminate\Foundation\Testing;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands;
trait DatabaseTruncation
{
use CanConfigureMigrationCommands;
/**
* The cached names of the database tables for each connection.
*
* @var array
*/
protected static array $allTables;
/**
* Truncate the database tables for all configured connections.
*
* @return void
*/
protected function truncateDatabaseTables(): void
{
// Migrate and seed the database on first run...
if (! RefreshDatabaseState::$migrated) {
$this->artisan('migrate:fresh', $this->migrateFreshUsing());
$this->app[Kernel::class]->setArtisan(null);
RefreshDatabaseState::$migrated = true;
return;
}
// Always clear any test data on subsequent runs...
$this->truncateTablesForAllConnections();
if ($seeder = $this->seeder()) {
// Use a specific seeder class...
$this->artisan('db:seed', ['--class' => $seeder]);
} elseif ($this->shouldSeed()) {
// Use the default seeder class...
$this->artisan('db:seed');
}
}
/**
* Truncate the database tables for all configured connections.
*
* @return void
*/
protected function truncateTablesForAllConnections(): void
{
$database = $this->app->make('db');
collect($this->connectionsToTruncate())
->each(function ($name) use ($database) {
$connection = $database->connection($name);
$connection->getSchemaBuilder()->withoutForeignKeyConstraints(
fn () => $this->truncateTablesForConnection($connection, $name)
);
});
}
/**
* Truncate the database tables for the given database connection.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @param string|null $name
* @return void
*/
protected function truncateTablesForConnection(ConnectionInterface $connection, ?string $name): void
{
$dispatcher = $connection->getEventDispatcher();
$connection->unsetEventDispatcher();
collect(static::$allTables[$name] ??= $connection->getDoctrineSchemaManager()->listTableNames())
->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());
$connection->setEventDispatcher($dispatcher);
}
/**
* The database connections that should have their tables truncated.
*
* @return array
*/
protected function connectionsToTruncate(): array
{
return property_exists($this, 'connectionsToTruncate')
? $this->connectionsToTruncate : [null];
}
/**
* Get the tables that should not be truncated.
*
* @param string|null $connectionName
* @return array
*/
protected function exceptTables(?string $connectionName): array
{
if (property_exists($this, 'exceptTables')) {
return array_merge(
$this->exceptTables[$connectionName] ?? [],
[$this->app['config']->get('database.migrations')]
);
}
return [$this->app['config']->get('database.migrations')];
}
}

View File

@@ -131,6 +131,10 @@ abstract class TestCase extends BaseTestCase
$this->runDatabaseMigrations();
}
if (isset($uses[DatabaseTruncation::class])) {
$this->truncateDatabaseTables();
}
if (isset($uses[DatabaseTransactions::class])) {
$this->beginDatabaseTransaction();
}

Some files were not shown because too many files have changed in this diff Show More