From 4a09c485d072f99ebf261337f03a9f19f568bb89 Mon Sep 17 00:00:00 2001 From: Rein Van Oyen Date: Thu, 9 Jul 2020 16:26:17 +0200 Subject: [PATCH] Implemented a basic scheduler for managing crontab and repeating tasks on the server --- composer.json | 3 +- src/Console/Input/ConsoleInput.php | 69 +--------- src/Console/Input/Input.php | 75 ++++++++++ src/Contracts/Console/InputInterface.php | 26 ++-- src/Contracts/Scheduler/JobInterface.php | 21 +++ .../Scheduler/SchedulerInterface.php | 26 ++++ src/Scheduler/Console/SchedulerCommand.php | 47 +++++++ src/Scheduler/Console/TickCommand.php | 49 +++++++ src/Scheduler/CrontabExpressionTrait.php | 130 ++++++++++++++++++ src/Scheduler/Job.php | 40 ++++++ src/Scheduler/Scheduler.php | 51 +++++++ src/Scheduler/SchedulerServiceProvider.php | 31 +++++ 12 files changed, 486 insertions(+), 82 deletions(-) create mode 100644 src/Contracts/Scheduler/JobInterface.php create mode 100644 src/Contracts/Scheduler/SchedulerInterface.php create mode 100644 src/Scheduler/Console/SchedulerCommand.php create mode 100644 src/Scheduler/Console/TickCommand.php create mode 100644 src/Scheduler/CrontabExpressionTrait.php create mode 100644 src/Scheduler/Job.php create mode 100644 src/Scheduler/Scheduler.php create mode 100644 src/Scheduler/SchedulerServiceProvider.php diff --git a/composer.json b/composer.json index 0f93c2f..0fcc769 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ "nyholm/psr7-server": "dev-master", "psr/http-server-handler": "^1.0@dev", "psr/http-server-middleware": "^1.0@dev", - "vlucas/phpdotenv": "^4.1@dev" + "vlucas/phpdotenv": "^4.1@dev", + "dragonmantank/cron-expression": "dev-master" }, "autoload": { "psr-4": { diff --git a/src/Console/Input/ConsoleInput.php b/src/Console/Input/ConsoleInput.php index 8d2cf00..73fefe7 100644 --- a/src/Console/Input/ConsoleInput.php +++ b/src/Console/Input/ConsoleInput.php @@ -11,16 +11,6 @@ */ class ConsoleInput extends Input { - /** - * @var array $rawArguments - */ - private $rawArguments = []; - - /** - * @var array $missingArguments - */ - private $missingArguments = []; - /** * @param Signature $signature */ @@ -52,64 +42,7 @@ private function parse() array_shift($this->rawArguments); - // First we check if any subcommand was requested - if (isset($this->rawArguments[0])) { - foreach ($this->getSignature()->getSubCommands() as $command) { - if ($this->rawArguments[0] === $command->getName()) { - $this->setSubCommand($command->getName()); - break; - } - } - } - - // Then we parse out the options - foreach ($this->getSignature()->getOptions() as $option) { - - $definitions = [ - '-'.$option->getName(), - '--'.$option->getName(), - ]; - - if ($alias = $option->getAlias()) { - $definitions[] = '-'.$alias; - $definitions[] = '--'.$alias; - } - - foreach ($definitions as $definition) { - $optionPosition = array_search($definition, $this->rawArguments); - - if ($optionPosition !== false) { - - // We found the option in the list of given arguments - if ( - isset($this->rawArguments[$optionPosition + 1]) && - substr($this->rawArguments[$optionPosition + 1], 0, strlen('-')) !== '-' - ) { - // We also found a value for the option - // We remove the value of the option from the argument list - $optionValue = $this->rawArguments[$optionPosition + 1]; - $this->setOption($option->getName(), $optionValue); - array_splice($this->rawArguments, $optionPosition + 1, 1); - - } else { - - $this->setOption($option->getName(), $option->getDefault()); - } - - array_splice($this->rawArguments, $optionPosition, 1); - break; - } - } - } - - // Now we loop all arguments and make sure they are present - foreach ($this->getSignature()->getArguments() as $position => $argument) { - if (isset($this->rawArguments[$position])) { - $this->setArgument($argument->getName(), $this->rawArguments[$position]); - } else { - $this->missingArguments[] = $argument->getName(); - } - } + $this->parseRawArguments(); } /** diff --git a/src/Console/Input/Input.php b/src/Console/Input/Input.php index ab5f146..29f0084 100644 --- a/src/Console/Input/Input.php +++ b/src/Console/Input/Input.php @@ -11,6 +11,16 @@ */ abstract class Input implements InputInterface { + /** + * @var array $rawArguments + */ + protected $rawArguments = []; + + /** + * @var array $missingArguments + */ + protected $missingArguments = []; + /** * Holds the given arguments * @@ -161,4 +171,69 @@ public function getSignature(): Signature { return $this->signature; } + + /** + * Parse the raw arguments + */ + protected function parseRawArguments() + { + // First we check if any subcommand was requested + if (isset($this->rawArguments[0])) { + foreach ($this->getSignature()->getSubCommands() as $command) { + if ($this->rawArguments[0] === $command->getName()) { + $this->setSubCommand($command->getName()); + break; + } + } + } + + // Then we parse out the options + foreach ($this->getSignature()->getOptions() as $option) { + + $definitions = [ + '-'.$option->getName(), + '--'.$option->getName(), + ]; + + if ($alias = $option->getAlias()) { + $definitions[] = '-'.$alias; + $definitions[] = '--'.$alias; + } + + foreach ($definitions as $definition) { + $optionPosition = array_search($definition, $this->rawArguments); + + if ($optionPosition !== false) { + + // We found the option in the list of given arguments + if ( + isset($this->rawArguments[$optionPosition + 1]) && + substr($this->rawArguments[$optionPosition + 1], 0, strlen('-')) !== '-' + ) { + // We also found a value for the option + // We remove the value of the option from the argument list + $optionValue = $this->rawArguments[$optionPosition + 1]; + $this->setOption($option->getName(), $optionValue); + array_splice($this->rawArguments, $optionPosition + 1, 1); + + } else { + + $this->setOption($option->getName(), $option->getDefault()); + } + + array_splice($this->rawArguments, $optionPosition, 1); + break; + } + } + } + + // Now we loop all arguments and make sure they are present + foreach ($this->getSignature()->getArguments() as $position => $argument) { + if (isset($this->rawArguments[$position])) { + $this->setArgument($argument->getName(), $this->rawArguments[$position]); + } else { + $this->missingArguments[] = $argument->getName(); + } + } + } } \ No newline at end of file diff --git a/src/Contracts/Console/InputInterface.php b/src/Contracts/Console/InputInterface.php index 86139ac..6143b1f 100644 --- a/src/Contracts/Console/InputInterface.php +++ b/src/Contracts/Console/InputInterface.php @@ -10,22 +10,22 @@ */ interface InputInterface { - public function setSignature(Signature $signature); - public function getSignature(): Signature; + public function setSignature(Signature $signature); + public function getSignature(): Signature; - public function validate(); + public function validate(); - public function getArguments(); - public function getArgument(string $name); + public function getArguments(); + public function getArgument(string $name); - public function hasArgument(string $name): bool; - public function setArgument(string $name, $value); + public function hasArgument(string $name): bool; + public function setArgument(string $name, $value); - public function getOptions(); - public function getOption(string $name); - public function setOption(string $name, $value); + public function getOptions(); + public function getOption(string $name); + public function setOption(string $name, $value); - public function hasSubCommand(): bool; - public function getSubCommand(); - public function setSubCommand(string $name); + public function hasSubCommand(): bool; + public function getSubCommand(); + public function setSubCommand(string $name); } \ No newline at end of file diff --git a/src/Contracts/Scheduler/JobInterface.php b/src/Contracts/Scheduler/JobInterface.php new file mode 100644 index 0000000..8b260ff --- /dev/null +++ b/src/Contracts/Scheduler/JobInterface.php @@ -0,0 +1,21 @@ +scheduler = $scheduler; + + parent::__construct($app); + } + + /** + * @param Signature $signature + * @return Signature + */ + protected function createSignature(Signature $signature): Signature + { + return $signature + ->setName('scheduler') + ->addSubCommand(TickCommand::class) + ; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + $this->scheduler->runSchedule($output); + } +} \ No newline at end of file diff --git a/src/Scheduler/Console/TickCommand.php b/src/Scheduler/Console/TickCommand.php new file mode 100644 index 0000000..8c273a9 --- /dev/null +++ b/src/Scheduler/Console/TickCommand.php @@ -0,0 +1,49 @@ +scheduler = $scheduler; + + parent::__construct($app); + } + + /** + * @param Signature $signature + * @return Signature + */ + protected function createSignature(Signature $signature): Signature + { + return $signature + ->setName('tick') + ; + } + + public function execute(InputInterface $input, OutputInterface $output) + { + while (true) { + $this->scheduler->runSchedule($output); + sleep(60); + } + } +} \ No newline at end of file diff --git a/src/Scheduler/CrontabExpressionTrait.php b/src/Scheduler/CrontabExpressionTrait.php new file mode 100644 index 0000000..530d1bd --- /dev/null +++ b/src/Scheduler/CrontabExpressionTrait.php @@ -0,0 +1,130 @@ +minute.' '.$this->hour.' '.$this->day.' '.$this->month.' '.$this->dayOfWeek; + } + + /** + * @param int $step + * @return $this + */ + public function every(int $step) + { + $this->currentStep = $step; + + return $this; + } + + public function minutes() + { + if (! $this->currentStep) { + return; + } + + $this->minute = '*/'.$this->currentStep; + + $this->currentStep = null; + } + + public function hours() + { + if (! $this->currentStep) { + return; + } + + $this->minute = 0; + $this->hour = '*/'.$this->currentStep; + + $this->currentStep = null; + } + + public function days() + { + if (! $this->currentStep) { + return; + } + + $this->minute = 0; + $this->hour = 0; + $this->day = '*/'.$this->currentStep; + + $this->currentStep = null; + } + + public function months() + { + if (! $this->currentStep) { + return; + } + + $this->minute = 0; + $this->hour = 0; + $this->day = 1; + $this->month = '*/'.$this->currentStep; + + $this->currentStep = null; + } + + public function hourly() + { + $this->hour = 0; + } + + public function daily() + { + $this->minute = 0; + $this->hour = 0; + } + + public function weekly() + { + $this->minute = 0; + $this->hour = 0; + $this->dayOfWeek = 0; + } + + public function monthly() + { + $this->minute = 0; + $this->hour = 0; + $this->day = 1; + } +} \ No newline at end of file diff --git a/src/Scheduler/Job.php b/src/Scheduler/Job.php new file mode 100644 index 0000000..f868ea2 --- /dev/null +++ b/src/Scheduler/Job.php @@ -0,0 +1,40 @@ +command = $command; + } + + /** + * @return string + */ + public function getCommand(): string + { + return $this->command; + } + + /** + * @return string + */ + public function execute(): string + { + return shell_exec($this->command); + } +} \ No newline at end of file diff --git a/src/Scheduler/Scheduler.php b/src/Scheduler/Scheduler.php new file mode 100644 index 0000000..1a264d3 --- /dev/null +++ b/src/Scheduler/Scheduler.php @@ -0,0 +1,51 @@ +jobs[] = $job; + + return $job; + } + + /** + * @param JobInterface $job + * @return bool|null + */ + public function isDue(JobInterface $job) + { + return CronExpression::factory($job->getCronExpression()) + ->isDue(); + } + + public function runSchedule(OutputInterface $output) + { + foreach ($this->jobs as $job) { + if ($this->isDue($job)) { + $output->writeLine('[Scheduler] Running '.$job->getCommand(), OutputInterface::TYPE_INFO); + $output->write($job->execute()); + $output->writeLine('[Scheduler] Done.', OutputInterface::TYPE_INFO); + } else { + $output->writeLine('Nothing to run', OutputInterface::TYPE_WARNING); + } + } + } +} \ No newline at end of file diff --git a/src/Scheduler/SchedulerServiceProvider.php b/src/Scheduler/SchedulerServiceProvider.php new file mode 100644 index 0000000..971557e --- /dev/null +++ b/src/Scheduler/SchedulerServiceProvider.php @@ -0,0 +1,31 @@ +isRunningInConsole()) { + $app->set(SchedulerCommand::class, SchedulerCommand::class); + $app->singleton(SchedulerInterface::class, Scheduler::class); + $app->set(JobInterface::class, Job::class); + } + } + + public function boot(ContainerInterface $app) + { + if ($app->isRunningInConsole()) { + $app->get(KernelInterface::class) + ->registerCommand(SchedulerCommand::class) + ; + } + } +} \ No newline at end of file