Upgrade framework

This commit is contained in:
2023-11-14 16:54:35 +01:00
parent 1648a5cd42
commit 4fcf6fffcc
10548 changed files with 693138 additions and 466698 deletions

View File

@@ -3,16 +3,25 @@
namespace Illuminate\Console;
use Closure;
use Illuminate\Contracts\Events\Dispatcher;
use Symfony\Component\Process\ProcessUtils;
use Illuminate\Console\Events\ArtisanStarting;
use Illuminate\Console\Events\CommandFinished;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Contracts\Console\Application as ApplicationContract;
use Illuminate\Contracts\Container\Container;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Console\Output\BufferedOutput;
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 Illuminate\Contracts\Console\Application as ApplicationContract;
use Symfony\Component\Console\Exception\CommandNotFoundException;
use Symfony\Component\Console\Input\ArgvInput;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Input\InputDefinition;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\StringInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Process\PhpExecutableFinder;
class Application extends SymfonyApplication implements ApplicationContract
{
@@ -23,6 +32,13 @@ class Application extends SymfonyApplication implements ApplicationContract
*/
protected $laravel;
/**
* The event dispatcher instance.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $events;
/**
* The output from the previous command.
*
@@ -37,6 +53,13 @@ class Application extends SymfonyApplication implements ApplicationContract
*/
protected static $bootstrappers = [];
/**
* A map of command names to classes.
*
* @var array
*/
protected $commandMap = [];
/**
* Create a new Artisan console application.
*
@@ -50,14 +73,41 @@ class Application extends SymfonyApplication implements ApplicationContract
parent::__construct('Laravel Framework', $version);
$this->laravel = $laravel;
$this->events = $events;
$this->setAutoExit(false);
$this->setCatchExceptions(false);
$events->dispatch(new Events\ArtisanStarting($this));
$this->events->dispatch(new ArtisanStarting($this));
$this->bootstrap();
}
/**
* {@inheritdoc}
*
* @return int
*/
public function run(InputInterface $input = null, OutputInterface $output = null): int
{
$commandName = $this->getCommandName(
$input = $input ?: new ArgvInput
);
$this->events->dispatch(
new CommandStarting(
$commandName, $input, $output = $output ?: new BufferedConsoleOutput
)
);
$exitCode = parent::run($input, $output);
$this->events->dispatch(
new CommandFinished($commandName, $input, $output, $exitCode)
);
return $exitCode;
}
/**
* Determine the proper PHP executable.
*
@@ -75,7 +125,7 @@ class Application extends SymfonyApplication implements ApplicationContract
*/
public static function artisanBinary()
{
return defined('ARTISAN_BINARY') ? ProcessUtils::escapeArgument(ARTISAN_BINARY) : 'artisan';
return ProcessUtils::escapeArgument(defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan');
}
/**
@@ -112,7 +162,7 @@ class Application extends SymfonyApplication implements ApplicationContract
}
}
/*
/**
* Clear the console application bootstrappers.
*
* @return void
@@ -127,22 +177,48 @@ class Application extends SymfonyApplication implements ApplicationContract
*
* @param string $command
* @param array $parameters
* @param \Symfony\Component\Console\Output\OutputInterface $outputBuffer
* @param \Symfony\Component\Console\Output\OutputInterface|null $outputBuffer
* @return int
*
* @throws \Symfony\Component\Console\Exception\CommandNotFoundException
*/
public function call($command, array $parameters = [], $outputBuffer = null)
{
$parameters = collect($parameters)->prepend($command);
[$command, $input] = $this->parseCommand($command, $parameters);
$this->lastOutput = $outputBuffer ?: new BufferedOutput;
if (! $this->has($command)) {
throw new CommandNotFoundException(sprintf('The command "%s" does not exist.', $command));
}
$this->setCatchExceptions(false);
return $this->run(
$input, $this->lastOutput = $outputBuffer ?: new BufferedOutput
);
}
$result = $this->run(new ArrayInput($parameters->toArray()), $this->lastOutput);
/**
* 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;
$this->setCatchExceptions(true);
$command = $this->laravel->make($command)->getName();
}
return $result;
if (! isset($callingClass) && empty($parameters)) {
$command = $this->getCommandName($input = new StringInput($command));
} else {
array_unshift($parameters, $command);
$input = new ArrayInput($parameters);
}
return [$command, $input];
}
/**
@@ -152,7 +228,9 @@ class Application extends SymfonyApplication implements ApplicationContract
*/
public function output()
{
return $this->lastOutput ? $this->lastOutput->fetch() : '';
return $this->lastOutput && method_exists($this->lastOutput, 'fetch')
? $this->lastOutput->fetch()
: '';
}
/**
@@ -184,11 +262,21 @@ class Application extends SymfonyApplication implements ApplicationContract
/**
* Add a command, resolving through the application.
*
* @param string $command
* @return \Symfony\Component\Console\Command\Command
* @param \Illuminate\Console\Command|string $command
* @return \Symfony\Component\Console\Command\Command|null
*/
public function resolve($command)
{
if (is_subclass_of($command, SymfonyCommand::class) && ($commandName = $command::getDefaultName())) {
$this->commandMap[$commandName] = $command;
return null;
}
if ($command instanceof Command) {
return $this->add($command);
}
return $this->add($this->laravel->make($command));
}
@@ -210,13 +298,25 @@ class Application extends SymfonyApplication implements ApplicationContract
}
/**
* Get the default input definitions for the applications.
* 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
*/
protected function getDefaultInputDefinition()
protected function getDefaultInputDefinition(): InputDefinition
{
return tap(parent::getDefaultInputDefinition(), function ($definition) {
$definition->addOption($this->getEnvironmentOption());

View File

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

View File

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

View File

@@ -2,19 +2,22 @@
namespace Illuminate\Console;
use Illuminate\Contracts\Support\Arrayable;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Question\Question;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Illuminate\Console\View\Components\Factory;
use Illuminate\Contracts\Console\Isolatable;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Command extends SymfonyCommand
{
use Concerns\CallsCommands,
Concerns\HasParameters,
Concerns\InteractsWithIO,
Concerns\InteractsWithSignals,
Macroable;
/**
* The Laravel application instance.
*
@@ -22,20 +25,6 @@ class Command extends SymfonyCommand
*/
protected $laravel;
/**
* The input interface implementation.
*
* @var \Symfony\Component\Console\Input\InputInterface
*/
protected $input;
/**
* The output interface implementation.
*
* @var \Illuminate\Console\OutputStyle
*/
protected $output;
/**
* The name and signature of the console command.
*
@@ -53,10 +42,17 @@ class Command extends SymfonyCommand
/**
* The console command description.
*
* @var string
* @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.
*
@@ -64,26 +60,6 @@ class Command extends SymfonyCommand
*/
protected $hidden = false;
/**
* 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,
];
/**
* Create a new console command instance.
*
@@ -103,13 +79,23 @@ class Command extends SymfonyCommand
// Once we have constructed the command, we'll set the description and other
// related properties of the command. If a signature wasn't used to build
// the command we'll set the arguments and the options on this command.
$this->setDescription($this->description);
if (! isset($this->description)) {
$this->setDescription((string) static::getDefaultDescription());
} else {
$this->setDescription((string) $this->description);
}
$this->setHidden($this->hidden);
$this->setHelp((string) $this->help);
$this->setHidden($this->isHidden());
if (! isset($this->signature)) {
$this->specifyParameters();
}
if ($this instanceof Isolatable) {
$this->configureIsolation();
}
}
/**
@@ -119,39 +105,31 @@ class Command extends SymfonyCommand
*/
protected function configureUsingFluentDefinition()
{
list($name, $arguments, $options) = Parser::parse($this->signature);
[$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.
foreach ($arguments as $argument) {
$this->getDefinition()->addArgument($argument);
}
foreach ($options as $option) {
$this->getDefinition()->addOption($option);
}
$this->getDefinition()->addArguments($arguments);
$this->getDefinition()->addOptions($options);
}
/**
* Specify the arguments and options on the command.
* Configure the console command for isolation.
*
* @return void
*/
protected function specifyParameters()
protected function configureIsolation()
{
// 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) {
call_user_func_array([$this, 'addArgument'], $arguments);
}
foreach ($this->getOptions() as $options) {
call_user_func_array([$this, 'addOption'], $options);
}
$this->getDefinition()->addOption(new InputOption(
'isolated',
null,
InputOption::VALUE_OPTIONAL,
'Do not run the command if another instance of the command is already running',
false
));
}
/**
@@ -161,11 +139,21 @@ class Command extends SymfonyCommand
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
public function run(InputInterface $input, OutputInterface $output)
public function run(InputInterface $input, OutputInterface $output): int
{
return parent::run(
$this->input = $input, $this->output = new OutputStyle($input, $output)
$this->output = $this->laravel->make(
OutputStyle::class, ['input' => $input, 'output' => $output]
);
$this->components = $this->laravel->make(Factory::class, ['output' => $this->output]);
try {
return parent::run(
$this->input = $input, $this->output
);
} finally {
$this->untrap();
}
}
/**
@@ -173,379 +161,87 @@ class Command extends SymfonyCommand
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return mixed
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$method = method_exists($this, 'handle') ? 'handle' : 'fire';
if ($this instanceof Isolatable && $this->option('isolated') !== false &&
! $this->commandIsolationMutex()->create($this)) {
$this->comment(sprintf(
'The [%s] command is already running.', $this->getName()
));
return $this->laravel->call([$this, $method]);
return (int) (is_numeric($this->option('isolated'))
? $this->option('isolated')
: self::SUCCESS);
}
$method = method_exists($this, 'handle') ? 'handle' : '__invoke';
try {
return (int) $this->laravel->call([$this, $method]);
} finally {
if ($this instanceof Isolatable && $this->option('isolated') !== false) {
$this->commandIsolationMutex()->forget($this);
}
}
}
/**
* Call another console command.
* Get a command isolation mutex instance for the command.
*
* @param string $command
* @param array $arguments
* @return int
* @return \Illuminate\Console\CommandMutex
*/
public function call($command, array $arguments = [])
protected function commandIsolationMutex()
{
$arguments['command'] = $command;
return $this->getApplication()->find($command)->run(
new ArrayInput($arguments), $this->output
);
return $this->laravel->bound(CommandMutex::class)
? $this->laravel->make(CommandMutex::class)
: $this->laravel->make(CacheCommandMutex::class);
}
/**
* Call another console command silently.
* Resolve the console command instance for the given command.
*
* @param string $command
* @param array $arguments
* @return int
* @param \Symfony\Component\Console\Command\Command|string $command
* @return \Symfony\Component\Console\Command\Command
*/
public function callSilent($command, array $arguments = [])
protected function resolveCommand($command)
{
$arguments['command'] = $command;
if (! class_exists($command)) {
return $this->getApplication()->find($command);
}
return $this->getApplication()->find($command)->run(
new ArrayInput($arguments), new NullOutput
);
$command = $this->laravel->make($command);
if ($command instanceof SymfonyCommand) {
$command->setApplication($this->getApplication());
}
if ($command instanceof self) {
$command->setLaravel($this->getLaravel());
}
return $command;
}
/**
* Determine if the given argument is present.
* {@inheritdoc}
*
* @param string|int $name
* @return bool
*/
public function hasArgument($name)
public function isHidden(): bool
{
return $this->input->hasArgument($name);
return $this->hidden;
}
/**
* Get the value of a command argument.
*
* @param string $key
* @return string|array
* {@inheritdoc}
*/
public function argument($key = null)
public function setHidden(bool $hidden = true): static
{
if (is_null($key)) {
return $this->input->getArguments();
}
parent::setHidden($this->hidden = $hidden);
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 $key
* @return string|array
*/
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 $default
* @return string
*/
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 $choices
* @param string $default
* @return string
*/
public function anticipate($question, array $choices, $default = null)
{
return $this->askWithCompletion($question, $choices, $default);
}
/**
* Prompt the user for input with auto completion.
*
* @param string $question
* @param array $choices
* @param string $default
* @return string
*/
public function askWithCompletion($question, array $choices, $default = null)
{
$question = new Question($question, $default);
$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 string
*/
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 $default
* @param mixed $attempts
* @param bool $multiple
* @return string
*/
public function choice($question, array $choices, $default = null, $attempts = null, $multiple = null)
{
$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 string $style
* @return void
*/
public function table(array $headers, $rows, $style = 'default')
{
$table = new Table($this->output);
if ($rows instanceof Arrayable) {
$rows = $rows->toArray();
}
$table->setHeaders($headers)->setRows($rows)->setStyle($style)->render();
}
/**
* Write a string as information output.
*
* @param string $string
* @param null|int|string $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 $style
* @param null|int|string $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 null|int|string $verbosity
* @return void
*/
public function comment($string, $verbosity = null)
{
$this->line($string, 'comment', $verbosity);
}
/**
* Write a string as question output.
*
* @param string $string
* @param null|int|string $verbosity
* @return void
*/
public function question($string, $verbosity = null)
{
$this->line($string, 'question', $verbosity);
}
/**
* Write a string as error output.
*
* @param string $string
* @param null|int|string $verbosity
* @return void
*/
public function error($string, $verbosity = null)
{
$this->line($string, 'error', $verbosity);
}
/**
* Write a string as warning output.
*
* @param string $string
* @param null|int|string $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
* @return void
*/
public function alert($string)
{
$this->comment(str_repeat('*', strlen($string) + 12));
$this->comment('* '.$string.' *');
$this->comment(str_repeat('*', strlen($string) + 12));
$this->output->writeln('');
}
/**
* 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 $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 console command arguments.
*
* @return array
*/
protected function getArguments()
{
return [];
}
/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [];
}
/**
* Get the output implementation.
*
* @return \Symfony\Component\Console\Output\OutputInterface
*/
public function getOutput()
{
return $this->output;
return $this;
}
/**

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,104 @@
<?php
namespace Illuminate\Console\Concerns;
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 the 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;
return $this->resolveCommand($command)->run(
$this->createInputFromArguments($arguments), $output
);
}
/**
* 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 collect($this->option())->only([
'ansi',
'no-ansi',
'no-interaction',
'quiet',
'verbose',
])->filter()->mapWithKeys(function ($value, $key) {
return ["--{$key}" => $value];
})->all();
}
}

View File

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

View File

@@ -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,453 @@
<?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 $value) {
$callback($value, $bar);
$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;
}
}

View File

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

View File

@@ -2,8 +2,6 @@
namespace Illuminate\Console;
use Closure;
trait ConfirmableTrait
{
/**
@@ -15,23 +13,25 @@ trait ConfirmableTrait
* @param \Closure|bool|null $callback
* @return bool
*/
public function confirmToProceed($warning = 'Application In Production!', $callback = null)
public function confirmToProceed($warning = 'Application In Production', $callback = null)
{
$callback = is_null($callback) ? $this->getDefaultConfirmCallback() : $callback;
$shouldConfirm = $callback instanceof Closure ? call_user_func($callback) : $callback;
$shouldConfirm = value($callback);
if ($shouldConfirm) {
if ($this->option('force')) {
if ($this->hasOption('force') && $this->option('force')) {
return true;
}
$this->alert($warning);
$this->components->alert($warning);
$confirmed = $this->confirm('Do you really wish to run this command?');
$confirmed = $this->components->confirm('Do you really wish to run this command?');
if (! $confirmed) {
$this->comment('Command Cancelled!');
$this->newLine();
$this->components->warn('Command canceled.');
return false;
}
@@ -48,7 +48,7 @@ trait ConfirmableTrait
protected function getDefaultConfirmCallback()
{
return function () {
return $this->getLaravel()->environment() == 'production';
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,13 @@
<?php
namespace Illuminate\Console\Contracts;
interface NewLineAware
{
/**
* Whether a newline has already been written.
*
* @return bool
*/
public function newLineWritten();
}

View File

@@ -1,18 +0,0 @@
<?php
namespace Illuminate\Console;
use Illuminate\Container\Container;
trait DetectsApplicationNamespace
{
/**
* Get the application namespace.
*
* @return string
*/
protected function getAppNamespace()
{
return Container::getInstance()->getNamespace();
}
}

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

@@ -2,8 +2,9 @@
namespace Illuminate\Console;
use Illuminate\Support\Str;
use Illuminate\Console\Concerns\CreatesMatchingTest;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputArgument;
abstract class GeneratorCommand extends Command
@@ -22,6 +23,95 @@ abstract class GeneratorCommand extends Command
*/
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',
'print',
'private',
'protected',
'public',
'readonly',
'require',
'require_once',
'return',
'static',
'switch',
'throw',
'trait',
'true',
'try',
'unset',
'use',
'var',
'while',
'xor',
'yield',
'__CLASS__',
'__DIR__',
'__FILE__',
'__FUNCTION__',
'__LINE__',
'__METHOD__',
'__NAMESPACE__',
'__TRAIT__',
];
/**
* Create a new controller creator command instance.
*
@@ -32,6 +122,10 @@ abstract class GeneratorCommand extends Command
{
parent::__construct();
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
$this->addTestOptions();
}
$this->files = $files;
}
@@ -46,18 +140,31 @@ abstract class GeneratorCommand extends Command
* Execute the console command.
*
* @return bool|null
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function fire()
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);
// First we will check to see if the class already exists. If it does, we don't want
// 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->alreadyExists($this->getNameInput())) {
$this->error($this->type.' already exists!');
if ((! $this->hasOption('force') ||
! $this->option('force')) &&
$this->alreadyExists($this->getNameInput())) {
$this->components->error($this->type.' already exists.');
return false;
}
@@ -67,9 +174,17 @@ abstract class GeneratorCommand extends Command
// stub files so that it gets the correctly formatted namespace and class name.
$this->makeDirectory($path);
$this->files->put($path, $this->buildClass($name));
$this->files->put($path, $this->sortImports($this->buildClass($name)));
$this->info($this->type.' created successfully.');
$info = $this->type;
if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) {
if ($this->handleTestCreation($path)) {
$info .= ' and test';
}
}
$this->components->info(sprintf('%s [%s] created successfully.', $info, $path));
}
/**
@@ -80,19 +195,44 @@ abstract class GeneratorCommand extends Command
*/
protected function qualifyClass($name)
{
$name = ltrim($name, '\\/');
$name = str_replace('/', '\\', $name);
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($name, $rootNamespace)) {
return $name;
}
$name = str_replace('/', '\\', $name);
return $this->qualifyClass(
$this->getDefaultNamespace(trim($rootNamespace, '\\')).'\\'.$name
);
}
/**
* Qualify the given model class base name.
*
* @param string $model
* @return string
*/
protected function qualifyModel(string $model)
{
$model = ltrim($model, '\\/');
$model = str_replace('/', '\\', $model);
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($model, $rootNamespace)) {
return $model;
}
return is_dir(app_path('Models'))
? $rootNamespace.'Models\\'.$model
: $rootNamespace.$model;
}
/**
* Get the default namespace for the class.
*
@@ -123,7 +263,7 @@ abstract class GeneratorCommand extends Command
*/
protected function getPath($name)
{
$name = str_replace_first($this->rootNamespace(), '', $name);
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'.php';
}
@@ -148,6 +288,8 @@ abstract class GeneratorCommand extends Command
*
* @param string $name
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function buildClass($name)
{
@@ -165,11 +307,19 @@ abstract class GeneratorCommand extends Command
*/
protected function replaceNamespace(&$stub, $name)
{
$stub = str_replace(
['DummyNamespace', 'DummyRootNamespace'],
[$this->getNamespace($name), $this->rootNamespace()],
$stub
);
$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;
}
@@ -196,7 +346,26 @@ abstract class GeneratorCommand extends Command
{
$class = str_replace($this->getNamespace($name).'\\', '', $name);
return str_replace('DummyClass', $class, $stub);
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;
}
/**
@@ -219,6 +388,46 @@ abstract class GeneratorCommand extends Command
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)
{
$name = strtolower($name);
return in_array($name, $this->reservedNames);
}
/**
* 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.
*

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

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

@@ -2,10 +2,9 @@
namespace Illuminate\Console;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
class Parser
{
@@ -21,10 +20,8 @@ class Parser
{
$name = static::name($expression);
if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches)) {
if (count($matches[1])) {
return array_merge([$name], static::parameters($matches[1]));
}
if (preg_match_all('/\{\s*(.*?)\s*\}/', $expression, $matches) && count($matches[1])) {
return array_merge([$name], static::parameters($matches[1]));
}
return [$name, [], []];
@@ -35,13 +32,11 @@ class Parser
*
* @param string $expression
* @return string
*
* @throws \InvalidArgumentException
*/
protected static function name($expression)
{
if (trim($expression) === '') {
throw new InvalidArgumentException('Console command definition is empty.');
}
if (! preg_match('/[^\s]+/', $expression, $matches)) {
throw new InvalidArgumentException('Unable to determine command name from signature.');
}
@@ -80,15 +75,17 @@ class Parser
*/
protected static function parseArgument($token)
{
list($token, $description) = static::extractDescription($token);
[$token, $description] = static::extractDescription($token);
switch (true) {
case Str::endsWith($token, '?*'):
case str_ends_with($token, '?*'):
return new InputArgument(trim($token, '?*'), InputArgument::IS_ARRAY, $description);
case Str::endsWith($token, '*'):
case str_ends_with($token, '*'):
return new InputArgument(trim($token, '*'), InputArgument::IS_ARRAY | InputArgument::REQUIRED, $description);
case Str::endsWith($token, '?'):
case str_ends_with($token, '?'):
return new InputArgument(trim($token, '?'), InputArgument::OPTIONAL, $description);
case preg_match('/(.+)\=\*(.+)/', $token, $matches):
return new InputArgument($matches[1], InputArgument::IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
case preg_match('/(.+)\=(.+)/', $token, $matches):
return new InputArgument($matches[1], InputArgument::OPTIONAL, $description, $matches[2]);
default:
@@ -104,7 +101,7 @@ class Parser
*/
protected static function parseOption($token)
{
list($token, $description) = static::extractDescription($token);
[$token, $description] = static::extractDescription($token);
$matches = preg_split('/\s*\|\s*/', $token, 2);
@@ -116,10 +113,12 @@ class Parser
}
switch (true) {
case Str::endsWith($token, '='):
case str_ends_with($token, '='):
return new InputOption(trim($token, '='), $shortcut, InputOption::VALUE_OPTIONAL, $description);
case Str::endsWith($token, '=*'):
case str_ends_with($token, '=*'):
return new InputOption(trim($token, '=*'), $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description);
case preg_match('/(.+)\=\*(.+)/', $token, $matches):
return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, $description, preg_split('/,\s?/', $matches[2]));
case preg_match('/(.+)\=(.+)/', $token, $matches):
return new InputOption($matches[1], $shortcut, InputOption::VALUE_OPTIONAL, $description, $matches[2]);
default:
@@ -137,6 +136,6 @@ class Parser
{
$parts = preg_split('/\s+:\s+/', trim($token), 2);
return count($parts) === 2 ? $parts : [$token, null];
return count($parts) === 2 ? $parts : [$token, ''];
}
}

View File

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

View File

@@ -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,81 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Contracts\Cache\Factory as Cache;
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)
{
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)
{
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)
{
$this->cache->store($this->store)->forget($event->mutexName());
}
/**
* Specify the cache store that should be used.
*
* @param string $store
* @return $this
*/
public function useStore($store)
{
$this->store = $store;
return $this;
}
}

View File

@@ -1,61 +0,0 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Contracts\Cache\Repository as Cache;
class CacheMutex implements Mutex
{
/**
* The cache repository implementation.
*
* @var \Illuminate\Contracts\Cache\Repository
*/
public $cache;
/**
* Create a new overlapping strategy.
*
* @param \Illuminate\Contracts\Cache\Repository $cache
* @return void
*/
public function __construct(Cache $cache)
{
$this->cache = $cache;
}
/**
* Attempt to obtain a mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
*/
public function create(Event $event)
{
return $this->cache->add(
$event->mutexName(), true, $event->expiresAt
);
}
/**
* Determine if a mutex exists for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
*/
public function exists(Event $event)
{
return $this->cache->has($event->mutexName());
}
/**
* Clear the mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
public function forget(Event $event)
{
$this->cache->forget($event->mutexName());
}
}

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

@@ -2,9 +2,12 @@
namespace Illuminate\Console\Scheduling;
use LogicException;
use InvalidArgumentException;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Reflector;
use InvalidArgumentException;
use LogicException;
use RuntimeException;
use Throwable;
class CallbackEvent extends Event
{
@@ -22,19 +25,34 @@ class CallbackEvent extends Event
*/
protected $parameters;
/**
* The result of the callback's execution.
*
* @var mixed
*/
protected $result;
/**
* The exception that was thrown when calling the callback, if any.
*
* @var \Throwable|null
*/
protected $exception;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\Mutex $mutex
* @param string $callback
* @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(Mutex $mutex, $callback, array $parameters = [])
public function __construct(EventMutex $mutex, $callback, array $parameters = [], $timezone = null)
{
if (! is_string($callback) && ! is_callable($callback)) {
if (! is_string($callback) && ! Reflector::isCallable($callback)) {
throw new InvalidArgumentException(
'Invalid scheduled callback event. Must be a string or callable.'
);
@@ -43,47 +61,68 @@ class CallbackEvent extends Event
$this->mutex = $mutex;
$this->callback = $callback;
$this->parameters = $parameters;
$this->timezone = $timezone;
}
/**
* Run the given event.
* Run the callback event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*
* @throws \Exception
* @throws \Throwable
*/
public function run(Container $container)
{
if ($this->description && $this->withoutOverlapping &&
! $this->mutex->create($this)) {
return;
parent::run($container);
if ($this->exception) {
throw $this->exception;
}
register_shutdown_function(function () {
$this->removeMutex();
});
try {
$response = $container->call($this->callback, $this->parameters);
} finally {
$this->removeMutex();
}
parent::callAfterCallbacks($container);
return $response;
return $this->result;
}
/**
* Clear the mutex for the event.
* Determine if the event should skip because another process is overlapping.
*
* @return bool
*/
public function shouldSkipDueToOverlapping()
{
return $this->description && parent::shouldSkipDueToOverlapping();
}
/**
* Indicate that the callback should run in the background.
*
* @return void
*
* @throws \RuntimeException
*/
protected function removeMutex()
public function runInBackground()
{
if ($this->description) {
$this->mutex->forget($this);
throw new RuntimeException('Scheduled closures can not be run in the background.');
}
/**
* Run the callback.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*/
protected function execute($container)
{
try {
$this->result = is_object($this->callback)
? $container->call([$this->callback, '__invoke'], $this->parameters)
: $container->call($this->callback, $this->parameters);
return $this->result === false ? 1 : 0;
} catch (Throwable $e) {
$this->exception = $e;
return 1;
}
}
@@ -92,6 +131,8 @@ class CallbackEvent extends Event
*
* @param int $expiresAt
* @return $this
*
* @throws \LogicException
*/
public function withoutOverlapping($expiresAt = 1440)
{
@@ -101,23 +142,25 @@ class CallbackEvent extends Event
);
}
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this->skip(function () {
return $this->mutex->exists($this);
});
return parent::withoutOverlapping($expiresAt);
}
/**
* Get the mutex name for the scheduled command.
* Allow the event to only run on one server for each cron expression.
*
* @return string
* @return $this
*
* @throws \LogicException
*/
public function mutexName()
public function onOneServer()
{
return 'framework/schedule-'.sha1($this->description);
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();
}
/**
@@ -131,6 +174,28 @@ class CallbackEvent extends Event
return $this->description;
}
return is_string($this->callback) ? $this->callback : 'Closure';
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

@@ -3,7 +3,7 @@
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Symfony\Component\Process\ProcessUtils;
use Illuminate\Support\ProcessUtils;
class CommandBuilder
{
@@ -17,9 +17,9 @@ class CommandBuilder
{
if ($event->runInBackground) {
return $this->buildBackgroundCommand($event);
} else {
return $this->buildForegroundCommand($event);
}
return $this->buildForegroundCommand($event);
}
/**
@@ -51,8 +51,12 @@ class CommandBuilder
$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 '.(windows_os() ? '&' : ';').' '.$finished.') > '
'('.$event->command.$redirect.$output.' 2>&1 ; '.$finished.' "$?") > '
.ProcessUtils::escapeArgument($event->getDefaultOutput()).' 2>&1 &'
);
}

View File

@@ -3,22 +3,30 @@
namespace Illuminate\Console\Scheduling;
use Closure;
use Carbon\Carbon;
use Cron\CronExpression;
use GuzzleHttp\Client as HttpClient;
use Illuminate\Contracts\Mail\Mailer;
use Symfony\Component\Process\Process;
use Illuminate\Support\Traits\Macroable;
use GuzzleHttp\Exception\TransferException;
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\Reflector;
use Illuminate\Support\Stringable;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\ReflectsClosures;
use Psr\Http\Client\ClientExceptionInterface;
use Symfony\Component\Process\Process;
use Throwable;
class Event
{
use Macroable, ManagesFrequencies;
use Macroable, ManagesFrequencies, ReflectsClosures;
/**
* The command string.
*
* @var string
* @var string|null
*/
public $command;
@@ -27,7 +35,7 @@ class Event
*
* @var string
*/
public $expression = '* * * * * *';
public $expression = '* * * * *';
/**
* The timezone the date should be evaluated on.
@@ -39,7 +47,7 @@ class Event
/**
* The user the command should run as.
*
* @var string
* @var string|null
*/
public $user;
@@ -64,6 +72,13 @@ class Event
*/
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 amount of time the mutex should be valid.
*
@@ -72,7 +87,7 @@ class Event
public $expiresAt = 1440;
/**
* Indicates if the command should run in background.
* Indicates if the command should run in the background.
*
* @var bool
*/
@@ -123,28 +138,45 @@ class Event
/**
* The human readable description of the event.
*
* @var string
* @var string|null
*/
public $description;
/**
* The mutex implementation.
* The event mutex implementation.
*
* @var \Illuminate\Console\Scheduling\Mutex
* @var \Illuminate\Console\Scheduling\EventMutex
*/
public $mutex;
/**
* The mutex name resolver callback.
*
* @var \Closure|null
*/
public $mutexNameResolver;
/**
* The exit status code of the command.
*
* @var int|null
*/
public $exitCode;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\Mutex $mutex
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @param string $command
* @param \DateTimeZone|string|null $timezone
* @return void
*/
public function __construct(Mutex $mutex, $command)
public function __construct(EventMutex $mutex, $command, $timezone = null)
{
$this->mutex = $mutex;
$this->command = $command;
$this->timezone = $timezone;
$this->output = $this->getDefaultOutput();
}
@@ -155,7 +187,7 @@ class Event
*/
public function getDefaultOutput()
{
return (DIRECTORY_SEPARATOR == '\\') ? 'NUL' : '/dev/null';
return (DIRECTORY_SEPARATOR === '\\') ? 'NUL' : '/dev/null';
}
/**
@@ -163,59 +195,82 @@ class Event
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*
* @throws \Throwable
*/
public function run(Container $container)
{
if ($this->withoutOverlapping &&
! $this->mutex->create($this)) {
if ($this->shouldSkipDueToOverlapping()) {
return;
}
$this->runInBackground
? $this->runCommandInBackground($container)
: $this->runCommandInForeground($container);
$exitCode = $this->start($container);
if (! $this->runInBackground) {
$this->finish($container, $exitCode);
}
}
/**
* Get the mutex name for the scheduled command.
* Determine if the event should skip because another process is overlapping.
*
* @return string
* @return bool
*/
public function mutexName()
public function shouldSkipDueToOverlapping()
{
return 'framework'.DIRECTORY_SEPARATOR.'schedule-'.sha1($this->expression.$this->command);
return $this->withoutOverlapping && ! $this->mutex->create($this);
}
/**
* Run the command in the foreground.
* Run the command process.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
* @return int
*
* @throws \Throwable
*/
protected function runCommandInForeground(Container $container)
protected function start($container)
{
$this->callBeforeCallbacks($container);
try {
$this->callBeforeCallbacks($container);
(new Process(
$this->buildCommand(), base_path(), null, null, null
))->run();
return $this->execute($container);
} catch (Throwable $exception) {
$this->removeMutex();
$this->callAfterCallbacks($container);
throw $exception;
}
}
/**
* Run the command in the background.
* Run the command process.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return int
*/
protected function execute($container)
{
return Process::fromShellCommandline(
$this->buildCommand(), base_path(), null, null, null
)->run();
}
/**
* Mark the command process as finished and run callbacks/cleanup.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param int $exitCode
* @return void
*/
protected function runCommandInBackground(Container $container)
public function finish(Container $container, $exitCode)
{
$this->callBeforeCallbacks($container);
$this->exitCode = (int) $exitCode;
(new Process(
$this->buildCommand(), base_path(), null, null, null
))->run();
try {
$this->callAfterCallbacks($container);
} finally {
$this->removeMutex();
}
}
/**
@@ -287,13 +342,13 @@ class Event
*/
protected function expressionPasses()
{
$date = Carbon::now();
$date = Date::now();
if ($this->timezone) {
$date->setTimezone($this->timezone);
$date = $date->setTimezone($this->timezone);
}
return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
return (new CronExpression($this->expression))->isDue($date->toDateTimeString());
}
/**
@@ -330,6 +385,18 @@ class Event
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.
*
@@ -368,9 +435,9 @@ class Event
*/
public function emailOutputTo($addresses, $onlyIfOutputExists = false)
{
$this->ensureOutputIsBeingCapturedForEmail();
$this->ensureOutputIsBeingCaptured();
$addresses = is_array($addresses) ? $addresses : [$addresses];
$addresses = Arr::wrap($addresses);
return $this->then(function (Mailer $mailer) use ($addresses, $onlyIfOutputExists) {
$this->emailOutput($mailer, $addresses, $onlyIfOutputExists);
@@ -391,11 +458,28 @@ class Event
}
/**
* Ensure that output is being captured for email.
* 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 ensureOutputIsBeingCapturedForEmail()
protected function ensureOutputIsBeingCaptured()
{
if (is_null($this->output) || $this->output == $this->getDefaultOutput()) {
$this->sendOutputTo(storage_path('logs/schedule-'.sha1($this->mutexName()).'.log'));
@@ -412,7 +496,7 @@ class Event
*/
protected function emailOutput(Mailer $mailer, $addresses, $onlyIfOutputExists = false)
{
$text = file_exists($this->output) ? file_get_contents($this->output) : '';
$text = is_file($this->output) ? file_get_contents($this->output) : '';
if ($onlyIfOutputExists && empty($text)) {
return;
@@ -434,7 +518,7 @@ class Event
return $this->description;
}
return 'Scheduled Job Output';
return "Scheduled Job Output For [{$this->command}]";
}
/**
@@ -445,9 +529,19 @@ class Event
*/
public function pingBefore($url)
{
return $this->before(function () use ($url) {
(new HttpClient)->get($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;
}
/**
@@ -458,13 +552,62 @@ class Event
*/
public function thenPing($url)
{
return $this->then(function () use ($url) {
(new HttpClient)->get($url);
});
return $this->then($this->pingCallback($url));
}
/**
* State that the command should run in background.
* 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 fails.
*
* @param string $url
* @return $this
*/
public function pingOnFailure($url)
{
return $this->onFailure($this->pingCallback($url));
}
/**
* Get the callback that pings the given URL.
*
* @param string $url
* @return \Closure
*/
protected function pingCallback($url)
{
return function (Container $container, HttpClient $http) use ($url) {
try {
$http->request('GET', $url);
} catch (ClientExceptionInterface|TransferException $e) {
$container->make(ExceptionHandler::class)->report($e);
}
};
}
/**
* State that the command should run in the background.
*
* @return $this
*/
@@ -516,28 +659,28 @@ class Event
/**
* Do not allow the event to overlap each other.
*
* @param int $expiresAt
* @return $this
*/
public function withoutOverlapping()
public function withoutOverlapping($expiresAt = 1440)
{
$this->withoutOverlapping = true;
return $this->then(function () {
$this->mutex->forget($this);
})->skip(function () {
$this->expiresAt = $expiresAt;
return $this->skip(function () {
return $this->mutex->exists($this);
});
}
/**
* Register a callback to further filter the schedule.
* Allow the event to only run on one server for each cron expression.
*
* @param \Closure $callback
* @return $this
*/
public function when(Closure $callback)
public function onOneServer()
{
$this->filters[] = $callback;
$this->onOneServer = true;
return $this;
}
@@ -545,12 +688,29 @@ class Event
/**
* Register a callback to further filter the schedule.
*
* @param \Closure $callback
* @param \Closure|bool $callback
* @return $this
*/
public function skip(Closure $callback)
public function when($callback)
{
$this->rejects[] = $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;
}
@@ -587,11 +747,119 @@ class Event
*/
public function then(Closure $callback)
{
$parameters = $this->closureParameterTypes($callback);
if (Arr::get($parameters, 'output') === Stringable::class) {
return $this->thenWithOutput($callback);
}
$this->afterCallbacks[] = $callback;
return $this;
}
/**
* 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)]);
};
}
/**
* Set the human-friendly description of the event.
*
@@ -633,16 +901,15 @@ class Event
/**
* Determine the next due date for an event.
*
* @param \DateTime|string $currentTime
* @param \DateTimeInterface|string $currentTime
* @param int $nth
* @param bool $allowCurrentDate
* @return \Carbon\Carbon
* @return \Illuminate\Support\Carbon
*/
public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return Carbon::instance($nextDue = CronExpression::factory(
$this->getExpression()
)->getNextRunDate($currentTime, $nth, $allowCurrentDate));
return Date::instance((new CronExpression($this->getExpression()))
->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone));
}
/**
@@ -656,15 +923,56 @@ class Event
}
/**
* Set the mutex implementation to be used.
* Set the event mutex implementation to be used.
*
* @param \Illuminate\Console\Scheduling\Mutex $mutex
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @return $this
*/
public function preventOverlapsUsing(Mutex $mutex)
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->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);
}
}
}

View File

@@ -2,10 +2,10 @@
namespace Illuminate\Console\Scheduling;
interface Mutex
interface EventMutex
{
/**
* Attempt to obtain a mutex for the given event.
* Attempt to obtain an event mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
@@ -13,7 +13,7 @@ interface Mutex
public function create(Event $event);
/**
* Determine if a mutex exists for the given event.
* Determine if an event mutex exists for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return bool
@@ -21,7 +21,7 @@ interface Mutex
public function exists(Event $event);
/**
* Clear the mutex for the given event.
* Clear the event mutex for the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void

View File

@@ -2,7 +2,7 @@
namespace Illuminate\Console\Scheduling;
use Carbon\Carbon;
use Illuminate\Support\Carbon;
trait ManagesFrequencies
{
@@ -52,15 +52,105 @@ trait ManagesFrequencies
*/
private function inTimeInterval($startTime, $endTime)
{
return function () use ($startTime, $endTime) {
return Carbon::now($this->timezone)->between(
Carbon::parse($startTime, $this->timezone),
Carbon::parse($endTime, $this->timezone),
true
);
[$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 function () use ($now, $startTime, $endTime) {
return $now->between($startTime, $endTime);
};
}
/**
* 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, '0,30');
}
/**
* Schedule the event to run hourly.
*
@@ -74,14 +164,70 @@ trait ManagesFrequencies
/**
* Schedule the event to run hourly at a given offset in the hour.
*
* @param int $offset
* @param array|int $offset
* @return $this
*/
public function hourlyAt($offset)
{
$offset = is_array($offset) ? implode(',', $offset) : $offset;
return $this->spliceIntoPosition(1, $offset);
}
/**
* Schedule the event to run every odd hour.
*
* @return $this
*/
public function everyOddHour()
{
return $this->spliceIntoPosition(1, 0)->spliceIntoPosition(2, '1-23/2');
}
/**
* Schedule the event to run every two hours.
*
* @return $this
*/
public function everyTwoHours()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, '*/2');
}
/**
* Schedule the event to run every three hours.
*
* @return $this
*/
public function everyThreeHours()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, '*/3');
}
/**
* Schedule the event to run every four hours.
*
* @return $this
*/
public function everyFourHours()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, '*/4');
}
/**
* Schedule the event to run every six hours.
*
* @return $this
*/
public function everySixHours()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, '*/6');
}
/**
* Schedule the event to run daily.
*
@@ -115,7 +261,7 @@ trait ManagesFrequencies
$segments = explode(':', $time);
return $this->spliceIntoPosition(2, (int) $segments[0])
->spliceIntoPosition(1, count($segments) == 2 ? (int) $segments[1] : '0');
->spliceIntoPosition(1, count($segments) === 2 ? (int) $segments[1] : '0');
}
/**
@@ -126,10 +272,23 @@ trait ManagesFrequencies
* @return $this
*/
public function twiceDaily($first = 1, $second = 13)
{
return $this->twiceDailyAt($first, $second, 0);
}
/**
* Schedule the event to run twice daily at a given offset.
*
* @param int $first
* @param int $second
* @param int $offset
* @return $this
*/
public function twiceDailyAt($first = 1, $second = 13, $offset = 0)
{
$hours = $first.','.$second;
return $this->spliceIntoPosition(1, 0)
return $this->spliceIntoPosition(1, $offset)
->spliceIntoPosition(2, $hours);
}
@@ -140,7 +299,7 @@ trait ManagesFrequencies
*/
public function weekdays()
{
return $this->spliceIntoPosition(5, '1-5');
return $this->days(Schedule::MONDAY.'-'.Schedule::FRIDAY);
}
/**
@@ -150,7 +309,7 @@ trait ManagesFrequencies
*/
public function weekends()
{
return $this->spliceIntoPosition(5, '0,6');
return $this->days(Schedule::SATURDAY.','.Schedule::SUNDAY);
}
/**
@@ -160,7 +319,7 @@ trait ManagesFrequencies
*/
public function mondays()
{
return $this->days(1);
return $this->days(Schedule::MONDAY);
}
/**
@@ -170,7 +329,7 @@ trait ManagesFrequencies
*/
public function tuesdays()
{
return $this->days(2);
return $this->days(Schedule::TUESDAY);
}
/**
@@ -180,7 +339,7 @@ trait ManagesFrequencies
*/
public function wednesdays()
{
return $this->days(3);
return $this->days(Schedule::WEDNESDAY);
}
/**
@@ -190,7 +349,7 @@ trait ManagesFrequencies
*/
public function thursdays()
{
return $this->days(4);
return $this->days(Schedule::THURSDAY);
}
/**
@@ -200,7 +359,7 @@ trait ManagesFrequencies
*/
public function fridays()
{
return $this->days(5);
return $this->days(Schedule::FRIDAY);
}
/**
@@ -210,7 +369,7 @@ trait ManagesFrequencies
*/
public function saturdays()
{
return $this->days(6);
return $this->days(Schedule::SATURDAY);
}
/**
@@ -220,7 +379,7 @@ trait ManagesFrequencies
*/
public function sundays()
{
return $this->days(0);
return $this->days(Schedule::SUNDAY);
}
/**
@@ -238,15 +397,15 @@ trait ManagesFrequencies
/**
* Schedule the event to run weekly on a given day and time.
*
* @param int $day
* @param array|mixed $dayOfWeek
* @param string $time
* @return $this
*/
public function weeklyOn($day, $time = '0:0')
public function weeklyOn($dayOfWeek, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(5, $day);
return $this->days($dayOfWeek);
}
/**
@@ -264,31 +423,45 @@ trait ManagesFrequencies
/**
* Schedule the event to run monthly on a given day and time.
*
* @param int $day
* @param int $dayOfMonth
* @param string $time
* @return $this
*/
public function monthlyOn($day = 1, $time = '0:0')
public function monthlyOn($dayOfMonth = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $day);
return $this->spliceIntoPosition(3, $dayOfMonth);
}
/**
* Schedule the event to run twice monthly.
* 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)
public function twiceMonthly($first = 1, $second = 16, $time = '0:0')
{
$days = $first.','.$second;
$daysOfMonth = $first.','.$second;
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, $days);
$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);
}
/**
@@ -301,7 +474,22 @@ trait ManagesFrequencies
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, '*/3');
->spliceIntoPosition(4, '1-12/3');
}
/**
* Schedule the event to run quarterly on a given day and time.
*
* @param int $dayOfQuarter
* @param int $time
* @return $this
*/
public function quarterlyOn($dayOfQuarter = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $dayOfQuarter)
->spliceIntoPosition(4, '1-12/3');
}
/**
@@ -318,43 +506,19 @@ trait ManagesFrequencies
}
/**
* Schedule the event to run every minute.
* 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 everyMinute()
public function yearlyOn($month = 1, $dayOfMonth = 1, $time = '0:0')
{
return $this->spliceIntoPosition(1, '*');
}
$this->dailyAt($time);
/**
* 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 thirty minutes.
*
* @return $this
*/
public function everyThirtyMinutes()
{
return $this->spliceIntoPosition(1, '0,30');
return $this->spliceIntoPosition(3, $dayOfMonth)
->spliceIntoPosition(4, $month);
}
/**
@@ -392,7 +556,7 @@ trait ManagesFrequencies
*/
protected function spliceIntoPosition($position, $value)
{
$segments = explode(' ', $this->expression);
$segments = preg_split("/\s+/", $this->expression);
$segments[$position - 1] = $value;

View File

@@ -2,12 +2,39 @@
namespace Illuminate\Console\Scheduling;
use Closure;
use DateTimeInterface;
use Illuminate\Bus\UniqueLock;
use Illuminate\Console\Application;
use Illuminate\Container\Container;
use Symfony\Component\Process\ProcessUtils;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Support\ProcessUtils;
use Illuminate\Support\Traits\Macroable;
use RuntimeException;
class Schedule
{
use Macroable;
const SUNDAY = 0;
const MONDAY = 1;
const TUESDAY = 2;
const WEDNESDAY = 3;
const THURSDAY = 4;
const FRIDAY = 5;
const SATURDAY = 6;
/**
* All of the events on the schedule.
*
@@ -16,37 +43,73 @@ class Schedule
protected $events = [];
/**
* The mutex implementation.
* The event mutex implementation.
*
* @var \Illuminate\Console\Scheduling\Mutex
* @var \Illuminate\Console\Scheduling\EventMutex
*/
protected $mutex;
protected $eventMutex;
/**
* Create a new event instance.
* The scheduling mutex implementation.
*
* @return void
* @var \Illuminate\Console\Scheduling\SchedulingMutex
*/
public function __construct()
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;
/**
* 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->mutex = $container->bound(Mutex::class)
? $container->make(Mutex::class)
: $container->make(CacheMutex::class);
$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
* @param array $parameters
* @return \Illuminate\Console\Scheduling\CallbackEvent
*/
public function call($callback, array $parameters = [])
{
$this->events[] = $event = new CallbackEvent(
$this->mutex, $callback, $parameters
$this->eventMutex, $callback, $parameters, $this->timezone
);
return $event;
@@ -62,7 +125,11 @@ class Schedule
public function command($command, array $parameters = [])
{
if (class_exists($command)) {
$command = Container::getInstance()->make($command)->getName();
$command = Container::getInstance()->make($command);
return $this->exec(
Application::formatCommandString($command->getName()), $parameters,
)->description($command->getDescription());
}
return $this->exec(
@@ -74,15 +141,90 @@ class Schedule
* 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)
public function job($job, $queue = null, $connection = null)
{
return $this->call(function () use ($job) {
dispatch(is_string($job) ? resolve($job) : $job);
return $this->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);
}
})->name(is_string($job) ? $job : get_class($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.
*
@@ -96,7 +238,7 @@ class Schedule
$command .= ' '.$this->compileParameters($parameters);
}
$this->events[] = $event = new Event($this->mutex, $command);
$this->events[] = $event = new Event($this->eventMutex, $command, $this->timezone);
return $event;
}
@@ -111,10 +253,10 @@ class Schedule
{
return collect($parameters)->map(function ($value, $key) {
if (is_array($value)) {
$value = collect($value)->map(function ($value) {
return ProcessUtils::escapeArgument($value);
})->implode(' ');
} elseif (! is_numeric($value) && ! preg_match('/^(-.$|--.*)/i', $value)) {
return $this->compileArrayInput($key, $value);
}
if (! is_numeric($value) && ! preg_match('/^(-.$|--.*)/i', $value)) {
$value = ProcessUtils::escapeArgument($value);
}
@@ -122,11 +264,49 @@ class Schedule
})->implode(' ');
}
/**
* Compile array input for a command.
*
* @param string|int $key
* @param array $value
* @return string
*/
public function compileArrayInput($key, $value)
{
$value = collect($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->schedulingMutex->create($event, $time);
}
/**
* Get all of the events on the schedule that are due.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return array
* @return \Illuminate\Support\Collection
*/
public function dueEvents($app)
{
@@ -142,4 +322,46 @@ class Schedule
{
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;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
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

@@ -3,7 +3,11 @@
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Console\Events\ScheduledBackgroundTaskFinished;
use Illuminate\Contracts\Events\Dispatcher;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:finish')]
class ScheduleFinishCommand extends Command
{
/**
@@ -11,7 +15,18 @@ class ScheduleFinishCommand extends Command
*
* @var string
*/
protected $signature = 'schedule:finish {id}';
protected $signature = 'schedule:finish {id} {code=0}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:finish';
/**
* The console command description.
@@ -28,34 +43,19 @@ class ScheduleFinishCommand extends Command
protected $hidden = true;
/**
* The schedule instance.
*
* @var \Illuminate\Console\Scheduling\Schedule
*/
protected $schedule;
/**
* Create a new command instance.
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function __construct(Schedule $schedule)
public function handle(Schedule $schedule)
{
$this->schedule = $schedule;
parent::__construct();
}
/**
* Execute the console command.
*
* @return void
*/
public function fire()
{
collect($this->schedule->events())->filter(function ($value) {
collect($schedule->events())->filter(function ($value) {
return $value->mutexName() == $this->argument('id');
})->each->callAfterCallbacks($this->laravel);
})->each(function ($event) {
$event->finish($this->laravel, $this->argument('code'));
$this->laravel->make(Dispatcher::class)->dispatch(new ScheduledBackgroundTaskFinished($event));
});
}
}

View File

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

View File

@@ -2,8 +2,20 @@
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\Debug\ExceptionHandler;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Date;
use Symfony\Component\Console\Attribute\AsCommand;
use Throwable;
#[AsCommand(name: 'schedule:run')]
class ScheduleRunCommand extends Command
{
/**
@@ -13,6 +25,17 @@ class ScheduleRunCommand extends Command
*/
protected $name = 'schedule:run';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:run';
/**
* The console command description.
*
@@ -27,15 +50,49 @@ class ScheduleRunCommand extends Command
*/
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 PHP binary used by the command.
*
* @var string
*/
protected $phpBinary;
/**
* Create a new command instance.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function __construct(Schedule $schedule)
public function __construct()
{
$this->schedule = $schedule;
$this->startedAt = Date::now();
parent::__construct();
}
@@ -43,26 +100,108 @@ class ScheduleRunCommand extends Command
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @param \Illuminate\Contracts\Debug\ExceptionHandler $handler
* @return void
*/
public function fire()
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
$eventsRan = false;
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
$this->phpBinary = Application::phpBinary();
$this->newLine();
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
$this->line('<info>Running scheduled command:</info> '.$event->getSummaryForDisplay());
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$event->run($this->laravel);
$eventsRan = true;
$this->eventsRan = true;
}
if (! $eventsRan) {
$this->info('No scheduled commands are ready to run.');
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(),
]);
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Application;
use Illuminate\Console\Command;
use Symfony\Component\Console\Attribute\AsCommand;
#[AsCommand(name: 'schedule:test')]
class ScheduleTestCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schedule:test {--name= : The name of the scheduled command to run}';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:test';
/**
* The console command description.
*
* @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 = array_search($this->components->choice('Which command would you like to run?', $commandNames), $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();
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
use Illuminate\Support\Carbon;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Process\Process;
#[AsCommand(name: 'schedule:work')]
class ScheduleWorkCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'schedule:work';
/**
* The name of the console command.
*
* This name is used to identify the command during lazy loading.
*
* @var string|null
*
* @deprecated
*/
protected static $defaultName = 'schedule:work';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Start the schedule worker';
/**
* Execute the console command.
*
* @return void
*/
public function handle()
{
$this->components->info('Running schedule tasks every minute.');
[$lastExecutionStartedAt, $executions] = [null, []];
while (true) {
usleep(100 * 1000);
if (Carbon::now()->second === 0 &&
! Carbon::now()->startOfMinute()->equalTo($lastExecutionStartedAt)) {
$executions[] = $execution = new Process([
PHP_BINARY,
defined('ARTISAN_BINARY') ? ARTISAN_BINARY : 'artisan',
'schedule:run',
]);
$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 array<int, array<int, callable(int $signal): void>>
*/
public function unregister()
{
$previousHandlers = $this->previousHandlers;
foreach ($previousHandlers as $signal => $handler) {
if (is_null($handler)) {
pcntl_signal($signal, SIG_DFL);
unset($previousHandlers[$signal]);
}
}
$this->setHandlers($previousHandlers);
}
/**
* Execute the given callback if "signals" should be used and are available.
*
* @param callable $callback
* @return void
*/
public static function whenAvailable($callback)
{
$resolver = static::$availabilityResolver;
if ($resolver()) {
$callback();
}
}
/**
* Get the registry's handlers.
*
* @return array<int, array<int, callable>>
*/
protected function getHandlers()
{
return (fn () => $this->signalHandlers)
->call($this->registry);
}
/**
* Set the registry's handlers.
*
* @param array<int, array<int, callable(int $signal):void>> $handlers
* @return void
*/
protected function setHandlers($handlers)
{
(fn () => $this->signalHandlers = $handlers)
->call($this->registry);
}
/**
* Set the availability resolver.
*
* @param callable(): bool
* @return void
*/
public static function resolveAvailabilityUsing($resolver)
{
static::$availabilityResolver = $resolver;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
<?php
namespace Illuminate\Console\View\Components\Mutators;
class EnsurePunctuation
{
/**
* Ensures the given string ends with punctuation.
*
* @param string $string
* @return string
*/
public function __invoke($string)
{
if (! str($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,57 @@
<?php
namespace Illuminate\Console\View\Components;
use Symfony\Component\Console\Output\OutputInterface;
use function Termwind\terminal;
use Throwable;
class Task extends Component
{
/**
* 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
? (' '.number_format((microtime(true) - $startTime) * 1000).'ms')
: '';
$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

@@ -14,11 +14,15 @@
}
],
"require": {
"php": ">=5.6.4",
"illuminate/contracts": "5.4.*",
"illuminate/support": "5.4.*",
"nesbot/carbon": "~1.20",
"symfony/console": "~3.2"
"php": "^8.0.2",
"illuminate/collections": "^9.0",
"illuminate/contracts": "^9.0",
"illuminate/macroable": "^9.0",
"illuminate/support": "^9.0",
"illuminate/view": "^9.0",
"nunomaduro/termwind": "^1.13",
"symfony/console": "^6.0.9",
"symfony/process": "^6.0"
},
"autoload": {
"psr-4": {
@@ -27,13 +31,16 @@
},
"extra": {
"branch-alias": {
"dev-master": "5.4-dev"
"dev-master": "9.x-dev"
}
},
"suggest": {
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (~6.0).",
"mtdowling/cron-expression": "Required to use scheduling component (~1.0).",
"symfony/process": "Required to use scheduling component (~3.2)."
"dragonmantank/cron-expression": "Required to use scheduler (^3.3.2).",
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (^7.5).",
"illuminate/bus": "Required to use the scheduled job dispatcher (^9.0).",
"illuminate/container": "Required to use the scheduler (^9.0).",
"illuminate/filesystem": "Required to use the generator command (^9.0).",
"illuminate/queue": "Required to use closures for scheduled jobs (^9.0)."
},
"config": {
"sort-packages": true

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>