dependencies-upgrade

This commit is contained in:
RafficMohammed
2023-01-08 02:20:59 +05:30
parent 7870479b18
commit 49021a4497
1711 changed files with 74994 additions and 70803 deletions

View File

@@ -2,9 +2,9 @@
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/d/total.svg" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/v/stable.svg" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://poser.pugx.org/laravel/framework/license.svg" alt="License"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel

View File

@@ -15,41 +15,43 @@
}
],
"require": {
"php": "^7.2.5|^8.0",
"php": "^7.3|^8.0",
"ext-json": "*",
"ext-mbstring": "*",
"ext-openssl": "*",
"doctrine/inflector": "^1.4|^2.0",
"dragonmantank/cron-expression": "^2.3.1",
"dragonmantank/cron-expression": "^3.0.2",
"egulias/email-validator": "^2.1.10",
"league/commonmark": "^1.3",
"laravel/serializable-closure": "^1.0",
"league/commonmark": "^1.3|^2.0.2",
"league/flysystem": "^1.1",
"monolog/monolog": "^2.0",
"nesbot/carbon": "^2.31",
"nesbot/carbon": "^2.53.1",
"opis/closure": "^3.6",
"psr/container": "^1.0",
"psr/log": "^1.0|^2.0",
"psr/simple-cache": "^1.0",
"ramsey/uuid": "^3.7|^4.0",
"swiftmailer/swiftmailer": "^6.0",
"symfony/console": "^5.0",
"symfony/error-handler": "^5.0",
"symfony/finder": "^5.0",
"symfony/http-foundation": "^5.0",
"symfony/http-kernel": "^5.0",
"symfony/mime": "^5.0",
"symfony/polyfill-php73": "^1.17",
"symfony/process": "^5.0",
"symfony/routing": "^5.0",
"symfony/var-dumper": "^5.0",
"ramsey/uuid": "^4.2.2",
"swiftmailer/swiftmailer": "^6.3",
"symfony/console": "^5.4",
"symfony/error-handler": "^5.4",
"symfony/finder": "^5.4",
"symfony/http-foundation": "^5.4",
"symfony/http-kernel": "^5.4",
"symfony/mime": "^5.4",
"symfony/process": "^5.4",
"symfony/routing": "^5.4",
"symfony/var-dumper": "^5.4",
"tijsverkoyen/css-to-inline-styles": "^2.2.2",
"vlucas/phpdotenv": "^4.0",
"voku/portable-ascii": "^1.4.8"
"vlucas/phpdotenv": "^5.4.1",
"voku/portable-ascii": "^1.6.1"
},
"replace": {
"illuminate/auth": "self.version",
"illuminate/broadcasting": "self.version",
"illuminate/bus": "self.version",
"illuminate/cache": "self.version",
"illuminate/collections": "self.version",
"illuminate/config": "self.version",
"illuminate/console": "self.version",
"illuminate/container": "self.version",
@@ -62,6 +64,7 @@
"illuminate/hashing": "self.version",
"illuminate/http": "self.version",
"illuminate/log": "self.version",
"illuminate/macroable": "self.version",
"illuminate/mail": "self.version",
"illuminate/notifications": "self.version",
"illuminate/pagination": "self.version",
@@ -77,32 +80,35 @@
"illuminate/view": "self.version"
},
"require-dev": {
"aws/aws-sdk-php": "^3.155",
"doctrine/dbal": "^2.6",
"filp/whoops": "^2.8",
"guzzlehttp/guzzle": "^6.3.1|^7.0.1",
"aws/aws-sdk-php": "^3.198.1",
"doctrine/dbal": "^2.13.3|^3.1.4",
"filp/whoops": "^2.14.3",
"guzzlehttp/guzzle": "^6.5.5|^7.0.1",
"league/flysystem-cached-adapter": "^1.0",
"mockery/mockery": "~1.3.3|^1.4.2",
"moontoast/math": "^1.1",
"orchestra/testbench-core": "^5.8",
"mockery/mockery": "^1.4.4",
"orchestra/testbench-core": "^6.27",
"pda/pheanstalk": "^4.0",
"phpunit/phpunit": "^8.4|^9.3.3",
"predis/predis": "^1.1.1",
"symfony/cache": "^5.0"
"phpunit/phpunit": "^8.5.19|^9.5.8",
"predis/predis": "^1.1.9",
"symfony/cache": "^5.4"
},
"provide": {
"psr/container-implementation": "1.0"
"psr/container-implementation": "1.0",
"psr/simple-cache-implementation": "1.0"
},
"conflict": {
"tightenco/collect": "<5.5.33"
},
"autoload": {
"files": [
"src/Illuminate/Collections/helpers.php",
"src/Illuminate/Events/functions.php",
"src/Illuminate/Foundation/helpers.php",
"src/Illuminate/Support/helpers.php"
],
"psr-4": {
"Illuminate\\": "src/Illuminate/"
"Illuminate\\": "src/Illuminate/",
"Illuminate\\Support\\": ["src/Illuminate/Macroable/", "src/Illuminate/Collections/"]
}
},
"autoload-dev": {
@@ -115,40 +121,45 @@
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
"dev-master": "8.x-dev"
}
},
"suggest": {
"ext-bcmath": "Required to use the multiple_of validation rule.",
"ext-ftp": "Required to use the Flysystem FTP driver.",
"ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
"ext-memcached": "Required to use the memcache cache driver.",
"ext-pcntl": "Required to use all features of the queue worker.",
"ext-posix": "Required to use all features of the queue worker.",
"ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0).",
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.155).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.6).",
"filp/whoops": "Required for friendly error pages in development (^2.8).",
"ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
"aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage and SES mail driver (^3.198.1).",
"brianium/paratest": "Required to run tests in parallel (^6.0).",
"doctrine/dbal": "Required to rename columns and drop SQLite columns (^2.13.3|^3.1.4).",
"filp/whoops": "Required for friendly error pages in development (^2.14.3).",
"fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
"guzzlehttp/guzzle": "Required to use the HTTP Client, Mailgun mail driver and the ping methods on schedules (^6.3.1|^7.0.1).",
"guzzlehttp/guzzle": "Required to use the HTTP Client, Mailgun mail driver and the ping methods on schedules (^6.5.5|^7.0.1).",
"laravel/tinker": "Required to use the tinker console command (^2.0).",
"league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^1.0).",
"league/flysystem-cached-adapter": "Required to use the Flysystem cache (^1.0).",
"league/flysystem-sftp": "Required to use the Flysystem SFTP driver (^1.0).",
"mockery/mockery": "Required to use mocking (~1.3.3|^1.4.2).",
"moontoast/math": "Required to use ordered UUIDs (^1.1).",
"mockery/mockery": "Required to use mocking (^1.4.4).",
"nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).",
"pda/pheanstalk": "Required to use the beanstalk queue driver (^4.0).",
"phpunit/phpunit": "Required to use assertions and run tests (^8.4|^9.3.3).",
"predis/predis": "Required to use the predis connector (^1.1.2).",
"phpunit/phpunit": "Required to use assertions and run tests (^8.5.19|^9.5.8).",
"predis/predis": "Required to use the predis connector (^1.1.9).",
"psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^5.0).",
"symfony/filesystem": "Required to create relative storage directory symbolic links (^5.0).",
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0|^5.0|^6.0|^7.0).",
"symfony/cache": "Required to PSR-6 cache bridge (^5.4).",
"symfony/filesystem": "Required to enable support for relative symbolic links (^5.4).",
"symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^2.0).",
"wildbit/swiftmailer-postmark": "Required to use Postmark mail driver (^3.0)."
},
"config": {
"sort-packages": true
"sort-packages": true,
"allow-plugins": {
"composer/package-versions-deprecated": true
}
},
"minimum-stability": "dev",
"prefer-stable": true

View File

@@ -0,0 +1,51 @@
<?php
namespace Illuminate\Auth\Access\Events;
class GateEvaluated
{
/**
* The authenticatable model.
*
* @var \Illuminate\Contracts\Auth\Authenticatable|null
*/
public $user;
/**
* The ability being evaluated.
*
* @var string
*/
public $ability;
/**
* The result of the evaluation.
*
* @var bool|null
*/
public $result;
/**
* The arguments given during evaluation.
*
* @var array
*/
public $arguments;
/**
* Create a new event instance.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param bool|null $result
* @param array $arguments
* @return void
*/
public function __construct($user, $ability, $result, $arguments)
{
$this->user = $user;
$this->ability = $ability;
$this->result = $result;
$this->arguments = $arguments;
}
}

View File

@@ -2,10 +2,13 @@
namespace Illuminate\Auth\Access;
use Closure;
use Exception;
use Illuminate\Contracts\Auth\Access\Gate as GateContract;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use InvalidArgumentException;
use ReflectionClass;
@@ -115,6 +118,64 @@ class Gate implements GateContract
return true;
}
/**
* Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is false.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function allowIf($condition, $message = null, $code = null)
{
return $this->authorizeOnDemand($condition, $message, $code, true);
}
/**
* Perform an on-demand authorization check. Throw an authorization exception if the condition or callback is true.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function denyIf($condition, $message = null, $code = null)
{
return $this->authorizeOnDemand($condition, $message, $code, false);
}
/**
* Authorize a given condition or callback.
*
* @param \Illuminate\Auth\Access\Response|\Closure|bool $condition
* @param string|null $message
* @param string|null $code
* @param bool $allowWhenResponseIs
* @return \Illuminate\Auth\Access\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
protected function authorizeOnDemand($condition, $message, $code, $allowWhenResponseIs)
{
$user = $this->resolveUser();
if ($condition instanceof Closure) {
$response = $this->canBeCalledWithUser($user, $condition)
? $condition($user)
: new Response(false, $message, $code);
} else {
$response = $condition;
}
return with($response instanceof Response ? $response : new Response(
(bool) $response === $allowWhenResponseIs, $message, $code
))->authorize();
}
/**
* Define a new ability.
*
@@ -155,10 +216,10 @@ class Gate implements GateContract
{
$abilities = $abilities ?: [
'viewAny' => 'viewAny',
'view' => 'view',
'create' => 'create',
'update' => 'update',
'delete' => 'delete',
'view' => 'view',
'create' => 'create',
'update' => 'update',
'delete' => 'delete',
];
foreach ($abilities as $ability => $method) {
@@ -373,9 +434,11 @@ class Gate implements GateContract
// After calling the authorization callback, we will call the "after" callbacks
// that are registered with the Gate, which allows a developer to do logging
// if that is required for this application. Then we'll return the result.
return $this->callAfterCallbacks(
return tap($this->callAfterCallbacks(
$user, $ability, $arguments, $result
);
), function ($result) use ($user, $ability, $arguments) {
$this->dispatchGateEvaluatedEvent($user, $ability, $arguments, $result);
});
}
/**
@@ -518,6 +581,24 @@ class Gate implements GateContract
return $result;
}
/**
* Dispatch a gate evaluation event.
*
* @param \Illuminate\Contracts\Auth\Authenticatable|null $user
* @param string $ability
* @param array $arguments
* @param bool|null $result
* @return void
*/
protected function dispatchGateEvaluatedEvent($user, $ability, array $arguments, $result)
{
if ($this->container->bound(Dispatcher::class)) {
$this->container->make(Dispatcher::class)->dispatch(
new Events\GateEvaluated($user, $ability, $result, $arguments)
);
}
}
/**
* Resolve the callable for the given ability and arguments.
*
@@ -599,7 +680,15 @@ class Gate implements GateContract
$classDirname = str_replace('/', '\\', dirname(str_replace('\\', '/', $class)));
return [$classDirname.'\\Policies\\'.class_basename($class).'Policy'];
$classDirnameSegments = explode('\\', $classDirname);
return Arr::wrap(Collection::times(count($classDirnameSegments), function ($index) use ($class, $classDirnameSegments) {
$classDirname = implode('\\', array_slice($classDirnameSegments, 0, $index));
return $classDirname.'\\Policies\\'.class_basename($class).'Policy';
})->reverse()->values()->first(function ($class) {
return class_exists($class);
}) ?: [$classDirname.'\\Policies\\'.class_basename($class).'Policy']);
}
/**
@@ -770,4 +859,17 @@ class Gate implements GateContract
{
return $this->policies;
}
/**
* Set the container instance used by the gate.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return $this
*/
public function setContainer(Container $container)
{
$this->container = $container;
return $this;
}
}

View File

@@ -122,7 +122,11 @@ class AuthManager implements FactoryContract
{
$provider = $this->createUserProvider($config['provider'] ?? null);
$guard = new SessionGuard($name, $provider, $this->app['session.store']);
$guard = new SessionGuard(
$name,
$provider,
$this->app['session.store'],
);
// When using the remember me functionality of the authentication services we
// will need to be set the encryption instance of the guard, which allows
@@ -139,6 +143,10 @@ class AuthManager implements FactoryContract
$guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
}
if (isset($config['remember'])) {
$guard->setRememberDuration($config['remember']);
}
return $guard;
}
@@ -295,6 +303,31 @@ class AuthManager implements FactoryContract
return count($this->guards) > 0;
}
/**
* Forget all of the resolved guard instances.
*
* @return $this
*/
public function forgetGuards()
{
$this->guards = [];
return $this;
}
/**
* Set the application instance used by the manager.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return $this
*/
public function setApplication($app)
{
$this->app = $app;
return $this;
}
/**
* Dynamically call the default driver instance.
*

View File

@@ -35,11 +35,6 @@ class AuthServiceProvider extends ServiceProvider
protected function registerAuthenticator()
{
$this->app->singleton('auth', function ($app) {
// Once the authentication service has actually been requested by the developer
// we will set a variable in the application indicating such. This helps us
// know that we need to set any queued cookies in the after event later.
$app['auth.loaded'] = true;
return new AuthManager($app);
});
@@ -55,11 +50,9 @@ 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, function ($app) {
return call_user_func($app['auth']->userResolver());
});
}
/**
@@ -83,15 +76,13 @@ class AuthServiceProvider extends ServiceProvider
*/
protected function registerRequirePassword()
{
$this->app->bind(
RequirePassword::class, function ($app) {
return new RequirePassword(
$app[ResponseFactory::class],
$app[UrlGenerator::class],
$app['config']->get('auth.password_timeout')
);
}
);
$this->app->bind(RequirePassword::class, function ($app) {
return new RequirePassword(
$app[ResponseFactory::class],
$app[UrlGenerator::class],
$app['config']->get('auth.password_timeout')
);
});
}
/**
@@ -116,11 +107,8 @@ class AuthServiceProvider extends ServiceProvider
protected function registerEventRebindHandler()
{
$this->app->rebinding('events', function ($app, $dispatcher) {
if (! $app->resolved('auth')) {
return;
}
if ($app['auth']->hasResolvedGuards() === false) {
if (! $app->resolved('auth') ||
$app['auth']->hasResolvedGuards() === false) {
return;
}

View File

@@ -31,6 +31,16 @@ trait Authenticatable
return $this->{$this->getAuthIdentifierName()};
}
/**
* Get the unique broadcast identifier for the user.
*
* @return mixed
*/
public function getAuthIdentifierForBroadcasting()
{
return $this->getAuthIdentifier();
}
/**
* Get the password for the user.
*

View File

@@ -16,7 +16,7 @@ class AuthenticationException extends Exception
/**
* The path the user should be redirected to.
*
* @var string
* @var string|null
*/
protected $redirectTo;
@@ -49,7 +49,7 @@ class AuthenticationException extends Exception
/**
* Get the path the user should be redirected to.
*
* @return string
* @return string|null
*/
public function redirectTo()
{

View File

@@ -13,8 +13,8 @@
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<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">

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Auth;
use Closure;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
@@ -117,6 +118,8 @@ class DatabaseUserProvider implements UserProvider
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
$value($query);
} else {
$query->where($key, $value);
}

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Auth;
use Closure;
use Illuminate\Contracts\Auth\Authenticatable as UserContract;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Hashing\Hasher as HasherContract;
@@ -123,6 +124,8 @@ class EloquentUserProvider implements UserProvider
if (is_array($value) || $value instanceof Arrayable) {
$query->whereIn($key, $value);
} elseif ($value instanceof Closure) {
$value($query);
} else {
$query->where($key, $value);
}

View File

@@ -25,7 +25,7 @@ trait GuardHelpers
protected $provider;
/**
* Determine if current user is authenticated. If not, throw an exception.
* Determine if the current user is authenticated. If not, throw an exception.
*
* @return \Illuminate\Contracts\Auth\Authenticatable
*

View File

@@ -5,6 +5,7 @@ namespace Illuminate\Auth\Middleware;
use Closure;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\URL;
class EnsureEmailIsVerified
{
@@ -14,7 +15,7 @@ class EnsureEmailIsVerified
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @param string|null $redirectToRoute
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse|null
*/
public function handle($request, Closure $next, $redirectToRoute = null)
{
@@ -23,7 +24,7 @@ class EnsureEmailIsVerified
! $request->user()->hasVerifiedEmail())) {
return $request->expectsJson()
? abort(403, 'Your email address is not verified.')
: Redirect::route($redirectToRoute ?: 'verification.notice');
: Redirect::guest(URL::route($redirectToRoute ?: 'verification.notice'));
}
return $next($request);

View File

