Primo Committ

This commit is contained in:
paoloar77
2024-05-07 12:17:25 +02:00
commit e73d0e5113
7204 changed files with 884387 additions and 0 deletions

View File

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

View File

@@ -0,0 +1,200 @@
<?php
namespace Illuminate\Console;
use Illuminate\Support\Traits\Macroable;
use Symfony\Component\Console\Command\Command as SymfonyCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Command extends SymfonyCommand
{
use Concerns\CallsCommands,
Concerns\HasParameters,
Concerns\InteractsWithIO,
Macroable;
/**
* The Laravel application instance.
*
* @var \Illuminate\Contracts\Foundation\Application
*/
protected $laravel;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature;
/**
* The console command name.
*
* @var string
*/
protected $name;
/**
* The console command description.
*
* @var string|null
*/
protected $description;
/**
* The console command help text.
*
* @var string|null
*/
protected $help;
/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = false;
/**
* Create a new console command instance.
*
* @return void
*/
public function __construct()
{
// We will go ahead and set the name, description, and parameters on console
// commands just to make things a little easier on the developer. This is
// so they don't have to all be manually specified in the constructors.
if (isset($this->signature)) {
$this->configureUsingFluentDefinition();
} else {
parent::__construct($this->name);
}
// Once we have constructed the command, we'll set the description and other
// related properties of the command. If a signature wasn't used to build
// the command we'll set the arguments and the options on this command.
$this->setDescription((string) $this->description);
$this->setHelp((string) $this->help);
$this->setHidden($this->isHidden());
if (! isset($this->signature)) {
$this->specifyParameters();
}
}
/**
* Configure the console command using a fluent definition.
*
* @return void
*/
protected function configureUsingFluentDefinition()
{
[$name, $arguments, $options] = Parser::parse($this->signature);
parent::__construct($this->name = $name);
// After parsing the signature we will spin through the arguments and options
// and set them on this command. These will already be changed into proper
// instances of these "InputArgument" and "InputOption" Symfony classes.
$this->getDefinition()->addArguments($arguments);
$this->getDefinition()->addOptions($options);
}
/**
* Run the console command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
public function run(InputInterface $input, OutputInterface $output)
{
$this->output = $this->laravel->make(
OutputStyle::class, ['input' => $input, 'output' => $output]
);
return parent::run(
$this->input = $input, $this->output
);
}
/**
* Execute the console command.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
return (int) $this->laravel->call([$this, 'handle']);
}
/**
* Resolve the console command instance for the given command.
*
* @param \Symfony\Component\Console\Command\Command|string $command
* @return \Symfony\Component\Console\Command\Command
*/
protected function resolveCommand($command)
{
if (! class_exists($command)) {
return $this->getApplication()->find($command);
}
$command = $this->laravel->make($command);
if ($command instanceof SymfonyCommand) {
$command->setApplication($this->getApplication());
}
if ($command instanceof self) {
$command->setLaravel($this->getLaravel());
}
return $command;
}
/**
* {@inheritdoc}
*/
public function isHidden()
{
return $this->hidden;
}
/**
* {@inheritdoc}
*/
public function setHidden(bool $hidden)
{
parent::setHidden($this->hidden = $hidden);
return $this;
}
/**
* Get the Laravel application instance.
*
* @return \Illuminate\Contracts\Foundation\Application
*/
public function getLaravel()
{
return $this->laravel;
}
/**
* Set the Laravel application instance.
*
* @param \Illuminate\Contracts\Container\Container $laravel
* @return void
*/
public function setLaravel($laravel)
{
$this->laravel = $laravel;
}
}

View File

