├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── codeception.yml ├── composer.json ├── src ├── ErrorHandler.php ├── Module.php ├── Task.php ├── TaskRunner.php ├── actions │ ├── IndexAction.php │ ├── UpdateAction.php │ └── ViewLogAction.php ├── console │ └── SchedulerController.php ├── events │ ├── SchedulerEvent.php │ └── TaskEvent.php ├── migrations │ ├── 000_init.sql │ └── m150510_090513_Scheduler.php ├── models │ ├── SchedulerLog.php │ ├── SchedulerTask.php │ └── base │ │ ├── SchedulerLog.php │ │ └── SchedulerTask.php └── views │ ├── index.php │ ├── update.php │ └── view-log.php └── tests ├── .gitignore ├── _app ├── assets │ └── .gitignore ├── components │ └── MailerMock.php ├── config │ ├── console.php │ ├── db.php │ └── web.php ├── controllers │ └── SiteController.php ├── runtime │ └── .gitignore ├── views │ ├── layouts │ │ └── main.php │ └── site │ │ └── index.php └── yii ├── _bootstrap.php ├── _config ├── functional.php └── unit.php ├── _output └── .gitignore ├── functional.suite.yml ├── functional └── _bootstrap.php ├── tasks ├── AlphabetTask.php ├── ErrorTask.php └── NumberTask.php ├── unit.suite.yml └── unit ├── ModuleTest.php ├── TaskRunnerTest.php ├── TaskTest.php └── _bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /build/ 3 | /.idea/ 4 | /.vagrant 5 | /Vagrantfile 6 | /vagrant 7 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: [src/*] 3 | excluded_paths: [tests/*] 4 | checks: 5 | php: 6 | code_rating: true 7 | remove_extra_empty_lines: true 8 | remove_php_closing_tag: true 9 | remove_trailing_whitespace: true 10 | fix_use_statements: 11 | remove_unused: true 12 | preserve_multiple: false 13 | preserve_blanklines: true 14 | order_alphabetically: true 15 | fix_php_opening_tag: true 16 | fix_linefeed: true 17 | fix_line_ending: true 18 | fix_identation_4spaces: true 19 | fix_doc_comments: true 20 | tools: 21 | external_code_coverage: 22 | timeout: 2100 23 | php_sim: false 24 | php_cpd: false -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | services: 4 | - mysql 5 | 6 | php: 7 | - 5.6 8 | - 7.0 9 | 10 | install: 11 | - composer self-update 12 | - composer global require "fxp/composer-asset-plugin" 13 | - travis_retry composer install --no-interaction --prefer-source 14 | 15 | before_script: 16 | - travis_retry mysql -e "CREATE DATABASE scheduler_test;" 17 | - php tests/_app/yii migrate/up --interactive=0 --migrationPath=src/migrations/ 18 | - vendor/bin/codecept build 19 | 20 | script: 21 | - vendor/bin/codecept run --coverage --coverage-xml 22 | 23 | after_script: 24 | - wget https://scrutinizer-ci.com/ocular.phar 25 | - php ocular.phar code-coverage:upload --format=php-clover tests/_output/coverage.xml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Webtools Ltd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yii2-scheduler 2 | 3 | [](LICENSE) 4 | [](https://travis-ci.org/webtoolsnz/yii2-scheduler) 5 | [](https://scrutinizer-ci.com/g/webtoolsnz/yii2-scheduler/code-structure) 6 | [](https://scrutinizer-ci.com/g/webtoolsnz/yii2-scheduler) 7 | 8 | 9 | A scheduled task manager for yii2 10 | 11 | ## Installation 12 | 13 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/). 14 | 15 | Install using the following command. 16 | 17 | ~~~bash 18 | $ composer require webtoolsnz/yii2-scheduler 19 | ~~~ 20 | 21 | Now that the package has been installed you need to configure the module in your application 22 | 23 | The `config/console.php` file should be updated to reflect the changes below 24 | ~~~php 25 | 'bootstrap' => ['log', 'scheduler'], 26 | 'modules' => [ 27 | 'scheduler' => ['class' => 'webtoolsnz\scheduler\Module'], 28 | ], 29 | 'components' => [ 30 | 'errorHandler' => [ 31 | 'class' => 'webtoolsnz\scheduler\ErrorHandler' 32 | ], 33 | 'log' => [ 34 | 'traceLevel' => YII_DEBUG ? 3 : 0, 35 | 'targets' => [ 36 | [ 37 | 'class' => 'yii\log\EmailTarget', 38 | 'mailer' =>'mailer', 39 | 'levels' => ['error', 'warning'], 40 | 'message' => [ 41 | 'to' => ['admin@example.com'], 42 | 'from' => [$params['adminEmail']], 43 | 'subject' => 'Scheduler Error - ####SERVERNAME####' 44 | ], 45 | 'except' => [ 46 | ], 47 | ], 48 | ], 49 | ], 50 | ] 51 | ~~~ 52 | 53 | also add this to the top of your `config/console.php` file 54 | ~~~php 55 | \yii\base\Event::on( 56 | \webtoolsnz\scheduler\console\SchedulerController::className(), 57 | \webtoolsnz\scheduler\events\SchedulerEvent::EVENT_AFTER_RUN, 58 | function ($event) { 59 | if (!$event->success) { 60 | foreach($event->exceptions as $exception) { 61 | throw $exception; 62 | } 63 | } 64 | } 65 | ); 66 | ~~~ 67 | 68 | To implement the GUI for scheduler also add the following to your `config/web.php` 69 | ~~~php 70 | 'bootstrap' => ['log', 'scheduler'], 71 | 'modules' => [ 72 | 'scheduler' => ['class' => 'webtoolsnz\scheduler\Module'], 73 | ], 74 | ~~~ 75 | 76 | After the configuration files have been updated, a `tasks` directory will need to be created in the root of your project. 77 | 78 | 79 | Run the database migrations, which will create the necessary tables for `scheduler` 80 | ~~~bash 81 | php yii migrate up --migrationPath=vendor/webtoolsnz/yii2-scheduler/src/migrations 82 | ~~~ 83 | 84 | Add a controller 85 | ~~~php 86 | [ 102 | 'class' => 'webtoolsnz\scheduler\actions\IndexAction', 103 | 'view' => '@scheduler/views/index', 104 | ], 105 | 'update' => [ 106 | 'class' => 'webtoolsnz\scheduler\actions\UpdateAction', 107 | 'view' => '@scheduler/views/update', 108 | ], 109 | 'view-log' => [ 110 | 'class' => 'webtoolsnz\scheduler\actions\ViewLogAction', 111 | 'view' => '@scheduler/views/view-log', 112 | ], 113 | ]; 114 | } 115 | } 116 | ~~~ 117 | 118 | ## Example Task 119 | 120 | You can now create your first task using scheduler, create the file `AlphabetTask.php` inside the `tasks` directory in your project root. 121 | 122 | Paste the below code into your task: 123 | ~~~php 124 | /dev/null & 174 | ``` 175 | 176 | ### Events & Errors 177 | 178 | Events are thrown before and running individual tasks as well as at a global level for multiple tasks 179 | 180 | Task Level 181 | 182 | ```php 183 | Event::on(AlphabetTask::className(), AlphabetTask::EVENT_BEFORE_RUN, function ($event) { 184 | Yii::trace($event->task->className . ' is about to run'); 185 | }); 186 | Event::on(AlphabetTask::className(), AlphabetTask::EVENT_AFTER_RUN, function ($event) { 187 | Yii::trace($event->task->className . ' just ran '.($event->success ? 'successfully' : 'and failed')); 188 | }); 189 | ``` 190 | 191 | or at the global level, to throw errors in `/yii` 192 | 193 | ```php 194 | $application->on(\webtoolsnz\scheduler\events\SchedulerEvent::EVENT_AFTER_RUN, function ($event) { 195 | if (!$event->success) { 196 | foreach($event->exceptions as $exception) { 197 | throw $exception; 198 | } 199 | } 200 | }); 201 | ``` 202 | 203 | You could throw the exceptions at the task level, however this will prevent further tasks from running. 204 | 205 | ## License 206 | 207 | The MIT License (MIT). Please see [LICENSE](LICENSE) for more information. 208 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | namespace: webtoolsnz\scheduler\tests 2 | actor: Tester 3 | paths: 4 | tests: tests 5 | log: tests/_output 6 | data: tests/_data 7 | helpers: tests/_support 8 | settings: 9 | bootstrap: _bootstrap.php 10 | colors: true 11 | memory_limit: 1024M 12 | coverage: 13 | enabled: true 14 | include: 15 | - src/* 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webtoolsnz/yii2-scheduler", 3 | "description": "A scheduled task runner for Yii2 applications", 4 | "keywords": ["yii2", "cron", "scheduler", "task"], 5 | "type": "yii2-extension", 6 | "license": "proprietary", 7 | "homepage": "https://github.com/webtoolsnz/yii2-scheduler", 8 | "authors": [ 9 | { 10 | "name": "Byron Adams", 11 | "email": "byron@webtools.co.nz" 12 | }, 13 | { 14 | "name": "Webtools Ltd", 15 | "email": "support@webtools.co.nz", 16 | "homepage": "http://www.webtools.co.nz", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "yiisoft/yii2": "^2.0.9", 22 | "mtdowling/cron-expression": "~1.0", 23 | "webtoolsnz/yii2-widgets": "*" 24 | }, 25 | "require-dev": { 26 | "guzzlehttp/guzzle": "~5.0|~4.0", 27 | "phpdocumentor/reflection-docblock" : "^2.0", 28 | "yiisoft/yii2-codeception": "*", 29 | "codeception/codeception": "*", 30 | "codeception/specify": "*", 31 | "codeception/verify": "*" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "webtoolsnz\\scheduler\\": "src", 36 | "webtoolsnz\\scheduler\\tests\\": "tests" 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ErrorHandler.php: -------------------------------------------------------------------------------- 1 | taskRunner && E_ERROR == $error['type']) { 34 | $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); 35 | $this->taskRunner->handleError($exception); 36 | } 37 | 38 | // Allow yiis error handler to take over and handle logging 39 | parent::handleFatalError(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Module.php: -------------------------------------------------------------------------------- 1 | controllerMap[$this->id])) { 37 | $app->controllerMap[$this->id] = [ 38 | 'class' => 'webtoolsnz\scheduler\console\SchedulerController', 39 | ]; 40 | } 41 | } 42 | 43 | /** 44 | * Scans the taskPath for any task files, if any are found it attempts to load them, 45 | * creates a new instance of each class and appends it to an array, which it returns. 46 | * 47 | * @return Task[] 48 | * @throws \yii\base\ErrorException 49 | */ 50 | public function getTasks() 51 | { 52 | $dir = Yii::getAlias($this->taskPath); 53 | 54 | if (!is_readable($dir)) { 55 | throw new \yii\base\ErrorException("Task directory ($dir) does not exist"); 56 | } 57 | 58 | $files = array_diff(scandir($dir), array('..', '.')); 59 | $tasks = []; 60 | 61 | foreach ($files as $fileName) { 62 | // strip out the file extension to derive the class name 63 | $className = preg_replace('/\.[^.]*$/', '', $fileName); 64 | 65 | // validate class name 66 | if (preg_match('/^[a-zA-Z0-9_]*Task$/', $className)) { 67 | $tasks[] = $this->loadTask($className); 68 | } 69 | } 70 | 71 | $this->cleanTasks($tasks); 72 | 73 | return $tasks; 74 | } 75 | 76 | /** 77 | * Removes any records of tasks that no longer exist. 78 | * 79 | * @param Task[] $tasks 80 | */ 81 | public function cleanTasks($tasks) 82 | { 83 | $currentTasks = ArrayHelper::map($tasks, function ($task) { 84 | return $task->getName(); 85 | }, 'description'); 86 | 87 | foreach (SchedulerTask::find()->indexBy('name')->all() as $name => $task) { /* @var SchedulerTask $task */ 88 | if (!array_key_exists($name, $currentTasks)) { 89 | SchedulerLog::deleteAll(['scheduler_task_id' => $task->id]); 90 | $task->delete(); 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Given the className of a task, it will return a new instance of that task. 97 | * If the task doesn't exist, null will be returned. 98 | * 99 | * @param $className 100 | * @return null|object 101 | * @throws \yii\base\InvalidConfigException 102 | */ 103 | public function loadTask($className) 104 | { 105 | $className = implode('\\', [$this->taskNameSpace, $className]); 106 | 107 | try { 108 | $task = Yii::createObject($className); 109 | $task->setModel(SchedulerTask::createTaskModel($task)); 110 | } catch (\ReflectionException $e) { 111 | $task = null; 112 | } 113 | 114 | return $task; 115 | } 116 | 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/Task.php: -------------------------------------------------------------------------------- 1 | db; 75 | $result = $db->createCommand("GET_LOCK(:lockname, 1)", [':lockname' => $lockName])->queryScalar(); 76 | 77 | if (!$result) { 78 | // we didn't get the lock which means the task is still running 79 | $event->cancel = true; 80 | } 81 | }); 82 | \yii\base\Event::on(self::className(), self::EVENT_AFTER_RUN, function ($event) use ($lockName) { 83 | // release the lock 84 | /* @var $event TaskEvent */ 85 | $db = \Yii::$app->db; 86 | $db->createCommand("RELEASE_LOCK(:lockname, 1)", [':lockname' => $lockName])->queryScalar(); 87 | }); 88 | } 89 | 90 | /** 91 | * The main method that gets invoked whenever a task is ran, any errors that occur 92 | * inside this method will be captured by the TaskRunner and logged against the task. 93 | * 94 | * @return mixed 95 | */ 96 | abstract public function run(); 97 | 98 | /** 99 | * @param string|\DateTime $currentTime 100 | * @return string 101 | */ 102 | public function getNextRunDate($currentTime = 'now') 103 | { 104 | return CronExpression::factory($this->schedule) 105 | ->getNextRunDate($currentTime) 106 | ->format('Y-m-d H:i:s'); 107 | } 108 | 109 | /** 110 | * @return string 111 | */ 112 | public function getName() 113 | { 114 | return StringHelper::basename(get_class($this)); 115 | } 116 | 117 | /** 118 | * @param SchedulerTask $model 119 | */ 120 | public function setModel($model) 121 | { 122 | $this->_model = $model; 123 | } 124 | 125 | /** 126 | * @return SchedulerTask 127 | */ 128 | public function getModel() 129 | { 130 | return $this->_model; 131 | } 132 | 133 | /** 134 | * @param $str 135 | */ 136 | public function writeLine($str) 137 | { 138 | echo $str.PHP_EOL; 139 | } 140 | 141 | /** 142 | * Mark the task as started 143 | */ 144 | public function start() 145 | { 146 | $model = $this->getModel(); 147 | $model->started_at = date('Y-m-d H:i:s'); 148 | $model->save(false);} 149 | 150 | /** 151 | * Mark the task as stopped. 152 | */ 153 | public function stop() 154 | { 155 | $model = $this->getModel(); 156 | $model->last_run = $model->started_at; 157 | $model->next_run = $this->getNextRunDate(); 158 | $model->started_at = null; 159 | $model->save(false); 160 | } 161 | 162 | /** 163 | * @param bool $forceRun 164 | * @return bool 165 | */ 166 | public function shouldRun($forceRun = false) 167 | { 168 | $model = $this->getModel(); 169 | $isDue = in_array($model->status_id, [SchedulerTask::STATUS_DUE, SchedulerTask::STATUS_OVERDUE, SchedulerTask::STATUS_ERROR]); 170 | $isRunning = $model->status_id == SchedulerTask::STATUS_RUNNING; 171 | $overdue = false; 172 | if((strtotime($model->started_at) + $this->overdueThreshold) <= strtotime("now")) { 173 | $overdue = true; 174 | } 175 | 176 | return ($model->active && ((!$isRunning && ($isDue || $forceRun)) || ($isRunning && $overdue))); 177 | } 178 | 179 | } 180 | -------------------------------------------------------------------------------- /src/TaskRunner.php: -------------------------------------------------------------------------------- 1 | _task = $task; 49 | } 50 | 51 | /** 52 | * @return Task 53 | */ 54 | public function getTask() 55 | { 56 | return $this->_task; 57 | } 58 | 59 | /** 60 | * @param \webtoolsnz\scheduler\models\SchedulerLog $log 61 | */ 62 | public function setLog($log) 63 | { 64 | $this->_log = $log; 65 | } 66 | 67 | /** 68 | * @return SchedulerLog 69 | */ 70 | public function getLog() 71 | { 72 | return $this->_log; 73 | } 74 | 75 | /** 76 | * @param bool $forceRun 77 | */ 78 | public function runTask($forceRun = false) 79 | { 80 | $task = $this->getTask(); 81 | 82 | if ($task->shouldRun($forceRun)) { 83 | $event = new TaskEvent([ 84 | 'task' => $task, 85 | 'success' => true, 86 | ]); 87 | $this->trigger(Task::EVENT_BEFORE_RUN, $event); 88 | if (!$event->cancel) { 89 | $task->start(); 90 | ob_start(); 91 | try { 92 | $this->running = true; 93 | $this->shutdownHandler(); 94 | $task->run(); 95 | $this->running = false; 96 | $output = ob_get_contents(); 97 | ob_end_clean(); 98 | $this->log($output); 99 | $task->stop(); 100 | } catch (\Exception $e) { 101 | $this->running = false; 102 | $task->exception = $e; 103 | $event->exception = $e; 104 | $event->success = false; 105 | $this->handleError($e); 106 | } 107 | $this->trigger(Task::EVENT_AFTER_RUN, $event); 108 | } 109 | } 110 | $task->getModel()->save(); 111 | } 112 | 113 | /** 114 | * If the yii error handler has been overridden with `\webtoolsnz\scheduler\ErrorHandler`, 115 | * pass it this instance of TaskRunner, so it can update the state of tasks in the event of a fatal error. 116 | */ 117 | public function shutdownHandler() 118 | { 119 | $errorHandler = Yii::$app->getErrorHandler(); 120 | 121 | if ($errorHandler instanceof \webtoolsnz\scheduler\ErrorHandler) { 122 | Yii::$app->getErrorHandler()->taskRunner = $this; 123 | } 124 | } 125 | 126 | /** 127 | * @param \Exception|ErrorException|Exception $exception 128 | */ 129 | public function handleError(\Exception $exception) 130 | { 131 | echo sprintf( 132 | "%s: %s \n\n Stack Trace: \n %s", 133 | method_exists($exception, 'getName') ? $exception->getName() : get_class($exception), 134 | $exception->getMessage(), 135 | $exception->getTraceAsString() 136 | ); 137 | 138 | // if the failed task was mid transaction, rollback so we can save. 139 | if (null !== ($tx = \Yii::$app->db->getTransaction())) { 140 | $tx->rollBack(); 141 | } 142 | 143 | $output = ''; 144 | 145 | if(ob_get_length() > 0) { 146 | $output = ob_get_contents(); 147 | ob_end_clean(); 148 | } 149 | 150 | $this->error = true; 151 | $this->log($output); 152 | $this->getTask()->getModel()->status_id = SchedulerTask::STATUS_ERROR; 153 | $this->getTask()->stop(); 154 | 155 | $this->getTask()->trigger(Task::EVENT_FAILURE, new TaskEvent([ 156 | 'task' => $this->getTask(), 157 | 'output' => $output, 158 | 'success' => false, 159 | 'exception' => $exception, 160 | ])); 161 | } 162 | 163 | /** 164 | * @param string $output 165 | */ 166 | public function log($output) 167 | { 168 | $model = $this->getTask()->getModel(); 169 | $log = $this->getLog(); 170 | $log->started_at = $model->started_at; 171 | $log->ended_at = date('Y-m-d H:i:s'); 172 | $log->error = $this->error ? 1 : 0; 173 | $log->output = $output; 174 | $log->scheduler_task_id = $model->id; 175 | $log->save(false); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/actions/IndexAction.php: -------------------------------------------------------------------------------- 1 | search($_GET); 30 | 31 | return $this->controller->render($this->view ?: $this->id, [ 32 | 'dataProvider' => $dataProvider, 33 | 'model' => $model, 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/actions/UpdateAction.php: -------------------------------------------------------------------------------- 1 | getRequest(); 31 | 32 | if (!$model) { 33 | throw new \yii\web\HttpException(404, 'The requested page does not exist.'); 34 | } 35 | 36 | if ($model->load($request->post())) { 37 | $model->save(); 38 | } 39 | 40 | $logModel = new SchedulerLog(); 41 | $logModel->scheduler_task_id = $model->id; 42 | $logDataProvider = $logModel->search($_GET); 43 | 44 | return $this->controller->render($this->view ?: $this->id, [ 45 | 'model' => $model, 46 | 'logModel' => $logModel, 47 | 'logDataProvider' => $logDataProvider, 48 | ]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/actions/ViewLogAction.php: -------------------------------------------------------------------------------- 1 | controller->render($this->view ?: $this->id, [ 35 | 'model' => $model, 36 | ]); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/console/SchedulerController.php: -------------------------------------------------------------------------------- 1 | Console::FG_BLUE, 46 | SchedulerTask::STATUS_DUE => Console::FG_YELLOW, 47 | SchedulerTask::STATUS_OVERDUE => Console::FG_RED, 48 | SchedulerTask::STATUS_RUNNING => Console::FG_GREEN, 49 | SchedulerTask::STATUS_ERROR => Console::FG_RED, 50 | ]; 51 | 52 | /** 53 | * @param string $actionId 54 | * @return array 55 | */ 56 | public function options($actionId) 57 | { 58 | $options = []; 59 | 60 | switch ($actionId) { 61 | case 'run-all': 62 | $options[] = 'force'; 63 | break; 64 | case 'run': 65 | $options[] = 'force'; 66 | $options[] = 'taskName'; 67 | break; 68 | } 69 | 70 | return $options; 71 | } 72 | 73 | /** 74 | * @return \webtoolsnz\scheduler\Module 75 | */ 76 | private function getScheduler() 77 | { 78 | return Yii::$app->getModule('scheduler'); 79 | } 80 | 81 | /** 82 | * List all tasks 83 | */ 84 | public function actionIndex() 85 | { 86 | // Update task index 87 | $this->getScheduler()->getTasks(); 88 | $models = SchedulerTask::find()->all(); 89 | 90 | echo $this->ansiFormat('Scheduled Tasks', Console::UNDERLINE).PHP_EOL; 91 | 92 | foreach ($models as $model) { /* @var SchedulerTask $model */ 93 | $row = sprintf( 94 | "%s\t%s\t%s\t%s\t%s", 95 | $model->name, 96 | $model->schedule, 97 | is_null($model->last_run) ? 'NULL' : $model->last_run, 98 | $model->next_run, 99 | $model->getStatus() 100 | ); 101 | 102 | $color = isset($this->_statusColors[$model->status_id]) ? $this->_statusColors[$model->status_id] : null; 103 | echo $this->ansiFormat($row, $color).PHP_EOL; 104 | } 105 | } 106 | 107 | /** 108 | * Run all due tasks 109 | */ 110 | public function actionRunAll() 111 | { 112 | $tasks = $this->getScheduler()->getTasks(); 113 | 114 | echo 'Running Tasks:'.PHP_EOL; 115 | $event = new SchedulerEvent([ 116 | 'tasks' => $tasks, 117 | 'success' => true, 118 | ]); 119 | $this->trigger(SchedulerEvent::EVENT_BEFORE_RUN, $event); 120 | foreach ($tasks as $task) { 121 | $this->runTask($task); 122 | if ($task->exception) { 123 | $event->success = false; 124 | $event->exceptions[] = $task->exception; 125 | } 126 | } 127 | $this->trigger(SchedulerEvent::EVENT_AFTER_RUN, $event); 128 | echo PHP_EOL; 129 | } 130 | 131 | /** 132 | * Run the specified task (if due) 133 | */ 134 | public function actionRun() 135 | { 136 | if (null === $this->taskName) { 137 | throw new InvalidParamException('taskName must be specified'); 138 | } 139 | 140 | /* @var Task $task */ 141 | $task = $this->getScheduler()->loadTask($this->taskName); 142 | 143 | if (!$task) { 144 | throw new InvalidParamException('Invalid taskName'); 145 | } 146 | $event = new SchedulerEvent([ 147 | 'tasks' => [$task], 148 | 'success' => true, 149 | ]); 150 | $this->trigger(SchedulerEvent::EVENT_BEFORE_RUN, $event); 151 | $this->runTask($task); 152 | if ($task->exception) { 153 | $event->success = false; 154 | $event->exceptions = [$task->exception]; 155 | } 156 | $this->trigger(SchedulerEvent::EVENT_AFTER_RUN, $event); 157 | } 158 | 159 | /** 160 | * @param Task $task 161 | */ 162 | private function runTask(Task $task) 163 | { 164 | echo sprintf("\tRunning %s...", $task->getName()); 165 | if ($task->shouldRun($this->force)) { 166 | $runner = new TaskRunner(); 167 | $runner->setTask($task); 168 | $runner->setLog(new SchedulerLog()); 169 | $runner->runTask($this->force); 170 | echo $runner->error ? 'error' : 'done'.PHP_EOL; 171 | } else { 172 | echo "Task is not due, use --force to run anyway".PHP_EOL; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/events/SchedulerEvent.php: -------------------------------------------------------------------------------- 1 | createTable('scheduler_log', [ 11 | 'id'=> Schema::TYPE_PK.'', 12 | 'scheduler_task_id'=> Schema::TYPE_INTEGER.'(11) NOT NULL', 13 | 'started_at'=> Schema::TYPE_TIMESTAMP.' NOT NULL DEFAULT CURRENT_TIMESTAMP', 14 | 'ended_at'=> Schema::TYPE_TIMESTAMP.' NULL DEFAULT NULL', 15 | 'output'=> Schema::TYPE_TEXT.' NOT NULL', 16 | 'error'=> Schema::TYPE_BOOLEAN.'(1) NOT NULL DEFAULT "0"', 17 | ], 'ENGINE=InnoDB'); 18 | 19 | $this->createIndex('id_UNIQUE', 'scheduler_log','id',1); 20 | $this->createIndex('fk_table1_scheduler_task_idx', 'scheduler_log','scheduler_task_id',0); 21 | 22 | $this->createTable('scheduler_task', [ 23 | 'id'=> Schema::TYPE_PK.'', 24 | 'name'=> Schema::TYPE_STRING.'(45) NOT NULL', 25 | 'schedule'=> Schema::TYPE_STRING.'(45) NOT NULL', 26 | 'description'=> Schema::TYPE_TEXT.' NOT NULL', 27 | 'status_id'=> Schema::TYPE_INTEGER.'(11) NOT NULL', 28 | 'started_at'=> Schema::TYPE_TIMESTAMP.' NULL DEFAULT NULL', 29 | 'last_run'=> Schema::TYPE_TIMESTAMP.' NULL DEFAULT NULL', 30 | 'next_run'=> Schema::TYPE_TIMESTAMP.' NULL DEFAULT NULL', 31 | 'active'=> Schema::TYPE_BOOLEAN.'(1) NOT NULL DEFAULT "0"', 32 | ], 'ENGINE=InnoDB'); 33 | 34 | $this->createIndex('id_UNIQUE', 'scheduler_task','id',1); 35 | $this->createIndex('name_UNIQUE', 'scheduler_task','name',1); 36 | $this->addForeignKey('fk_scheduler_log_scheduler_task_id', 'scheduler_log', 'scheduler_task_id', 'scheduler_task', 'id'); 37 | } 38 | 39 | public function safeDown() 40 | { 41 | $this->delete('scheduler_log'); 42 | $this->delete('scheduler_task'); 43 | 44 | $this->dropForeignKey('fk_scheduler_log_scheduler_task_id', 'scheduler_log'); 45 | $this->dropTable('scheduler_log'); 46 | $this->dropTable('scheduler_task'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/models/SchedulerLog.php: -------------------------------------------------------------------------------- 1 | formatter->asDatetime($this->started_at); 15 | } 16 | 17 | public function getDuration() 18 | { 19 | $start = new \DateTime($this->started_at); 20 | $end = new \DateTime($this->ended_at); 21 | $diff = $start->diff($end); 22 | 23 | return $diff->format('%hh %im %Ss'); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/models/SchedulerTask.php: -------------------------------------------------------------------------------- 1 | 'Inactive', 25 | self::STATUS_PENDING => 'Pending', 26 | self::STATUS_DUE => 'Due', 27 | self::STATUS_RUNNING => 'Running', 28 | self::STATUS_OVERDUE => 'Overdue', 29 | self::STATUS_ERROR => 'Error', 30 | ]; 31 | 32 | /** 33 | * Return Taskname 34 | * @return string 35 | */ 36 | public function __toString() 37 | { 38 | return Inflector::camel2words($this->name); 39 | } 40 | 41 | /** 42 | * @param $task 43 | * @return array|null|SchedulerTask|\yii\db\ActiveRecord 44 | */ 45 | public static function createTaskModel($task) 46 | { 47 | $model = SchedulerTask::find() 48 | ->where(['name' => $task->getName()]) 49 | ->one(); 50 | 51 | if (!$model) { 52 | $model = new SchedulerTask(); 53 | $model->name = $task->getName(); 54 | $model->active = $task->active; 55 | $model->next_run = $task->getNextRunDate(); 56 | $model->last_run = NULL; 57 | $model->status_id = self::STATUS_PENDING; 58 | } 59 | 60 | $model->description = $task->description; 61 | $model->schedule = $task->schedule; 62 | $model->save(false); 63 | 64 | return $model; 65 | } 66 | 67 | /** 68 | * @return string|null 69 | */ 70 | public function getStatus() 71 | { 72 | return isset(self::$_statuses[$this->status_id]) ? self::$_statuses[$this->status_id] : null; 73 | } 74 | 75 | 76 | /** 77 | * Update the status of the task based on various factors. 78 | */ 79 | public function updateStatus() 80 | { 81 | $status = $this->status_id; 82 | $isDue = in_array( 83 | $status, 84 | [ 85 | self::STATUS_PENDING, 86 | self::STATUS_DUE, 87 | self::STATUS_OVERDUE, 88 | ] 89 | ) && strtotime($this->next_run) <= time(); 90 | 91 | if ($isDue && $this->started_at == null) { 92 | $status = self::STATUS_DUE; 93 | } elseif ($this->started_at !== null) { 94 | $status = self::STATUS_RUNNING; 95 | } elseif ($this->status_id == self::STATUS_ERROR) { 96 | $status = $this->status_id; 97 | } elseif (!$isDue) { 98 | $status = self::STATUS_PENDING; 99 | } 100 | 101 | if (!$this->active) { 102 | $status = self::STATUS_INACTIVE; 103 | } 104 | 105 | $this->status_id = $status; 106 | } 107 | 108 | public function beforeSave($insert) 109 | { 110 | $this->updateStatus(); 111 | return parent::beforeSave($insert); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/models/base/SchedulerLog.php: -------------------------------------------------------------------------------- 1 | $n]); 36 | } 37 | 38 | /** 39 | * 40 | */ 41 | public function __toString() 42 | { 43 | return (string) $this->id; 44 | } 45 | 46 | /** 47 | * @inheritdoc 48 | */ 49 | public function rules() 50 | { 51 | return [ 52 | [['scheduler_task_id', 'output'], 'required'], 53 | [['scheduler_task_id', 'error'], 'integer'], 54 | [['started_at', 'ended_at'], 'safe'], 55 | [['output'], 'string'] 56 | ]; 57 | } 58 | 59 | /** 60 | * @inheritdoc 61 | */ 62 | public function attributeLabels() 63 | { 64 | return [ 65 | 'id' => Yii::t('app', 'ID'), 66 | 'scheduler_task_id' => Yii::t('app', 'Scheduler Task ID'), 67 | 'started_at' => Yii::t('app', 'Started At'), 68 | 'ended_at' => Yii::t('app', 'Ended At'), 69 | 'output' => Yii::t('app', 'Output'), 70 | 'error' => Yii::t('app', 'Error'), 71 | ]; 72 | } 73 | 74 | /** 75 | * @return \yii\db\ActiveQuery 76 | */ 77 | public function getSchedulerTask() 78 | { 79 | return $this->hasOne(\webtoolsnz\scheduler\models\SchedulerTask::className(), ['id' => 'scheduler_task_id']); 80 | } 81 | 82 | /** 83 | * Creates data provider instance with search query applied 84 | * 85 | * @param array $params 86 | * 87 | * @return ActiveDataProvider 88 | */ 89 | public function search($params = null) 90 | { 91 | $formName = $this->formName(); 92 | $params = !$params ? Yii::$app->request->get($formName, array()) : $params; 93 | $query = self::find(); 94 | 95 | $dataProvider = new ActiveDataProvider([ 96 | 'query' => $query, 97 | 'sort' => ['defaultOrder'=>['id'=>SORT_DESC]], 98 | ]); 99 | 100 | $this->load($params, $formName); 101 | 102 | $query->andFilterWhere([ 103 | 'id' => $this->id, 104 | 'scheduler_task_id' => $this->scheduler_task_id, 105 | 'error' => $this->error, 106 | ]); 107 | 108 | $query->andFilterWhere(['like', 'started_at', $this->started_at]) 109 | ->andFilterWhere(['like', 'ended_at', $this->ended_at]) 110 | ->andFilterWhere(['like', 'output', $this->output]); 111 | 112 | return $dataProvider; 113 | } 114 | } 115 | 116 | -------------------------------------------------------------------------------- /src/models/base/SchedulerTask.php: -------------------------------------------------------------------------------- 1 | $n]); 39 | } 40 | 41 | /** 42 | * 43 | */ 44 | public function __toString() 45 | { 46 | return (string) $this->id; 47 | } 48 | 49 | /** 50 | * @inheritdoc 51 | */ 52 | public function rules() 53 | { 54 | return [ 55 | [['name', 'schedule', 'description', 'status_id'], 'required'], 56 | [['description'], 'string'], 57 | [['status_id', 'active'], 'integer'], 58 | [['started_at', 'last_run', 'next_run'], 'safe'], 59 | [['name', 'schedule'], 'string', 'max' => 45], 60 | [['name'], 'unique'] 61 | ]; 62 | } 63 | 64 | /** 65 | * @inheritdoc 66 | */ 67 | public function attributeLabels() 68 | { 69 | return [ 70 | 'id' => Yii::t('app', 'ID'), 71 | 'name' => Yii::t('app', 'Name'), 72 | 'schedule' => Yii::t('app', 'Schedule'), 73 | 'description' => Yii::t('app', 'Description'), 74 | 'status_id' => Yii::t('app', 'Status ID'), 75 | 'started_at' => Yii::t('app', 'Started At'), 76 | 'last_run' => Yii::t('app', 'Last Run'), 77 | 'next_run' => Yii::t('app', 'Next Run'), 78 | 'active' => Yii::t('app', 'Active'), 79 | ]; 80 | } 81 | 82 | /** 83 | * @return \yii\db\ActiveQuery 84 | */ 85 | public function getSchedulerLogs() 86 | { 87 | return $this->hasMany(\webtoolsnz\scheduler\models\SchedulerLog::className(), ['scheduled_task_id' => 'id']); 88 | } 89 | 90 | /** 91 | * Creates data provider instance with search query applied 92 | * 93 | * @param array $params 94 | * 95 | * @return ActiveDataProvider 96 | */ 97 | public function search($params = null) 98 | { 99 | $formName = $this->formName(); 100 | $params = !$params ? Yii::$app->request->get($formName, array()) : $params; 101 | $query = self::find(); 102 | 103 | $dataProvider = new ActiveDataProvider([ 104 | 'query' => $query, 105 | 'sort' => ['defaultOrder'=>['id'=>SORT_DESC]], 106 | ]); 107 | 108 | $this->load($params, $formName); 109 | 110 | $query->andFilterWhere([ 111 | 'id' => $this->id, 112 | 'status_id' => $this->status_id, 113 | 'active' => $this->active, 114 | ]); 115 | 116 | $query->andFilterWhere(['like', 'name', $this->name]) 117 | ->andFilterWhere(['like', 'schedule', $this->schedule]) 118 | ->andFilterWhere(['like', 'description', $this->description]) 119 | ->andFilterWhere(['like', 'started_at', $this->started_at]) 120 | ->andFilterWhere(['like', 'last_run', $this->last_run]) 121 | ->andFilterWhere(['like', 'next_run', $this->next_run]); 122 | 123 | return $dataProvider; 124 | } 125 | } 126 | 127 | -------------------------------------------------------------------------------- /src/views/index.php: -------------------------------------------------------------------------------- 1 | title = \webtoolsnz\scheduler\models\SchedulerTask::label(2); 15 | $this->params['breadcrumbs'][] = $this->title; 16 | ?> 17 | 18 |