@@ -63,15 +63,17 @@ class ResetPassword extends Notification
return call_user_func(static::$toMailCallback, $notifiable, $this->token);
}
if (static::$createUrlCallback) {
$url = call_user_func(static::$createUrlCallback, $notifiable, $this->token);
} else {
$url = url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
return $this->buildMailMessage($this->resetUrl($notifiable));
}
/**
* Get the reset password notification mail message for the given URL.
*
* @param string $url
* @return \Illuminate\Notifications\Messages\MailMessage
*/
protected function buildMailMessage($url)
{
return (new MailMessage)
->subject(Lang::get('Reset Password Notification'))
->line(Lang::get('You are receiving this email because we received a password reset request for your account.'))
@@ -80,6 +82,24 @@ class ResetPassword extends Notification
->line(Lang::get('If you did not request a password reset, no further action is required.'));
}
/**
* Get the reset URL for the given notifiable.
*
* @param mixed $notifiable
* @return string
*/
protected function resetUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable, $this->token);
}
return url(route('password.reset', [
'token' => $this->token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
}
/**
* Set a callback that should be used when creating the reset password button URL.
*

View File

@@ -11,6 +11,13 @@ use Illuminate\Support\Facades\URL;
class VerifyEmail extends Notification
{
/**
* The callback that should be used to create the verify email URL.
*
* @var \Closure|null
*/
public static $createUrlCallback;
/**
* The callback that should be used to build the mail message.
*
@@ -43,10 +50,21 @@ class VerifyEmail extends Notification
return call_user_func(static::$toMailCallback, $notifiable, $verificationUrl);
}
return $this->buildMailMessage($verificationUrl);
}
/**
* Get the verify email notification mail message for the given URL.
*
* @param string $url
* @return \Illuminate\Notifications\Messages\MailMessage
*/
protected function buildMailMessage($url)
{
return (new MailMessage)
->subject(Lang::get('Verify Email Address'))
->line(Lang::get('Please click the button below to verify your email address.'))
->action(Lang::get('Verify Email Address'), $verificationUrl)
->action(Lang::get('Verify Email Address'), $url)
->line(Lang::get('If you did not create an account, no further action is required.'));
}
@@ -58,6 +76,10 @@ class VerifyEmail extends Notification
*/
protected function verificationUrl($notifiable)
{
if (static::$createUrlCallback) {
return call_user_func(static::$createUrlCallback, $notifiable);
}
return URL::temporarySignedRoute(
'verification.verify',
Carbon::now()->addMinutes(Config::get('auth.verification.expire', 60)),
@@ -68,6 +90,17 @@ class VerifyEmail extends Notification
);
}
/**
* Set a callback that should be used when creating the email verification URL.
*
* @param \Closure $callback
* @return void
*/
public static function createUrlUsing($callback)
{
static::$createUrlCallback = $callback;
}
/**
* Set a callback that should be used when building the notification mail message.
*

View File

@@ -42,9 +42,10 @@ class PasswordBroker implements PasswordBrokerContract
* Send a password reset link to a user.
*
* @param array $credentials
* @param \Closure|null $callback
* @return string
*/
public function sendResetLink(array $credentials)
public function sendResetLink(array $credentials, Closure $callback = null)
{
// First we will check to see if we found a user at the given credentials and
// if we did not we will redirect back to this current URI with a piece of
@@ -59,12 +60,16 @@ class PasswordBroker implements PasswordBrokerContract
return static::RESET_THROTTLED;
}
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification(
$this->tokens->create($user)
);
$token = $this->tokens->create($user);
if ($callback) {
$callback($user, $token);
} else {
// Once we have the reset token, we are ready to send the message out to this
// user with a link to reset their password. We will then redirect back to
// the current URI having nothing set in the session to indicate errors.
$user->sendPasswordResetNotification($token);
}
return static::RESET_LINK_SENT;
}

View File

@@ -17,9 +17,12 @@ use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Cookie\QueueingFactory as CookieJar;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Contracts\Session\Session;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Illuminate\Support\Timebox;
use Illuminate\Support\Traits\Macroable;
use InvalidArgumentException;
use RuntimeException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
@@ -29,7 +32,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
use GuardHelpers, Macroable;
/**
* The name of the Guard. Typically "session".
* The name of the guard. Typically "web".
*
* Corresponds to guard name in authentication configuration.
*
@@ -51,6 +54,13 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected $viaRemember = false;
/**
* The number of minutes that the "remember me" cookie should be valid for.
*
* @var int
*/
protected $rememberDuration = 2628000;
/**
* The session used by the guard.
*
@@ -79,6 +89,13 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected $events;
/**
* The timebox instance.
*
* @var \Illuminate\Support\Timebox
*/
protected $timebox;
/**
* Indicates if the logout method has been called.
*
@@ -100,17 +117,20 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
* @param \Illuminate\Contracts\Auth\UserProvider $provider
* @param \Illuminate\Contracts\Session\Session $session
* @param \Symfony\Component\HttpFoundation\Request|null $request
* @param \Illuminate\Support\Timebox|null $timebox
* @return void
*/
public function __construct($name,
UserProvider $provider,
Session $session,
Request $request = null)
Request $request = null,
Timebox $timebox = null)
{
$this->name = $name;
$this->session = $session;
$this->request = $request;
$this->provider = $provider;
$this->timebox = $timebox ?: new Timebox;
}
/**
@@ -320,7 +340,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
}
/**
* Get the credential array for a HTTP Basic request.
* Get the credential array for an HTTP Basic request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @param string $field
@@ -373,6 +393,34 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
return false;
}
/**
* Attempt to authenticate a user with credentials and additional callbacks.
*
* @param array $credentials
* @param array|callable $callbacks
* @param false $remember
* @return bool
*/
public function attemptWhen(array $credentials = [], $callbacks = null, $remember = false)
{
$this->fireAttemptEvent($credentials, $remember);
$this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
// This method does the exact same thing as attempt, but also executes callbacks after
// the user is retrieved and validated. If one of the callbacks returns falsy we do
// not login the user. Instead, we will fail the specific authentication attempt.
if ($this->hasValidCredentials($user, $credentials) && $this->shouldLogin($callbacks, $user)) {
$this->login($user, $remember);
return true;
}
$this->fireFailedEvent($user, $credentials);
return false;
}
/**
* Determine if the user matches the credentials.
*
@@ -382,13 +430,35 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function hasValidCredentials($user, $credentials)
{
$validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
return $this->timebox->call(function ($timebox) use ($user, $credentials) {
$validated = ! is_null($user) && $this->provider->validateCredentials($user, $credentials);
if ($validated) {
$this->fireValidatedEvent($user);
if ($validated) {
$timebox->returnEarly();
$this->fireValidatedEvent($user);
}
return $validated;
}, 200 * 1000);
}
/**
* Determine if the user should login by executing the given callbacks.
*
* @param array|callable|null $callbacks
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @return bool
*/
protected function shouldLogin($callbacks, AuthenticatableContract $user)
{
foreach (Arr::wrap($callbacks) as $callback) {
if (! $callback($user, $this)) {
return false;
}
}
return $validated;
return true;
}
/**
@@ -484,7 +554,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*/
protected function createRecaller($value)
{
return $this->getCookieJar()->forever($this->getRecallerName(), $value);
return $this->getCookieJar()->make($this->getRecallerName(), $value, $this->getRememberDuration());
}
/**
@@ -517,6 +587,34 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
$this->loggedOut = true;
}
/**
* Log the user out of the application on their current device only.
*
* This method does not cycle the "remember" token.
*
* @return void
*/
public function logoutCurrentDevice()
{
$user = $this->user();
$this->clearUserDataFromStorage();
// If we have an event dispatcher instance, we can fire off the logout event
// so any further processing can be done. This allows the developer to be
// listening for anytime a user signs out of this application manually.
if (isset($this->events)) {
$this->events->dispatch(new CurrentDeviceLogout($this->name, $user));
}
// Once we have fired the logout event we will clear the users out of memory
// so they are no longer available as the user is no longer considered as
// being signed into this application and should not be available here.
$this->user = null;
$this->loggedOut = true;
}
/**
* Remove the user data from the session and cookies.
*
@@ -545,32 +643,6 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
$this->provider->updateRememberToken($user, $token);
}
/**
* Log the user out of the application on their current device only.
*
* @return void
*/
public function logoutCurrentDevice()
{
$user = $this->user();
$this->clearUserDataFromStorage();
// If we have an event dispatcher instance, we can fire off the logout event
// so any further processing can be done. This allows the developer to be
// listening for anytime a user signs out of this application manually.
if (isset($this->events)) {
$this->events->dispatch(new CurrentDeviceLogout($this->name, $user));
}
// Once we have fired the logout event we will clear the users out of memory
// so they are no longer available as the user is no longer considered as
// being signed into this application and should not be available here.
$this->user = null;
$this->loggedOut = true;
}
/**
* Invalidate other sessions for the current user.
*
@@ -578,7 +650,9 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
*
* @param string $password
* @param string $attribute
* @return bool|null
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*
* @throws \Illuminate\Auth\AuthenticationException
*/
public function logoutOtherDevices($password, $attribute = 'password')
{
@@ -586,9 +660,7 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
return;
}
$result = tap($this->user()->forceFill([
$attribute => Hash::make($password),
]))->save();
$result = $this->rehashUserPassword($password, $attribute);
if ($this->recaller() ||
$this->getCookieJar()->hasQueued($this->getRecallerName())) {
@@ -600,6 +672,26 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
return $result;
}
/**
* Rehash the current user's password.
*
* @param string $password
* @param string $attribute
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*
* @throws \InvalidArgumentException
*/
protected function rehashUserPassword($password, $attribute)
{
if (! Hash::check($password, $this->user()->{$attribute})) {
throw new InvalidArgumentException('The given password does not match the current password.');
}
return tap($this->user()->forceFill([
$attribute => Hash::make($password),
]))->save();
}
/**
* Register an authentication attempt event listener.
*
@@ -746,6 +838,29 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
return $this->viaRemember;
}
/**
* Get the number of minutes the remember me cookie should be valid for.
*
* @return int
*/
protected function getRememberDuration()
{
return $this->rememberDuration;
}
/**
* Set the number of minutes the remember me cookie should be valid for.
*
* @param int $minutes
* @return $this
*/
public function setRememberDuration($minutes)
{
$this->rememberDuration = $minutes;
return $this;
}
/**
* Get the cookie creator instance used by the guard.
*
@@ -853,4 +968,14 @@ class SessionGuard implements StatefulGuard, SupportsBasicAuth
return $this;
}
/**
* Get the timebox instance used by the guard.
*
* @return \Illuminate\Support\Timebox
*/
public function getTimebox()
{
return $this->timebox;
}
}

View File

@@ -14,11 +14,13 @@
}
],
"require": {
"php": "^7.2.5|^8.0",
"illuminate/contracts": "^7.0",
"illuminate/http": "^7.0",
"illuminate/queue": "^7.0",
"illuminate/support": "^7.0"
"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"
},
"autoload": {
"psr-4": {
@@ -27,13 +29,13 @@
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
"dev-master": "8.x-dev"
}
},
"suggest": {
"illuminate/console": "Required to use the auth:clear-resets command (^7.0).",
"illuminate/queue": "Required to fire login / logout events (^7.0).",
"illuminate/session": "Required to use the session based guard (^7.0)."
"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)."
},
"config": {
"sort-packages": true

View File

@@ -3,7 +3,7 @@
namespace Illuminate\Broadcasting;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Broadcasting\Broadcaster;
use Illuminate\Contracts\Broadcasting\Factory as BroadcastingFactory;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Arr;
@@ -46,23 +46,37 @@ 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->afterCommit = property_exists($event, 'afterCommit') ? $event->afterCommit : null;
}
/**
* Handle the queued job.
*
* @param \Illuminate\Contracts\Broadcasting\Broadcaster $broadcaster
* @param \Illuminate\Contracts\Broadcasting\Factory $manager
* @return void
*/
public function handle(Broadcaster $broadcaster)
public function handle(BroadcastingFactory $manager)
{
$name = method_exists($this->event, 'broadcastAs')
? $this->event->broadcastAs() : get_class($this->event);
$broadcaster->broadcast(
Arr::wrap($this->event->broadcastOn()), $name,
$this->getPayloadFromEvent($this->event)
);
$channels = Arr::wrap($this->event->broadcastOn());
if (empty($channels)) {
return;
}
$connections = method_exists($this->event, 'broadcastConnections')
? $this->event->broadcastConnections()
: [null];
$payload = $this->getPayloadFromEvent($this->event);
foreach ($connections as $connection) {
$manager->connection($connection)->broadcast(
$channels, $name, $payload
);
}
}
/**
@@ -73,10 +87,9 @@ class BroadcastEvent implements ShouldQueue
*/
protected function getPayloadFromEvent($event)
{
if (method_exists($event, 'broadcastWith')) {
return array_merge(
$event->broadcastWith(), ['socket' => data_get($event, 'socket')]
);
if (method_exists($event, 'broadcastWith') &&
! is_null($payload = $event->broadcastWith())) {
return array_merge($payload, ['socket' => data_get($event, 'socket')]);
}
$payload = [];

View File

@@ -2,7 +2,9 @@
namespace Illuminate\Broadcasting;
use Ably\AblyRest;
use Closure;
use Illuminate\Broadcasting\Broadcasters\AblyBroadcaster;
use Illuminate\Broadcasting\Broadcasters\LogBroadcaster;
use Illuminate\Broadcasting\Broadcasters\NullBroadcaster;
use Illuminate\Broadcasting\Broadcasters\PusherBroadcaster;
@@ -70,7 +72,7 @@ class BroadcastManager implements FactoryContract
$router->match(
['get', 'post'], '/broadcasting/auth',
'\\'.BroadcastController::class.'@authenticate'
);
)->withoutMiddleware([\Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class]);
});
}
@@ -110,7 +112,10 @@ class BroadcastManager implements FactoryContract
*/
public function queue($event)
{
if ($event instanceof ShouldBroadcastNow) {
if ($event instanceof ShouldBroadcastNow ||
(is_object($event) &&
method_exists($event, 'shouldBroadcastNow') &&
$event->shouldBroadcastNow())) {
return $this->app->make(BusDispatcherContract::class)->dispatchNow(new BroadcastEvent(clone $event));
}
@@ -220,6 +225,17 @@ class BroadcastManager implements FactoryContract
return new PusherBroadcaster($pusher);
}
/**
* Create an instance of the driver.
*
* @param array $config
* @return \Illuminate\Contracts\Broadcasting\Broadcaster
*/
protected function createAblyDriver(array $config)
{
return new AblyBroadcaster(new AblyRest($config));
}
/**
* Create an instance of the driver.
*
@@ -294,6 +310,19 @@ class BroadcastManager implements FactoryContract
$this->app['config']['broadcasting.default'] = $name;
}
/**
* Disconnect the given disk and remove from local cache.
*
* @param string|null $name
* @return void
*/
public function purge($name = null)
{
$name = $name ?? $this->getDefaultDriver();
unset($this->drivers[$name]);
}
/**
* Register a custom driver creator Closure.
*
@@ -308,6 +337,41 @@ class BroadcastManager implements FactoryContract
return $this;
}
/**
* Get the application instance used by the manager.
*
* @return \Illuminate\Contracts\Foundation\Application
*/
public function getApplication()
{
return $this->app;
}
/**
* Set the application instance used by the manager.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return $this
*/
public function setApplication($app)
{
$this->app = $app;
return $this;
}
/**
* Forget all of the resolved driver instances.
*
* @return $this
*/
public function forgetDrivers()
{
$this->drivers = [];
return $this;
}
/**
* Dynamically call the default driver instance.
*

View File

@@ -0,0 +1,225 @@
<?php
namespace Illuminate\Broadcasting\Broadcasters;
use Ably\AblyRest;
use Ably\Models\Message as AblyMessage;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* @author Matthew Hall (matthall28@gmail.com)
* @author Taylor Otwell (taylor@laravel.com)
*/
class AblyBroadcaster extends Broadcaster
{
/**
* The AblyRest SDK instance.
*
* @var \Ably\AblyRest
*/
protected $ably;
/**
* Create a new broadcaster instance.
*
* @param \Ably\AblyRest $ably
* @return void
*/
public function __construct(AblyRest $ably)
{
$this->ably = $ably;
}
/**
* Authenticate the incoming request for a given channel.
*
* @param \Illuminate\Http\Request $request
* @return mixed
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
*/
public function auth($request)
{
$channelName = $this->normalizeChannelName($request->channel_name);
if (empty($request->channel_name) ||
($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName))) {
throw new AccessDeniedHttpException;
}
return parent::verifyUserCanAccessChannel(
$request, $channelName
);
}
/**
* Return the valid authentication response.
*
* @param \Illuminate\Http\Request $request
* @param mixed $result
* @return mixed
*/
public function validAuthenticationResponse($request, $result)
{
if (Str::startsWith($request->channel_name, 'private')) {
$signature = $this->generateAblySignature(
$request->channel_name, $request->socket_id
);
return ['auth' => $this->getPublicToken().':'.$signature];
}
$channelName = $this->normalizeChannelName($request->channel_name);
$user = $this->retrieveUser($request, $channelName);
$broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting')
? $user->getAuthIdentifierForBroadcasting()
: $user->getAuthIdentifier();
$signature = $this->generateAblySignature(
$request->channel_name,
$request->socket_id,
$userData = array_filter([
'user_id' => (string) $broadcastIdentifier,
'user_info' => $result,
])
);
return [
'auth' => $this->getPublicToken().':'.$signature,
'channel_data' => json_encode($userData),
];
}
/**
* Generate the signature needed for Ably authentication headers.
*
* @param string $channelName
* @param string $socketId
* @param array|null $userData
* @return string
*/
public function generateAblySignature($channelName, $socketId, $userData = null)
{
return hash_hmac(
'sha256',
sprintf('%s:%s%s', $socketId, $channelName, $userData ? ':'.json_encode($userData) : ''),
$this->getPrivateToken(),
);
}
/**
* Broadcast the given event.
*
* @param array $channels
* @param string $event
* @param array $payload
* @return void
*/
public function broadcast(array $channels, $event, array $payload = [])
{
foreach ($this->formatChannels($channels) as $channel) {
$this->ably->channels->get($channel)->publish(
$this->buildAblyMessage($event, $payload)
);
}
}
/**
* Build an Ably message object for broadcasting.
*
* @param string $event
* @param array $payload
* @return \Ably\Models\Message
*/
protected function buildAblyMessage($event, array $payload = [])
{
return tap(new AblyMessage, function ($message) use ($event, $payload) {
$message->name = $event;
$message->data = $payload;
$message->connectionKey = data_get($payload, 'socket');
});
}
/**
* Return true if the channel is protected by authentication.
*
* @param string $channel
* @return bool
*/
public function isGuardedChannel($channel)
{
return Str::startsWith($channel, ['private-', 'presence-']);
}
/**
* Remove prefix from channel name.
*
* @param string $channel
* @return string
*/
public function normalizeChannelName($channel)
{
if ($this->isGuardedChannel($channel)) {
return Str::startsWith($channel, 'private-')
? Str::replaceFirst('private-', '', $channel)
: Str::replaceFirst('presence-', '', $channel);
}
return $channel;
}
/**
* Format the channel array into an array of strings.
*
* @param array $channels
* @return array
*/
protected function formatChannels(array $channels)
{
return array_map(function ($channel) {
$channel = (string) $channel;
if (Str::startsWith($channel, ['private-', 'presence-'])) {
return Str::startsWith($channel, 'private-')
? Str::replaceFirst('private-', 'private:', $channel)
: Str::replaceFirst('presence-', 'presence:', $channel);
}
return 'public:'.$channel;
}, $channels);
}
/**
* Get the public token value from the Ably key.
*
* @return mixed
*/
protected function getPublicToken()
{
return Str::before($this->ably->options->key, ':');
}
/**
* Get the private token value from the Ably key.
*
* @return mixed
*/
protected function getPrivateToken()
{
return Str::after($this->ably->options->key, ':');
}
/**
* Get the underlying Ably SDK instance.
*
* @return \Ably\AblyRest
*/
public function getAbly()
{
return $this->ably;
}
}

View File

@@ -5,6 +5,7 @@ namespace Illuminate\Broadcasting\Broadcasters;
use Exception;
use Illuminate\Container\Container;
use Illuminate\Contracts\Broadcasting\Broadcaster as BroadcasterContract;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
use Illuminate\Contracts\Routing\BindingRegistrar;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Support\Arr;
@@ -40,13 +41,19 @@ abstract class Broadcaster implements BroadcasterContract
/**
* Register a channel authenticator.
*
* @param string $channel
* @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $channel
* @param callable|string $callback
* @param array $options
* @return $this
*/
public function channel($channel, $callback, $options = [])
{
if ($channel instanceof HasBroadcastChannel) {
$channel = $channel->broadcastChannelRoute();
} elseif (is_string($channel) && class_exists($channel) && is_a($channel, HasBroadcastChannel::class, true)) {
$channel = (new $channel)->broadcastChannelRoute();
}
$this->channels[$channel] = $callback;
$this->channelOptions[$channel] = $options;
@@ -317,7 +324,7 @@ abstract class Broadcaster implements BroadcasterContract
}
/**
* Check if channel name from request match a pattern from registered channels.
* Check if the channel name from the request matches a pattern from registered channels.
*
* @param string $channel
* @param string $pattern

View File

@@ -5,6 +5,7 @@ namespace Illuminate\Broadcasting\Broadcasters;
use Illuminate\Broadcasting\BroadcastException;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use Pusher\ApiErrorException;
use Pusher\Pusher;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -42,8 +43,9 @@ class PusherBroadcaster extends Broadcaster
{
$channelName = $this->normalizeChannelName($request->channel_name);
if ($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName)) {
if (empty($request->channel_name) ||
($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName))) {
throw new AccessDeniedHttpException;
}
@@ -69,11 +71,17 @@ class PusherBroadcaster extends Broadcaster
$channelName = $this->normalizeChannelName($request->channel_name);
$user = $this->retrieveUser($request, $channelName);
$broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting')
? $user->getAuthIdentifierForBroadcasting()
: $user->getAuthIdentifier();
return $this->decodePusherResponse(
$request,
$this->pusher->presence_auth(
$request->channel_name, $request->socket_id,
$this->retrieveUser($request, $channelName)->getAuthIdentifier(), $result
$broadcastIdentifier, $result
)
);
}
@@ -109,20 +117,44 @@ class PusherBroadcaster extends Broadcaster
{
$socket = Arr::pull($payload, 'socket');
$response = $this->pusher->trigger(
$this->formatChannels($channels), $event, $payload, $socket, true
);
if ($this->pusherServerIsVersionFiveOrGreater()) {
$parameters = $socket !== null ? ['socket_id' => $socket] : [];
if ((is_array($response) && $response['status'] >= 200 && $response['status'] <= 299)
|| $response === true) {
return;
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;
}
throw new BroadcastException(
! empty($response['body'])
? sprintf('Pusher error: %s.', $response['body'])
: 'Failed to connect to Pusher.'
);
}
}
throw new BroadcastException(
! empty($response['body'])
? sprintf('Pusher error: %s.', $response['body'])
: 'Failed to connect to Pusher.'
);
/**
* Determine if the Pusher PHP server is version 5.0 or greater.
*
* @return bool
*/
protected function pusherServerIsVersionFiveOrGreater()
{
return class_exists(ApiErrorException::class);
}
/**
@@ -134,4 +166,15 @@ class PusherBroadcaster extends Broadcaster
{
return $this->pusher;
}
/**
* Set the Pusher SDK instance.
*
* @param \Pusher\Pusher $pusher
* @return void
*/
public function setPusher($pusher)
{
$this->pusher = $pusher;
}
}

View File

