├── tests
├── _app
│ ├── views
│ │ ├── site
│ │ │ └── index.php
│ │ └── layouts
│ │ │ └── main.php
│ ├── assets
│ │ └── .gitignore
│ ├── runtime
│ │ └── .gitignore
│ ├── controllers
│ │ └── SiteController.php
│ ├── yii
│ ├── config
│ │ ├── console.php
│ │ ├── db.php
│ │ └── web.php
│ └── components
│ │ └── MailerMock.php
├── _output
│ └── .gitignore
├── unit.suite.yml
├── unit
│ ├── _bootstrap.php
│ ├── ModuleTest.php
│ ├── TaskTest.php
│ └── TaskRunnerTest.php
├── functional
│ └── _bootstrap.php
├── .gitignore
├── functional.suite.yml
├── _config
│ ├── unit.php
│ └── functional.php
├── tasks
│ ├── NumberTask.php
│ ├── ErrorTask.php
│ └── AlphabetTask.php
└── _bootstrap.php
├── .gitignore
├── codeception.yml
├── src
├── events
│ ├── SchedulerEvent.php
│ └── TaskEvent.php
├── models
│ ├── SchedulerLog.php
│ ├── base
│ │ ├── SchedulerLog.php
│ │ └── SchedulerTask.php
│ └── SchedulerTask.php
├── actions
│ ├── IndexAction.php
│ ├── ViewLogAction.php
│ └── UpdateAction.php
├── ErrorHandler.php
├── views
│ ├── index.php
│ ├── view-log.php
│ └── update.php
├── migrations
│ ├── 000_init.sql
│ └── m150510_090513_Scheduler.php
├── Module.php
├── TaskRunner.php
├── Task.php
└── console
│ └── SchedulerController.php
├── .travis.yml
├── .scrutinizer.yml
├── LICENSE
├── composer.json
└── README.md
/tests/_app/views/site/index.php:
--------------------------------------------------------------------------------
1 | Index
2 |
--------------------------------------------------------------------------------
/tests/_output/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/tests/_app/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/tests/_app/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | /build/
3 | /.idea/
4 | /.vagrant
5 | /Vagrantfile
6 | /vagrant
7 |
--------------------------------------------------------------------------------
/tests/unit.suite.yml:
--------------------------------------------------------------------------------
1 | class_name: UnitTester
2 | modules:
3 | enabled:
4 | - Asserts
5 |
--------------------------------------------------------------------------------
/tests/unit/_bootstrap.php:
--------------------------------------------------------------------------------
1 | render('index');
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/_app/views/layouts/main.php:
--------------------------------------------------------------------------------
1 | user->getIsGuest()) {
4 | echo \yii\helpers\Html::a('Login', ['/user/security/login']);
5 | echo \yii\helpers\Html::a('Registration', ['/user/registration/register']);
6 | } else {
7 | echo \yii\helpers\Html::a('Logout', ['/user/security/logout']);
8 | }
9 |
10 | echo $content;
11 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/events/SchedulerEvent.php:
--------------------------------------------------------------------------------
1 | run();
12 | exit($exitCode);
--------------------------------------------------------------------------------
/tests/tasks/NumberTask.php:
--------------------------------------------------------------------------------
1 | 'yii2-test--console',
5 | 'basePath' => dirname(__DIR__),
6 | 'controllerMap' => [
7 | 'migrate' => [
8 | 'class' => 'yii\console\controllers\MigrateController',
9 | 'migrationPath' => __DIR__.'/../../../../src/migrations',
10 | ],
11 | ],
12 | 'components' => [
13 | 'log' => null,
14 | 'cache' => null,
15 | 'db' => require __DIR__.'/db.php',
16 | ],
17 | ];
18 |
--------------------------------------------------------------------------------
/src/events/TaskEvent.php:
--------------------------------------------------------------------------------
1 | 'yii\db\Connection',
5 | 'dsn' => 'mysql:host=localhost;dbname=scheduler_test',
6 | 'username' => 'scheduler_test',
7 | 'password' => 'scheduler_test',
8 | 'charset' => 'utf8',
9 | ];
10 |
11 | if (getenv('TRAVIS') == true) {
12 | $db['username'] = 'root';
13 | $db['password'] = '';
14 | }
15 |
16 |
17 | if (file_exists(__DIR__ . '/db.local.php')) {
18 | $db = array_merge($db, require(__DIR__ . '/db.local.php'));
19 | }
20 |
21 | return $db;
22 |
23 |
--------------------------------------------------------------------------------
/tests/_config/functional.php:
--------------------------------------------------------------------------------
1 | [
13 | 'user' => [
14 | 'mailer' => [
15 | 'class' => 'app\components\MailerMock',
16 | ],
17 | ],
18 | ],
19 | ]
20 | );
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/_bootstrap.php:
--------------------------------------------------------------------------------
1 | mailer;
16 | $mailer->viewPath = $this->viewPath;
17 | $body = $mailer->render($view, $params);
18 | MailHelper::$mails[] = [
19 | 'body' => $body,
20 | 'to' => $to,
21 | 'subject' => $subject,
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/src/actions/ViewLogAction.php:
--------------------------------------------------------------------------------
1 | controller->render($this->view ?: $this->id, [
35 | 'model' => $model,
36 | ]);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/tests/unit/ModuleTest.php:
--------------------------------------------------------------------------------
1 | '\webtoolsnz\scheduler\Module',
17 | 'taskPath' => '@tests/tasks',
18 | 'taskNameSpace' => '\webtoolsnz\scheduler\tests\tasks'
19 | ], ['scheduler']);
20 |
21 | $tasks = $module->getTasks();
22 |
23 | $this->assertEquals(3, count($tasks));
24 |
25 | $this->assertEquals('AlphabetTask', $tasks[0]->getName());
26 | $this->assertEquals('NumberTask', $tasks[2]->getName());
27 | $this->assertEquals('ErrorTask', $tasks[1]->getName());
28 | }
29 |
30 | public function testGetTaskInvalidPath()
31 | {
32 | $this->setExpectedException('ErrorException');
33 |
34 | $module = Yii::createObject([
35 | 'class' => '\webtoolsnz\scheduler\Module',
36 | 'taskPath' => '@tests/some/random/path',
37 | ], ['scheduler']);
38 |
39 | $module->getTasks();
40 | }
41 | }
--------------------------------------------------------------------------------
/tests/_app/config/web.php:
--------------------------------------------------------------------------------
1 | 'yii2-user-test',
5 | 'basePath' => dirname(__DIR__),
6 | 'bootstrap' => ['scheduler'],
7 | 'extensions' => require(VENDOR_DIR.'/yiisoft/extensions.php'),
8 | 'aliases' => [
9 | '@vendor' => VENDOR_DIR,
10 | '@bower' => VENDOR_DIR.'/bower',
11 | '@tests' => dirname(__DIR__).'/../',
12 | '@tests/config' => '@tests/_config',
13 | ],
14 | 'modules' => [
15 | 'scheduler' => [
16 | 'class' => 'webtoolsnz\scheduler\Module',
17 | ],
18 | ],
19 | 'components' => [
20 | 'assetManager' => [
21 | 'basePath' => __DIR__.'/../assets',
22 | ],
23 | 'log' => null,
24 | 'cache' => null,
25 | 'request' => [
26 | 'enableCsrfValidation' => false,
27 | 'enableCookieValidation' => false,
28 | ],
29 | 'db' => require __DIR__.'/db.php',
30 | 'mailer' => [
31 | 'class' => 'yii\swiftmailer\Mailer',
32 | 'useFileTransport' => true,
33 | ],
34 | ],
35 | ];
36 |
37 | if (defined('YII_APP_BASE_PATH')) {
38 | $config = Codeception\Configuration::mergeConfigs(
39 | $config,
40 | require YII_APP_BASE_PATH.'/tests/codeception/config/config.php'
41 | );
42 | }
43 |
44 | return $config;
45 |
--------------------------------------------------------------------------------
/src/views/index.php:
--------------------------------------------------------------------------------
1 | title = \webtoolsnz\scheduler\models\SchedulerTask::label(2);
15 | $this->params['breadcrumbs'][] = $this->title;
16 | ?>
17 |
18 |
19 |
20 |
= $this->title ?>
21 |
22 |
23 |
24 | = GridView::widget([
25 | 'layout' => '{summary}{pager}{items}{pager}',
26 | 'dataProvider' => $dataProvider,
27 | 'pager' => [
28 | 'class' => yii\widgets\LinkPager::className(),
29 | 'firstPageLabel' => Yii::t('app', 'First'),
30 | 'lastPageLabel' => Yii::t('app', 'Last'),
31 | ],
32 | 'columns' => [
33 | [
34 | 'attribute' => 'name',
35 | 'format' => 'raw',
36 | 'value' => function ($t) {
37 | return Html::a($t->name, ['update', 'id' => $t->id]);
38 | }
39 | ],
40 |
41 | 'name',
42 | 'description',
43 | 'schedule',
44 | 'status'
45 | ],
46 | ]); ?>
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/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/migrations/000_init.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS `scheduler_log` ;
2 | DROP TABLE IF EXISTS `scheduler_task` ;
3 |
4 | -- -----------------------------------------------------
5 | -- Table `scheduler_task`
6 | -- -----------------------------------------------------
7 | CREATE TABLE IF NOT EXISTS `scheduler_task` (
8 | `id` INT(11) NOT NULL AUTO_INCREMENT,
9 | `name` VARCHAR(45) NOT NULL,
10 | `schedule` VARCHAR(45) NOT NULL,
11 | `description` TEXT NOT NULL,
12 | `status_id` INT NOT NULL,
13 | `started_at` TIMESTAMP NULL DEFAULT NULL,
14 | `last_run` TIMESTAMP NULL DEFAULT NULL,
15 | `next_run` TIMESTAMP NULL DEFAULT NULL,
16 | `active` TINYINT(1) NOT NULL DEFAULT 0,
17 | PRIMARY KEY (`id`),
18 | UNIQUE INDEX `id_UNIQUE` (`id` ASC),
19 | UNIQUE INDEX `name_UNIQUE` (`name` ASC))
20 | ENGINE = InnoDB;
21 |
22 |
23 | -- -----------------------------------------------------
24 | -- Table `scheduler_log`
25 | -- -----------------------------------------------------
26 | CREATE TABLE IF NOT EXISTS `scheduler_log` (
27 | `id` INT(11) NOT NULL AUTO_INCREMENT,
28 | `scheduled_task_id` INT(11) NOT NULL,
29 | `started_at` TIMESTAMP NOT NULL,
30 | `ended_at` TIMESTAMP NOT NULL,
31 | `output` TEXT NOT NULL,
32 | `error` TINYINT(1) NOT NULL DEFAULT 0,
33 | PRIMARY KEY (`id`),
34 | UNIQUE INDEX `id_UNIQUE` (`id` ASC),
35 | INDEX `fk_table1_scheduled_task_idx` (`scheduled_task_id` ASC),
36 | CONSTRAINT `fk_table1_scheduled_task`
37 | FOREIGN KEY (`scheduled_task_id`)
38 | REFERENCES `scheduler_task` (`id`)
39 | ON DELETE CASCADE
40 | ON UPDATE CASCADE)
41 | ENGINE = InnoDB;
42 |
--------------------------------------------------------------------------------
/src/views/view-log.php:
--------------------------------------------------------------------------------
1 | title = $model->__toString();
14 | $this->params['breadcrumbs'][] = ['label' => SchedulerTask::label(2), 'url' => ['index']];
15 | $this->params['breadcrumbs'][] = ['label' => $model->schedulerTask->__toString(), 'url' => ['update', 'id' => $model->scheduler_task_id]];
16 | $this->params['breadcrumbs'][] = $model->__toString();
17 | ?>
18 |
19 |
20 |
21 |
=$this->title ?>
22 |
23 |
24 |
25 | - Description
26 | - = Html::encode($model->schedulerTask->description) ?>
27 |
28 | - = $model->getAttributeLabel('started_at') ?>
29 | - = Yii::$app->formatter->asDatetime($model->started_at) ?>
30 |
31 | - = $model->getAttributeLabel('ended_at') ?>
32 | - = Yii::$app->formatter->asDatetime($model->ended_at) ?>
33 |
34 | - Duration
35 | - = $model->getDuration() ?>
36 |
37 | - Result
38 | -
39 | error): ?>
40 | Error
41 |
42 | Success
43 |
44 |
45 |
46 |
47 |
Output
48 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/src/migrations/m150510_090513_Scheduler.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/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/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 |
--------------------------------------------------------------------------------
/tests/unit/TaskTest.php:
--------------------------------------------------------------------------------
1 | function () {
18 | // return true;
19 | // }]);
20 | //
21 | // $task = new AlphabetTask;
22 | // $task->setModel($model);
23 | //
24 | // $start = date('Y-m-d H:i:s');
25 | // $nextRun = date('Y-m-d H:i:00', strtotime('+1 minute'));
26 | //
27 | // $task->start();
28 | // $this->assertEquals($start, $model->started_at);
29 | //
30 | // $task->stop();
31 | // $this->assertNull($model->started_at);
32 | // $this->assertEquals($start, $model->last_run);
33 | // $this->assertEquals($nextRun, $model->next_run);
34 | // }
35 | //
36 | // public function testGetName()
37 | // {
38 | // $task = new AlphabetTask();
39 | // $this->assertEquals('AlphabetTask', $task->getName());
40 | // }
41 | //
42 | // /**
43 | // * @dataProvider runDateProvider
44 | // */
45 | // public function testGetNextRunDate($expression, $currentTime, $nextRun)
46 | // {
47 | // $task = new AlphabetTask();
48 | // $task->schedule = $expression;
49 | //
50 | // $this->assertEquals($nextRun, $task->getNextRunDate($currentTime));
51 | // }
52 | //
53 | // public function runDateProvider()
54 | // {
55 | // return [
56 | // ['*/5 * * * *', new \DateTime('1987-11-15 05:25:00'), '1987-11-15 05:30:00'],
57 | // ['0 */1 * * *', new \DateTime('2015-05-03 16:45:46'), '2015-05-03 17:00:00'],
58 | // ['0 0 * * *', new \DateTime('2015-05-03 16:45:46'), '2015-05-04 00:00:00']
59 | // ];
60 | // }
61 | //
62 | // /**
63 | // * @dataProvider shouldRunProvider
64 | // */
65 | // public function testShouldRun($expected, $status_id, $active, $force)
66 | // {
67 | // $model = test::double(new SchedulerTask(), ['save' => function () {
68 | // return true;
69 | // }]);
70 | //
71 | // $model->status_id = $status_id;
72 | // $model->active = $active;
73 | //
74 | // $task = new AlphabetTask();
75 | // $task->setModel($model);
76 | //
77 | // $this->assertEquals($expected, $task->shouldRun($force));
78 | //
79 | // }
80 | //
81 | // public function shouldRunProvider()
82 | // {
83 | // return [
84 | // [false, SchedulerTask::STATUS_PENDING, 1, false],
85 | // [true, SchedulerTask::STATUS_DUE, 1, false],
86 | // [false, SchedulerTask::STATUS_RUNNING, 1, false],
87 | // [false, SchedulerTask::STATUS_DUE, 0, true],
88 | // [true, SchedulerTask::STATUS_PENDING, 1, true],
89 | // [true, SchedulerTask::STATUS_OVERDUE, 1, false],
90 | // ];
91 | // }
92 | //
93 | // public function testWriteLine()
94 | // {
95 | // $task = new AlphabetTask();
96 | //
97 | // ob_start();
98 | // $task->writeLine('test');
99 | // $output = ob_get_contents();
100 | // ob_end_clean();
101 | //
102 | // $this->assertEquals("test\n", $output);
103 | // }
104 | //
105 | // public function testGetSetModel()
106 | // {
107 | // $task = new AlphabetTask();
108 | // $model = new \stdClass();
109 | //
110 | // $task->setModel($model);
111 | // $this->assertEquals($model, $task->getModel());
112 | // }
113 |
114 |
115 | }
--------------------------------------------------------------------------------
/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/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/update.php:
--------------------------------------------------------------------------------
1 | title = $model->__toString();
18 | $this->params['breadcrumbs'][] = ['label' => SchedulerTask::label(2), 'url' => ['index']];
19 | $this->params['breadcrumbs'][] = $model->__toString();
20 | ?>
21 |
22 |
23 |
=$this->title ?>
24 |
25 | beginBlock('main'); ?>
26 | $model->formName(),
28 | 'layout' => 'horizontal',
29 | 'enableClientValidation' => false,
30 | ]); ?>
31 |
32 | = $form->field($model, 'name', ['inputOptions' => ['disabled' => 'disabled']]) ?>
33 | = $form->field($model, 'description', ['inputOptions' => ['disabled' => 'disabled']]) ?>
34 | = $form->field($model, 'schedule', ['inputOptions' => ['disabled' => 'disabled']]) ?>
35 | = $form->field($model, 'status', ['inputOptions' => ['disabled' => 'disabled']]) ?>
36 |
37 | status_id == SchedulerTask::STATUS_RUNNING): ?>
38 | = $form->field($model, 'started_at', ['inputOptions' => ['disabled' => 'disabled']]) ?>
39 |
40 |
41 | = $form->field($model, 'last_run', ['inputOptions' => ['disabled' => 'disabled']]) ?>
42 | = $form->field($model, 'next_run', ['inputOptions' => ['disabled' => 'disabled']]) ?>
43 |
44 | = $form->field($model, 'active')->widget(RadioButtonGroup::className(), [
45 | 'items' => [1 => 'Yes', 0 => 'No'],
46 | 'itemOptions' => [
47 | 'buttons' => [0 => ['activeState' => 'btn active btn-danger']]
48 | ]
49 | ]); ?>
50 |
51 | = Html::submitButton('
' . ($model->isNewRecord ? Yii::t('app', 'Create') : Yii::t('app', 'Save')), [
52 | 'id' => 'save-' . $model->formName(),
53 | 'class' => 'btn btn-primary'
54 | ]); ?>
55 |
56 |
57 | endBlock(); ?>
58 |
59 |
60 |
61 | beginBlock('logs'); ?>
62 |
63 | 'logs']); ?>
64 | = GridView::widget([
65 | 'layout' => '{summary}{pager}{items}{pager}',
66 | 'dataProvider' => $logDataProvider,
67 | 'pager' => [
68 | 'class' => yii\widgets\LinkPager::className(),
69 | 'firstPageLabel' => Yii::t('app', 'First'),
70 | 'lastPageLabel' => Yii::t('app', 'Last'),
71 | ],
72 | 'columns' => [
73 | [
74 | 'attribute' => 'started_at',
75 | 'format' => 'raw',
76 | 'value' => function ($m) {
77 | return Html::a(Yii::$app->getFormatter()->asDatetime($m->started_at), ['view-log', 'id' => $m->id]);
78 | }
79 | ],
80 | 'ended_at:datetime',
81 | [
82 | 'label' => 'Duration',
83 | 'value' => function ($m) {
84 | return $m->getDuration();
85 | }
86 | ],
87 | [
88 | 'label' => 'Result',
89 | 'format' => 'raw',
90 | 'contentOptions' => ['class' => 'text-center'],
91 | 'value' => function ($m) {
92 | return Html::tag('span', '', [
93 | 'class' => $m->error == 0 ? 'text-success glyphicon glyphicon-ok-circle' : 'text-danger glyphicon glyphicon-remove-circle'
94 | ]);
95 | }
96 | ],
97 | ],
98 | ]); ?>
99 |
100 |
101 | endBlock(); ?>
102 |
103 | = Tabs::widget([
104 | 'encodeLabels' => false,
105 | 'id' => 'customer',
106 | 'items' => [
107 | 'overview' => [
108 | 'label' => Yii::t('app', 'Overview'),
109 | 'content' => $this->blocks['main'],
110 | 'active' => true,
111 | ],
112 | 'logs' => [
113 | 'label' => 'Logs',
114 | 'content' => $this->blocks['logs'],
115 | ],
116 | ]
117 | ]);?>
118 |
119 |
--------------------------------------------------------------------------------
/tests/unit/TaskRunnerTest.php:
--------------------------------------------------------------------------------
1 | setTask($task);
23 | $this->assertEquals($task, $runner->getTask());
24 |
25 | }
26 |
27 | public function testGetSetLog()
28 | {
29 | $runner = new TaskRunner();
30 | $log = new SchedulerLog();
31 |
32 | $runner->setLog($log);
33 |
34 | $this->assertEquals($log, $runner->getLog());
35 |
36 | }
37 | /*
38 | public function testBadCodeException()
39 | {
40 | $runner = new TaskRunner();
41 | $runner->errorSetup();
42 | $e = null;
43 |
44 | try {
45 | eval('echo $foo;');
46 | $this->fail('Error not caught');
47 | } catch (\ErrorException $e) {
48 |
49 | }
50 |
51 | $this->assertEquals('Undefined variable: foo', $e->getMessage());
52 | $this->assertEquals(1, $e->getLine());
53 | $this->assertEquals(0, $e->getCode());
54 |
55 | $runner->errorTearDown();
56 | }
57 | */
58 | // public function testRunTask()
59 | // {
60 | // $task = new AlphabetTask();
61 | //
62 | // $model = test::double(new SchedulerTask(), ['save' => function () {
63 | // $this->beforeSave(false);
64 | // return true;
65 | // }]);
66 | //
67 | // $model->id = 1;
68 | //
69 | // $model->attributes = [
70 | // 'name' => $task->getName(),
71 | // 'description' => $task->description,
72 | // 'status_id' => SchedulerTask::STATUS_DUE,
73 | // 'active' => 1,
74 | // ];
75 | //
76 | // $task->setModel($model);
77 | //
78 | // $logModel = test::double(new SchedulerLog(), ['save' => function () {
79 | // return true;
80 | // }]);
81 | //
82 | // $runner = new TaskRunner();
83 | // $runner->setTask($task);
84 | // $runner->setLog($logModel);
85 | //
86 | // $this->assertEquals(SchedulerTask::STATUS_DUE, $model->status_id);
87 | //
88 | // $started_at = date('Y-m-d H:i:s');
89 | // $runner->runTask(true);
90 | // $ended_at = date('Y-m-d H:i:s');
91 | //
92 | // $this->assertEquals(SchedulerTask::STATUS_PENDING, $model->status_id);
93 | // $this->assertEquals($model->id, $logModel->scheduler_task_id);
94 | // $this->assertLessThan(2, abs(strtotime($logModel->started_at) - strtotime($started_at)));
95 | // $this->assertLessThan(2, abs(strtotime($logModel->ended_at) - strtotime($ended_at)));
96 | // $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $logModel->output);
97 | // }
98 | // public function testRunErrorTask()
99 | // {
100 | // $task = new ErrorTask();
101 | // /* @var SchedulerTask $model */
102 | // $model = SchedulerTask::find()->where(['name' => $task->getName()])->one();
103 | //
104 | // $model->attributes = [
105 | // 'name' => $task->getName(),
106 | // 'description' => $task->description,
107 | // 'status_id' => SchedulerTask::STATUS_DUE,
108 | // 'active' => 1,
109 | // ];
110 | // $model->save();
111 | //
112 | // $task->setModel($model);
113 | //
114 | // /* @var SchedulerLog $logModel */
115 | // $logModel = test::double(new SchedulerLog(), ['save' => function () {
116 | // return true;
117 | // }]);
118 | //
119 | // $runner = new TaskRunner();
120 | // $runner->setTask($task);
121 | // $runner->setLog($logModel);
122 | //
123 | // $runner->runTask(true);
124 | // $model->refresh();
125 | // $this->assertEquals(SchedulerTask::STATUS_ERROR, $model->status_id);
126 | // $this->assertEquals($model->id, $logModel->scheduler_task_id);
127 | // $this->assertEquals(1, $logModel->error);
128 | // $this->assertContains('this is an error', $logModel->output);
129 | // }
130 | // public function testRunningErroredTask()
131 | // {
132 | // $task = new ErrorTask();
133 | // /* @var SchedulerTask $model */
134 | // $model = SchedulerTask::find()->where(['name' => $task->getName()])->one();
135 | //
136 | // $model->attributes = [
137 | // 'name' => $task->getName(),
138 | // 'description' => $task->description,
139 | // 'status_id' => SchedulerTask::STATUS_ERROR,
140 | // 'active' => 1,
141 | // 'next_run' => date('Y-m-d H:i:s', strtotime('-1 week'))
142 | // ];
143 | // $model->save();
144 | //
145 | // $task->setModel($model);
146 | // $this->assertTrue($task->shouldRun());
147 | // }
148 | }
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------