Initial commit

This commit is contained in:
2025-08-04 16:33:07 +03:30
commit f798e8e35c
9595 changed files with 1208683 additions and 0 deletions

View File

@@ -0,0 +1,321 @@
<?php
namespace Illuminate\Console;
use Closure;
use Illuminate\Console\Events\ArtisanStarting;
use Illuminate\Contracts\Console\Application as ApplicationContract;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\ProcessUtils;
use Symfony\Component\Console\Application as SymfonyApplication;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use function Illuminate\Support\php_binary;
class Application extends SymfonyApplication implements ApplicationContract
{
/**
* The Laravel application instance.
*
* @var \Illuminate\Contracts\Container\Container
*/
protected $laravel;
/**
* The event dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* The output from the previous command.
*
* @var \Symfony\Component\Console\Output\BufferedOutput
*/
protected $lastOutput;
/**
* The console application bootstrappers.
*
* @var array<array-key, \Closure($this): void>
*/
protected static $bootstrappers = [];
/**
* A map of command names to classes.
*
* @var array
*/
protected $commandMap = [];
/**
* Create a new Artisan console application.
*
* @param \Illuminate\Contracts\Container\Container $laravel
* @param \Illuminate\Contracts\Events\Dispatcher $events
* @param string $version
* @return void
*/
public function __construct(Container $laravel, Dispatcher $events, $version)
{
parent::__construct('Laravel Framework', $version);
$this->laravel = $laravel;
$this->events = $events;
$this->setAutoExit(false);
$this->setCatchExceptions(false);
$this->events->dispatch(new ArtisanStarting($this));
$this->bootstrap();
}
/**
* Determine the proper PHP executable.
*
* @return string
*/
public static function phpBinary()
{
return ProcessUtils::escapeArgument(php_binary());
}
/**
* Determine the proper Artisan executable.
*
* @return string
*/
public static function artisanBinary()
{
return ProcessUtils::escapeArgument(defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan');
}
/**
* Format the given command as a fully-qualified executable command.
*
* @param string $string
* @return string
*/
public static function formatCommandString($string)
{
return sprintf('%s %s %s', static::phpBinary(), static::artisanBinary(), $string);
}
/**
* Register a console "starting" bootstrapper.
*
* @param \Closure($this): void $callback
* @return void
*/
public static function starting(Closure $callback)
{
static::$bootstrappers[] = $callback;
}
/**
* Bootstrap the console application.
*
* @return void
*/
protected function bootstrap()
{
foreach (static::$bootstrappers as $bootstrapper) {
$bootstrapper($this);
}
}
/**
* Clear the console application bootstrappers.
*
* @return void
*/
public static function forgetBootstrappers()
{
static::$bootstrappers = [];
}
/**
* Run an Artisan console command by name.
*
* @param string $command
* @param array $parameters
* @param \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer
* @return int
*
* @throws \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function call($command, array $parameters = [], $outputBuffer = null)
{
[$command, $input] = $this->parseCommand($command, $parameters);
if (! $this->has($command)) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $command));
}
return $this->run(
$input, $this->lastOutput = $outputBuffer ?: new BufferedOutput
);
}
/**
* Parse the incoming Artisan command and its input.
*
* @param string $command
* @param array $parameters
* @return array
*/
protected function parseCommand($command, $parameters)
{
if (is_subclass_of($command, SymfonyCommand::class)) {
$callingClass = true;
$command = $this->laravel->make($command)->getName();
}
if (! isset($callingClass) && empty($parameters)) {
$command = $this->getCommandName($input = new StringInput($command));
} else {
array_unshift($parameters, $command);
$input = new ArrayInput($parameters);
}
return [$command, $input];
}
/**
* Get the output for the last run command.
*
* @return string
*/
public function output()
{
return $this->lastOutput && method_exists($this->lastOutput, 'fetch')
? $this->lastOutput->fetch()
: '';
}
/**
* Add a command to the console.
*
* @param \Symfony\Component\Console\Command\Command $command
* @return \Symfony\Component\Console\Command\Command|null
*/
#[\Override]
public function add(SymfonyCommand $command): ?SymfonyCommand
{
if ($command instanceof Command) {
$command->setLaravel($this->laravel);
}
return $this->addToParent($command);
}
/**
* Add the command to the parent instance.
*
* @param \Symfony\Component\Console\Command\Command $command
* @return \Symfony\Component\Console\Command\Command
*/
protected function addToParent(SymfonyCommand $command)
{
return parent::add($command);
}
/**
* Add a command, resolving through the application.
*
* @param \Illuminate\Console\Command|string $command
* @return \Symfony\Component\Console\Command\Command|null
*/
public function resolve($command)
{
if (is_subclass_of($command, SymfonyCommand::class) && ($commandName = $command::getDefaultName())) {
foreach (explode('|', $commandName) as $name) {
$this->commandMap[$name] = $command;
}
return null;
}
if ($command instanceof Command) {
return $this->add($command);
}
return $this->add($this->laravel->make($command));
}
/**
* Resolve an array of commands through the application.
*
* @param array|mixed $commands
* @return $this
*/
public function resolveCommands($commands)
{
$commands = is_array($commands) ? $commands : func_get_args();
foreach ($commands as $command) {
$this->resolve($command);
}
return $this;
}
/**
* Set the container command loader for lazy resolution.
*
* @return $this
*/
public function setContainerCommandLoader()
{
$this->setCommandLoader(new ContainerCommandLoader($this->laravel, $this->commandMap));
return $this;
}
/**
* Get the default input definition for the application.
*
* This is used to add the --env option to every available command.
*
* @return \Symfony\Component\Console\Input\InputDefinition
*/
#[\Override]
protected function getDefaultInputDefinition(): InputDefinition
{
return tap(parent::getDefaultInputDefinition(), function ($definition) {
$definition->addOption($this->getEnvironmentOption());
});
}
/**
* Get the global environment option for the definition.
*
* @return \Symfony\Component\Console\Input\InputOption
*/
protected function getEnvironmentOption()
{
$message = 'The environment the command should run under';
return new InputOption('--env', null, InputOption::VALUE_OPTIONAL, $message);
}
/**
* Get the Laravel application instance.
*
* @return \Illuminate\Contracts\Foundation\Application
*/
public function getLaravel()
{
return $this->laravel;
}
}

View File

@@ -0,0 +1,42 @@
<?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}
*/
#[\Override]
protected function doWrite(string $message, bool $newline): void
{
$this->buffer .= $message;
if ($newline) {
$this->buffer .= \PHP_EOL;
}
parent::doWrite($message, $newline);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Illuminate\Console;
use Carbon\CarbonInterval;
use Illuminate\Cache\DynamoDbStore;
use Illuminate\Contracts\Cache\Factory as Cache;
use Illuminate\Contracts\Cache\LockProvider;
use Illuminate\Support\InteractsWithTime;
class CacheCommandMutex implements CommandMutex
{
use InteractsWithTime;
/**
* The cache factory implementation.
*
* @var \Illuminate\Contracts\Cache\Factory
*/
public $cache;
/**
* The cache store that should be used.
*
* @var string|null
*/
public $store = null;
/**
* Create a new command mutex.
*
* @param \Illuminate\Contracts\Cache\Factory $cache
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Attempt to obtain a command mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function create($command)
{
$store = $this->cache->store($this->store);
$expiresAt = method_exists($command, 'isolationLockExpiresAt')
? $command->isolationLockExpiresAt()
: CarbonInterval::hour();
if ($this->shouldUseLocks($store->getStore())) {
return $store->getStore()->lock(
$this->commandMutexName($command),
$this->secondsUntil($expiresAt)
)->get();
}
return $store->add($this->commandMutexName($command), true, $expiresAt);
}
/**
* Determine if a command mutex exists for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function exists($command)
{
$store = $this->cache->store($this->store);
if ($this->shouldUseLocks($store->getStore())) {
$lock = $store->getStore()->lock($this->commandMutexName($command));
return tap(! $lock->get(), function ($exists) use ($lock) {
if ($exists) {
$lock->release();
}
});
}
return $this->cache->store($this->store)->has($this->commandMutexName($command));
}
/**
* Release the mutex for the given command.
*
* @param \Illuminate\Console\Command $command
* @return bool
*/
public function forget($command)
{
$store = $this->cache->store($this->store);
if ($this->shouldUseLocks($store->getStore())) {
return $store->getStore()->lock($this->commandMutexName($command))->forceRelease();
}
return $this->cache->store($this->store)->forget($this->commandMutexName($command));
}
/**
* Get the isolatable command mutex name.
*
* @param \Illuminate\Console\Command $command
* @return string
*/
protected function commandMutexName($command)
{
$baseName = 'framework'.DIRECTORY_SEPARATOR.'command-'.$command->getName();
return method_exists($command, 'isolatableId')
? $baseName.'-'.$command->isolatableId()
: $baseName;
}
/**
* Specify the cache store that should be used.
*
* @param string|null $store
* @return $this
*/
public function useStore($store)
{
$this->store = $store;
return $this;
}
/**
* Determine if the given store should use locks for command mutexes.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @return bool
*/
protected function shouldUseLocks($store)
{
return $store instanceof LockProvider && ! $store instanceof DynamoDbStore;
}
}

View File

@@ -0,0 +1,327 @@
<?php
namespace Illuminate\Console;
use Illuminate\Console\View\Components\Factory;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
class Command extends SymfonyCommand
{
use Concerns\CallsCommands,
Concerns\ConfiguresPrompts,
Concerns\HasParameters,
Concerns\InteractsWithIO,
Concerns\InteractsWithSignals,
Concerns\PromptsForMissingInput,
Macroable;
/**
* The Laravel application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $laravel;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature;
/**
* The console command name.
*
* @var string
*/
protected $name;
/**
* The console command description.
*
* @var string|null
*/
protected $description;
/**
* The console command help text.
*
* @var string
*/
protected $help;
/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = false;
/**
* Indicates whether only one instance of the command can run at any given time.
*
* @var bool
*/
protected $isolated = false;
/**
* The default exit code for isolated commands.
*
* @var int
*/
protected $isolatedExitCode = self::SUCCESS;
/**
* The console command name aliases.
*
* @var array
*/
protected $aliases;
/**
* Create a new console command instance.
*
* @return void
*/
public function __construct()
{
// We will go ahead and set the name, description, and parameters on console
// commands just to make things a little easier on the developer. This is
// so they don't have to all be manually specified in the constructors.
if (isset($this->signature)) {
$this->configureUsingFluentDefinition();
} else {
parent::__construct($this->name);
}
// Once we have constructed the command, we'll set the description and other
// related properties of the command. If a signature wasn't used to build
// the command we'll set the arguments and the options on this command.
if (! isset($this->description)) {
$this->setDescription((string) static::getDefaultDescription());
} else {
$this->setDescription((string) $this->description);
}
$this->setHelp((string) $this->help);
$this->setHidden($this->isHidden());
if (isset($this->aliases)) {
$this->setAliases((array) $this->aliases);
}
if (! isset($this->signature)) {
$this->specifyParameters();
}
if ($this instanceof Isolatable) {
$this->configureIsolation();
}
}
/**
* Configure the console command using a fluent definition.
*
* @return void
*/
protected function configureUsingFluentDefinition()
{
[$name, $arguments, $options] = Parser::parse($this->signature);
parent::__construct($this->name = $name);
// After parsing the signature we will spin through the arguments and options
// and set them on this command. These will already be changed into proper
// instances of these "InputArgument" and "InputOption" Symfony classes.
$this->getDefinition()->addArguments($arguments);
$this->getDefinition()->addOptions($options);
}
/**
* Configure the console command for isolation.
*
* @return void
*/
protected function configureIsolation()
{
$this->getDefinition()->addOption(new InputOption(
'isolated',
null,
InputOption::VALUE_OPTIONAL,
'Do not run the command if another instance of the command is already running',
$this->isolated
));
}
/**
* Run the console command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
#[\Override]
public function run(InputInterface $input, OutputInterface $output): int
{
$this->output = $output instanceof OutputStyle ? $output : $this->laravel->make(
OutputStyle::class, ['input' => $input, 'output' => $output]
);
$this->components = $this->laravel->make(Factory::class, ['output' => $this->output]);
$this->configurePrompts($input);
try {
return parent::run(
$this->input = $input, $this->output
);
} finally {
$this->untrap();
}
}
/**
* Execute the console command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
*/
#[\Override]
protected function execute(InputInterface $input, OutputInterface $output): int
{
if ($this instanceof Isolatable && $this->option('isolated') !== false &&
! $this->commandIsolationMutex()->create($this)) {
$this->comment(sprintf(
'The [%s] command is already running.', $this->getName()
));
return (int) (is_numeric($this->option('isolated'))
? $this->option('isolated')
: $this->isolatedExitCode);
}
$method = method_exists($this, 'handle') ? 'handle' : '__invoke';
try {
return (int) $this->laravel->call([$this, $method]);
} catch (ManuallyFailedException $e) {
$this->components->error($e->getMessage());
return static::FAILURE;
} finally {
if ($this instanceof Isolatable && $this->option('isolated') !== false) {
$this->commandIsolationMutex()->forget($this);
}
}
}
/**
* Get a command isolation mutex instance for the command.
*
* @return \Illuminate\Console\CommandMutex
*/
protected function commandIsolationMutex()
{
return $this->laravel->bound(CommandMutex::class)
? $this->laravel->make(CommandMutex::class)
: $this->laravel->make(CacheCommandMutex::class);
}
/**
* Resolve the console command instance for the given command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @return \Symfony\Component\Console\Command\Command
*/
protected function resolveCommand($command)
{
if (is_string($command)) {
if (! class_exists($command)) {
return $this->getApplication()->find($command);
}
$command = $this->laravel->make($command);
}
if ($command instanceof SymfonyCommand) {
$command->setApplication($this->getApplication());
}
if ($command instanceof self) {
$command->setLaravel($this->getLaravel());
}
return $command;
}
/**
* Fail the command manually.
*
* @param \Throwable|string|null $exception
* @return void
*
* @throws \Illuminate\Console\ManuallyFailedException|\Throwable
*/
public function fail(Throwable|string|null $exception = null)
{
if (is_null($exception)) {
$exception = 'Command failed manually.';
}
if (is_string($exception)) {
$exception = new ManuallyFailedException($exception);
}
throw $exception;
}
/**
* {@inheritdoc}
*
* @return bool
*/
#[\Override]
public function isHidden(): bool
{
return $this->hidden;
}
/**
* {@inheritdoc}
*/
#[\Override]
public function setHidden(bool $hidden = true): static
{
parent::setHidden($this->hidden = $hidden);
return $this;
}
/**
* Get the Laravel application instance.
*
* @return \Illuminate\Contracts\Foundation\Application
*/
public function getLaravel()
{
return $this->laravel;
}
/**
* Set the Laravel application instance.
*
* @param \Illuminate\Contracts\Container\Container $laravel
* @return void
*/
public function setLaravel($laravel)
{
$this->laravel = $laravel;
}
}

View File

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

View File

@@ -0,0 +1,111 @@
<?php
namespace Illuminate\Console\Concerns;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
trait CallsCommands
{
/**
* Resolve the console command instance for the given command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @return \Symfony\Component\Console\Command\Command
*/
abstract protected function resolveCommand($command);
/**
* Call another console command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
* @return int
*/
public function call($command, array $arguments = [])
{
return $this->runCommand($command, $arguments, $this->output);
}
/**
* Call another console command without output.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
* @return int
*/
public function callSilent($command, array $arguments = [])
{
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 console command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @param array $arguments
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
protected function runCommand($command, array $arguments, OutputInterface $output)
{
$arguments['command'] = $command;
$result = $this->resolveCommand($command)->run(
$this->createInputFromArguments($arguments), $output
);
$this->restorePrompts();
return $result;
}
/**
* Create an input instance from the given arguments.
*
* @param array $arguments
* @return \Symfony\Component\Console\Input\ArrayInput
*/
protected function createInputFromArguments(array $arguments)
{
return tap(new ArrayInput(array_merge($this->context(), $arguments)), function ($input) {
if ($input->getParameterOption('--no-interaction')) {
$input->setInteractive(false);
}
});
}
/**
* Get all of the context passed to the command.
*
* @return array
*/
protected function context()
{
return (new Collection($this->option()))
->only([
'ansi',
'no-ansi',
'no-interaction',
'quiet',
'verbose',
])
->filter()
->mapWithKeys(fn ($value, $key) => ["--{$key}" => $value])
->all();
}
}

View File

@@ -0,0 +1,290 @@
<?php
namespace Illuminate\Console\Concerns;
use Illuminate\Console\PromptValidationException;
use Laravel\Prompts\ConfirmPrompt;
use Laravel\Prompts\MultiSearchPrompt;
use Laravel\Prompts\MultiSelectPrompt;
use Laravel\Prompts\PasswordPrompt;
use Laravel\Prompts\PausePrompt;
use Laravel\Prompts\Prompt;
use Laravel\Prompts\SearchPrompt;
use Laravel\Prompts\SelectPrompt;
use Laravel\Prompts\SuggestPrompt;
use Laravel\Prompts\TextareaPrompt;
use Laravel\Prompts\TextPrompt;
use stdClass;
use Symfony\Component\Console\Input\InputInterface;
trait ConfiguresPrompts
{
/**
* Configure the prompt fallbacks.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return void
*/
protected function configurePrompts(InputInterface $input)
{
Prompt::setOutput($this->output);
Prompt::interactive(($input->isInteractive() && defined('STDIN') && stream_isatty(STDIN)) || $this->laravel->runningUnitTests());
Prompt::validateUsing(fn (Prompt $prompt) => $this->validatePrompt($prompt->value(), $prompt->validate));
Prompt::fallbackWhen(windows_os() || $this->laravel->runningUnitTests());
TextPrompt::fallbackUsing(fn (TextPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->ask($prompt->label, $prompt->default ?: null) ?? '',
$prompt->required,
$prompt->validate
));
TextareaPrompt::fallbackUsing(fn (TextareaPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->ask($prompt->label, $prompt->default ?: null, multiline: true) ?? '',
$prompt->required,
$prompt->validate
));
PasswordPrompt::fallbackUsing(fn (PasswordPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->secret($prompt->label) ?? '',
$prompt->required,
$prompt->validate
));
PausePrompt::fallbackUsing(fn (PausePrompt $prompt) => $this->promptUntilValid(
function () use ($prompt) {
$this->components->ask($prompt->message, $prompt->value());
return $prompt->value();
},
$prompt->required,
$prompt->validate
));
ConfirmPrompt::fallbackUsing(fn (ConfirmPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->confirm($prompt->label, $prompt->default),
$prompt->required,
$prompt->validate
));
SelectPrompt::fallbackUsing(fn (SelectPrompt $prompt) => $this->promptUntilValid(
fn () => $this->selectFallback($prompt->label, $prompt->options, $prompt->default),
false,
$prompt->validate
));
MultiSelectPrompt::fallbackUsing(fn (MultiSelectPrompt $prompt) => $this->promptUntilValid(
fn () => $this->multiselectFallback($prompt->label, $prompt->options, $prompt->default, $prompt->required),
$prompt->required,
$prompt->validate
));
SuggestPrompt::fallbackUsing(fn (SuggestPrompt $prompt) => $this->promptUntilValid(
fn () => $this->components->askWithCompletion($prompt->label, $prompt->options, $prompt->default ?: null) ?? '',
$prompt->required,
$prompt->validate
));
SearchPrompt::fallbackUsing(fn (SearchPrompt $prompt) => $this->promptUntilValid(
function () use ($prompt) {
$query = $this->components->ask($prompt->label);
$options = ($prompt->options)($query);
return $this->selectFallback($prompt->label, $options);
},
false,
$prompt->validate
));
MultiSearchPrompt::fallbackUsing(fn (MultiSearchPrompt $prompt) => $this->promptUntilValid(
function () use ($prompt) {
$query = $this->components->ask($prompt->label);
$options = ($prompt->options)($query);
return $this->multiselectFallback($prompt->label, $options, required: $prompt->required);
},
$prompt->required,
$prompt->validate
));
}
/**
* Prompt the user until the given validation callback passes.
*
* @param \Closure $prompt
* @param bool|string $required
* @param \Closure|null $validate
* @return mixed
*/
protected function promptUntilValid($prompt, $required, $validate)
{
while (true) {
$result = $prompt();
if ($required && ($result === '' || $result === [] || $result === false)) {
$this->components->error(is_string($required) ? $required : 'Required.');
if ($this->laravel->runningUnitTests()) {
throw new PromptValidationException;
} else {
continue;
}
}
$error = is_callable($validate) ? $validate($result) : $this->validatePrompt($result, $validate);
if (is_string($error) && strlen($error) > 0) {
$this->components->error($error);
if ($this->laravel->runningUnitTests()) {
throw new PromptValidationException;
} else {
continue;
}
}
return $result;
}
}
/**
* Validate the given prompt value using the validator.
*
* @param mixed $value
* @param mixed $rules
* @return ?string
*/
protected function validatePrompt($value, $rules)
{
if ($rules instanceof stdClass) {
$messages = $rules->messages ?? [];
$attributes = $rules->attributes ?? [];
$rules = $rules->rules ?? null;
}
if (! $rules) {
return;
}
$field = 'answer';
if (is_array($rules) && ! array_is_list($rules)) {
[$field, $rules] = [key($rules), current($rules)];
}
return $this->getPromptValidatorInstance(
$field, $value, $rules, $messages ?? [], $attributes ?? []
)->errors()->first();
}
/**
* Get the validator instance that should be used to validate prompts.
*
* @param mixed $field
* @param mixed $value
* @param mixed $rules
* @param array $messages
* @param array $attributes
* @return \Illuminate\Validation\Validator
*/
protected function getPromptValidatorInstance($field, $value, $rules, array $messages = [], array $attributes = [])
{
return $this->laravel['validator']->make(
[$field => $value],
[$field => $rules],
empty($messages) ? $this->validationMessages() : $messages,
empty($attributes) ? $this->validationAttributes() : $attributes,
);
}
/**
* Get the validation messages that should be used during prompt validation.
*
* @return array
*/
protected function validationMessages()
{
return [];
}
/**
* Get the validation attributes that should be used during prompt validation.
*
* @return array
*/
protected function validationAttributes()
{
return [];
}
/**
* Restore the prompts output.
*
* @return void
*/
protected function restorePrompts()
{
Prompt::setOutput($this->output);
}
/**
* Select fallback.
*
* @param string $label
* @param array $options
* @param string|int|null $default
* @return string|int
*/
private function selectFallback($label, $options, $default = null)
{
$answer = $this->components->choice($label, $options, $default);
if (! array_is_list($options) && $answer === (string) (int) $answer) {
return (int) $answer;
}
return $answer;
}
/**
* Multi-select fallback.
*
* @param string $label
* @param array $options
* @param array $default
* @param bool|string $required
* @return array
*/
private function multiselectFallback($label, $options, $default = [], $required = false)
{
$default = $default !== [] ? implode(',', $default) : null;
if ($required === false && ! $this->laravel->runningUnitTests()) {
$options = array_is_list($options)
? ['None', ...$options]
: ['' => 'None'] + $options;
if ($default === null) {
$default = 'None';
}
}
$answers = $this->components->choice($label, $options, $default, null, true);
if (! array_is_list($options)) {
$answers = array_map(fn ($value) => $value === (string) (int) $value ? (int) $value : $value, $answers);
}
if ($required === false) {
return array_is_list($options)
? array_values(array_filter($answers, fn ($value) => $value !== 'None'))
: array_filter($answers, fn ($value) => $value !== '');
}
return $answers;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Console\Concerns;
use Illuminate\Support\Stringable;
use Symfony\Component\Console\Input\InputOption;
trait CreatesMatchingTest
{
/**
* Add the standard command options for generating matching tests.
*
* @return void
*/
protected function addTestOptions()
{
foreach (['test' => 'Test', 'pest' => 'Pest', 'phpunit' => 'PHPUnit'] 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 bool
*/
protected function handleTestCreation($path)
{
if (! $this->option('test') && ! $this->option('pest') && ! $this->option('phpunit')) {
return false;
}
return $this->call('make:test', [
'name' => (new Stringable($path))->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'),
'--pest' => $this->option('pest'),
'--phpunit' => $this->option('phpunit'),
]) == 0;
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Illuminate\Console\Concerns;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
trait HasParameters
{
/**
* Specify the arguments and options on the command.
*
* @return void
*/
protected function specifyParameters()
{
// We will loop through all of the arguments and options for the command and
// set them all on the base command instance. This specifies what can get
// passed into these commands as "parameters" to control the execution.
foreach ($this->getArguments() as $arguments) {
if ($arguments instanceof InputArgument) {
$this->getDefinition()->addArgument($arguments);
} else {
$this->addArgument(...$arguments);
}
}
foreach ($this->getOptions() as $options) {
if ($options instanceof InputOption) {
$this->getDefinition()->addOption($options);
} else {
$this->addOption(...$options);
}
}
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [];
}
}

View File

@@ -0,0 +1,463 @@
<?php
namespace Illuminate\Console\Concerns;
use Closure;
use Illuminate\Console\OutputStyle;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Question\Question;
trait InteractsWithIO
{
/**
* The console components factory.
*
* @var \Illuminate\Console\View\Components\Factory
*
* @internal This property is not meant to be used or overwritten outside the framework.
*/
protected $components;
/**
* The input interface implementation.
*
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* The output interface implementation.
*
* @var \Illuminate\Console\OutputStyle
*/
protected $output;
/**
* The default verbosity of output commands.
*
* @var int
*/
protected $verbosity = OutputInterface::VERBOSITY_NORMAL;
/**
* The mapping between human readable verbosity levels and Symfony's OutputInterface.
*
* @var array
*/
protected $verbosityMap = [
'v' => OutputInterface::VERBOSITY_VERBOSE,
'vv' => OutputInterface::VERBOSITY_VERY_VERBOSE,
'vvv' => OutputInterface::VERBOSITY_DEBUG,
'quiet' => OutputInterface::VERBOSITY_QUIET,
'normal' => OutputInterface::VERBOSITY_NORMAL,
];
/**
* Determine if the given argument is present.
*
* @param string|int $name
* @return bool
*/
public function hasArgument($name)
{
return $this->input->hasArgument($name);
}
/**
* Get the value of a command argument.
*
* @param string|null $key
* @return array|string|bool|null
*/
public function argument($key = null)
{
if (is_null($key)) {
return $this->input->getArguments();
}
return $this->input->getArgument($key);
}
/**
* Get all of the arguments passed to the command.
*
* @return array
*/
public function arguments()
{
return $this->argument();
}
/**
* Determine if the given option is present.
*
* @param string $name
* @return bool
*/
public function hasOption($name)
{
return $this->input->hasOption($name);
}
/**
* Get the value of a command option.
*
* @param string|null $key
* @return string|array|bool|null
*/
public function option($key = null)
{
if (is_null($key)) {
return $this->input->getOptions();
}
return $this->input->getOption($key);
}
/**
* Get all of the options passed to the command.
*
* @return array
*/
public function options()
{
return $this->option();
}
/**
* Confirm a question with the user.
*
* @param string $question
* @param bool $default
* @return bool
*/
public function confirm($question, $default = false)
{
return $this->output->confirm($question, $default);
}
/**
* Prompt the user for input.
*
* @param string $question
* @param string|null $default
* @return mixed
*/
public function ask($question, $default = null)
{
return $this->output->ask($question, $default);
}
/**
* Prompt the user for input with auto completion.
*
* @param string $question
* @param array|callable $choices
* @param string|null $default
* @return mixed
*/
public function anticipate($question, $choices, $default = null)
{
return $this->askWithCompletion($question, $choices, $default);
}
/**
* Prompt the user for input with auto completion.
*
* @param string $question
* @param array|callable $choices
* @param string|null $default
* @return mixed
*/
public function askWithCompletion($question, $choices, $default = null)
{
$question = new Question($question, $default);
is_callable($choices)
? $question->setAutocompleterCallback($choices)
: $question->setAutocompleterValues($choices);
return $this->output->askQuestion($question);
}
/**
* Prompt the user for input but hide the answer from the console.
*
* @param string $question
* @param bool $fallback
* @return mixed
*/
public function secret($question, $fallback = true)
{
$question = new Question($question);
$question->setHidden(true)->setHiddenFallback($fallback);
return $this->output->askQuestion($question);
}
/**
* Give the user a single choice from an array of answers.
*
* @param string $question
* @param array $choices
* @param string|int|null $default
* @param mixed|null $attempts
* @param bool $multiple
* @return string|array
*/
public function choice($question, array $choices, $default = null, $attempts = null, $multiple = false)
{
$question = new ChoiceQuestion($question, $choices, $default);
$question->setMaxAttempts($attempts)->setMultiselect($multiple);
return $this->output->askQuestion($question);
}
/**
* Format input to textual table.
*
* @param array $headers
* @param \Illuminate\Contracts\Support\Arrayable|array $rows
* @param \Symfony\Component\Console\Helper\TableStyle|string $tableStyle
* @param array $columnStyles
* @return void
*/
public function table($headers, $rows, $tableStyle = 'default', array $columnStyles = [])
{
$table = new Table($this->output);
if ($rows instanceof Arrayable) {
$rows = $rows->toArray();
}
$table->setHeaders((array) $headers)->setRows($rows)->setStyle($tableStyle);
foreach ($columnStyles as $columnIndex => $columnStyle) {
$table->setColumnStyle($columnIndex, $columnStyle);
}
$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 $key => $value) {
$callback($value, $bar, $key);
$bar->advance();
}
} else {
$callback($bar);
}
$bar->finish();
if (is_iterable($totalSteps)) {
return $totalSteps;
}
}
/**
* Write a string as information output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function info($string, $verbosity = null)
{
$this->line($string, 'info', $verbosity);
}
/**
* Write a string as standard output.
*
* @param string $string
* @param string|null $style
* @param int|string|null $verbosity
* @return void
*/
public function line($string, $style = null, $verbosity = null)
{
$styled = $style ? "<$style>$string</$style>" : $string;
$this->output->writeln($styled, $this->parseVerbosity($verbosity));
}
/**
* Write a string as comment output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function comment($string, $verbosity = null)
{
$this->line($string, 'comment', $verbosity);
}
/**
* Write a string as question output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function question($string, $verbosity = null)
{
$this->line($string, 'question', $verbosity);
}
/**
* Write a string as error output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function error($string, $verbosity = null)
{
$this->line($string, 'error', $verbosity);
}
/**
* Write a string as warning output.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function warn($string, $verbosity = null)
{
if (! $this->output->getFormatter()->hasStyle('warning')) {
$style = new OutputFormatterStyle('yellow');
$this->output->getFormatter()->setStyle('warning', $style);
}
$this->line($string, 'warning', $verbosity);
}
/**
* Write a string in an alert box.
*
* @param string $string
* @param int|string|null $verbosity
* @return void
*/
public function alert($string, $verbosity = null)
{
$length = Str::length(strip_tags($string)) + 12;
$this->comment(str_repeat('*', $length), $verbosity);
$this->comment('* '.$string.' *', $verbosity);
$this->comment(str_repeat('*', $length), $verbosity);
$this->comment('', $verbosity);
}
/**
* Write a blank line.
*
* @param int $count
* @return $this
*/
public function newLine($count = 1)
{
$this->output->newLine($count);
return $this;
}
/**
* Set the input interface implementation.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return void
*/
public function setInput(InputInterface $input)
{
$this->input = $input;
}
/**
* Set the output interface implementation.
*
* @param \Illuminate\Console\OutputStyle $output
* @return void
*/
public function setOutput(OutputStyle $output)
{
$this->output = $output;
}
/**
* Set the verbosity level.
*
* @param string|int $level
* @return void
*/
protected function setVerbosity($level)
{
$this->verbosity = $this->parseVerbosity($level);
}
/**
* Get the verbosity level in terms of Symfony's OutputInterface level.
*
* @param string|int|null $level
* @return int
*/
protected function parseVerbosity($level = null)
{
if (isset($this->verbosityMap[$level])) {
$level = $this->verbosityMap[$level];
} elseif (! is_int($level)) {
$level = $this->verbosity;
}
return $level;
}
/**
* Get the output implementation.
*
* @return \Illuminate\Console\OutputStyle
*/
public function getOutput()
{
return $this->output;
}
/**
* Get the output component factory implementation.
*
* @return \Illuminate\Console\View\Components\Factory
*/
public function outputComponents()
{
return $this->components;
}
}

View File

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

View File

@@ -0,0 +1,109 @@
<?php
namespace Illuminate\Console\Concerns;
use Closure;
use Illuminate\Contracts\Console\PromptsForMissingInput as PromptsForMissingInputContract;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function Laravel\Prompts\text;
trait PromptsForMissingInput
{
/**
* Interact with the user before validating the input.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function interact(InputInterface $input, OutputInterface $output)
{
parent::interact($input, $output);
if ($this instanceof PromptsForMissingInputContract) {
$this->promptForMissingArguments($input, $output);
}
}
/**
* Prompt the user for any missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function promptForMissingArguments(InputInterface $input, OutputInterface $output)
{
$prompted = (new Collection($this->getDefinition()->getArguments()))
->reject(fn (InputArgument $argument) => $argument->getName() === 'command')
->filter(fn (InputArgument $argument) => $argument->isRequired() && match (true) {
$argument->isArray() => empty($input->getArgument($argument->getName())),
default => is_null($input->getArgument($argument->getName())),
})
->each(function (InputArgument $argument) use ($input) {
$label = $this->promptForMissingArgumentsUsing()[$argument->getName()] ??
'What is '.lcfirst($argument->getDescription() ?: ('the '.$argument->getName())).'?';
if ($label instanceof Closure) {
return $input->setArgument($argument->getName(), $argument->isArray() ? Arr::wrap($label()) : $label());
}
if (is_array($label)) {
[$label, $placeholder] = $label;
}
$answer = text(
label: $label,
placeholder: $placeholder ?? '',
validate: fn ($value) => empty($value) ? "The {$argument->getName()} is required." : null,
);
$input->setArgument($argument->getName(), $argument->isArray() ? [$answer] : $answer);
})
->isNotEmpty();
if ($prompted) {
$this->afterPromptingForMissingArguments($input, $output);
}
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [];
}
/**
* Perform actions after the user was prompted for missing arguments.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output)
{
//
}
/**
* Whether the input contains any options that differ from the default values.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @return bool
*/
protected function didReceiveOptions(InputInterface $input)
{
return (new Collection($this->getDefinition()->getOptions()))
->reject(fn ($option) => $input->getOption($option->getName()) === $option->getDefault())
->isNotEmpty();
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Illuminate\Console;
use function Laravel\Prompts\confirm;
trait ConfirmableTrait
{
/**
* Confirm before proceeding with the action.
*
* This method only asks for confirmation in production.
*
* @param string $warning
* @param \Closure|bool|null $callback
* @return bool
*/
public function confirmToProceed($warning = 'Application In Production', $callback = null)
{
$callback = is_null($callback) ? $this->getDefaultConfirmCallback() : $callback;
$shouldConfirm = value($callback);
if ($shouldConfirm) {
if ($this->hasOption('force') && $this->option('force')) {
return true;
}
$this->components->alert($warning);
$confirmed = confirm('Are you sure you want to run this command?', default: false);
if (! $confirmed) {
$this->components->warn('Command cancelled.');
return false;
}
}
return true;
}
/**
* Get the default confirmation callback.
*
* @return \Closure
*/
protected function getDefaultConfirmCallback()
{
return function () {
return $this->getLaravel()->environment() === 'production';
};
}
}

View File

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

View File

@@ -0,0 +1,22 @@
<?php
namespace Illuminate\Console\Contracts;
interface NewLineAware
{
/**
* How many trailing newlines were written.
*
* @return int
*/
public function newLinesWritten();
/**
* Whether a newline has already been written.
*
* @return bool
*
* @deprecated use newLinesWritten
*/
public function newLineWritten();
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Console\Events;
class ArtisanStarting
{
/**
* The Artisan application instance.
*
* @var \Illuminate\Console\Application
*/
public $artisan;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Application $artisan
* @return void
*/
public function __construct($artisan)
{
$this->artisan = $artisan;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Illuminate\Console\Events;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CommandFinished
{
/**
* The command name.
*
* @var string
*/
public $command;
/**
* The console input implementation.
*
* @var \Symfony\Component\Console\Input\InputInterface|null
*/
public $input;
/**
* The command output implementation.
*
* @var \Symfony\Component\Console\Output\OutputInterface|null
*/
public $output;
/**
* The command exit code.
*
* @var int
*/
public $exitCode;
/**
* Create a new event instance.
*
* @param string $command
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @param int $exitCode
* @return void
*/
public function __construct($command, InputInterface $input, OutputInterface $output, $exitCode)
{
$this->input = $input;
$this->output = $output;
$this->command = $command;
$this->exitCode = $exitCode;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Illuminate\Console\Events;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class CommandStarting
{
/**
* The command name.
*
* @var string
*/
public $command;
/**
* The console input implementation.
*
* @var \Symfony\Component\Console\Input\InputInterface|null
*/
public $input;
/**
* The command output implementation.
*
* @var \Symfony\Component\Console\Output\OutputInterface|null
*/
public $output;
/**
* Create a new event instance.
*
* @param string $command
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct($command, InputInterface $input, OutputInterface $output)
{
$this->input = $input;
$this->output = $output;
$this->command = $command;
}
}

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

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

View File

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

View File

@@ -0,0 +1,26 @@
<?php
namespace Illuminate\Console\Events;
use Illuminate\Console\Scheduling\Event;
class ScheduledTaskSkipped
{
/**
* The scheduled event being run.
*
* @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

@@ -0,0 +1,26 @@
<?php
namespace Illuminate\Console\Events;
use Illuminate\Console\Scheduling\Event;
class ScheduledTaskStarting
{
/**
* The scheduled event being run.
*
* @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

@@ -0,0 +1,534 @@
<?php
namespace Illuminate\Console;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Contracts\Console\PromptsForMissingInput;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Finder\Finder;
abstract class GeneratorCommand extends Command implements PromptsForMissingInput
{
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* The type of class being generated.
*
* @var string
*/
protected $type;
/**
* Reserved names that cannot be used for generation.
*
* @var string[]
*/
protected $reservedNames = [
'__halt_compiler',
'abstract',
'and',
'array',
'as',
'break',
'callable',
'case',
'catch',
'class',
'clone',
'const',
'continue',
'declare',
'default',
'die',
'do',
'echo',
'else',
'elseif',
'empty',
'enddeclare',
'endfor',
'endforeach',
'endif',
'endswitch',
'endwhile',
'enum',
'eval',
'exit',
'extends',
'false',
'final',
'finally',
'fn',
'for',
'foreach',
'function',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'isset',
'list',
'match',
'namespace',
'new',
'or',
'parent',
'print',
'private',
'protected',
'public',
'readonly',
'require',
'require_once',
'return',
'self',
'static',
'switch',
'throw',
'trait',
'true',
'try',
'unset',
'use',
'var',
'while',
'xor',
'yield',
'__CLASS__',
'__DIR__',
'__FILE__',
'__FUNCTION__',
'__LINE__',
'__METHOD__',
'__NAMESPACE__',
'__TRAIT__',
];
/**
* Create a new generator command instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function __construct(Filesystem $files)
{
parent::__construct();
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
$this->addTestOptions();
}
$this->files = $files;
}
/**
* Get the stub file for the generator.
*
* @return string
*/
abstract protected function getStub();
/**
* Execute the console command.
*
* @return bool|null
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function handle()
{
// First we need to ensure that the given name is not a reserved word within the PHP
// language and that the class name will actually be valid. If it is not valid we
// can error now and prevent from polluting the filesystem using invalid files.
if ($this->isReservedName($this->getNameInput())) {
$this->components->error('The name "'.$this->getNameInput().'" is reserved by PHP.');
return false;
}
$name = $this->qualifyClass($this->getNameInput());
$path = $this->getPath($name);
// Next, We will check to see if the class already exists. If it does, we don't want
// to create the class and overwrite the user's code. So, we will bail out so the
// code is untouched. Otherwise, we will continue generating this class' files.
if ((! $this->hasOption('force') ||
! $this->option('force')) &&
$this->alreadyExists($this->getNameInput())) {
$this->components->error($this->type.' already exists.');
return false;
}
// Next, we will generate the path to the location where this class' file should get
// written. Then, we will build the class and make the proper replacements on the
// stub files so that it gets the correctly formatted namespace and class name.
$this->makeDirectory($path);
$this->files->put($path, $this->sortImports($this->buildClass($name)));
$info = $this->type;
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
$this->handleTestCreation($path);
}
if (windows_os()) {
$path = str_replace('/', '\\', $path);
}
$this->components->info(sprintf('%s [%s] created successfully.', $info, $path));
}
/**
* Parse the class name and format according to the root namespace.
*
* @param string $name
* @return string
*/
protected function qualifyClass($name)
{
$name = ltrim($name, '\\/');
$name = str_replace('/', '\\', $name);
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($name, $rootNamespace)) {
return $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 a list of possible model names.
*
* @return array<int, string>
*/
protected function possibleModels()
{
$modelPath = is_dir(app_path('Models')) ? app_path('Models') : app_path();
return (new Collection(Finder::create()->files()->depth(0)->in($modelPath)))
->map(fn ($file) => $file->getBasename('.php'))
->sort()
->values()
->all();
}
/**
* Get a list of possible event names.
*
* @return array<int, string>
*/
protected function possibleEvents()
{
$eventPath = app_path('Events');
if (! is_dir($eventPath)) {
return [];
}
return (new Collection(Finder::create()->files()->depth(0)->in($eventPath)))
->map(fn ($file) => $file->getBasename('.php'))
->sort()
->values()
->all();
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace;
}
/**
* Determine if the class already exists.
*
* @param string $rawName
* @return bool
*/
protected function alreadyExists($rawName)
{
return $this->files->exists($this->getPath($this->qualifyClass($rawName)));
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'.php';
}
/**
* Build the directory for the class if necessary.
*
* @param string $path
* @return string
*/
protected function makeDirectory($path)
{
if (! $this->files->isDirectory(dirname($path))) {
$this->files->makeDirectory(dirname($path), 0777, true, true);
}
return $path;
}
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function buildClass($name)
{
$stub = $this->files->get($this->getStub());
return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name);
}
/**
* Replace the namespace for the given stub.
*
* @param string $stub
* @param string $name
* @return $this
*/
protected function replaceNamespace(&$stub, $name)
{
$searches = [
['DummyNamespace', 'DummyRootNamespace', 'NamespacedDummyUserModel'],
['{{ namespace }}', '{{ rootNamespace }}', '{{ namespacedUserModel }}'],
['{{namespace}}', '{{rootNamespace}}', '{{namespacedUserModel}}'],
];
foreach ($searches as $search) {
$stub = str_replace(
$search,
[$this->getNamespace($name), $this->rootNamespace(), $this->userProviderModel()],
$stub
);
}
return $this;
}
/**
* Get the full namespace for a given class, without the class name.
*
* @param string $name
* @return string
*/
protected function getNamespace($name)
{
return trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
}
/**
* Replace the class name for the given stub.
*
* @param string $stub
* @param string $name
* @return string
*/
protected function replaceClass($stub, $name)
{
$class = str_replace($this->getNamespace($name).'\\', '', $name);
return str_replace(['DummyClass', '{{ class }}', '{{class}}'], $class, $stub);
}
/**
* Alphabetically sorts the imports for the given stub.
*
* @param string $stub
* @return string
*/
protected function sortImports($stub)
{
if (preg_match('/(?P<imports>(?:^use [^;{]+;$\n?)+)/m', $stub, $match)) {
$imports = explode("\n", trim($match['imports']));
sort($imports);
return str_replace(trim($match['imports']), implode("\n", $imports), $stub);
}
return $stub;
}
/**
* Get the desired class name from the input.
*
* @return string
*/
protected function getNameInput()
{
$name = trim($this->argument('name'));
if (Str::endsWith($name, '.php')) {
return Str::substr($name, 0, -4);
}
return $name;
}
/**
* Get the root namespace for the class.
*
* @return string
*/
protected function rootNamespace()
{
return $this->laravel->getNamespace();
}
/**
* Get the model for the default guard's user provider.
*
* @return string|null
*/
protected function userProviderModel()
{
$config = $this->laravel['config'];
$provider = $config->get('auth.guards.'.$config->get('auth.defaults.guard').'.provider');
return $config->get("auth.providers.{$provider}.model");
}
/**
* Checks whether the given name is reserved.
*
* @param string $name
* @return bool
*/
protected function isReservedName($name)
{
return in_array(
strtolower($name),
(new Collection($this->reservedNames))
->transform(fn ($name) => strtolower($name))
->all()
);
}
/**
* Get the first view directory path from the application configuration.
*
* @param string $path
* @return string
*/
protected function viewPath($path = '')
{
$views = $this->laravel['config']['view.paths'][0] ?? resource_path('views');
return $views.($path ? DIRECTORY_SEPARATOR.$path : $path);
}
/**
* Get the console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the '.strtolower($this->type)],
];
}
/**
* Prompt for missing input arguments using the returned questions.
*
* @return array
*/
protected function promptForMissingArgumentsUsing()
{
return [
'name' => [
'What should the '.strtolower($this->type).' be named?',
match ($this->type) {
'Cast' => 'E.g. Json',
'Channel' => 'E.g. OrderChannel',
'Console command' => 'E.g. SendEmails',
'Component' => 'E.g. Alert',
'Controller' => 'E.g. UserController',
'Event' => 'E.g. PodcastProcessed',
'Exception' => 'E.g. InvalidOrderException',
'Factory' => 'E.g. PostFactory',
'Job' => 'E.g. ProcessPodcast',
'Listener' => 'E.g. SendPodcastNotification',
'Mailable' => 'E.g. OrderShipped',
'Middleware' => 'E.g. EnsureTokenIsValid',
'Model' => 'E.g. Flight',
'Notification' => 'E.g. InvoicePaid',
'Observer' => 'E.g. UserObserver',
'Policy' => 'E.g. PostPolicy',
'Provider' => 'E.g. ElasticServiceProvider',
'Request' => 'E.g. StorePodcastRequest',
'Resource' => 'E.g. UserResource',
'Rule' => 'E.g. Uppercase',
'Scope' => 'E.g. TrendingScope',
'Seeder' => 'E.g. UserSeeder',
'Test' => 'E.g. UserTest',
default => '',
},
],
];
}
}

View File

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

View File

@@ -0,0 +1,10 @@
<?php
namespace Illuminate\Console;
use RuntimeException;
class ManuallyFailedException extends RuntimeException
{
//
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Illuminate\Console;
use Illuminate\Filesystem\Filesystem;
use function Illuminate\Filesystem\join_paths;
abstract class MigrationGeneratorCommand extends Command
{
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* Create a new migration generator command instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
/**
* Get the migration table name.
*
* @return string
*/
abstract protected function migrationTableName();
/**
* Get the path to the migration stub file.
*
* @return string
*/
abstract protected function migrationStubFile();
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$table = $this->migrationTableName();
if ($this->migrationExists($table)) {
$this->components->error('Migration already exists.');
return 1;
}
$this->replaceMigrationPlaceholders(
$this->createBaseMigration($table), $table
);
$this->components->info('Migration created successfully.');
return 0;
}
/**
* Create a base migration file for the table.
*
* @param string $table
* @return string
*/
protected function createBaseMigration($table)
{
return $this->laravel['migration.creator']->create(
'create_'.$table.'_table', $this->laravel->databasePath('/migrations')
);
}
/**
* Replace the placeholders in the generated migration file.
*
* @param string $path
* @param string $table
* @return void
*/
protected function replaceMigrationPlaceholders($path, $table)
{
$stub = str_replace(
'{{table}}', $table, $this->files->get($this->migrationStubFile())
);
$this->files->put($path, $stub);
}
/**
* Determine whether a migration for the table already exists.
*
* @param string $table
* @return bool
*/
protected function migrationExists($table)
{
return count($this->files->glob(
join_paths($this->laravel->databasePath('migrations'), '*_*_*_*_create_'.$table.'_table.php')
)) !== 0;
}
}

View File

@@ -0,0 +1,197 @@
<?php
namespace Illuminate\Console;
use Illuminate\Console\Contracts\NewLineAware;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Style\SymfonyStyle;
class OutputStyle extends SymfonyStyle implements NewLineAware
{
/**
* The output instance.
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
private $output;
/**
* The number of trailing new lines written by the last output.
*
* This is initialized as 1 to account for the new line written by the shell after executing a command.
*
* @var int
*/
protected $newLinesWritten = 1;
/**
* If the last output written wrote a new line.
*
* @var bool
*
* @deprecated use $newLinesWritten
*/
protected $newLineWritten = false;
/**
* Create a new Console OutputStyle instance.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
parent::__construct($input, $output);
}
/**
* {@inheritdoc}
*/
#[\Override]
public function askQuestion(Question $question): mixed
{
try {
return parent::askQuestion($question);
} finally {
$this->newLinesWritten++;
}
}
/**
* {@inheritdoc}
*/
#[\Override]
public function write(string|iterable $messages, bool $newline = false, int $options = 0): void
{
$this->newLinesWritten = $this->trailingNewLineCount($messages) + (int) $newline;
$this->newLineWritten = $this->newLinesWritten > 0;
parent::write($messages, $newline, $options);
}
/**
* {@inheritdoc}
*/
#[\Override]
public function writeln(string|iterable $messages, int $type = self::OUTPUT_NORMAL): void
{
$this->newLinesWritten = $this->trailingNewLineCount($messages) + 1;
$this->newLineWritten = true;
parent::writeln($messages, $type);
}
/**
* {@inheritdoc}
*/
#[\Override]
public function newLine(int $count = 1): void
{
$this->newLinesWritten += $count;
$this->newLineWritten = $this->newLinesWritten > 0;
parent::newLine($count);
}
/**
* {@inheritdoc}
*/
public function newLinesWritten()
{
if ($this->output instanceof static) {
return $this->output->newLinesWritten();
}
return $this->newLinesWritten;
}
/**
* {@inheritdoc}
*
* @deprecated use newLinesWritten
*/
public function newLineWritten()
{
if ($this->output instanceof static && $this->output->newLineWritten()) {
return true;
}
return $this->newLineWritten;
}
/*
* Count the number of trailing new lines in a string.
*
* @param string|iterable $messages
* @return int
*/
protected function trailingNewLineCount($messages)
{
if (is_iterable($messages)) {
$string = '';
foreach ($messages as $message) {
$string .= $message.PHP_EOL;
}
} else {
$string = $messages;
}
return strlen($string) - strlen(rtrim($string, PHP_EOL));
}
/**
* Returns whether verbosity is quiet (-q).
*
* @return bool
*/
public function isQuiet(): bool
{
return $this->output->isQuiet();
}
/**
* Returns whether verbosity is verbose (-v).
*
* @return bool
*/
public function isVerbose(): bool
{
return $this->output->isVerbose();
}
/**
* Returns whether verbosity is very verbose (-vv).
*
* @return bool
*/
public function isVeryVerbose(): bool
{
return $this->output->isVeryVerbose();
}
/**
* Returns whether verbosity is debug (-vvv).
*
* @return bool
*/
public function isDebug(): bool
{
return $this->output->isDebug();
}
/**
* Get the underlying Symfony output implementation.
*
* @return \Symfony\Component\Console\Output\OutputInterface
*/
public function getOutput()
{
return $this->output;
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace Illuminate\Console;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class Parser
{
/**
* Parse the given console command definition into an array.
*
* @param string $expression
* @return array
*
* @throws \InvalidArgumentException
*/
public static function parse(string $expression)
{
$name = static::name($expression);
if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches) && count($matches[1])) {
return array_merge([$name], static::parameters($matches[1]));
}
return [$name, [], []];
}
/**
* Extract the name of the command from the expression.
*
* @param string $expression
* @return string
*
* @throws \InvalidArgumentException
*/
protected static function name(string $expression)
{
if (! preg_match('/[^\s]+/', $expression, $matches)) {
throw new InvalidArgumentException('Unable to determine command name from signature.');
}
return $matches[0];
}
/**
* Extract all parameters from the tokens.
*
* @param array $tokens
* @return array
*/
protected static function parameters(array $tokens)
{
$arguments = [];
$options = [];
foreach ($tokens as $token) {
if (preg_match('/^-{2,}(.*)/', $token, $matches)) {
$options[] = static::parseOption($matches[1]);
} else {
$arguments[] = static::parseArgument($token);
}
}
return [$arguments, $options];
}
/**
* Parse an argument expression.
*
* @param string $token
* @return \Symfony\Component\Console\Input\InputArgument
*/
protected static function parseArgument(string $token)
{
[$token, $description] = static::extractDescription($token);
switch (true) {
case str_ends_with($token, '?*'):
return new InputArgument(trim($token, '?*'), InputArgument::IS_ARRAY, $description);
case str_ends_with($token, '*'):
return new InputArgument(trim($token, '*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description);
case str_ends_with($token, '?'):
return new InputArgument(trim($token, '?'), InputArgument::OPTIONAL, $description);
case preg_match('/(.+)\=\*(.+)/', $token, $matches):
return new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
case preg_match('/(.+)\=(.+)/', $token, $matches):
return new InputArgument($matches[1], InputArgument::OPTIONAL, $description, $matches[2]);
default:
return new InputArgument($token, InputArgument::REQUIRED, $description);
}
}
/**
* Parse an option expression.
*
* @param string $token
* @return \Symfony\Component\Console\Input\InputOption
*/
protected static function parseOption(string $token)
{
[$token, $description] = static::extractDescription($token);
$matches = preg_split('/\s*\|\s*/', $token, 2);
$shortcut = null;
if (isset($matches[1])) {
$shortcut = $matches[0];
$token = $matches[1];
}
switch (true) {
case str_ends_with($token, '='):
return new InputOption(trim($token, '='), $shortcut, InputOption::VALUE_OPTIONAL, $description);
case str_ends_with($token, '=*'):
return new InputOption(trim($token, '=*'), $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description);
case preg_match('/(.+)\=\*(.+)/', $token, $matches):
return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
case preg_match('/(.+)\=(.+)/', $token, $matches):
return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL, $description, $matches[2]);
default:
return new InputOption($token, $shortcut, InputOption::VALUE_NONE, $description);
}
}
/**
* Parse the token into its token and description segments.
*
* @param string $token
* @return array
*/
protected static function extractDescription(string $token)
{
$parts = preg_split('/\s+:\s+/', trim($token), 2);
return count($parts) === 2 ? $parts : [$token, ''];
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Illuminate\Console;
trait Prohibitable
{
/**
* Indicates if the command should be prohibited from running.
*
* @var bool
*/
protected static $prohibitedFromRunning = false;
/**
* Indicate whether the command should be prohibited from running.
*
* @param bool $prohibit
* @return void
*/
public static function prohibit($prohibit = true)
{
static::$prohibitedFromRunning = $prohibit;
}
/**
* Determine if the command is prohibited from running and display a warning if so.
*
* @param bool $quiet
* @return bool
*/
protected function isProhibited(bool $quiet = false)
{
if (! static::$prohibitedFromRunning) {
return false;
}
if (! $quiet) {
$this->components->warn('This command is prohibited from running in this environment.');
}
return true;
}
}

View File

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

View File

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

View File

@@ -0,0 +1,14 @@
<?php
namespace Illuminate\Console\Scheduling;
interface CacheAware
{
/**
* Specify the cache store that should be used.
*
* @param string $store
* @return $this
*/
public function useStore($store);
}

View File

@@ -0,0 +1,114 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Cache\DynamoDbStore;
use Illuminate\Contracts\Cache\Factory as Cache;
use Illuminate\Contracts\Cache\LockProvider;
class CacheEventMutex implements EventMutex, CacheAware
{
/**
* The cache repository implementation.
*
* @var \Illuminate\Contracts\Cache\Factory
*/
public $cache;
/**
* The cache store that should be used.
*
* @var string|null
*/
public $store;
/**
* Create a new overlapping strategy.
*
* @param \Illuminate\Contracts\Cache\Factory $cache
* @return void
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Attempt to obtain an event mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
*/
public function create(Event $event)
{
if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) {
return $this->cache->store($this->store)->getStore()
->lock($event->mutexName(), $event->expiresAt * 60)
->acquire();
}
return $this->cache->store($this->store)->add(
$event->mutexName(), true, $event->expiresAt * 60
);
}
/**
* Determine if an event mutex exists for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
*/
public function exists(Event $event)
{
if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) {
return ! $this->cache->store($this->store)->getStore()
->lock($event->mutexName(), $event->expiresAt * 60)
->get(fn () => true);
}
return $this->cache->store($this->store)->has($event->mutexName());
}
/**
* Clear the event mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
public function forget(Event $event)
{
if ($this->shouldUseLocks($this->cache->store($this->store)->getStore())) {
$this->cache->store($this->store)->getStore()
->lock($event->mutexName(), $event->expiresAt * 60)
->forceRelease();
return;
}
$this->cache->store($this->store)->forget($event->mutexName());
}
/**
* Determine if the given store should use locks for cache event mutexes.
*
* @param \Illuminate\Contracts\Cache\Store $store
* @return bool
*/
protected function shouldUseLocks($store)
{
return $store instanceof LockProvider && ! $store instanceof DynamoDbStore;
}
/**
* Specify the cache store that should be used.
*
* @param string $store
* @return $this
*/
public function useStore($store)
{
$this->store = $store;
return $this;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Illuminate\Console\Scheduling;
use DateTimeInterface;
use Illuminate\Contracts\Cache\Factory as Cache;
class CacheSchedulingMutex implements SchedulingMutex, CacheAware
{
/**
* The cache factory implementation.
*
* @var \Illuminate\Contracts\Cache\Factory
*/
public $cache;
/**
* The cache store that should be used.
*
* @var string|null
*/
public $store;
/**
* Create a new scheduling strategy.
*
* @param \Illuminate\Contracts\Cache\Factory $cache
* @return void
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Attempt to obtain a scheduling mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeInterface $time
* @return bool
*/
public function create(Event $event, DateTimeInterface $time)
{
return $this->cache->store($this->store)->add(
$event->mutexName().$time->format('Hi'), true, 3600
);
}
/**
* Determine if a scheduling mutex exists for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeInterface $time
* @return bool
*/
public function exists(Event $event, DateTimeInterface $time)
{
return $this->cache->store($this->store)->has(
$event->mutexName().$time->format('Hi')
);
}
/**
* Specify the cache store that should be used.
*
* @param string $store
* @return $this
*/
public function useStore($store)
{
$this->store = $store;
return $this;
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Reflector;
use InvalidArgumentException;
use LogicException;
use RuntimeException;
use Throwable;
class CallbackEvent extends Event
{
/**
* The callback to call.
*
* @var string
*/
protected $callback;
/**
* The parameters to pass to the method.
*
* @var array
*/
protected $parameters;
/**
* The result of the callback's execution.
*
* @var mixed
*/
protected $result;
/**
* The exception that was thrown when calling the callback, if any.
*
* @var \Throwable|null
*/
protected $exception;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @param string|callable $callback
* @param array $parameters
* @param \DateTimeZone|string|null $timezone
* @return void
*
* @throws \InvalidArgumentException
*/
public function __construct(EventMutex $mutex, $callback, array $parameters = [], $timezone = null)
{
if (! is_string($callback) && ! Reflector::isCallable($callback)) {
throw new InvalidArgumentException(
'Invalid scheduled callback event. Must be a string or callable.'
);
}
$this->mutex = $mutex;
$this->callback = $callback;
$this->parameters = $parameters;
$this->timezone = $timezone;
}
/**
* Run the callback event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*
* @throws \Throwable
*/
public function run(Container $container)
{
parent::run($container);
if ($this->exception) {
throw $this->exception;
}
return $this->result;
}
/**
* Determine if the event should skip because another process is overlapping.
*
* @return bool
*/
public function shouldSkipDueToOverlapping()
{
return $this->description && parent::shouldSkipDueToOverlapping();
}
/**
* Indicate that the callback should run in the background.
*
* @return void
*
* @throws \RuntimeException
*/
public function runInBackground()
{
throw new RuntimeException('Scheduled closures can not be run in the background.');
}
/**
* Run the callback.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*/
protected function execute($container)
{
try {
$this->result = is_object($this->callback)
? $container->call([$this->callback, '__invoke'], $this->parameters)
: $container->call($this->callback, $this->parameters);
return $this->result === false ? 1 : 0;
} catch (Throwable $e) {
$this->exception = $e;
return 1;
}
}
/**
* Do not allow the event to overlap each other.
*
* The expiration time of the underlying cache lock may be specified in minutes.
*
* @param int $expiresAt
* @return $this
*
* @throws \LogicException
*/
public function withoutOverlapping($expiresAt = 1440)
{
if (! isset($this->description)) {
throw new LogicException(
"A scheduled event name is required to prevent overlapping. Use the 'name' method before 'withoutOverlapping'."
);
}
return parent::withoutOverlapping($expiresAt);
}
/**
* Allow the event to only run on one server for each cron expression.
*
* @return $this
*
* @throws \LogicException
*/
public function onOneServer()
{
if (! isset($this->description)) {
throw new LogicException(
"A scheduled event name is required to only run on one server. Use the 'name' method before 'onOneServer'."
);
}
return parent::onOneServer();
}
/**
* Get the summary of the event for display.
*
* @return string
*/
public function getSummaryForDisplay()
{
if (is_string($this->description)) {
return $this->description;
}
return is_string($this->callback) ? $this->callback : 'Callback';
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
return 'framework/schedule-'.sha1($this->description ?? '');
}
/**
* Clear the mutex for the event.
*
* @return void
*/
protected function removeMutex()
{
if ($this->description) {
parent::removeMutex();
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Support\ProcessUtils;
class CommandBuilder
{
/**
* Build the command for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return string
*/
public function buildCommand(Event $event)
{
if ($event->runInBackground) {
return $this->buildBackgroundCommand($event);
}
return $this->buildForegroundCommand($event);
}
/**
* Build the command for running the event in the foreground.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return string
*/
protected function buildForegroundCommand(Event $event)
{
$output = ProcessUtils::escapeArgument($event->output);
return laravel_cloud()
? $this->ensureCorrectUser($event, $event->command.' 2>&1 | tee '.($event->shouldAppendOutput ? '-a ' : '').$output)
: $this->ensureCorrectUser($event, $event->command.($event->shouldAppendOutput ? ' >> ' : ' > ').$output.' 2>&1');
}
/**
* Build the command for running the event in the background.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return string
*/
protected function buildBackgroundCommand(Event $event)
{
$output = ProcessUtils::escapeArgument($event->output);
$redirect = $event->shouldAppendOutput ? ' >> ' : ' > ';
$finished = Application::formatCommandString('schedule:finish').' "'.$event->mutexName().'"';
if (windows_os()) {
return 'start /b cmd /v:on /c "('.$event->command.' & '.$finished.' ^!ERRORLEVEL^!)'.$redirect.$output.' 2>&1"';
}
return $this->ensureCorrectUser($event,
'('.$event->command.$redirect.$output.' 2>&1 ; '.$finished.' "$?") > '
.ProcessUtils::escapeArgument($event->getDefaultOutput()).' 2>&1 &'
);
}
/**
* Finalize the event's command syntax with the correct user.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param string $command
* @return string
*/
protected function ensureCorrectUser(Event $event, $command)
{
return $event->user && ! windows_os() ? 'sudo -u '.$event->user.' -- sh -c \''.$command.'\'' : $command;
}
}

View File

@@ -0,0 +1,860 @@
<?php
namespace Illuminate\Console\Scheduling;
use Closure;
use Cron\CronExpression;
use GuzzleHttp\Client as HttpClient;
use GuzzleHttp\ClientInterface as HttpClientInterface;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Console\Application;
use Illuminate\Contracts\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Stringable;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\ReflectsClosures;
use Illuminate\Support\Traits\Tappable;
use Psr\Http\Client\ClientExceptionInterface;
use Symfony\Component\Process\Process;
use Throwable;
class Event
{
use Macroable, ManagesAttributes, ManagesFrequencies, ReflectsClosures, Tappable;
/**
* The command string.
*
* @var string|null
*/
public $command;
/**
* The location that output should be sent to.
*
* @var string
*/
public $output = '/dev/null';
/**
* Indicates whether output should be appended.
*
* @var bool
*/
public $shouldAppendOutput = false;
/**
* The array of callbacks to be run before the event is started.
*
* @var array
*/
protected $beforeCallbacks = [];
/**
* The array of callbacks to be run after the event is finished.
*
* @var array
*/
protected $afterCallbacks = [];
/**
* The event mutex implementation.
*
* @var \Illuminate\Console\Scheduling\EventMutex
*/
public $mutex;
/**
* The mutex name resolver callback.
*
* @var \Closure|null
*/
public $mutexNameResolver;
/**
* The last time the event was checked for eligibility to run.
*
* Utilized by sub-minute repeated events.
*
* @var \Illuminate\Support\Carbon|null
*/
protected $lastChecked;
/**
* The exit status code of the command.
*
* @var int|null
*/
public $exitCode;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @param string $command
* @param \DateTimeZone|string|null $timezone
* @return void
*/
public function __construct(EventMutex $mutex, $command, $timezone = null)
{
$this->mutex = $mutex;
$this->command = $command;
$this->timezone = $timezone;
$this->output = $this->getDefaultOutput();
}
/**
* Get the default output depending on the OS.
*
* @return string
*/
public function getDefaultOutput()
{
return (DIRECTORY_SEPARATOR === '\\') ? 'NUL' : '/dev/null';
}
/**
* Run the given event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*
* @throws \Throwable
*/
public function run(Container $container)
{
if ($this->shouldSkipDueToOverlapping()) {
return;
}
$exitCode = $this->start($container);
if (! $this->runInBackground) {
$this->finish($container, $exitCode);
}
}
/**
* Determine if the event should skip because another process is overlapping.
*
* @return bool
*/
public function shouldSkipDueToOverlapping()
{
return $this->withoutOverlapping && ! $this->mutex->create($this);
}
/**
* Determine if the event has been configured to repeat multiple times per minute.
*
* @return bool
*/
public function isRepeatable()
{
return ! is_null($this->repeatSeconds);
}
/**
* Determine if the event is ready to repeat.
*
* @return bool
*/
public function shouldRepeatNow()
{
return $this->isRepeatable()
&& $this->lastChecked?->diffInSeconds() >= $this->repeatSeconds;
}
/**
* Run the command process.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*
* @throws \Throwable
*/
protected function start($container)
{
try {
$this->callBeforeCallbacks($container);
return $this->execute($container);
} catch (Throwable $exception) {
$this->removeMutex();
throw $exception;
}
}
/**
* Run the command process.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*/
protected function execute($container)
{
return Process::fromShellCommandline(
$this->buildCommand(), base_path(), null, null, null
)->run(
laravel_cloud()
? fn ($type, $line) => fwrite($type === 'out' ? STDOUT : STDERR, $line)
: fn () => true
);
}
/**
* Mark the command process as finished and run callbacks/cleanup.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param int $exitCode
* @return void
*/
public function finish(Container $container, $exitCode)
{
$this->exitCode = (int) $exitCode;
try {
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
}
}
/**
* Call all of the "before" callbacks for the event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function callBeforeCallbacks(Container $container)
{
foreach ($this->beforeCallbacks as $callback) {
$container->call($callback);
}
}
/**
* Call all of the "after" callbacks for the event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function callAfterCallbacks(Container $container)
{
foreach ($this->afterCallbacks as $callback) {
$container->call($callback);
}
}
/**
* Build the command string.
*
* @return string
*/
public function buildCommand()
{
return (new CommandBuilder)->buildCommand($this);
}
/**
* Determine if the given event should run based on the Cron expression.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return bool
*/
public function isDue($app)
{
if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
return false;
}
return $this->expressionPasses() &&
$this->runsInEnvironment($app->environment());
}
/**
* Determine if the event runs in maintenance mode.
*
* @return bool
*/
public function runsInMaintenanceMode()
{
return $this->evenInMaintenanceMode;
}
/**
* Determine if the Cron expression passes.
*
* @return bool
*/
protected function expressionPasses()
{
$date = Date::now();
if ($this->timezone) {
$date = $date->setTimezone($this->timezone);
}
return (new CronExpression($this->expression))->isDue($date->toDateTimeString());
}
/**
* Determine if the event runs in the given environment.
*
* @param string $environment
* @return bool
*/
public function runsInEnvironment($environment)
{
return empty($this->environments) || in_array($environment, $this->environments);
}
/**
* Determine if the filters pass for the event.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return bool
*/
public function filtersPass($app)
{
$this->lastChecked = Date::now();
foreach ($this->filters as $callback) {
if (! $app->call($callback)) {
return false;
}
}
foreach ($this->rejects as $callback) {
if ($app->call($callback)) {
return false;
}
}
return true;
}
/**
* Ensure that the output is stored on disk in a log file.
*
* @return $this
*/
public function storeOutput()
{
$this->ensureOutputIsBeingCaptured();
return $this;
}
/**
* Send the output of the command to a given location.
*
* @param string $location
* @param bool $append
* @return $this
*/
public function sendOutputTo($location, $append = false)
{
$this->output = $location;
$this->shouldAppendOutput = $append;
return $this;
}
/**
* Append the output of the command to a given location.
*
* @param string $location
* @return $this
*/
public function appendOutputTo($location)
{
return $this->sendOutputTo($location, true);
}
/**
* E-mail the results of the scheduled operation.
*
* @param array|mixed $addresses
* @param bool $onlyIfOutputExists
* @return $this
*
* @throws \LogicException
*/
public function emailOutputTo($addresses, $onlyIfOutputExists = false)
{
$this->ensureOutputIsBeingCaptured();
$addresses = Arr::wrap($addresses);
return $this->then(function (Mailer $mailer) use ($addresses, $onlyIfOutputExists) {
$this->emailOutput($mailer, $addresses, $onlyIfOutputExists);
});
}
/**
* E-mail the results of the scheduled operation if it produces output.
*
* @param array|mixed $addresses
* @return $this
*
* @throws \LogicException
*/
public function emailWrittenOutputTo($addresses)
{
return $this->emailOutputTo($addresses, true);
}
/**
* E-mail the results of the scheduled operation if it fails.
*
* @param array|mixed $addresses
* @return $this
*/
public function emailOutputOnFailure($addresses)
{
$this->ensureOutputIsBeingCaptured();
$addresses = Arr::wrap($addresses);
return $this->onFailure(function (Mailer $mailer) use ($addresses) {
$this->emailOutput($mailer, $addresses, false);
});
}
/**
* Ensure that the command output is being captured.
*
* @return void
*/
protected function ensureOutputIsBeingCaptured()
{
if (is_null($this->output) || $this->output == $this->getDefaultOutput()) {
$this->sendOutputTo(storage_path('logs/schedule-'.sha1($this->mutexName()).'.log'));
}
}
/**
* E-mail the output of the event to the recipients.
*
* @param \Illuminate\Contracts\Mail\Mailer $mailer
* @param array $addresses
* @param bool $onlyIfOutputExists
* @return void
*/
protected function emailOutput(Mailer $mailer, $addresses, $onlyIfOutputExists = false)
{
$text = is_file($this->output) ? file_get_contents($this->output) : '';
if ($onlyIfOutputExists && empty($text)) {
return;
}
$mailer->raw($text, function ($m) use ($addresses) {
$m->to($addresses)->subject($this->getEmailSubject());
});
}
/**
* Get the e-mail subject line for output results.
*
* @return string
*/
protected function getEmailSubject()
{
if ($this->description) {
return $this->description;
}
return "Scheduled Job Output For [{$this->command}]";
}
/**
* Register a callback to ping a given URL before the job runs.
*
* @param string $url
* @return $this
*/
public function pingBefore($url)
{
return $this->before($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL before the job runs if the given condition is true.
*
* @param bool $value
* @param string $url
* @return $this
*/
public function pingBeforeIf($value, $url)
{
return $value ? $this->pingBefore($url) : $this;
}
/**
* Register a callback to ping a given URL after the job runs.
*
* @param string $url
* @return $this
*/
public function thenPing($url)
{
return $this->then($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL after the job runs if the given condition is true.
*
* @param bool $value
* @param string $url
* @return $this
*/
public function thenPingIf($value, $url)
{
return $value ? $this->thenPing($url) : $this;
}
/**
* Register a callback to ping a given URL if the operation succeeds.
*
* @param string $url
* @return $this
*/
public function pingOnSuccess($url)
{
return $this->onSuccess($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL if the operation succeeds and if the given condition is true.
*
* @param bool $value
* @param string $url
* @return $this
*/
public function pingOnSuccessIf($value, $url)
{
return $value ? $this->onSuccess($this->pingCallback($url)) : $this;
}
/**
* Register a callback to ping a given URL if the operation fails.
*
* @param string $url
* @return $this
*/
public function pingOnFailure($url)
{
return $this->onFailure($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL if the operation fails and if the given condition is true.
*
* @param bool $value
* @param string $url
* @return $this
*/
public function pingOnFailureIf($value, $url)
{
return $value ? $this->onFailure($this->pingCallback($url)) : $this;
}
/**
* Get the callback that pings the given URL.
*
* @param string $url
* @return \Closure
*/
protected function pingCallback($url)
{
return function (Container $container) use ($url) {
try {
$this->getHttpClient($container)->request('GET', $url);
} catch (ClientExceptionInterface|TransferException $e) {
$container->make(ExceptionHandler::class)->report($e);
}
};
}
/**
* Get the Guzzle HTTP client to use to send pings.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return \GuzzleHttp\ClientInterface
*/
protected function getHttpClient(Container $container)
{
return match (true) {
$container->bound(HttpClientInterface::class) => $container->make(HttpClientInterface::class),
$container->bound(HttpClient::class) => $container->make(HttpClient::class),
default => new HttpClient([
'connect_timeout' => 10,
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT,
'timeout' => 30,
]),
};
}
/**
* Register a callback to be called before the operation.
*
* @param \Closure $callback
* @return $this
*/
public function before(Closure $callback)
{
$this->beforeCallbacks[] = $callback;
return $this;
}
/**
* Register a callback to be called after the operation.
*
* @param \Closure $callback
* @return $this
*/
public function after(Closure $callback)
{
return $this->then($callback);
}
/**
* Register a callback to be called after the operation.
*
* @param \Closure $callback
* @return $this
*/
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;
}
/**
* Register a callback that uses the output after the job runs.
*
* @param \Closure $callback
* @param bool $onlyIfOutputExists
* @return $this
*/
public function thenWithOutput(Closure $callback, $onlyIfOutputExists = false)
{
$this->ensureOutputIsBeingCaptured();
return $this->then($this->withOutputCallback($callback, $onlyIfOutputExists));
}
/**
* Register a callback to be called if the operation succeeds.
*
* @param \Closure $callback
* @return $this
*/
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 ($this->exitCode === 0) {
$container->call($callback);
}
});
}
/**
* Register a callback that uses the output if the operation succeeds.
*
* @param \Closure $callback
* @param bool $onlyIfOutputExists
* @return $this
*/
public function onSuccessWithOutput(Closure $callback, $onlyIfOutputExists = false)
{
$this->ensureOutputIsBeingCaptured();
return $this->onSuccess($this->withOutputCallback($callback, $onlyIfOutputExists));
}
/**
* Register a callback to be called if the operation fails.
*
* @param \Closure $callback
* @return $this
*/
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 ($this->exitCode !== 0) {
$container->call($callback);
}
});
}
/**
* Register a callback that uses the output if the operation fails.
*
* @param \Closure $callback
* @param bool $onlyIfOutputExists
* @return $this
*/
public function onFailureWithOutput(Closure $callback, $onlyIfOutputExists = false)
{
$this->ensureOutputIsBeingCaptured();
return $this->onFailure($this->withOutputCallback($callback, $onlyIfOutputExists));
}
/**
* Get a callback that provides output.
*
* @param \Closure $callback
* @param bool $onlyIfOutputExists
* @return \Closure
*/
protected function withOutputCallback(Closure $callback, $onlyIfOutputExists = false)
{
return function (Container $container) use ($callback, $onlyIfOutputExists) {
$output = $this->output && is_file($this->output) ? file_get_contents($this->output) : '';
return $onlyIfOutputExists && empty($output)
? null
: $container->call($callback, ['output' => new Stringable($output)]);
};
}
/**
* Get the summary of the event for display.
*
* @return string
*/
public function getSummaryForDisplay()
{
if (is_string($this->description)) {
return $this->description;
}
return $this->buildCommand();
}
/**
* Determine the next due date for an event.
*
* @param \DateTimeInterface|string $currentTime
* @param int $nth
* @param bool $allowCurrentDate
* @return \Illuminate\Support\Carbon
*/
public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return Date::instance((new CronExpression($this->getExpression()))
->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone));
}
/**
* Get the Cron expression for the event.
*
* @return string
*/
public function getExpression()
{
return $this->expression;
}
/**
* Set the event mutex implementation to be used.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @return $this
*/
public function preventOverlapsUsing(EventMutex $mutex)
{
$this->mutex = $mutex;
return $this;
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
$mutexNameResolver = $this->mutexNameResolver;
if (! is_null($mutexNameResolver) && is_callable($mutexNameResolver)) {
return $mutexNameResolver($this);
}
return 'framework'.DIRECTORY_SEPARATOR.'schedule-'.
sha1($this->expression.$this->normalizeCommand($this->command ?? ''));
}
/**
* Set the mutex name or name resolver callback.
*
* @param \Closure|string $mutexName
* @return $this
*/
public function createMutexNameUsing(Closure|string $mutexName)
{
$this->mutexNameResolver = is_string($mutexName) ? fn () => $mutexName : $mutexName;
return $this;
}
/**
* Delete the mutex for the event.
*
* @return void
*/
protected function removeMutex()
{
if ($this->withoutOverlapping) {
$this->mutex->forget($this);
}
}
/**
* Format the given command string with a normalized PHP binary path.
*
* @param string $command
* @return string
*/
public static function normalizeCommand($command)
{
return str_replace([
Application::phpBinary(),
Application::artisanBinary(),
], [
'php',
preg_replace("#['\"]#", '', Application::artisanBinary()),
], $command);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Illuminate\Console\Scheduling;
interface EventMutex
{
/**
* Attempt to obtain an event mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
*/
public function create(Event $event);
/**
* Determine if an event mutex exists for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
*/
public function exists(Event $event);
/**
* Clear the event mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
public function forget(Event $event);
}

View File

@@ -0,0 +1,233 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Support\Reflector;
trait ManagesAttributes
{
/**
* The cron expression representing the event's frequency.
*
* @var string
*/
public $expression = '* * * * *';
/**
* How often to repeat the event during a minute.
*
* @var int|null
*/
public $repeatSeconds = null;
/**
* The timezone the date should be evaluated on.
*
* @var \DateTimeZone|string
*/
public $timezone;
/**
* The user the command should run as.
*
* @var string|null
*/
public $user;
/**
* The list of environments the command should run under.
*
* @var array
*/
public $environments = [];
/**
* Indicates if the command should run in maintenance mode.
*
* @var bool
*/
public $evenInMaintenanceMode = false;
/**
* Indicates if the command should not overlap itself.
*
* @var bool
*/
public $withoutOverlapping = false;
/**
* Indicates if the command should only be allowed to run on one server for each cron expression.
*
* @var bool
*/
public $onOneServer = false;
/**
* The number of minutes the mutex should be valid.
*
* @var int
*/
public $expiresAt = 1440;
/**
* Indicates if the command should run in the background.
*
* @var bool
*/
public $runInBackground = false;
/**
* The array of filter callbacks.
*
* @var array
*/
protected $filters = [];
/**
* The array of reject callbacks.
*
* @var array
*/
protected $rejects = [];
/**
* The human readable description of the event.
*
* @var string|null
*/
public $description;
/**
* Set which user the command should run as.
*
* @param string $user
* @return $this
*/
public function user($user)
{
$this->user = $user;
return $this;
}
/**
* Limit the environments the command should run in.
*
* @param array|mixed $environments
* @return $this
*/
public function environments($environments)
{
$this->environments = is_array($environments) ? $environments : func_get_args();
return $this;
}
/**
* State that the command should run even in maintenance mode.
*
* @return $this
*/
public function evenInMaintenanceMode()
{
$this->evenInMaintenanceMode = true;
return $this;
}
/**
* Do not allow the event to overlap each other.
* The expiration time of the underlying cache lock may be specified in minutes.
*
* @param int $expiresAt
* @return $this
*/
public function withoutOverlapping($expiresAt = 1440)
{
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this->skip(function () {
return $this->mutex->exists($this);
});
}
/**
* Allow the event to only run on one server for each cron expression.
*
* @return $this
*/
public function onOneServer()
{
$this->onOneServer = true;
return $this;
}
/**
* State that the command should run in the background.
*
* @return $this
*/
public function runInBackground()
{
$this->runInBackground = true;
return $this;
}
/**
* Register a callback to further filter the schedule.
*
* @param \Closure|bool $callback
* @return $this
*/
public function when($callback)
{
$this->filters[] = Reflector::isCallable($callback) ? $callback : function () use ($callback) {
return $callback;
};
return $this;
}
/**
* Register a callback to further filter the schedule.
*
* @param \Closure|bool $callback
* @return $this
*/
public function skip($callback)
{
$this->rejects[] = Reflector::isCallable($callback) ? $callback : function () use ($callback) {
return $callback;
};
return $this;
}
/**
* Set the human-friendly description of the event.
*
* @param string $description
* @return $this
*/
public function name($description)
{
return $this->description($description);
}
/**
* Set the human-friendly description of the event.
*
* @param string $description
* @return $this
*/
public function description($description)
{
$this->description = $description;
return $this;
}
}

View File

@@ -0,0 +1,667 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Support\Carbon;
use InvalidArgumentException;
trait ManagesFrequencies
{
/**
* The Cron expression representing the event's frequency.
*
* @param string $expression
* @return $this
*/
public function cron($expression)
{
$this->expression = $expression;
return $this;
}
/**
* Schedule the event to run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return $this
*/
public function between($startTime, $endTime)
{
return $this->when($this->inTimeInterval($startTime, $endTime));
}
/**
* Schedule the event to not run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return $this
*/
public function unlessBetween($startTime, $endTime)
{
return $this->skip($this->inTimeInterval($startTime, $endTime));
}
/**
* Schedule the event to run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return \Closure
*/
private function inTimeInterval($startTime, $endTime)
{
[$now, $startTime, $endTime] = [
Carbon::now($this->timezone),
Carbon::parse($startTime, $this->timezone),
Carbon::parse($endTime, $this->timezone),
];
if ($endTime->lessThan($startTime)) {
if ($startTime->greaterThan($now)) {
$startTime = $startTime->subDay(1);
} else {
$endTime = $endTime->addDay(1);
}
}
return fn () => $now->between($startTime, $endTime);
}
/**
* Schedule the event to run every second.
*
* @return $this
*/
public function everySecond()
{
return $this->repeatEvery(1);
}
/**
* Schedule the event to run every two seconds.
*
* @return $this
*/
public function everyTwoSeconds()
{
return $this->repeatEvery(2);
}
/**
* Schedule the event to run every five seconds.
*
* @return $this
*/
public function everyFiveSeconds()
{
return $this->repeatEvery(5);
}
/**
* Schedule the event to run every ten seconds.
*
* @return $this
*/
public function everyTenSeconds()
{
return $this->repeatEvery(10);
}
/**
* Schedule the event to run every fifteen seconds.
*
* @return $this
*/
public function everyFifteenSeconds()
{
return $this->repeatEvery(15);
}
/**
* Schedule the event to run every twenty seconds.
*
* @return $this
*/
public function everyTwentySeconds()
{
return $this->repeatEvery(20);
}
/**
* Schedule the event to run every thirty seconds.
*
* @return $this
*/
public function everyThirtySeconds()
{
return $this->repeatEvery(30);
}
/**
* Schedule the event to run multiple times per minute.
*
* @param int $seconds
* @return $this
*/
protected function repeatEvery($seconds)
{
if (60 % $seconds !== 0) {
throw new InvalidArgumentException("The seconds [$seconds] are not evenly divisible by 60.");
}
$this->repeatSeconds = $seconds;
return $this->everyMinute();
}
/**
* Schedule the event to run every minute.
*
* @return $this
*/
public function everyMinute()
{
return $this->spliceIntoPosition(1, '*');
}
/**
* Schedule the event to run every two minutes.
*
* @return $this
*/
public function everyTwoMinutes()
{
return $this->spliceIntoPosition(1, '*/2');
}
/**
* Schedule the event to run every three minutes.
*
* @return $this
*/
public function everyThreeMinutes()
{
return $this->spliceIntoPosition(1, '*/3');
}
/**
* Schedule the event to run every four minutes.
*
* @return $this
*/
public function everyFourMinutes()
{
return $this->spliceIntoPosition(1, '*/4');
}
/**
* Schedule the event to run every five minutes.
*
* @return $this
*/
public function everyFiveMinutes()
{
return $this->spliceIntoPosition(1, '*/5');
}
/**
* Schedule the event to run every ten minutes.
*
* @return $this
*/
public function everyTenMinutes()
{
return $this->spliceIntoPosition(1, '*/10');
}
/**
* Schedule the event to run every fifteen minutes.
*
* @return $this
*/
public function everyFifteenMinutes()
{
return $this->spliceIntoPosition(1, '*/15');
}
/**
* Schedule the event to run every thirty minutes.
*
* @return $this
*/
public function everyThirtyMinutes()
{
return $this->spliceIntoPosition(1, '*/30');
}
/**
* Schedule the event to run hourly.
*
* @return $this
*/
public function hourly()
{
return $this->spliceIntoPosition(1, 0);
}
/**
* Schedule the event to run hourly at a given offset in the hour.
*
* @param array|string|int $offset
* @return $this
*/
public function hourlyAt($offset)
{
return $this->hourBasedSchedule($offset, '*');
}
/**
* Schedule the event to run every odd hour.
*
* @param array|string|int $offset
* @return $this
*/
public function everyOddHour($offset = 0)
{
return $this->hourBasedSchedule($offset, '1-23/2');
}
/**
* Schedule the event to run every two hours.
*
* @param array|string|int $offset
* @return $this
*/
public function everyTwoHours($offset = 0)
{
return $this->hourBasedSchedule($offset, '*/2');
}
/**
* Schedule the event to run every three hours.
*
* @param array|string|int $offset
* @return $this
*/
public function everyThreeHours($offset = 0)
{
return $this->hourBasedSchedule($offset, '*/3');
}
/**
* Schedule the event to run every four hours.
*
* @param array|string|int $offset
* @return $this
*/
public function everyFourHours($offset = 0)
{
return $this->hourBasedSchedule($offset, '*/4');
}
/**
* Schedule the event to run every six hours.
*
* @param array|string|int $offset
* @return $this
*/
public function everySixHours($offset = 0)
{
return $this->hourBasedSchedule($offset, '*/6');
}
/**
* Schedule the event to run daily.
*
* @return $this
*/
public function daily()
{
return $this->hourBasedSchedule(0, 0);
}
/**
* Schedule the command at a given time.
*
* @param string $time
* @return $this
*/
public function at($time)
{
return $this->dailyAt($time);
}
/**
* Schedule the event to run daily at a given time (10:00, 19:30, etc).
*
* @param string $time
* @return $this
*/
public function dailyAt($time)
{
$segments = explode(':', $time);
return $this->hourBasedSchedule(
count($segments) === 2 ? (int) $segments[1] : '0',
(int) $segments[0]
);
}
/**
* Schedule the event to run twice daily.
*
* @param int $first
* @param int $second
* @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->hourBasedSchedule($offset, $hours);
}
/**
* Schedule the event to run at the given minutes and hours.
*
* @param array|string|int $minutes
* @param array|string|int $hours
* @return $this
*/
protected function hourBasedSchedule($minutes, $hours)
{
$minutes = is_array($minutes) ? implode(',', $minutes) : $minutes;
$hours = is_array($hours) ? implode(',', $hours) : $hours;
return $this->spliceIntoPosition(1, $minutes)
->spliceIntoPosition(2, $hours);
}
/**
* Schedule the event to run only on weekdays.
*
* @return $this
*/
public function weekdays()
{
return $this->days(Schedule::MONDAY.'-'.Schedule::FRIDAY);
}
/**
* Schedule the event to run only on weekends.
*
* @return $this
*/
public function weekends()
{
return $this->days(Schedule::SATURDAY.','.Schedule::SUNDAY);
}
/**
* Schedule the event to run only on Mondays.
*
* @return $this
*/
public function mondays()
{
return $this->days(Schedule::MONDAY);
}
/**
* Schedule the event to run only on Tuesdays.
*
* @return $this
*/
public function tuesdays()
{
return $this->days(Schedule::TUESDAY);
}
/**
* Schedule the event to run only on Wednesdays.
*
* @return $this
*/
public function wednesdays()
{
return $this->days(Schedule::WEDNESDAY);
}
/**
* Schedule the event to run only on Thursdays.
*
* @return $this
*/
public function thursdays()
{
return $this->days(Schedule::THURSDAY);
}
/**
* Schedule the event to run only on Fridays.
*
* @return $this
*/
public function fridays()
{
return $this->days(Schedule::FRIDAY);
}
/**
* Schedule the event to run only on Saturdays.
*
* @return $this
*/
public function saturdays()
{
return $this->days(Schedule::SATURDAY);
}
/**
* Schedule the event to run only on Sundays.
*
* @return $this
*/
public function sundays()
{
return $this->days(Schedule::SUNDAY);
}
/**
* Schedule the event to run weekly.
*
* @return $this
*/
public function weekly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(5, 0);
}
/**
* Schedule the event to run weekly on a given day and time.
*
* @param array|mixed $dayOfWeek
* @param string $time
* @return $this
*/
public function weeklyOn($dayOfWeek, $time = '0:0')
{
$this->dailyAt($time);
return $this->days($dayOfWeek);
}
/**
* Schedule the event to run monthly.
*
* @return $this
*/
public function monthly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1);
}
/**
* Schedule the event to run monthly on a given day and time.
*
* @param int $dayOfMonth
* @param string $time
* @return $this
*/
public function monthlyOn($dayOfMonth = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $dayOfMonth);
}
/**
* Schedule the event to run twice monthly at a given time.
*
* @param int $first
* @param int $second
* @param string $time
* @return $this
*/
public function twiceMonthly($first = 1, $second = 16, $time = '0:0')
{
$daysOfMonth = $first.','.$second;
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $daysOfMonth);
}
/**
* Schedule the event to run on the last day of the month.
*
* @param string $time
* @return $this
*/
public function lastDayOfMonth($time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, Carbon::now()->endOfMonth()->day);
}
/**
* Schedule the event to run quarterly.
*
* @return $this
*/
public function quarterly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, '1-12/3');
}
/**
* Schedule the event to run quarterly on a given day and time.
*
* @param int $dayOfQuarter
* @param string $time
* @return $this
*/
public function quarterlyOn($dayOfQuarter = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $dayOfQuarter)
->spliceIntoPosition(4, '1-12/3');
}
/**
* Schedule the event to run yearly.
*
* @return $this
*/
public function yearly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->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.
*
* @param array|mixed $days
* @return $this
*/
public function days($days)
{
$days = is_array($days) ? $days : func_get_args();
return $this->spliceIntoPosition(5, implode(',', $days));
}
/**
* Set the timezone the date should be evaluated on.
*
* @param \DateTimeZone|string $timezone
* @return $this
*/
public function timezone($timezone)
{
$this->timezone = $timezone;
return $this;
}
/**
* Splice the given value into the given position of the expression.
*
* @param int $position
* @param string $value
* @return $this
*/
protected function spliceIntoPosition($position, $value)
{
$segments = preg_split("/\s+/", $this->expression);
$segments[$position - 1] = $value;
return $this->cron(implode(' ', $segments));
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Illuminate\Console\Scheduling;
/**
* @mixin \Illuminate\Console\Scheduling\Schedule
*/
class PendingEventAttributes
{
use ManagesAttributes, ManagesFrequencies;
/**
* Create a new pending event attributes instance.
*/
public function __construct(
protected Schedule $schedule,
) {
}
/**
* Do not allow the event to overlap each other.
*
* The expiration time of the underlying cache lock may be specified in minutes.
*
* @param int $expiresAt
* @return $this
*/
public function withoutOverlapping($expiresAt = 1440)
{
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this;
}
/**
* Merge the current attributes into the given event.
*/
public function mergeAttributes(Event $event): void
{
$event->expression = $this->expression;
$event->repeatSeconds = $this->repeatSeconds;
if ($this->description !== null) {
$event->name($this->description);
}
if ($this->timezone !== null) {
$event->timezone($this->timezone);
}
if ($this->user !== null) {
$event->user = $this->user;
}
if (! empty($this->environments)) {
$event->environments($this->environments);
}
if ($this->evenInMaintenanceMode) {
$event->evenInMaintenanceMode();
}
if ($this->withoutOverlapping) {
$event->withoutOverlapping($this->expiresAt);
}
if ($this->onOneServer) {
$event->onOneServer();
}
if ($this->runInBackground) {
$event->runInBackground();
}
foreach ($this->filters as $filter) {
$event->when($filter);
}
foreach ($this->rejects as $reject) {
$event->skip($reject);
}
}
/**
* Proxy missing methods onto the underlying schedule.
*/
public function __call(string $method, array $parameters): mixed
{
return $this->schedule->{$method}(...$parameters);
}
}

View File

@@ -0,0 +1,473 @@
<?php
namespace Illuminate\Console\Scheduling;
use BadMethodCallException;
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\Collection;
use Illuminate\Support\ProcessUtils;
use Illuminate\Support\Traits\Macroable;
use RuntimeException;
/**
* @mixin \Illuminate\Console\Scheduling\PendingEventAttributes
*/
class Schedule
{
use Macroable {
__call as macroCall;
}
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.
*
* @var \Illuminate\Console\Scheduling\Event[]
*/
protected $events = [];
/**
* The event mutex implementation.
*
* @var \Illuminate\Console\Scheduling\EventMutex
*/
protected $eventMutex;
/**
* The scheduling mutex implementation.
*
* @var \Illuminate\Console\Scheduling\SchedulingMutex
*/
protected $schedulingMutex;
/**
* The timezone the date should be evaluated on.
*
* @var \DateTimeZone|string
*/
protected $timezone;
/**
* The job dispatcher implementation.
*
* @var \Illuminate\Contracts\Bus\Dispatcher
*/
protected $dispatcher;
/**
* The cache of mutex results.
*
* @var array<string, bool>
*/
protected $mutexCache = [];
/**
* The attributes to pass to the event.
*
* @var \Illuminate\Console\Scheduling\PendingEventAttributes|null
*/
protected $attributes;
/**
* The schedule group attributes stack.
*
* @var array<int, PendingEventAttributes>
*/
protected array $groupStack = [];
/**
* Create a new schedule instance.
*
* @param \DateTimeZone|string|null $timezone
* @return void
*
* @throws \RuntimeException
*/
public function __construct($timezone = null)
{
$this->timezone = $timezone;
if (! class_exists(Container::class)) {
throw new RuntimeException(
'A container implementation is required to use the scheduler. Please install the illuminate/container package.'
);
}
$container = Container::getInstance();
$this->eventMutex = $container->bound(EventMutex::class)
? $container->make(EventMutex::class)
: $container->make(CacheEventMutex::class);
$this->schedulingMutex = $container->bound(SchedulingMutex::class)
? $container->make(SchedulingMutex::class)
: $container->make(CacheSchedulingMutex::class);
}
/**
* Add a new callback event to the schedule.
*
* @param string|callable $callback
* @param array $parameters
* @return \Illuminate\Console\Scheduling\CallbackEvent
*/
public function call($callback, array $parameters = [])
{
$this->events[] = $event = new CallbackEvent(
$this->eventMutex, $callback, $parameters, $this->timezone
);
$this->mergePendingAttributes($event);
return $event;
}
/**
* Add a new Artisan command event to the schedule.
*
* @param string $command
* @param array $parameters
* @return \Illuminate\Console\Scheduling\Event
*/
public function command($command, array $parameters = [])
{
if (class_exists($command)) {
$command = Container::getInstance()->make($command);
return $this->exec(
Application::formatCommandString($command->getName()), $parameters,
)->description($command->getDescription());
}
return $this->exec(
Application::formatCommandString($command), $parameters
);
}
/**
* Add a new job callback event to the schedule.
*
* @param object|string $job
* @param string|null $queue
* @param string|null $connection
* @return \Illuminate\Console\Scheduling\CallbackEvent
*/
public function job($job, $queue = null, $connection = null)
{
$jobName = $job;
if (! is_string($job)) {
$jobName = method_exists($job, 'displayName')
? $job->displayName()
: $job::class;
}
return $this->name($jobName)->call(function () use ($job, $queue, $connection) {
$job = is_string($job) ? Container::getInstance()->make($job) : $job;
if ($job instanceof ShouldQueue) {
$this->dispatchToQueue($job, $queue ?? $job->queue, $connection ?? $job->connection);
} else {
$this->dispatchNow($job);
}
});
}
/**
* Dispatch the given job to the queue.
*
* @param object $job
* @param string|null $queue
* @param string|null $connection
* @return void
*
* @throws \RuntimeException
*/
protected function dispatchToQueue($job, $queue, $connection)
{
if ($job instanceof Closure) {
if (! class_exists(CallQueuedClosure::class)) {
throw new RuntimeException(
'To enable support for closure jobs, please install the illuminate/queue package.'
);
}
$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)
);
}
/**
* Dispatch the given job right now.
*
* @param object $job
* @return void
*/
protected function dispatchNow($job)
{
$this->getDispatcher()->dispatchNow($job);
}
/**
* Add a new command event to the schedule.
*
* @param string $command
* @param array $parameters
* @return \Illuminate\Console\Scheduling\Event
*/
public function exec($command, array $parameters = [])
{
if (count($parameters)) {
$command .= ' '.$this->compileParameters($parameters);
}
$this->events[] = $event = new Event($this->eventMutex, $command, $this->timezone);
$this->mergePendingAttributes($event);
return $event;
}
/**
* Create new schedule group.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*
* @throws \RuntimeException
*/
public function group(Closure $events)
{
if ($this->attributes === null) {
throw new RuntimeException('Invoke an attribute method such as Schedule::daily() before defining a schedule group.');
}
$this->groupStack[] = $this->attributes;
$events($this);
array_pop($this->groupStack);
}
/**
* Merge the current group attributes with the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
protected function mergePendingAttributes(Event $event)
{
if (isset($this->attributes)) {
$this->attributes->mergeAttributes($event);
$this->attributes = null;
}
if (! empty($this->groupStack)) {
$group = end($this->groupStack);
$group->mergeAttributes($event);
}
}
/**
* Compile parameters for a command.
*
* @param array $parameters
* @return string
*/
protected function compileParameters(array $parameters)
{
return (new Collection($parameters))->map(function ($value, $key) {
if (is_array($value)) {
return $this->compileArrayInput($key, $value);
}
if (! is_numeric($value) && ! preg_match('/^(-.$|--.*)/i', $value)) {
$value = ProcessUtils::escapeArgument($value);
}
return is_numeric($key) ? $value : "{$key}={$value}";
})->implode(' ');
}
/**
* Compile array input for a command.
*
* @param string|int $key
* @param array $value
* @return string
*/
public function compileArrayInput($key, $value)
{
$value = (new Collection($value))->map(function ($value) {
return ProcessUtils::escapeArgument($value);
});
if (str_starts_with($key, '--')) {
$value = $value->map(function ($value) use ($key) {
return "{$key}={$value}";
});
} elseif (str_starts_with($key, '-')) {
$value = $value->map(function ($value) use ($key) {
return "{$key} {$value}";
});
}
return $value->implode(' ');
}
/**
* Determine if the server is allowed to run this event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeInterface $time
* @return bool
*/
public function serverShouldRun(Event $event, DateTimeInterface $time)
{
return $this->mutexCache[$event->mutexName()] ??= $this->schedulingMutex->create($event, $time);
}
/**
* Get all of the events on the schedule that are due.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return \Illuminate\Support\Collection
*/
public function dueEvents($app)
{
return (new Collection($this->events))->filter->isDue($app);
}
/**
* Get all of the events on the schedule.
*
* @return \Illuminate\Console\Scheduling\Event[]
*/
public function events()
{
return $this->events;
}
/**
* Specify the cache store that should be used to store mutexes.
*
* @param string $store
* @return $this
*/
public function useCache($store)
{
if ($this->eventMutex instanceof CacheAware) {
$this->eventMutex->useStore($store);
}
if ($this->schedulingMutex instanceof CacheAware) {
$this->schedulingMutex->useStore($store);
}
return $this;
}
/**
* Get the job dispatcher, if available.
*
* @return \Illuminate\Contracts\Bus\Dispatcher
*
* @throws \RuntimeException
*/
protected function getDispatcher()
{
if ($this->dispatcher === null) {
try {
$this->dispatcher = Container::getInstance()->make(Dispatcher::class);
} catch (BindingResolutionException $e) {
throw new RuntimeException(
'Unable to resolve the dispatcher from the service container. Please bind it or install the illuminate/bus package.',
is_int($e->getCode()) ? $e->getCode() : 0, $e
);
}
}
return $this->dispatcher;
}
/**
* Dynamically handle calls into the schedule instance.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public function __call($method, $parameters)
{
if (static::hasMacro($method)) {
return $this->macroCall($method, $parameters);
}
if (method_exists(PendingEventAttributes::class, $method)) {
$this->attributes ??= end($this->groupStack) ?: new PendingEventAttributes($this);
return $this->attributes->$method(...$parameters);
}
throw new BadMethodCallException(sprintf(
'Method %s::%s does not exist.', static::class, $method
));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:clear-cache')]
class ScheduleClearCacheCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'schedule:clear-cache';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete the cached mutex files created by scheduler';
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function handle(Schedule $schedule)
{
$mutexCleared = false;
foreach ($schedule->events($this->laravel) as $event) {
if ($event->mutex->exists($event)) {
$this->components->info(sprintf('Deleting mutex for [%s]', $event->command));
$event->mutex->forget($event);
$mutexCleared = true;
}
}
if (! $mutexCleared) {
$this->components->info('No mutex files were found.');
}
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Console\Events\ScheduledBackgroundTaskFinished;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Collection;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:finish')]
class ScheduleFinishCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schedule:finish {id} {code=0}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Handle the completion of a scheduled command';
/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = true;
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function handle(Schedule $schedule)
{
(new Collection($schedule->events()))->filter(function ($value) {
return $value->mutexName() == $this->argument('id');
})->each(function ($event) {
$event->finish($this->laravel, $this->argument('code'));
$this->laravel->make(Dispatcher::class)->dispatch(new ScheduledBackgroundTaskFinished($event));
});
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Support\Facades\Date;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:interrupt')]
class ScheduleInterruptCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'schedule:interrupt';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Interrupt the current schedule run';
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* Create a new schedule interrupt command.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @return void
*/
public function __construct(Cache $cache)
{
parent::__construct();
$this->cache = $cache;
}
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->cache->put('illuminate:schedule:interrupt', true, Date::now()->endOfMinute());
$this->components->info('Broadcasting schedule interrupt signal.');
}
}

View File

@@ -0,0 +1,304 @@
<?php
namespace Illuminate\Console\Scheduling;
use Closure;
use Cron\CronExpression;
use DateTimeZone;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionFunction;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Terminal;
#[AsCommand(name: 'schedule:list')]
class ScheduleListCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schedule:list
{--timezone= : The timezone that times should be displayed in}
{--next : Sort the listed tasks by their next due date}
';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List all scheduled tasks';
/**
* The terminal width resolver callback.
*
* @var \Closure|null
*/
protected static $terminalWidthResolver;
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*
* @throws \Exception
*/
public function handle(Schedule $schedule)
{
$events = new Collection($schedule->events());
if ($events->isEmpty()) {
$this->components->info('No scheduled tasks have been defined.');
return;
}
$terminalWidth = self::getTerminalWidth();
$expressionSpacing = $this->getCronExpressionSpacing($events);
$repeatExpressionSpacing = $this->getRepeatExpressionSpacing($events);
$timezone = new DateTimeZone($this->option('timezone') ?? config('app.timezone'));
$events = $this->sortEvents($events, $timezone);
$events = $events->map(function ($event) use ($terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone) {
return $this->listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone);
});
$this->line(
$events->flatten()->filter()->prepend('')->push('')->toArray()
);
}
/**
* Get the spacing to be used on each event row.
*
* @param \Illuminate\Support\Collection $events
* @return array<int, int>
*/
private function getCronExpressionSpacing($events)
{
$rows = $events->map(fn ($event) => array_map('mb_strlen', preg_split("/\s+/", $event->expression)));
return (new Collection($rows[0] ?? []))->keys()->map(fn ($key) => $rows->max($key))->all();
}
/**
* Get the spacing to be used on each event row.
*
* @param \Illuminate\Support\Collection $events
* @return int
*/
private function getRepeatExpressionSpacing($events)
{
return $events->map(fn ($event) => mb_strlen($this->getRepeatExpression($event)))->max();
}
/**
* List the given even in the console.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param int $terminalWidth
* @param array $expressionSpacing
* @param int $repeatExpressionSpacing
* @param \DateTimeZone $timezone
* @return array
*/
private function listEvent($event, $terminalWidth, $expressionSpacing, $repeatExpressionSpacing, $timezone)
{
$expression = $this->formatCronExpression($event->expression, $expressionSpacing);
$repeatExpression = str_pad($this->getRepeatExpression($event), $repeatExpressionSpacing);
$command = $event->command ?? '';
$description = $event->description ?? '';
if (! $this->output->isVerbose()) {
$command = $event->normalizeCommand($command);
}
if ($event instanceof CallbackEvent) {
$command = $event->getSummaryForDisplay();
if (in_array($command, ['Closure', 'Callback'])) {
$command = 'Closure at: '.$this->getClosureLocation($event);
}
}
$command = mb_strlen($command) > 1 ? "{$command} " : '';
$nextDueDateLabel = 'Next Due:';
$nextDueDate = $this->getNextDueDateForEvent($event, $timezone);
$nextDueDate = $this->output->isVerbose()
? $nextDueDate->format('Y-m-d H:i:s P')
: $nextDueDate->diffForHumans();
$hasMutex = $event->mutex->exists($event) ? 'Has Mutex ' : '';
$dots = str_repeat('.', max(
$terminalWidth - mb_strlen($expression.$repeatExpression.$command.$nextDueDateLabel.$nextDueDate.$hasMutex) - 8, 0
));
// Highlight the parameters...
$command = preg_replace("#(php artisan [\w\-:]+) (.+)#", '$1 <fg=yellow;options=bold>$2</>', $command);
return [sprintf(
' <fg=yellow>%s</> <fg=#6C7280>%s</> %s<fg=#6C7280>%s %s%s %s</>',
$expression,
$repeatExpression,
$command,
$dots,
$hasMutex,
$nextDueDateLabel,
$nextDueDate
), $this->output->isVerbose() && mb_strlen($description) > 1 ? sprintf(
' <fg=#6C7280>%s%s %s</>',
str_repeat(' ', mb_strlen($expression) + 2),
'⇁',
$description
) : ''];
}
/**
* Get the repeat expression for an event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return string
*/
private function getRepeatExpression($event)
{
return $event->isRepeatable() ? "{$event->repeatSeconds}s " : '';
}
/**
* Sort the events by due date if option set.
*
* @param \Illuminate\Support\Collection $events
* @param \DateTimeZone $timezone
* @return \Illuminate\Support\Collection
*/
private function sortEvents(\Illuminate\Support\Collection $events, DateTimeZone $timezone)
{
return $this->option('next')
? $events->sortBy(fn ($event) => $this->getNextDueDateForEvent($event, $timezone))
: $events;
}
/**
* Get the next due date for an event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeZone $timezone
* @return \Illuminate\Support\Carbon
*/
private function getNextDueDateForEvent($event, DateTimeZone $timezone)
{
$nextDueDate = Carbon::instance(
(new CronExpression($event->expression))
->getNextRunDate(Carbon::now()->setTimezone($event->timezone))
->setTimezone($timezone)
);
if (! $event->isRepeatable()) {
return $nextDueDate;
}
$previousDueDate = Carbon::instance(
(new CronExpression($event->expression))
->getPreviousRunDate(Carbon::now()->setTimezone($event->timezone), allowCurrentDate: true)
->setTimezone($timezone)
);
$now = Carbon::now()->setTimezone($event->timezone);
if (! $now->copy()->startOfMinute()->eq($previousDueDate)) {
return $nextDueDate;
}
return $now
->endOfSecond()
->ceilSeconds($event->repeatSeconds);
}
/**
* Format the cron expression based on the spacing provided.
*
* @param string $expression
* @param array<int, int> $spacing
* @return string
*/
private function formatCronExpression($expression, $spacing)
{
$expressions = preg_split("/\s+/", $expression);
return (new Collection($spacing))
->map(fn ($length, $index) => str_pad($expressions[$index], $length))
->implode(' ');
}
/**
* Get the file and line number for the event closure.
*
* @param \Illuminate\Console\Scheduling\CallbackEvent $event
* @return string
*/
private function getClosureLocation(CallbackEvent $event)
{
$callback = (new ReflectionClass($event))->getProperty('callback')->getValue($event);
if ($callback instanceof Closure) {
$function = new ReflectionFunction($callback);
return sprintf(
'%s:%s',
str_replace($this->laravel->basePath().DIRECTORY_SEPARATOR, '', $function->getFileName() ?: ''),
$function->getStartLine()
);
}
if (is_string($callback)) {
return $callback;
}
if (is_array($callback)) {
$className = is_string($callback[0]) ? $callback[0] : $callback[0]::class;
return sprintf('%s::%s', $className, $callback[1]);
}
return sprintf('%s::__invoke', $callback::class);
}
/**
* Get the terminal width.
*
* @return int
*/
public static function getTerminalWidth()
{
return is_null(static::$terminalWidthResolver)
? (new Terminal)->getWidth()
: call_user_func(static::$terminalWidthResolver);
}
/**
* Set a callback that should be used when resolving the terminal width.
*
* @param \Closure|null $resolver
* @return void
*/
public static function resolveTerminalWidthUsing($resolver)
{
static::$terminalWidthResolver = $resolver;
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Events\ScheduledTaskSkipped;
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Sleep;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;
#[AsCommand(name: 'schedule:run')]
class ScheduleRunCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'schedule:run';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the scheduled commands';
/**
* The schedule instance.
*
* @var \Illuminate\Console\Scheduling\Schedule
*/
protected $schedule;
/**
* The 24 hour timestamp this scheduler command started running.
*
* @var \Illuminate\Support\Carbon
*/
protected $startedAt;
/**
* Check if any events ran.
*
* @var bool
*/
protected $eventsRan = false;
/**
* The event dispatcher.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $dispatcher;
/**
* The exception handler.
*
* @var \Illuminate\Contracts\Debug\ExceptionHandler
*/
protected $handler;
/**
* The cache store implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
protected $cache;
/**
* The PHP binary used by the command.
*
* @var string
*/
protected $phpBinary;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
$this->startedAt = Date::now();
parent::__construct();
}
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @param \Illuminate\Contracts\Cache\Repository $cache
* @param \Illuminate\Contracts\Debug\ExceptionHandler $handler
* @return void
*/
public function handle(Schedule $schedule, Dispatcher $dispatcher, Cache $cache, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->cache = $cache;
$this->handler = $handler;
$this->phpBinary = Application::phpBinary();
$this->clearInterruptSignal();
$this->newLine();
$events = $this->schedule->dueEvents($this->laravel);
foreach ($events as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
if ($events->contains->isRepeatable()) {
$this->repeatEvents($events->filter->isRepeatable());
}
if (! $this->eventsRan) {
$this->components->info('No scheduled commands are ready to run.');
} else {
$this->newLine();
}
}
/**
* Run the given single server event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
protected function runSingleServerEvent($event)
{
if ($this->schedule->serverShouldRun($event, $this->startedAt)) {
$this->runEvent($event);
} else {
$this->components->info(sprintf(
'Skipping [%s], as command already run on another server.', $event->getSummaryForDisplay()
));
}
}
/**
* Run the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
protected function runEvent($event)
{
$summary = $event->getSummaryForDisplay();
$command = $event instanceof CallbackEvent
? $summary
: trim(str_replace($this->phpBinary, '', $event->command));
$description = sprintf(
'<fg=gray>%s</> Running [%s]%s',
Carbon::now()->format('Y-m-d H:i:s'),
$command,
$event->runInBackground ? ' in background' : '',
);
$this->components->task($description, function () use ($event) {
$this->dispatcher->dispatch(new ScheduledTaskStarting($event));
$start = microtime(true);
try {
$event->run($this->laravel);
$this->dispatcher->dispatch(new ScheduledTaskFinished(
$event,
round(microtime(true) - $start, 2)
));
$this->eventsRan = true;
} catch (Throwable $e) {
$this->dispatcher->dispatch(new ScheduledTaskFailed($event, $e));
$this->handler->report($e);
}
return $event->exitCode == 0;
});
if (! $event instanceof CallbackEvent) {
$this->components->bulletList([
$event->getSummaryForDisplay(),
]);
}
}
/**
* Run the given repeating events.
*
* @param \Illuminate\Support\Collection<\Illuminate\Console\Scheduling\Event> $events
* @return void
*/
protected function repeatEvents($events)
{
$hasEnteredMaintenanceMode = false;
while (Date::now()->lte($this->startedAt->endOfMinute())) {
foreach ($events as $event) {
if ($this->shouldInterrupt()) {
return;
}
if (! $event->shouldRepeatNow()) {
continue;
}
$hasEnteredMaintenanceMode = $hasEnteredMaintenanceMode || $this->laravel->isDownForMaintenance();
if ($hasEnteredMaintenanceMode && ! $event->runsInMaintenanceMode()) {
continue;
}
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
Sleep::usleep(100000);
}
}
/**
* Determine if the schedule run should be interrupted.
*
* @return bool
*/
protected function shouldInterrupt()
{
return $this->cache->get('illuminate:schedule:interrupt', false);
}
/**
* Ensure the interrupt signal is cleared.
*
* @return void
*/
protected function clearInterruptSignal()
{
$this->cache->forget('illuminate:schedule:interrupt');
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use function Laravel\Prompts\select;
#[AsCommand(name: 'schedule:test')]
class ScheduleTestCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schedule:test {--name= : The name of the scheduled command to run}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run a scheduled command';
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function handle(Schedule $schedule)
{
$phpBinary = Application::phpBinary();
$commands = $schedule->events();
$commandNames = [];
foreach ($commands as $command) {
$commandNames[] = $command->command ?? $command->getSummaryForDisplay();
}
if (empty($commandNames)) {
return $this->components->info('No scheduled commands have been defined.');
}
if (! empty($name = $this->option('name'))) {
$commandBinary = $phpBinary.' '.Application::artisanBinary();
$matches = array_filter($commandNames, function ($commandName) use ($commandBinary, $name) {
return trim(str_replace($commandBinary, '', $commandName)) === $name;
});
if (count($matches) !== 1) {
$this->components->info('No matching scheduled command found.');
return;
}
$index = key($matches);
} else {
$index = $this->getSelectedCommandByIndex($commandNames);
}
$event = $commands[$index];
$summary = $event->getSummaryForDisplay();
$command = $event instanceof CallbackEvent
? $summary
: trim(str_replace($phpBinary, '', $event->command));
$description = sprintf(
'Running [%s]%s',
$command,
$event->runInBackground ? ' in background' : '',
);
$this->components->task($description, fn () => $event->run($this->laravel));
if (! $event instanceof CallbackEvent) {
$this->components->bulletList([$event->getSummaryForDisplay()]);
}
$this->newLine();
}
/**
* Get the selected command name by index.
*
* @param array $commandNames
* @return int
*/
protected function getSelectedCommandByIndex(array $commandNames)
{
if (count($commandNames) !== count(array_unique($commandNames))) {
// Some commands (likely closures) have the same name, append unique indexes to each one...
$uniqueCommandNames = array_map(function ($index, $value) {
return "$value [$index]";
}, array_keys($commandNames), $commandNames);
$selectedCommand = select('Which command would you like to run?', $uniqueCommandNames);
preg_match('/\[(\d+)\]/', $selectedCommand, $choice);
return (int) $choice[1];
} else {
return array_search(
select('Which command would you like to run?', $commandNames),
$commandNames
);
}
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Illuminate\Support\ProcessUtils;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\Process;
#[AsCommand(name: 'schedule:work')]
class ScheduleWorkCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'schedule:work {--run-output-file= : The file to direct <info>schedule:run</info> output to}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start the schedule worker';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->components->info(
'Running scheduled tasks.',
$this->getLaravel()->environment('local') ? OutputInterface::VERBOSITY_NORMAL : OutputInterface::VERBOSITY_VERBOSE
);
[$lastExecutionStartedAt, $executions] = [Carbon::now()->subMinutes(10), []];
$command = Application::formatCommandString('schedule:run');
if ($this->option('run-output-file')) {
$command .= ' >> '.ProcessUtils::escapeArgument($this->option('run-output-file')).' 2>&1';
}
while (true) {
usleep(100 * 1000);
if (Carbon::now()->second === 0 &&
! Carbon::now()->startOfMinute()->equalTo($lastExecutionStartedAt)) {
$executions[] = $execution = Process::fromShellCommandline($command);
$execution->start();
$lastExecutionStartedAt = Carbon::now()->startOfMinute();
}
foreach ($executions as $key => $execution) {
$output = $execution->getIncrementalOutput().
$execution->getIncrementalErrorOutput();
$this->output->write(ltrim($output, "\n"));
if (! $execution->isRunning()) {
unset($executions[$key]);
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Illuminate\Console\Scheduling;
use DateTimeInterface;
interface SchedulingMutex
{
/**
* Attempt to obtain a scheduling mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeInterface $time
* @return bool
*/
public function create(Event $event, DateTimeInterface $time);
/**
* Determine if a scheduling mutex exists for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @param \DateTimeInterface $time
* @return bool
*/
public function exists(Event $event, DateTimeInterface $time);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,48 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Question\ChoiceQuestion;
class Choice extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $question
* @param array<array-key, string> $choices
* @param mixed $default
* @param int $attempts
* @param bool $multiple
* @return mixed
*/
public function render($question, $choices, $default = null, $attempts = null, $multiple = false)
{
return $this->usingQuestionHelper(
fn () => $this->output->askQuestion(
$this->getChoiceQuestion($question, $choices, $default)
->setMaxAttempts($attempts)
->setMultiselect($multiple)
),
);
}
/**
* Get a ChoiceQuestion instance that handles array keys like Prompts.
*
* @param string $question
* @param array $choices
* @param mixed $default
* @return \Symfony\Component\Console\Question\ChoiceQuestion
*/
protected function getChoiceQuestion($question, $choices, $default)
{
return new class($question, $choices, $default) extends ChoiceQuestion
{
protected function isAssoc(array $array): bool
{
return ! array_is_list($array);
}
};
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,23 @@
<?php
namespace Illuminate\Console\View\Components\Mutators;
use Illuminate\Support\Stringable;
class EnsureNoPunctuation
{
/**
* Ensures the given string does not end with punctuation.
*
* @param string $string
* @return string
*/
public function __invoke($string)
{
if ((new Stringable($string))->endsWith(['.', '?', '!', ':'])) {
return substr_replace($string, '', -1);
}
return $string;
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace Illuminate\Console\View\Components\Mutators;
use Illuminate\Support\Stringable;
class EnsurePunctuation
{
/**
* Ensures the given string ends with punctuation.
*
* @param string $string
* @return string
*/
public function __invoke($string)
{
if (! (new Stringable($string))->endsWith(['.', '?', '!', ':'])) {
return "$string.";
}
return $string;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Illuminate\Console\View\Components\Mutators;
class EnsureRelativePaths
{
/**
* Ensures the given string only contains relative paths.
*
* @param string $string
* @return string
*/
public function __invoke($string)
{
if (function_exists('app') && app()->has('path.base')) {
$string = str_replace(base_path().'/', '', $string);
}
return $string;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Question\Question;
class Secret extends Component
{
/**
* Renders the component using the given arguments.
*
* @param string $question
* @param bool $fallback
* @return mixed
*/
public function render($question, $fallback = true)
{
$question = new Question($question);
$question->setHidden(true)->setHiddenFallback($fallback);
return $this->usingQuestionHelper(fn () => $this->output->askQuestion($question));
}
}

View File

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

View File

@@ -0,0 +1,61 @@
<?php
namespace Illuminate\Console\View\Components;
use Illuminate\Support\InteractsWithTime;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use function Termwind\terminal;
class Task extends Component
{
use InteractsWithTime;
/**
* Renders the component using the given arguments.
*
* @param string $description
* @param (callable(): bool)|null $task
* @param int $verbosity
* @return void
*/
public function render($description, $task = null, $verbosity = OutputInterface::VERBOSITY_NORMAL)
{
$description = $this->mutate($description, [
Mutators\EnsureDynamicContentIsHighlighted::class,
Mutators\EnsureNoPunctuation::class,
Mutators\EnsureRelativePaths::class,
]);
$descriptionWidth = mb_strlen(preg_replace("/\<[\w=#\/\;,:.&,%?]+\>|\\e\[\d+m/", '$1', $description) ?? '');
$this->output->write(" $description ", false, $verbosity);
$startTime = microtime(true);
$result = false;
try {
$result = ($task ?: fn () => true)();
} catch (Throwable $e) {
throw $e;
} finally {
$runTime = $task
? (' '.$this->runTimeForHumans($startTime))
: '';
$runTimeWidth = mb_strlen($runTime);
$width = min(terminal()->width(), 150);
$dots = max($width - $descriptionWidth - $runTimeWidth - 10, 0);
$this->output->write(str_repeat('<fg=gray>.</>', $dots), false, $verbosity);
$this->output->write("<fg=gray>$runTime</>", false, $verbosity);
$this->output->writeln(
$result !== false ? ' <fg=green;options=bold>DONE</>' : ' <fg=red;options=bold>FAIL</>',
$verbosity,
);
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
{
"name": "illuminate/console",
"description": "The Illuminate Console package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^8.2",
"ext-mbstring": "*",
"illuminate/collections": "^11.0",
"illuminate/contracts": "^11.0",
"illuminate/macroable": "^11.0",
"illuminate/support": "^11.0",
"illuminate/view": "^11.0",
"laravel/prompts": "^0.1.20|^0.2|^0.3",
"nunomaduro/termwind": "^2.0",
"symfony/console": "^7.0.3",
"symfony/polyfill-php83": "^1.31",
"symfony/process": "^7.0.3"
},
"autoload": {
"psr-4": {
"Illuminate\\Console\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "11.x-dev"
}
},
"suggest": {
"ext-pcntl": "Required to use signal trapping.",
"dragonmantank/cron-expression": "Required to use scheduler (^3.3.2).",
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (^7.8).",
"illuminate/bus": "Required to use the scheduled job dispatcher (^11.0).",
"illuminate/container": "Required to use the scheduler (^11.0).",
"illuminate/filesystem": "Required to use the generator command (^11.0).",
"illuminate/queue": "Required to use closures for scheduled jobs (^11.0)."
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}

View File

@@ -0,0 +1,3 @@
<div class="w-full mx-2 py-1 mt-1 bg-yellow text-black text-center uppercase">
<?php echo htmlspecialchars($content) ?>
</div>

View File

@@ -0,0 +1,7 @@
<div>
<?php foreach ($elements as $element) { ?>
<div class="text-gray mx-2">
⇂ <?php echo htmlspecialchars($element) ?>
</div>
<?php } ?>
</div>

View File

@@ -0,0 +1,8 @@
<div class="mx-2 mb-1 mt-<?php echo $marginTop ?>">
<span class="px-1 bg-<?php echo $bgColor ?> text-<?php echo $fgColor ?> uppercase"><?php echo $title ?></span>
<span class="<?php if ($title) {
echo 'ml-1';
} ?>">
<?php echo htmlspecialchars($content) ?>
</span>
</div>

View File

@@ -0,0 +1,11 @@
<div class="flex mx-2 max-w-150">
<span>
<?php echo htmlspecialchars($first) ?>
</span>
<span class="flex-1 content-repeat-[.] text-gray ml-1"></span>
<?php if ($second !== '') { ?>
<span class="ml-1">
<?php echo htmlspecialchars($second) ?>
</span>
<?php } ?>
</div>