From 9ec29185f0ce8a97d2b2bd56a32680ac21d13e23 Mon Sep 17 00:00:00 2001 From: Azurre Date: Tue, 6 Jul 2021 19:45:38 +0400 Subject: [PATCH] Refactoring --- README.md | 62 ++-- composer.json | 46 ++- src/Azurre/Component/Cron/Scheduler.php | 93 ------ src/Expression.php | 371 ++++++++++++++++++++++++ src/Scheduler.php | 59 ++++ 5 files changed, 489 insertions(+), 142 deletions(-) delete mode 100644 src/Azurre/Component/Cron/Scheduler.php create mode 100644 src/Expression.php create mode 100644 src/Scheduler.php diff --git a/README.md b/README.md index 1a7d96f..a3063bb 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,19 @@ -# Scheduler [![Latest Version](https://img.shields.io/github/release/azurre/scheduler.svg?style=flat-square)](https://github.com/azurre/scheduler/releases) +# PHP Cron Scheduler [![Latest Version](https://img.shields.io/github/release/azurre/php-cron-scheduler.svg?style=flat-square)](https://github.com/azurre/php-cron-scheduler/releases) + Simple cron jobs manager. Keep your project cron jobs in your project! # Installation -Install composer in your project: -``` -curl -s https://getcomposer.org/installer | php -``` - Require the package with composer: + ``` -composer require azurre/php-scheduler +composer require azurre/php-cron-scheduler ``` # Usage Add scheduler starter to cron: + ```bash $ crontab -e ``` @@ -25,32 +23,46 @@ $ crontab -e ``` Sample of scheduler.php + ```php $loader = require_once __DIR__ . '/vendor/autoload.php'; use Azurre\Component\Cron\Scheduler; +use Azurre\Component\Cron\Expression; -$php = '/usr/bin/php'; -$path = dirname(__FILE__) . '/'; +$e = new Expression(); -$Scheduler = new Scheduler(); -$Scheduler - ->setJobPath($path) - ->setLogsPath($path); +echo $e->monthly(28); // 0 0 28 * * +echo $e->weekly($e::FRIDAY)->at('05:30'); // 30 5 * * 5 +echo $e->daily('06:10'); // 10 6 * * * -$Scheduler->addJob('* * * * *', function($logsPath){ - // just do something - file_put_contents($logsPath . 'log.log', 'OK', FILE_APPEND); -}); +echo Expression::create() // */5 0 16 1 5 + ->setMinute('*/5') + ->setHour('*') + ->setDayOfMonth(16) + ->setDayOfWeek('fri') + ->setMonth('Jan'); -$Scheduler->addJob('*/2 * * * *', function ($logsPath, $jobPath) use ($php) { - // run standalone php script - $cmd = "{$php} {$jobPath}calculate.php >> {$logsPath}calculate.log 2>&1"; - $result = `$cmd`; -}); +// ------------ -// Do something ... +$testFunc = function () { + echo 'TEST OK'; +}; +$scheduler = new Scheduler(); +$scheduler + ->addJob('* * * * *', function() { + // just do something + })->addJob('0 0 * * * *', $testFunc); +$scheduler->run(); -echo date('d-m-Y H:i:s') . ' Run scheduler...' . PHP_EOL; -$Scheduler->run(); +// ----------- + +$logPath = '/path/to/log.log'; +$scheduler = new Scheduler('2021-07-05 06:10:00'); +$scheduler->addJob($e, function () use($logPath) { + // run standalone php script + $cmd = "/usr/bin/php /path/to/script.php >> {$logPath} 2>&1"; + system($cmd); +}); +$scheduler->run(); ``` diff --git a/composer.json b/composer.json index c07c2fd..a1e26dd 100644 --- a/composer.json +++ b/composer.json @@ -1,24 +1,22 @@ -{ - "name": "azurre/php-scheduler", - "description": "Cron jobs manager", - "keywords": ["cron"], - "authors": [ - { - "name": "Aleksandr Milenin", - "email": "admin@azrr.info" - } - ], - "require": { - "yalesov/cron-expr-parser": "2.*" - }, - "autoload": { - "psr-0": { - "Azurre\\Component\\Cron": "src/" - } - }, - "license": "GPL-3.0", - "homepage": "https://github.com/azurre/scheduler", - "support": { - "issues": "https://github.com/azurre/scheduler/issues" - } -} +{ + "version": "1.2", + "name": "azurre/php-cron-scheduler", + "description": "Cron jobs manager", + "keywords": ["cron","scheduler"], + "authors": [ + { + "name": "Aleksandr Milenin", + "email": "admin@azrr.info" + } + ], + "autoload": { + "psr-4": { + "Azurre\\Component\\Cron\\": "src/" + } + }, + "license": "MIT", + "homepage": "https://github.com/azurre/scheduler", + "support": { + "issues": "https://github.com/azurre/scheduler/issues" + } +} diff --git a/src/Azurre/Component/Cron/Scheduler.php b/src/Azurre/Component/Cron/Scheduler.php deleted file mode 100644 index 63f8734..0000000 --- a/src/Azurre/Component/Cron/Scheduler.php +++ /dev/null @@ -1,93 +0,0 @@ -startTime = $startTime ? $startTime : time(); - } - - /** - * @param string $path - * - * @return $this - */ - public function setJobPath($path) - { - $this->jobsPath = $path; - - return $this; - } - - /** - * @param string $path - * - * @return $this - */ - public function setLogsPath($path) - { - $this->logsPath = $path; - - return $this; - } - - /** - * @param string $expr - * @param callable $callback - * - * @return $this - */ - public function addJob($expr, $callback) - { - $this->jobs[] = ['expr' => $expr, 'callback' => $callback]; - - return $this; - } - - /** - * @return $this - */ - public function clearJobs() - { - $this->jobs[] = []; - - return $this; - } - - - /** - * Run matched jobs - * - * @return $this - * - * @throws \Exception - */ - public function run() - { - foreach ($this->jobs as $job) { - if (Parser::matchTime($this->startTime, $job['expr'])) { - call_user_func_array($job['callback'], [$this->logsPath, $this->jobsPath]); - } - } - - return $this; - } -} \ No newline at end of file diff --git a/src/Expression.php b/src/Expression.php new file mode 100644 index 0000000..5dc3c78 --- /dev/null +++ b/src/Expression.php @@ -0,0 +1,371 @@ +expression = array_slice($expr, 0, 5); + return $instance; + } + + /** + * @param string|number|null $time + * @return bool + */ + public function match($time = null) + { + return static::matchTime($time === null ? time() : $time, (string)$this); + } + + /** + * @param int $minute 0 - 59 + * @return $this + */ + public function hourly($minute = 0) + { + return $this->reset()->setMinute($minute); + } + + /** + * @param int|string|null $timeOrHour + * @param int|null $minutes + * @return $this + */ + public function daily($timeOrHour = null, $minutes = null) + { + return $this->reset()->at($timeOrHour, $minutes); + } + + /** + * @param int|string $dayOfWeek + */ + public function weekly($dayOfWeek = self::MONDAY) + { + return $this->reset()->setDayOfWeek($dayOfWeek)->at(0, 0); + } + + /** + * @param int $dayOfMonth 1 - 31 + * @return $this + */ + public function monthly($dayOfMonth = 1) + { + return $this->reset()->setDayOfMonth($dayOfMonth)->at(0, 0); + } + + /** + * @param int|string|null $timeOrHour + * @param int|null $minutes + * @return $this + */ + public function at($timeOrHour = null, $minutes = null) + { + if (is_numeric($timeOrHour) && is_numeric($minutes)) { + $hour = (int)$timeOrHour; + $minutes = (int)$minutes; + } else if (is_string($timeOrHour) && strpos($timeOrHour, ':') !== false && $minutes === null) { + list($hour, $minutes) = explode(':', $timeOrHour, 2); + $hour = (int)$hour; + $minutes = (int)$minutes; + } else { + $hour = $minutes = 0; + } + return $this->setHour($hour)->setMinute($minutes); + } + + /** + * @param string|int $minute + * @return $this + */ + public function setMinute($minute) + { + $this->expression[static::MINUTE] = $minute; + return $this; + } + + /** + * @param string|int $hour + * @return $this + */ + public function setHour($hour) + { + $this->expression[static::HOUR] = $hour; + return $this; + } + + /** + * @param int $dayOfMonth + * @return $this + */ + public function setDayOfMonth($dayOfMonth) + { + $this->expression[static::DAY_OF_MONTH] = $dayOfMonth; + return $this; + } + + /** + * @param string|int $month + * @return $this + */ + public function setMonth($month) + { + $monthNumeric = static::exprToNumeric($month); + $this->expression[static::MONTH] = $monthNumeric === false ? $month: $monthNumeric; + return $this; + } + + /** + * @param string|int $dayOfWeek + * @return $this + */ + public function setDayOfWeek($dayOfWeek) + { + $dayOfWeekNumeric = static::exprToNumeric($dayOfWeek); + $this->expression[static::DAY_OF_WEEk] = $dayOfWeekNumeric === false ? $dayOfWeek: $dayOfWeekNumeric; + return $this; + } + + /** + * @return $this + */ + public function reset() + { + $this->expression = self::DEFAULT_EXPRESSION; + return $this; + } + + /** + * determine whether a given time falls within the given cron expr + * + * @param string|numeric $time + * timestamp or strtotime()-compatible string + * @param string $expr + * any valid cron expression, in addition supporting: + * range: '0-5' + * range + interval: '10-59/5' + * comma-separated combinations of these: '1,4,7,10-20' + * English months: 'january' + * English months (abbreviated to three letters): 'jan' + * English weekdays: 'monday' + * English weekdays (abbreviated to three letters): 'mon' + * These text counterparts can be used in all places where their + * numerical counterparts are allowed, e.g. 'jan-jun/2' + * A full example: + * '0-5,10-59/5 * 2-10,15-25 january-june/2 mon-fri' - + * every minute between minute 0-5 + every 5th min between 10-59 + * every hour + * every day between day 2-10 and day 15-25 + * every 2nd month between January-June + * Monday-Friday + * @return bool + */ + public static function matchTime($time, $expr) + { + $cronExpr = preg_split('/\s+/', $expr, null, PREG_SPLIT_NO_EMPTY); + if (count($cronExpr) !== 5) { + throw new \InvalidArgumentException( + sprintf( + 'cron expression should have exactly 5 arguments, "%s" given', + $expr + ) + ); + } + if (is_string($time)) { + $time = strtotime($time); + } + $date = getdate($time); + return self::matchTimeComponent($cronExpr[0], $date['minutes']) && self::matchTimeComponent( + $cronExpr[1], + $date['hours'] + ) && self::matchTimeComponent($cronExpr[2], $date['mday']) && self::matchTimeComponent( + $cronExpr[3], + $date['mon'] + ) && self::matchTimeComponent($cronExpr[4], $date['wday']); + } + + /** + * match a cron expression component to a given corresponding date/time + * + * In the expression, * * * * *, each component + * *[1] *[2] *[3] *[4] *[5] + * will correspond to a getdate() component + * 1. $date['minutes'] + * 2. $date['hours'] + * 3. $date['mday'] + * 4. $date['mon'] + * 5. $date['wday'] + * + * @param string $expr + * @param numeric $num + * @return bool + * @see self::exprToNumeric() for additional valid string values + * + */ + public static function matchTimeComponent($expr, $num) + { + //handle all match + if ($expr === '*') { + return true; + } + //handle multiple options + if (strpos($expr, ',') !== false) { + $args = explode(',', $expr); + foreach ($args as $arg) { + if (self::matchTimeComponent($arg, $num)) { + return true; + } + } + return false; + } + //handle modulus + if (strpos($expr, '/') !== false) { + $arg = explode('/', $expr); + if (count($arg) !== 2) { + throw new \InvalidArgumentException( + sprintf( + 'invalid cron expression component: ' . 'expecting match/modulus, "%s" given', + $expr + ) + ); + } + if (!is_numeric($arg[1])) { + throw new \InvalidArgumentException( + sprintf( + 'invalid cron expression component: ' . 'expecting numeric modulus, "%s" given', + $expr + ) + ); + } + $expr = $arg[0]; + $mod = $arg[1]; + } else { + $mod = 1; + } + //handle all match by modulus + if ($expr === '*') { + $from = 0; + $to = 60; + } //handle range + elseif (strpos($expr, '-') !== false) { + $arg = explode('-', $expr); + if (count($arg) !== 2) { + throw new \InvalidArgumentException( + sprintf( + 'invalid cron expression component: ' . 'expecting from-to structure, "%s" given', + $expr + ) + ); + } + $from = self::exprToNumeric($arg[0]); + $to = self::exprToNumeric($arg[1]); + } //handle regular token + else { + $from = self::exprToNumeric($expr); + $to = $from; + } + if ($from === false || $to === false) { + throw new \InvalidArgumentException( + sprintf( + 'invalid cron expression component: ' . 'expecting numeric or valid string, "%s" given', + $expr + ) + ); + } + return ($num >= $from) && ($num <= $to) && ($num % $mod === 0); + } + + /** + * parse a string month / weekday expression to its numeric equivalent + * + * @param string|numeric $value + * accepts, case insensitive, + * - Jan - Dec + * - Sun - Sat + * - (or their long forms - only the first three letters important) + * @return int|false + */ + public static function exprToNumeric($value) + { + static $data = [ + 'jan' => 1, + 'feb' => 2, + 'mar' => 3, + 'apr' => 4, + 'may' => 5, + 'jun' => 6, + 'jul' => 7, + 'aug' => 8, + 'sep' => 9, + 'oct' => 10, + 'nov' => 11, + 'dec' => 12, + 'sun' => 0, + 'mon' => 1, + 'tue' => 2, + 'wed' => 3, + 'thu' => 4, + 'fri' => 5, + 'sat' => 6, + ]; + if (is_numeric($value)) { + // allow all numerics values, this change fix the bug for minutes range like 0-59 or hour range like 0-20 + return $value; + } + if (is_string($value)) { + $value = strtolower(substr($value, 0, 3)); + if (isset($data[$value])) { + return $data[$value]; + } + } + return false; + } + + public function __toString() + { + return implode($this->expression, ' '); + } +} diff --git a/src/Scheduler.php b/src/Scheduler.php new file mode 100644 index 0000000..79740e9 --- /dev/null +++ b/src/Scheduler.php @@ -0,0 +1,59 @@ +startTime = $startTime ?: $_SERVER['REQUEST_TIME']; + } + + /** + * @param string|Expression $expr + * @param callable $callback + * + * @return $this + */ + public function addJob($expr, callable $callback) + { + $this->jobs[] = [$expr, $callback]; + return $this; + } + + /** + * @return $this + */ + public function clearJobs() + { + $this->jobs[] = []; + return $this; + } + + + /** + * Run matched jobs + * + * @return void + */ + public function run() + { + foreach ($this->jobs as $job) { + list($expr, $callback) = $job; + if (Expression::matchTime($this->startTime, (string)$expr)) { + $callback($expr); + } + } + } +} \ No newline at end of file