package and depencies

This commit is contained in:
RafficMohammed
2023-01-08 02:57:24 +05:30
parent d5332eb421
commit 1d54b8bc7f
4309 changed files with 193331 additions and 172289 deletions

View File

@@ -14,6 +14,13 @@ class AuthorizationException extends Exception
*/
protected $response;
/**
* The HTTP response status code.
*
* @var int|null
*/
protected $status;
/**
* Create a new authorization exception instance.
*
@@ -52,6 +59,49 @@ class AuthorizationException extends Exception
return $this;
}
/**
* Set the HTTP response status code.
*
* @param int|null $status
* @return $this
*/
public function withStatus($status)
{
$this->status = $status;
return $this;
}
/**
* Set the HTTP response status code to 404.
*
* @return $this
*/
public function asNotFound()
{
return $this->withStatus(404);
}
/**
* Determine if the HTTP status code has been set.
*
* @return bool
*/
public function hasStatus()
{
return $this->status !== null;
}
/**
* Get the HTTP status code.
*
* @return int|null
*/
public function status()
{
return $this->status;
}
/**
* Create a deny response object from this exception.
*
@@ -59,6 +109,6 @@ class AuthorizationException extends Exception
*/
public function toResponse()
{
return Response::deny($this->message, $this->code);
return Response::deny($this->message, $this->code)->withStatus($this->status);
}
}

View File