@@ -20,16 +20,16 @@ class RedisBroadcaster extends Broadcaster
/**
* The Redis connection to use for broadcasting.
*
* @var string
* @var ?string
*/
protected $connection;
protected $connection = null;
/**
* The Redis key prefix.
*
* @var string
*/
protected $prefix;
protected $prefix = '';
/**
* Create a new broadcaster instance.
@@ -60,8 +60,9 @@ class RedisBroadcaster extends Broadcaster
str_replace($this->prefix, '', $request->channel_name)
);
if ($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName)) {
if (empty($request->channel_name) ||
($this->isGuardedChannel($request->channel_name) &&
! $this->retrieveUser($request, $channelName))) {
throw new AccessDeniedHttpException;
}
@@ -85,8 +86,14 @@ class RedisBroadcaster extends Broadcaster
$channelName = $this->normalizeChannelName($request->channel_name);
$user = $this->retrieveUser($request, $channelName);
$broadcastIdentifier = method_exists($user, 'getAuthIdentifierForBroadcasting')
? $user->getAuthIdentifierForBroadcasting()
: $user->getAuthIdentifier();
return json_encode(['channel_data' => [
'user_id' => $this->retrieveUser($request, $channelName)->getAuthIdentifier(),
'user_id' => $broadcastIdentifier,
'user_info' => $result,
]]);
}

View File

@@ -7,7 +7,7 @@ use Illuminate\Support\Str;
trait UsePusherChannelConventions
{
/**
* Return true if channel is protected by authentication.
* Return true if the channel is protected by authentication.
*
* @param string $channel
* @return bool

View File

@@ -2,6 +2,8 @@
namespace Illuminate\Broadcasting;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
class Channel
{
/**
@@ -14,12 +16,12 @@ class Channel
/**
* Create a new channel instance.
*
* @param string $name
* @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $name
* @return void
*/
public function __construct($name)
{
$this->name = $name;
$this->name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name;
}
/**

View File

@@ -0,0 +1,40 @@
<?php
namespace Illuminate\Broadcasting;
use Illuminate\Support\Arr;
trait InteractsWithBroadcasting
{
/**
* The broadcaster connection to use to broadcast the event.
*
* @var array
*/
protected $broadcastConnection = [null];
/**
* Broadcast the event using a specific broadcaster.
*
* @param array|string|null $connection
* @return $this
*/
public function broadcastVia($connection = null)
{
$this->broadcastConnection = is_null($connection)
? [null]
: Arr::wrap($connection);
return $this;
}
/**
* Get the broadcaster connections the event should be broadcast on.
*
* @return array
*/
public function broadcastConnections()
{
return $this->broadcastConnection;
}
}

View File

@@ -33,6 +33,21 @@ class PendingBroadcast
$this->events = $events;
}
/**
* Broadcast the event using a specific broadcaster.
*
* @param string|null $connection
* @return $this
*/
public function via($connection = null)
{
if (method_exists($this->event, 'broadcastVia')) {
$this->event->broadcastVia($connection);
}
return $this;
}
/**
* Broadcast the event to everyone except the current user.
*

View File

@@ -2,16 +2,20 @@
namespace Illuminate\Broadcasting;
use Illuminate\Contracts\Broadcasting\HasBroadcastChannel;
class PrivateChannel extends Channel
{
/**
* Create a new channel instance.
*
* @param string $name
* @param \Illuminate\Contracts\Broadcasting\HasBroadcastChannel|string $name
* @return void
*/
public function __construct($name)
{
$name = $name instanceof HasBroadcastChannel ? $name->broadcastChannel() : $name;
parent::__construct('private-'.$name);
}
}

View File