@@ -0,0 +1,92 @@
<?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 silently.
*
* @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);
}
/**
* 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,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(...array_values($arguments));
}
}
foreach ($this->getOptions() as $options) {
if ($options instanceof InputOption) {
$this->getDefinition()->addOption($options);
} else {
$this->addOption(...array_values($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,397 @@
<?php
namespace Illuminate\Console\Concerns;
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 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 string|array|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|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 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();
}
/**
* 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
* @return void
*/
public function alert($string)
{
$length = Str::length(strip_tags($string)) + 12;
$this->comment(str_repeat('*', $length));
$this->comment('* '.$string.' *');
$this->comment(str_repeat('*', $length));
$this->output->newLine();
}
/**
* 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,52 @@
<?php
namespace Illuminate\Console;
trait ConfirmableTrait
{
/**
* Confirm before proceeding with the action.
*
* This method only asks for confirmation in production.
*
* @param string $warning
* @param \Closure|bool|null $callback
* @return bool
*/
public function confirmToProceed($warning = 'Application In Production!', $callback = null)
{
$callback = is_null($callback) ? $this->getDefaultConfirmCallback() : $callback;
$shouldConfirm = value($callback);
if ($shouldConfirm) {
if ($this->hasOption('force') && $this->option('force')) {
return true;
}
$this->alert($warning);
$confirmed = $this->confirm('Do you really wish to run this command?');
if (! $confirmed) {
$this->comment('Command Canceled!');
return false;
}
}
return true;
}
/**
* Get the default confirmation callback.
*
* @return \Closure
*/
protected function getDefaultConfirmCallback()
{
return function () {
return $this->getLaravel()->environment() === 'production';
};
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
<?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
*/
public function __construct(Event $task, Throwable $exception)
{
$this->task = $task;
$this->exception = $exception;
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,393 @@
<?php
namespace Illuminate\Console;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Console\Input\InputArgument;
abstract class GeneratorCommand extends Command
{
/**
* The filesystem instance.
*
* @var \Illuminate\Filesystem\Filesystem
*/
protected $files;
/**
* The type of class being generated.
*
* @var string
*/
protected $type;
/**
* Reserved names that cannot be used for generation.
*
* @var array
*/
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',
'eval',
'exit',
'extends',
'final',
'finally',
'fn',
'for',
'foreach',
'function',
'global',
'goto',
'if',
'implements',
'include',
'include_once',
'instanceof',
'insteadof',
'interface',
'isset',
'list',
'namespace',
'new',
'or',
'print',
'private',
'protected',
'public',
'require',
'require_once',
'return',
'static',
'switch',
'throw',
'trait',
'try',
'unset',
'use',
'var',
'while',
'xor',
'yield',
];
/**
* Create a new controller creator command instance.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function __construct(Filesystem $files)
{
parent::__construct();
$this->files = $files;
}
/**
* Get the stub file for the generator.
*
* @return string
*/
abstract protected function getStub();
/**
* Execute the console command.
*
* @return bool|null
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function handle()
{
// First we need to ensure that the given name is not a reserved word within the PHP
// language and that the class name will actually be valid. If it is not valid we
// can error now and prevent from polluting the filesystem using invalid files.
if ($this->isReservedName($this->getNameInput())) {
$this->error('The name "'.$this->getNameInput().'" is reserved by PHP.');
return false;
}
$name = $this->qualifyClass($this->getNameInput());
$path = $this->getPath($name);
// Next, We will check to see if the class already exists. If it does, we don't want
// to create the class and overwrite the user's code. So, we will bail out so the
// code is untouched. Otherwise, we will continue generating this class' files.
if ((! $this->hasOption('force') ||
! $this->option('force')) &&
$this->alreadyExists($this->getNameInput())) {
$this->error($this->type.' already exists!');
return false;
}
// Next, we will generate the path to the location where this class' file should get
// written. Then, we will build the class and make the proper replacements on the
// stub files so that it gets the correctly formatted namespace and class name.
$this->makeDirectory($path);
$this->files->put($path, $this->sortImports($this->buildClass($name)));
$this->info($this->type.' created successfully.');
}
/**
* Parse the class name and format according to the root namespace.
*
* @param string $name
* @return string
*/
protected function qualifyClass($name)
{
$name = ltrim($name, '\\/');
$rootNamespace = $this->rootNamespace();
if (Str::startsWith($name, $rootNamespace)) {
return $name;
}
$name = str_replace('/', '\\', $name);
return $this->qualifyClass(
$this->getDefaultNamespace(trim($rootNamespace, '\\')).'\\'.$name
);
}
/**
* Get the default namespace for the class.
*
* @param string $rootNamespace
* @return string
*/
protected function getDefaultNamespace($rootNamespace)
{
return $rootNamespace;
}
/**
* Determine if the class already exists.
*
* @param string $rawName
* @return bool
*/
protected function alreadyExists($rawName)
{
return $this->files->exists($this->getPath($this->qualifyClass($rawName)));
}
/**
* Get the destination class path.
*
* @param string $name
* @return string
*/
protected function getPath($name)
{
$name = Str::replaceFirst($this->rootNamespace(), '', $name);
return $this->laravel['path'].'/'.str_replace('\\', '/', $name).'.php';
}
/**
* Build the directory for the class if necessary.
*
* @param string $path
* @return string
*/
protected function makeDirectory($path)
{
if (! $this->files->isDirectory(dirname($path))) {
$this->files->makeDirectory(dirname($path), 0777, true, true);
}
return $path;
}
/**
* Build the class with the given name.
*
* @param string $name
* @return string
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function buildClass($name)
{
$stub = $this->files->get($this->getStub());
return $this->replaceNamespace($stub, $name)->replaceClass($stub, $name);
}
/**
* Replace the namespace for the given stub.
*
* @param string $stub
* @param string $name
* @return $this
*/
protected function replaceNamespace(&$stub, $name)
{
$searches = [
['DummyNamespace', 'DummyRootNamespace', 'NamespacedDummyUserModel'],
['{{ namespace }}', '{{ rootNamespace }}', '{{ namespacedUserModel }}'],
['{{namespace}}', '{{rootNamespace}}', '{{namespacedUserModel}}'],
];
foreach ($searches as $search) {
$stub = str_replace(
$search,
[$this->getNamespace($name), $this->rootNamespace(), $this->userProviderModel()],
$stub
);
}
return $this;
}
/**
* Get the full namespace for a given class, without the class name.
*
* @param string $name
* @return string
*/
protected function getNamespace($name)
{
return trim(implode('\\', array_slice(explode('\\', $name), 0, -1)), '\\');
}
/**
* Replace the class name for the given stub.
*
* @param string $stub
* @param string $name
* @return string
*/
protected function replaceClass($stub, $name)
{
$class = str_replace($this->getNamespace($name).'\\', '', $name);
return str_replace(['DummyClass', '{{ class }}', '{{class}}'], $class, $stub);
}
/**
* Alphabetically sorts the imports for the given stub.
*
* @param string $stub
* @return string
*/
protected function sortImports($stub)
{
if (preg_match('/(?P<imports>(?:use [^;]+;$\n?)+)/m', $stub, $match)) {
$imports = explode("\n", trim($match['imports']));
sort($imports);
return str_replace(trim($match['imports']), implode("\n", $imports), $stub);
}
return $stub;
}
/**
* Get the desired class name from the input.
*
* @return string
*/
protected function getNameInput()
{
return trim($this->argument('name'));
}
/**
* Get the root namespace for the class.
*
* @return string
*/
protected function rootNamespace()
{
return $this->laravel->getNamespace();
}
/**
* Get the model for the default guard's user provider.
*
* @return string|null
*/
protected function userProviderModel()
{
$config = $this->laravel['config'];
$provider = $config->get('auth.guards.'.$config->get('auth.defaults.guard').'.provider');
return $config->get("auth.providers.{$provider}.model");
}
/**
* Checks whether the given name is reserved.
*
* @param string $name
* @return bool
*/
protected function isReservedName($name)
{
$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.
*
* @return array
*/
protected function getArguments()
{
return [
['name', InputArgument::REQUIRED, 'The name of the class'],
];
}
}

View File

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

View File

@@ -0,0 +1,71 @@
<?php
namespace Illuminate\Console;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
class OutputStyle extends SymfonyStyle
{
/**
* The output instance.
*
* @var \Symfony\Component\Console\Output\OutputInterface
*/
private $output;
/**
* Create a new Console OutputStyle instance.
*
* @param \Symfony\Component\Console\Input\InputInterface $input
* @param \Symfony\Component\Console\Output\OutputInterface $output
* @return void
*/
public function __construct(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
parent::__construct($input, $output);
}
/**
* Returns whether verbosity is quiet (-q).
*
* @return bool
*/
public function isQuiet()
{
return $this->output->isQuiet();
}
/**
* Returns whether verbosity is verbose (-v).
*
* @return bool
*/
public function isVerbose()
{
return $this->output->isVerbose();
}
/**
* Returns whether verbosity is very verbose (-vv).
*
* @return bool
*/
public function isVeryVerbose()
{
return $this->output->isVeryVerbose();
}
/**
* Returns whether verbosity is debug (-vvv).
*
* @return bool
*/
public function isDebug()
{
return $this->output->isDebug();
}
}

View File

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

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

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

View File

@@ -0,0 +1,169 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Contracts\Container\Container;
use Illuminate\Support\Reflector;
use InvalidArgumentException;
use LogicException;
class CallbackEvent extends Event
{
/**
* The callback to call.
*
* @var string
*/
protected $callback;
/**
* The parameters to pass to the method.
*
* @var array
*/
protected $parameters;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @param string $callback
* @param array $parameters
* @param \DateTimeZone|string|null $timezone
* @return void
*
* @throws \InvalidArgumentException
*/
public function __construct(EventMutex $mutex, $callback, array $parameters = [], $timezone = null)
{
if (! is_string($callback) && ! Reflector::isCallable($callback)) {
throw new InvalidArgumentException(
'Invalid scheduled callback event. Must be a string or callable.'
);
}
$this->mutex = $mutex;
$this->callback = $callback;
$this->parameters = $parameters;
$this->timezone = $timezone;
}
/**
* Run the given event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return mixed
*
* @throws \Exception
*/
public function run(Container $container)
{
if ($this->description && $this->withoutOverlapping &&
! $this->mutex->create($this)) {
return;
}
$pid = getmypid();
register_shutdown_function(function () use ($pid) {
if ($pid === getmypid()) {
$this->removeMutex();
}
});
parent::callBeforeCallbacks($container);
try {
$response = is_object($this->callback)
? $container->call([$this->callback, '__invoke'], $this->parameters)
: $container->call($this->callback, $this->parameters);
} finally {
$this->removeMutex();
parent::callAfterCallbacks($container);
}
return $response;
}
/**
* Clear the mutex for the event.
*
* @return void
*/
protected function removeMutex()
{
if ($this->description && $this->withoutOverlapping) {
$this->mutex->forget($this);
}
}
/**
* Do not allow the event to overlap each other.
*
* @param int $expiresAt
* @return $this
*
* @throws \LogicException
*/
public function withoutOverlapping($expiresAt = 1440)
{
if (! isset($this->description)) {
throw new LogicException(
"A scheduled event name is required to prevent overlapping. Use the 'name' method before 'withoutOverlapping'."
);
}
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this->skip(function () {
return $this->mutex->exists($this);
});
}
/**
* Allow the event to only run on one server for each cron expression.
*
* @return $this
*
* @throws \LogicException
*/
public function onOneServer()
{
if (! isset($this->description)) {
throw new LogicException(
"A scheduled event name is required to only run on one server. Use the 'name' method before 'onOneServer'."
);
}
$this->onOneServer = true;
return $this;
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
return 'framework/schedule-'.sha1($this->description);
}
/**
* Get the summary of the event for display.
*
* @return string
*/
public function getSummaryForDisplay()
{
if (is_string($this->description)) {
return $this->description;
}
return is_string($this->callback) ? $this->callback : 'Callback';
}
}

View File

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

View File

@@ -0,0 +1,901 @@
<?php
namespace Illuminate\Console\Scheduling;
use Closure;
use Cron\CronExpression;
use GuzzleHttp\Client as HttpClient;
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\Carbon;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Reflector;
use Illuminate\Support\Traits\Macroable;
use Illuminate\Support\Traits\ReflectsClosures;
use Psr\Http\Client\ClientExceptionInterface;
use Symfony\Component\Process\Process;
class Event
{
use Macroable, ManagesFrequencies, ReflectsClosures;
/**
* The command string.
*
* @var string
*/
public $command;
/**
* The cron expression representing the event's frequency.
*
* @var string
*/
public $expression = '* * * * *';
/**
* The timezone the date should be evaluated on.
*
* @var \DateTimeZone|string
*/
public $timezone;
/**
* The user the command should run as.
*
* @var string
*/
public $user;
/**
* The list of environments the command should run under.
*
* @var array
*/
public $environments = [];
/**
* Indicates if the command should run in maintenance mode.
*
* @var bool
*/
public $evenInMaintenanceMode = false;
/**
* Indicates if the command should not overlap itself.
*
* @var bool
*/
public $withoutOverlapping = false;
/**
* Indicates if the command should only be allowed to run on one server for each cron expression.
*
* @var bool
*/
public $onOneServer = false;
/**
* The amount of time the mutex should be valid.
*
* @var int
*/
public $expiresAt = 1440;
/**
* Indicates if the command should run in background.
*
* @var bool
*/
public $runInBackground = false;
/**
* The array of filter callbacks.
*
* @var array
*/
protected $filters = [];
/**
* The array of reject callbacks.
*
* @var array
*/
protected $rejects = [];
/**
* The location that output should be sent to.
*
* @var string
*/
public $output = '/dev/null';
/**
* Indicates whether output should be appended.
*
* @var bool
*/
public $shouldAppendOutput = false;
/**
* The array of callbacks to be run before the event is started.
*
* @var array
*/
protected $beforeCallbacks = [];
/**
* The array of callbacks to be run after the event is finished.
*
* @var array
*/
protected $afterCallbacks = [];
/**
* The human readable description of the event.
*
* @var string
*/
public $description;
/**
* The event mutex implementation.
*
* @var \Illuminate\Console\Scheduling\EventMutex
*/
public $mutex;
/**
* The exit status code of the command.
*
* @var int|null
*/
public $exitCode;
/**
* Create a new event instance.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @param string $command
* @param \DateTimeZone|string|null $timezone
* @return void
*/
public function __construct(EventMutex $mutex, $command, $timezone = null)
{
$this->mutex = $mutex;
$this->command = $command;
$this->timezone = $timezone;
$this->output = $this->getDefaultOutput();
}
/**
* Get the default output depending on the OS.
*
* @return string
*/
public function getDefaultOutput()
{
return (DIRECTORY_SEPARATOR === '\\') ? 'NUL' : '/dev/null';
}
/**
* Run the given event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function run(Container $container)
{
if ($this->withoutOverlapping &&
! $this->mutex->create($this)) {
return;
}
$this->runInBackground
? $this->runCommandInBackground($container)
: $this->runCommandInForeground($container);
}
/**
* Get the mutex name for the scheduled command.
*
* @return string
*/
public function mutexName()
{
return 'framework'.DIRECTORY_SEPARATOR.'schedule-'.sha1($this->expression.$this->command);
}
/**
* Run the command in the foreground.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
protected function runCommandInForeground(Container $container)
{
$this->callBeforeCallbacks($container);
$this->exitCode = Process::fromShellCommandline($this->buildCommand(), base_path(), null, null, null)->run();
$this->callAfterCallbacks($container);
}
/**
* Run the command in the background.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
protected function runCommandInBackground(Container $container)
{
$this->callBeforeCallbacks($container);
Process::fromShellCommandline($this->buildCommand(), base_path(), null, null, null)->run();
}
/**
* Call all of the "before" callbacks for the event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function callBeforeCallbacks(Container $container)
{
foreach ($this->beforeCallbacks as $callback) {
$container->call($callback);
}
}
/**
* Call all of the "after" callbacks for the event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function callAfterCallbacks(Container $container)
{
foreach ($this->afterCallbacks as $callback) {
$container->call($callback);
}
}
/**
* Call all of the "after" callbacks for the event.
*
* @param \Illuminate\Contracts\Container\Container $container
* @param int $exitCode
* @return void
*/
public function callAfterCallbacksWithExitCode(Container $container, $exitCode)
{
$this->exitCode = (int) $exitCode;
$this->callAfterCallbacks($container);
}
/**
* Build the command string.
*
* @return string
*/
public function buildCommand()
{
return (new CommandBuilder)->buildCommand($this);
}
/**
* Determine if the given event should run based on the Cron expression.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return bool
*/
public function isDue($app)
{
if (! $this->runsInMaintenanceMode() && $app->isDownForMaintenance()) {
return false;
}
return $this->expressionPasses() &&
$this->runsInEnvironment($app->environment());
}
/**
* Determine if the event runs in maintenance mode.
*
* @return bool
*/
public function runsInMaintenanceMode()
{
return $this->evenInMaintenanceMode;
}
/**
* Determine if the Cron expression passes.
*
* @return bool
*/
protected function expressionPasses()
{
$date = Carbon::now();
if ($this->timezone) {
$date->setTimezone($this->timezone);
}
return CronExpression::factory($this->expression)->isDue($date->toDateTimeString());
}
/**
* Determine if the event runs in the given environment.
*
* @param string $environment
* @return bool
*/
public function runsInEnvironment($environment)
{
return empty($this->environments) || in_array($environment, $this->environments);
}
/**
* Determine if the filters pass for the event.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return bool
*/
public function filtersPass($app)
{
foreach ($this->filters as $callback) {
if (! $app->call($callback)) {
return false;
}
}
foreach ($this->rejects as $callback) {
if ($app->call($callback)) {
return false;
}
}
return true;
}
/**
* Ensure that the output is stored on disk in a log file.
*
* @return $this
*/
public function storeOutput()
{
$this->ensureOutputIsBeingCaptured();
return $this;
}
/**
* Send the output of the command to a given location.
*
* @param string $location
* @param bool $append
* @return $this
*/
public function sendOutputTo($location, $append = false)
{
$this->output = $location;
$this->shouldAppendOutput = $append;
return $this;
}
/**
* Append the output of the command to a given location.
*
* @param string $location
* @return $this
*/
public function appendOutputTo($location)
{
return $this->sendOutputTo($location, true);
}
/**
* E-mail the results of the scheduled operation.
*
* @param array|mixed $addresses
* @param bool $onlyIfOutputExists
* @return $this
*
* @throws \LogicException
*/
public function emailOutputTo($addresses, $onlyIfOutputExists = false)
{
$this->ensureOutputIsBeingCaptured();
$addresses = Arr::wrap($addresses);
return $this->then(function (Mailer $mailer) use ($addresses, $onlyIfOutputExists) {
$this->emailOutput($mailer, $addresses, $onlyIfOutputExists);
});
}
/**
* E-mail the results of the scheduled operation if it produces output.
*
* @param array|mixed $addresses
* @return $this
*
* @throws \LogicException
*/
public function emailWrittenOutputTo($addresses)
{
return $this->emailOutputTo($addresses, true);
}
/**
* E-mail the results of the scheduled operation if it fails.
*
* @param array|mixed $addresses
* @return $this
*/
public function emailOutputOnFailure($addresses)
{
$this->ensureOutputIsBeingCaptured();
$addresses = Arr::wrap($addresses);
return $this->onFailure(function (Mailer $mailer) use ($addresses) {
$this->emailOutput($mailer, $addresses, false);
});
}
/**
* Ensure that the command output is being captured.
*
* @return void
*/
protected function ensureOutputIsBeingCaptured()
{
if (is_null($this->output) || $this->output == $this->getDefaultOutput()) {
$this->sendOutputTo(storage_path('logs/schedule-'.sha1($this->mutexName()).'.log'));
}
}
/**
* E-mail the output of the event to the recipients.
*
* @param \Illuminate\Contracts\Mail\Mailer $mailer
* @param array $addresses
* @param bool $onlyIfOutputExists
* @return void
*/
protected function emailOutput(Mailer $mailer, $addresses, $onlyIfOutputExists = false)
{
$text = file_exists($this->output) ? file_get_contents($this->output) : '';
if ($onlyIfOutputExists && empty($text)) {
return;
}
$mailer->raw($text, function ($m) use ($addresses) {
$m->to($addresses)->subject($this->getEmailSubject());
});
}
/**
* Get the e-mail subject line for output results.
*
* @return string
*/
protected function getEmailSubject()
{
if ($this->description) {
return $this->description;
}
return "Scheduled Job Output For [{$this->command}]";
}
/**
* Register a callback to ping a given URL before the job runs.
*
* @param string $url
* @return $this
*/
public function pingBefore($url)
{
return $this->before($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL before the job runs if the given condition is true.
*
* @param bool $value
* @param string $url
* @return $this
*/
public function pingBeforeIf($value, $url)
{
return $value ? $this->pingBefore($url) : $this;
}
/**
* Register a callback to ping a given URL after the job runs.
*
* @param string $url
* @return $this
*/
public function thenPing($url)
{
return $this->then($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL after the job runs if the given condition is true.
*
* @param bool $value
* @param string $url
* @return $this
*/
public function thenPingIf($value, $url)
{
return $value ? $this->thenPing($url) : $this;
}
/**
* Register a callback to ping a given URL if the operation succeeds.
*
* @param string $url
* @return $this
*/
public function pingOnSuccess($url)
{
return $this->onSuccess($this->pingCallback($url));
}
/**
* Register a callback to ping a given URL if the operation 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 background.
*
* @return $this
*/
public function runInBackground()
{
$this->runInBackground = true;
return $this;
}
/**
* Set which user the command should run as.
*
* @param string $user
* @return $this
*/
public function user($user)
{
$this->user = $user;
return $this;
}
/**
* Limit the environments the command should run in.
*
* @param array|mixed $environments
* @return $this
*/
public function environments($environments)
{
$this->environments = is_array($environments) ? $environments : func_get_args();
return $this;
}
/**
* State that the command should run even in maintenance mode.
*
* @return $this
*/
public function evenInMaintenanceMode()
{
$this->evenInMaintenanceMode = true;
return $this;
}
/**
* Do not allow the event to overlap each other.
*
* @param int $expiresAt
* @return $this
*/
public function withoutOverlapping($expiresAt = 1440)
{
$this->withoutOverlapping = true;
$this->expiresAt = $expiresAt;
return $this->then(function () {
$this->mutex->forget($this);
})->skip(function () {
return $this->mutex->exists($this);
});
}
/**
* Allow the event to only run on one server for each cron expression.
*
* @return $this
*/
public function onOneServer()
{
$this->onOneServer = true;
return $this;
}
/**
* Register a callback to further filter the schedule.
*
* @param \Closure|bool $callback
* @return $this
*/
public function when($callback)
{
$this->filters[] = Reflector::isCallable($callback) ? $callback : function () use ($callback) {
return $callback;
};
return $this;
}
/**
* Register a callback to further filter the schedule.
*
* @param \Closure|bool $callback
* @return $this
*/
public function skip($callback)
{
$this->rejects[] = Reflector::isCallable($callback) ? $callback : function () use ($callback) {
return $callback;
};
return $this;
}
/**
* Register a callback to be called before the operation.
*
* @param \Closure $callback
* @return $this
*/
public function before(Closure $callback)
{
$this->beforeCallbacks[] = $callback;
return $this;
}
/**
* Register a callback to be called after the operation.
*
* @param \Closure $callback
* @return $this
*/
public function after(Closure $callback)
{
return $this->then($callback);
}
/**
* Register a callback to be called after the operation.
*
* @param \Closure $callback
* @return $this
*/
public function then(Closure $callback)
{
$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)
{
return $this->then(function (Container $container) use ($callback) {
if (0 === $this->exitCode) {
$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)
{
return $this->then(function (Container $container) use ($callback) {
if (0 !== $this->exitCode) {
$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 && file_exists($this->output) ? file_get_contents($this->output) : '';
return $onlyIfOutputExists && empty($output)
? null
: $container->call($callback, ['output' => $output]);
};
}
/**
* Set the human-friendly description of the event.
*
* @param string $description
* @return $this
*/
public function name($description)
{
return $this->description($description);
}
/**
* Set the human-friendly description of the event.
*
* @param string $description
* @return $this
*/
public function description($description)
{
$this->description = $description;
return $this;
}
/**
* Get the summary of the event for display.
*
* @return string
*/
public function getSummaryForDisplay()
{
if (is_string($this->description)) {
return $this->description;
}
return $this->buildCommand();
}
/**
* Determine the next due date for an event.
*
* @param \DateTimeInterface|string $currentTime
* @param int $nth
* @param bool $allowCurrentDate
* @return \Illuminate\Support\Carbon
*/
public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return Date::instance(CronExpression::factory(
$this->getExpression()
)->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone));
}
/**
* Get the Cron expression for the event.
*
* @return string
*/
public function getExpression()
{
return $this->expression;
}
/**
* Set the event mutex implementation to be used.
*
* @param \Illuminate\Console\Scheduling\EventMutex $mutex
* @return $this
*/
public function preventOverlapsUsing(EventMutex $mutex)
{
$this->mutex = $mutex;
return $this;
}
}

View File

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

View File

@@ -0,0 +1,513 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Support\Carbon;
trait ManagesFrequencies
{
/**
* The Cron expression representing the event's frequency.
*
* @param string $expression
* @return $this
*/
public function cron($expression)
{
$this->expression = $expression;
return $this;
}
/**
* Schedule the event to run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return $this
*/
public function between($startTime, $endTime)
{
return $this->when($this->inTimeInterval($startTime, $endTime));
}
/**
* Schedule the event to not run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return $this
*/
public function unlessBetween($startTime, $endTime)
{
return $this->skip($this->inTimeInterval($startTime, $endTime));
}
/**
* Schedule the event to run between start and end time.
*
* @param string $startTime
* @param string $endTime
* @return \Closure
*/
private function inTimeInterval($startTime, $endTime)
{
[$now, $startTime, $endTime] = [
Carbon::now($this->timezone),
Carbon::parse($startTime, $this->timezone),
Carbon::parse($endTime, $this->timezone),
];
if ($endTime->lessThan($startTime)) {
if ($startTime->greaterThan($now)) {
$startTime->subDay(1);
} else {
$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.
*
* @return $this
*/
public function hourly()
{
return $this->spliceIntoPosition(1, 0);
}
/**
* Schedule the event to run hourly at a given offset in the hour.
*
* @param array|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 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.
*
* @return $this
*/
public function daily()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0);
}
/**
* Schedule the command at a given time.
*
* @param string $time
* @return $this
*/
public function at($time)
{
return $this->dailyAt($time);
}
/**
* Schedule the event to run daily at a given time (10:00, 19:30, etc).
*
* @param string $time
* @return $this
*/
public function dailyAt($time)
{
$segments = explode(':', $time);
return $this->spliceIntoPosition(2, (int) $segments[0])
->spliceIntoPosition(1, count($segments) === 2 ? (int) $segments[1] : '0');
}
/**
* Schedule the event to run twice daily.
*
* @param int $first
* @param int $second
* @return $this
*/
public function twiceDaily($first = 1, $second = 13)
{
$hours = $first.','.$second;
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, $hours);
}
/**
* Schedule the event to run only on weekdays.
*
* @return $this
*/
public function weekdays()
{
return $this->spliceIntoPosition(5, '1-5');
}
/**
* Schedule the event to run only on weekends.
*
* @return $this
*/
public function weekends()
{
return $this->spliceIntoPosition(5, '0,6');
}
/**
* Schedule the event to run only on Mondays.
*
* @return $this
*/
public function mondays()
{
return $this->days(1);
}
/**
* Schedule the event to run only on Tuesdays.
*
* @return $this
*/
public function tuesdays()
{
return $this->days(2);
}
/**
* Schedule the event to run only on Wednesdays.
*
* @return $this
*/
public function wednesdays()
{
return $this->days(3);
}
/**
* Schedule the event to run only on Thursdays.
*
* @return $this
*/
public function thursdays()
{
return $this->days(4);
}
/**
* Schedule the event to run only on Fridays.
*
* @return $this
*/
public function fridays()
{
return $this->days(5);
}
/**
* Schedule the event to run only on Saturdays.
*
* @return $this
*/
public function saturdays()
{
return $this->days(6);
}
/**
* Schedule the event to run only on Sundays.
*
* @return $this
*/
public function sundays()
{
return $this->days(0);
}
/**
* Schedule the event to run weekly.
*
* @return $this
*/
public function weekly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(5, 0);
}
/**
* Schedule the event to run weekly on a given day and time.
*
* @param int $day
* @param string $time
* @return $this
*/
public function weeklyOn($day, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(5, $day);
}
/**
* Schedule the event to run monthly.
*
* @return $this
*/
public function monthly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1);
}
/**
* Schedule the event to run monthly on a given day and time.
*
* @param int $day
* @param string $time
* @return $this
*/
public function monthlyOn($day = 1, $time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, $day);
}
/**
* Schedule the event to run twice monthly at a given time.
*
* @param int $first
* @param int $second
* @param string $time
* @return $this
*/
public function twiceMonthly($first = 1, $second = 16, $time = '0:0')
{
$days = $first.','.$second;
$this->dailyAt($time);
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, $days);
}
/**
* Schedule the event to run on the last day of the month.
*
* @param string $time
* @return $this
*/
public function lastDayOfMonth($time = '0:0')
{
$this->dailyAt($time);
return $this->spliceIntoPosition(3, Carbon::now()->endOfMonth()->day);
}
/**
* Schedule the event to run quarterly.
*
* @return $this
*/
public function quarterly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, '1-12/3');
}
/**
* Schedule the event to run yearly.
*
* @return $this
*/
public function yearly()
{
return $this->spliceIntoPosition(1, 0)
->spliceIntoPosition(2, 0)
->spliceIntoPosition(3, 1)
->spliceIntoPosition(4, 1);
}
/**
* Set the days of the week the command should run on.
*
* @param array|mixed $days
* @return $this
*/
public function days($days)
{
$days = is_array($days) ? $days : func_get_args();
return $this->spliceIntoPosition(5, implode(',', $days));
}
/**
* Set the timezone the date should be evaluated on.
*
* @param \DateTimeZone|string $timezone
* @return $this
*/
public function timezone($timezone)
{
$this->timezone = $timezone;
return $this;
}
/**
* Splice the given value into the given position of the expression.
*
* @param int $position
* @param string $value
* @return $this
*/
protected function spliceIntoPosition($position, $value)
{
$segments = explode(' ', $this->expression);
$segments[$position - 1] = $value;
return $this->cron(implode(' ', $segments));
}
}

View File

@@ -0,0 +1,312 @@
<?php
namespace Illuminate\Console\Scheduling;
use Closure;
use DateTimeInterface;
use Illuminate\Console\Application;
use Illuminate\Container\Container;
use Illuminate\Contracts\Bus\Dispatcher;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\CallQueuedClosure;
use Illuminate\Support\ProcessUtils;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use RuntimeException;
class Schedule
{
use Macroable;
/**
* All of the events on the schedule.
*
* @var \Illuminate\Console\Scheduling\Event[]
*/
protected $events = [];
/**
* The event mutex implementation.
*
* @var \Illuminate\Console\Scheduling\EventMutex
*/
protected $eventMutex;
/**
* The scheduling mutex implementation.
*
* @var \Illuminate\Console\Scheduling\SchedulingMutex
*/
protected $schedulingMutex;
/**
* The timezone the date should be evaluated on.
*
* @var \DateTimeZone|string
*/
protected $timezone;
/**
* The job dispatcher implementation.
*
* @var \Illuminate\Contracts\Bus\Dispatcher
*/
protected $dispatcher;
/**
* Create a new schedule instance.
*
* @param \DateTimeZone|string|null $timezone
* @return void
*/
public function __construct($timezone = null)
{
$this->timezone = $timezone;
if (! class_exists(Container::class)) {
throw new RuntimeException(
'A container implementation is required to use the scheduler. Please install the illuminate/container package.'
);
}
$container = Container::getInstance();
$this->eventMutex = $container->bound(EventMutex::class)
? $container->make(EventMutex::class)
: $container->make(CacheEventMutex::class);
$this->schedulingMutex = $container->bound(SchedulingMutex::class)
? $container->make(SchedulingMutex::class)
: $container->make(CacheSchedulingMutex::class);
}
/**
* Add a new callback event to the schedule.
*
* @param string|callable $callback
* @param array $parameters
* @return \Illuminate\Console\Scheduling\CallbackEvent
*/
public function call($callback, array $parameters = [])
{
$this->events[] = $event = new CallbackEvent(
$this->eventMutex, $callback, $parameters, $this->timezone
);
return $event;
}
/**
* Add a new Artisan command event to the schedule.
*
* @param string $command
* @param array $parameters
* @return \Illuminate\Console\Scheduling\Event
*/
public function command($command, array $parameters = [])
{
if (class_exists($command)) {
$command = Container::getInstance()->make($command)->getName();
}
return $this->exec(
Application::formatCommandString($command), $parameters
);
}
/**
* Add a new job callback event to the schedule.
*
* @param object|string $job
* @param string|null $queue
* @param string|null $connection
* @return \Illuminate\Console\Scheduling\CallbackEvent
*/
public function job($job, $queue = null, $connection = null)
{
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
*/
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);
}
$this->getDispatcher()->dispatch(
$job->onConnection($connection)->onQueue($queue)
);
}
/**
* Dispatch the given job right now.
*
* @param object $job
* @return void
*/
protected function dispatchNow($job)
{
$this->getDispatcher()->dispatchNow($job);
}
/**
* Add a new command event to the schedule.
*
* @param string $command
* @param array $parameters
* @return \Illuminate\Console\Scheduling\Event
*/
public function exec($command, array $parameters = [])
{
if (count($parameters)) {
$command .= ' '.$this->compileParameters($parameters);
}
$this->events[] = $event = new Event($this->eventMutex, $command, $this->timezone);
return $event;
}
/**
* Compile parameters for a command.
*
* @param array $parameters
* @return string
*/
protected function compileParameters(array $parameters)
{
return collect($parameters)->map(function ($value, $key) {
if (is_array($value)) {
return $this->compileArrayInput($key, $value);
}
if (! is_numeric($value) && ! preg_match('/^(-.$|--.*)/i', $value)) {
$value = ProcessUtils::escapeArgument($value);
}
return is_numeric($key) ? $value : "{$key}={$value}";
})->implode(' ');
}
/**
* Compile array input for a command.
*
* @param string|int $key
* @param array $value
* @return string
*/
public function compileArrayInput($key, $value)
{
$value = collect($value)->map(function ($value) {
return ProcessUtils::escapeArgument($value);
});
if (Str::startsWith($key, '--')) {
$value = $value->map(function ($value) use ($key) {
return "{$key}={$value}";
});
} elseif (Str::startsWith($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 \Illuminate\Support\Collection
*/
public function dueEvents($app)
{
return collect($this->events)->filter->isDue($app);
}
/**
* Get all of the events on the schedule.
*
* @return \Illuminate\Console\Scheduling\Event[]
*/
public function events()
{
return $this->events;
}
/**
* Specify the cache store that should be used to store mutexes.
*
* @param string $store
* @return $this
*/
public function useCache($store)
{
if ($this->eventMutex instanceof CacheAware) {
$this->eventMutex->useStore($store);
}
if ($this->schedulingMutex instanceof CacheAware) {
$this->schedulingMutex->useStore($store);
}
return $this;
}
/**
* Get the job dispatcher, if available.
*
* @return \Illuminate\Contracts\Bus\Dispatcher
*/
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.',
$e->getCode(), $e
);
}
}
return $this->dispatcher;
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace Illuminate\Console\Scheduling;
use Illuminate\Console\Command;
class ScheduleFinishCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $signature = 'schedule:finish {id} {code=0}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Handle the completion of a scheduled command';
/**
* Indicates whether the command should be shown in the Artisan command list.
*
* @var bool
*/
protected $hidden = true;
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
public function handle(Schedule $schedule)
{
collect($schedule->events())->filter(function ($value) {
return $value->mutexName() == $this->argument('id');
})->each->callAfterCallbacksWithExitCode($this->laravel, $this->argument('code'));
}
}

