diff --git a/composer.json b/composer.json
index fb7d800..0405b04 100644
--- a/composer.json
+++ b/composer.json
@@ -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/"
}
diff --git a/defines.php b/defines.php
new file mode 100644
index 0000000..f2cfd18
--- /dev/null
+++ b/defines.php
@@ -0,0 +1,13 @@
+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('Checking for tasks to run...');
+ }
+
+ $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('Found ' . count($tasks) . ' task(s) to run>');
+ $this->io->writeln('');
+ $this->io->writeln('Running tasks...');
+ }
+
+ $hasFailure = false;
+
+ // Run tasks
+ foreach ($tasks as $task) {
+ // Display the class we are going to use as info
+ $this->io->write('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('Running task `' . $task['class'] . '`...>');
+ }
+ $this->io->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;
+ }
+
+ }
+}
diff --git a/src/Bakery/ScheduleListCommand.php b/src/Bakery/ScheduleListCommand.php
new file mode 100644
index 0000000..8616521
--- /dev/null
+++ b/src/Bakery/ScheduleListCommand.php
@@ -0,0 +1,49 @@
+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;
+ }
+}
diff --git a/src/Bakery/ScheduleRunCommand.php b/src/Bakery/ScheduleRunCommand.php
new file mode 100644
index 0000000..acd687f
--- /dev/null
+++ b/src/Bakery/ScheduleRunCommand.php
@@ -0,0 +1,92 @@
+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('Running task `' . get_class($taskClass) . '`...>');
+
+ $start = hrtime(true);
+ try {
+ $scheduler->runTask($taskClass);
+ } catch (\Exception $e) {
+ $this->io->writeln('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;
+ }
+}
\ No newline at end of file
diff --git a/src/Scheduler.php b/src/Scheduler.php
index 4d02487..eda96eb 100644
--- a/src/Scheduler.php
+++ b/src/Scheduler.php
@@ -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);
+ }
}
diff --git a/src/Scheduler/BaseTask.php b/src/Scheduler/BaseTask.php
new file mode 100644
index 0000000..9528d82
--- /dev/null
+++ b/src/Scheduler/BaseTask.php
@@ -0,0 +1,111 @@
+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();
+}
diff --git a/src/Scheduler/Scheduler.php b/src/Scheduler/Scheduler.php
new file mode 100644
index 0000000..b16af55
--- /dev/null
+++ b/src/Scheduler/Scheduler.php
@@ -0,0 +1,215 @@
+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,
+ ];
+ }
+}
diff --git a/src/Scheduler/TaskInterface.php b/src/Scheduler/TaskInterface.php
new file mode 100644
index 0000000..ad9b937
--- /dev/null
+++ b/src/Scheduler/TaskInterface.php
@@ -0,0 +1,51 @@
+