@@ -14,13 +14,14 @@
}
],
"require": {
"php": "^7.2.5|^8.0",
"php": "^7.3|^8.0",
"ext-json": "*",
"psr/log": "^1.0",
"illuminate/bus": "^7.0",
"illuminate/contracts": "^7.0",
"illuminate/queue": "^7.0",
"illuminate/support": "^7.0"
"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"
},
"autoload": {
"psr-4": {
@@ -29,11 +30,12 @@
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
"dev-master": "8.x-dev"
}
},
"suggest": {
"pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^4.0)."
"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)."
},
"config": {
"sort-packages": true

View File

@@ -0,0 +1,481 @@
<?php
namespace Illuminate\Bus;
use Carbon\CarbonImmutable;
use Closure;
use Illuminate\Contracts\Queue\Factory as QueueFactory;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use JsonSerializable;
use Throwable;
class Batch implements Arrayable, JsonSerializable
{
/**
* The queue factory implementation.
*
* @var \Illuminate\Contracts\Queue\Factory
*/
protected $queue;
/**
* The repository implementation.
*
* @var \Illuminate\Bus\BatchRepository
*/
protected $repository;
/**
* The batch ID.
*
* @var string
*/
public $id;
/**
* The batch name.
*
* @var string
*/
public $name;
/**
* The total number of jobs that belong to the batch.
*
* @var int
*/
public $totalJobs;
/**
* The total number of jobs that are still pending.
*
* @var int
*/
public $pendingJobs;
/**
* The total number of jobs that have failed.
*
* @var int
*/
public $failedJobs;
/**
* The IDs of the jobs that have failed.
*
* @var array
*/
public $failedJobIds;
/**
* The batch options.
*
* @var array
*/
public $options;
/**
* The date indicating when the batch was created.
*
* @var \Carbon\CarbonImmutable
*/
public $createdAt;
/**
* The date indicating when the batch was cancelled.
*
* @var \Carbon\CarbonImmutable|null
*/
public $cancelledAt;
/**
* The date indicating when the batch was finished.
*
* @var \Carbon\CarbonImmutable|null
*/
public $finishedAt;
/**
* Create a new batch instance.
*
* @param \Illuminate\Contracts\Queue\Factory $queue
* @param \Illuminate\Bus\BatchRepository $repository
* @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 void
*/
public function __construct(QueueFactory $queue,
BatchRepository $repository,
string $id,
string $name,
int $totalJobs,
int $pendingJobs,
int $failedJobs,
array $failedJobIds,
array $options,
CarbonImmutable $createdAt,
?CarbonImmutable $cancelledAt = null,
?CarbonImmutable $finishedAt = null)
{
$this->queue = $queue;
$this->repository = $repository;
$this->id = $id;
$this->name = $name;
$this->totalJobs = $totalJobs;
$this->pendingJobs = $pendingJobs;
$this->failedJobs = $failedJobs;
$this->failedJobIds = $failedJobIds;
$this->options = $options;
$this->createdAt = $createdAt;
$this->cancelledAt = $cancelledAt;
$this->finishedAt = $finishedAt;
}
/**
* Get a fresh instance of the batch represented by this ID.
*
* @return self
*/
public function fresh()
{
return $this->repository->find($this->id);
}
/**
* Add additional jobs to the batch.
*
* @param \Illuminate\Support\Enumerable|array $jobs
* @return self
*/
public function add($jobs)
{
$count = 0;
$jobs = Collection::wrap($jobs)->map(function ($job) use (&$count) {
$job = $job instanceof Closure ? CallQueuedClosure::create($job) : $job;
if (is_array($job)) {
$count += count($job);
return with($this->prepareBatchedChain($job), function ($chain) {
return $chain->first()
->allOnQueue($this->options['queue'] ?? null)
->allOnConnection($this->options['connection'] ?? null)
->chain($chain->slice(1)->values()->all());
});
} else {
$job->withBatchId($this->id);
$count++;
}
return $job;
});
$this->repository->transaction(function () use ($jobs, $count) {
$this->repository->incrementTotalJobs($this->id, $count);
$this->queue->connection($this->options['connection'] ?? null)->bulk(
$jobs->all(),
$data = '',
$this->options['queue'] ?? null
);
});
return $this->fresh();
}
/**
* Prepare a chain that exists within the jobs being added.
*
* @param array $chain
* @return \Illuminate\Support\Collection
*/
protected function prepareBatchedChain(array $chain)
{
return collect($chain)->map(function ($job) {
$job = $job instanceof Closure ? CallQueuedClosure::create($job) : $job;
return $job->withBatchId($this->id);
});
}
/**
* Get the total number of jobs that have been processed by the batch thus far.
*
* @return int
*/
public function processedJobs()
{
return $this->totalJobs - $this->pendingJobs;
}
/**
* Get the percentage of jobs that have been processed (between 0-100).
*
* @return int
*/
public function progress()
{
return $this->totalJobs > 0 ? round(($this->processedJobs() / $this->totalJobs) * 100) : 0;
}
/**
* Record that a job within the batch finished successfully, executing any callbacks if necessary.
*
* @param string $jobId
* @return void
*/
public function recordSuccessfulJob(string $jobId)
{
$counts = $this->decrementPendingJobs($jobId);
if ($counts->pendingJobs === 0) {
$this->repository->markAsFinished($this->id);
}
if ($counts->pendingJobs === 0 && $this->hasThenCallbacks()) {
$batch = $this->fresh();
collect($this->options['then'])->each(function ($handler) use ($batch) {
$this->invokeHandlerCallback($handler, $batch);
});
}
if ($counts->allJobsHaveRanExactlyOnce() && $this->hasFinallyCallbacks()) {
$batch = $this->fresh();
collect($this->options['finally'])->each(function ($handler) use ($batch) {
$this->invokeHandlerCallback($handler, $batch);
});
}
}
/**
* Decrement the pending jobs for the batch.
*
* @param string $jobId
* @return \Illuminate\Bus\UpdatedBatchJobCounts
*/
public function decrementPendingJobs(string $jobId)
{
return $this->repository->decrementPendingJobs($this->id, $jobId);
}
/**
* Determine if the batch has finished executing.
*
* @return bool
*/
public function finished()
{
return ! is_null($this->finishedAt);
}
/**
* Determine if the batch has "success" callbacks.
*
* @return bool
*/
public function hasThenCallbacks()
{
return isset($this->options['then']) && ! empty($this->options['then']);
}
/**
* Determine if the batch allows jobs to fail without cancelling the batch.
*
* @return bool
*/
public function allowsFailures()
{
return Arr::get($this->options, 'allowFailures', false) === true;
}
/**
* Determine if the batch has job failures.
*
* @return bool
*/
public function hasFailures()
{
return $this->failedJobs > 0;
}
/**
* Record that a job within the batch failed to finish successfully, executing any callbacks if necessary.
*
* @param string $jobId
* @param \Throwable $e
* @return void
*/
public function recordFailedJob(string $jobId, $e)
{
$counts = $this->incrementFailedJobs($jobId);
if ($counts->failedJobs === 1 && ! $this->allowsFailures()) {
$this->cancel();
}
if ($counts->failedJobs === 1 && $this->hasCatchCallbacks()) {
$batch = $this->fresh();
collect($this->options['catch'])->each(function ($handler) use ($batch, $e) {
$this->invokeHandlerCallback($handler, $batch, $e);
});
}
if ($counts->allJobsHaveRanExactlyOnce() && $this->hasFinallyCallbacks()) {
$batch = $this->fresh();
collect($this->options['finally'])->each(function ($handler) use ($batch, $e) {
$this->invokeHandlerCallback($handler, $batch, $e);
});
}
}
/**
* Increment the failed jobs for the batch.
*
* @param string $jobId
* @return \Illuminate\Bus\UpdatedBatchJobCounts
*/
public function incrementFailedJobs(string $jobId)
{
return $this->repository->incrementFailedJobs($this->id, $jobId);
}
/**
* Determine if the batch has "catch" callbacks.
*
* @return bool
*/
public function hasCatchCallbacks()
{
return isset($this->options['catch']) && ! empty($this->options['catch']);
}
/**
* Determine if the batch has "finally" callbacks.
*
* @return bool
*/
public function hasFinallyCallbacks()
{
return isset($this->options['finally']) && ! empty($this->options['finally']);
}
/**
* Cancel the batch.
*
* @return void
*/
public function cancel()
{
$this->repository->cancel($this->id);
}
/**
* Determine if the batch has been cancelled.
*
* @return bool
*/
public function canceled()
{
return $this->cancelled();
}
/**
* Determine if the batch has been cancelled.
*
* @return bool
*/
public function cancelled()
{
return ! is_null($this->cancelledAt);
}
/**
* Delete the batch from storage.
*
* @return void
*/
public function delete()
{
$this->repository->delete($this->id);
}
/**
* Invoke a batch callback handler.
*
* @param callable $handler
* @param \Illuminate\Bus\Batch $batch
* @param \Throwable|null $e
* @return void
*/
protected function invokeHandlerCallback($handler, Batch $batch, Throwable $e = null)
{
try {
return $handler($batch, $e);
} catch (Throwable $e) {
if (function_exists('report')) {
report($e);
}
}
}
/**
* Convert the batch to an array.
*
* @return array
*/
public function toArray()
{
return [
'id' => $this->id,
'name' => $this->name,
'totalJobs' => $this->totalJobs,
'pendingJobs' => $this->pendingJobs,
'processedJobs' => $this->processedJobs(),
'progress' => $this->progress(),
'failedJobs' => $this->failedJobs,
'options' => $this->options,
'createdAt' => $this->createdAt,
'cancelledAt' => $this->cancelledAt,
'finishedAt' => $this->finishedAt,
];
}
/**
* Get the JSON serializable representation of the object.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return $this->toArray();
}
/**
* Dynamically access the batch's "options" via properties.
*
* @param string $key
* @return mixed
*/
public function __get($key)
{
return $this->options[$key] ?? null;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Illuminate\Bus;
use Carbon\CarbonImmutable;
use Illuminate\Contracts\Queue\Factory as QueueFactory;
class BatchFactory
{
/**
* The queue factory implementation.
*
* @var \Illuminate\Contracts\Queue\Factory
*/
protected $queue;
/**
* Create a new batch factory instance.
*
* @param \Illuminate\Contracts\Queue\Factory $queue
* @return void
*/
public function __construct(QueueFactory $queue)
{
$this->queue = $queue;
}
/**
* Create a new batch instance.
*
* @param \Illuminate\Bus\BatchRepository $repository
* @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 \Illuminate\Bus\Batch
*/
public function make(BatchRepository $repository,
string $id,
string $name,
int $totalJobs,
int $pendingJobs,
int $failedJobs,
array $failedJobIds,
array $options,
CarbonImmutable $createdAt,
?CarbonImmutable $cancelledAt,
?CarbonImmutable $finishedAt)
{
return new Batch($this->queue, $repository, $id, $name, $totalJobs, $pendingJobs, $failedJobs, $failedJobIds, $options, $createdAt, $cancelledAt, $finishedAt);
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Illuminate\Bus;
use Closure;
interface BatchRepository
{
/**
* Retrieve a list of batches.
*
* @param int $limit
* @param mixed $before
* @return \Illuminate\Bus\Batch[]
*/
public function get($limit, $before);
/**
* Retrieve information about an existing batch.
*
* @param string $batchId
* @return \Illuminate\Bus\Batch|null
*/
public function find(string $batchId);
/**
* Store a new pending batch.
*
* @param \Illuminate\Bus\PendingBatch $batch
* @return \Illuminate\Bus\Batch
*/
public function store(PendingBatch $batch);
/**
* Increment the total number of jobs within the batch.
*
* @param string $batchId
* @param int $amount
* @return void
*/
public function incrementTotalJobs(string $batchId, int $amount);
/**
* Decrement the total number of pending jobs for the batch.
*
* @param string $batchId
* @param string $jobId
* @return \Illuminate\Bus\UpdatedBatchJobCounts
*/
public function decrementPendingJobs(string $batchId, string $jobId);
/**
* Increment the total number of failed jobs for the batch.
*
* @param string $batchId
* @param string $jobId
* @return \Illuminate\Bus\UpdatedBatchJobCounts
*/
public function incrementFailedJobs(string $batchId, string $jobId);
/**
* Mark the batch that has the given ID as finished.
*
* @param string $batchId
* @return void
*/
public function markAsFinished(string $batchId);
/**
* Cancel the batch that has the given ID.
*
* @param string $batchId
* @return void
*/
public function cancel(string $batchId);
/**
* Delete the batch that has the given ID.
*
* @param string $batchId
* @return void
*/
public function delete(string $batchId);
/**
* Execute the given Closure within a storage specific transaction.
*
* @param \Closure $callback
* @return mixed
*/
public function transaction(Closure $callback);
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Illuminate\Bus;
use Illuminate\Container\Container;
trait Batchable
{
/**
* The batch ID (if applicable).
*
* @var string
*/
public $batchId;
/**
* Get the batch instance for the job, if applicable.
*
* @return \Illuminate\Bus\Batch|null
*/
public function batch()
{
if ($this->batchId) {
return Container::getInstance()->make(BatchRepository::class)->find($this->batchId);
}
}
/**
* Determine if the batch is still active and processing.
*
* @return bool
*/
public function batching()
{
$batch = $this->batch();
return $batch && ! $batch->cancelled();
}
/**
* Set the batch ID on the job.
*
* @param string $batchId
* @return $this
*/
public function withBatchId(string $batchId)
{
$this->batchId = $batchId;
return $this;
}
}

View File

@@ -23,6 +23,8 @@ class BusServiceProvider extends ServiceProvider implements DeferrableProvider
});
});
$this->registerBatchServices();
$this->app->alias(
Dispatcher::class, DispatcherContract::class
);
@@ -32,6 +34,24 @@ class BusServiceProvider extends ServiceProvider implements DeferrableProvider
);
}
/**
* Register the batch handling services.
*
* @return void
*/
protected function registerBatchServices()
{
$this->app->singleton(BatchRepository::class, DatabaseBatchRepository::class);
$this->app->singleton(DatabaseBatchRepository::class, function ($app) {
return new DatabaseBatchRepository(
$app->make(BatchFactory::class),
$app->make('db')->connection($app->config->get('queue.batching.database')),
$app->config->get('queue.batching.table', 'job_batches')
);
});
}
/**
* Get the services provided by the provider.
*
@@ -43,6 +63,7 @@ class BusServiceProvider extends ServiceProvider implements DeferrableProvider
Dispatcher::class,
DispatcherContract::class,
QueueingDispatcherContract::class,
BatchRepository::class,
];
}
}

View File

@@ -0,0 +1,347 @@
<?php
namespace Illuminate\Bus;
use Carbon\CarbonImmutable;
use Closure;
use DateTimeInterface;
use Illuminate\Database\Connection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\Query\Expression;
use Illuminate\Support\Str;
class DatabaseBatchRepository implements PrunableBatchRepository
{
/**
* The batch factory instance.
*
* @var \Illuminate\Bus\BatchFactory
*/
protected $factory;
/**
* The database connection instance.
*
* @var \Illuminate\Database\Connection
*/
protected $connection;
/**
* The database table to use to store batch information.
*
* @var string
*/
protected $table;
/**
* Create a new batch repository instance.
*
* @param \Illuminate\Bus\BatchFactory $factory
* @param \Illuminate\Database\Connection $connection
* @param string $table
*/
public function __construct(BatchFactory $factory, Connection $connection, string $table)
{
$this->factory = $factory;
$this->connection = $connection;
$this->table = $table;
}
/**
* Retrieve a list of batches.
*
* @param int $limit
* @param mixed $before
* @return \Illuminate\Bus\Batch[]
*/
public function get($limit = 50, $before = null)
{
return $this->connection->table($this->table)
->orderByDesc('id')
->take($limit)
->when($before, function ($q) use ($before) {
return $q->where('id', '<', $before);
})
->get()
->map(function ($batch) {
return $this->toBatch($batch);
})
->all();
}
/**
* Retrieve information about an existing batch.
*
* @param string $batchId
* @return \Illuminate\Bus\Batch|null
*/
public function find(string $batchId)
{
$batch = $this->connection->table($this->table)
->where('id', $batchId)
->first();
if ($batch) {
return $this->toBatch($batch);
}
}
/**
* Store a new pending batch.
*
* @param \Illuminate\Bus\PendingBatch $batch
* @return \Illuminate\Bus\Batch
*/
public function store(PendingBatch $batch)
{
$id = (string) Str::orderedUuid();
$this->connection->table($this->table)->insert([
'id' => $id,
'name' => $batch->name,
'total_jobs' => 0,
'pending_jobs' => 0,
'failed_jobs' => 0,
'failed_job_ids' => '[]',
'options' => $this->serialize($batch->options),
'created_at' => time(),
'cancelled_at' => null,
'finished_at' => null,
]);
return $this->find($id);
}
/**
* Increment the total number of jobs within the batch.
*
* @param string $batchId
* @param int $amount
* @return void
*/
public function incrementTotalJobs(string $batchId, int $amount)
{
$this->connection->table($this->table)->where('id', $batchId)->update([
'total_jobs' => new Expression('total_jobs + '.$amount),
'pending_jobs' => new Expression('pending_jobs + '.$amount),
'finished_at' => null,
]);
}
/**
* Decrement the total number of pending jobs for the batch.
*
* @param string $batchId
* @param string $jobId
* @return \Illuminate\Bus\UpdatedBatchJobCounts
*/
public function decrementPendingJobs(string $batchId, string $jobId)
{
$values = $this->updateAtomicValues($batchId, function ($batch) use ($jobId) {
return [
'pending_jobs' => $batch->pending_jobs - 1,
'failed_jobs' => $batch->failed_jobs,
'failed_job_ids' => json_encode(array_values(array_diff(json_decode($batch->failed_job_ids, true), [$jobId]))),
];
});
return new UpdatedBatchJobCounts(
$values['pending_jobs'],
$values['failed_jobs']
);
}
/**
* Increment the total number of failed jobs for the batch.
*
* @param string $batchId
* @param string $jobId
* @return \Illuminate\Bus\UpdatedBatchJobCounts
*/
public function incrementFailedJobs(string $batchId, string $jobId)
{
$values = $this->updateAtomicValues($batchId, function ($batch) use ($jobId) {
return [
'pending_jobs' => $batch->pending_jobs,
'failed_jobs' => $batch->failed_jobs + 1,
'failed_job_ids' => json_encode(array_values(array_unique(array_merge(json_decode($batch->failed_job_ids, true), [$jobId])))),
];
});
return new UpdatedBatchJobCounts(
$values['pending_jobs'],
$values['failed_jobs']
);
}
/**
* Update an atomic value within the batch.
*
* @param string $batchId
* @param \Closure $callback
* @return int|null
*/
protected function updateAtomicValues(string $batchId, Closure $callback)
{
return $this->connection->transaction(function () use ($batchId, $callback) {
$batch = $this->connection->table($this->table)->where('id', $batchId)
->lockForUpdate()
->first();
return is_null($batch) ? [] : tap($callback($batch), function ($values) use ($batchId) {
$this->connection->table($this->table)->where('id', $batchId)->update($values);
});
});
}
/**
* Mark the batch that has the given ID as finished.
*
* @param string $batchId
* @return void
*/
public function markAsFinished(string $batchId)
{
$this->connection->table($this->table)->where('id', $batchId)->update([
'finished_at' => time(),
]);
}
/**
* Cancel the batch that has the given ID.
*
* @param string $batchId
* @return void
*/
public function cancel(string $batchId)
{
$this->connection->table($this->table)->where('id', $batchId)->update([
'cancelled_at' => time(),
'finished_at' => time(),
]);
}
/**
* Delete the batch that has the given ID.
*
* @param string $batchId
* @return void
*/
public function delete(string $batchId)
{
$this->connection->table($this->table)->where('id', $batchId)->delete();
}
/**
* Prune all of the entries older than the given date.
*
* @param \DateTimeInterface $before
* @return int
*/
public function prune(DateTimeInterface $before)
{
$query = $this->connection->table($this->table)
->whereNotNull('finished_at')
->where('finished_at', '<', $before->getTimestamp());
$totalDeleted = 0;
do {
$deleted = $query->take(1000)->delete();
$totalDeleted += $deleted;
} while ($deleted !== 0);
return $totalDeleted;
}
/**
* Prune all of the unfinished entries older than the given date.
*
* @param \DateTimeInterface $before
* @return int
*/
public function pruneUnfinished(DateTimeInterface $before)
{
$query = $this->connection->table($this->table)
->whereNull('finished_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.
*
* @param \Closure $callback
* @return mixed
*/
public function transaction(Closure $callback)
{
return $this->connection->transaction(function () use ($callback) {
return $callback();
});
}
/**
* Serialize the given value.
*
* @param mixed $value
* @return string
*/
protected function serialize($value)
{
$serialized = serialize($value);
return $this->connection instanceof PostgresConnection
? base64_encode($serialized)
: $serialized;
}
/**
* Unserialize the given value.
*
* @param string $serialized
* @return mixed
*/
protected function unserialize($serialized)
{
if ($this->connection instanceof PostgresConnection &&
! Str::contains($serialized, [':', ';'])) {
$serialized = base64_decode($serialized);
}
return unserialize($serialized);
}
/**
* Convert the given raw batch to a Batch object.
*
* @param object $batch
* @return \Illuminate\Bus\Batch
*/
protected function toBatch($batch)
{
return $this->factory->make(
$this,
$batch->id,
$batch->name,
(int) $batch->total_jobs,
(int) $batch->pending_jobs,
(int) $batch->failed_jobs,
json_decode($batch->failed_job_ids, true),
$this->unserialize($batch->options),
CarbonImmutable::createFromTimestamp($batch->created_at),
$batch->cancelled_at ? CarbonImmutable::createFromTimestamp($batch->cancelled_at) : $batch->cancelled_at,
$batch->finished_at ? CarbonImmutable::createFromTimestamp($batch->finished_at) : $batch->finished_at
);
}
}

View File

@@ -7,7 +7,11 @@ use Illuminate\Contracts\Bus\QueueingDispatcher;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Queue\Queue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\PendingChain;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Jobs\SyncJob;
use Illuminate\Support\Collection;
use RuntimeException;
class Dispatcher implements QueueingDispatcher
@@ -69,35 +73,100 @@ class Dispatcher implements QueueingDispatcher
*/
public function dispatch($command)
{
if ($this->queueResolver && $this->commandShouldBeQueued($command)) {
return $this->dispatchToQueue($command);
}
return $this->dispatchNow($command);
return $this->queueResolver && $this->commandShouldBeQueued($command)
? $this->dispatchToQueue($command)
: $this->dispatchNow($command);
}
/**
* Dispatch a command to its appropriate handler in the current process.
*
* Queueable jobs will be dispatched to the "sync" queue.
*
* @param mixed $command
* @param mixed $handler
* @return mixed
*/
public function dispatchSync($command, $handler = null)
{
if ($this->queueResolver &&
$this->commandShouldBeQueued($command) &&
method_exists($command, 'onConnection')) {
return $this->dispatchToQueue($command->onConnection('sync'));
}
return $this->dispatchNow($command, $handler);
}
/**
* Dispatch a command to its appropriate handler in the current process without using the synchronous queue.
*
* @param mixed $command
* @param mixed $handler
* @return mixed
*/
public function dispatchNow($command, $handler = null)
{
$uses = class_uses_recursive($command);
if (in_array(InteractsWithQueue::class, $uses) &&
in_array(Queueable::class, $uses) &&
! $command->job) {
$command->setJob(new SyncJob($this->container, json_encode([]), 'sync', 'sync'));
}
if ($handler || $handler = $this->getCommandHandler($command)) {
$callback = function ($command) use ($handler) {
return $handler->handle($command);
$method = method_exists($handler, 'handle') ? 'handle' : '__invoke';
return $handler->{$method}($command);
};
} else {
$callback = function ($command) {
return $this->container->call([$command, 'handle']);
$method = method_exists($command, 'handle') ? 'handle' : '__invoke';
return $this->container->call([$command, $method]);
};
}
return $this->pipeline->send($command)->through($this->pipes)->then($callback);
}
/**
* Attempt to find the batch with the given ID.
*
* @param string $batchId
* @return \Illuminate\Bus\Batch|null
*/
public function findBatch(string $batchId)
{
return $this->container->make(BatchRepository::class)->find($batchId);
}
/**
* Create a new batch of queueable jobs.
*
* @param \Illuminate\Support\Collection|array|mixed $jobs
* @return \Illuminate\Bus\PendingBatch
*/
public function batch($jobs)
{
return new PendingBatch($this->container, Collection::wrap($jobs));
}
/**
* Create a new chain of queueable jobs.
*
* @param \Illuminate\Support\Collection|array $jobs
* @return \Illuminate\Foundation\Bus\PendingChain
*/
public function chain($jobs)
{
$jobs = Collection::wrap($jobs);
return new PendingChain($jobs->shift(), $jobs->toArray());
}
/**
* Determine if the given command has a handler.
*
@@ -140,6 +209,8 @@ class Dispatcher implements QueueingDispatcher
*
* @param mixed $command
* @return mixed
*
* @throws \RuntimeException
*/
public function dispatchToQueue($command)
{

View File

@@ -0,0 +1,26 @@
<?php
namespace Illuminate\Bus\Events;
use Illuminate\Bus\Batch;
class BatchDispatched
{
/**
* The batch instance.
*
* @var \Illuminate\Bus\Batch
*/
public $batch;
/**
* Create a new event instance.
*
* @param \Illuminate\Bus\Batch $batch
* @return void
*/
public function __construct(Batch $batch)
{
$this->batch = $batch;
}
}

View File

@@ -0,0 +1,272 @@
<?php
namespace Illuminate\Bus;
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 Throwable;
class PendingBatch
{
/**
* The IoC container instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* The batch name.
*
* @var string
*/
public $name = '';
/**
* The jobs that belong to the batch.
*
* @var \Illuminate\Support\Collection
*/
public $jobs;
/**
* The batch options.
*
* @var array
*/
public $options = [];
/**
* Create a new pending batch instance.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param \Illuminate\Support\Collection $jobs
* @return void
*/
public function __construct(Container $container, Collection $jobs)
{
$this->container = $container;
$this->jobs = $jobs;
}
/**
* Add jobs to the batch.
*
* @param iterable $jobs
* @return $this
*/
public function add($jobs)
{
foreach ($jobs as $job) {
$this->jobs->push($job);
}
return $this;
}
/**
* Add a callback to be executed after all jobs in the batch have executed successfully.
*
* @param callable $callback
* @return $this
*/
public function then($callback)
{
$this->options['then'][] = $callback instanceof Closure
? SerializableClosureFactory::make($callback)
: $callback;
return $this;
}
/**
* Get the "then" callbacks that have been registered with the pending batch.
*
* @return array
*/
public function thenCallbacks()
{
return $this->options['then'] ?? [];
}
/**
* Add a callback to be executed after the first failing job in the batch.
*
* @param callable $callback
* @return $this
*/
public function catch($callback)
{
$this->options['catch'][] = $callback instanceof Closure
? SerializableClosureFactory::make($callback)
: $callback;
return $this;
}
/**
* Get the "catch" callbacks that have been registered with the pending batch.
*
* @return array
*/
public function catchCallbacks()
{
return $this->options['catch'] ?? [];
}
/**
* Add a callback to be executed after the batch has finished executing.
*
* @param callable $callback
* @return $this
*/
public function finally($callback)
{
$this->options['finally'][] = $callback instanceof Closure
? SerializableClosureFactory::make($callback)
: $callback;
return $this;
}
/**
* Get the "finally" callbacks that have been registered with the pending batch.
*
* @return array
*/
public function finallyCallbacks()
{
return $this->options['finally'] ?? [];
}
/**
* Indicate that the batch should not be cancelled when a job within the batch fails.
*
* @param bool $allowFailures
* @return $this
*/
public function allowFailures($allowFailures = true)
{
$this->options['allowFailures'] = $allowFailures;
return $this;
}
/**
* Determine if the pending batch allows jobs to fail without cancelling the batch.
*
* @return bool
*/
public function allowsFailures()
{
return Arr::get($this->options, 'allowFailures', false) === true;
}
/**
* Set the name for the batch.
*
* @param string $name
* @return $this
*/
public function name(string $name)
{
$this->name = $name;
return $this;
}
/**
* Specify the queue connection that the batched jobs should run on.
*
* @param string $connection
* @return $this
*/
public function onConnection(string $connection)
{
$this->options['connection'] = $connection;
return $this;
}
/**
* Get the connection used by the pending batch.
*
* @return string|null
*/
public function connection()
{
return $this->options['connection'] ?? null;
}
/**
* Specify the queue that the batched jobs should run on.
*
* @param string $queue
* @return $this
*/
public function onQueue(string $queue)
{
$this->options['queue'] = $queue;
return $this;
}
/**
* Get the queue used by the pending batch.
*
* @return string|null
*/
public function queue()
{
return $this->options['queue'] ?? null;
}
/**
* Add additional data into the batch's options array.
*
* @param string $key
* @param mixed $value
* @return $this
*/
public function withOption(string $key, $value)
{
$this->options[$key] = $value;
return $this;
}
/**
* Dispatch the batch.
*
* @return \Illuminate\Bus\Batch
*
* @throws \Throwable
*/
public function dispatch()
{
$repository = $this->container->make(BatchRepository::class);
try {
$batch = $repository->store($this);
$batch = $batch->add($this->jobs);
} catch (Throwable $e) {
if (isset($batch)) {
$repository->delete($batch->id);
}
throw $e;
}
$this->container->make(EventDispatcher::class)->dispatch(
new BatchDispatched($batch)
);
return $batch;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Bus;
use DateTimeInterface;
interface PrunableBatchRepository extends BatchRepository
{
/**
* Prune all of the entries older than the given date.
*
* @param \DateTimeInterface $before
* @return int
*/
public function prune(DateTimeInterface $before);
}

View File

@@ -37,6 +37,13 @@ trait Queueable
*/
public $chainQueue;
/**
* The callbacks to be executed on chain failure.
*
* @var array|null
*/
public $chainCatchCallbacks;
/**
* The number of seconds before the job should be made available.
*
@@ -44,6 +51,13 @@ trait Queueable
*/
public $delay;
/**
* Indicates whether the job should be dispatched after all database transactions have committed.
*
* @var bool|null
*/
public $afterCommit;
/**
* The middleware the job should be dispatched through.
*
@@ -125,6 +139,30 @@ trait Queueable
return $this;
}
/**
* Indicate that the job should be dispatched after all database transactions have committed.
*
* @return $this
*/
public function afterCommit()
{
$this->afterCommit = true;
return $this;
}
/**
* Indicate that the job should not wait until database transactions have been committed before dispatching.
*
* @return $this
*/
public function beforeCommit()
{
$this->afterCommit = false;
return $this;
}
/**
* Specify the middleware the job should be dispatched through.
*
@@ -158,6 +196,8 @@ trait Queueable
*
* @param mixed $job
* @return string
*
* @throws \RuntimeException
*/
protected function serializeJob($job)
{
@@ -190,7 +230,21 @@ trait Queueable
$next->chainConnection = $this->chainConnection;
$next->chainQueue = $this->chainQueue;
$next->chainCatchCallbacks = $this->chainCatchCallbacks;
}));
}
}
/**
* Invoke all of the chain's failed job callbacks.
*
* @param \Throwable $e
* @return void
*/
public function invokeChainCatchCallbacks($e)
{
collect($this->chainCatchCallbacks)->each(function ($callback) use ($e) {
$callback($e);
});
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Illuminate\Bus;
use Illuminate\Contracts\Cache\Repository as Cache;
class UniqueLock
{
/**
* The cache repository implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* Create a new unique lock manager instance.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @return void
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Attempt to acquire a lock for the given job.
*
* @param mixed $job
* @return bool
*/
public function acquire($job)
{
$uniqueId = method_exists($job, 'uniqueId')
? $job->uniqueId()
: ($job->uniqueId ?? '');
$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();
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Illuminate\Bus;
class UpdatedBatchJobCounts
{
/**
* The number of pending jobs remaining for the batch.
*
* @var int
*/
public $pendingJobs;
/**
* The number of failed jobs that belong to the batch.
*
* @var int
*/
public $failedJobs;
/**
* Create a new batch job counts object.
*
* @param int $pendingJobs
* @param int $failedJobs
* @return void
*/
public function __construct(int $pendingJobs = 0, int $failedJobs = 0)
{
$this->pendingJobs = $pendingJobs;
$this->failedJobs = $failedJobs;
}
/**
* Determine if all jobs have run exactly once.
*
* @return bool
*/
public function allJobsHaveRanExactlyOnce()
{
return ($this->pendingJobs - $this->failedJobs) === 0;
}
}

View File

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

View File

@@ -0,0 +1,85 @@
<?php
namespace Illuminate\Cache;
class CacheLock extends Lock
{
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Store
*/
protected $store;
/**
* Create a new lock instance.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return void
*/
public function __construct($store, $name, $seconds, $owner = null)
{
parent::__construct($name, $seconds, $owner);
$this->store = $store;
}
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
if (method_exists($this->store, 'add') && $this->seconds > 0) {
return $this->store->add(
$this->name, $this->owner, $this->seconds
);
}
if (! is_null($this->store->get($this->name))) {
return false;
}
return ($this->seconds > 0)
? $this->store->put($this->name, $this->owner, $this->seconds)
: $this->store->forever($this->name, $this->owner, $this->seconds);
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
if ($this->isOwnedByCurrentProcess()) {
return $this->store->forget($this->name);
}
return false;
}
/**
* Releases this lock regardless of ownership.
*
* @return void
*/
public function forceRelease()
{
$this->store->forget($this->name);
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->store->get($this->name);
}
}

View File

@@ -199,7 +199,11 @@ class CacheManager implements FactoryContract
$connection = $config['connection'] ?? 'default';
return $this->repository(new RedisStore($redis, $this->getPrefix($config), $connection));
$store = new RedisStore($redis, $this->getPrefix($config), $connection);
return $this->repository(
$store->setLockConnection($config['lock_connection'] ?? $connection)
);
}
/**
@@ -212,15 +216,17 @@ class CacheManager implements FactoryContract
{
$connection = $this->app['db']->connection($config['connection'] ?? null);
return $this->repository(
new DatabaseStore(
$connection,
$config['table'],
$this->getPrefix($config),
$config['lock_table'] ?? 'cache_locks',
$config['lock_lottery'] ?? [2, 100]
)
$store = new DatabaseStore(
$connection,
$config['table'],
$this->getPrefix($config),
$config['lock_table'] ?? 'cache_locks',
$config['lock_lottery'] ?? [2, 100]
);
return $this->repository($store->setLockConnection(
$this->app['db']->connection($config['lock_connection'] ?? $config['connection'] ?? null)
));
}
/**
@@ -231,21 +237,11 @@ class CacheManager implements FactoryContract
*/
protected function createDynamodbDriver(array $config)
{
$dynamoConfig = [
'region' => $config['region'],
'version' => 'latest',
'endpoint' => $config['endpoint'] ?? null,
];
if ($config['key'] && $config['secret']) {
$dynamoConfig['credentials'] = Arr::only(
$config, ['key', 'secret', 'token']
);
}
$client = $this->newDynamodbClient($config);
return $this->repository(
new DynamoDbStore(
new DynamoDbClient($dynamoConfig),
$client,
$config['table'],
$config['attributes']['key'] ?? 'key',
$config['attributes']['value'] ?? 'value',
@@ -255,6 +251,28 @@ class CacheManager implements FactoryContract
);
}
/**
* Create new DynamoDb Client instance.
*
* @return DynamoDbClient
*/
protected function newDynamodbClient(array $config)
{
$dynamoConfig = [
'region' => $config['region'],
'version' => 'latest',
'endpoint' => $config['endpoint'] ?? null,
];
if (isset($config['key']) && isset($config['secret'])) {
$dynamoConfig['credentials'] = Arr::only(
$config, ['key', 'secret', 'token']
);
}
return new DynamoDbClient($dynamoConfig);
}
/**
* Create a new cache repository with the given implementation.
*
@@ -314,7 +332,11 @@ class CacheManager implements FactoryContract
*/
protected function getConfig($name)
{
return $this->app['config']["cache.stores.{$name}"];
if (! is_null($name) && $name !== 'null') {
return $this->app['config']["cache.stores.{$name}"];
}
return ['driver' => 'null'];
}
/**
@@ -357,6 +379,19 @@ class CacheManager implements FactoryContract
return $this;
}
/**
* Disconnect the given driver and remove from local cache.
*
* @param string|null $name
* @return void
*/
public function purge($name = null)
{
$name = $name ?? $this->getDefaultDriver();
unset($this->stores[$name]);
}
/**
* Register a custom driver creator Closure.
*

View File

@@ -30,6 +30,12 @@ class CacheServiceProvider extends ServiceProvider implements DeferrableProvider
$this->app->singleton('memcached.connector', function () {
return new MemcachedConnector;
});
$this->app->singleton(RateLimiter::class, function ($app) {
return new RateLimiter($app->make('cache')->driver(
$app['config']->get('cache.limiter')
));
});
}
/**
@@ -40,7 +46,7 @@ class CacheServiceProvider extends ServiceProvider implements DeferrableProvider
public function provides()
{
return [
'cache', 'cache.store', 'cache.psr6', 'memcached.connector',
'cache', 'cache.store', 'cache.psr6', 'memcached.connector', RateLimiter::class,
];
}
}

View File

@@ -116,7 +116,7 @@ class ClearCommand extends Command
*/
protected function tags()
{
return array_filter(explode(',', $this->option('tags')));
return array_filter(explode(',', $this->option('tags') ?? ''));
}
/**

View File

@@ -14,10 +14,16 @@ class CreateCacheTable extends Migration
public function up()
{
Schema::create('cache', function (Blueprint $table) {
$table->string('key')->unique();
$table->string('key')->primary();
$table->mediumText('value');
$table->integer('expiration');
});
Schema::create('cache_locks', function (Blueprint $table) {
$table->string('key')->primary();
$table->string('owner');
$table->integer('expiration');
});
}
/**
@@ -28,5 +34,6 @@ class CreateCacheTable extends Migration
public function down()
{
Schema::dropIfExists('cache');
Schema::dropIfExists('cache_locks');
}
}

View File

@@ -136,4 +136,14 @@ class DatabaseLock extends Lock
{
return optional($this->connection->table($this->table)->where('key', $this->name)->first())->owner;
}
/**
* Get the name of the database connection being used to manage the lock.
*
* @return string
*/
public function getConnectionName()
{
return $this->connection->getName();
}
}

View File

@@ -23,6 +23,13 @@ class DatabaseStore implements LockProvider, Store
*/
protected $connection;
/**
* The database connection instance that should be used to manage locks.
*
* @var \Illuminate\Database\ConnectionInterface
*/
protected $lockConnection;
/**
* The name of the cache table.
*
@@ -155,8 +162,6 @@ class DatabaseStore implements LockProvider, Store
'expiration' => $expiration,
]) >= 1;
}
return false;
}
/**
@@ -267,7 +272,7 @@ class DatabaseStore implements LockProvider, Store
public function lock($name, $seconds = 0, $owner = null)
{
return new DatabaseLock(
$this->connection,
$this->lockConnection ?? $this->connection,
$this->lockTable,
$this->prefix.$name,
$seconds,
@@ -333,6 +338,19 @@ class DatabaseStore implements LockProvider, Store
return $this->connection;
}
/**
* Specify the name of the connection that should be used to manage locks.
*
* @param \Illuminate\Database\ConnectionInterface $connection
* @return $this
*/
public function setLockConnection($connection)
{
$this->lockConnection = $connection;
return $this;
}
/**
* Get the cache key prefix.
*

View File

@@ -34,9 +34,11 @@ class DynamoDbLock extends Lock
*/
public function acquire()
{
return $this->dynamo->add(
$this->name, $this->owner, $this->seconds
);
if ($this->seconds > 0) {
return $this->dynamo->add($this->name, $this->owner, $this->seconds);
} else {
return $this->dynamo->add($this->name, $this->owner, 86400);
}
}
/**

View File

@@ -525,4 +525,14 @@ class DynamoDbStore implements LockProvider, Store
{
$this->prefix = ! empty($prefix) ? $prefix.':' : '';
}
/**
* Get the DynamoDb Client instance.
*
* @return DynamoDbClient
*/
public function getClient()
{
return $this->dynamo;
}
}

