Basic scheduler completed

This commit is contained in:
2022-03-16 12:01:55 +00:00
parent 5e67853951
commit c542721982
10 changed files with 737 additions and 0 deletions

View File

@@ -2,7 +2,13 @@
"name": "avsdev/sprinkle-scheduler",
"type": "userfrosting-sprinkle",
"description": "A sprinkle to add scheduled events.",
"require": {
"dragonmantank/cron-expression": "*"
},
"autoload": {
"files" : [
"defines.php"
],
"psr-4": {
"UserFrosting\\Sprinkle\\Scheduler\\": "src/"
}

13
defines.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting;
// Relative path from within sprinkle directory
define('UserFrosting\SCHEDULED_TASKS_DIR', SRC_DIR_NAME . DS . 'Scheduler' . DS . 'Tasks');

View File

@@ -0,0 +1,139 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\Bakery;
use Exception;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use UserFrosting\Sprinkle\Core\Bakery\Helper\ConfirmableTrait;
use UserFrosting\Sprinkle\Core\Database\Tasker\Tasker;
use UserFrosting\System\Bakery\BaseCommand;
/**
* scheduler Bakery Command
* Run a scheduled task.
*
* @author Craig Williams (https://avsdev.uk)
*/
class ScheduleCommand extends BaseCommand
{
use ConfirmableTrait;
/**
* @var Tasker
*/
protected $scheduler;
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('schedule')
->setDescription('Run the schedule')
->setHelp('This command runs the schedule, checking if any tasks are due and executing them.');
#->addOption('quiet', null, InputOption::VALUE_NONE, 'Do not output any status messages.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
// Get options
$quiet = $input->getOption('quiet');
if (!$quiet) {
$this->io->title("UserFrosting's Scheduler");
}
// Prepare task locator
$scheduler = $this->ci->scheduler;
if (!$quiet) {
$this->io->writeln('<comment>Checking for tasks to run...</comment>');
}
$tasks = [];
// Start by getting tasks
foreach ($scheduler->getTasks() as $task) {
if ($task['instance']->isDue()) {
// Add task class to list
$tasks[] = $task;
}
}
if (count($tasks) == 0) {
if (!$quiet) {
$this->io->success('Nothing to do');
}
return self::SUCCESS;
}
if (!$quiet) {
$this->io->writeln('<info>Found ' . count($tasks) . ' task(s) to run</>');
$this->io->writeln('');
$this->io->writeln('<comment>Running tasks...</comment>');
}
$hasFailure = false;
// Run tasks
foreach ($tasks as $task) {
// Display the class we are going to use as info
$this->io->write('<info>Running task `' . $task['class'] . '`...</>');
$start = hrtime(true);
try {
if (!($taskSuccess = $task['instance']->run())) {
throw new Exception('Task returned failure');
}
} catch (\Exception $e) {
$taskSuccess = false;
$hasFailure = true;
if ($quiet) {
$this->io->write('<info>Running task `' . $task['class'] . '`...</>');
}
$this->io->error('<error> [ERROR] ' . $e->getMessage() . ' </>');
}
if (!$quiet && $taskSuccess) {
$end = hrtime(true);
$tdiff = round(($end-$start) / 1e+6);
$tunit = 'ms';
if ($tdiff > 1000) {
$tdiff = round($tdiff / 1000);
$tunit = 's';
}
$this->io->writeln($tdiff . $tunit);
}
}
// Success
if (!$hasFailure) {
if (!$quiet) {
$this->io->success('Schedule success !');
}
return self::SUCCESS;
} else {
if (!$quiet) {
$this->io->error('Schedule failed !');
}
return self::FAILURE;
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\Bakery;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use UserFrosting\System\Bakery\BaseCommand;
/**
* schedule Bakery Command
* List scheduled tasks.
*
* @author Craig Williams (https://avsdev.uk)
*/
class ScheduleListCommand extends BaseCommand
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('schedule:list')
->setDescription('List all scheduled tasks available')
->setHelp('This command returns a list of scheduled tasks that can be called using the `schedule:run` command.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->io->title('Scheduled Tasks List');
$tasks = $this->ci->scheduler->getTasks();
$tasks = array_map(function($task) {
unset($task['instance']);
return $task;
}, $tasks);
$this->io->table(['Schedule', 'Sprinkle', 'Name', 'Namespace'], $tasks);
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,92 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\Bakery;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use UserFrosting\System\Bakery\BaseCommand;
/**
* schedule:run Bakery Command
* Run scheduled tasks.
*
* @author Craig Williams (https://avsdev.uk)
*/
class ScheduleRunCommand extends BaseCommand
{
/**
* {@inheritdoc}
*/
protected function configure()
{
$this->setName('schedule:run')
->setDescription('Run a scheduled task now')
->setHelp('This command runs a task immediately, ignoring all schedule constraints.')
->addArgument('name', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The name of the task(s). Separate multiple tasks with a space.');
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->io->title('Running Task(s)');
$scheduler = $this->ci->scheduler;
// Get options
$names = $input->getArgument('name');
$hasFailure = false;
// Start by getting seeds
foreach ($names as $taskName) {
// Get task class and
try {
$taskClass = $scheduler->getTaskClass($taskName);
} catch (\Exception $e) {
$this->io->error($e->getMessage());
exit(1);
}
// Display the class we are going to use as info
$this->io->write('<info>Running task `' . get_class($taskClass) . '`...</>');
$start = hrtime(true);
try {
$scheduler->runTask($taskClass);
} catch (\Exception $e) {
$this->io->writeln('<error>failed</>');
$this->io->error($e->getMessage());
$hasFailure = true;
}
$end = hrtime(true);
$tdiff = round(($end-$start) / 1e+6);
$tunit = 'ms';
if ($tdiff > 1000) {
$tdiff = round($tdiff / 1000);
$tunit = 's';
}
$this->io->writeln($tdiff . $tunit);
}
if (!$hasFailure) {
$this->io->success('Run task success !');
} else {
$this->io->error('Run task failed !');
}
return self::SUCCESS;
}
}

View File

@@ -18,4 +18,23 @@ use UserFrosting\System\Sprinkle\Sprinkle;
*/
class Scheduler extends Sprinkle
{
/**
* Set static references to DI container in necessary classes.
*/
public function onSprinklesInitialized()
{
$this->registerStreams();
}
/**
* Register Scheduler sprinkle locator streams.
*/
protected function registerStreams()
{
/** @var \UserFrosting\UniformResourceLocator\ResourceLocator $locator */
$locator = $this->ci->locator;
// Register scheduler sprinkle class streams
$locator->registerStream('tasks', '', \UserFrosting\SCHEDULED_TASKS_DIR);
}
}

111
src/Scheduler/BaseTask.php Normal file
View File

@@ -0,0 +1,111 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\Scheduler;
use Cron\CronExpression;
use Psr\Container\ContainerInterface;
use Illuminate\Console\Scheduling\ManagesFrequencies;
use Illuminate\Support\Facades\Date;
/**
* Task Class
* Base class for scheduled tasks.
*
* @author Craig Williams (https://avsdev.uk)
*/
abstract class BaseTask implements TaskInterface
{
use ManagesFrequencies;
/**
* 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 human readable description of the task.
*
* @var string
*/
public $description;
/**
* @var ContainerInterface
*/
protected $ci;
/**
* Constructor.
*
* @param ContainerInterface $ci
*/
public function __construct(ContainerInterface $ci)
{
$this->ci = $ci;
$this->schedule();
}
/**
* Function used to retrieve the cron expression for the task.
*/
public function cronExpression()
{
return $this->expression;
}
/**
* Determine if the given event should run based on the Cron expression.
*
* @return bool
*/
public function isDue()
{
$date = Date::now();
if ($this->timezone) {
$date = $date->setTimezone($this->timezone);
}
return (new CronExpression($this->expression))->isDue($date->toDateTimeString());
}
/**
* Determine the next due date for an event.
*
* @param \DateTimeInterface|string $currentTime
* @param int $nth
* @param bool $allowCurrentDate
* @return \Illuminate\Support\Carbon
*/
public function nextRunDate($currentTime = 'now', $nth = 0, $allowCurrentDate = false)
{
return Date::instance((new CronExpression($this->getExpression()))
->getNextRunDate($currentTime, $nth, $allowCurrentDate, $this->timezone));
}
/**
* Function used to specify the schedule for the task.
*/
abstract public function schedule();
/**
* Function used to specify what the task does.
*/
abstract public function run();
}

215
src/Scheduler/Scheduler.php Normal file
View File

@@ -0,0 +1,215 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\Scheduler;
use Illuminate\Support\Str;
use Psr\Container\ContainerInterface;
use UserFrosting\UniformResourceLocator\Resource as ResourceInstance;
/**
* Scheduler Class.
*
* Finds all tasks class across sprinkles
*
* @author Craig Williams (https://avsdev.uk)
*/
class Scheduler
{
/**
* @var ContainerInterface
*/
protected $ci;
/**
* @var string The resource locator scheme
*/
protected $scheme = 'tasks://';
/**
* Class Constructor.
*
* @param ContainerInterface $ci
*/
public function __construct(ContainerInterface $ci)
{
$this->ci = $ci;
}
/**
* Loop all the available sprinkles and return a list of their tasks.
*
* @return array An array of all the task classes found for every sprinkle
*/
public function getTasks()
{
$tasks = $this->ci->locator->listResources($this->scheme, false, false);
return $this->loadTasks($tasks);
}
/**
* Get a single task info.
*
* @param string $name The task name
*
* @throws \Exception If task not found
*
* @return array The details about a task file [name, class, sprinkle]
*/
public function getTask($name)
{
// Get task resource
$taskResource = $this->ci->locator->getResource($this->scheme . $name . '.php');
// Make sure we found something
if (!$taskResource) {
throw new \Exception("Task $name not found");
}
// Return the task info
return $this->getTaskDetails($taskResource);
}
/**
* Return the class instance of a task.
*
* @param string $name The task name
*
* @throws \Exception If class doesn't exist or is not right interface
*
* @return TaskInterface The task class instance
*/
public function getTaskClass($name)
{
// Try to get task info
$task = $this->getTask($name);
// Make sure class exist
$classPath = $task['class'];
if (!class_exists($classPath)) {
throw new \Exception("Task class `$classPath` not found. Make sure the class has the correct namespace.");
}
// Create a new class instance
$taskClass = new $classPath($this->ci);
// Class must be an instance of `TaskerInterface`
if (!$taskClass instanceof TaskInterface) {
throw new \Exception('Task class must be an instance of `TaskerInterface`');
}
return $taskClass;
}
/**
* Run a task class.
*
* @param TaskInterface $task The task to run
*/
public function runTask(TaskInterface $task)
{
$task->run();
}
/**
* Run a task based on it's name.
*
* @param string $taskName
*/
public function run($taskName)
{
$task = $this->getTaskClass($taskName);
$this->runTask($task);
}
/**
* Return a list of tasks due to be run
*/
public function dueTasks()
{
$tasks = $this->getTasks();
return array_filter($tasks, function($task) {
// Get the task's class
$classPath = $task['class'];
// Create a new class instance
$taskClass = new $classPath($this->ci);
// Class must be an instance of `TaskerInterface`
if (!$taskClass instanceof TaskInterface) {
throw new \Exception('Task class must be an instance of `TaskerInterface`');
}
return $taskClass->isDue();
});
}
/**
* Process tasks Resource into info.
*
* @param array $taskFiles List of tasks files
*
* @return array
*/
protected function loadTasks(array $taskFiles)
{
$tasks = [];
foreach ($taskFiles as $taskFile) {
$tasks[] = $this->getTaskDetails($taskFile);
}
return $tasks;
}
/**
* Return an array of task details including the class name and the sprinkle name.
*
* @param ResourceInstance $file The task file
*
* @return array The details about a task file [name, class, sprinkle]
*/
protected function getTaskDetails(ResourceInstance $file)
{
// Format the sprinkle name for the namespace
$sprinkleName = $file->getLocation()->getName();
$sprinkleName = Str::studly($sprinkleName);
// Getting base path, name and class name
$basePath = str_replace($file->getBasename(), '', $file->getBasePath());
$name = $basePath . $file->getFilename();
$className = str_replace('/', '\\', $basePath) . $file->getFilename();
$classPath = "\\UserFrosting\\Sprinkle\\$sprinkleName\\Scheduler\\Tasks\\$className";
if (!class_exists($classPath)) {
throw new \Exception("Task class `$className` not found. Make sure the class has the correct namespace.");
}
// Create a new class instance
$taskClass = new $classPath($this->ci);
// Class must be an instance of `TaskerInterface`
if (!$taskClass instanceof TaskInterface) {
throw new \Exception('Task class must be an instance of `TaskerInterface`');
}
$schedule = $taskClass->cronExpression();
// Build the class name and namespace
return [
'schedule' => $schedule,
'sprinkle' => $sprinkleName,
'name' => $name,
'class' => $classPath,
'instance' => $taskClass,
];
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\Scheduler;
/**
* All tasks must implement this interface.
*
* @author Craig Williams (https://avsdev.uk)
*/
interface TaskInterface
{
/**
* Function used to retrieve the cron expression for the task.
*/
public function cronExpression();
/**
* Determine if the given event should run based on the Cron expression.
*
* @return bool
*/
public function isDue();
/**
* 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);
/**
* Function used to specify the schedule for the task.
*/
public function schedule();
/**
* Function used to specify what the task does.
*/
public function run();
}

View File

@@ -0,0 +1,42 @@
<?php
/*
* AVSDev UF Scheduler (https://avsdev.uk)
*
* @link https://git.avsdev.uk/avsdev/sprinkle-scheduler
* @license https://git.avsdev.uk/avsdev/sprinkle-scheduler/blob/master/LICENSE.md (LGPL-3.0 License)
*/
namespace UserFrosting\Sprinkle\Scheduler\ServicesProvider;
use Illuminate\Container\Container;
use Psr\Container\ContainerInterface;
use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler;
use UserFrosting\Sprinkle\Core\ServicesProvider\BaseServicesProvider;
/**
* Sheduler services provider.
*
* Registers core services for the Sheduler sprinkle.
*
* @author Craig Williams (https://avsdev.uk)
*/
class ServicesProvider
{
/**
* Register Sheduler's services.
*
* @param ContainerInterface $container A DI container implementing ArrayAccess and psr-container.
*/
public function register(ContainerInterface $container)
{
/*
* Return an instance of the task scheduler
*
* @return \UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler
*/
$container['scheduler'] = function ($c) {
return new Scheduler($c);
};
}
}