@@ -4,6 +4,7 @@ namespace Illuminate\Auth\Access;
use Closure;
use Exception;
use Illuminate\Auth\Access\Events\GateEvaluated;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
@@ -180,7 +181,7 @@ class Gate implements GateContract
* Define a new ability.
*
* @param string $ability
* @param callable|string $callback
* @param callable|array|string $callback
* @return $this
*
* @throws \InvalidArgumentException
@@ -198,7 +199,7 @@ class Gate implements GateContract
$this->abilities[$ability] = $this->buildAbilityCallback($ability, $callback);
} else {
throw new InvalidArgumentException("Callback must be a callable or a 'Class@method' string.");
throw new InvalidArgumentException("Callback must be a callable, callback array, or a 'Class@method' string.");
}
return $this;
@@ -239,7 +240,7 @@ class Gate implements GateContract
protected function buildAbilityCallback($ability, $callback)
{
return function () use ($ability, $callback) {
if (Str::contains($callback, '@')) {
if (str_contains($callback, '@')) {
[$class, $method] = Str::parseCallback($callback);
} else {
$class = $callback;
@@ -338,9 +339,9 @@ class Gate implements GateContract
*/
public function check($abilities, $arguments = [])
{
return collect($abilities)->every(function ($ability) use ($arguments) {
return $this->inspect($ability, $arguments)->allowed();
});
return collect($abilities)->every(
fn ($ability) => $this->inspect($ability, $arguments)->allowed()
);
}
/**
@@ -352,9 +353,7 @@ class Gate implements GateContract
*/
public function any($abilities, $arguments = [])
{
return collect($abilities)->contains(function ($ability) use ($arguments) {
return $this->check($ability, $arguments);
});
return collect($abilities)->contains(fn ($ability) => $this->check($ability, $arguments));
}
/**
@@ -575,7 +574,7 @@ class Gate implements GateContract
$afterResult = $after($user, $ability, $result, $arguments);
$result = $result ?? $afterResult;
$result ??= $afterResult;
}
return $result;
@@ -594,7 +593,7 @@ class Gate implements GateContract
{
if ($this->container->bound(Dispatcher::class)) {
$this->container->make(Dispatcher::class)->dispatch(
new Events\GateEvaluated($user, $ability, $result, $arguments)
new GateEvaluated($user, $ability, $result, $arguments)
);
}
}
@@ -808,7 +807,7 @@ class Gate implements GateContract
*/
protected function formatAbilityToMethod($ability)
{
return strpos($ability, '-') !== false ? Str::camel($ability) : $ability;
return str_contains($ability, '-') ? Str::camel($ability) : $ability;
}
/**
@@ -819,9 +818,7 @@ class Gate implements GateContract
*/
public function forUser($user)
{
$callback = function () use ($user) {
return $user;
};
$callback = fn () => $user;
return new static(
$this->container, $callback, $this->abilities,

View File

@@ -27,4 +27,29 @@ trait HandlesAuthorization
{
return Response::deny($message, $code);
}
/**
* Deny with a HTTP status code.
*
* @param int $status
* @param ?string $message
* @param ?int $code
* @return \Illuminate\Auth\Access\Response
*/
public function denyWithStatus($status, $message = null, $code = null)
{
return Response::denyWithStatus($status, $message, $code);
}
/**
* Deny with a 404 HTTP status code.
*
* @param ?string $message
* @param ?int $code
* @return \Illuminate\Auth\Access\Response
*/
public function denyAsNotFound($message = null, $code = null)
{
return Response::denyWithStatus(404, $message, $code);
}
}

View File

@@ -27,6 +27,13 @@ class Response implements Arrayable
*/
protected $code;
/**
* The HTTP response status code.
*
* @var int|null
*/
protected $status;
/**
* Create a new response.
*
@@ -66,6 +73,31 @@ class Response implements Arrayable
return new static(false, $message, $code);
}
/**
* Create a new "deny" Response with a HTTP status code.
*
* @param int $status
* @param string|null $message
* @param mixed $code
* @return \Illuminate\Auth\Access\Response
*/
public static function denyWithStatus($status, $message = null, $code = null)
{
return static::deny($message, $code)->withStatus($status);
}
/**
* Create a new "deny" Response with a 404 HTTP status code.
*
* @param string|null $message
* @param mixed $code
* @return \Illuminate\Auth\Access\Response
*/
public static function denyAsNotFound($message = null, $code = null)
{
return static::denyWithStatus(404, $message, $code);
}
/**
* Determine if the response was allowed.
*
@@ -117,12 +149,46 @@ class Response implements Arrayable
{
if ($this->denied()) {
throw (new AuthorizationException($this->message(), $this->code()))
->setResponse($this);
->setResponse($this)
->withStatus($this->status);
}
return $this;
}
/**
* Set the HTTP response status code.
*
* @param null|int $status
* @return $this
*/
public function withStatus($status)
{
$this->status = $status;
return $this;
}
/**
* Set the HTTP response status code to 404.
*
* @return $this
*/
public function asNotFound()
{
return $this->withStatus(404);
}
/**
* Get the HTTP status code.
*
* @return int|null
*/
public function status()
{
return $this->status;
}
/**
* Convert the response to an array.
*

View File

@@ -6,6 +6,10 @@ use Closure;
use Illuminate\Contracts\Auth\Factory as FactoryContract;
use InvalidArgumentException;
/**
* @mixin \Illuminate\Contracts\Auth\Guard
* @mixin \Illuminate\Contracts\Auth\StatefulGuard
*/
class AuthManager implements FactoryContract
{
use CreatesUserProviders;
@@ -50,9 +54,7 @@ class AuthManager implements FactoryContract
{
$this->app = $app;
$this->userResolver = function ($guard = null) {
return $this->guard($guard)->user();
};
$this->userResolver = fn ($guard = null) => $this->guard($guard)->user();
}
/**
@@ -208,9 +210,7 @@ class AuthManager implements FactoryContract
$this->setDefaultDriver($name);
$this->userResolver = function ($name = null) {
return $this->guard($name)->user();
};
$this->userResolver = fn ($name = null) => $this->guard($name)->user();
}
/**

View File

@@ -34,13 +34,9 @@ class AuthServiceProvider extends ServiceProvider
*/
protected function registerAuthenticator()
{
$this->app->singleton('auth', function ($app) {
return new AuthManager($app);
});
$this->app->singleton('auth', fn ($app) => new AuthManager($app));
$this->app->singleton('auth.driver', function ($app) {
return $app['auth']->guard();
});
$this->app->singleton('auth.driver', fn ($app) => $app['auth']->guard());
}
/**
@@ -50,9 +46,7 @@ class AuthServiceProvider extends ServiceProvider
*/
protected function registerUserResolver()
{
$this->app->bind(AuthenticatableContract::class, function ($app) {
return call_user_func($app['auth']->userResolver());
});
$this->app->bind(AuthenticatableContract::class, fn ($app) => call_user_func($app['auth']->userResolver()));
}
/**
@@ -63,9 +57,7 @@ class AuthServiceProvider extends ServiceProvider
protected function registerAccessGate()
{
$this->app->singleton(GateContract::class, function ($app) {
return new Gate($app, function () use ($app) {
return call_user_func($app['auth']->userResolver());
});
return new Gate($app, fn () => call_user_func($app['auth']->userResolver()));
});
}

View File

@@ -3,7 +3,9 @@
namespace Illuminate\Auth\Console;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'auth:clear-resets')]
class ClearResetsCommand extends Command
{
/**
@@ -13,6 +15,17 @@ class ClearResetsCommand extends Command
*/
protected $signature = 'auth:clear-resets {name? : The name of the password broker}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'auth:clear-resets';
/**
* The console command description.
*
@@ -29,6 +42,6 @@ class ClearResetsCommand extends Command
{
$this->laravel['auth.password']->broker($this->argument('name'))->getRepository()->deleteExpired();
$this->info('Expired reset tokens cleared!');
$this->components->info('Expired reset tokens cleared successfully.');
}
}

View File

@@ -12,10 +12,6 @@
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Nunito&display=swap" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>

View File

@@ -33,16 +33,13 @@ trait CreatesUserProviders
);
}
switch ($driver) {
case 'database':
return $this->createDatabaseProvider($config);
case 'eloquent':
return $this->createEloquentProvider($config);
default:
throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
);
}
return match ($driver) {
'database' => $this->createDatabaseProvider($config),
'eloquent' => $this->createEloquentProvider($config),
default => throw new InvalidArgumentException(
"Authentication user provider [{$driver}] is not defined."
),
};
}
/**

View File

@@ -8,7 +8,6 @@ use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Support\Str;
class DatabaseUserProvider implements UserProvider
{
@@ -17,7 +16,7 @@ class DatabaseUserProvider implements UserProvider
*
* @var \Illuminate\Database\ConnectionInterface
*/
protected $conn;
protected $connection;
/**
* The hasher implementation.
@@ -36,14 +35,14 @@ class DatabaseUserProvider implements UserProvider
/**
* Create a new database user provider.
*
* @param \Illuminate\Database\ConnectionInterface $conn
* @param \Illuminate\Database\ConnectionInterface $connection
* @param \Illuminate\Contracts\Hashing\Hasher $hasher
* @param string $table
* @return void
*/
public function __construct(ConnectionInterface $conn, HasherContract $hasher, $table)
public function __construct(ConnectionInterface $connection, HasherContract $hasher, $table)
{
$this->conn = $conn;
$this->connection = $connection;
$this->table = $table;
$this->hasher = $hasher;
}
@@ -56,7 +55,7 @@ class DatabaseUserProvider implements UserProvider
*/
public function retrieveById($identifier)
{
$user = $this->conn->table($this->table)->find($identifier);
$user = $this->connection->table($this->table)->find($identifier);
return $this->getGenericUser($user);
}
@@ -71,7 +70,7 @@ class DatabaseUserProvider implements UserProvider
public function retrieveByToken($identifier, $token)
{
$user = $this->getGenericUser(
$this->conn->table($this->table)->find($identifier)
$this->connection->table($this->table)->find($identifier)
);
return $user && $user->getRememberToken() && hash_equals($user->getRememberToken(), $token)
@@ -87,7 +86,7 @@ class DatabaseUserProvider implements UserProvider
*/
public function updateRememberToken(UserContract $user, $token)
{
$this->conn->table($this->table)
$this->connection->table($this->table)
->where($user->getAuthIdentifierName(), $user->getAuthIdentifier())
->update([$user->getRememberTokenName() => $token]);
}
@@ -100,22 +99,22 @@ class DatabaseUserProvider implements UserProvider
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
array_key_exists('password', $credentials))) {
$credentials = array_filter(
$credentials,
fn ($key) => ! str_contains($key, 'password'),
ARRAY_FILTER_USE_KEY
);
if (empty($credentials)) {
return;
}
// First we will add each credential element to the query as a where clause.
// Then we can execute the query and, if we found a user, return it in a
// generic "user" object that will be utilized by the Guard instances.
$query = $this->conn->table($this->table);
$query = $this->connection->table($this->table);
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
@@ -125,9 +124,9 @@ class DatabaseUserProvider implements UserProvider
}
}
// Now we are ready to execute the query to see if we have an user matching
// the given credentials. If not, we will just return nulls and indicate
// that there are no matching users for these given credential arrays.
// Now we are ready to execute the query to see if we have a user matching
// the given credentials. If not, we will just return null and indicate
// that there are no matching users from the given credential arrays.
$user = $query->first();
return $this->getGenericUser($user);

View File

@@ -7,7 +7,6 @@ use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
class EloquentUserProvider implements UserProvider
{
@@ -25,6 +24,13 @@ class EloquentUserProvider implements UserProvider
*/
protected $model;
/**
* The callback that may modify the user retrieval queries.
*
* @var (\Closure(\Illuminate\Database\Eloquent\Builder):mixed)|null
*/
protected $queryCallback;
/**
* Create a new database user provider.
*
@@ -74,14 +80,13 @@ class EloquentUserProvider implements UserProvider
$rememberToken = $retrievedModel->getRememberToken();
return $rememberToken && hash_equals($rememberToken, $token)
? $retrievedModel : null;
return $rememberToken && hash_equals($rememberToken, $token) ? $retrievedModel : null;
}
/**
* Update the "remember me" token for the given user in storage.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|\Illuminate\Database\Eloquent\Model $user
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param string $token
* @return void
*/
@@ -106,9 +111,13 @@ class EloquentUserProvider implements UserProvider
*/
public function retrieveByCredentials(array $credentials)
{
if (empty($credentials) ||
(count($credentials) === 1 &&
Str::contains($this->firstCredentialKey($credentials), 'password'))) {
$credentials = array_filter(
$credentials,
fn ($key) => ! str_contains($key, 'password'),
ARRAY_FILTER_USE_KEY
);
if (empty($credentials)) {
return;
}
@@ -118,10 +127,6 @@ class EloquentUserProvider implements UserProvider
$query = $this->newModelQuery();
foreach ($credentials as $key => $value) {
if (Str::contains($key, 'password')) {
continue;
}
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
@@ -134,19 +139,6 @@ class EloquentUserProvider implements UserProvider
return $query->first();
}
/**
* Get the first key from the credential array.
*
* @param array $credentials
* @return string|null
*/
protected function firstCredentialKey(array $credentials)
{
foreach ($credentials as $key => $value) {
return $key;
}
}
/**
* Validate a user against the given credentials.
*
@@ -156,7 +148,9 @@ class EloquentUserProvider implements UserProvider
*/
public function validateCredentials(UserContract $user, array $credentials)
{
$plain = $credentials['password'];
if (is_null($plain = $credentials['password'])) {
return false;
}
return $this->hasher->check($plain, $user->getAuthPassword());
}
@@ -169,9 +163,13 @@ class EloquentUserProvider implements UserProvider
*/
protected function newModelQuery($model = null)
{
return is_null($model)
$query = is_null($model)
? $this->createModel()->newQuery()
: $model->newQuery();
with($query, $this->queryCallback);
return $query;
}
/**
@@ -231,4 +229,27 @@ class EloquentUserProvider implements UserProvider
return $this;
}
/**
* Get the callback that modifies the query before retrieving users.
*
* @return \Closure|null
*/
public function getQueryCallback()
{
return $this->queryCallback;
}
/**
* Sets the callback to modify the query before retrieving users.
*
* @param (\Closure(\Illuminate\Database\Eloquent\Builder):mixed)|null $queryCallback
* @return $this
*/
public function withQuery($queryCallback = null)
{
$this->queryCallback = $queryCallback;
return $this;
}
}

View File

@@ -95,6 +95,18 @@ trait GuardHelpers
return $this;
}
/**
* Forget the current user.
*
* @return $this
*/
public function forgetUser()
{
$this->user = null;
return $this;
}
/**
* Get the user provider used by the guard.
*

View File

@@ -75,7 +75,7 @@ class Authorize
if ($this->isClassName($model)) {
return trim($model);
} else {
return $request->route($model, null) ?:
return $request->route($model, null) ??
((preg_match("/^['\"](.*)['\"]$/", trim($model), $matches)) ? $matches[1] : null);
}
}
@@ -88,6 +88,6 @@ class Authorize
*/
protected function isClassName($value)
{
return strpos($value, '\\') !== false;
return str_contains($value, '\\');
}
}

View File

@@ -50,11 +50,12 @@ class RequirePassword
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $redirectToRoute
* @param int|null $passwordTimeoutSeconds
* @return mixed
*/
public function handle($request, Closure $next, $redirectToRoute = null)
public function handle($request, Closure $next, $redirectToRoute = null, $passwordTimeoutSeconds = null)
{
if ($this->shouldConfirmPassword($request)) {
if ($this->shouldConfirmPassword($request, $passwordTimeoutSeconds)) {
if ($request->expectsJson()) {
return $this->responseFactory->json([
'message' => 'Password confirmation required.',
@@ -73,12 +74,13 @@ class RequirePassword
* Determine if the confirmation timeout has expired.
*
* @param \Illuminate\Http\Request $request
* @param int|null $passwordTimeoutSeconds
* @return bool
*/
protected function shouldConfirmPassword($request)
protected function shouldConfirmPassword($request, $passwordTimeoutSeconds = null)
{
$confirmedAt = time() - $request->session()->get('auth.password_confirmed_at', 0);
return $confirmedAt > $this->passwordTimeout;
return $confirmedAt > ($passwordTimeoutSeconds ?? $this->passwordTimeout);
}
}

View File

@@ -18,14 +18,14 @@ class ResetPassword extends Notification
/**
* The callback that should be used to create the reset password URL.
*
* @var \Closure|null
* @var (\Closure(mixed, string): string)|null
*/
public static $createUrlCallback;
/**
* The callback that should be used to build the mail message.
*
* @var \Closure|null
* @var (\Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage)|null
*/
public static $toMailCallback;
@@ -103,7 +103,7 @@ class ResetPassword extends Notification
/**
* Set a callback that should be used when creating the reset password button URL.
*
* @param \Closure $callback
* @param \Closure(mixed, string): string $callback
* @return void
*/
public static function createUrlUsing($callback)
@@ -114,7 +114,7 @@ class ResetPassword extends Notification
/**
* Set a callback that should be used when building the notification mail message.
*
* @param \Closure $callback
* @param \Closure(mixed, string): \Illuminate\Notifications\Messages\MailMessage $callback
* @return void
*/
public static function toMailUsing($callback)

View File

@@ -3,7 +3,6 @@
namespace Illuminate\Auth\Passwords;
use Illuminate\Contracts\Auth\PasswordBrokerFactory as FactoryContract;
use Illuminate\Support\Str;
use InvalidArgumentException;
/**
@@ -84,7 +83,7 @@ class PasswordBrokerManager implements FactoryContract
{
$key = $this->app['config']['app.key'];
if (Str::startsWith($key, 'base64:')) {
if (str_starts_with($key, 'base64:')) {
$key = base64_decode(substr($key, 7));
}

View File

@@ -2,8 +2,6 @@
namespace Illuminate\Auth;
use Illuminate\Support\Str;
class Recaller
{
/**
@@ -51,7 +49,7 @@ class Recaller
*/
public function hash()
{
return explode('|', $this->recaller, 3)[2];
return explode('|', $this->recaller, 4)[2];
}
/**
@@ -71,7 +69,7 @@ class Recaller
*/
protected function properString()
{
return is_string($this->recaller) && Str::contains($this->recaller, '|');
return is_string($this->recaller) && str_contains($this->recaller, '|');
}
/**
@@ -83,6 +81,16 @@ class Recaller
{
$segments = explode('|', $this->recaller);
return count($segments) === 3 && trim($segments[0]) !== '' && trim($segments[1]) !== '';
return count($segments) >= 3 && trim($segments[0]) !== '' && trim($segments[1]) !== '';
}
/**
* Get the recaller's segments.
*
* @return array
*/
public function segments()
{
return explode('|', $this->recaller);
}
}

View File

@@ -59,7 +59,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*
* @var int
*/
protected $rememberDuration = 2628000;
protected $rememberDuration = 576000;
/**
* The session used by the guard.
@@ -287,6 +287,8 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
* @param string $field
* @param array $extraConditions
* @return \Symfony\Component\HttpFoundation\Response|null
*
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
*/
public function basic($field = 'email', $extraConditions = [])
{
@@ -310,6 +312,8 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
* @param string $field
* @param array $extraConditions
* @return \Symfony\Component\HttpFoundation\Response|null
*
* @throws \Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException
*/
public function onceBasic($field = 'email', $extraConditions = [])
{
@@ -397,8 +401,8 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
* Attempt to authenticate a user with credentials and additional callbacks.
*
* @param array $credentials
* @param array|callable $callbacks
* @param false $remember
* @param array|callable|null $callbacks
* @param bool $remember
* @return bool
*/
public function attemptWhen(array $credentials = [], $callbacks = null, $remember = false)
@@ -624,9 +628,12 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
{
$this->session->remove($this->getName());
$this->getCookieJar()->unqueue($this->getRecallerName());
if (! is_null($this->recaller())) {
$this->getCookieJar()->queue($this->getCookieJar()
->forget($this->getRecallerName()));
$this->getCookieJar()->queue(
$this->getCookieJar()->forget($this->getRecallerName())
);
}
}
@@ -700,9 +707,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
public function attempting($callback)
{
if (isset($this->events)) {
$this->events->listen(Events\Attempting::class, $callback);
}
$this->events?->listen(Events\Attempting::class, $callback);
}
/**
@@ -714,11 +719,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function fireAttemptEvent(array $credentials, $remember = false)
{
if (isset($this->events)) {
$this->events->dispatch(new Attempting(
$this->name, $credentials, $remember
));
}
$this->events?->dispatch(new Attempting($this->name, $credentials, $remember));
}
/**
@@ -729,11 +730,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function fireValidatedEvent($user)
{
if (isset($this->events)) {
$this->events->dispatch(new Validated(
$this->name, $user
));
}
$this->events?->dispatch(new Validated($this->name, $user));
}
/**
@@ -745,11 +742,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function fireLoginEvent($user, $remember = false)
{
if (isset($this->events)) {
$this->events->dispatch(new Login(
$this->name, $user, $remember
));
}
$this->events?->dispatch(new Login($this->name, $user, $remember));
}
/**
@@ -760,11 +753,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function fireAuthenticatedEvent($user)
{
if (isset($this->events)) {
$this->events->dispatch(new Authenticated(
$this->name, $user
));
}
$this->events?->dispatch(new Authenticated($this->name, $user));
}
/**
@@ -775,11 +764,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function fireOtherDeviceLogoutEvent($user)
{
if (isset($this->events)) {
$this->events->dispatch(new OtherDeviceLogout(
$this->name, $user
));
}
$this->events?->dispatch(new OtherDeviceLogout($this->name, $user));
}
/**
@@ -791,11 +776,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function fireFailedEvent($user, array $credentials)
{
if (isset($this->events)) {
$this->events->dispatch(new Failed(
$this->name, $user, $credentials
));
}
$this->events?->dispatch(new Failed($this->name, $user, $credentials));
}
/**

View File

@@ -14,13 +14,13 @@
}
],
"require": {
"php": "^7.3|^8.0",
"illuminate/collections": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/http": "^8.0",
"illuminate/macroable": "^8.0",
"illuminate/queue": "^8.0",
"illuminate/support": "^8.0"
"php": "^8.0.2",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/http": "^9.0",
"illuminate/macroable": "^9.0",
"illuminate/queue": "^9.0",
"illuminate/support": "^9.0"
},
"autoload": {
"psr-4": {
@@ -29,13 +29,13 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"illuminate/console": "Required to use the auth:clear-resets command (^8.0).",
"illuminate/queue": "Required to fire login / logout events (^8.0).",
"illuminate/session": "Required to use the session based guard (^8.0)."
"illuminate/console": "Required to use the auth:clear-resets command (^9.0).",
"illuminate/queue": "Required to fire login / logout events (^9.0).",
"illuminate/session": "Required to use the session based guard (^9.0)."
},
"config": {
"sort-packages": true

View File

@@ -5,6 +5,7 @@ namespace Illuminate\Broadcasting;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Broadcast;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class BroadcastController extends Controller
{
@@ -22,4 +23,22 @@ class BroadcastController extends Controller
return Broadcast::auth($request);
}
/**
* Authenticate the current user.
*
* See: https://pusher.com/docs/channels/server_api/authenticating-users/#user-authentication.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function authenticateUser(Request $request)
{
if ($request->hasSession()) {
$request->session()->reflash();
}
return Broadcast::resolveAuthenticatedUser($request)
?? throw new AccessDeniedHttpException;
}
}

View File

@@ -35,6 +35,13 @@ class BroadcastEvent implements ShouldQueue
*/
public $timeout;
/**
* The number of seconds to wait before retrying the job when encountering an uncaught exception.
*
* @var int
*/
public $backoff;
/**
* Create a new job handler instance.
*
@@ -46,6 +53,7 @@ class BroadcastEvent implements ShouldQueue
$this->event = $event;
$this->tries = property_exists($event, 'tries') ? $event->tries : null;
$this->timeout = property_exists($event, 'timeout') ? $event->timeout : null;
$this->backoff = property_exists($event, 'backoff') ? $event->backoff : null;
$this->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null;
}

View File

@@ -4,14 +4,18 @@ namespace Illuminate\Broadcasting;
use Ably\AblyRest;
use Closure;
use GuzzleHttp\Client as GuzzleClient;
use Illuminate\Broadcasting\Broadcasters\AblyBroadcaster;
use Illuminate\Broadcasting\Broadcasters\LogBroadcaster;
use Illuminate\Broadcasting\Broadcasters\NullBroadcaster;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
use Illuminate\Broadcasting\Broadcasters\RedisBroadcaster;
use Illuminate\Bus\UniqueLock;
use Illuminate\Contracts\Broadcasting\Factory as FactoryContract;
use Illuminate\Contracts\Broadcasting\ShouldBeUnique;
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
use Illuminate\Contracts\Bus\Dispatcher as BusDispatcherContract;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Foundation\CachesRoutes;
use InvalidArgumentException;
use Psr\Log\LoggerInterface;
@@ -55,7 +59,7 @@ class BroadcastManager implements FactoryContract
}
/**
* Register the routes for handling broadcast authentication and sockets.
* Register the routes for handling broadcast channel authentication and sockets.
*
* @param array|null $attributes
* @return void
@@ -76,6 +80,41 @@ class BroadcastManager implements FactoryContract
});
}
/**
* Register the routes for handling broadcast user authentication.
*
* @param array|null $attributes
* @return void
*/
public function userRoutes(array $attributes = null)
{
if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
return;
}
$attributes = $attributes ?: ['middleware' => ['web']];
$this->app['router']->group($attributes, function ($router) {
$router->match(
['get', 'post'], '/broadcasting/user-auth',
'\\'.BroadcastController::class.'@authenticateUser'
)->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
});
}
/**
* Register the routes for handling broadcast authentication and sockets.
*
* Alias of "routes" method.
*
* @param array|null $attributes
* @return void
*/
public function channelRoutes(array $attributes = null)
{
return $this->routes($attributes);
}
/**
* Get the socket ID for the given request.
*
@@ -129,9 +168,34 @@ class BroadcastManager implements FactoryContract
$queue = $event->queue;
}
$this->app->make('queue')->connection($event->connection ?? null)->pushOn(
$queue, new BroadcastEvent(clone $event)
);
$broadcastEvent = new BroadcastEvent(clone $event);
if ($event instanceof ShouldBeUnique) {
$broadcastEvent = new UniqueBroadcastEvent(clone $event);
if ($this->mustBeUniqueAndCannotAcquireLock($broadcastEvent)) {
return;
}
}
$this->app->make('queue')
->connection($event->connection ?? null)
->pushOn($queue, $broadcastEvent);
}
/**
* Determine if the broadcastable event must be unique and determine if we can acquire the necessary lock.
*
* @param mixed $event
* @return bool
*/
protected function mustBeUniqueAndCannotAcquireLock($event)
{
return ! (new UniqueLock(
method_exists($event, 'uniqueVia')
? $event->uniqueVia()
: $this->app->make(Cache::class)
))->acquire($event);
}
/**
@@ -181,6 +245,10 @@ class BroadcastManager implements FactoryContract
{
$config = $this->getConfig($name);
if (is_null($config)) {
throw new InvalidArgumentException("Broadcast connection [{$name}] is not defined.");
}
if (isset($this->customCreators[$config['driver']])) {
return $this->callCustomCreator($config);
}
@@ -212,17 +280,33 @@ class BroadcastManager implements FactoryContract
* @return \Illuminate\Contracts\Broadcasting\Broadcaster
*/
protected function createPusherDriver(array $config)
{
return new PusherBroadcaster($this->pusher($config));
}
/**
* Get a Pusher instance for the given configuration.
*
* @param array $config
* @return \Pusher\Pusher
*/
public function pusher(array $config)
{
$pusher = new Pusher(
$config['key'], $config['secret'],
$config['app_id'], $config['options'] ?? []
$config['key'],
$config['secret'],
$config['app_id'],
$config['options'] ?? [],
isset($config['client_options']) && ! empty($config['client_options'])
? new GuzzleClient($config['client_options'])
: null,
);
if ($config['log'] ?? false) {
$pusher->setLogger($this->app->make(LoggerInterface::class));
}
return new PusherBroadcaster($pusher);
return $pusher;
}
/**
@@ -233,7 +317,18 @@ class BroadcastManager implements FactoryContract
*/
protected function createAblyDriver(array $config)
{
return new AblyBroadcaster(new AblyRest($config));
return new AblyBroadcaster($this->ably($config));
}
/**
* Get an Ably instance for the given configuration.
*
* @param array $config
* @return \Ably\AblyRest
*/
public function ably(array $config)
{
return new AblyRest($config);
}
/**
@@ -318,7 +413,7 @@ class BroadcastManager implements FactoryContract
*/
public function purge($name = null)
{
$name = $name ?? $this->getDefaultDriver();
$name ??= $this->getDefaultDriver();
unset($this->drivers[$name]);
}

View File

@@ -3,7 +3,9 @@
namespace Illuminate\Broadcasting\Broadcasters;
use Ably\AblyRest;
use Ably\Exceptions\AblyException;
use Ably\Models\Message as AblyMessage;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -63,7 +65,7 @@ class AblyBroadcaster extends Broadcaster
*/
public function validAuthenticationResponse($request, $result)
{
if (Str::startsWith($request->channel_name, 'private')) {
if (str_starts_with($request->channel_name, 'private')) {
$signature = $this->generateAblySignature(
$request->channel_name, $request->socket_id
);
@@ -118,12 +120,20 @@ class AblyBroadcaster extends Broadcaster
* @param string $event
* @param array $payload
* @return void
*
* @throws \Illuminate\Broadcasting\BroadcastException
*/
public function broadcast(array $channels, $event, array $payload = [])
{
foreach ($this->formatChannels($channels) as $channel) {
$this->ably->channels->get($channel)->publish(
$this->buildAblyMessage($event, $payload)
try {
foreach ($this->formatChannels($channels) as $channel) {
$this->ably->channels->get($channel)->publish(
$this->buildAblyMessage($event, $payload)
);
}
} catch (AblyException $e) {
throw new BroadcastException(
sprintf('Ably error: %s', $e->getMessage())
);
}
}
@@ -164,7 +174,7 @@ class AblyBroadcaster extends Broadcaster
public function normalizeChannelName($channel)
{
if ($this->isGuardedChannel($channel)) {
return Str::startsWith($channel, 'private-')
return str_starts_with($channel, 'private-')
? Str::replaceFirst('private-', '', $channel)
: Str::replaceFirst('presence-', '', $channel);
}
@@ -184,7 +194,7 @@ class AblyBroadcaster extends Broadcaster
$channel = (string) $channel;
if (Str::startsWith($channel, ['private-', 'presence-'])) {
return Str::startsWith($channel, 'private-')
return str_starts_with($channel, 'private-')
? Str::replaceFirst('private-', 'private:', $channel)
: Str::replaceFirst('presence-', 'presence:', $channel);
}

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Broadcasting\Broadcasters;
use Closure;
use Exception;
use Illuminate\Container\Container;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
@@ -10,13 +11,19 @@ use Illuminate\Contracts\Routing\BindingRegistrar;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Support\Arr;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionFunction;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
abstract class Broadcaster implements BroadcasterContract
{
/**
* The callback to resolve the authenticated user information.
*
* @var \Closure|null
*/
protected $authenticatedUserCallback = null;
/**
* The registered channel authenticators.
*
@@ -38,6 +45,34 @@ abstract class Broadcaster implements BroadcasterContract
*/
protected $bindingRegistrar;
/**
* Resolve the authenticated user payload for the incoming connection request.
*
* See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication.
*
* @param \Illuminate\Http\Request $request
* @return array|null
*/
public function resolveAuthenticatedUser($request)
{
if ($this->authenticatedUserCallback) {
return $this->authenticatedUserCallback->__invoke($request);
}
}
/**
* Register the user retrieval callback used to authenticate connections.
*
* See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication.
*
* @param \Closure $callback
* @return void
*/
public function resolveAuthenticatedUserUsing(Closure $callback)
{
$this->authenticatedUserCallback = $callback;
}
/**
* Register a channel authenticator.
*
@@ -81,7 +116,11 @@ abstract class Broadcaster implements BroadcasterContract
$handler = $this->normalizeChannelHandlerToCallable($callback);
if ($result = $handler($this->retrieveUser($request, $channel), ...$parameters)) {
$result = $handler($this->retrieveUser($request, $channel), ...$parameters);
if ($result === false) {
throw new AccessDeniedHttpException;
} elseif ($result) {
return $this->validAuthenticationResponse($request, $result);
}
}
@@ -332,6 +371,6 @@ abstract class Broadcaster implements BroadcasterContract
*/
protected function channelNameMatchesPattern($channel, $pattern)
{
return Str::is(preg_replace('/\{(.*?)\}/', '*', $pattern), $channel);
return preg_match('/'.preg_replace('/\{(.*?)\}/', '([^\.]+)', $pattern).'$/', $channel);
}
}

View File

@@ -4,7 +4,7 @@ namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Illuminate\Support\Collection;
use Pusher\ApiErrorException;
use Pusher\Pusher;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -31,6 +31,39 @@ class PusherBroadcaster extends Broadcaster
$this->pusher = $pusher;
}
/**
* Resolve the authenticated user payload for an incoming connection request.
*
* See: https://pusher.com/docs/channels/library_auth_reference/auth-signatures/#user-authentication
* See: https://pusher.com/docs/channels/server_api/authenticating-users/#response
*
* @param \Illuminate\Http\Request $request
* @return array|null
*/
public function resolveAuthenticatedUser($request)
{
if (! $user = parent::resolveAuthenticatedUser($request)) {
return;
}
if (method_exists($this->pusher, 'authenticateUser')) {
return $this->pusher->authenticateUser($request->socket_id, $user);
}
$settings = $this->pusher->getSettings();
$encodedUser = json_encode($user);
$decodedString = "{$request->socket_id}::user::{$encodedUser}";
$auth = $settings['auth_key'].':'.hash_hmac(
'sha256', $decodedString, $settings['secret']
);
return [
'auth' => $auth,
'user_data' => $encodedUser,
];
}
/**
* Authenticate the incoming request for a given channel.
*
@@ -63,9 +96,12 @@ class PusherBroadcaster extends Broadcaster
*/
public function validAuthenticationResponse($request, $result)
{
if (Str::startsWith($request->channel_name, 'private')) {
if (str_starts_with($request->channel_name, 'private')) {
return $this->decodePusherResponse(
$request, $this->pusher->socket_auth($request->channel_name, $request->socket_id)
$request,
method_exists($this->pusher, 'authorizeChannel')
? $this->pusher->authorizeChannel($request->channel_name, $request->socket_id)
: $this->pusher->socket_auth($request->channel_name, $request->socket_id)
);
}
@@ -79,10 +115,9 @@ class PusherBroadcaster extends Broadcaster
return $this->decodePusherResponse(
$request,
$this->pusher->presence_auth(
$request->channel_name, $request->socket_id,
$broadcastIdentifier, $result
)
method_exists($this->pusher, 'authorizePresenceChannel')
? $this->pusher->authorizePresenceChannel($request->channel_name, $request->socket_id, $broadcastIdentifier, $result)
: $this->pusher->presence_auth($request->channel_name, $request->socket_id, $broadcastIdentifier, $result)
);
}
@@ -117,46 +152,21 @@ class PusherBroadcaster extends Broadcaster
{
$socket = Arr::pull($payload, 'socket');
if ($this->pusherServerIsVersionFiveOrGreater()) {
$parameters = $socket !== null ? ['socket_id' => $socket] : [];
$parameters = $socket !== null ? ['socket_id' => $socket] : [];
try {
$this->pusher->trigger(
$this->formatChannels($channels), $event, $payload, $parameters
);
} catch (ApiErrorException $e) {
throw new BroadcastException(
sprintf('Pusher error: %s.', $e->getMessage())
);
}
} else {
$response = $this->pusher->trigger(
$this->formatChannels($channels), $event, $payload, $socket, true
);
if ((is_array($response) && $response['status'] >= 200 && $response['status'] <= 299)
|| $response === true) {
return;
}
$channels = Collection::make($this->formatChannels($channels));
try {
$channels->chunk(100)->each(function ($channels) use ($event, $payload, $parameters) {
$this->pusher->trigger($channels->toArray(), $event, $payload, $parameters);
});
} catch (ApiErrorException $e) {
throw new BroadcastException(
! empty($response['body'])
? sprintf('Pusher error: %s.', $response['body'])
: 'Failed to connect to Pusher.'
sprintf('Pusher error: %s.', $e->getMessage())
);
}
}
/**
* Determine if the Pusher PHP server is version 5.0 or greater.
*
* @return bool
*/
protected function pusherServerIsVersionFiveOrGreater()
{
return class_exists(ApiErrorException::class);
}
/**
* Get the Pusher SDK instance.
*

View File

@@ -2,8 +2,11 @@
namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Support\Arr;
use Predis\Connection\ConnectionException;
use RedisException;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
class RedisBroadcaster extends Broadcaster
@@ -105,6 +108,8 @@ class RedisBroadcaster extends Broadcaster
* @param string $event
* @param array $payload
* @return void
*
* @throws \Illuminate\Broadcasting\BroadcastException
*/
public function broadcast(array $channels, $event, array $payload = [])
{
@@ -120,10 +125,16 @@ class RedisBroadcaster extends Broadcaster
'socket' => Arr::pull($payload, 'socket'),
]);
$connection->eval(
$this->broadcastMultipleChannelsScript(),
0, $payload, ...$this->formatChannels($channels)
);
try {
$connection->eval(
$this->broadcastMultipleChannelsScript(),
0, $payload, ...$this->formatChannels($channels)
);
} catch (ConnectionException|RedisException $e) {
throw new BroadcastException(
sprintf('Redis error: %s.', $e->getMessage())
);
}
}
/**

View File

@@ -0,0 +1,61 @@
<?php
namespace Illuminate\Broadcasting;
use Illuminate\Container\Container;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UniqueBroadcastEvent extends BroadcastEvent implements ShouldBeUnique
{
/**
* The unique lock identifier.
*
* @var mixed
*/
public $uniqueId;
/**
* The number of seconds the unique lock should be maintained.
*
* @var int
*/
public $uniqueFor;
/**
* Create a new event instance.
*
* @param mixed $event
* @return void
*/
public function __construct($event)
{
$this->uniqueId = get_class($event);
if (method_exists($event, 'uniqueId')) {
$this->uniqueId .= $event->uniqueId();
} elseif (property_exists($event, 'uniqueId')) {
$this->uniqueId .= $event->uniqueId;
}
if (method_exists($event, 'uniqueFor')) {
$this->uniqueFor = $event->uniqueFor();
} elseif (property_exists($event, 'uniqueFor')) {
$this->uniqueFor = $event->uniqueFor;
}
parent::__construct($event);
}
/**
* Resolve the cache implementation that should manage the event's uniqueness.
*
* @return \Illuminate\Contracts\Cache\Repository
*/
public function uniqueVia()
{
return method_exists($this->event, 'uniqueVia')
? $this->event->uniqueVia()
: Container::getInstance()->make(Repository::class);
}
}

View File

@@ -14,14 +14,15 @@
}
],
"require": {
"php": "^7.3|^8.0",
"php": "^8.0.2",
"ext-json": "*",
"psr/log": "^1.0|^2.0",
"illuminate/bus": "^8.0",
"illuminate/collections": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/queue": "^8.0",
"illuminate/support": "^8.0"
"psr/log": "^1.0|^2.0|^3.0",
"illuminate/bus": "^9.0",
"illuminate/collections": "^9.0",
"illuminate/container": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/queue": "^9.0",
"illuminate/support": "^9.0"
},
"autoload": {
"psr-4": {
@@ -30,12 +31,12 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0|^5.0|^6.0|^7.0)."
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0)."
},
"config": {
"sort-packages": true

View File

@@ -155,7 +155,7 @@ class Batch implements Arrayable, JsonSerializable
/**
* Add additional jobs to the batch.
*
* @param \Illuminate\Support\Enumerable|array $jobs
* @param \Illuminate\Support\Enumerable|object|array $jobs
* @return self
*/
public function add($jobs)
@@ -462,8 +462,7 @@ class Batch implements Arrayable, JsonSerializable
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
public function jsonSerialize(): array
{
return $this->toArray();
}

View File

@@ -2,7 +2,10 @@
namespace Illuminate\Bus;
use Carbon\CarbonImmutable;
use Illuminate\Container\Container;
use Illuminate\Support\Str;
use Illuminate\Support\Testing\Fakes\BatchFake;
trait Batchable
{
@@ -13,6 +16,13 @@ trait Batchable
*/
public $batchId;
/**
* The fake batch, if applicable.
*
* @var \Illuminate\Support\Testing\BatchFake
*/
private $fakeBatch;
/**
* Get the batch instance for the job, if applicable.
*
@@ -20,6 +30,10 @@ trait Batchable
*/
public function batch()
{
if ($this->fakeBatch) {
return $this->fakeBatch;
}
if ($this->batchId) {
return Container::getInstance()->make(BatchRepository::class)->find($this->batchId);
}
@@ -49,4 +63,46 @@ trait Batchable
return $this;
}
/**
* Indicate that the job should use a fake batch.
*
* @param string $id
* @param string $name
* @param int $totalJobs
* @param int $pendingJobs
* @param int $failedJobs
* @param array $failedJobIds
* @param array $options
* @param \Carbon\CarbonImmutable $createdAt
* @param \Carbon\CarbonImmutable|null $cancelledAt
* @param \Carbon\CarbonImmutable|null $finishedAt
* @return array{0: $this, 1: \Illuminate\Support\Testing\BatchFake}
*/
public function withFakeBatch(string $id = '',
string $name = '',
int $totalJobs = 0,
int $pendingJobs = 0,
int $failedJobs = 0,
array $failedJobIds = [],
array $options = [],
CarbonImmutable $createdAt = null,
?CarbonImmutable $cancelledAt = null,
?CarbonImmutable $finishedAt = null)
{
$this->fakeBatch = new BatchFake(
empty($id) ? (string) Str::uuid() : $id,
$name,
$totalJobs,
$pendingJobs,
$failedJobs,
$failedJobIds,
$options,
$createdAt ?? CarbonImmutable::now(),
$cancelledAt,
$finishedAt,
);
return [$this, $this->fakeBatch];
}
}

View File

@@ -64,6 +64,7 @@ class BusServiceProvider extends ServiceProvider implements DeferrableProvider
DispatcherContract::class,
QueueingDispatcherContract::class,
BatchRepository::class,
DatabaseBatchRepository::class,
];
}
}

View File

@@ -59,9 +59,7 @@ class DatabaseBatchRepository implements PrunableBatchRepository
return $this->connection->table($this->table)
->orderByDesc('id')
->take($limit)
->when($before, function ($q) use ($before) {
return $q->where('id', '<', $before);
})
->when($before, fn ($q) => $q->where('id', '<', $before))
->get()
->map(function ($batch) {
return $this->toBatch($batch);
@@ -78,6 +76,7 @@ class DatabaseBatchRepository implements PrunableBatchRepository
public function find(string $batchId)
{
$batch = $this->connection->table($this->table)
->useWritePdo()
->where('id', $batchId)
->first();
@@ -278,6 +277,29 @@ class DatabaseBatchRepository implements PrunableBatchRepository
return $totalDeleted;
}
/**
* Prune all of the cancelled entries older than the given date.
*
* @param \DateTimeInterface $before
* @return int
*/
public function pruneCancelled(DateTimeInterface $before)
{
$query = $this->connection->table($this->table)
->whereNotNull('cancelled_at')
->where('created_at', '<', $before->getTimestamp());
$totalDeleted = 0;
do {
$deleted = $query->take(1000)->delete();
$totalDeleted += $deleted;
} while ($deleted !== 0);
return $totalDeleted;
}
/**
* Execute the given Closure within a storage specific transaction.
*
@@ -286,9 +308,7 @@ class DatabaseBatchRepository implements PrunableBatchRepository
*/
public function transaction(Closure $callback)
{
return $this->connection->transaction(function () use ($callback) {
return $callback();
});
return $this->connection->transaction(fn () => $callback());
}
/**
@@ -344,4 +364,25 @@ class DatabaseBatchRepository implements PrunableBatchRepository
$batch->finished_at ? CarbonImmutable::createFromTimestamp($batch->finished_at) : $batch->finished_at
);
}
/**
* Get the underlying database connection.
*
* @return \Illuminate\Database\Connection
*/
public function getConnection()
{
return $this->connection;
}
/**
* Set the underlying database connection.
*
* @param \Illuminate\Database\Connection $connection
* @return void
*/
public function setConnection(Connection $connection)
{
$this->connection = $connection;
}
}

View File

@@ -6,9 +6,9 @@ use Closure;
use Illuminate\Bus\Events\BatchDispatched;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher as EventDispatcher;
use Illuminate\Queue\SerializableClosureFactory;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Laravel\SerializableClosure\SerializableClosure;
use Throwable;
class PendingBatch
@@ -57,11 +57,13 @@ class PendingBatch
/**
* Add jobs to the batch.
*
* @param iterable $jobs
* @param iterable|object|array $jobs
* @return $this
*/
public function add($jobs)
{
$jobs = is_iterable($jobs) ? $jobs : Arr::wrap($jobs);
foreach ($jobs as $job) {
$this->jobs->push($job);
}
@@ -78,7 +80,7 @@ class PendingBatch
public function then($callback)
{
$this->options['then'][] = $callback instanceof Closure
? SerializableClosureFactory::make($callback)
? new SerializableClosure($callback)
: $callback;
return $this;
@@ -103,7 +105,7 @@ class PendingBatch
public function catch($callback)
{
$this->options['catch'][] = $callback instanceof Closure
? SerializableClosureFactory::make($callback)
? new SerializableClosure($callback)
: $callback;
return $this;
@@ -128,7 +130,7 @@ class PendingBatch
public function finally($callback)
{
$this->options['finally'][] = $callback instanceof Closure
? SerializableClosureFactory::make($callback)
? new SerializableClosure($callback)
: $callback;
return $this;
@@ -269,4 +271,49 @@ class PendingBatch
return $batch;
}
/**
* Dispatch the batch after the response is sent to the browser.
*
* @return \Illuminate\Bus\Batch
*/
public function dispatchAfterResponse()
{
$repository = $this->container->make(BatchRepository::class);
$batch = $repository->store($this);
if ($batch) {
$this->container->terminating(function () use ($batch) {
$this->dispatchExistingBatch($batch);
});
}
return $batch;
}
/**
* Dispatch an existing batch.
*
* @param \Illuminate\Bus\Batch $batch
* @return void
*
* @throws \Throwable
*/
protected function dispatchExistingBatch($batch)
{
try {
$batch = $batch->add($this->jobs);
} catch (Throwable $e) {
if (isset($batch)) {
$batch->delete();
}
throw $e;
}
$this->container->make(EventDispatcher::class)->dispatch(
new BatchDispatched($batch)
);
}
}

View File

@@ -47,7 +47,7 @@ trait Queueable
/**
* The number of seconds before the job should be made available.
*
* @var \DateTimeInterface|\DateInterval|int|null
* @var \DateTimeInterface|\DateInterval|array|int|null
*/
public $delay;
@@ -127,9 +127,9 @@ trait Queueable
}
/**
* Set the desired delay for the job.
* Set the desired delay in seconds for the job.
*
* @param \DateTimeInterface|\DateInterval|int|null $delay
* @param \DateTimeInterface|\DateInterval|array|int|null $delay
* @return $this
*/
public function delay($delay)
@@ -191,6 +191,32 @@ trait Queueable
return $this;
}
/**
* Prepend a job to the current chain so that it is run after the currently running job.
*
* @param mixed $job
* @return $this
*/
public function prependToChain($job)
{
$this->chained = Arr::prepend($this->chained, $this->serializeJob($job));
return $this;
}
/**
* Append a job to the end of the current chain.
*
* @param mixed $job
* @return $this
*/
public function appendToChain($job)
{
$this->chained = array_merge($this->chained, [$this->serializeJob($job)]);
return $this;
}
/**
* Serialize a job for queuing.
*

View File

@@ -32,17 +32,44 @@ class UniqueLock
*/
public function acquire($job)
{
$uniqueId = method_exists($job, 'uniqueId')
? $job->uniqueId()
: ($job->uniqueId ?? '');
$uniqueFor = method_exists($job, 'uniqueFor')
? $job->uniqueFor()
: ($job->uniqueFor ?? 0);
$cache = method_exists($job, 'uniqueVia')
? $job->uniqueVia()
: $this->cache;
return (bool) $cache->lock(
$key = 'laravel_unique_job:'.get_class($job).$uniqueId,
$job->uniqueFor ?? 0
)->get();
return (bool) $cache->lock($this->getKey($job), $uniqueFor)->get();
}
/**
* Release the lock for the given job.
*
* @param mixed $job
* @return void
*/
public function release($job)
{
$cache = method_exists($job, 'uniqueVia')
? $job->uniqueVia()
: $this->cache;
$cache->lock($this->getKey($job))->forceRelease();
}
/**
* Generate the lock key for the given job.
*
* @param mixed $job
* @return string
*/
protected function getKey($job)
{
$uniqueId = method_exists($job, 'uniqueId')
? $job->uniqueId()
: ($job->uniqueId ?? '');
return 'laravel_unique_job:'.get_class($job).$uniqueId;
}
}

View File

@@ -14,11 +14,11 @@
}
],
"require": {
"php": "^7.3|^8.0",
"illuminate/collections": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/pipeline": "^8.0",
"illuminate/support": "^8.0"
"php": "^8.0.2",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/pipeline": "^9.0",
"illuminate/support": "^9.0"
},
"autoload": {
"psr-4": {
@@ -27,7 +27,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {

View File

@@ -46,7 +46,7 @@ class CacheLock extends Lock
return ($this->seconds > 0)
? $this->store->put($this->name, $this->owner, $this->seconds)
: $this->store->forever($this->name, $this->owner, $this->seconds);
: $this->store->forever($this->name, $this->owner);
}
/**

View File

@@ -11,7 +11,8 @@ use Illuminate\Support\Arr;
use InvalidArgumentException;
/**
* @mixin \Illuminate\Contracts\Cache\Repository
* @mixin \Illuminate\Cache\Repository
* @mixin \Illuminate\Contracts\Cache\LockProvider
*/
class CacheManager implements FactoryContract
{
@@ -254,7 +255,7 @@ class CacheManager implements FactoryContract
/**
* Create new DynamoDb Client instance.
*
* @return DynamoDbClient
* @return \Aws\DynamoDb\DynamoDbClient
*/
protected function newDynamodbClient(array $config)
{
@@ -264,7 +265,7 @@ class CacheManager implements FactoryContract
'endpoint' => $config['endpoint'] ?? null,
];
if (isset($config['key']) && isset($config['secret'])) {
if (isset($config['key'], $config['secret'])) {
$dynamoConfig['credentials'] = Arr::only(
$config, ['key', 'secret', 'token']
);
@@ -328,7 +329,7 @@ class CacheManager implements FactoryContract
* Get the cache connection configuration.
*
* @param string $name
* @return array
* @return array|null
*/
protected function getConfig($name)
{
@@ -368,7 +369,7 @@ class CacheManager implements FactoryContract
*/
public function forgetDriver($name = null)
{
$name = $name ?? $this->getDefaultDriver();
$name ??= $this->getDefaultDriver();
foreach ((array) $name as $cacheName) {
if (isset($this->stores[$cacheName])) {
@@ -387,7 +388,7 @@ class CacheManager implements FactoryContract
*/
public function purge($name = null)
{
$name = $name ?? $this->getDefaultDriver();
$name ??= $this->getDefaultDriver();
unset($this->stores[$name]);
}

View File

@@ -5,7 +5,9 @@ namespace Illuminate\Cache\Console;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Composer;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cache:table')]
class CacheTableCommand extends Command
{
/**
@@ -15,6 +17,17 @@ class CacheTableCommand extends Command
*/
protected $name = 'cache:table';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'cache:table';
/**
* The console command description.
*
@@ -60,7 +73,7 @@ class CacheTableCommand extends Command
$this->files->put($fullPath, $this->files->get(__DIR__.'/stubs/cache.stub'));
$this->info('Migration created successfully!');
$this->components->info('Migration created successfully.');
$this->composer->dumpAutoloads();
}

View File

@@ -5,9 +5,11 @@ namespace Illuminate\Cache\Console;
use Illuminate\Cache\CacheManager;
use Illuminate\Console\Command;
use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
#[AsCommand(name: 'cache:clear')]
class ClearCommand extends Command
{
/**
@@ -17,6 +19,17 @@ class ClearCommand extends Command
*/
protected $name = 'cache:clear';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'cache:clear';
/**
* The console command description.
*
@@ -69,14 +82,14 @@ class ClearCommand extends Command
$this->flushFacades();
if (! $successful) {
return $this->error('Failed to clear cache. Make sure you have the appropriate permissions.');
return $this->components->error('Failed to clear cache. Make sure you have the appropriate permissions.');
}
$this->laravel['events']->dispatch(
'cache:cleared', [$this->argument('store'), $this->tags()]
);
$this->info('Application cache cleared!');
$this->components->info('Application cache cleared successfully.');
}
/**

View File

@@ -4,7 +4,9 @@ namespace Illuminate\Cache\Console;
use Illuminate\Cache\CacheManager;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'cache:forget')]
class ForgetCommand extends Command
{
/**
@@ -14,6 +16,17 @@ class ForgetCommand extends Command
*/
protected $signature = 'cache:forget {key : The key to remove} {store? : The store to remove the key from}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'cache:forget';
/**
* The console command description.
*
@@ -52,6 +65,6 @@ class ForgetCommand extends Command
$this->argument('key')
);
$this->info('The ['.$this->argument('key').'] key has been removed from the cache.');
$this->components->info('The ['.$this->argument('key').'] key has been removed from the cache.');
}
}

View File

@@ -4,7 +4,7 @@ use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCacheTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -36,4 +36,4 @@ class CreateCacheTable extends Migration
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
}
};

View File

@@ -56,8 +56,6 @@ class DatabaseLock extends Lock
*/
public function acquire()
{
$acquired = false;
try {
$this->connection->table($this->table)->insert([
'key' => $this->name,

View File

@@ -371,7 +371,7 @@ class DatabaseStore implements LockProvider, Store
{
$result = serialize($value);
if ($this->connection instanceof PostgresConnection && Str::contains($result, "\0")) {
if ($this->connection instanceof PostgresConnection && str_contains($result, "\0")) {
$result = base64_encode($result);
}

View File

@@ -211,7 +211,7 @@ class DynamoDbStore implements LockProvider, Store
}
/**
* Store multiple items in the cache for a given number of $seconds.
* Store multiple items in the cache for a given number of seconds.
*
* @param array $values
* @param int $seconds
@@ -284,7 +284,7 @@ class DynamoDbStore implements LockProvider, Store
return true;
} catch (DynamoDbException $e) {
if (Str::contains($e->getMessage(), 'ConditionalCheckFailed')) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
@@ -329,7 +329,7 @@ class DynamoDbStore implements LockProvider, Store
return (int) $response['Attributes'][$this->valueAttribute]['N'];
} catch (DynamoDbException $e) {
if (Str::contains($e->getMessage(), 'ConditionalCheckFailed')) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
@@ -374,7 +374,7 @@ class DynamoDbStore implements LockProvider, Store
return (int) $response['Attributes'][$this->valueAttribute]['N'];
} catch (DynamoDbException $e) {
if (Str::contains($e->getMessage(), 'ConditionalCheckFailed')) {
if (str_contains($e->getMessage(), 'ConditionalCheckFailed')) {
return false;
}
@@ -529,7 +529,7 @@ class DynamoDbStore implements LockProvider, Store
/**
* Get the DynamoDb Client instance.
*
* @return DynamoDbClient
* @return \Aws\DynamoDb\DynamoDbClient
*/
public function getClient()
{

View File

@@ -89,10 +89,8 @@ class RateLimiter
*/
public function tooManyAttempts($key, $maxAttempts)
{
$key = $this->cleanRateLimiterKey($key);
if ($this->attempts($key) >= $maxAttempts) {
if ($this->cache->has($key.':timer')) {
if ($this->cache->has($this->cleanRateLimiterKey($key).':timer')) {
return true;
}

View File

@@ -7,7 +7,7 @@ class Limit
/**
* The rate limit signature key.
*
* @var mixed|string
* @var mixed
*/
public $key;
@@ -35,7 +35,7 @@ class Limit
/**
* Create a new limit instance.
*
* @param mixed|string $key
* @param mixed $key
* @param int $maxAttempts
* @param int $decayMinutes
* @return void
@@ -107,7 +107,7 @@ class Limit
/**
* Set the key of the rate limit.
*
* @param string $key
* @param mixed $key
* @return $this
*/
public function by($key)

View File

@@ -62,10 +62,10 @@ class Repository implements ArrayAccess, CacheContract
/**
* Determine if an item exists in the cache.
*
* @param string $key
* @param array|string $key
* @return bool
*/
public function has($key)
public function has($key): bool
{
return ! is_null($this->get($key));
}
@@ -84,11 +84,13 @@ class Repository implements ArrayAccess, CacheContract
/**
* Retrieve an item from the cache by key.
*
* @param string $key
* @param mixed $default
* @return mixed
* @template TCacheValue
*
* @param array|string $key
* @param TCacheValue|(\Closure(): TCacheValue) $default
* @return (TCacheValue is null ? mixed : TCacheValue)
*/
public function get($key, $default = null)
public function get($key, $default = null): mixed
{
if (is_array($key)) {
return $this->many($key);
@@ -134,7 +136,7 @@ class Repository implements ArrayAccess, CacheContract
*
* @return iterable
*/
public function getMultiple($keys, $default = null)
public function getMultiple($keys, $default = null): iterable
{
$defaults = [];
@@ -175,9 +177,11 @@ class Repository implements ArrayAccess, CacheContract
/**
* Retrieve an item from the cache and delete it.
*
* @param string $key
* @param mixed $default
* @return mixed
* @template TCacheValue
*
* @param array|string $key
* @param TCacheValue|(\Closure(): TCacheValue) $default
* @return (TCacheValue is null ? mixed : TCacheValue)
*/
public function pull($key, $default = null)
{
@@ -189,7 +193,7 @@ class Repository implements ArrayAccess, CacheContract
/**
* Store an item in the cache.
*
* @param string $key
* @param array|string $key
* @param mixed $value
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @return bool
@@ -224,7 +228,7 @@ class Repository implements ArrayAccess, CacheContract
*
* @return bool
*/
public function set($key, $value, $ttl = null)
public function set($key, $value, $ttl = null): bool
{
return $this->put($key, $value, $ttl);
}
@@ -283,7 +287,7 @@ class Repository implements ArrayAccess, CacheContract
*
* @return bool
*/
public function setMultiple($values, $ttl = null)
public function setMultiple($values, $ttl = null): bool
{
return $this->putMany(is_array($values) ? $values : iterator_to_array($values), $ttl);
}
@@ -372,10 +376,12 @@ class Repository implements ArrayAccess, CacheContract
/**
* Get an item from the cache, or execute the given Closure and store the result.
*
* @template TCacheValue
*
* @param string $key
* @param \Closure|\DateTimeInterface|\DateInterval|int|null $ttl
* @param \Closure $callback
* @return mixed
* @param \Closure(): TCacheValue $callback
* @return TCacheValue
*/
public function remember($key, $ttl, Closure $callback)
{
@@ -396,9 +402,11 @@ class Repository implements ArrayAccess, CacheContract
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @template TCacheValue
*
* @param string $key
* @param \Closure $callback
* @return mixed
* @param \Closure(): TCacheValue $callback
* @return TCacheValue
*/
public function sear($key, Closure $callback)
{
@@ -408,9 +416,11 @@ class Repository implements ArrayAccess, CacheContract
/**
* Get an item from the cache, or execute the given Closure and store the result forever.
*
* @template TCacheValue
*
* @param string $key
* @param \Closure $callback
* @return mixed
* @param \Closure(): TCacheValue $callback
* @return TCacheValue
*/
public function rememberForever($key, Closure $callback)
{
@@ -448,7 +458,7 @@ class Repository implements ArrayAccess, CacheContract
*
* @return bool
*/
public function delete($key)
public function delete($key): bool
{
return $this->forget($key);
}
@@ -458,7 +468,7 @@ class Repository implements ArrayAccess, CacheContract
*
* @return bool
*/
public function deleteMultiple($keys)
public function deleteMultiple($keys): bool
{
$result = true;
@@ -476,7 +486,7 @@ class Repository implements ArrayAccess, CacheContract
*
* @return bool
*/
public function clear()
public function clear(): bool
{
return $this->store->flush();
}
@@ -583,9 +593,7 @@ class Repository implements ArrayAccess, CacheContract
*/
protected function event($event)
{
if (isset($this->events)) {
$this->events->dispatch($event);
}
$this->events?->dispatch($event);
}
/**
@@ -615,8 +623,7 @@ class Repository implements ArrayAccess, CacheContract
* @param string $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
public function offsetExists($key): bool
{
return $this->has($key);
}
@@ -627,8 +634,7 @@ class Repository implements ArrayAccess, CacheContract
* @param string $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
public function offsetGet($key): mixed
{
return $this->get($key);
}
@@ -640,8 +646,7 @@ class Repository implements ArrayAccess, CacheContract
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
public function offsetSet($key, $value): void
{
$this->put($key, $value, $this->default);
}
@@ -652,8 +657,7 @@ class Repository implements ArrayAccess, CacheContract
* @param string $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
public function offsetUnset($key): void
{
$this->forget($key);
}

View File

@@ -14,14 +14,14 @@
}
],
"require": {
"php": "^7.3|^8.0",
"illuminate/collections": "^8.0",
"illuminate/contracts": "^8.0",
"illuminate/macroable": "^8.0",
"illuminate/support": "^8.0"
"php": "^8.0.2",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0",
"illuminate/support": "^9.0"
},
"provide": {
"psr/simple-cache-implementation": "1.0"
"psr/simple-cache-implementation": "1.0|2.0|3.0"
},
"autoload": {
"psr-4": {
@@ -30,15 +30,15 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"ext-memcached": "Required to use the memcache cache driver.",
"illuminate/database": "Required to use the database cache driver (^8.0).",
"illuminate/filesystem": "Required to use the file cache driver (^8.0).",
"illuminate/redis": "Required to use the redis cache driver (^8.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^5.4)."
"illuminate/database": "Required to use the database cache driver (^9.0).",
"illuminate/filesystem": "Required to use the file cache driver (^9.0).",
"illuminate/redis": "Required to use the redis cache driver (^9.0).",
"symfony/cache": "Required to use PSR-6 cache bridge (^6.0)."
},
"config": {
"sort-packages": true

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Support;
use ArgumentCountError;
use ArrayAccess;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
@@ -25,7 +26,7 @@ class Arr
* Add an element to an array using "dot" notation if it doesn't exist.
*
* @param array $array
* @param string $key
* @param string|int|float $key
* @param mixed $value
* @return array
*/
@@ -142,7 +143,7 @@ class Arr
* Get all of the given array except for a specified array of keys.
*
* @param array $array
* @param array|string $keys
* @param array|string|int|float $keys
* @return array
*/
public static function except($array, $keys)
@@ -169,6 +170,10 @@ class Arr
return $array->offsetExists($key);
}
if (is_float($key)) {
$key = (string) $key;
}
return array_key_exists($key, $array);
}
@@ -252,7 +257,7 @@ class Arr
* Remove one or many array items from a given array using "dot" notation.
*
* @param array $array
* @param array|string $keys
* @param array|string|int|float $keys
* @return void
*/
public static function forget(&$array, $keys)
@@ -281,7 +286,7 @@ class Arr
while (count($parts) > 1) {
$part = array_shift($parts);
if (isset($array[$part]) && is_array($array[$part])) {
if (isset($array[$part]) && static::accessible($array[$part])) {
$array = &$array[$part];
} else {
continue 2;
@@ -314,7 +319,7 @@ class Arr
return $array[$key];
}
if (strpos($key, '.') === false) {
if (! str_contains($key, '.')) {
return $array[$key] ?? value($default);
}
@@ -423,6 +428,59 @@ class Arr
return ! self::isAssoc($array);
}
/**
* Join all items using a string. The final items can use a separate glue string.
*
* @param array $array
* @param string $glue
* @param string $finalGlue
* @return string
*/
public static function join($array, $glue, $finalGlue = '')
{
if ($finalGlue === '') {
return implode($glue, $array);
}
if (count($array) === 0) {
return '';
}
if (count($array) === 1) {
return end($array);
}
$finalItem = array_pop($array);
return implode($glue, $array).$finalGlue.$finalItem;
}
/**
* Key an associative array by a field or using a callback.
*
* @param array $array
* @param callable|array|string $keyBy
* @return array
*/
public static function keyBy($array, $keyBy)
{
return Collection::make($array)->keyBy($keyBy)->all();
}
/**
* Prepend the key names of an associative array.
*
* @param array $array
* @param string $prependWith
* @return array
*/
public static function prependKeysWith($array, $prependWith)
{
return Collection::make($array)->mapWithKeys(function ($item, $key) use ($prependWith) {
return [$prependWith.$key => $item];
})->all();
}
/**
* Get a subset of the items from the given array.
*
@@ -487,6 +545,26 @@ class Arr
return [$value, $key];
}
/**
* Run a map over each of the items in the array.
*
* @param array $array
* @param callable $callback
* @return array
*/
public static function map(array $array, callable $callback)
{
$keys = array_keys($array);
try {
$items = array_map($callback, $array, $keys);
} catch (ArgumentCountError) {
$items = array_map($callback, $array);
}
return array_combine($keys, $items);
}
/**
* Push an item onto the beginning of an array.
*
@@ -539,7 +617,7 @@ class Arr
*
* @param array $array
* @param int|null $number
* @param bool|false $preserveKeys
* @param bool $preserveKeys
* @return mixed
*
* @throws \InvalidArgumentException
@@ -587,7 +665,7 @@ class Arr
* If no key is given to the method, the entire array will be replaced.
*
* @param array $array
* @param string|null $key
* @param string|int|null $key
* @param mixed $value
* @return array
*/

View File

@@ -8,22 +8,33 @@ use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
use Illuminate\Support\Traits\EnumeratesValues;
use Illuminate\Support\Traits\Macroable;
use stdClass;
use Traversable;
/**
* @template TKey of array-key
* @template TValue
*
* @implements \ArrayAccess<TKey, TValue>
* @implements \Illuminate\Support\Enumerable<TKey, TValue>
*/
class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerable
{
/**
* @use \Illuminate\Support\Traits\EnumeratesValues<TKey, TValue>
*/
use EnumeratesValues, Macroable;
/**
* The items contained in the collection.
*
* @var array
* @var array<TKey, TValue>
*/
protected $items = [];
/**
* Create a new collection.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>|null $items
* @return void
*/
public function __construct($items = [])
@@ -36,7 +47,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*
* @param int $from
* @param int $to
* @return static
* @return static<int, int>
*/
public static function range($from, $to)
{
@@ -46,7 +57,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get all of the items in the collection.
*
* @return array
* @return array<TKey, TValue>
*/
public function all()
{
@@ -56,7 +67,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get a lazy collection for the items in this collection.
*
* @return \Illuminate\Support\LazyCollection
* @return \Illuminate\Support\LazyCollection<TKey, TValue>
*/
public function lazy()
{
@@ -66,8 +77,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the average value of a given key.
*
* @param callable|string|null $callback
* @return mixed
* @param (callable(TValue): float|int)|string|null $callback
* @return float|int|null
*/
public function avg($callback = null)
{
@@ -87,15 +98,14 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the median of a given key.
*
* @param string|array|null $key
* @return mixed
* @param string|array<array-key, string>|null $key
* @return float|int|null
*/
public function median($key = null)
{
$values = (isset($key) ? $this->pluck($key) : $this)
->filter(function ($item) {
return ! is_null($item);
})->sort()->values();
->filter(fn ($item) => ! is_null($item))
->sort()->values();
$count = $values->count();
@@ -117,8 +127,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the mode of a given key.
*
* @param string|array|null $key
* @return array|null
* @param string|array<array-key, string>|null $key
* @return array<int, float|int>|null
*/
public function mode($key = null)
{
@@ -130,23 +140,20 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
$counts = new static;
$collection->each(function ($value) use ($counts) {
$counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1;
});
$collection->each(fn ($value) => $counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1);
$sorted = $counts->sort();
$highestValue = $sorted->last();
return $sorted->filter(function ($value) use ($highestValue) {
return $value == $highestValue;
})->sort()->keys()->all();
return $sorted->filter(fn ($value) => $value == $highestValue)
->sort()->keys()->all();
}
/**
* Collapse the collection of items into a single array.
*
* @return static
* @return static<int, mixed>
*/
public function collapse()
{
@@ -156,7 +163,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Determine if an item exists in the collection.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|TValue|string $key
* @param mixed $operator
* @param mixed $value
* @return bool
@@ -176,6 +183,26 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
return $this->contains($this->operatorForWhere(...func_get_args()));
}
/**
* Determine if an item exists, using strict comparison.
*
* @param (callable(TValue): bool)|TValue|array-key $key
* @param TValue|null $value
* @return bool
*/
public function containsStrict($key, $value = null)
{
if (func_num_args() === 2) {
return $this->contains(fn ($item) => data_get($item, $key) === $value);
}
if ($this->useAsCallable($key)) {
return ! is_null($this->first($key));
}
return in_array($key, $this->items, true);
}
/**
* Determine if an item is not contained in the collection.
*
@@ -192,8 +219,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Cross join with the given lists, returning all possible permutations.
*
* @param mixed ...$lists
* @return static
* @template TCrossJoinKey
* @template TCrossJoinValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TCrossJoinKey, TCrossJoinValue>|iterable<TCrossJoinKey, TCrossJoinValue> ...$lists
* @return static<int, array<int, TValue|TCrossJoinValue>>
*/
public function crossJoin(...$lists)
{
@@ -205,7 +235,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items in the collection that are not present in the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @return static
*/
public function diff($items)
@@ -216,8 +246,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items in the collection that are not present in the given items, using the callback.
*
* @param mixed $items
* @param callable $callback
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @param callable(TValue, TValue): int $callback
* @return static
*/
public function diffUsing($items, callable $callback)
@@ -228,7 +258,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items in the collection whose keys and values are not present in the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function diffAssoc($items)
@@ -239,8 +269,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items in the collection whose keys and values are not present in the given items, using the callback.
*
* @param mixed $items
* @param callable $callback
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @param callable(TKey, TKey): int $callback
* @return static
*/
public function diffAssocUsing($items, callable $callback)
@@ -251,7 +281,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items in the collection whose keys are not present in the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function diffKeys($items)
@@ -262,8 +292,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items in the collection whose keys are not present in the given items, using the callback.
*
* @param mixed $items
* @param callable $callback
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @param callable(TKey, TKey): int $callback
* @return static
*/
public function diffKeysUsing($items, callable $callback)
@@ -274,7 +304,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Retrieve duplicate items from the collection.
*
* @param callable|string|null $callback
* @param (callable(TValue): bool)|string|null $callback
* @param bool $strict
* @return static
*/
@@ -302,7 +332,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Retrieve duplicate items from the collection using strict comparison.
*
* @param callable|string|null $callback
* @param (callable(TValue): bool)|string|null $callback
* @return static
*/
public function duplicatesStrict($callback = null)
@@ -314,7 +344,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Get the comparison function to detect duplicates.
*
* @param bool $strict
* @return \Closure
* @return callable(TValue, TValue): bool
*/
protected function duplicateComparator($strict)
{
@@ -332,7 +362,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get all items except for those with the specified keys.
*
* @param \Illuminate\Support\Collection|mixed $keys
* @param \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey> $keys
* @return static
*/
public function except($keys)
@@ -349,7 +379,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Run a filter over each of the items.
*
* @param callable|null $callback
* @param (callable(TValue, TKey): bool)|null $callback
* @return static
*/
public function filter(callable $callback = null)
@@ -364,9 +394,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the first item from the collection passing the given truth test.
*
* @param callable|null $callback
* @param mixed $default
* @return mixed
* @template TFirstDefault
*
* @param (callable(TValue, TKey): bool)|null $callback
* @param TFirstDefault|(\Closure(): TFirstDefault) $default
* @return TValue|TFirstDefault
*/
public function first(callable $callback = null, $default = null)
{
@@ -377,7 +409,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Get a flattened array of the items in the collection.
*
* @param int $depth
* @return static
* @return static<int, mixed>
*/
public function flatten($depth = INF)
{
@@ -387,7 +419,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Flip the items in the collection.
*
* @return static
* @return static<TValue, TKey>
*/
public function flip()
{
@@ -397,7 +429,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Remove an item from the collection by key.
*
* @param string|int|array $keys
* @param TKey|array<array-key, TKey> $keys
* @return $this
*/
public function forget($keys)
@@ -412,9 +444,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get an item from the collection by key.
*
* @param mixed $key
* @param mixed $default
* @return mixed
* @template TGetDefault
*
* @param TKey $key
* @param TGetDefault|(\Closure(): TGetDefault) $default
* @return TValue|TGetDefault
*/
public function get($key, $default = null)
{
@@ -446,9 +480,9 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Group an associative array by a field or using a callback.
*
* @param array|callable|string $groupBy
* @param (callable(TValue, TKey): array-key)|array|string $groupBy
* @param bool $preserveKeys
* @return static
* @return static<array-key, static<array-key, TValue>>
*/
public function groupBy($groupBy, $preserveKeys = false)
{
@@ -470,7 +504,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
}
foreach ($groupKeys as $groupKey) {
$groupKey = is_bool($groupKey) ? (int) $groupKey : $groupKey;
$groupKey = match (true) {
is_bool($groupKey) => (int) $groupKey,
$groupKey instanceof \Stringable => (string) $groupKey,
default => $groupKey,
};
if (! array_key_exists($groupKey, $results)) {
$results[$groupKey] = new static;
@@ -492,8 +530,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Key an associative array by a field or using a callback.
*
* @param callable|string $keyBy
* @return static
* @param (callable(TValue, TKey): array-key)|array|string $keyBy
* @return static<array-key, TValue>
*/
public function keyBy($keyBy)
{
@@ -517,7 +555,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Determine if an item exists in the collection by key.
*
* @param mixed $key
* @param TKey|array<array-key, TKey> $key
* @return bool
*/
public function has($key)
@@ -559,12 +597,16 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Concatenate values of a given key as a string.
*
* @param string $value
* @param callable|string $value
* @param string|null $glue
* @return string
*/
public function implode($value, $glue = null)
{
if ($this->useAsCallable($value)) {
return implode($glue ?? '', $this->map($value)->all());
}
$first = $this->first();
if (is_array($first) || (is_object($first) && ! $first instanceof Stringable)) {
@@ -577,7 +619,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Intersect the collection with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function intersect($items)
@@ -588,7 +630,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Intersect the collection with the given items by key.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function intersectByKeys($items)
@@ -651,7 +693,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the keys of the collection items.
*
* @return static
* @return static<int, TKey>
*/
public function keys()
{
@@ -661,9 +703,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the last item from the collection.
*
* @param callable|null $callback
* @param mixed $default
* @return mixed
* @template TLastDefault
*
* @param (callable(TValue, TKey): bool)|null $callback
* @param TLastDefault|(\Closure(): TLastDefault) $default
* @return TValue|TLastDefault
*/
public function last(callable $callback = null, $default = null)
{
@@ -673,9 +717,9 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the values of a given key.
*
* @param string|array|int|null $value
* @param string|int|array<array-key, string> $value
* @param string|null $key
* @return static
* @return static<int, mixed>
*/
public function pluck($value, $key = null)
{
@@ -685,16 +729,14 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Run a map over each of the items.
*
* @param callable $callback
* @return static
* @template TMapValue
*
* @param callable(TValue, TKey): TMapValue $callback
* @return static<TKey, TMapValue>
*/
public function map(callable $callback)
{
$keys = array_keys($this->items);
$items = array_map($callback, $this->items, $keys);
return new static(array_combine($keys, $items));
return new static(Arr::map($this->items, $callback));
}
/**
@@ -702,8 +744,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*
* The callback should return an associative array with a single key/value pair.
*
* @param callable $callback
* @return static
* @template TMapToDictionaryKey of array-key
* @template TMapToDictionaryValue
*
* @param callable(TValue, TKey): array<TMapToDictionaryKey, TMapToDictionaryValue> $callback
* @return static<TMapToDictionaryKey, array<int, TMapToDictionaryValue>>
*/
public function mapToDictionary(callable $callback)
{
@@ -731,8 +776,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*
* The callback should return an associative array with a single key/value pair.
*
* @param callable $callback
* @return static
* @template TMapWithKeysKey of array-key
* @template TMapWithKeysValue
*
* @param callable(TValue, TKey): array<TMapWithKeysKey, TMapWithKeysValue> $callback
* @return static<TMapWithKeysKey, TMapWithKeysValue>
*/
public function mapWithKeys(callable $callback)
{
@@ -752,7 +800,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Merge the collection with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function merge($items)
@@ -763,8 +811,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Recursively merge the collection with the given items.
*
* @param mixed $items
* @return static
* @template TMergeRecursiveValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TMergeRecursiveValue>|iterable<TKey, TMergeRecursiveValue> $items
* @return static<TKey, TValue|TMergeRecursiveValue>
*/
public function mergeRecursive($items)
{
@@ -774,8 +824,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Create a collection by using this collection for keys and another for its values.
*
* @param mixed $values
* @return static
* @template TCombineValue
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TCombineValue>|iterable<array-key, TCombineValue> $values
* @return static<TValue, TCombineValue>
*/
public function combine($values)
{
@@ -785,7 +837,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Union the collection with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function union($items)
@@ -820,7 +872,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the items with the specified keys.
*
* @param mixed $keys
* @param \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey>|string $keys
* @return static
*/
public function only($keys)
@@ -842,7 +894,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Get and remove the last N items from the collection.
*
* @param int $count
* @return mixed
* @return static<int, TValue>|TValue|null
*/
public function pop($count = 1)
{
@@ -868,8 +920,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Push an item onto the beginning of the collection.
*
* @param mixed $value
* @param mixed $key
* @param TValue $value
* @param TKey $key
* @return $this
*/
public function prepend($value, $key = null)
@@ -882,7 +934,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Push one or more items onto the end of the collection.
*
* @param mixed $values
* @param TValue ...$values
* @return $this
*/
public function push(...$values)
@@ -897,7 +949,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Push all of the given items onto the collection.
*
* @param iterable $source
* @param iterable<array-key, TValue> $source
* @return static
*/
public function concat($source)
@@ -914,9 +966,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get and remove an item from the collection.
*
* @param mixed $key
* @param mixed $default
* @return mixed
* @template TPullDefault
*
* @param TKey $key
* @param TPullDefault|(\Closure(): TPullDefault) $default
* @return TValue|TPullDefault
*/
public function pull($key, $default = null)
{
@@ -926,8 +980,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Put an item in the collection by key.
*
* @param mixed $key
* @param mixed $value
* @param TKey $key
* @param TValue $value
* @return $this
*/
public function put($key, $value)
@@ -940,8 +994,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get one or a specified number of items randomly from the collection.
*
* @param int|null $number
* @return static|mixed
* @param (callable(self<TKey, TValue>): int)|int|null $number
* @return static<int, TValue>|TValue
*
* @throws \InvalidArgumentException
*/
@@ -951,13 +1005,17 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
return Arr::random($this->items);
}
if (is_callable($number)) {
return new static(Arr::random($this->items, $number($this)));
}
return new static(Arr::random($this->items, $number));
}
/**
* Replace the collection items with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function replace($items)
@@ -968,7 +1026,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Recursively replace the collection items with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function replaceRecursive($items)
@@ -989,9 +1047,9 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Search the collection for a given value and return the corresponding key if successful.
*
* @param mixed $value
* @param TValue|(callable(TValue,TKey): bool) $value
* @param bool $strict
* @return mixed
* @return TKey|bool
*/
public function search($value, $strict = false)
{
@@ -1012,7 +1070,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Get and remove the first N items from the collection.
*
* @param int $count
* @return mixed
* @return static<int, TValue>|TValue|null
*/
public function shift($count = 1)
{
@@ -1051,7 +1109,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*
* @param int $size
* @param int $step
* @return static
* @return static<int, static>
*/
public function sliding($size = 2, $step = 1)
{
@@ -1076,7 +1134,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Skip items in the collection until the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function skipUntil($value)
@@ -1087,7 +1145,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Skip items in the collection while the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function skipWhile($value)
@@ -1111,7 +1169,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Split a collection into a certain number of groups.
*
* @param int $numberOfGroups
* @return static
* @return static<int, static>
*/
public function split($numberOfGroups)
{
@@ -1148,7 +1206,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Split a collection into a certain number of groups, and fill the first groups completely.
*
* @param int $numberOfGroups
* @return static
* @return static<int, static>
*/
public function splitIn($numberOfGroups)
{
@@ -1158,10 +1216,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|string $key
* @param mixed $operator
* @param mixed $value
* @return mixed
* @return TValue
*
* @throws \Illuminate\Support\ItemNotFoundException
* @throws \Illuminate\Support\MultipleItemsFoundException
@@ -1172,14 +1230,16 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
? $this->operatorForWhere(...func_get_args())
: $key;
$items = $this->when($filter)->filter($filter);
$items = $this->unless($filter == null)->filter($filter);
if ($items->isEmpty()) {
$count = $items->count();
if ($count === 0) {
throw new ItemNotFoundException;
}
if ($items->count() > 1) {
throw new MultipleItemsFoundException;
if ($count > 1) {
throw new MultipleItemsFoundException($count);
}
return $items->first();
@@ -1188,10 +1248,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get the first item in the collection but throw an exception if no matching items exist.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|string $key
* @param mixed $operator
* @param mixed $value
* @return mixed
* @return TValue
*
* @throws \Illuminate\Support\ItemNotFoundException
*/
@@ -1216,7 +1276,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* Chunk the collection into chunks of the given size.
*
* @param int $size
* @return static
* @return static<int, static>
*/
public function chunk($size)
{
@@ -1236,8 +1296,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Chunk the collection into chunks with a callback.
*
* @param callable $callback
* @return static
* @param callable(TValue, TKey, static<int, TValue>): bool $callback
* @return static<int, static<int, TValue>>
*/
public function chunkWhile(callable $callback)
{
@@ -1249,7 +1309,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Sort through each item with a callback.
*
* @param callable|int|null $callback
* @param (callable(TValue, TValue): int)|null|int $callback
* @return static
*/
public function sort($callback = null)
@@ -1281,7 +1341,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Sort the collection using the given callback.
*
* @param callable|array|string $callback
* @param array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string $callback
* @param int $options
* @param bool $descending
* @return static
@@ -1319,14 +1379,14 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Sort the collection using multiple comparisons.
*
* @param array $comparisons
* @param array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}> $comparisons
* @return static
*/
protected function sortByMany(array $comparisons = [])
{
$items = $this->items;
usort($items, function ($a, $b) use ($comparisons) {
uasort($items, function ($a, $b) use ($comparisons) {
foreach ($comparisons as $comparison) {
$comparison = Arr::wrap($comparison);
@@ -1335,8 +1395,6 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
$ascending = Arr::get($comparison, 1, true) === true ||
Arr::get($comparison, 1, true) === 'asc';
$result = 0;
if (! is_string($prop) && is_callable($prop)) {
$result = $prop($a, $b);
} else {
@@ -1363,7 +1421,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Sort the collection in descending order using the given callback.
*
* @param callable|string $callback
* @param array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string $callback
* @param int $options
* @return static
*/
@@ -1402,7 +1460,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Sort the collection keys using a callback.
*
* @param callable $callback
* @param callable(TKey, TKey): int $callback
* @return static
*/
public function sortKeysUsing(callable $callback)
@@ -1419,7 +1477,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*
* @param int $offset
* @param int|null $length
* @param mixed $replacement
* @param array<array-key, TValue> $replacement
* @return static
*/
public function splice($offset, $length = null, $replacement = [])
@@ -1449,7 +1507,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Take items in the collection until the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function takeUntil($value)
@@ -1460,7 +1518,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Take items in the collection while the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function takeWhile($value)
@@ -1471,7 +1529,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Transform each item in the collection using a callback.
*
* @param callable $callback
* @param callable(TValue, TKey): TValue $callback
* @return $this
*/
public function transform(callable $callback)
@@ -1494,7 +1552,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Return only unique items from the collection array.
*
* @param string|callable|null $key
* @param (callable(TValue, TKey): mixed)|string|null $key
* @param bool $strict
* @return static
*/
@@ -1520,7 +1578,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Reset the keys on the underlying array.
*
* @return static
* @return static<int, TValue>
*/
public function values()
{
@@ -1533,8 +1591,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
* e.g. new Collection([1, 2, 3])->zip([4, 5, 6]);
* => [[1, 4], [2, 5], [3, 6]]
*
* @param mixed ...$items
* @return static
* @template TZipValue
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue> ...$items
* @return static<int, static<int, TValue|TZipValue>>
*/
public function zip($items)
{
@@ -1552,9 +1612,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Pad collection to the specified length with a value.
*
* @template TPadValue
*
* @param int $size
* @param mixed $value
* @return static
* @param TPadValue $value
* @return static<int, TValue|TPadValue>
*/
public function pad($size, $value)
{
@@ -1564,10 +1626,9 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get an iterator for the items.
*
* @return \ArrayIterator
* @return \ArrayIterator<TKey, TValue>
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Traversable
{
return new ArrayIterator($this->items);
}
@@ -1577,8 +1638,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
public function count(): int
{
return count($this->items);
}
@@ -1586,8 +1646,8 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Count the number of items in the collection by a field or using a callback.
*
* @param callable|string $countBy
* @return static
* @param (callable(TValue, TKey): mixed)|string|null $countBy
* @return static<array-key, int>
*/
public function countBy($countBy = null)
{
@@ -1597,7 +1657,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Add an item to the collection.
*
* @param mixed $item
* @param TValue $item
* @return $this
*/
public function add($item)
@@ -1610,7 +1670,7 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get a base Support collection instance from this collection.
*
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<TKey, TValue>
*/
public function toBase()
{
@@ -1620,11 +1680,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Determine if an item exists at an offset.
*
* @param mixed $key
* @param TKey $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
public function offsetExists($key): bool
{
return isset($this->items[$key]);
}
@@ -1632,11 +1691,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Get an item at a given offset.
*
* @param mixed $key
* @return mixed
* @param TKey $key
* @return TValue
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
public function offsetGet($key): mixed
{
return $this->items[$key];
}
@@ -1644,12 +1702,11 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Set the item at a given offset.
*
* @param mixed $key
* @param mixed $value
* @param TKey|null $key
* @param TValue $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
public function offsetSet($key, $value): void
{
if (is_null($key)) {
$this->items[] = $value;
@@ -1661,11 +1718,10 @@ class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerabl
/**
* Unset the item at a given offset.
*
* @param mixed $key
* @param TKey $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
public function offsetUnset($key): void
{
unset($this->items[$key]);
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +0,0 @@
<?php
namespace Illuminate\Support;
/**
* @mixin \Illuminate\Support\Enumerable
*/
class HigherOrderWhenProxy
{
/**
* The collection being operated on.
*
* @var \Illuminate\Support\Enumerable
*/
protected $collection;
/**
* The condition for proxying.
*
* @var bool
*/
protected $condition;
/**
* Create a new proxy instance.
*
* @param \Illuminate\Support\Enumerable $collection
* @param bool $condition
* @return void
*/
public function __construct(Enumerable $collection, $condition)
{
$this->condition = $condition;
$this->collection = $collection;
}
/**
* Proxy accessing an attribute onto the collection.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->condition
? $this->collection->{$key}
: $this->collection;
}
/**
* Proxy a method call onto the collection.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
return $this->condition
? $this->collection->{$method}(...$parameters)
: $this->collection;
}
}

View File

@@ -5,27 +5,39 @@ namespace Illuminate\Support;
use ArrayIterator;
use Closure;
use DateTimeInterface;
use Generator;
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
use Illuminate\Support\Traits\EnumeratesValues;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use IteratorAggregate;
use stdClass;
use Traversable;
/**
* @template TKey of array-key
* @template TValue
*
* @implements \Illuminate\Support\Enumerable<TKey, TValue>
*/
class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
{
/**
* @use \Illuminate\Support\Traits\EnumeratesValues<TKey, TValue>
*/
use EnumeratesValues, Macroable;
/**
* The source from which to generate items.
*
* @var callable|static
* @var (Closure(): \Generator<TKey, TValue, mixed, void>)|static|array<TKey, TValue>
*/
public $source;
/**
* Create a new lazy collection instance.
*
* @param mixed $source
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>|(Closure(): \Generator<TKey, TValue, mixed, void>)|self<TKey, TValue>|array<TKey, TValue>|null $source
* @return void
*/
public function __construct($source = null)
@@ -34,17 +46,35 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
$this->source = $source;
} elseif (is_null($source)) {
$this->source = static::empty();
} elseif ($source instanceof Generator) {
throw new InvalidArgumentException(
'Generators should not be passed directly to LazyCollection. Instead, pass a generator function.'
);
} else {
$this->source = $this->getArrayableItems($source);
}
}
/**
* Create a new collection instance if the value isn't one already.
*
* @template TMakeKey of array-key
* @template TMakeValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TMakeKey, TMakeValue>|iterable<TMakeKey, TMakeValue>|(Closure(): \Generator<TMakeKey, TMakeValue, mixed, void>)|self<TMakeKey, TMakeValue>|array<TMakeKey, TMakeValue>|null $items
* @return static<TMakeKey, TMakeValue>
*/
public static function make($items = [])
{
return new static($items);
}
/**
* Create a collection with the given range.
*
* @param int $from
* @param int $to
* @return static
* @return static<int, int>
*/
public static function range($from, $to)
{
@@ -64,7 +94,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get all items in the enumerable.
*
* @return array
* @return array<TKey, TValue>
*/
public function all()
{
@@ -126,8 +156,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the average value of a given key.
*
* @param callable|string|null $callback
* @return mixed
* @param (callable(TValue): float|int)|string|null $callback
* @return float|int|null
*/
public function avg($callback = null)
{
@@ -137,8 +167,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the median of a given key.
*
* @param string|array|null $key
* @return mixed
* @param string|array<array-key, string>|null $key
* @return float|int|null
*/
public function median($key = null)
{
@@ -148,8 +178,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the mode of a given key.
*
* @param string|array|null $key
* @return array|null
* @param string|array<string>|null $key
* @return array<int, float|int>|null
*/
public function mode($key = null)
{
@@ -159,7 +189,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Collapse the collection of items into a single array.
*
* @return static
* @return static<int, mixed>
*/
public function collapse()
{
@@ -177,7 +207,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Determine if an item exists in the enumerable.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|TValue|string $key
* @param mixed $operator
* @param mixed $value
* @return bool
@@ -187,6 +217,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
if (func_num_args() === 1 && $this->useAsCallable($key)) {
$placeholder = new stdClass;
/** @var callable $key */
return $this->first($key, $placeholder) !== $placeholder;
}
@@ -205,6 +236,32 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
return $this->contains($this->operatorForWhere(...func_get_args()));
}
/**
* Determine if an item exists, using strict comparison.
*
* @param (callable(TValue): bool)|TValue|array-key $key
* @param TValue|null $value
* @return bool
*/
public function containsStrict($key, $value = null)
{
if (func_num_args() === 2) {
return $this->contains(fn ($item) => data_get($item, $key) === $value);
}
if ($this->useAsCallable($key)) {
return ! is_null($this->first($key));
}
foreach ($this as $item) {
if ($item === $key) {
return true;
}
}
return false;
}
/**
* Determine if an item is not contained in the enumerable.
*
@@ -221,8 +278,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Cross join the given iterables, returning all possible permutations.
*
* @param array ...$arrays
* @return static
* @template TCrossJoinKey
* @template TCrossJoinValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TCrossJoinKey, TCrossJoinValue>|iterable<TCrossJoinKey, TCrossJoinValue> ...$arrays
* @return static<int, array<int, TValue|TCrossJoinValue>>
*/
public function crossJoin(...$arrays)
{
@@ -232,8 +292,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Count the number of items in the collection by a field or using a callback.
*
* @param callable|string $countBy
* @return static
* @param (callable(TValue, TKey): mixed)|string|null $countBy
* @return static<array-key, int>
*/
public function countBy($countBy = null)
{
@@ -261,7 +321,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items that are not present in the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @return static
*/
public function diff($items)
@@ -272,8 +332,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items that are not present in the given items, using the callback.
*
* @param mixed $items
* @param callable $callback
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TValue>|iterable<array-key, TValue> $items
* @param callable(TValue, TValue): int $callback
* @return static
*/
public function diffUsing($items, callable $callback)
@@ -284,7 +344,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items whose keys and values are not present in the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function diffAssoc($items)
@@ -295,8 +355,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items whose keys and values are not present in the given items, using the callback.
*
* @param mixed $items
* @param callable $callback
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @param callable(TKey, TKey): int $callback
* @return static
*/
public function diffAssocUsing($items, callable $callback)
@@ -307,7 +367,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items whose keys are not present in the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function diffKeys($items)
@@ -318,8 +378,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items whose keys are not present in the given items, using the callback.
*
* @param mixed $items
* @param callable $callback
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @param callable(TKey, TKey): int $callback
* @return static
*/
public function diffKeysUsing($items, callable $callback)
@@ -330,7 +390,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Retrieve duplicate items.
*
* @param callable|string|null $callback
* @param (callable(TValue): bool)|string|null $callback
* @param bool $strict
* @return static
*/
@@ -342,7 +402,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Retrieve duplicate items using strict comparison.
*
* @param callable|string|null $callback
* @param (callable(TValue): bool)|string|null $callback
* @return static
*/
public function duplicatesStrict($callback = null)
@@ -353,7 +413,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get all items except for those with the specified keys.
*
* @param mixed $keys
* @param \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey> $keys
* @return static
*/
public function except($keys)
@@ -364,7 +424,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Run a filter over each of the items.
*
* @param callable|null $callback
* @param (callable(TValue, TKey): bool)|null $callback
* @return static
*/
public function filter(callable $callback = null)
@@ -387,9 +447,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the first item from the enumerable passing the given truth test.
*
* @param callable|null $callback
* @param mixed $default
* @return mixed
* @template TFirstDefault
*
* @param (callable(TValue): bool)|null $callback
* @param TFirstDefault|(\Closure(): TFirstDefault) $default
* @return TValue|TFirstDefault
*/
public function first(callable $callback = null, $default = null)
{
@@ -416,7 +478,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* Get a flattened list of the items in the collection.
*
* @param int $depth
* @return static
* @return static<int, mixed>
*/
public function flatten($depth = INF)
{
@@ -438,7 +500,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Flip the items in the collection.
*
* @return static
* @return static<TValue, TKey>
*/
public function flip()
{
@@ -452,9 +514,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get an item by key.
*
* @param mixed $key
* @param mixed $default
* @return mixed
* @template TGetDefault
*
* @param TKey|null $key
* @param TGetDefault|(\Closure(): TGetDefault) $default
* @return TValue|TGetDefault
*/
public function get($key, $default = null)
{
@@ -474,9 +538,9 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Group an associative array by a field or using a callback.
*
* @param array|callable|string $groupBy
* @param (callable(TValue, TKey): array-key)|array|string $groupBy
* @param bool $preserveKeys
* @return static
* @return static<array-key, static<array-key, TValue>>
*/
public function groupBy($groupBy, $preserveKeys = false)
{
@@ -486,8 +550,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Key an associative array by a field or using a callback.
*
* @param callable|string $keyBy
* @return static
* @param (callable(TValue, TKey): array-key)|array|string $keyBy
* @return static<array-key, TValue>
*/
public function keyBy($keyBy)
{
@@ -548,7 +612,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Concatenate values of a given key as a string.
*
* @param string $value
* @param callable|string $value
* @param string|null $glue
* @return string
*/
@@ -560,7 +624,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Intersect the collection with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function intersect($items)
@@ -571,7 +635,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Intersect the collection with the given items by key.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function intersectByKeys($items)
@@ -614,7 +678,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the keys of the collection items.
*
* @return static
* @return static<int, TKey>
*/
public function keys()
{
@@ -628,9 +692,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the last item from the collection.
*
* @param callable|null $callback
* @param mixed $default
* @return mixed
* @template TLastDefault
*
* @param (callable(TValue, TKey): bool)|null $callback
* @param TLastDefault|(\Closure(): TLastDefault) $default
* @return TValue|TLastDefault
*/
public function last(callable $callback = null, $default = null)
{
@@ -648,9 +714,9 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the values of a given key.
*
* @param string|array $value
* @param string|array<array-key, string> $value
* @param string|null $key
* @return static
* @return static<int, mixed>
*/
public function pluck($value, $key = null)
{
@@ -678,8 +744,10 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Run a map over each of the items.
*
* @param callable $callback
* @return static
* @template TMapValue
*
* @param callable(TValue, TKey): TMapValue $callback
* @return static<TKey, TMapValue>
*/
public function map(callable $callback)
{
@@ -695,8 +763,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
*
* The callback should return an associative array with a single key/value pair.
*
* @param callable $callback
* @return static
* @template TMapToDictionaryKey of array-key
* @template TMapToDictionaryValue
*
* @param callable(TValue, TKey): array<TMapToDictionaryKey, TMapToDictionaryValue> $callback
* @return static<TMapToDictionaryKey, array<int, TMapToDictionaryValue>>
*/
public function mapToDictionary(callable $callback)
{
@@ -708,8 +779,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
*
* The callback should return an associative array with a single key/value pair.
*
* @param callable $callback
* @return static
* @template TMapWithKeysKey of array-key
* @template TMapWithKeysValue
*
* @param callable(TValue, TKey): array<TMapWithKeysKey, TMapWithKeysValue> $callback
* @return static<TMapWithKeysKey, TMapWithKeysValue>
*/
public function mapWithKeys(callable $callback)
{
@@ -723,7 +797,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Merge the collection with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function merge($items)
@@ -734,8 +808,10 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Recursively merge the collection with the given items.
*
* @param mixed $items
* @return static
* @template TMergeRecursiveValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TMergeRecursiveValue>|iterable<TKey, TMergeRecursiveValue> $items
* @return static<TKey, TValue|TMergeRecursiveValue>
*/
public function mergeRecursive($items)
{
@@ -745,8 +821,10 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Create a collection by using this collection for keys and another for its values.
*
* @param mixed $values
* @return static
* @template TCombineValue
*
* @param \IteratorAggregate<array-key, TCombineValue>|array<array-key, TCombineValue>|(callable(): \Generator<array-key, TCombineValue>) $values
* @return static<TValue, TCombineValue>
*/
public function combine($values)
{
@@ -776,7 +854,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Union the collection with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function union($items)
@@ -809,7 +887,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the items with the specified keys.
*
* @param mixed $keys
* @param \Illuminate\Support\Enumerable<array-key, TKey>|array<array-key, TKey>|string $keys
* @return static
*/
public function only($keys)
@@ -844,7 +922,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Push all of the given items onto the collection.
*
* @param iterable $source
* @param iterable<array-key, TValue> $source
* @return static
*/
public function concat($source)
@@ -859,7 +937,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* Get one or a specified number of items randomly from the collection.
*
* @param int|null $number
* @return static|mixed
* @return static<int, TValue>|TValue
*
* @throws \InvalidArgumentException
*/
@@ -873,7 +951,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Replace the collection items with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function replace($items)
@@ -900,7 +978,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Recursively replace the collection items with the given items.
*
* @param mixed $items
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue> $items
* @return static
*/
public function replaceRecursive($items)
@@ -921,12 +999,13 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Search the collection for a given value and return the corresponding key if successful.
*
* @param mixed $value
* @param TValue|(callable(TValue,TKey): bool) $value
* @param bool $strict
* @return mixed
* @return TKey|bool
*/
public function search($value, $strict = false)
{
/** @var (callable(TValue,TKey): bool) $predicate */
$predicate = $this->useAsCallable($value)
? $value
: function ($item) use ($value, $strict) {
@@ -958,7 +1037,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
*
* @param int $size
* @param int $step
* @return static
* @return static<int, static>
*/
public function sliding($size = 2, $step = 1)
{
@@ -971,7 +1050,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
$chunk[$iterator->key()] = $iterator->current();
if (count($chunk) == $size) {
yield tap(new static($chunk), function () use (&$chunk, $step) {
yield (new static($chunk))->tap(function () use (&$chunk, $step) {
$chunk = array_slice($chunk, $step, null, true);
});
@@ -1018,7 +1097,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Skip items in the collection until the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function skipUntil($value)
@@ -1031,7 +1110,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Skip items in the collection while the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function skipWhile($value)
@@ -1075,7 +1154,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* Split a collection into a certain number of groups.
*
* @param int $numberOfGroups
* @return static
* @return static<int, static>
*/
public function split($numberOfGroups)
{
@@ -1085,10 +1164,10 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|string $key
* @param mixed $operator
* @param mixed $value
* @return mixed
* @return TValue
*
* @throws \Illuminate\Support\ItemNotFoundException
* @throws \Illuminate\Support\MultipleItemsFoundException
@@ -1100,7 +1179,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
: $key;
return $this
->when($filter)
->unless($filter == null)
->filter($filter)
->take(2)
->collect()
@@ -1110,10 +1189,10 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the first item in the collection but throw an exception if no matching items exist.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|string $key
* @param mixed $operator
* @param mixed $value
* @return mixed
* @return TValue
*
* @throws \Illuminate\Support\ItemNotFoundException
*/
@@ -1124,7 +1203,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
: $key;
return $this
->when($filter)
->unless($filter == null)
->filter($filter)
->take(1)
->collect()
@@ -1135,7 +1214,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* Chunk the collection into chunks of the given size.
*
* @param int $size
* @return static
* @return static<int, static>
*/
public function chunk($size)
{
@@ -1174,7 +1253,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* Split a collection into a certain number of groups, and fill the first groups completely.
*
* @param int $numberOfGroups
* @return static
* @return static<int, static>
*/
public function splitIn($numberOfGroups)
{
@@ -1184,8 +1263,8 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Chunk the collection into chunks with a callback.
*
* @param callable $callback
* @return static
* @param callable(TValue, TKey, Collection<TKey, TValue>): bool $callback
* @return static<int, static<int, TValue>>
*/
public function chunkWhile(callable $callback)
{
@@ -1221,7 +1300,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Sort through each item with a callback.
*
* @param callable|null|int $callback
* @param (callable(TValue, TValue): int)|null|int $callback
* @return static
*/
public function sort($callback = null)
@@ -1243,7 +1322,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Sort the collection using the given callback.
*
* @param callable|string $callback
* @param array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string $callback
* @param int $options
* @param bool $descending
* @return static
@@ -1256,7 +1335,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Sort the collection in descending order using the given callback.
*
* @param callable|string $callback
* @param array<array-key, (callable(TValue, TValue): mixed)|(callable(TValue, TKey): mixed)|string|array{string, string}>|(callable(TValue, TKey): mixed)|string $callback
* @param int $options
* @return static
*/
@@ -1291,7 +1370,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Sort the collection keys using a callback.
*
* @param callable $callback
* @param callable(TKey, TKey): int $callback
* @return static
*/
public function sortKeysUsing(callable $callback)
@@ -1331,11 +1410,12 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Take items in the collection until the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
public function takeUntil($value)
{
/** @var callable(TValue, TKey): bool $callback */
$callback = $this->useAsCallable($value) ? $value : $this->equality($value);
return new static(function () use ($callback) {
@@ -1359,19 +1439,30 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
{
$timeout = $timeout->getTimestamp();
return $this->takeWhile(function () use ($timeout) {
return $this->now() < $timeout;
return new static(function () use ($timeout) {
if ($this->now() >= $timeout) {
return;
}
foreach ($this as $key => $value) {
yield $key => $value;
if ($this->now() >= $timeout) {
break;
}
}
});
}
/**
* Take items in the collection while the given condition is met.
*
* @param mixed $value
* @param TValue|callable(TValue,TKey): bool $value
* @return static
*/
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) {
@@ -1382,7 +1473,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Pass each item in the collection to the given callback, lazily.
*
* @param callable $callback
* @param callable(TValue, TKey): mixed $callback
* @return static
*/
public function tapEach(callable $callback)
@@ -1409,7 +1500,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Return only unique items from the collection array.
*
* @param string|callable|null $key
* @param (callable(TValue, TKey): mixed)|string|null $key
* @param bool $strict
* @return static
*/
@@ -1433,7 +1524,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Reset the keys on the underlying array.
*
* @return static
* @return static<int, TValue>
*/
public function values()
{
@@ -1450,8 +1541,10 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* e.g. new LazyCollection([1, 2, 3])->zip([4, 5, 6]);
* => [[1, 4], [2, 5], [3, 6]]
*
* @param mixed ...$items
* @return static
* @template TZipValue
*
* @param \Illuminate\Contracts\Support\Arrayable<array-key, TZipValue>|iterable<array-key, TZipValue> ...$items
* @return static<int, static<int, TValue|TZipValue>>
*/
public function zip($items)
{
@@ -1473,9 +1566,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Pad collection to the specified length with a value.
*
* @template TPadValue
*
* @param int $size
* @param mixed $value
* @return static
* @param TPadValue $value
* @return static<int, TValue|TPadValue>
*/
public function pad($size, $value)
{
@@ -1501,10 +1596,9 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Get the values iterator.
*
* @return \Traversable
* @return \Traversable<TKey, TValue>
*/
#[\ReturnTypeWillChange]
public function getIterator()
public function getIterator(): Traversable
{
return $this->makeIterator($this->source);
}
@@ -1514,8 +1608,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
public function count(): int
{
if (is_array($this->source)) {
return count($this->source);
@@ -1527,8 +1620,11 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Make an iterator from the given source.
*
* @param mixed $source
* @return \Traversable
* @template TIteratorKey of array-key
* @template TIteratorValue
*
* @param \IteratorAggregate<TIteratorKey, TIteratorValue>|array<TIteratorKey, TIteratorValue>|(callable(): \Generator<TIteratorKey, TIteratorValue>) $source
* @return \Traversable<TIteratorKey, TIteratorValue>
*/
protected function makeIterator($source)
{
@@ -1546,9 +1642,9 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
/**
* Explode the "value" and "key" arguments passed to "pluck".
*
* @param string|array $value
* @param string|array|null $key
* @return array
* @param string|string[] $value
* @param string|string[]|null $key
* @return array{string[],string[]|null}
*/
protected function explodePluckParameters($value, $key)
{
@@ -1563,7 +1659,7 @@ class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
* Pass this lazy collection through a method on the collection class.
*
* @param string $method
* @param array $params
* @param array<mixed> $params
* @return static
*/
protected function passthru($method, array $params)

View File

@@ -6,4 +6,35 @@ use RuntimeException;
class MultipleItemsFoundException extends RuntimeException
{
/**
* The number of items found.
*
* @var int
*/
public $count;
/**
* Create a new exception instance.
*
* @param int $count
* @param int $code
* @param \Throwable|null $previous
* @return void
*/
public function __construct($count, $code = 0, $previous = null)
{
$this->count = $count;
parent::__construct("$count items were found.", $code, $previous);
}
/**
* Get the number of items found.
*
* @return int
*/
public function getCount()
{
return $this->count;
}
}

View File

@@ -11,7 +11,6 @@ use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use Illuminate\Support\HigherOrderCollectionProxy;
use Illuminate\Support\HigherOrderWhenProxy;
use JsonSerializable;
use Symfony\Component\VarDumper\VarDumper;
use Traversable;
@@ -19,6 +18,9 @@ use UnexpectedValueException;
use UnitEnum;
/**
* @template TKey of array-key
* @template TValue
*
* @property-read HigherOrderCollectionProxy $average
* @property-read HigherOrderCollectionProxy $avg
* @property-read HigherOrderCollectionProxy $contains
@@ -35,19 +37,23 @@ use UnitEnum;
* @property-read HigherOrderCollectionProxy $min
* @property-read HigherOrderCollectionProxy $partition
* @property-read HigherOrderCollectionProxy $reject
* @property-read HigherOrderCollectionProxy $skipUntil
* @property-read HigherOrderCollectionProxy $skipWhile
* @property-read HigherOrderCollectionProxy $some
* @property-read HigherOrderCollectionProxy $sortBy
* @property-read HigherOrderCollectionProxy $sortByDesc
* @property-read HigherOrderCollectionProxy $skipUntil
* @property-read HigherOrderCollectionProxy $skipWhile
* @property-read HigherOrderCollectionProxy $sum
* @property-read HigherOrderCollectionProxy $takeUntil
* @property-read HigherOrderCollectionProxy $takeWhile
* @property-read HigherOrderCollectionProxy $unique
* @property-read HigherOrderCollectionProxy $unless
* @property-read HigherOrderCollectionProxy $until
* @property-read HigherOrderCollectionProxy $when
*/
trait EnumeratesValues
{
use Conditionable;
/**
* Indicates that the object's string representation should be escaped when __toString is invoked.
*
@@ -58,7 +64,7 @@ trait EnumeratesValues
/**
* The methods that can be proxied.
*
* @var string[]
* @var array<int, string>
*/
protected static $proxies = [
'average',
@@ -86,14 +92,19 @@ trait EnumeratesValues
'takeUntil',
'takeWhile',
'unique',
'unless',
'until',
'when',
];
/**
* Create a new collection instance if the value isn't one already.
*
* @param mixed $items
* @return static
* @template TMakeKey of array-key
* @template TMakeValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TMakeKey, TMakeValue>|iterable<TMakeKey, TMakeValue>|null $items
* @return static<TMakeKey, TMakeValue>
*/
public static function make($items = [])
{
@@ -103,8 +114,11 @@ trait EnumeratesValues
/**
* Wrap the given value in a collection if applicable.
*
* @param mixed $value
* @return static
* @template TWrapKey of array-key
* @template TWrapValue
*
* @param iterable<TWrapKey, TWrapValue> $value
* @return static<TWrapKey, TWrapValue>
*/
public static function wrap($value)
{
@@ -116,8 +130,11 @@ trait EnumeratesValues
/**
* Get the underlying items from the given collection if applicable.
*
* @param array|static $value
* @return array
* @template TUnwrapKey of array-key
* @template TUnwrapValue
*
* @param array<TUnwrapKey, TUnwrapValue>|static<TUnwrapKey, TUnwrapValue> $value
* @return array<TUnwrapKey, TUnwrapValue>
*/
public static function unwrap($value)
{
@@ -137,9 +154,11 @@ trait EnumeratesValues
/**
* Create a new collection by invoking the callback a given amount of times.
*
* @template TTimesValue
*
* @param int $number
* @param callable|null $callback
* @return static
* @param (callable(int): TTimesValue)|null $callback
* @return static<int, TTimesValue>
*/
public static function times($number, callable $callback = null)
{
@@ -148,15 +167,15 @@ trait EnumeratesValues
}
return static::range(1, $number)
->when($callback)
->unless($callback == null)
->map($callback);
}
/**
* Alias for the "avg" method.
*
* @param callable|string|null $callback
* @return mixed
* @param (callable(TValue): float|int)|string|null $callback
* @return float|int|null
*/
public function average($callback = null)
{
@@ -166,7 +185,7 @@ trait EnumeratesValues
/**
* Alias for the "contains" method.
*
* @param mixed $key
* @param (callable(TValue, TKey): bool)|TValue|string $key
* @param mixed $operator
* @param mixed $value
* @return bool
@@ -176,39 +195,11 @@ trait EnumeratesValues
return $this->contains(...func_get_args());
}
/**
* Determine if an item exists, using strict comparison.
*
* @param mixed $key
* @param mixed $value
* @return bool
*/
public function containsStrict($key, $value = null)
{
if (func_num_args() === 2) {
return $this->contains(function ($item) use ($key, $value) {
return data_get($item, $key) === $value;
});
}
if ($this->useAsCallable($key)) {
return ! is_null($this->first($key));
}
foreach ($this as $item) {
if ($item === $key) {
return true;
}
}
return false;
}
/**
* Dump the items and end the script.
*
* @param mixed ...$args
* @return void
* @return never
*/
public function dd(...$args)
{
@@ -236,7 +227,7 @@ trait EnumeratesValues
/**
* Execute a callback over each item.
*
* @param callable $callback
* @param callable(TValue, TKey): mixed $callback
* @return $this
*/
public function each(callable $callback)
@@ -253,7 +244,7 @@ trait EnumeratesValues
/**
* Execute a callback over each nested chunk of items.
*
* @param callable $callback
* @param callable(...mixed): mixed $callback
* @return static
*/
public function eachSpread(callable $callback)
@@ -268,7 +259,7 @@ trait EnumeratesValues
/**
* Determine if all items pass the given truth test.
*
* @param string|callable $key
* @param (callable(TValue, TKey): bool)|TValue|string $key
* @param mixed $operator
* @param mixed $value
* @return bool
@@ -293,16 +284,32 @@ trait EnumeratesValues
/**
* Get the first item by the given key value pair.
*
* @param string $key
* @param callable|string $key
* @param mixed $operator
* @param mixed $value
* @return mixed
* @return TValue|null
*/
public function firstWhere($key, $operator = null, $value = null)
{
return $this->first($this->operatorForWhere(...func_get_args()));
}
/**
* Get a single key's value from the first matching item in the collection.
*
* @param string $key
* @param mixed $default
* @return mixed
*/
public function value($key, $default = null)
{
if ($value = $this->firstWhere($key)) {
return data_get($value, $key, $default);
}
return value($default);
}
/**
* Determine if the collection is not empty.
*
@@ -316,8 +323,10 @@ trait EnumeratesValues
/**
* Run a map over each nested chunk of items.
*
* @param callable $callback
* @return static
* @template TMapSpreadValue
*
* @param callable(mixed): TMapSpreadValue $callback
* @return static<TKey, TMapSpreadValue>
*/
public function mapSpread(callable $callback)
{
@@ -333,8 +342,11 @@ trait EnumeratesValues
*
* The callback should return an associative array with a single key/value pair.
*
* @param callable $callback
* @return static
* @template TMapToGroupsKey of array-key
* @template TMapToGroupsValue
*
* @param callable(TValue, TKey): array<TMapToGroupsKey, TMapToGroupsValue> $callback
* @return static<TMapToGroupsKey, static<int, TMapToGroupsValue>>
*/
public function mapToGroups(callable $callback)
{
@@ -346,8 +358,11 @@ trait EnumeratesValues
/**
* Map a collection and flatten the result by a single level.
*
* @param callable $callback
* @return static
* @template TFlatMapKey of array-key
* @template TFlatMapValue
*
* @param callable(TValue, TKey): (\Illuminate\Support\Collection<TFlatMapKey, TFlatMapValue>|array<TFlatMapKey, TFlatMapValue>) $callback
* @return static<TFlatMapKey, TFlatMapValue>
*/
public function flatMap(callable $callback)
{
@@ -357,48 +372,42 @@ trait EnumeratesValues
/**
* Map the values into a new class.
*
* @param string $class
* @return static
* @template TMapIntoValue
*
* @param class-string<TMapIntoValue> $class
* @return static<TKey, TMapIntoValue>
*/
public function mapInto($class)
{
return $this->map(function ($value, $key) use ($class) {
return new $class($value, $key);
});
return $this->map(fn ($value, $key) => new $class($value, $key));
}
/**
* Get the min value of a given key.
*
* @param callable|string|null $callback
* @param (callable(TValue):mixed)|string|null $callback
* @return mixed
*/
public function min($callback = null)
{
$callback = $this->valueRetriever($callback);
return $this->map(function ($value) use ($callback) {
return $callback($value);
})->filter(function ($value) {
return ! is_null($value);
})->reduce(function ($result, $value) {
return is_null($result) || $value < $result ? $value : $result;
});
return $this->map(fn ($value) => $callback($value))
->filter(fn ($value) => ! is_null($value))
->reduce(fn ($result, $value) => is_null($result) || $value < $result ? $value : $result);
}
/**
* Get the max value of a given key.
*
* @param callable|string|null $callback
* @param (callable(TValue):mixed)|string|null $callback
* @return mixed
*/
public function max($callback = null)
{
$callback = $this->valueRetriever($callback);
return $this->filter(function ($value) {
return ! is_null($value);
})->reduce(function ($result, $item) use ($callback) {
return $this->filter(fn ($value) => ! is_null($value))->reduce(function ($result, $item) use ($callback) {
$value = $callback($item);
return is_null($result) || $value > $result ? $value : $result;
@@ -422,10 +431,10 @@ trait EnumeratesValues
/**
* Partition the collection into two arrays using the given callback or key.
*
* @param callable|string $key
* @param mixed $operator
* @param mixed $value
* @return static
* @param (callable(TValue, TKey): bool)|TValue|string $key
* @param TValue|string|null $operator
* @param TValue|null $value
* @return static<int<0, 1>, static<TKey, TValue>>
*/
public function partition($key, $operator = null, $value = null)
{
@@ -450,7 +459,7 @@ trait EnumeratesValues
/**
* Get the sum of the given values.
*
* @param callable|string|null $callback
* @param (callable(TValue): mixed)|string|null $callback
* @return mixed
*/
public function sum($callback = null)
@@ -459,40 +468,17 @@ trait EnumeratesValues
? $this->identity()
: $this->valueRetriever($callback);
return $this->reduce(function ($result, $item) use ($callback) {
return $result + $callback($item);
}, 0);
}
/**
* Apply the callback if the value is truthy.
*
* @param bool|mixed $value
* @param callable|null $callback
* @param callable|null $default
* @return static|mixed
*/
public function when($value, callable $callback = null, callable $default = null)
{
if (! $callback) {
return new HigherOrderWhenProxy($this, $value);
}
if ($value) {
return $callback($this, $value);
} elseif ($default) {
return $default($this, $value);
}
return $this;
return $this->reduce(fn ($result, $item) => $result + $callback($item), 0);
}
/**
* Apply the callback if the collection is empty.
*
* @param callable $callback
* @param callable|null $default
* @return static|mixed
* @template TWhenEmptyReturnType
*
* @param (callable($this): TWhenEmptyReturnType) $callback
* @param (callable($this): TWhenEmptyReturnType)|null $default
* @return $this|TWhenEmptyReturnType
*/
public function whenEmpty(callable $callback, callable $default = null)
{
@@ -502,34 +488,25 @@ trait EnumeratesValues
/**
* Apply the callback if the collection is not empty.
*
* @param callable $callback
* @param callable|null $default
* @return static|mixed
* @template TWhenNotEmptyReturnType
*
* @param callable($this): TWhenNotEmptyReturnType $callback
* @param (callable($this): TWhenNotEmptyReturnType)|null $default
* @return $this|TWhenNotEmptyReturnType
*/
public function whenNotEmpty(callable $callback, callable $default = null)
{
return $this->when($this->isNotEmpty(), $callback, $default);
}
/**
* Apply the callback if the value is falsy.
*
* @param bool $value
* @param callable $callback
* @param callable|null $default
* @return static|mixed
*/
public function unless($value, callable $callback, callable $default = null)
{
return $this->when(! $value, $callback, $default);
}
/**
* Apply the callback unless the collection is empty.
*
* @param callable $callback
* @param callable|null $default
* @return static|mixed
* @template TUnlessEmptyReturnType
*
* @param callable($this): TUnlessEmptyReturnType $callback
* @param (callable($this): TUnlessEmptyReturnType)|null $default
* @return $this|TUnlessEmptyReturnType
*/
public function unlessEmpty(callable $callback, callable $default = null)
{
@@ -539,9 +516,11 @@ trait EnumeratesValues
/**
* Apply the callback unless the collection is not empty.
*
* @param callable $callback
* @param callable|null $default
* @return static|mixed
* @template TUnlessNotEmptyReturnType
*
* @param callable($this): TUnlessNotEmptyReturnType $callback
* @param (callable($this): TUnlessNotEmptyReturnType)|null $default
* @return $this|TUnlessNotEmptyReturnType
*/
public function unlessNotEmpty(callable $callback, callable $default = null)
{
@@ -551,7 +530,7 @@ trait EnumeratesValues
/**
* Filter items by the given key value pair.
*
* @param string $key
* @param callable|string $key
* @param mixed $operator
* @param mixed $value
* @return static
@@ -599,7 +578,7 @@ trait EnumeratesValues
* Filter items by the given key value pair.
*
* @param string $key
* @param mixed $values
* @param \Illuminate\Contracts\Support\Arrayable|iterable $values
* @param bool $strict
* @return static
*/
@@ -607,16 +586,14 @@ trait EnumeratesValues
{
$values = $this->getArrayableItems($values);
return $this->filter(function ($item) use ($key, $values, $strict) {
return in_array(data_get($item, $key), $values, $strict);
});
return $this->filter(fn ($item) => in_array(data_get($item, $key), $values, $strict));
}
/**
* Filter items by the given key value pair using strict comparison.
*
* @param string $key
* @param mixed $values
* @param \Illuminate\Contracts\Support\Arrayable|iterable $values
* @return static
*/
public function whereInStrict($key, $values)
@@ -628,7 +605,7 @@ trait EnumeratesValues
* Filter items such that the value of the given key is between the given values.
*
* @param string $key
* @param array $values
* @param \Illuminate\Contracts\Support\Arrayable|iterable $values
* @return static
*/
public function whereBetween($key, $values)
@@ -640,21 +617,21 @@ trait EnumeratesValues
* Filter items such that the value of the given key is not between the given values.
*
* @param string $key
* @param array $values
* @param \Illuminate\Contracts\Support\Arrayable|iterable $values
* @return static
*/
public function whereNotBetween($key, $values)
{
return $this->filter(function ($item) use ($key, $values) {
return data_get($item, $key) < reset($values) || data_get($item, $key) > end($values);
});
return $this->filter(
fn ($item) => data_get($item, $key) < reset($values) || data_get($item, $key) > end($values)
);
}
/**
* Filter items by the given key value pair.
*
* @param string $key
* @param mixed $values
* @param \Illuminate\Contracts\Support\Arrayable|iterable $values
* @param bool $strict
* @return static
*/
@@ -662,16 +639,14 @@ trait EnumeratesValues
{
$values = $this->getArrayableItems($values);
return $this->reject(function ($item) use ($key, $values, $strict) {
return in_array(data_get($item, $key), $values, $strict);
});
return $this->reject(fn ($item) => in_array(data_get($item, $key), $values, $strict));
}
/**
* Filter items by the given key value pair using strict comparison.
*
* @param string $key
* @param mixed $values
* @param \Illuminate\Contracts\Support\Arrayable|iterable $values
* @return static
*/
public function whereNotInStrict($key, $values)
@@ -682,8 +657,10 @@ trait EnumeratesValues
/**
* Filter the items, removing any items that don't match the given type(s).
*
* @param string|string[] $type
* @return static
* @template TWhereInstanceOf
*
* @param class-string<TWhereInstanceOf>|array<array-key, class-string<TWhereInstanceOf>> $type
* @return static<TKey, TWhereInstanceOf>
*/
public function whereInstanceOf($type)
{
@@ -705,8 +682,10 @@ trait EnumeratesValues
/**
* Pass the collection to the given callback and return the result.
*
* @param callable $callback
* @return mixed
* @template TPipeReturnType
*
* @param callable($this): TPipeReturnType $callback
* @return TPipeReturnType
*/
public function pipe(callable $callback)
{
@@ -716,7 +695,7 @@ trait EnumeratesValues
/**
* Pass the collection into a new class.
*
* @param string $class
* @param class-string $class
* @return mixed
*/
public function pipeInto($class)
@@ -727,38 +706,26 @@ trait EnumeratesValues
/**
* Pass the collection through a series of callable pipes and return the result.
*
* @param array<callable> $pipes
* @param array<callable> $callbacks
* @return mixed
*/
public function pipeThrough($pipes)
public function pipeThrough($callbacks)
{
return static::make($pipes)->reduce(
function ($carry, $pipe) {
return $pipe($carry);
},
return Collection::make($callbacks)->reduce(
fn ($carry, $callback) => $callback($carry),
$this,
);
}
/**
* Pass the collection to the given callback and then return it.
*
* @param callable $callback
* @return $this
*/
public function tap(callable $callback)
{
$callback(clone $this);
return $this;
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
* @template TReduceInitial
* @template TReduceReturnType
*
* @param callable(TReduceInitial|TReduceReturnType, TValue, TKey): TReduceReturnType $callback
* @param TReduceInitial $initial
* @return TReduceReturnType
*/
public function reduce(callable $callback, $initial = null)
{
@@ -771,22 +738,6 @@ trait EnumeratesValues
return $result;
}
/**
* Reduce the collection to multiple aggregate values.
*
* @param callable $callback
* @param mixed ...$initial
* @return array
*
* @deprecated Use "reduceSpread" instead
*
* @throws \UnexpectedValueException
*/
public function reduceMany(callable $callback, ...$initial)
{
return $this->reduceSpread($callback, ...$initial);
}
/**
* Reduce the collection to multiple aggregate values.
*
@@ -805,7 +756,7 @@ trait EnumeratesValues
if (! is_array($result)) {
throw new UnexpectedValueException(sprintf(
"%s::reduceMany expects reducer to return an array, but got a '%s' instead.",
"%s::reduceSpread expects reducer to return an array, but got a '%s' instead.",
class_basename(static::class), gettype($result)
));
}
@@ -814,22 +765,10 @@ trait EnumeratesValues
return $result;
}
/**
* Reduce an associative collection to a single value.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduceWithKeys(callable $callback, $initial = null)
{
return $this->reduce($callback, $initial);
}
/**
* Create a collection of all elements that do not pass a given truth test.
*
* @param callable|mixed $callback
* @param (callable(TValue, TKey): bool)|bool|TValue $callback
* @return static
*/
public function reject($callback = true)
@@ -843,10 +782,45 @@ trait EnumeratesValues
});
}
/**
* Pass the collection to the given callback and then return it.
*
* @param callable($this): mixed $callback
* @return $this
*/
public function tap(callable $callback)
{
$callback($this);
return $this;
}
/**
* Return only unique items from the collection array.
*
* @param (callable(TValue, TKey): mixed)|string|null $key
* @param bool $strict
* @return static
*/
public function unique($key = null, $strict = false)
{
$callback = $this->valueRetriever($key);
$exists = [];
return $this->reject(function ($item, $key) use ($callback, $strict, &$exists) {
if (in_array($id = $callback($item, $key), $exists, $strict)) {
return true;
}
$exists[] = $id;
});
}
/**
* Return only unique items from the collection array using strict comparison.
*
* @param string|callable|null $key
* @param (callable(TValue, TKey): mixed)|string|null $key
* @return static
*/
public function uniqueStrict($key = null)
@@ -857,7 +831,7 @@ trait EnumeratesValues
/**
* Collect the values into a collection.
*
* @return \Illuminate\Support\Collection
* @return \Illuminate\Support\Collection<TKey, TValue>
*/
public function collect()
{
@@ -867,22 +841,19 @@ trait EnumeratesValues
/**
* Get the collection of items as a plain array.
*
* @return array
* @return array<TKey, mixed>
*/
public function toArray()
{
return $this->map(function ($value) {
return $value instanceof Arrayable ? $value->toArray() : $value;
})->all();
return $this->map(fn ($value) => $value instanceof Arrayable ? $value->toArray() : $value)->all();
}
/**
* Convert the object into something JSON serializable.
*
* @return array
* @return array<TKey, mixed>
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
public function jsonSerialize(): array
{
return array_map(function ($value) {
if ($value instanceof JsonSerializable) {
@@ -976,7 +947,7 @@ trait EnumeratesValues
* Results array of items from Collection or Arrayable.
*
* @param mixed $items
* @return array
* @return array<TKey, TValue>
*/
protected function getArrayableItems($items)
{
@@ -986,12 +957,12 @@ trait EnumeratesValues
return $items->all();
} elseif ($items instanceof Arrayable) {
return $items->toArray();
} elseif ($items instanceof Traversable) {
return iterator_to_array($items);
} elseif ($items instanceof Jsonable) {
return json_decode($items->toJson(), true);
} elseif ($items instanceof JsonSerializable) {
return (array) $items->jsonSerialize();
} elseif ($items instanceof Traversable) {
return iterator_to_array($items);
} elseif ($items instanceof UnitEnum) {
return [$items];
}
@@ -1002,13 +973,17 @@ trait EnumeratesValues
/**
* Get an operator checker callback.
*
* @param string $key
* @param callable|string $key
* @param string|null $operator
* @param mixed $value
* @return \Closure
*/
protected function operatorForWhere($key, $operator = null, $value = null)
{
if ($this->useAsCallable($key)) {
return $key;
}
if (func_num_args() === 1) {
$value = true;
@@ -1044,6 +1019,7 @@ trait EnumeratesValues
case '>=': return $retrieved >= $value;
case '===': return $retrieved === $value;
case '!==': return $retrieved !== $value;
case '<=>': return $retrieved <=> $value;
}
};
}
@@ -1071,22 +1047,18 @@ trait EnumeratesValues
return $value;
}
return function ($item) use ($value) {
return data_get($item, $value);
};
return fn ($item) => data_get($item, $value);
}
/**
* Make a function to check an item's equality.
*
* @param mixed $value
* @return \Closure
* @return \Closure(mixed): bool
*/
protected function equality($value)
{
return function ($item) use ($value) {
return $item === $value;
};
return fn ($item) => $item === $value;
}
/**
@@ -1097,20 +1069,16 @@ trait EnumeratesValues
*/
protected function negate(Closure $callback)
{
return function (...$params) use ($callback) {
return ! $callback(...$params);
};
return fn (...$params) => ! $callback(...$params);
}
/**
* Make a function that returns what's passed to it.
*
* @return \Closure
* @return \Closure(TValue): TValue
*/
protected function identity()
{
return function ($value) {
return $value;
};
return fn ($value) => $value;
}
}

View File

@@ -14,9 +14,10 @@
}
],
"require": {
"php": "^7.3|^8.0",
"illuminate/contracts": "^8.0",
"illuminate/macroable": "^8.0"
"php": "^8.0.2",
"illuminate/conditionable": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0"
},
"autoload": {
"psr-4": {
@@ -28,11 +29,11 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"symfony/var-dumper": "Required to use the dump method (^5.4)."
"symfony/var-dumper": "Required to use the dump method (^6.0)."
},
"config": {
"sort-packages": true

View File

@@ -7,10 +7,13 @@ if (! function_exists('collect')) {
/**
* Create a collection from the given value.
*
* @param mixed $value
* @return \Illuminate\Support\Collection
* @template TKey of array-key
* @template TValue
*
* @param \Illuminate\Contracts\Support\Arrayable<TKey, TValue>|iterable<TKey, TValue>|null $value
* @return \Illuminate\Support\Collection<TKey, TValue>
*/
function collect($value = null)
function collect($value = [])
{
return new Collection($value);
}
@@ -58,7 +61,7 @@ if (! function_exists('data_get')) {
if ($segment === '*') {
if ($target instanceof Collection) {
$target = $target->all();
} elseif (! is_array($target)) {
} elseif (! is_iterable($target)) {
return value($default);
}
@@ -177,6 +180,7 @@ if (! function_exists('value')) {
* Return the default value of the given value.
*
* @param mixed $value
* @param mixed $args
* @return mixed
*/
function value($value, ...$args)

View File

@@ -0,0 +1,109 @@
<?php
namespace Illuminate\Support;
class HigherOrderWhenProxy
{
/**
* The target being conditionally operated on.
*
* @var mixed
*/
protected $target;
/**
* The condition for proxying.
*
* @var bool
*/
protected $condition;
/**
* Indicates whether the proxy has a condition.
*
* @var bool
*/
protected $hasCondition = false;
/**
* Determine whether the condition should be negated.
*
* @var bool
*/
protected $negateConditionOnCapture;
/**
* Create a new proxy instance.
*
* @param mixed $target
* @return void
*/
public function __construct($target)
{
$this->target = $target;
}
/**
* Set the condition on the proxy.
*
* @param bool $condition
* @return $this
*/
public function condition($condition)
{
[$this->condition, $this->hasCondition] = [$condition, true];
return $this;
}
/**
* Indicate that the condition should be negated.
*
* @return $this
*/
public function negateConditionOnCapture()
{
$this->negateConditionOnCapture = true;
return $this;
}
/**
* Proxy accessing an attribute onto the target.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
if (! $this->hasCondition) {
$condition = $this->target->{$key};
return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition);
}
return $this->condition
? $this->target->{$key}
: $this->target;
}
/**
* Proxy a method call on the target.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (! $this->hasCondition) {
$condition = $this->target->{$method}(...$parameters);
return $this->condition($this->negateConditionOnCapture ? ! $condition : $condition);
}
return $this->condition
? $this->target->{$method}(...$parameters)
: $this->target;
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Taylor Otwell
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,73 @@
<?php
namespace Illuminate\Support\Traits;
use Closure;
use Illuminate\Support\HigherOrderWhenProxy;
trait Conditionable
{
/**
* Apply the callback if the given "value" is (or resolves to) truthy.
*
* @template TWhenParameter
* @template TWhenReturnType
*
* @param (\Closure($this): TWhenParameter)|TWhenParameter|null $value
* @param (callable($this, TWhenParameter): TWhenReturnType)|null $callback
* @param (callable($this, TWhenParameter): TWhenReturnType)|null $default
* @return $this|TWhenReturnType
*/
public function when($value = null, callable $callback = null, callable $default = null)
{
$value = $value instanceof Closure ? $value($this) : $value;
if (func_num_args() === 0) {
return new HigherOrderWhenProxy($this);
}
if (func_num_args() === 1) {
return (new HigherOrderWhenProxy($this))->condition($value);
}
if ($value) {
return $callback($this, $value) ?? $this;
} elseif ($default) {
return $default($this, $value) ?? $this;
}
return $this;
}
/**
* Apply the callback if the given "value" is (or resolves to) falsy.
*
* @template TUnlessParameter
* @template TUnlessReturnType
*
* @param (\Closure($this): TUnlessParameter)|TUnlessParameter|null $value
* @param (callable($this, TUnlessParameter): TUnlessReturnType)|null $callback
* @param (callable($this, TUnlessParameter): TUnlessReturnType)|null $default
* @return $this|TUnlessReturnType
*/
public function unless($value = null, callable $callback = null, callable $default = null)
{
$value = $value instanceof Closure ? $value($this) : $value;
if (func_num_args() === 0) {
return (new HigherOrderWhenProxy($this))->negateConditionOnCapture();
}
if (func_num_args() === 1) {
return (new HigherOrderWhenProxy($this))->condition(! $value);
}
if (! $value) {
return $callback($this, $value) ?? $this;
} elseif ($default) {
return $default($this, $value) ?? $this;
}
return $this;
}
}

View File

@@ -0,0 +1,33 @@
{
"name": "illuminate/conditionable",
"description": "The Illuminate Conditionable package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.0.2"
},
"autoload": {
"psr-4": {
"Illuminate\\Support\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "9.x-dev"
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}

View File

@@ -5,9 +5,12 @@ namespace Illuminate\Config;
use ArrayAccess;
use Illuminate\Contracts\Config\Repository as ConfigContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Traits\Macroable;
class Repository implements ArrayAccess, ConfigContract
{
use Macroable;
/**
* All of the configuration items.
*
@@ -138,8 +141,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param string $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
public function offsetExists($key): bool
{
return $this->has($key);
}
@@ -150,8 +152,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param string $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
public function offsetGet($key): mixed
{
return $this->get($key);
}
@@ -163,8 +164,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
public function offsetSet($key, $value): void
{
$this->set($key, $value);
}
@@ -175,8 +175,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param string $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
public function offsetUnset($key): void
{
$this->set($key, null);
}

View File

@@ -14,9 +14,9 @@
}
],
"require": {
"php": "^7.3|^8.0",
"illuminate/collections": "^8.0",
"illuminate/contracts": "^8.0"
"php": "^8.0.2",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0"
},
"autoload": {
"psr-4": {
@@ -25,7 +25,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
"dev-master": "9.x-dev"
}
},
"config": {

View File

@@ -15,6 +15,7 @@ use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\StringInput;
@@ -31,6 +32,13 @@ class Application extends SymfonyApplication implements ApplicationContract
*/
protected $laravel;
/**
* The event dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* The output from the previous command.
*
@@ -46,11 +54,11 @@ class Application extends SymfonyApplication implements ApplicationContract
protected static $bootstrappers = [];
/**
* The Event Dispatcher.
* A map of command names to classes.
*
* @var \Illuminate\Contracts\Events\Dispatcher
* @var array
*/
protected $events;
protected $commandMap = [];
/**
* Create a new Artisan console application.
@@ -79,7 +87,7 @@ class Application extends SymfonyApplication implements ApplicationContract
*
* @return int
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
public function run(InputInterface $input = null, OutputInterface $output = null): int
{
$commandName = $this->getCommandName(
$input = $input ?: new ArgvInput
@@ -254,11 +262,21 @@ class Application extends SymfonyApplication implements ApplicationContract
/**
* Add a command, resolving through the application.
*
* @param string $command
* @return \Symfony\Component\Console\Command\Command
* @param \Illuminate\Console\Command|string $command
* @return \Symfony\Component\Console\Command\Command|null
*/
public function resolve($command)
{
if (is_subclass_of($command, SymfonyCommand::class) && ($commandName = $command::getDefaultName())) {
$this->commandMap[$commandName] = $command;
return null;
}
if ($command instanceof Command) {
return $this->add($command);
}
return $this->add($this->laravel->make($command));
}
@@ -279,6 +297,18 @@ class Application extends SymfonyApplication implements ApplicationContract
return $this;
}
/**
* Set the container command loader for lazy resolution.
*
* @return $this
*/
public function setContainerCommandLoader()
{
$this->setCommandLoader(new ContainerCommandLoader($this->laravel, $this->commandMap));
return $this;
}
/**
* Get the default input definition for the application.
*
@@ -286,7 +316,7 @@ class Application extends SymfonyApplication implements ApplicationContract
*
* @return \Symfony\Component\Console\Input\InputDefinition
*/
protected function getDefaultInputDefinition()
protected function getDefaultInputDefinition(): InputDefinition
{
return tap(parent::getDefaultInputDefinition(), function ($definition) {
$definition->addOption($this->getEnvironmentOption());

View File

@@ -0,0 +1,98 @@
<?php
namespace Illuminate\Console;
use Carbon\CarbonInterval;
use Illuminate\Contracts\Cache\Factory as Cache;
class CacheCommandMutex implements CommandMutex
{
/**
* The cache factory implementation.
*
* @var \Illuminate\Contracts\Cache\Factory
*/
public $cache;
/**
* The cache store that should be used.
*
* @var string|null
*/
public $store = null;
/**
* Create a new command mutex.
*
* @param \Illuminate\Contracts\Cache\Factory $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Attempt to obtain a command mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function create($command)
{
return $this->cache->store($this->store)->add(
$this->commandMutexName($command),
true,
method_exists($command, 'isolationLockExpiresAt')
? $command->isolationLockExpiresAt()
: CarbonInterval::hour(),
);
}
/**
* Determine if a command mutex exists for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function exists($command)
{
return $this->cache->store($this->store)->has(
$this->commandMutexName($command)
);
}
/**
* Release the mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function forget($command)
{
return $this->cache->store($this->store)->forget(
$this->commandMutexName($command)
);
}
/**
* @param \Illuminate\Console\Command $command
* @return string
*/
protected function commandMutexName($command)
{
return 'framework'.DIRECTORY_SEPARATOR.'command-'.$command->getName();
}
/**
* Specify the cache store that should be used.
*
* @param string|null $store
* @return $this
*/
public function useStore($store)
{
$this->store = $store;
return $this;
}
}

View File

@@ -2,9 +2,12 @@
namespace Illuminate\Console;
use Illuminate\Console\View\Components\Factory;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Command extends SymfonyCommand
@@ -12,6 +15,7 @@ class Command extends SymfonyCommand
use Concerns\CallsCommands,
Concerns\HasParameters,
Concerns\InteractsWithIO,
Concerns\InteractsWithSignals,
Macroable;
/**
@@ -38,7 +42,7 @@ class Command extends SymfonyCommand
/**
* The console command description.
*
* @var string
* @var string|null
*/
protected $description;
@@ -75,7 +79,11 @@ class Command extends SymfonyCommand
// Once we have constructed the command, we'll set the description and other
// related properties of the command. If a signature wasn't used to build
// the command we'll set the arguments and the options on this command.
$this->setDescription((string) $this->description);
if (! isset($this->description)) {
$this->setDescription((string) static::getDefaultDescription());
} else {
$this->setDescription((string) $this->description);
}
$this->setHelp((string) $this->help);
@@ -84,6 +92,10 @@ class Command extends SymfonyCommand
if (! isset($this->signature)) {
$this->specifyParameters();
}
if ($this instanceof Isolatable) {
$this->configureIsolation();
}
}
/**
@@ -104,6 +116,22 @@ class Command extends SymfonyCommand
$this->getDefinition()->addOptions($options);
}
/**
* Configure the console command for isolation.
*
* @return void
*/
protected function configureIsolation()
{
$this->getDefinition()->addOption(new InputOption(
'isolated',
null,
InputOption::VALUE_OPTIONAL,
'Do not run the command if another instance of the command is already running',
false
));
}
/**
* Run the console command.
*
@@ -111,15 +139,21 @@ class Command extends SymfonyCommand
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
public function run(InputInterface $input, OutputInterface $output)
public function run(InputInterface $input, OutputInterface $output): int
{
$this->output = $this->laravel->make(
OutputStyle::class, ['input' => $input, 'output' => $output]
);
return parent::run(
$this->input = $input, $this->output
);
$this->components = $this->laravel->make(Factory::class, ['output' => $this->output]);
try {
return parent::run(
$this->input = $input, $this->output
);
} finally {
$this->untrap();
}
}
/**
@@ -131,9 +165,38 @@ class Command extends SymfonyCommand
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
if ($this instanceof Isolatable && $this->option('isolated') !== false &&
! $this->commandIsolationMutex()->create($this)) {
$this->comment(sprintf(
'The [%s] command is already running.', $this->getName()
));
return (int) (is_numeric($this->option('isolated'))
? $this->option('isolated')
: self::SUCCESS);
}
$method = method_exists($this, 'handle') ? 'handle' : '__invoke';
return (int) $this->laravel->call([$this, $method]);
try {
return (int) $this->laravel->call([$this, $method]);
} finally {
if ($this instanceof Isolatable && $this->option('isolated') !== false) {
$this->commandIsolationMutex()->forget($this);
}
}
}
/**
* Get a command isolation mutex instance for the command.
*
* @return \Illuminate\Console\CommandMutex
*/
protected function commandIsolationMutex()
{
return $this->laravel->bound(CommandMutex::class)
? $this->laravel->make(CommandMutex::class)
: $this->laravel->make(CacheCommandMutex::class);
}
/**
@@ -166,17 +229,15 @@ class Command extends SymfonyCommand
*
* @return bool
*/
public function isHidden()
public function isHidden(): bool
{
return $this->hidden;
}
/**
* {@inheritdoc}
*
* @return static
*/
public function setHidden(bool $hidden)
public function setHidden(bool $hidden = true): static
{
parent::setHidden($this->hidden = $hidden);

View File

@@ -0,0 +1,30 @@
<?php
namespace Illuminate\Console;
interface CommandMutex
{
/**
* Attempt to obtain a command mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function create($command);
/**
* Determine if a command mutex exists for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function exists($command);
/**
* Release the mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function forget($command);
}

View File

@@ -28,17 +28,17 @@ trait CreatesMatchingTest
* Create the matching test case if requested.
*
* @param string $path
* @return void
* @return bool
*/
protected function handleTestCreation($path)
{
if (! $this->option('test') && ! $this->option('pest')) {
return;
return false;
}
$this->call('make:test', [
return $this->callSilent('make:test', [
'name' => Str::of($path)->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'),
'--pest' => $this->option('pest'),
]);
]) == 0;
}
}

View File

@@ -21,7 +21,7 @@ trait HasParameters
if ($arguments instanceof InputArgument) {
$this->getDefinition()->addArgument($arguments);
} else {
$this->addArgument(...array_values($arguments));
$this->addArgument(...$arguments);
}
}
@@ -29,7 +29,7 @@ trait HasParameters
if ($options instanceof InputOption) {
$this->getDefinition()->addOption($options);
} else {
$this->addOption(...array_values($options));
$this->addOption(...$options);
}
}
}

View File

@@ -15,6 +15,15 @@ use Symfony\Component\Console\Question\Question;
trait InteractsWithIO
{
/**
* The console components factory.
*
* @var \Illuminate\Console\View\Components\Factory
*
* @internal This property is not meant to be used or overwritten outside the framework.
*/
protected $components;
/**
* The input interface implementation.
*
@@ -64,7 +73,7 @@ trait InteractsWithIO
* Get the value of a command argument.
*
* @param string|null $key
* @return string|array|null
* @return array|string|bool|null
*/
public function argument($key = null)
{
@@ -198,7 +207,7 @@ trait InteractsWithIO
*
* @param string $question
* @param array $choices
* @param string|null $default
* @param string|int|null $default
* @param mixed|null $attempts
* @param bool $multiple
* @return string|array
@@ -217,7 +226,7 @@ trait InteractsWithIO
*
* @param array $headers
* @param \Illuminate\Contracts\Support\Arrayable|array $rows
* @param string $tableStyle
* @param \Symfony\Component\Console\Helper\TableStyle|string $tableStyle
* @param array $columnStyles
* @return void
*/
@@ -355,28 +364,31 @@ trait InteractsWithIO
* Write a string in an alert box.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function alert($string)
public function alert($string, $verbosity = null)
{
$length = Str::length(strip_tags($string)) + 12;
$this->comment(str_repeat('*', $length));
$this->comment('* '.$string.' *');
$this->comment(str_repeat('*', $length));
$this->comment(str_repeat('*', $length), $verbosity);
$this->comment('* '.$string.' *', $verbosity);
$this->comment(str_repeat('*', $length), $verbosity);
$this->newLine();
$this->comment('', $verbosity);
}
/**
* Write a blank line.
*
* @param int $count
* @return void
* @return $this
*/
public function newLine($count = 1)
{
$this->output->newLine($count);
return $this;
}
/**

View File

@@ -0,0 +1,51 @@
<?php
namespace Illuminate\Console\Concerns;
use Illuminate\Console\Signals;
use Illuminate\Support\Arr;
trait InteractsWithSignals
{
/**
* The signal registrar instance.
*
* @var \Illuminate\Console\Signals|null
*/
protected $signals;
/**
* Define a callback to be run when the given signal(s) occurs.
*
* @param iterable<array-key, int>|int $signals
* @param callable(int $signal): void $callback
* @return void
*/
public function trap($signals, $callback)
{
Signals::whenAvailable(function () use ($signals, $callback) {
$this->signals ??= new Signals(
$this->getApplication()->getSignalRegistry(),
);
collect(Arr::wrap($signals))
->each(fn ($signal) => $this->signals->register($signal, $callback));
});
}
/**
* Untrap signal handlers set within the command's handler.
*
* @return void
*
* @internal
*/
public function untrap()
{
if (! is_null($this->signals)) {
$this->signals->unregister();
$this->signals = null;
}
}
}

View File

@@ -13,7 +13,7 @@ trait ConfirmableTrait
* @param \Closure|bool|null $callback
* @return bool
*/
public function confirmToProceed($warning = 'Application In Production!', $callback = null)
public function confirmToProceed($warning = 'Application In Production', $callback = null)
{
$callback = is_null($callback) ? $this->getDefaultConfirmCallback() : $callback;
@@ -24,12 +24,14 @@ trait ConfirmableTrait
return true;
}
$this->alert($warning);
$this->components->alert($warning);
$confirmed = $this->confirm('Do you really wish to run this command?');
$confirmed = $this->components->confirm('Do you really wish to run this command?');
if (! $confirmed) {
$this->comment('Command Canceled!');
$this->newLine();
$this->components->warn('Command canceled.');
return false;
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Illuminate\Console;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
use Symfony\Component\Console\Exception\CommandNotFoundException;
class ContainerCommandLoader implements CommandLoaderInterface
{
/**
* The container instance.
*
* @var \Psr\Container\ContainerInterface
*/
protected $container;
/**
* A map of command names to classes.
*
* @var array
*/
protected $commandMap;
/**
* Create a new command loader instance.
*
* @param \Psr\Container\ContainerInterface $container
* @param array $commandMap
* @return void
*/
public function __construct(ContainerInterface $container, array $commandMap)
{
$this->container = $container;
$this->commandMap = $commandMap;
}
/**
* Resolve a command from the container.
*
* @param string $name
* @return \Symfony\Component\Console\Command\Command
*
* @throws \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function get(string $name): Command
{
if (! $this->has($name)) {
throw new CommandNotFoundException(sprintf('Command "%s" does not exist.', $name));
}
return $this->container->get($this->commandMap[$name]);
}
/**
* Determines if a command exists.
*
* @param string $name
* @return bool
*/
public function has(string $name): bool
{
return $name && isset($this->commandMap[$name]);
}
/**
* Get the command names.
*
* @return string[]
*/
public function getNames(): array
{
return array_keys($this->commandMap);
}
}

View File

@@ -0,0 +1,13 @@
<?php
namespace Illuminate\Console\Contracts;
interface NewLineAware
{
/**
* Whether a newline has already been written.
*
* @return bool
*/
public function newLineWritten();
}

View File

@@ -56,9 +56,11 @@ abstract class GeneratorCommand extends Command
'endif',
'endswitch',
'endwhile',
'enum',
'eval',
'exit',
'extends',
'false',
'final',
'finally',
'fn',
@@ -76,6 +78,7 @@ abstract class GeneratorCommand extends Command
'interface',
'isset',
'list',
'match',
'namespace',
'new',
'or',
@@ -83,6 +86,7 @@ abstract class GeneratorCommand extends Command
'private',
'protected',
'public',
'readonly',
'require',
'require_once',
'return',
@@ -90,6 +94,7 @@ abstract class GeneratorCommand extends Command
'switch',
'throw',
'trait',
'true',
'try',
'unset',
'use',
@@ -97,6 +102,14 @@ abstract class GeneratorCommand extends Command
'while',
'xor',
'yield',
'__CLASS__',
'__DIR__',
'__FILE__',
'__FUNCTION__',
'__LINE__',
'__METHOD__',
'__NAMESPACE__',
'__TRAIT__',
];
/**
@@ -136,7 +149,7 @@ abstract class GeneratorCommand extends Command
// language and that the class name will actually be valid. If it is not valid we
// can error now and prevent from polluting the filesystem using invalid files.
if ($this->isReservedName($this->getNameInput())) {
$this->error('The name "'.$this->getNameInput().'" is reserved by PHP.');
$this->components->error('The name "'.$this->getNameInput().'" is reserved by PHP.');
return false;
}
@@ -151,7 +164,7 @@ abstract class GeneratorCommand extends Command
if ((! $this->hasOption('force') ||
! $this->option('force')) &&
$this->alreadyExists($this->getNameInput())) {
$this->error($this->type.' already exists!');
$this->components->error($this->type.' already exists.');
return false;
}
@@ -163,11 +176,15 @@ abstract class GeneratorCommand extends Command
$this->files->put($path, $this->sortImports($this->buildClass($name)));
$this->info($this->type.' created successfully.');
$info = $this->type;
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
$this->handleTestCreation($path);
if ($this->handleTestCreation($path)) {
$info .= ' and test';
}
}
$this->components->info(sprintf('%s [%s] created successfully.', $info, $path));
}
/**
@@ -340,7 +357,7 @@ abstract class GeneratorCommand extends Command
*/
protected function sortImports($stub)
{
if (preg_match('/(?P<imports>(?:use [^;]+;$\n?)+)/m', $stub, $match)) {
if (preg_match('/(?P<imports>(?:^use [^;{]+;$\n?)+)/m', $stub, $match)) {
$imports = explode("\n", trim($match['imports']));
sort($imports);

View File

@@ -2,11 +2,12 @@
namespace Illuminate\Console;
use Illuminate\Console\Contracts\NewLineAware;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class OutputStyle extends SymfonyStyle
class OutputStyle extends SymfonyStyle implements NewLineAware
{
/**
* The output instance.
@@ -15,6 +16,13 @@ class OutputStyle extends SymfonyStyle
*/
private $output;
/**
* If the last output written wrote a new line.
*
* @var bool
*/
protected $newLineWritten = false;
/**
* Create a new Console OutputStyle instance.
*
@@ -29,12 +37,54 @@ class OutputStyle extends SymfonyStyle
parent::__construct($input, $output);
}
/**
* {@inheritdoc}
*/
public function write(string|iterable $messages, bool $newline = false, int $options = 0)
{
$this->newLineWritten = $newline;
parent::write($messages, $newline, $options);
}
/**
* {@inheritdoc}
*/
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL)
{
$this->newLineWritten = true;
parent::writeln($messages, $type);
}
/**
* {@inheritdoc}
*/
public function newLine(int $count = 1)
{
$this->newLineWritten = $count > 0;
parent::newLine($count);
}
/**
* {@inheritdoc}
*/
public function newLineWritten()
{
if ($this->output instanceof static && $this->output->newLineWritten()) {
return true;
}
return $this->newLineWritten;
}
/**
* Returns whether verbosity is quiet (-q).
*
* @return bool
*/
public function isQuiet()
public function isQuiet(): bool
{
return $this->output->isQuiet();
}
@@ -44,7 +94,7 @@ class OutputStyle extends SymfonyStyle
*
* @return bool
*/
public function isVerbose()
public function isVerbose(): bool
{
return $this->output->isVerbose();
}
@@ -54,7 +104,7 @@ class OutputStyle extends SymfonyStyle
*
* @return bool
*/
public function isVeryVerbose()
public function isVeryVerbose(): bool
{
return $this->output->isVeryVerbose();
}
@@ -64,7 +114,7 @@ class OutputStyle extends SymfonyStyle
*
* @return bool
*/
public function isDebug()
public function isDebug(): bool
{
return $this->output->isDebug();
}

View File

@@ -2,7 +2,6 @@
namespace Illuminate\Console;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@@ -21,10 +20,8 @@ class Parser
{
$name = static::name($expression);
if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches)) {
if (count($matches[1])) {
return array_merge([$name], static::parameters($matches[1]));
}
if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches) && count($matches[1])) {
return array_merge([$name], static::parameters($matches[1]));
}
return [$name, [], []];
@@ -81,11 +78,11 @@ class Parser
[$token, $description] = static::extractDescription($token);
switch (true) {
case Str::endsWith($token, '?*'):
case str_ends_with($token, '?*'):
return new InputArgument(trim($token, '?*'), InputArgument::IS_ARRAY, $description);
case Str::endsWith($token, '*'):
case str_ends_with($token, '*'):
return new InputArgument(trim($token, '*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description);
case Str::endsWith($token, '?'):
case str_ends_with($token, '?'):
return new InputArgument(trim($token, '?'), InputArgument::OPTIONAL, $description);
case preg_match('/(.+)\=\*(.+)/', $token, $matches):
return new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
@@ -116,9 +113,9 @@ class Parser
}
switch (true) {
case Str::endsWith($token, '='):
case str_ends_with($token, '='):
return new InputOption(trim($token, '='), $shortcut, InputOption::VALUE_OPTIONAL, $description);
case Str::endsWith($token, '=*'):
case str_ends_with($token, '=*'):
return new InputOption(trim($token, '=*'), $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description);
case preg_match('/(.+)\=\*(.+)/', $token, $matches):
return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));

View File

@@ -0,0 +1,77 @@
<?php
namespace Illuminate\Console;
use Illuminate\Console\View\Components\TwoColumnDetail;
use Symfony\Component\Console\Formatter\OutputFormatter;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;
class QuestionHelper extends SymfonyQuestionHelper
{
/**
* {@inheritdoc}
*/
protected function writePrompt(OutputInterface $output, Question $question)
{
$text = OutputFormatter::escapeTrailingBackslash($question->getQuestion());
$text = $this->ensureEndsWithPunctuation($text);
$text = " <fg=default;options=bold>$text</></>";
$default = $question->getDefault();
if ($question->isMultiline()) {
$text .= sprintf(' (press %s to continue)', 'Windows' == PHP_OS_FAMILY
? '<comment>Ctrl+Z</comment> then <comment>Enter</comment>'
: '<comment>Ctrl+D</comment>');
}
switch (true) {
case null === $default:
$text = sprintf('<info>%s</info>', $text);
break;
case $question instanceof ConfirmationQuestion:
$text = sprintf('<info>%s (yes/no)</info> [<comment>%s</comment>]', $text, $default ? 'yes' : 'no');
break;
case $question instanceof ChoiceQuestion:
$choices = $question->getChoices();
$text = sprintf('<info>%s</info> [<comment>%s</comment>]', $text, OutputFormatter::escape($choices[$default] ?? $default));
break;
}
$output->writeln($text);
if ($question instanceof ChoiceQuestion) {
foreach ($question->getChoices() as $key => $value) {
with(new TwoColumnDetail($output))->render($value, $key);
}
}
$output->write('<options=bold> </>');
}
/**
* Ensures the given string ends with punctuation.
*
* @param string $string
* @return string
*/
protected function ensureEndsWithPunctuation($string)
{
if (! str($string)->endsWith(['?', ':', '!', '.'])) {
return "$string:";
}
return $string;
}
}

View File

@@ -6,6 +6,7 @@ use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Reflector;
use InvalidArgumentException;
use LogicException;
use RuntimeException;
use Throwable;
class CallbackEvent extends Event
@@ -24,11 +25,25 @@ class CallbackEvent extends Event
*/
protected $parameters;
/**
* The result of the callback's execution.
*
* @var mixed
*/
protected $result;
/**
* The exception that was thrown when calling the callback, if any.
*
* @var \Throwable|null
*/
protected $exception;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @param string $callback
* @param string|callable $callback
* @param array $parameters
* @param \DateTimeZone|string|null $timezone
* @return void
@@ -50,58 +65,64 @@ class CallbackEvent extends Event
}
/**
* Run the given event.
* Run the callback event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*
* @throws \Exception
* @throws \Throwable
*/
public function run(Container $container)
{
if ($this->description && $this->withoutOverlapping &&
! $this->mutex->create($this)) {
return;
parent::run($container);
if ($this->exception) {
throw $this->exception;
}
$pid = getmypid();
register_shutdown_function(function () use ($pid) {
if ($pid === getmypid()) {
$this->removeMutex();
}
});
parent::callBeforeCallbacks($container);
try {
$response = is_object($this->callback)
? $container->call([$this->callback, '__invoke'], $this->parameters)
: $container->call($this->callback, $this->parameters);
$this->exitCode = $response === false ? 1 : 0;
} catch (Throwable $e) {
$this->exitCode = 1;
throw $e;
} finally {
$this->removeMutex();
parent::callAfterCallbacks($container);
}
return $response;
return $this->result;
}
/**
* Clear the mutex for the event.
* Determine if the event should skip because another process is overlapping.
*
* @return bool
*/
public function shouldSkipDueToOverlapping()
{
return $this->description && parent::shouldSkipDueToOverlapping();
}
/**
* Indicate that the callback should run in the background.
*
* @return void
*
* @throws \RuntimeException
*/
protected function removeMutex()
public function runInBackground()
{
if ($this->description && $this->withoutOverlapping) {
$this->mutex->forget($this);
throw new RuntimeException('Scheduled closures can not be run in the background.');
}
/**
* Run the callback.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*/
protected function execute($container)
{
try {
$this->result = is_object($this->callback)
? $container->call([$this->callback, '__invoke'], $this->parameters)
: $container->call($this->callback, $this->parameters);
return $this->result === false ? 1 : 0;
} catch (Throwable $e) {
$this->exception = $e;
return 1;
}
}
@@ -121,13 +142,7 @@ class CallbackEvent extends Event
);
}
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this->skip(function () {
return $this->mutex->exists($this);
});
return parent::withoutOverlapping($expiresAt);
}
/**
@@ -145,19 +160,7 @@ class CallbackEvent extends Event
);
}
$this->onOneServer = true;
return $this;
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
return 'framework/schedule-'.sha1($this->description);
return parent::onOneServer();
}
/**
@@ -173,4 +176,26 @@ class CallbackEvent extends Event
return is_string($this->callback) ? $this->callback : 'Callback';
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
return 'framework/schedule-'.sha1($this->description ?? '');
}
/**
* Clear the mutex for the event.
*
* @return void
*/
protected function removeMutex()
{
if ($this->description) {
parent::removeMutex();
}
}
}

View File

@@ -26,7 +26,7 @@ class Event
/**
* The command string.
*
* @var string
* @var string|null
*/
public $command;
@@ -47,7 +47,7 @@ class Event
/**
* The user the command should run as.
*
* @var string
* @var string|null
*/
public $user;
@@ -138,7 +138,7 @@ class Event
/**
* The human readable description of the event.
*
* @var string
* @var string|null
*/
public $description;
@@ -149,6 +149,13 @@ class Event
*/
public $mutex;
/**
* The mutex name resolver callback.
*
* @var \Closure|null
*/
public $mutexNameResolver;
/**
* The exit status code of the command.
*
@@ -188,62 +195,46 @@ class Event
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*
* @throws \Throwable
*/
public function run(Container $container)
{
if ($this->withoutOverlapping &&
! $this->mutex->create($this)) {
if ($this->shouldSkipDueToOverlapping()) {
return;
}
$this->runInBackground
? $this->runCommandInBackground($container)
: $this->runCommandInForeground($container);
}
$exitCode = $this->start($container);
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
return 'framework'.DIRECTORY_SEPARATOR.'schedule-'.sha1($this->expression.$this->command);
}
/**
* Run the command in the foreground.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
protected function runCommandInForeground(Container $container)
{
try {
$this->callBeforeCallbacks($container);
$this->exitCode = Process::fromShellCommandline(
$this->buildCommand(), base_path(), null, null, null
)->run();
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
if (! $this->runInBackground) {
$this->finish($container, $exitCode);
}
}
/**
* Run the command in the background.
* Determine if the event should skip because another process is overlapping.
*
* @return bool
*/
public function shouldSkipDueToOverlapping()
{
return $this->withoutOverlapping && ! $this->mutex->create($this);
}
/**
* Run the command process.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
* @return int
*
* @throws \Throwable
*/
protected function runCommandInBackground(Container $container)
protected function start($container)
{
try {
$this->callBeforeCallbacks($container);
Process::fromShellCommandline($this->buildCommand(), base_path(), null, null, null)->run();
return $this->execute($container);
} catch (Throwable $exception) {
$this->removeMutex();
@@ -251,6 +242,37 @@ class Event
}
}
/**
* Run the command process.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*/
protected function execute($container)
{
return Process::fromShellCommandline(
$this->buildCommand(), base_path(), null, null, null
)->run();
}
/**
* Mark the command process as finished and run callbacks/cleanup.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param int $exitCode
* @return void
*/
public function finish(Container $container, $exitCode)
{
$this->exitCode = (int) $exitCode;
try {
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
}
}
/**
* Call all of the "before" callbacks for the event.
*
@@ -277,24 +299,6 @@ class Event
}
}
/**
* Call all of the "after" callbacks for the event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param int $exitCode
* @return void
*/
public function callAfterCallbacksWithExitCode(Container $container, $exitCode)
{
$this->exitCode = (int) $exitCode;
try {
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
}
}
/**
* Build the command string.
*
@@ -783,7 +787,7 @@ class Event
}
return $this->then(function (Container $container) use ($callback) {
if (0 === $this->exitCode) {
if ($this->exitCode === 0) {
$container->call($callback);
}
});
@@ -818,7 +822,7 @@ class Event
}
return $this->then(function (Container $container) use ($callback) {
if (0 !== $this->exitCode) {
if ($this->exitCode !== 0) {
$container->call($callback);
}
});
@@ -931,6 +935,35 @@ class Event
return $this;
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
$mutexNameResolver = $this->mutexNameResolver;
if (! is_null($mutexNameResolver) && is_callable($mutexNameResolver)) {
return $mutexNameResolver($this);
}
return 'framework'.DIRECTORY_SEPARATOR.'schedule-'.sha1($this->expression.$this->command);
}
/**
* Set the mutex name or name resolver callback.
*
* @param \Closure|string $mutexName
* @return $this
*/
public function createMutexNameUsing(Closure|string $mutexName)
{
$this->mutexNameResolver = is_string($mutexName) ? fn () => $mutexName : $mutexName;
return $this;
}
/**
* Delete the mutex for the event.
*

View File

@@ -60,9 +60,9 @@ trait ManagesFrequencies
if ($endTime->lessThan($startTime)) {
if ($startTime->greaterThan($now)) {
$startTime->subDay(1);
$startTime = $startTime->subDay(1);
} else {
$endTime->addDay(1);
$endTime = $endTime->addDay(1);
}
}
@@ -174,6 +174,16 @@ trait ManagesFrequencies
return $this->spliceIntoPosition(1, $offset);
}
/**
* Schedule the event to run every odd hour.
*
* @return $this
*/
public function everyOddHour()
{
return $this->spliceIntoPosition(1, 0)->spliceIntoPosition(2, '1-23/2');
}
/**
* Schedule the event to run every two hours.
*
@@ -467,6 +477,21 @@ trait ManagesFrequencies
->spliceIntoPosition(4, '1-12/3');
}
/**
* Schedule the event to run quarterly on a given day and time.
*
* @param int $dayOfQuarter
* @param int $time
* @return $this
*/
public function quarterlyOn($dayOfQuarter = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $dayOfQuarter)
->spliceIntoPosition(4, '1-12/3');
}
/**
* Schedule the event to run yearly.
*
@@ -531,7 +556,7 @@ trait ManagesFrequencies
*/
protected function spliceIntoPosition($position, $value)
{
$segments = explode(' ', $this->expression);
$segments = preg_split("/\s+/", $this->expression);
$segments[$position - 1] = $value;

View File

@@ -14,7 +14,6 @@ use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Support\ProcessUtils;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use RuntimeException;
@@ -23,11 +22,17 @@ class Schedule
use Macroable;
const SUNDAY = 0;
const MONDAY = 1;
const TUESDAY = 2;
const WEDNESDAY = 3;
const THURSDAY = 4;
const FRIDAY = 5;
const SATURDAY = 6;
/**
@@ -272,11 +277,11 @@ class Schedule
return ProcessUtils::escapeArgument($value);
});
if (Str::startsWith($key, '--')) {
if (str_starts_with($key, '--')) {
$value = $value->map(function ($value) use ($key) {
return "{$key}={$value}";
});
} elseif (Str::startsWith($key, '-')) {
} elseif (str_starts_with($key, '-')) {
$value = $value->map(function ($value) use ($key) {
return "{$key} {$value}";
});
@@ -352,7 +357,7 @@ class Schedule
} catch (BindingResolutionException $e) {
throw new RuntimeException(
'Unable to resolve the dispatcher from the service container. Please bind it or install the illuminate/bus package.',
$e->getCode(), $e
is_int($e->getCode()) ? $e->getCode() : 0, $e
);
}
}

View File

@@ -32,7 +32,7 @@ class ScheduleClearCacheCommand extends Command
foreach ($schedule->events($this->laravel) as $event) {
if ($event->mutex->exists($event)) {
$this->line('<info>Deleting mutex for:</info> '.$event->command);
$this->components->info(sprintf('Deleting mutex for [%s]', $event->command));
$event->mutex->forget($event);
@@ -41,7 +41,7 @@ class ScheduleClearCacheCommand extends Command
}
if (! $mutexCleared) {
$this->info('No mutex files were found.');
$this->components->info('No mutex files were found.');
}
}
}

View File

@@ -5,7 +5,9 @@ namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Console\Events\ScheduledBackgroundTaskFinished;
use Illuminate\Contracts\Events\Dispatcher;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:finish')]
class ScheduleFinishCommand extends Command
{
/**
@@ -15,6 +17,17 @@ class ScheduleFinishCommand extends Command
*/
protected $signature = 'schedule:finish {id} {code=0}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:finish';
/**
* The console command description.
*
@@ -40,7 +53,7 @@ class ScheduleFinishCommand extends Command
collect($schedule->events())->filter(function ($value) {
return $value->mutexName() == $this->argument('id');
})->each(function ($event) {
$event->callafterCallbacksWithExitCode($this->laravel, $this->argument('code'));
$event->finish($this->laravel, $this->argument('code'));
$this->laravel->make(Dispatcher::class)->dispatch(new ScheduledBackgroundTaskFinished($event));
});

View File

@@ -2,11 +2,18 @@
namespace Illuminate\Console\Scheduling;
use Closure;
use Cron\CronExpression;
use DateTimeZone;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use ReflectionClass;
use ReflectionFunction;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Terminal;
#[AsCommand(name: 'schedule:list')]
class ScheduleListCommand extends Command
{
/**
@@ -14,14 +21,35 @@ class ScheduleListCommand extends Command
*
* @var string
*/
protected $signature = 'schedule:list {--timezone= : The timezone that times should be displayed in}';
protected $signature = 'schedule:list
{--timezone= : The timezone that times should be displayed in}
{--next : Sort the listed tasks by their next due date}
';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:list';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List the scheduled commands';
protected $description = 'List all scheduled tasks';
/**
* The terminal width resolver callback.
*
* @var \Closure|null
*/
protected static $terminalWidthResolver;
/**
* Execute the console command.
@@ -33,23 +61,199 @@ class ScheduleListCommand extends Command
*/
public function handle(Schedule $schedule)
{
foreach ($schedule->events() as $event) {
$rows[] = [
$event->command,
$event->expression,
$event->description,
(new CronExpression($event->expression))
->getNextRunDate(Carbon::now()->setTimezone($event->timezone))
->setTimezone(new DateTimeZone($this->option('timezone') ?? config('app.timezone')))
->format('Y-m-d H:i:s P'),
];
$events = collect($schedule->events());
if ($events->isEmpty()) {
$this->components->info('No scheduled tasks have been defined.');
return;
}
$this->table([
'Command',
'Interval',
'Description',
'Next Due',
], $rows ?? []);
$terminalWidth = self::getTerminalWidth();
$expressionSpacing = $this->getCronExpressionSpacing($events);
$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));
$events = $this->sortEvents($events, $timezone);
$events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $timezone) {
$expression = $this->formatCronExpression($event->expression, $expressionSpacing);
$command = $event->command ?? '';
$description = $event->description ?? '';
if (! $this->output->isVerbose()) {
$command = str_replace([Application::phpBinary(), Application::artisanBinary()], [
'php',
preg_replace("#['\"]#", '', Application::artisanBinary()),
], $command);
}
if ($event instanceof CallbackEvent) {
if (class_exists($description)) {
$command = $description;
$description = '';
} else {
$command = 'Closure at: '.$this->getClosureLocation($event);
}
}
$command = mb_strlen($command) > 1 ? "{$command} " : '';
$nextDueDateLabel = 'Next Due:';
$nextDueDate = $this->getNextDueDateForEvent($event, $timezone);
$nextDueDate = $this->output->isVerbose()
? $nextDueDate->format('Y-m-d H:i:s P')
: $nextDueDate->diffForHumans();
$hasMutex = $event->mutex->exists($event) ? 'Has Mutex ' : '';
$dots = str_repeat('.', max(
$terminalWidth - mb_strlen($expression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0
));
// Highlight the parameters...
$command = preg_replace("#(php artisan [\w\-:]+) (.+)#", '$1 <fg=yellow;options=bold>$2</>', $command);
return [sprintf(
' <fg=yellow>%s</> %s<fg=#6C7280>%s %s%s %s</>',
$expression,
$command,
$dots,
$hasMutex,
$nextDueDateLabel,
$nextDueDate
), $this->output->isVerbose() && mb_strlen($description) > 1 ? sprintf(
' <fg=#6C7280>%s%s %s</>',
str_repeat(' ', mb_strlen($expression) + 2),
'⇁',
$description
) : ''];
});
$this->line(
$events->flatten()->filter()->prepend('')->push('')->toArray()
);
}
/**
* Gets the spacing to be used on each event row.
*
* @param \Illuminate\Support\Collection $events
* @return array<int, int>
*/
private function getCronExpressionSpacing($events)
{
$rows = $events->map(fn ($event) => array_map('mb_strlen', preg_split("/\s+/", $event->expression)));
return collect($rows[0] ?? [])->keys()->map(fn ($key) => $rows->max($key))->all();
}
/**
* Sorts the events by due date if option set.
*
* @param \Illuminate\Support\Collection $events
* @param \DateTimeZone $timezone
* @return \Illuminate\Support\Collection
*/
private function sortEvents(\Illuminate\Support\Collection $events, DateTimeZone $timezone)
{
return $this->option('next')
? $events->sortBy(fn ($event) => $this->getNextDueDateForEvent($event, $timezone))
: $events;
}
/**
* Get the next due date for an event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeZone $timezone
* @return \Illuminate\Support\Carbon
*/
private function getNextDueDateForEvent($event, DateTimeZone $timezone)
{
return Carbon::instance(
(new CronExpression($event->expression))
->getNextRunDate(Carbon::now()->setTimezone($event->timezone))
->setTimezone($timezone)
);
}
/**
* Formats the cron expression based on the spacing provided.
*
* @param string $expression
* @param array<int, int> $spacing
* @return string
*/
private function formatCronExpression($expression, $spacing)
{
$expressions = preg_split("/\s+/", $expression);
return collect($spacing)
->map(fn ($length, $index) => str_pad($expressions[$index], $length))
->implode(' ');
}
/**
* Get the file and line number for the event closure.
*
* @param \Illuminate\Console\Scheduling\CallbackEvent $event
* @return string
*/
private function getClosureLocation(CallbackEvent $event)
{
$callback = tap((new ReflectionClass($event))->getProperty('callback'))
->setAccessible(true)
->getValue($event);
if ($callback instanceof Closure) {
$function = new ReflectionFunction($callback);
return sprintf(
'%s:%s',
str_replace($this->laravel->basePath().DIRECTORY_SEPARATOR, '', $function->getFileName() ?: ''),
$function->getStartLine()
);
}
if (is_string($callback)) {
return $callback;
}
if (is_array($callback)) {
$className = is_string($callback[0]) ? $callback[0] : $callback[0]::class;
return sprintf('%s::%s', $className, $callback[1]);
}
return sprintf('%s::__invoke', $callback::class);
}
/**
* Get the terminal width.
*
* @return int
*/
public static function getTerminalWidth()
{
return is_null(static::$terminalWidthResolver)
? (new Terminal)->getWidth()
: call_user_func(static::$terminalWidthResolver);
}
/**
* Set a callback that should be used when resolving the terminal width.
*
* @param \Closure|null $resolver
* @return void
*/
public static function resolveTerminalWidthUsing($resolver)
{
static::$terminalWidthResolver = $resolver;
}
}

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Console\Events\ScheduledTaskFinished;
@@ -9,9 +10,12 @@ use Illuminate\Console\Events\ScheduledTaskSkipped;
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;
#[AsCommand(name: 'schedule:run')]
class ScheduleRunCommand extends Command
{
/**
@@ -21,6 +25,17 @@ class ScheduleRunCommand extends Command
*/
protected $name = 'schedule:run';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:run';
/**
* The console command description.
*
@@ -63,6 +78,13 @@ class ScheduleRunCommand extends Command
*/
protected $handler;
/**
* The PHP binary used by the command.
*
* @var string
*/
protected $phpBinary;
/**
* Create a new command instance.
*
@@ -88,6 +110,9 @@ class ScheduleRunCommand extends Command
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
$this->phpBinary = Application::phpBinary();
$this->newLine();
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
@@ -106,7 +131,9 @@ class ScheduleRunCommand extends Command
}
if (! $this->eventsRan) {
$this->info('No scheduled commands are ready to run.');
$this->components->info('No scheduled commands are ready to run.');
} else {
$this->newLine();
}
}
@@ -121,7 +148,9 @@ class ScheduleRunCommand extends Command
if ($this->schedule->serverShouldRun($event, $this->startedAt)) {
$this->runEvent($event);
} else {
$this->line('<info>Skipping command (has already run on another server):</info> '.$event->getSummaryForDisplay());
$this->components->info(sprintf(
'Skipping [%s], as command already run on another server.', $event->getSummaryForDisplay()
));
}
}
@@ -133,25 +162,46 @@ class ScheduleRunCommand extends Command
*/
protected function runEvent($event)
{
$this->line('<info>['.date('c').'] Running scheduled command:</info> '.$event->getSummaryForDisplay());
$summary = $event->getSummaryForDisplay();
$this->dispatcher->dispatch(new ScheduledTaskStarting($event));
$command = $event instanceof CallbackEvent
? $summary
: trim(str_replace($this->phpBinary, '', $event->command));
$start = microtime(true);
$description = sprintf(
'<fg=gray>%s</> Running [%s]%s',
Carbon::now()->format('Y-m-d H:i:s'),
$command,
$event->runInBackground ? ' in background' : '',
);
try {
$event->run($this->laravel);
$this->components->task($description, function () use ($event) {
$this->dispatcher->dispatch(new ScheduledTaskStarting($event));
$this->dispatcher->dispatch(new ScheduledTaskFinished(
$event,
round(microtime(true) - $start, 2)
));
$start = microtime(true);
$this->eventsRan = true;
} catch (Throwable $e) {
$this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e));
try {
$event->run($this->laravel);
$this->handler->report($e);
$this->dispatcher->dispatch(new ScheduledTaskFinished(
$event,
round(microtime(true) - $start, 2)
));
$this->eventsRan = true;
} catch (Throwable $e) {
$this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e));
$this->handler->report($e);
}
return $event->exitCode == 0;
});
if (! $event instanceof CallbackEvent) {
$this->components->bulletList([
$event->getSummaryForDisplay(),
]);
}
}
}

View File

@@ -2,8 +2,11 @@
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:test')]
class ScheduleTestCommand extends Command
{
/**
@@ -11,7 +14,18 @@ class ScheduleTestCommand extends Command
*
* @var string
*/
protected $name = 'schedule:test';
protected $signature = 'schedule:test {--name= : The name of the scheduled command to run}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:test';
/**
* The console command description.
@@ -28,6 +42,8 @@ class ScheduleTestCommand extends Command
*/
public function handle(Schedule $schedule)
{
$phpBinary = Application::phpBinary();
$commands = $schedule->events();
$commandNames = [];
@@ -36,12 +52,48 @@ class ScheduleTestCommand extends Command
$commandNames[] = $command->command ?? $command->getSummaryForDisplay();
}
$index = array_search($this->choice('Which command would you like to run?', $commandNames), $commandNames);
if (empty($commandNames)) {
return $this->components->info('No scheduled commands have been defined.');
}
if (! empty($name = $this->option('name'))) {
$commandBinary = $phpBinary.' '.Application::artisanBinary();
$matches = array_filter($commandNames, function ($commandName) use ($commandBinary, $name) {
return trim(str_replace($commandBinary, '', $commandName)) === $name;
});
if (count($matches) !== 1) {
$this->components->info('No matching scheduled command found.');
return;
}
$index = key($matches);
} else {
$index = array_search($this->components->choice('Which command would you like to run?', $commandNames), $commandNames);
}
$event = $commands[$index];
$this->line('<info>['.date('c').'] Running scheduled command:</info> '.$event->getSummaryForDisplay());
$summary = $event->getSummaryForDisplay();
$event->run($this->laravel);
$command = $event instanceof CallbackEvent
? $summary
: trim(str_replace($phpBinary, '', $event->command));
$description = sprintf(
'Running [%s]%s',
$command,
$event->runInBackground ? ' in background' : '',
);
$this->components->task($description, fn () => $event->run($this->laravel));
if (! $event instanceof CallbackEvent) {
$this->components->bulletList([$event->getSummaryForDisplay()]);
}
$this->newLine();
}
}

View File

@@ -4,8 +4,10 @@ namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Process\Process;
#[AsCommand(name: 'schedule:work')]
class ScheduleWorkCommand extends Command
{
/**
@@ -15,6 +17,17 @@ class ScheduleWorkCommand extends Command
*/
protected $name = 'schedule:work';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:work';
/**
* The console command description.
*
@@ -29,9 +42,9 @@ class ScheduleWorkCommand extends Command
*/
public function handle()
{
$this->info('Schedule worker started successfully.');
$this->components->info('Running schedule tasks every minute.');
[$lastExecutionStartedAt, $keyOfLastExecutionWithOutput, $executions] = [null, null, []];
[$lastExecutionStartedAt, $executions] = [null, []];
while (true) {
usleep(100 * 1000);
@@ -50,18 +63,10 @@ class ScheduleWorkCommand extends Command
}
foreach ($executions as $key => $execution) {
$output = trim($execution->getIncrementalOutput()).
trim($execution->getIncrementalErrorOutput());
$output = $execution->getIncrementalOutput().
$execution->getIncrementalErrorOutput();
if (! empty($output)) {
if ($key !== $keyOfLastExecutionWithOutput) {
$this->info(PHP_EOL.'['.date('c').'] Execution #'.($key + 1).' output:');
$keyOfLastExecutionWithOutput = $key;
}
$this->output->writeln($output);
}
$this->output->write(ltrim($output, "\n"));
if (! $execution->isRunning()) {
unset($executions[$key]);

View File

@@ -0,0 +1,152 @@
<?php
namespace Illuminate\Console;
/**
* @internal
*/
class Signals
{
/**
* The signal registry instance.
*
* @var \Symfony\Component\Console\SignalRegistry\SignalRegistry
*/
protected $registry;
/**
* The signal registry's previous list of handlers.
*
* @var array<int, array<int, callable>>|null
*/
protected $previousHandlers;
/**
* The current availability resolver, if any.
*
* @var (callable(): bool)|null
*/
protected static $availabilityResolver;
/**
* Create a new signal registrar instance.
*
* @param \Symfony\Component\Console\SignalRegistry\SignalRegistry $registry
* @return void
*/
public function __construct($registry)
{
$this->registry = $registry;
$this->previousHandlers = $this->getHandlers();
}
/**
* Register a new signal handler.
*
* @param int $signal
* @param callable(int $signal): void $callback
* @return void
*/
public function register($signal, $callback)
{
$this->previousHandlers[$signal] ??= $this->initializeSignal($signal);
with($this->getHandlers(), function ($handlers) use ($signal) {
$handlers[$signal] ??= $this->initializeSignal($signal);
$this->setHandlers($handlers);
});
$this->registry->register($signal, $callback);
with($this->getHandlers(), function ($handlers) use ($signal) {
$lastHandlerInserted = array_pop($handlers[$signal]);
array_unshift($handlers[$signal], $lastHandlerInserted);
$this->setHandlers($handlers);
});
}
/**
* Gets the signal's existing handler in array format.
*
* @return array<int, callable(int $signal): void>
*/
protected function initializeSignal($signal)
{
return is_callable($existingHandler = pcntl_signal_get_handler($signal))
? [$existingHandler]
: null;
}
/**
* Unregister the current signal handlers.
*
* @return array<int, array<int, callable(int $signal): void>>
*/
public function unregister()
{
$previousHandlers = $this->previousHandlers;
foreach ($previousHandlers as $signal => $handler) {
if (is_null($handler)) {
pcntl_signal($signal, SIG_DFL);
unset($previousHandlers[$signal]);
}
}
$this->setHandlers($previousHandlers);
}
/**
* Execute the given callback if "signals" should be used and are available.
*
* @param callable $callback
* @return void
*/
public static function whenAvailable($callback)
{
$resolver = static::$availabilityResolver;
if ($resolver()) {
$callback();
}
}
/**
* Get the registry's handlers.
*
* @return array<int, array<int, callable>>
*/
protected function getHandlers()
{
return (fn () => $this->signalHandlers)
->call($this->registry);
}
/**
* Set the registry's handlers.
*
* @param array<int, array<int, callable(int $signal):void>> $handlers
* @return void
*/
protected function setHandlers($handlers)
{
(fn () => $this->signalHandlers = $handlers)
->call($this->registry);
}
/**
* Set the availability resolver.
*
* @param callable(): bool
* @return void
*/
public static function resolveAvailabilityUsing($resolver)
{
static::$availabilityResolver = $resolver;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Output\OutputInterface;
class Alert extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $string
* @param int $verbosity
* @return void
*/
public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
$string = $this->mutate($string, [
Mutators\EnsureDynamicContentIsHighlighted::class,
Mutators\EnsurePunctuation::class,
Mutators\EnsureRelativePaths::class,
]);
$this->renderView('alert', [
'content' => $string,
], $verbosity);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Output\OutputInterface;
class BulletList extends Component
{
/**
* Renders the component using the given arguments.
*
* @param array<int, string> $elements
* @param int $verbosity
* @return void
*/
public function render($elements, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
$elements = $this->mutate($elements, [
Mutators\EnsureDynamicContentIsHighlighted::class,
Mutators\EnsureNoPunctuation::class,
Mutators\EnsureRelativePaths::class,
]);
$this->renderView('bullet-list', [
'elements' => $elements,
], $verbosity);
}
}

View File

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

View File

@@ -0,0 +1,124 @@
<?php
namespace Illuminate\Console\View\Components;
use Illuminate\Console\OutputStyle;
use Illuminate\Console\QuestionHelper;
use ReflectionClass;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
use function Termwind\render;
use function Termwind\renderUsing;
abstract class Component
{
/**
* The output style implementation.
*
* @var \Illuminate\Console\OutputStyle
*/
protected $output;
/**
* The list of mutators to apply on the view data.
*
* @var array<int, callable(string): string>
*/
protected $mutators;
/**
* Creates a new component instance.
*
* @param \Illuminate\Console\OutputStyle $output
* @return void
*/
public function __construct($output)
{
$this->output = $output;
}
/**
* Renders the given view.
*
* @param string $view
* @param \Illuminate\Contracts\Support\Arrayable|array $data
* @param int $verbosity
* @return void
*/
protected function renderView($view, $data, $verbosity)
{
renderUsing($this->output);
render((string) $this->compile($view, $data), $verbosity);
}
/**
* Compile the given view contents.
*
* @param string $view
* @param array $data
* @return void
*/
protected function compile($view, $data)
{
extract($data);
ob_start();
include __DIR__."/../../resources/views/components/$view.php";
return tap(ob_get_contents(), function () {
ob_end_clean();
});
}
/**
* Mutates the given data with the given set of mutators.
*
* @param array<int, string>|string $data
* @param array<int, callable(string): string> $mutators
* @return array<int, string>|string
*/
protected function mutate($data, $mutators)
{
foreach ($mutators as $mutator) {
$mutator = new $mutator;
if (is_iterable($data)) {
foreach ($data as $key => $value) {
$data[$key] = $mutator($value);
}
} else {
$data = $mutator($data);
}
}
return $data;
}
/**
* Eventually performs a question using the component's question helper.
*
* @param callable $callable
* @return mixed
*/
protected function usingQuestionHelper($callable)
{
$property = with(new ReflectionClass(OutputStyle::class))
->getParentClass()
->getProperty('questionHelper');
$property->setAccessible(true);
$currentHelper = $property->isInitialized($this->output)
? $property->getValue($this->output)
: new SymfonyQuestionHelper();
$property->setValue($this->output, new QuestionHelper);
try {
return $callable();
} finally {
$property->setValue($this->output, $currentHelper);
}
}
}

View File

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

View File

@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Output\OutputInterface;
class Error extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $string
* @param int $verbosity
* @return void
*/
public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
with(new Line($this->output))->render('error', $string, $verbosity);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Illuminate\Console\View\Components;
use InvalidArgumentException;
/**
* @method void alert(string $string, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method void bulletList(array $elements, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method mixed choice(string $question, array $choices, $default = null)
* @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)
* @method void line(string $style, string $string, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method void task(string $description, ?callable $task = null, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method void twoColumnDetail(string $first, ?string $second = null, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
* @method void warn(string $string, int $verbosity = \Symfony\Component\Console\Output\OutputInterface::VERBOSITY_NORMAL)
*/
class Factory
{
/**
* The output interface implementation.
*
* @var \Illuminate\Console\OutputStyle
*/
protected $output;
/**
* Creates a new factory instance.
*
* @param \Illuminate\Console\OutputStyle $output
* @return void
*/
public function __construct($output)
{
$this->output = $output;
}
/**
* Dynamically handle calls into the component instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*
* @throws \InvalidArgumentException
*/
public function __call($method, $parameters)
{
$component = '\Illuminate\Console\View\Components\\'.ucfirst($method);
throw_unless(class_exists($component), new InvalidArgumentException(sprintf(
'Console component [%s] not found.', $method
)));
return with(new $component($this->output))->render(...$parameters);
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Output\OutputInterface;
class Info extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $string
* @param int $verbosity
* @return void
*/
public function render($string, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
with(new Line($this->output))->render('info', $string, $verbosity);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Illuminate\Console\View\Components;
use Illuminate\Console\Contracts\NewLineAware;
use Symfony\Component\Console\Output\OutputInterface;
class Line extends Component
{
/**
* The possible line styles.
*
* @var array<string, array<string, string>>
*/
protected static $styles = [
'info' => [
'bgColor' => 'blue',
'fgColor' => 'white',
'title' => 'info',
],
'warn' => [
'bgColor' => 'yellow',
'fgColor' => 'black',
'title' => 'warn',
],
'error' => [
'bgColor' => 'red',
'fgColor' => 'white',
'title' => 'error',
],
];
/**
* Renders the component using the given arguments.
*
* @param string $style
* @param string $string
* @param int $verbosity
* @return void
*/
public function render($style, $string, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
$string = $this->mutate($string, [
Mutators\EnsureDynamicContentIsHighlighted::class,
Mutators\EnsurePunctuation::class,
Mutators\EnsureRelativePaths::class,
]);
$this->renderView('line', array_merge(static::$styles[$style], [
'marginTop' => ($this->output instanceof NewLineAware && $this->output->newLineWritten()) ? 0 : 1,
'content' => $string,
]), $verbosity);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Illuminate\Console\View\Components\Mutators;
class EnsureDynamicContentIsHighlighted
{
/**
* Highlight dynamic content within the given string.
*
* @param string $string
* @return string
*/
public function __invoke($string)
{
return preg_replace('/\[([^\]]+)\]/', '<options=bold>[$1]</>', (string) $string);
}
}

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