View File

@@ -3,13 +3,16 @@
namespace Illuminate\Cache;
use Exception;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Cache\Store;
use Illuminate\Contracts\Filesystem\LockTimeoutException;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Filesystem\LockableFile;
use Illuminate\Support\InteractsWithTime;
class FileStore implements Store
class FileStore implements Store, LockProvider
{
use InteractsWithTime, RetrievesMultipleKeys;
use InteractsWithTime, HasCacheLock, RetrievesMultipleKeys;
/**
* The Illuminate Filesystem instance.
@@ -75,7 +78,7 @@ class FileStore implements Store
);
if ($result !== false && $result > 0) {
$this->ensureFileHasCorrectPermissions($path);
$this->ensurePermissionsAreCorrect($path);
return true;
}
@@ -83,6 +86,45 @@ class FileStore implements Store
return false;
}
/**
* Store an item in the cache if the key doesn't exist.
*
* @param string $key
* @param mixed $value
* @param int $seconds
* @return bool
*/
public function add($key, $value, $seconds)
{
$this->ensureCacheDirectoryExists($path = $this->path($key));
$file = new LockableFile($path, 'c+');
try {
$file->getExclusiveLock();
} catch (LockTimeoutException $e) {
$file->close();
return false;
}
$expire = $file->read(10);
if (empty($expire) || $this->currentTime() >= $expire) {
$file->truncate()
->write($this->expiration($seconds).serialize($value))
->close();
$this->ensurePermissionsAreCorrect($path);
return true;
}
$file->close();
return false;
}
/**
* Create the file cache directory if necessary.
*
@@ -91,18 +133,24 @@ class FileStore implements Store
*/
protected function ensureCacheDirectoryExists($path)
{
if (! $this->files->exists(dirname($path))) {
$this->files->makeDirectory(dirname($path), 0777, true, true);
$directory = dirname($path);
if (! $this->files->exists($directory)) {
$this->files->makeDirectory($directory, 0777, true, true);
// We're creating two levels of directories (e.g. 7e/24), so we check them both...
$this->ensurePermissionsAreCorrect($directory);
$this->ensurePermissionsAreCorrect(dirname($directory));
}
}
/**
* Ensure the cache file has the correct permissions.
* Ensure the created node has the correct permissions.
*
* @param string $path
* @return void
*/
protected function ensureFileHasCorrectPermissions($path)
protected function ensurePermissionsAreCorrect($path)
{
if (is_null($this->filePermission) ||
intval($this->files->chmod($path), 8) == $this->filePermission) {

View File

@@ -0,0 +1,31 @@
<?php
namespace Illuminate\Cache;
trait HasCacheLock
{
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new CacheLock($this, $name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
}

View File

@@ -105,7 +105,7 @@ abstract class Lock implements LockContract
*
* @param int $seconds
* @param callable|null $callback
* @return bool
* @return mixed
*
* @throws \Illuminate\Contracts\Cache\LockTimeoutException
*/
@@ -153,7 +153,7 @@ abstract class Lock implements LockContract
}
/**
* Specify the number of milliseconds to sleep in between blocked lock aquisition attempts.
* Specify the number of milliseconds to sleep in between blocked lock acquisition attempts.
*
* @param int $milliseconds
* @return $this

View File

@@ -0,0 +1,46 @@
<?php
namespace Illuminate\Cache;
class NoLock extends Lock
{
/**
* Attempt to acquire the lock.
*
* @return bool
*/
public function acquire()
{
return true;
}
/**
* Release the lock.
*
* @return bool
*/
public function release()
{
return true;
}
/**
* Releases this lock in disregard of ownership.
*
* @return void
*/
public function forceRelease()
{
//
}
/**
* Returns the owner value written into the driver for this lock.
*
* @return mixed
*/
protected function getCurrentOwner()
{
return $this->owner;
}
}

View File

@@ -2,7 +2,9 @@
namespace Illuminate\Cache;
class NullStore extends TaggableStore
use Illuminate\Contracts\Cache\LockProvider;
class NullStore extends TaggableStore implements LockProvider
{
use RetrievesMultipleKeys;
@@ -10,7 +12,7 @@ class NullStore extends TaggableStore
* Retrieve an item from the cache by key.
*
* @param string $key
* @return mixed
* @return void
*/
public function get($key)
{
@@ -35,7 +37,7 @@ class NullStore extends TaggableStore
*
* @param string $key
* @param mixed $value
* @return int|bool
* @return bool
*/
public function increment($key, $value = 1)
{
@@ -47,7 +49,7 @@ class NullStore extends TaggableStore
*
* @param string $key
* @param mixed $value
* @return int|bool
* @return bool
*/
public function decrement($key, $value = 1)
{
@@ -66,6 +68,31 @@ class NullStore extends TaggableStore
return false;
}
/**
* Get a lock instance.
*
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new NoLock($name, $seconds, $owner);
}
/**
* Restore a lock instance using the owner identifier.
*
* @param string $name
* @param string $owner
* @return \Illuminate\Contracts\Cache\Lock
*/
public function restoreLock($name, $owner)
{
return $this->lock($name, 0, $owner);
}
/**
* Remove an item from the cache.
*

View File

@@ -0,0 +1,35 @@
<?php
namespace Illuminate\Cache;
use Illuminate\Redis\Connections\PhpRedisConnection;
class PhpRedisLock extends RedisLock
{
/**
* Create a new phpredis lock instance.
*
* @param \Illuminate\Redis\Connections\PhpRedisConnection $redis
* @param string $name
* @param int $seconds
* @param string|null $owner
* @return void
*/
public function __construct(PhpRedisConnection $redis, string $name, int $seconds, ?string $owner = null)
{
parent::__construct($redis, $name, $seconds, $owner);
}
/**
* {@inheritDoc}
*/
public function release()
{
return (bool) $this->redis->eval(
LuaScripts::releaseLock(),
1,
$this->name,
...$this->redis->pack([$this->owner])
);
}
}

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Cache;
use Closure;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\InteractsWithTime;
@@ -16,6 +17,13 @@ class RateLimiter
*/
protected $cache;
/**
* The configured limit object resolvers.
*
* @var array
*/
protected $limiters = [];
/**
* Create a new rate limiter instance.
*
@@ -27,6 +35,51 @@ class RateLimiter
$this->cache = $cache;
}
/**
* Register a named limiter configuration.
*
* @param string $name
* @param \Closure $callback
* @return $this
*/
public function for(string $name, Closure $callback)
{
$this->limiters[$name] = $callback;
return $this;
}
/**
* Get the given named rate limiter.
*
* @param string $name
* @return \Closure
*/
public function limiter(string $name)
{
return $this->limiters[$name] ?? null;
}
/**
* Attempts to execute a callback if it's not limited.
*
* @param string $key
* @param int $maxAttempts
* @param \Closure $callback
* @param int $decaySeconds
* @return mixed
*/
public function attempt($key, $maxAttempts, Closure $callback, $decaySeconds = 60)
{
if ($this->tooManyAttempts($key, $maxAttempts)) {
return false;
}
return tap($callback() ?: true, function () use ($key, $decaySeconds) {
$this->hit($key, $decaySeconds);
});
}
/**
* Determine if the given key has been "accessed" too many times.
*
@@ -36,6 +89,8 @@ class RateLimiter
*/
public function tooManyAttempts($key, $maxAttempts)
{
$key = $this->cleanRateLimiterKey($key);
if ($this->attempts($key) >= $maxAttempts) {
if ($this->cache->has($key.':timer')) {
return true;
@@ -56,6 +111,8 @@ class RateLimiter
*/
public function hit($key, $decaySeconds = 60)
{
$key = $this->cleanRateLimiterKey($key);
$this->cache->add(
$key.':timer', $this->availableAt($decaySeconds), $decaySeconds
);
@@ -79,6 +136,8 @@ class RateLimiter
*/
public function attempts($key)
{
$key = $this->cleanRateLimiterKey($key);
return $this->cache->get($key, 0);
}
@@ -90,9 +149,27 @@ class RateLimiter
*/
public function resetAttempts($key)
{
$key = $this->cleanRateLimiterKey($key);
return $this->cache->forget($key);
}
/**
* Get the number of retries left for the given key.
*
* @param string $key
* @param int $maxAttempts
* @return int
*/
public function remaining($key, $maxAttempts)
{
$key = $this->cleanRateLimiterKey($key);
$attempts = $this->attempts($key);
return $maxAttempts - $attempts;
}
/**
* Get the number of retries left for the given key.
*
@@ -102,9 +179,7 @@ class RateLimiter
*/
public function retriesLeft($key, $maxAttempts)
{
$attempts = $this->attempts($key);
return $maxAttempts - $attempts;
return $this->remaining($key, $maxAttempts);
}
/**
@@ -115,6 +190,8 @@ class RateLimiter
*/
public function clear($key)
{
$key = $this->cleanRateLimiterKey($key);
$this->resetAttempts($key);
$this->cache->forget($key.':timer');
@@ -128,6 +205,19 @@ class RateLimiter
*/
public function availableIn($key)
{
return $this->cache->get($key.':timer') - $this->currentTime();
$key = $this->cleanRateLimiterKey($key);
return max(0, $this->cache->get($key.':timer') - $this->currentTime());
}
/**
* Clean the rate limiter key from unicode characters.
*
* @param string $key
* @return string
*/
public function cleanRateLimiterKey($key)
{
return preg_replace('/&([a-z])[a-z]+;/i', '$1', htmlentities($key));
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class GlobalLimit extends Limit
{
/**
* Create a new limit instance.
*
* @param int $maxAttempts
* @param int $decayMinutes
* @return void
*/
public function __construct(int $maxAttempts, int $decayMinutes = 1)
{
parent::__construct('', $maxAttempts, $decayMinutes);
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class Limit
{
/**
* The rate limit signature key.
*
* @var mixed|string
*/
public $key;
/**
* The maximum number of attempts allowed within the given number of minutes.
*
* @var int
*/
public $maxAttempts;
/**
* The number of minutes until the rate limit is reset.
*
* @var int
*/
public $decayMinutes;
/**
* The response generator callback.
*
* @var callable
*/
public $responseCallback;
/**
* Create a new limit instance.
*
* @param mixed|string $key
* @param int $maxAttempts
* @param int $decayMinutes
* @return void
*/
public function __construct($key = '', int $maxAttempts = 60, int $decayMinutes = 1)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decayMinutes = $decayMinutes;
}
/**
* Create a new rate limit.
*
* @param int $maxAttempts
* @return static
*/
public static function perMinute($maxAttempts)
{
return new static('', $maxAttempts);
}
/**
* Create a new rate limit using minutes as decay time.
*
* @param int $decayMinutes
* @param int $maxAttempts
* @return static
*/
public static function perMinutes($decayMinutes, $maxAttempts)
{
return new static('', $maxAttempts, $decayMinutes);
}
/**
* Create a new rate limit using hours as decay time.
*
* @param int $maxAttempts
* @param int $decayHours
* @return static
*/
public static function perHour($maxAttempts, $decayHours = 1)
{
return new static('', $maxAttempts, 60 * $decayHours);
}
/**
* Create a new rate limit using days as decay time.
*
* @param int $maxAttempts
* @param int $decayDays
* @return static
*/
public static function perDay($maxAttempts, $decayDays = 1)
{
return new static('', $maxAttempts, 60 * 24 * $decayDays);
}
/**
* Create a new unlimited rate limit.
*
* @return static
*/
public static function none()
{
return new Unlimited;
}
/**
* Set the key of the rate limit.
*
* @param string $key
* @return $this
*/
public function by($key)
{
$this->key = $key;
return $this;
}
/**
* Set the callback that should generate the response when the limit is exceeded.
*
* @param callable $callback
* @return $this
*/
public function response(callable $callback)
{
$this->responseCallback = $callback;
return $this;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Illuminate\Cache\RateLimiting;
class Unlimited extends GlobalLimit
{
/**
* Create a new limit instance.
*
* @return void
*/
public function __construct()
{
parent::__construct(PHP_INT_MAX);
}
}

View File

@@ -70,4 +70,14 @@ class RedisLock extends Lock
{
return $this->redis->get($this->name);
}
/**
* Get the name of the Redis connection being used to manage the lock.
*
* @return string
*/
public function getConnectionName()
{
return $this->redis->getName();
}
}

View File

@@ -4,6 +4,7 @@ namespace Illuminate\Cache;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Contracts\Redis\Factory as Redis;
use Illuminate\Redis\Connections\PhpRedisConnection;
class RedisStore extends TaggableStore implements LockProvider
{
@@ -22,12 +23,19 @@ class RedisStore extends TaggableStore implements LockProvider
protected $prefix;
/**
* The Redis connection that should be used.
* The Redis connection instance that should be used to manage locks.
*
* @var string
*/
protected $connection;
/**
* The name of the connection that should be used for locks.
*
* @var string
*/
protected $lockConnection;
/**
* Create a new Redis store.
*
@@ -181,7 +189,15 @@ class RedisStore extends TaggableStore implements LockProvider
*/
public function lock($name, $seconds = 0, $owner = null)
{
return new RedisLock($this->connection(), $this->prefix.$name, $seconds, $owner);
$lockName = $this->prefix.$name;
$lockConnection = $this->lockConnection();
if ($lockConnection instanceof PhpRedisConnection) {
return new PhpRedisLock($lockConnection, $lockName, $seconds, $owner);
}
return new RedisLock($lockConnection, $lockName, $seconds, $owner);
}
/**
@@ -243,7 +259,17 @@ class RedisStore extends TaggableStore implements LockProvider
}
/**
* Set the connection name to be used.
* Get the Redis connection instance that should be used to manage locks.
*
* @return \Illuminate\Redis\Connections\Connection
*/
public function lockConnection()
{
return $this->redis->connection($this->lockConnection ?? $this->connection);
}
/**
* Specify the name of the connection that should be used to store data.
*
* @param string $connection
* @return void
@@ -253,6 +279,19 @@ class RedisStore extends TaggableStore implements LockProvider
$this->connection = $connection;
}
/**
* Specify the name of the connection that should be used to manage locks.
*
* @param string $connection
* @return $this
*/
public function setLockConnection($connection)
{
$this->lockConnection = $connection;
return $this;
}
/**
* Get the Redis database instance.
*

View File

@@ -10,6 +10,7 @@ class RedisTaggedCache extends TaggedCache
* @var string
*/
const REFERENCE_KEY_FOREVER = 'forever_ref';
/**
* Standard reference key.
*
@@ -41,13 +42,13 @@ class RedisTaggedCache extends TaggedCache
*
* @param string $key
* @param mixed $value
* @return void
* @return int|bool
*/
public function increment($key, $value = 1)
{
$this->pushStandardKeys($this->tags->getNamespace(), $key);
parent::increment($key, $value);
return parent::increment($key, $value);
}
/**
@@ -55,13 +56,13 @@ class RedisTaggedCache extends TaggedCache
*
* @param string $key
* @param mixed $value
* @return void
* @return int|bool
*/
public function decrement($key, $value = 1)
{
$this->pushStandardKeys($this->tags->getNamespace(), $key);
parent::decrement($key, $value);
return parent::decrement($key, $value);
}
/**
@@ -88,7 +89,9 @@ class RedisTaggedCache extends TaggedCache
$this->deleteForeverKeys();
$this->deleteStandardKeys();
return parent::flush();
$this->tags->flush();
return true;
}
/**
@@ -175,13 +178,26 @@ class RedisTaggedCache extends TaggedCache
*/
protected function deleteValues($referenceKey)
{
$values = array_unique($this->store->connection()->smembers($referenceKey));
$cursor = $defaultCursorValue = '0';
if (count($values) > 0) {
foreach (array_chunk($values, 1000) as $valuesChunk) {
do {
[$cursor, $valuesChunk] = $this->store->connection()->sscan(
$referenceKey, $cursor, ['match' => '*', 'count' => 1000]
);
// PhpRedis client returns false if set does not exist or empty. Array destruction
// on false stores null in each variable. If valuesChunk is null, it means that
// there were not results from the previously executed "sscan" Redis command.
if (is_null($valuesChunk)) {
break;
}
$valuesChunk = array_unique($valuesChunk);
if (count($valuesChunk) > 0) {
$this->store->connection()->del(...$valuesChunk);
}
}
} while (((string) $cursor) !== $defaultCursorValue);
}
/**

View File

@@ -131,6 +131,8 @@ class Repository implements ArrayAccess, CacheContract
/**
* {@inheritdoc}
*
* @return iterable
*/
public function getMultiple($keys, $default = null)
{
@@ -219,6 +221,8 @@ class Repository implements ArrayAccess, CacheContract
/**
* {@inheritdoc}
*
* @return bool
*/
public function set($key, $value, $ttl = null)
{
@@ -276,6 +280,8 @@ class Repository implements ArrayAccess, CacheContract
/**
* {@inheritdoc}
*
* @return bool
*/
public function setMultiple($values, $ttl = null)
{
@@ -292,8 +298,12 @@ class Repository implements ArrayAccess, CacheContract
*/
public function add($key, $value, $ttl = null)
{
$seconds = null;
if ($ttl !== null) {
if ($this->getSeconds($ttl) <= 0) {
$seconds = $this->getSeconds($ttl);
if ($seconds <= 0) {
return false;
}
@@ -301,8 +311,6 @@ class Repository implements ArrayAccess, CacheContract
// has a chance to override this logic. Some drivers better support the way
// this operation should work with a total "atomic" implementation of it.
if (method_exists($this->store, 'add')) {
$seconds = $this->getSeconds($ttl);
return $this->store->add(
$this->itemKey($key), $value, $seconds
);
@@ -313,7 +321,7 @@ class Repository implements ArrayAccess, CacheContract
// so it exists for subsequent requests. Then, we will return true so it is
// easy to know if the value gets added. Otherwise, we will return false.
if (is_null($this->get($key))) {
return $this->put($key, $value, $ttl);
return $this->put($key, $value, $seconds);
}
return false;
@@ -365,7 +373,7 @@ class Repository implements ArrayAccess, CacheContract
* Get an item from the cache, or execute the given Closure and store the result.
*
* @param string $key
* @param \DateTimeInterface|\DateInterval|int|null $ttl
* @param \Closure|\DateTimeInterface|\DateInterval|int|null $ttl
* @param \Closure $callback
* @return mixed
*/
@@ -380,7 +388,7 @@ class Repository implements ArrayAccess, CacheContract
return $value;
}
$this->put($key, $value = $callback(), $ttl);
$this->put($key, $value = $callback(), value($ttl));
return $value;
}
@@ -437,6 +445,8 @@ class Repository implements ArrayAccess, CacheContract
/**
* {@inheritdoc}
*
* @return bool
*/
public function delete($key)
{
@@ -445,6 +455,8 @@ class Repository implements ArrayAccess, CacheContract
/**
* {@inheritdoc}
*
* @return bool
*/
public function deleteMultiple($keys)
{
@@ -461,6 +473,8 @@ class Repository implements ArrayAccess, CacheContract
/**
* {@inheritdoc}
*
* @return bool
*/
public function clear()
{
@@ -477,7 +491,7 @@ class Repository implements ArrayAccess, CacheContract
*/
public function tags($names)
{
if (! method_exists($this->store, 'tags')) {
if (! $this->supportsTags()) {
throw new BadMethodCallException('This cache store does not support tagging.');
}
@@ -501,6 +515,33 @@ class Repository implements ArrayAccess, CacheContract
return $key;
}
/**
* Calculate the number of seconds for the given TTL.
*
* @param \DateTimeInterface|\DateInterval|int $ttl
* @return int
*/
protected function getSeconds($ttl)
{
$duration = $this->parseDateInterval($ttl);
if ($duration instanceof DateTimeInterface) {
$duration = Carbon::now()->diffInRealSeconds($duration, false);
}
return (int) ($duration > 0 ? $duration : 0);
}
/**
* Determine if the current store supports tags.
*
* @return bool
*/
public function supportsTags()
{
return method_exists($this->store, 'tags');
}
/**
* Get the default cache time.
*
@@ -537,7 +578,7 @@ class Repository implements ArrayAccess, CacheContract
/**
* Fire an event for this cache instance.
*
* @param string $event
* @param object|string $event
* @return void
*/
protected function event($event)
@@ -574,6 +615,7 @@ class Repository implements ArrayAccess, CacheContract
* @param string $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
{
return $this->has($key);
@@ -585,6 +627,7 @@ class Repository implements ArrayAccess, CacheContract
* @param string $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->get($key);
@@ -597,6 +640,7 @@ class Repository implements ArrayAccess, CacheContract
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
$this->put($key, $value, $this->default);
@@ -608,28 +652,12 @@ class Repository implements ArrayAccess, CacheContract
* @param string $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
$this->forget($key);
}
/**
* Calculate the number of seconds for the given TTL.
*
* @param \DateTimeInterface|\DateInterval|int $ttl
* @return int
*/
protected function getSeconds($ttl)
{
$duration = $this->parseDateInterval($ttl);
if ($duration instanceof DateTimeInterface) {
$duration = Carbon::now()->diffInRealSeconds($duration, false);
}
return (int) $duration > 0 ? $duration : 0;
}
/**
* Handle dynamic calls into macros or pass missing methods to the store.
*

View File

@@ -16,8 +16,12 @@ trait RetrievesMultipleKeys
{
$return = [];
foreach ($keys as $key) {
$return[$key] = $this->get($key);
$keys = collect($keys)->mapWithKeys(function ($value, $key) {
return [is_string($key) ? $key : $value => is_string($key) ? $value : null];
})->all();
foreach ($keys as $key => $default) {
$return[$key] = $this->get($key, $default);
}
return $return;

View File

@@ -56,6 +56,26 @@ class TagSet
return $id;
}
/**
* Flush all the tags in the set.
*
* @return void
*/
public function flush()
{
array_walk($this->names, [$this, 'flushTag']);
}
/**
* Flush the tag from the cache.
*
* @param string $name
*/
public function flushTag($name)
{
$this->store->forget($this->tagKey($name));
}
/**
* Get a unique namespace that changes when any of the tags are flushed.
*

View File

@@ -52,11 +52,11 @@ class TaggedCache extends Repository
*
* @param string $key
* @param mixed $value
* @return void
* @return int|bool
*/
public function increment($key, $value = 1)
{
$this->store->increment($this->itemKey($key), $value);
return $this->store->increment($this->itemKey($key), $value);
}
/**
@@ -64,11 +64,11 @@ class TaggedCache extends Repository
*
* @param string $key
* @param mixed $value
* @return void
* @return int|bool
*/
public function decrement($key, $value = 1)
{
$this->store->decrement($this->itemKey($key), $value);
return $this->store->decrement($this->itemKey($key), $value);
}
/**
@@ -105,7 +105,7 @@ class TaggedCache extends Repository
/**
* Fire an event for this cache instance.
*
* @param string $event
* @param \Illuminate\Cache\Events\CacheEvent $event
* @return void
*/
protected function event($event)

View File

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

View File

@@ -121,6 +121,23 @@ class Arr
return $results;
}
/**
* Convert a flatten "dot" notation array into an expanded array.
*
* @param iterable $array
* @return array
*/
public static function undot($array)
{
$results = [];
foreach ($array as $key => $value) {
static::set($results, $key, $value);
}
return $results;
}
/**
* Get all of the given array except for a specified array of keys.
*
@@ -144,6 +161,10 @@ class Arr
*/
public static function exists($array, $key)
{
if ($array instanceof Enumerable) {
return $array->has($key);
}
if ($array instanceof ArrayAccess) {
return $array->offsetExists($key);
}
@@ -389,6 +410,19 @@ class Arr
return array_keys($keys) !== $keys;
}
/**
* Determines if an array is a list.
*
* An array is a "list" if all array keys are sequential integers starting from 0 with no gaps in between.
*
* @param array $array
* @return bool
*/
public static function isList($array)
{
return ! self::isAssoc($array);
}
/**
* Get a subset of the items from the given array.
*
@@ -405,7 +439,7 @@ class Arr
* Pluck an array of values from an array.
*
* @param iterable $array
* @param string|array $value
* @param string|array|int|null $value
* @param string|array|null $key
* @return array
*/
@@ -463,7 +497,7 @@ class Arr
*/
public static function prepend($array, $value, $key = null)
{
if (is_null($key)) {
if (func_num_args() == 2) {
array_unshift($array, $value);
} else {
$array = [$key => $value] + $array;
@@ -476,7 +510,7 @@ class Arr
* Get a value from the array, and remove it.
*
* @param array $array
* @param string $key
* @param string|int $key
* @param mixed $default
* @return mixed
*/
@@ -489,16 +523,28 @@ class Arr
return $value;
}
/**
* Convert the array into a query string.
*
* @param array $array
* @return string
*/
public static function query($array)
{
return http_build_query($array, '', '&', PHP_QUERY_RFC3986);
}
/**
* Get one or a specified number of random values from an array.
*
* @param array $array
* @param int|null $number
* @param bool|false $preserveKeys
* @return mixed
*
* @throws \InvalidArgumentException
*/
public static function random($array, $number = null)
public static function random($array, $number = null, $preserveKeys = false)
{
$requested = is_null($number) ? 1 : $number;
@@ -522,8 +568,14 @@ class Arr
$results = [];
foreach ((array) $keys as $key) {
$results[] = $array[$key];
if ($preserveKeys) {
foreach ((array) $keys as $key) {
$results[$key] = $array[$key];
}
} else {
foreach ((array) $keys as $key) {
$results[] = $array[$key];
}
}
return $results;
@@ -593,7 +645,7 @@ class Arr
* Sort the array using the given callback or "dot" notation.
*
* @param array $array
* @param callable|string|null $callback
* @param callable|array|string|null $callback
* @return array
*/
public static function sort($array, $callback = null)
@@ -605,34 +657,52 @@ class Arr
* Recursively sort an array by keys and values.
*
* @param array $array
* @param int $options
* @param bool $descending
* @return array
*/
public static function sortRecursive($array)
public static function sortRecursive($array, $options = SORT_REGULAR, $descending = false)
{
foreach ($array as &$value) {
if (is_array($value)) {
$value = static::sortRecursive($value);
$value = static::sortRecursive($value, $options, $descending);
}
}
if (static::isAssoc($array)) {
ksort($array);
$descending
? krsort($array, $options)
: ksort($array, $options);
} else {
sort($array);
$descending
? rsort($array, $options)
: sort($array, $options);
}
return $array;
}
/**
* Convert the array into a query string.
* Conditionally compile classes from an array into a CSS class list.
*
* @param array $array
* @return string
*/
public static function query($array)
public static function toCssClasses($array)
{
return http_build_query($array, '', '&', PHP_QUERY_RFC3986);
$classList = static::wrap($array);
$classes = [];
foreach ($classList as $class => $constraint) {
if (is_numeric($class)) {
$classes[] = $constraint;
} elseif ($constraint) {
$classes[] = $class;
}
}
return implode(' ', $classes);
}
/**
@@ -647,6 +717,19 @@ class Arr
return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);
}
/**
* Filter items where the value is not null.
*
* @param array $array
* @return array
*/
public static function whereNotNull($array)
{
return static::where($array, function ($value) {
return ! is_null($value);
});
}
/**
* If the given value is not an array and not null, wrap it in one.
*

View File

@@ -4,11 +4,12 @@ namespace Illuminate\Support;
use ArrayAccess;
use ArrayIterator;
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
use Illuminate\Support\Traits\EnumeratesValues;
use Illuminate\Support\Traits\Macroable;
use stdClass;
class Collection implements ArrayAccess, Enumerable
class Collection implements ArrayAccess, CanBeEscapedWhenCastToString, Enumerable
{
use EnumeratesValues, Macroable;
@@ -31,23 +32,15 @@ class Collection implements ArrayAccess, Enumerable
}
/**
* Create a new collection by invoking the callback a given amount of times.
* Create a collection with the given range.
*
* @param int $number
* @param callable|null $callback
* @param int $from
* @param int $to
* @return static
*/
public static function times($number, callable $callback = null)
public static function range($from, $to)
{
if ($number < 1) {
return new static;
}
if (is_null($callback)) {
return new static(range(1, $number));
}
return (new static(range(1, $number)))->map($callback);
return new static(range($from, $to));
}
/**
@@ -135,7 +128,7 @@ class Collection implements ArrayAccess, Enumerable
$collection = isset($key) ? $this->pluck($key) : $this;
$counts = new self;
$counts = new static;
$collection->each(function ($value) use ($counts) {
$counts[$value] = isset($counts[$value]) ? $counts[$value] + 1 : 1;
@@ -183,6 +176,19 @@ class Collection implements ArrayAccess, Enumerable
return $this->contains($this->operatorForWhere(...func_get_args()));
}
/**
* Determine if an item is not contained in the collection.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function doesntContain($key, $operator = null, $value = null)
{
return ! $this->contains(...func_get_args());
}
/**
* Cross join with the given lists, returning all possible permutations.
*
@@ -268,7 +274,7 @@ class Collection implements ArrayAccess, Enumerable
/**
* Retrieve duplicate items from the collection.
*
* @param callable|null $callback
* @param callable|string|null $callback
* @param bool $strict
* @return static
*/
@@ -296,7 +302,7 @@ class Collection implements ArrayAccess, Enumerable
/**
* Retrieve duplicate items from the collection using strict comparison.
*
* @param callable|null $callback
* @param callable|string|null $callback
* @return static
*/
public function duplicatesStrict($callback = null)
@@ -391,7 +397,7 @@ class Collection implements ArrayAccess, Enumerable
/**
* Remove an item from the collection by key.
*
* @param string|array $keys
* @param string|int|array $keys
* @return $this
*/
public function forget($keys)
@@ -412,13 +418,31 @@ class Collection implements ArrayAccess, Enumerable
*/
public function get($key, $default = null)
{
if ($this->offsetExists($key)) {
if (array_key_exists($key, $this->items)) {
return $this->items[$key];
}
return value($default);
}
/**
* Get an item from the collection by key or add it to collection if it does not exist.
*
* @param mixed $key
* @param mixed $value
* @return mixed
*/
public function getOrPut($key, $value)
{
if (array_key_exists($key, $this->items)) {
return $this->items[$key];
}
$this->offsetSet($key, $value = value($value));
return $value;
}
/**
* Group an associative array by a field or using a callback.
*
@@ -501,7 +525,7 @@ class Collection implements ArrayAccess, Enumerable
$keys = is_array($key) ? $key : func_get_args();
foreach ($keys as $value) {
if (! $this->offsetExists($value)) {
if (! array_key_exists($value, $this->items)) {
return false;
}
}
@@ -509,6 +533,29 @@ class Collection implements ArrayAccess, Enumerable
return true;
}
/**
* Determine if any of the keys exist in the collection.
*
* @param mixed $key
* @return bool
*/
public function hasAny($key)
{
if ($this->isEmpty()) {
return false;
}
$keys = is_array($key) ? $key : func_get_args();
foreach ($keys as $value) {
if ($this->has($value)) {
return true;
}
}
return false;
}
/**
* Concatenate values of a given key as a string.
*
@@ -520,11 +567,11 @@ class Collection implements ArrayAccess, Enumerable
{
$first = $this->first();
if (is_array($first) || is_object($first)) {
return implode($glue, $this->pluck($value)->all());
if (is_array($first) || (is_object($first) && ! $first instanceof Stringable)) {
return implode($glue ?? '', $this->pluck($value)->all());
}
return implode($value, $this->items);
return implode($value ?? '', $this->items);
}
/**
@@ -561,6 +608,16 @@ class Collection implements ArrayAccess, Enumerable
return empty($this->items);
}
/**
* Determine if the collection contains a single item.
*
* @return bool
*/
public function containsOneItem()
{
return $this->count() === 1;
}
/**
* Join all items from the collection using a string. The final items can use a separate glue string.
*
@@ -616,7 +673,7 @@ class Collection implements ArrayAccess, Enumerable
/**
* Get the values of a given key.
*
* @param string|array $value
* @param string|array|int|null $value
* @param string|null $key
* @return static
*/
@@ -749,8 +806,8 @@ class Collection implements ArrayAccess, Enumerable
$position = 0;
foreach ($this->items as $item) {
if ($position % $step === $offset) {
foreach ($this->slice($offset)->items as $item) {
if ($position % $step === 0) {
$new[] = $item;
}
@@ -782,13 +839,30 @@ class Collection implements ArrayAccess, Enumerable
}
/**
* Get and remove the last item from the collection.
* Get and remove the last N items from the collection.
*
* @param int $count
* @return mixed
*/
public function pop()
public function pop($count = 1)
{
return array_pop($this->items);
if ($count === 1) {
return array_pop($this->items);
}
if ($this->isEmpty()) {
return new static;
}
$results = [];
$collectionCount = $this->count();
foreach (range(1, min($count, $collectionCount)) as $item) {
array_push($results, array_pop($this->items));
}
return new static($results);
}
/**
@@ -800,7 +874,7 @@ class Collection implements ArrayAccess, Enumerable
*/
public function prepend($value, $key = null)
{
$this->items = Arr::prepend($this->items, $value, $key);
$this->items = Arr::prepend($this->items, ...func_get_args());
return $this;
}
@@ -880,18 +954,6 @@ class Collection implements ArrayAccess, Enumerable
return new static(Arr::random($this->items, $number));
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduce(callable $callback, $initial = null)
{
return array_reduce($this->items, $callback, $initial);
}
/**
* Replace the collection items with the given items.
*
@@ -947,13 +1009,30 @@ class Collection implements ArrayAccess, Enumerable
}
/**
* Get and remove the first item from the collection.
* Get and remove the first N items from the collection.
*
* @param int $count
* @return mixed
*/
public function shift()
public function shift($count = 1)
{
return array_shift($this->items);
if ($count === 1) {
return array_shift($this->items);
}
if ($this->isEmpty()) {
return new static;
}
$results = [];
$collectionCount = $this->count();
foreach (range(1, min($count, $collectionCount)) as $item) {
array_push($results, array_shift($this->items));
}
return new static($results);
}
/**
@@ -967,6 +1046,22 @@ class Collection implements ArrayAccess, Enumerable
return new static(Arr::shuffle($this->items, $seed));
}
/**
* Create chunks representing a "sliding window" view of the items in the collection.
*
* @param int $size
* @param int $step
* @return static
*/
public function sliding($size = 2, $step = 1)
{
$chunks = floor(($this->count() - $size) / $step) + 1;
return static::times($chunks, function ($number) use ($size, $step) {
return $this->slice(($number - 1) * $step, $size);
});
}
/**
* Skip the first {$count} items.
*
@@ -1049,6 +1144,74 @@ class Collection implements ArrayAccess, Enumerable
return $groups;
}
/**
* Split a collection into a certain number of groups, and fill the first groups completely.
*
* @param int $numberOfGroups
* @return static
*/
public function splitIn($numberOfGroups)
{
return $this->chunk(ceil($this->count() / $numberOfGroups));
}
/**
* Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return mixed
*
* @throws \Illuminate\Support\ItemNotFoundException
* @throws \Illuminate\Support\MultipleItemsFoundException
*/
public function sole($key = null, $operator = null, $value = null)
{
$filter = func_num_args() > 1
? $this->operatorForWhere(...func_get_args())
: $key;
$items = $this->when($filter)->filter($filter);
if ($items->isEmpty()) {
throw new ItemNotFoundException;
}
if ($items->count() > 1) {
throw new MultipleItemsFoundException;
}
return $items->first();
}
/**
* Get the first item in the collection but throw an exception if no matching items exist.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return mixed
*
* @throws \Illuminate\Support\ItemNotFoundException
*/
public function firstOrFail($key = null, $operator = null, $value = null)
{
$filter = func_num_args() > 1
? $this->operatorForWhere(...func_get_args())
: $key;
$placeholder = new stdClass();
$item = $this->first($filter, $placeholder);
if ($item === $placeholder) {
throw new ItemNotFoundException;
}
return $item;
}
/**
* Chunk the collection into chunks of the given size.
*
@@ -1070,6 +1233,19 @@ class Collection implements ArrayAccess, Enumerable
return new static($chunks);
}
/**
* Chunk the collection into chunks with a callback.
*
* @param callable $callback
* @return static
*/
public function chunkWhile(callable $callback)
{
return new static(
$this->lazy()->chunkWhile($callback)->mapInto(static::class)
);
}
/**
* Sort through each item with a callback.
*
@@ -1082,7 +1258,7 @@ class Collection implements ArrayAccess, Enumerable
$callback && is_callable($callback)
? uasort($items, $callback)
: asort($items, $callback);
: asort($items, $callback ?? SORT_REGULAR);
return new static($items);
}
@@ -1105,20 +1281,24 @@ class Collection implements ArrayAccess, Enumerable
/**
* Sort the collection using the given callback.
*
* @param callable|string $callback
* @param callable|array|string $callback
* @param int $options
* @param bool $descending
* @return static
*/
public function sortBy($callback, $options = SORT_REGULAR, $descending = false)
{
if (is_array($callback) && ! is_callable($callback)) {
return $this->sortByMany($callback);
}
$results = [];
$callback = $this->valueRetriever($callback);
// First we will loop through the items and get the comparator from a callback
// function which we were given. Then, we will sort the returned values and
// and grab the corresponding values for the sorted keys from this array.
// grab all the corresponding values for the sorted keys from this array.
foreach ($this->items as $key => $value) {
$results[$key] = $callback($value, $key);
}
@@ -1136,6 +1316,50 @@ class Collection implements ArrayAccess, Enumerable
return new static($results);
}
/**
* Sort the collection using multiple comparisons.
*
* @param array $comparisons
* @return static
*/
protected function sortByMany(array $comparisons = [])
{
$items = $this->items;
usort($items, function ($a, $b) use ($comparisons) {
foreach ($comparisons as $comparison) {
$comparison = Arr::wrap($comparison);
$prop = $comparison[0];
$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 {
$values = [data_get($a, $prop), data_get($b, $prop)];
if (! $ascending) {
$values = array_reverse($values);
}
$result = $values[0] <=> $values[1];
}
if ($result === 0) {
continue;
}
return $result;
}
});
return new static($items);
}
/**
* Sort the collection in descending order using the given callback.
*
@@ -1175,6 +1399,21 @@ class Collection implements ArrayAccess, Enumerable
return $this->sortKeys($options, true);
}
/**
* Sort the collection keys using a callback.
*
* @param callable $callback
* @return static
*/
public function sortKeysUsing(callable $callback)
{
$items = $this->items;
uksort($items, $callback);
return new static($items);
}
/**
* Splice a portion of the underlying collection array.
*
@@ -1189,7 +1428,7 @@ class Collection implements ArrayAccess, Enumerable
return new static(array_splice($this->items, $offset));
}
return new static(array_splice($this->items, $offset, $length, $replacement));
return new static(array_splice($this->items, $offset, $length, $this->getArrayableItems($replacement)));
}
/**
@@ -1242,6 +1481,42 @@ class Collection implements ArrayAccess, Enumerable
return $this;
}
/**
* Convert a flatten "dot" notation array into an expanded array.
*
* @return static
*/
public function undot()
{
return new static(Arr::undot($this->all()));
}
/**
* Return only unique items from the collection array.
*
* @param string|callable|null $key
* @param bool $strict
* @return static
*/
public function unique($key = null, $strict = false)
{
if (is_null($key) && $strict === false) {
return new static(array_unique($this->items, SORT_REGULAR));
}
$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;
});
}
/**
* Reset the keys on the underlying array.
*
@@ -1291,6 +1566,7 @@ class Collection implements ArrayAccess, Enumerable
*
* @return \ArrayIterator
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return new ArrayIterator($this->items);
@@ -1301,6 +1577,7 @@ class Collection implements ArrayAccess, Enumerable
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
return count($this->items);
@@ -1346,9 +1623,10 @@ class Collection implements ArrayAccess, Enumerable
* @param mixed $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
{
return array_key_exists($key, $this->items);
return isset($this->items[$key]);
}
/**
@@ -1357,6 +1635,7 @@ class Collection implements ArrayAccess, Enumerable
* @param mixed $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->items[$key];
@@ -1369,6 +1648,7 @@ class Collection implements ArrayAccess, Enumerable
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
if (is_null($key)) {
@@ -1381,9 +1661,10 @@ class Collection implements ArrayAccess, Enumerable
/**
* Unset the item at a given offset.
*
* @param string $key
* @param mixed $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
unset($this->items[$key]);

View File

@@ -27,6 +27,15 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public static function times($number, callable $callback = null);
/**
* Create a collection with the given range.
*
* @param int $from
* @param int $to
* @return static
*/
public static function range($from, $to);
/**
* Wrap the given value in a collection if applicable.
*
@@ -43,6 +52,13 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public static function unwrap($value);
/**
* Create a new instance with no items.
*
* @return static
*/
public static function empty();
/**
* Get all items in the enumerable.
*
@@ -118,6 +134,14 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function contains($key, $operator = null, $value = null);
/**
* Cross join with the given lists, returning all possible permutations.
*
* @param mixed ...$lists
* @return static
*/
public function crossJoin(...$lists);
/**
* Dump the collection and end the script.
*
@@ -187,7 +211,7 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
/**
* Retrieve duplicate items.
*
* @param callable|null $callback
* @param callable|string|null $callback
* @param bool $strict
* @return static
*/
@@ -196,7 +220,7 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
/**
* Retrieve duplicate items using strict comparison.
*
* @param callable|null $callback
* @param callable|string|null $callback
* @return static
*/
public function duplicatesStrict($callback = null);
@@ -309,6 +333,22 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function where($key, $operator = null, $value = null);
/**
* Filter items where the value for the given key is null.
*
* @param string|null $key
* @return static
*/
public function whereNull($key = null);
/**
* Filter items where the value for the given key is not null.
*
* @param string|null $key
* @return static
*/
public function whereNotNull($key = null);
/**
* Filter items by the given key value pair using strict comparison.
*
@@ -375,9 +415,9 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
public function whereNotInStrict($key, $values);
/**
* Filter the items, removing any items that don't match the given type.
* Filter the items, removing any items that don't match the given type(s).
*
* @param string $type
* @param string|string[] $type
* @return static
*/
public function whereInstanceOf($type);
@@ -401,6 +441,14 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function firstWhere($key, $operator = null, $value = null);
/**
* Get a flattened array of the items in the collection.
*
* @param int $depth
* @return static
*/
public function flatten($depth = INF);
/**
* Flip the values with their keys.
*
@@ -727,6 +775,22 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function skip($count);
/**
* Skip items in the collection until the given condition is met.
*
* @param mixed $value
* @return static
*/
public function skipUntil($value);
/**
* Skip items in the collection while the given condition is met.
*
* @param mixed $value
* @return static
*/
public function skipWhile($value);
/**
* Get a slice of items from the enumerable.
*
@@ -752,6 +816,14 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function chunk($size);
/**
* Chunk the collection into chunks with a callback.
*
* @param callable $callback
* @return static
*/
public function chunkWhile(callable $callback);
/**
* Sort through each item with a callback.
*
@@ -820,6 +892,22 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function take($limit);
/**
* Take items in the collection until the given condition is met.
*
* @param mixed $value
* @return static
*/
public function takeUntil($value);
/**
* Take items in the collection while the given condition is met.
*
* @param mixed $value
* @return static
*/
public function takeWhile($value);
/**
* Pass the collection to the given callback and then return it.
*
@@ -894,6 +982,17 @@ interface Enumerable extends Arrayable, Countable, IteratorAggregate, Jsonable,
*/
public function countBy($callback = null);
/**
* Zip the collection together with one or more arrays.
*
* e.g. new Collection([1, 2, 3])->zip([4, 5, 6]);
* => [[1, 4], [2, 5], [3, 6]]
*
* @param mixed ...$items
* @return static
*/
public function zip($items);
/**
* Collect the values into a collection.
*

View File

@@ -0,0 +1,9 @@
<?php
namespace Illuminate\Support;
use RuntimeException;
class ItemNotFoundException extends RuntimeException
{
}

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

@@ -4,12 +4,14 @@ namespace Illuminate\Support;
use ArrayIterator;
use Closure;
use DateTimeInterface;
use Illuminate\Contracts\Support\CanBeEscapedWhenCastToString;
use Illuminate\Support\Traits\EnumeratesValues;
use Illuminate\Support\Traits\Macroable;
use IteratorAggregate;
use stdClass;
class LazyCollection implements Enumerable
class LazyCollection implements CanBeEscapedWhenCastToString, Enumerable
{
use EnumeratesValues, Macroable;
@@ -38,39 +40,7 @@ class LazyCollection implements Enumerable
}
/**
* Create a new instance with no items.
*
* @return static
*/
public static function empty()
{
return new static([]);
}
/**
* Create a new instance by invoking the callback a given amount of times.
*
* @param int $number
* @param callable|null $callback
* @return static
*/
public static function times($number, callable $callback = null)
{
if ($number < 1) {
return new static;
}
$instance = new static(function () use ($number) {
for ($current = 1; $current <= $number; $current++) {
yield $current;
}
});
return is_null($callback) ? $instance : $instance->map($callback);
}
/**
* Create an enumerable with the given range.
* Create a collection with the given range.
*
* @param int $from
* @param int $to
@@ -79,8 +49,14 @@ class LazyCollection implements Enumerable
public static function range($from, $to)
{
return new static(function () use ($from, $to) {
for (; $from <= $to; $from++) {
yield $from;
if ($from <= $to) {
for (; $from <= $to; $from++) {
yield $from;
}
} else {
for (; $from >= $to; $from--) {
yield $from;
}
}
});
}
@@ -229,6 +205,19 @@ class LazyCollection implements Enumerable
return $this->contains($this->operatorForWhere(...func_get_args()));
}
/**
* Determine if an item is not contained in the enumerable.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return bool
*/
public function doesntContain($key, $operator = null, $value = null)
{
return ! $this->contains(...func_get_args());
}
/**
* Cross join the given iterables, returning all possible permutations.
*
@@ -341,7 +330,7 @@ class LazyCollection implements Enumerable
/**
* Retrieve duplicate items.
*
* @param callable|null $callback
* @param callable|string|null $callback
* @param bool $strict
* @return static
*/
@@ -353,7 +342,7 @@ class LazyCollection implements Enumerable
/**
* Retrieve duplicate items using strict comparison.
*
* @param callable|null $callback
* @param callable|string|null $callback
* @return static
*/
public function duplicatesStrict($callback = null)
@@ -537,6 +526,25 @@ class LazyCollection implements Enumerable
return false;
}
/**
* Determine if any of the keys exist in the collection.
*
* @param mixed $key
* @return bool
*/
public function hasAny($key)
{
$keys = array_flip(is_array($key) ? $key : func_get_args());
foreach ($this as $key => $value) {
if (array_key_exists($key, $keys)) {
return true;
}
}
return false;
}
/**
* Concatenate values of a given key as a string.
*
@@ -572,7 +580,7 @@ class LazyCollection implements Enumerable
}
/**
* Determine if the items is empty or not.
* Determine if the items are empty or not.
*
* @return bool
*/
@@ -581,6 +589,16 @@ class LazyCollection implements Enumerable
return ! $this->getIterator()->valid();
}
/**
* Determine if the collection contains a single item.
*
* @return bool
*/
public function containsOneItem()
{
return $this->take(2)->count() === 1;
}
/**
* Join all items from the collection using a string. The final items can use a separate glue string.
*
@@ -778,8 +796,8 @@ class LazyCollection implements Enumerable
return new static(function () use ($step, $offset) {
$position = 0;
foreach ($this as $item) {
if ($position % $step === $offset) {
foreach ($this->slice($offset) as $item) {
if ($position % $step === 0) {
yield $item;
}
@@ -852,24 +870,6 @@ class LazyCollection implements Enumerable
return is_null($number) ? $result : new static($result);
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduce(callable $callback, $initial = null)
{
$result = $initial;
foreach ($this as $value) {
$result = $callback($result, $value);
}
return $result;
}
/**
* Replace the collection items with the given items.
*
@@ -953,6 +953,45 @@ class LazyCollection implements Enumerable
return $this->passthru('shuffle', func_get_args());
}
/**
* Create chunks representing a "sliding window" view of the items in the collection.
*
* @param int $size
* @param int $step
* @return static
*/
public function sliding($size = 2, $step = 1)
{
return new static(function () use ($size, $step) {
$iterator = $this->getIterator();
$chunk = [];
while ($iterator->valid()) {
$chunk[$iterator->key()] = $iterator->current();
if (count($chunk) == $size) {
yield tap(new static($chunk), function () use (&$chunk, $step) {
$chunk = array_slice($chunk, $step, null, true);
});
// If the $step between chunks is bigger than each chunk's $size
// we will skip the extra items (which should never be in any
// chunk) before we continue to the next chunk in the loop.
if ($step > $size) {
$skip = $step - $size;
for ($i = 0; $i < $skip && $iterator->valid(); $i++) {
$iterator->next();
}
}
}
$iterator->next();
}
});
}
/**
* Skip the first {$count} items.
*
@@ -1043,6 +1082,55 @@ class LazyCollection implements Enumerable
return $this->passthru('split', func_get_args());
}
/**
* Get the first item in the collection, but only if exactly one item exists. Otherwise, throw an exception.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return mixed
*
* @throws \Illuminate\Support\ItemNotFoundException
* @throws \Illuminate\Support\MultipleItemsFoundException
*/
public function sole($key = null, $operator = null, $value = null)
{
$filter = func_num_args() > 1
? $this->operatorForWhere(...func_get_args())
: $key;
return $this
->when($filter)
->filter($filter)
->take(2)
->collect()
->sole();
}
/**
* Get the first item in the collection but throw an exception if no matching items exist.
*
* @param mixed $key
* @param mixed $operator
* @param mixed $value
* @return mixed
*
* @throws \Illuminate\Support\ItemNotFoundException
*/
public function firstOrFail($key = null, $operator = null, $value = null)
{
$filter = func_num_args() > 1
? $this->operatorForWhere(...func_get_args())
: $key;
return $this
->when($filter)
->filter($filter)
->take(1)
->collect()
->firstOrFail();
}
/**
* Chunk the collection into chunks of the given size.
*
@@ -1082,6 +1170,54 @@ class LazyCollection implements Enumerable
});
}
/**
* Split a collection into a certain number of groups, and fill the first groups completely.
*
* @param int $numberOfGroups
* @return static
*/
public function splitIn($numberOfGroups)
{
return $this->chunk(ceil($this->count() / $numberOfGroups));
}
/**
* Chunk the collection into chunks with a callback.
*
* @param callable $callback
* @return static
*/
public function chunkWhile(callable $callback)
{
return new static(function () use ($callback) {
$iterator = $this->getIterator();
$chunk = new Collection;
if ($iterator->valid()) {
$chunk[$iterator->key()] = $iterator->current();
$iterator->next();
}
while ($iterator->valid()) {
if (! $callback($iterator->current(), $iterator->key(), $chunk)) {
yield new static($chunk);
$chunk = new Collection;
}
$chunk[$iterator->key()] = $iterator->current();
$iterator->next();
}
if ($chunk->isNotEmpty()) {
yield new static($chunk);
}
});
}
/**
* Sort through each item with a callback.
*
@@ -1152,6 +1288,17 @@ class LazyCollection implements Enumerable
return $this->passthru('sortKeysDesc', func_get_args());
}
/**
* Sort the collection keys using a callback.
*
* @param callable $callback
* @return static
*/
public function sortKeysUsing(callable $callback)
{
return $this->passthru('sortKeysUsing', func_get_args());
}
/**
* Take the first or last {$limit} items.
*
@@ -1202,6 +1349,21 @@ class LazyCollection implements Enumerable
});
}
/**
* Take items in the collection until a given point in time.
*
* @param \DateTimeInterface $timeout
* @return static
*/
public function takeUntilTimeout(DateTimeInterface $timeout)
{
$timeout = $timeout->getTimestamp();
return $this->takeWhile(function () use ($timeout) {
return $this->now() < $timeout;
});
}
/**
* Take items in the collection while the given condition is met.
*
@@ -1212,7 +1374,9 @@ class LazyCollection implements Enumerable
{
$callback = $this->useAsCallable($value) ? $value : $this->equality($value);
return $this->takeUntil($this->negate($callback));
return $this->takeUntil(function ($item, $key) use ($callback) {
return ! $callback($item, $key);
});
}
/**
@@ -1232,6 +1396,40 @@ class LazyCollection implements Enumerable
});
}
/**
* Convert a flatten "dot" notation array into an expanded array.
*
* @return static
*/
public function undot()
{
return $this->passthru('undot', []);
}
/**
* Return only unique items from the collection array.
*
* @param string|callable|null $key
* @param bool $strict
* @return static
*/
public function unique($key = null, $strict = false)
{
$callback = $this->valueRetriever($key);
return new static(function () use ($callback, $strict) {
$exists = [];
foreach ($this as $key => $item) {
if (! in_array($id = $callback($item, $key), $exists, $strict)) {
yield $key => $item;
$exists[] = $id;
}
}
});
}
/**
* Reset the keys on the underlying array.
*
@@ -1305,6 +1503,7 @@ class LazyCollection implements Enumerable
*
* @return \Traversable
*/
#[\ReturnTypeWillChange]
public function getIterator()
{
return $this->makeIterator($this->source);
@@ -1315,6 +1514,7 @@ class LazyCollection implements Enumerable
*
* @return int
*/
#[\ReturnTypeWillChange]
public function count()
{
if (is_array($this->source)) {
@@ -1372,4 +1572,14 @@ class LazyCollection implements Enumerable
yield from $this->collect()->$method(...$params);
});
}
/**
* Get the current time.
*
* @return int
*/
protected function now()
{
return time();
}
}

View File

@@ -0,0 +1,9 @@
<?php
namespace Illuminate\Support;
use RuntimeException;
class MultipleItemsFoundException extends RuntimeException
{
}

View File

@@ -15,11 +15,14 @@ use Illuminate\Support\HigherOrderWhenProxy;
use JsonSerializable;
use Symfony\Component\VarDumper\VarDumper;
use Traversable;
use UnexpectedValueException;
use UnitEnum;
/**
* @property-read HigherOrderCollectionProxy $average
* @property-read HigherOrderCollectionProxy $avg
* @property-read HigherOrderCollectionProxy $contains
* @property-read HigherOrderCollectionProxy $doesntContain
* @property-read HigherOrderCollectionProxy $each
* @property-read HigherOrderCollectionProxy $every
* @property-read HigherOrderCollectionProxy $filter
@@ -35,21 +38,33 @@ use Traversable;
* @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 $until
*/
trait EnumeratesValues
{
/**
* Indicates that the object's string representation should be escaped when __toString is invoked.
*
* @var bool
*/
protected $escapeWhenCastingToString = false;
/**
* The methods that can be proxied.
*
* @var array
* @var string[]
*/
protected static $proxies = [
'average',
'avg',
'contains',
'doesntContain',
'each',
'every',
'filter',
@@ -109,6 +124,34 @@ trait EnumeratesValues
return $value instanceof Enumerable ? $value->all() : $value;
}
/**
* Create a new instance with no items.
*
* @return static
*/
public static function empty()
{
return new static([]);
}
/**
* Create a new collection by invoking the callback a given amount of times.
*
* @param int $number
* @param callable|null $callback
* @return static
*/
public static function times($number, callable $callback = null)
{
if ($number < 1) {
return new static;
}
return static::range(1, $number)
->when($callback)
->map($callback);
}
/**
* Alias for the "avg" method.
*
@@ -519,7 +562,7 @@ trait EnumeratesValues
}
/**
* Filter items where the given key is not null.
* Filter items where the value for the given key is null.
*
* @param string|null $key
* @return static
@@ -530,7 +573,7 @@ trait EnumeratesValues
}
/**
* Filter items where the given key is null.
* Filter items where the value for the given key is not null.
*
* @param string|null $key
* @return static
@@ -637,14 +680,24 @@ trait EnumeratesValues
}
/**
* Filter the items, removing any items that don't match the given type.
* Filter the items, removing any items that don't match the given type(s).
*
* @param string $type
* @param string|string[] $type
* @return static
*/
public function whereInstanceOf($type)
{
return $this->filter(function ($value) use ($type) {
if (is_array($type)) {
foreach ($type as $classType) {
if ($value instanceof $classType) {
return true;
}
}
return false;
}
return $value instanceof $type;
});
}
@@ -660,6 +713,33 @@ trait EnumeratesValues
return $callback($this);
}
/**
* Pass the collection into a new class.
*
* @param string $class
* @return mixed
*/
public function pipeInto($class)
{
return new $class($this);
}
/**
* Pass the collection through a series of callable pipes and return the result.
*
* @param array<callable> $pipes
* @return mixed
*/
public function pipeThrough($pipes)
{
return static::make($pipes)->reduce(
function ($carry, $pipe) {
return $pipe($carry);
},
$this,
);
}
/**
* Pass the collection to the given callback and then return it.
*
@@ -673,6 +753,79 @@ trait EnumeratesValues
return $this;
}
/**
* Reduce the collection to a single value.
*
* @param callable $callback
* @param mixed $initial
* @return mixed
*/
public function reduce(callable $callback, $initial = null)
{
$result = $initial;
foreach ($this as $key => $value) {
$result = $callback($result, $value, $key);
}
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.
*
* @param callable $callback
* @param mixed ...$initial
* @return array
*
* @throws \UnexpectedValueException
*/
public function reduceSpread(callable $callback, ...$initial)
{
$result = $initial;
foreach ($this as $key => $value) {
$result = call_user_func_array($callback, array_merge($result, [$value, $key]));
if (! is_array($result)) {
throw new UnexpectedValueException(sprintf(
"%s::reduceMany expects reducer to return an array, but got a '%s' instead.",
class_basename(static::class), gettype($result)
));
}
}
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.
*
@@ -690,28 +843,6 @@ trait EnumeratesValues
});
}
/**
* Return only unique items from the collection array.
*
* @param string|callable|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.
*
@@ -723,21 +854,6 @@ trait EnumeratesValues
return $this->unique($key, true);
}
/**
* Take items in the collection until the given condition is met.
*
* This is an alias to the "takeUntil" method.
*
* @param mixed $value
* @return static
*
* @deprecated Use the "takeUntil" method directly.
*/
public function until($value)
{
return $this->takeUntil($value);
}
/**
* Collect the values into a collection.
*
@@ -765,6 +881,7 @@ trait EnumeratesValues
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
return array_map(function ($value) {
@@ -809,7 +926,22 @@ trait EnumeratesValues
*/
public function __toString()
{
return $this->toJson();
return $this->escapeWhenCastingToString
? e($this->toJson())
: $this->toJson();
}
/**
* Indicate that the model's string representation should be escaped when __toString is invoked.
*
* @param bool $escape
* @return $this
*/
public function escapeWhenCastingToString($escape = true)
{
$this->escapeWhenCastingToString = $escape;
return $this;
}
/**
@@ -860,6 +992,8 @@ trait EnumeratesValues
return (array) $items->jsonSerialize();
} elseif ($items instanceof Traversable) {
return iterator_to_array($items);
} elseif ($items instanceof UnitEnum) {
return [$items];
}
return (array) $items;

View File

@@ -0,0 +1,41 @@
{
"name": "illuminate/collections",
"description": "The Illuminate Collections 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": "^7.3|^8.0",
"illuminate/contracts": "^8.0",
"illuminate/macroable": "^8.0"
},
"autoload": {
"psr-4": {
"Illuminate\\Support\\": ""
},
"files": [
"helpers.php"
]
},
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"suggest": {
"symfony/var-dumper": "Required to use the dump method (^5.4)."
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,186 @@
<?php
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
if (! function_exists('collect')) {
/**
* Create a collection from the given value.
*
* @param mixed $value
* @return \Illuminate\Support\Collection
*/
function collect($value = null)
{
return new Collection($value);
}
}
if (! function_exists('data_fill')) {
/**
* Fill in data where it's missing.
*
* @param mixed $target
* @param string|array $key
* @param mixed $value
* @return mixed
*/
function data_fill(&$target, $key, $value)
{
return data_set($target, $key, $value, false);
}
}
if (! function_exists('data_get')) {
/**
* Get an item from an array or object using "dot" notation.
*
* @param mixed $target
* @param string|array|int|null $key
* @param mixed $default
* @return mixed
*/
function data_get($target, $key, $default = null)
{
if (is_null($key)) {
return $target;
}
$key = is_array($key) ? $key : explode('.', $key);
foreach ($key as $i => $segment) {
unset($key[$i]);
if (is_null($segment)) {
return $target;
}
if ($segment === '*') {
if ($target instanceof Collection) {
$target = $target->all();
} elseif (! is_array($target)) {
return value($default);
}
$result = [];
foreach ($target as $item) {
$result[] = data_get($item, $key);
}
return in_array('*', $key) ? Arr::collapse($result) : $result;
}
if (Arr::accessible($target) && Arr::exists($target, $segment)) {
$target = $target[$segment];
} elseif (is_object($target) && isset($target->{$segment})) {
$target = $target->{$segment};
} else {
return value($default);
}
}
return $target;
}
}
if (! function_exists('data_set')) {
/**
* Set an item on an array or object using dot notation.
*
* @param mixed $target
* @param string|array $key
* @param mixed $value
* @param bool $overwrite
* @return mixed
*/
function data_set(&$target, $key, $value, $overwrite = true)
{
$segments = is_array($key) ? $key : explode('.', $key);
if (($segment = array_shift($segments)) === '*') {
if (! Arr::accessible($target)) {
$target = [];
}
if ($segments) {
foreach ($target as &$inner) {
data_set($inner, $segments, $value, $overwrite);
}
} elseif ($overwrite) {
foreach ($target as &$inner) {
$inner = $value;
}
}
} elseif (Arr::accessible($target)) {
if ($segments) {
if (! Arr::exists($target, $segment)) {
$target[$segment] = [];
}
data_set($target[$segment], $segments, $value, $overwrite);
} elseif ($overwrite || ! Arr::exists($target, $segment)) {
$target[$segment] = $value;
}
} elseif (is_object($target)) {
if ($segments) {
if (! isset($target->{$segment})) {
$target->{$segment} = [];
}
data_set($target->{$segment}, $segments, $value, $overwrite);
} elseif ($overwrite || ! isset($target->{$segment})) {
$target->{$segment} = $value;
}
} else {
$target = [];
if ($segments) {
data_set($target[$segment], $segments, $value, $overwrite);
} elseif ($overwrite) {
$target[$segment] = $value;
}
}
return $target;
}
}
if (! function_exists('head')) {
/**
* Get the first element of an array. Useful for method chaining.
*
* @param array $array
* @return mixed
*/
function head($array)
{
return reset($array);
}
}
if (! function_exists('last')) {
/**
* Get the last element from an array.
*
* @param array $array
* @return mixed
*/
function last($array)
{
return end($array);
}
}
if (! function_exists('value')) {
/**
* Return the default value of the given value.
*
* @param mixed $value
* @return mixed
*/
function value($value, ...$args)
{
return $value instanceof Closure ? $value(...$args) : $value;
}
}

View File

@@ -99,7 +99,7 @@ class Repository implements ArrayAccess, ConfigContract
*/
public function prepend($key, $value)
{
$array = $this->get($key);
$array = $this->get($key, []);
array_unshift($array, $value);
@@ -115,7 +115,7 @@ class Repository implements ArrayAccess, ConfigContract
*/
public function push($key, $value)
{
$array = $this->get($key);
$array = $this->get($key, []);
$array[] = $value;
@@ -138,6 +138,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param string $key
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
{
return $this->has($key);
@@ -149,6 +150,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param string $key
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->get($key);
@@ -161,6 +163,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param mixed $value
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
$this->set($key, $value);
@@ -172,6 +175,7 @@ class Repository implements ArrayAccess, ConfigContract
* @param string $key
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
$this->set($key, null);

View File

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

View File

@@ -19,7 +19,6 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
@@ -77,6 +76,8 @@ class Application extends SymfonyApplication implements ApplicationContract
/**
* {@inheritdoc}
*
* @return int
*/
public function run(InputInterface $input = null, OutputInterface $output = null)
{
@@ -86,7 +87,7 @@ class Application extends SymfonyApplication implements ApplicationContract
$this->events->dispatch(
new CommandStarting(
$commandName, $input, $output = $output ?: new ConsoleOutput
$commandName, $input, $output = $output ?: new BufferedConsoleOutput
)
);
@@ -116,7 +117,7 @@ class Application extends SymfonyApplication implements ApplicationContract
*/
public static function artisanBinary()
{
return defined('ARTISAN_BINARY') ? ProcessUtils::escapeArgument(ARTISAN_BINARY) : 'artisan';
return ProcessUtils::escapeArgument(defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan');
}
/**
@@ -209,7 +210,7 @@ class Application extends SymfonyApplication implements ApplicationContract
$input = new ArrayInput($parameters);
}
return [$command, $input ?? null];
return [$command, $input];
}
/**

View File

@@ -0,0 +1,41 @@
<?php
namespace Illuminate\Console;
use Symfony\Component\Console\Output\ConsoleOutput;
class BufferedConsoleOutput extends ConsoleOutput
{
/**
* The current buffer.
*
* @var string
*/
protected $buffer = '';
/**
* Empties the buffer and returns its content.
*
* @return string
*/
public function fetch()
{
return tap($this->buffer, function () {
$this->buffer = '';
});
}
/**
* {@inheritdoc}
*/
protected function doWrite(string $message, bool $newline)
{
$this->buffer .= $message;
if ($newline) {
$this->buffer .= \PHP_EOL;
}
return parent::doWrite($message, $newline);
}
}

View File

@@ -38,14 +38,14 @@ class Command extends SymfonyCommand
/**
* The console command description.
*
* @var string|null
* @var string
*/
protected $description;
/**
* The console command help text.
*
* @var string|null
* @var string
*/
protected $help;
@@ -131,7 +131,9 @@ class Command extends SymfonyCommand
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
return (int) $this->laravel->call([$this, 'handle']);
$method = method_exists($this, 'handle') ? 'handle' : '__invoke';
return (int) $this->laravel->call([$this, $method]);
}
/**
@@ -161,6 +163,8 @@ class Command extends SymfonyCommand
/**
* {@inheritdoc}
*
* @return bool
*/
public function isHidden()
{
@@ -169,6 +173,8 @@ class Command extends SymfonyCommand
/**
* {@inheritdoc}
*
* @return static
*/
public function setHidden(bool $hidden)
{

View File

@@ -29,7 +29,7 @@ trait CallsCommands
}
/**
* Call another console command silently.
* Call another console command without output.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
@@ -40,6 +40,18 @@ trait CallsCommands
return $this->runCommand($command, $arguments, new NullOutput);
}
/**
* Call another console command without output.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
* @return int
*/
public function callSilently($command, array $arguments = [])
{
return $this->callSilent($command, $arguments);
}
/**
* Run the given the console command.
*

View File

@@ -0,0 +1,44 @@
<?php
namespace Illuminate\Console\Concerns;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputOption;
trait CreatesMatchingTest
{
/**
* Add the standard command options for generating matching tests.
*
* @return void
*/
protected function addTestOptions()
{
foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) {
$this->getDefinition()->addOption(new InputOption(
$option,
null,
InputOption::VALUE_NONE,
"Generate an accompanying {$name} test for the {$this->type}"
));
}
}
/**
* Create the matching test case if requested.
*
* @param string $path
* @return void
*/
protected function handleTestCreation($path)
{
if (! $this->option('test') && ! $this->option('pest')) {
return;
}
$this->call('make:test', [
'name' => Str::of($path)->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'),
'--pest' => $this->option('pest'),
]);
}
}

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Console\Concerns;
use Closure;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
@@ -237,6 +238,38 @@ trait InteractsWithIO
$table->render();
}
/**
* Execute a given callback while advancing a progress bar.
*
* @param iterable|int $totalSteps
* @param \Closure $callback
* @return mixed|void
*/
public function withProgressBar($totalSteps, Closure $callback)
{
$bar = $this->output->createProgressBar(
is_iterable($totalSteps) ? count($totalSteps) : $totalSteps
);
$bar->start();
if (is_iterable($totalSteps)) {
foreach ($totalSteps as $value) {
$callback($value, $bar);
$bar->advance();
}
} else {
$callback($bar);
}
$bar->finish();
if (is_iterable($totalSteps)) {
return $totalSteps;
}
}
/**
* Write a string as information output.
*
@@ -332,7 +365,18 @@ trait InteractsWithIO
$this->comment('* '.$string.' *');
$this->comment(str_repeat('*', $length));
$this->output->newLine();
$this->newLine();
}
/**
* Write a blank line.
*
* @param int $count
* @return void
*/
public function newLine($count = 1)
{
$this->output->newLine($count);
}
/**

View File

@@ -0,0 +1,26 @@
<?php
namespace Illuminate\Console\Events;
use Illuminate\Console\Scheduling\Event;
class ScheduledBackgroundTaskFinished
{
/**
* The scheduled event that ran.
*
* @var \Illuminate\Console\Scheduling\Event
*/
public $task;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\Event $task
* @return void
*/
public function __construct(Event $task)
{
$this->task = $task;
}
}

View File

@@ -26,6 +26,7 @@ class ScheduledTaskFailed
*
* @param \Illuminate\Console\Scheduling\Event $task
* @param \Throwable $exception
* @return void
*/
public function __construct(Event $task, Throwable $exception)
{

View File

@@ -2,6 +2,7 @@
namespace Illuminate\Console;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputArgument;
@@ -25,7 +26,7 @@ abstract class GeneratorCommand extends Command
/**
* Reserved names that cannot be used for generation.
*
* @var array
* @var string[]
*/
protected $reservedNames = [
'__halt_compiler',
@@ -108,6 +109,10 @@ abstract class GeneratorCommand extends Command
{
parent::__construct();
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
$this->addTestOptions();
}
$this->files = $files;
}
@@ -159,6 +164,10 @@ abstract class GeneratorCommand extends Command
$this->files->put($path, $this->sortImports($this->buildClass($name)));
$this->info($this->type.' created successfully.');
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
$this->handleTestCreation($path);
}
}
/**
@@ -171,19 +180,42 @@ abstract class GeneratorCommand extends Command
{
$name = ltrim($name, '\\/');
$name = str_replace('/', '\\', $name);
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($name, $rootNamespace)) {
return $name;
}
$name = str_replace('/', '\\', $name);
return $this->qualifyClass(
$this->getDefaultNamespace(trim($rootNamespace, '\\')).'\\'.$name
);
}
/**
* Qualify the given model class base name.
*
* @param string $model
* @return string
*/
protected function qualifyModel(string $model)
{
$model = ltrim($model, '\\/');
$model = str_replace('/', '\\', $model);
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($model, $rootNamespace)) {
return $model;
}
return is_dir(app_path('Models'))
? $rootNamespace.'Models\\'.$model
: $rootNamespace.$model;
}
/**
* Get the default namespace for the class.
*

View File

@@ -68,4 +68,14 @@ class OutputStyle extends SymfonyStyle
{
return $this->output->isDebug();
}
/**
* Get the underlying Symfony output implementation.
*
* @return \Symfony\Component\Console\Output\OutputInterface
*/
public function getOutput()
{
return $this->output;
}
}

View File

@@ -6,6 +6,7 @@ use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Reflector;
use InvalidArgumentException;
use LogicException;
use Throwable;
class CallbackEvent extends Event
{
@@ -77,6 +78,12 @@ class CallbackEvent extends Event
$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();

View File

@@ -52,7 +52,7 @@ class CommandBuilder
$finished = Application::formatCommandString('schedule:finish').' "'.$event->mutexName().'"';
if (windows_os()) {
return 'start /b cmd /c "('.$event->command.' & '.$finished.' "%errorlevel%")'.$redirect.$output.' 2>&1"';
return 'start /b cmd /v:on /c "('.$event->command.' & '.$finished.' ^!ERRORLEVEL^!)'.$redirect.$output.' 2>&1"';
}
return $this->ensureCorrectUser($event,

View File

@@ -10,13 +10,14 @@ use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Support\Arr;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Reflector;
use Illuminate\Support\Stringable;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\ReflectsClosures;
use Psr\Http\Client\ClientExceptionInterface;
use Symfony\Component\Process\Process;
use Throwable;
class Event
{
@@ -86,7 +87,7 @@ class Event
public $expiresAt = 1440;
/**
* Indicates if the command should run in background.
* Indicates if the command should run in the background.
*
* @var bool
*/
@@ -218,11 +219,17 @@ class Event
*/
protected function runCommandInForeground(Container $container)
{
$this->callBeforeCallbacks($container);
try {
$this->callBeforeCallbacks($container);
$this->exitCode = Process::fromShellCommandline($this->buildCommand(), base_path(), null, null, null)->run();
$this->exitCode = Process::fromShellCommandline(
$this->buildCommand(), base_path(), null, null, null
)->run();
$this->callAfterCallbacks($container);
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
}
}
/**
@@ -233,9 +240,15 @@ class Event
*/
protected function runCommandInBackground(Container $container)
{
$this->callBeforeCallbacks($container);
try {
$this->callBeforeCallbacks($container);
Process::fromShellCommandline($this->buildCommand(), base_path(), null, null, null)->run();
Process::fromShellCommandline($this->buildCommand(), base_path(), null, null, null)->run();
} catch (Throwable $exception) {
$this->removeMutex();
throw $exception;
}
}
/**
@@ -275,7 +288,11 @@ class Event
{
$this->exitCode = (int) $exitCode;
$this->callAfterCallbacks($container);
try {
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
}
}
/**
@@ -321,13 +338,13 @@ class Event
*/
protected function expressionPasses()
{
$date = Carbon::now();
$date = Date::now();
if ($this->timezone) {
$date->setTimezone($this->timezone);
$date = $date->setTimezone($this->timezone);
}
return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
return (new CronExpression($this->expression))->isDue($date->toDateTimeString());
}
/**
@@ -475,7 +492,7 @@ class Event
*/
protected function emailOutput(Mailer $mailer, $addresses, $onlyIfOutputExists = false)
{
$text = file_exists($this->output) ? file_get_contents($this->output) : '';
$text = is_file($this->output) ? file_get_contents($this->output) : '';
if ($onlyIfOutputExists && empty($text)) {
return;
@@ -586,7 +603,7 @@ class Event
}
/**
* State that the command should run in background.
* State that the command should run in the background.
*
* @return $this
*/
@@ -647,9 +664,7 @@ class Event
$this->expiresAt = $expiresAt;
return $this->then(function () {
$this->mutex->forget($this);
})->skip(function () {
return $this->skip(function () {
return $this->mutex->exists($this);
});
}
@@ -728,6 +743,12 @@ class Event
*/
public function then(Closure $callback)
{
$parameters = $this->closureParameterTypes($callback);
if (Arr::get($parameters, 'output') === Stringable::class) {
return $this->thenWithOutput($callback);
}
$this->afterCallbacks[] = $callback;
return $this;
@@ -755,6 +776,12 @@ class Event
*/
public function onSuccess(Closure $callback)
{
$parameters = $this->closureParameterTypes($callback);
if (Arr::get($parameters, 'output') === Stringable::class) {
return $this->onSuccessWithOutput($callback);
}
return $this->then(function (Container $container) use ($callback) {
if (0 === $this->exitCode) {
$container->call($callback);
@@ -784,6 +811,12 @@ class Event
*/
public function onFailure(Closure $callback)
{
$parameters = $this->closureParameterTypes($callback);
if (Arr::get($parameters, 'output') === Stringable::class) {
return $this->onFailureWithOutput($callback);
}
return $this->then(function (Container $container) use ($callback) {
if (0 !== $this->exitCode) {
$container->call($callback);
@@ -815,11 +848,11 @@ class Event
protected function withOutputCallback(Closure $callback, $onlyIfOutputExists = false)
{
return function (Container $container) use ($callback, $onlyIfOutputExists) {
$output = $this->output && file_exists($this->output) ? file_get_contents($this->output) : '';
$output = $this->output && is_file($this->output) ? file_get_contents($this->output) : '';
return $onlyIfOutputExists && empty($output)
? null
: $container->call($callback, ['output' => $output]);
: $container->call($callback, ['output' => new Stringable($output)]);
};
}
@@ -871,9 +904,8 @@ class Event
*/
public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return Date::instance(CronExpression::factory(
$this->getExpression()
)->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone));
return Date::instance((new CronExpression($this->getExpression()))
->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone));
}
/**
@@ -898,4 +930,16 @@ class Event
return $this;
}
/**
* Delete the mutex for the event.
*
* @return void
*/
protected function removeMutex()
{
if ($this->withoutOverlapping) {
$this->mutex->forget($this);
}
}
}

View File

@@ -262,10 +262,23 @@ trait ManagesFrequencies
* @return $this
*/
public function twiceDaily($first = 1, $second = 13)
{
return $this->twiceDailyAt($first, $second, 0);
}
/**
* Schedule the event to run twice daily at a given offset.
*
* @param int $first
* @param int $second
* @param int $offset
* @return $this
*/
public function twiceDailyAt($first = 1, $second = 13, $offset = 0)
{
$hours = $first.','.$second;
return $this->spliceIntoPosition(1, 0)
return $this->spliceIntoPosition(1, $offset)
->spliceIntoPosition(2, $hours);
}
@@ -276,7 +289,7 @@ trait ManagesFrequencies
*/
public function weekdays()
{
return $this->spliceIntoPosition(5, '1-5');
return $this->days(Schedule::MONDAY.'-'.Schedule::FRIDAY);
}
/**
@@ -286,7 +299,7 @@ trait ManagesFrequencies
*/
public function weekends()
{
return $this->spliceIntoPosition(5, '0,6');
return $this->days(Schedule::SATURDAY.','.Schedule::SUNDAY);
}
/**
@@ -296,7 +309,7 @@ trait ManagesFrequencies
*/
public function mondays()
{
return $this->days(1);
return $this->days(Schedule::MONDAY);
}
/**
@@ -306,7 +319,7 @@ trait ManagesFrequencies
*/
public function tuesdays()
{
return $this->days(2);
return $this->days(Schedule::TUESDAY);
}
/**
@@ -316,7 +329,7 @@ trait ManagesFrequencies
*/
public function wednesdays()
{
return $this->days(3);
return $this->days(Schedule::WEDNESDAY);
}
/**
@@ -326,7 +339,7 @@ trait ManagesFrequencies
*/
public function thursdays()
{
return $this->days(4);
return $this->days(Schedule::THURSDAY);
}
/**
@@ -336,7 +349,7 @@ trait ManagesFrequencies
*/
public function fridays()
{
return $this->days(5);
return $this->days(Schedule::FRIDAY);
}
/**
@@ -346,7 +359,7 @@ trait ManagesFrequencies
*/
public function saturdays()
{
return $this->days(6);
return $this->days(Schedule::SATURDAY);
}
/**
@@ -356,7 +369,7 @@ trait ManagesFrequencies
*/
public function sundays()
{
return $this->days(0);
return $this->days(Schedule::SUNDAY);
}
/**
@@ -374,15 +387,15 @@ trait ManagesFrequencies
/**
* Schedule the event to run weekly on a given day and time.
*
* @param int $day
* @param array|mixed $dayOfWeek
* @param string $time
* @return $this
*/
public function weeklyOn($day, $time = '0:0')
public function weeklyOn($dayOfWeek, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(5, $day);
return $this->days($dayOfWeek);
}
/**
@@ -400,15 +413,15 @@ trait ManagesFrequencies
/**
* Schedule the event to run monthly on a given day and time.
*
* @param int $day
* @param int $dayOfMonth
* @param string $time
* @return $this
*/
public function monthlyOn($day = 1, $time = '0:0')
public function monthlyOn($dayOfMonth = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $day);
return $this->spliceIntoPosition(3, $dayOfMonth);
}
/**
@@ -421,13 +434,11 @@ trait ManagesFrequencies
*/
public function twiceMonthly($first = 1, $second = 16, $time = '0:0')
{
$days = $first.','.$second;
$daysOfMonth = $first.','.$second;
$this->dailyAt($time);
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, $days);
return $this->spliceIntoPosition(3, $daysOfMonth);
}
/**
@@ -469,6 +480,22 @@ trait ManagesFrequencies
->spliceIntoPosition(4, 1);
}
/**
* Schedule the event to run yearly on a given month, day, and time.
*
* @param int $month
* @param int|string $dayOfMonth
* @param string $time
* @return $this
*/
public function yearlyOn($month = 1, $dayOfMonth = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $dayOfMonth)
->spliceIntoPosition(4, $month);
}
/**
* Set the days of the week the command should run on.
*

View File

@@ -4,10 +4,13 @@ namespace Illuminate\Console\Scheduling;
use Closure;
use DateTimeInterface;
use Illuminate\Bus\UniqueLock;
use Illuminate\Console\Application;
use Illuminate\Container\Container;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Support\ProcessUtils;
@@ -19,6 +22,14 @@ 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;
/**
* All of the events on the schedule.
*
@@ -59,6 +70,8 @@ class Schedule
*
* @param \DateTimeZone|string|null $timezone
* @return void
*
* @throws \RuntimeException
*/
public function __construct($timezone = null)
{
@@ -107,7 +120,11 @@ class Schedule
public function command($command, array $parameters = [])
{
if (class_exists($command)) {
$command = Container::getInstance()->make($command)->getName();
$command = Container::getInstance()->make($command);
return $this->exec(
Application::formatCommandString($command->getName()), $parameters,
)->description($command->getDescription());
}
return $this->exec(
@@ -143,6 +160,8 @@ class Schedule
* @param string|null $queue
* @param string|null $connection
* @return void
*
* @throws \RuntimeException
*/
protected function dispatchToQueue($job, $queue, $connection)
{
@@ -156,6 +175,35 @@ class Schedule
$job = CallQueuedClosure::create($job);
}
if ($job instanceof ShouldBeUnique) {
return $this->dispatchUniqueJobToQueue($job, $queue, $connection);
}
$this->getDispatcher()->dispatch(
$job->onConnection($connection)->onQueue($queue)
);
}
/**
* Dispatch the given unique job to the queue.
*
* @param object $job
* @param string|null $queue
* @param string|null $connection
* @return void
*
* @throws \RuntimeException
*/
protected function dispatchUniqueJobToQueue($job, $queue, $connection)
{
if (! Container::getInstance()->bound(Cache::class)) {
throw new RuntimeException('Cache driver not available. Scheduling unique jobs not supported.');
}
if (! (new UniqueLock(Container::getInstance()->make(Cache::class)))->acquire($job)) {
return;
}
$this->getDispatcher()->dispatch(
$job->onConnection($connection)->onQueue($queue)
);
@@ -293,6 +341,8 @@ class Schedule
* Get the job dispatcher, if available.
*
* @return \Illuminate\Contracts\Bus\Dispatcher
*
* @throws \RuntimeException
*/
protected function getDispatcher()
{

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