View File

@@ -0,0 +1,157 @@
<?php
namespace Illuminate\Console\Scheduling;
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\Facades\Date;
use Throwable;
class ScheduleRunCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'schedule:run';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Run the scheduled commands';
/**
* The schedule instance.
*
* @var \Illuminate\Console\Scheduling\Schedule
*/
protected $schedule;
/**
* The 24 hour timestamp this scheduler command started running.
*
* @var \Illuminate\Support\Carbon
*/
protected $startedAt;
/**
* Check if any events ran.
*
* @var bool
*/
protected $eventsRan = false;
/**
* The event dispatcher.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
protected $dispatcher;
/**
* The exception handler.
*
* @var \Illuminate\Contracts\Debug\ExceptionHandler
*/
protected $handler;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
$this->startedAt = Date::now();
parent::__construct();
}
/**
* Execute the console command.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param \Illuminate\Contracts\Events\Dispatcher $dispatcher
* @param \Illuminate\Contracts\Debug\ExceptionHandler $handler
* @return void
*/
public function handle(Schedule $schedule, Dispatcher $dispatcher, ExceptionHandler $handler)
{
$this->schedule = $schedule;
$this->dispatcher = $dispatcher;
$this->handler = $handler;
foreach ($this->schedule->dueEvents($this->laravel) as $event) {
if (! $event->filtersPass($this->laravel)) {
$this->dispatcher->dispatch(new ScheduledTaskSkipped($event));
continue;
}
if ($event->onOneServer) {
$this->runSingleServerEvent($event);
} else {
$this->runEvent($event);
}
$this->eventsRan = true;
}
if (! $this->eventsRan) {
$this->info('No scheduled commands are ready to run.');
}
}
/**
* 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->line('<info>Skipping command (has already run on another server):</info> '.$event->getSummaryForDisplay());
}
}
/**
* Run the given event.
*
* @param \Illuminate\Console\Scheduling\Event $event
* @return void
*/
protected function runEvent($event)
{
$this->line('<info>Running scheduled command:</info> '.$event->getSummaryForDisplay());
$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);
}
}
}

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,45 @@
{
"name": "illuminate/console",
"description": "The Illuminate Console package.",
"license": "MIT",
"homepage": "https://laravel.com",
"support": {
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"authors": [
{
"name": "Taylor Otwell",
"email": "taylor@laravel.com"
}
],
"require": {
"php": "^7.2.5|^8.0",
"illuminate/contracts": "^7.0",
"illuminate/support": "^7.0",
"symfony/console": "^5.0",
"symfony/process": "^5.0"
},
"autoload": {
"psr-4": {
"Illuminate\\Console\\": ""
}
},
"extra": {
"branch-alias": {
"dev-master": "7.x-dev"
}
},
"suggest": {
"dragonmantank/cron-expression": "Required to use scheduler (^2.3.1).",
"guzzlehttp/guzzle": "Required to use the ping methods on schedules (^6.3.1|^7.0.1).",
"illuminate/bus": "Required to use the scheduled job dispatcher (^7.0).",
"illuminate/container": "Required to use the scheduler (^7.0).",
"illuminate/filesystem": "Required to use the generator command (^7.0).",
"illuminate/queue": "Required to use closures for scheduled jobs (^7.0)."
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev"
}