Created
July 26, 2022 18:28
-
-
Save Baha2Odeh/250922e09b2173c66e0883c5048e21b8 to your computer and use it in GitHub Desktop.
YII2 CronController
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// common/config/bootstrap.php | |
.... | |
.... | |
.... | |
Yii::setAlias('@runnerScript', dirname(__DIR__) .'/../yii'); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// console/controllers/CronController.php | |
namespace console\controllers; | |
use Yii; | |
use yii\console\Controller; | |
use yii\console\Exception; | |
use yii\helpers\FileHelper; | |
class CronController extends Controller { | |
const CATEGORY_LOGS = 'cron_controller_logs'; | |
/** | |
* @var string PHPDoc tag prefix for using by PHPDocCrontab extension. | |
*/ | |
public $tagPrefix = 'cron'; | |
/** | |
* @var string PHP interpriter path (if empty, path will be checked automaticly) | |
*/ | |
public $interpreterPath = null; | |
/** | |
* @var string path to writing logs | |
*/ | |
public $logsDir = null; | |
/** | |
* Update or rewrite log file | |
* False - rewrite True - update(add to end logs) | |
* @var bool | |
*/ | |
public $updateLogFile = true; | |
/** | |
* Placeholders: | |
* %L - logsDir path | |
* %C - name of command | |
* %A - name of action | |
* %P - pid of runner-script (current) | |
* %D(string formatted as arg of date() function) - formatted date | |
* @var string mask log file name | |
*/ | |
public $logFileName = '%L/%C.%A.log'; | |
/** | |
* @var string Bootstrap script path (if empty, current command runner will be used) | |
*/ | |
public $bootstrapScript = null; | |
/** | |
* @var string Timestamp used as current datetime | |
* @see http://php.net/manual/en/function.strtotime.php | |
*/ | |
public $timestamp = 'now'; | |
/** | |
* @var string the name of the default action. Defaults to 'run'. | |
*/ | |
public $defaultAction = 'run'; | |
/** | |
* Initialize empty config parameters. | |
*/ | |
public function init() { | |
parent::init(); | |
//Checking PHP interpriter path | |
if ($this->interpreterPath === null){ | |
if ($this->isWindowsOS()){ | |
//Windows OS | |
$this->interpreterPath = 'php.exe'; | |
} | |
else{ | |
//nix based OS | |
$this->interpreterPath = '/usr/bin/env php'; | |
} | |
} | |
//Checking logs directory | |
if ($this->logsDir === null){ | |
$this->logsDir = Yii::$app->getRuntimePath(); | |
} | |
//Checking bootstrap script | |
if ($this->bootstrapScript === null){ | |
$this->bootstrapScript = Yii::getAlias('@runnerScript'); | |
} | |
} | |
/** | |
* Provides the command description. | |
* @return string the command description. | |
*/ | |
public function getHelp() { | |
$commandUsage = Yii::getAlias('@runnerScript').' '.$this->id; | |
return <<<RAW | |
Usage: {$commandUsage} <action> | |
Actions: | |
view <tags> - Show active tasks, specified by tags. | |
run <options> <tags> - Run suitable tasks, specified by tags (default action). | |
help - Show this help. | |
Tags: | |
[tag1] [tag2] [...] [tagN] - List of tags | |
Options: | |
[--tagPrefix=value] | |
[--interpreterPath=value] | |
[--logsDir=value] | |
[--logFileName=value] | |
[--bootstrapScript=value] | |
[--timestamp=value] | |
RAW; | |
} | |
/** | |
* Transform string datetime expressions to array sets | |
* | |
* @param array $parameters | |
* @return array | |
*/ | |
protected function transformDatePieces(array $parameters){ | |
$dimensions = array( | |
array(0,59), //Minutes | |
array(0,23), //Hours | |
array(1,31), //Days | |
array(1,12), //Months | |
array(0,6), //Weekdays | |
); | |
foreach ($parameters AS $n => &$repeat) { | |
list($repeat, $every) = explode('\\', $repeat, 2) + array(false, 1); | |
if ($repeat === '*') $repeat = range($dimensions[$n][0], $dimensions[$n][1]); | |
else if(strpos($repeat,'/') === 1) { | |
$repeat = explode("/",$repeat); | |
$repeat = range($dimensions[$n][0], $dimensions[$n][1],$repeat[1]); | |
} | |
else { | |
$repeatPiece = array(); | |
foreach (explode(',', $repeat) as $piece) { | |
$piece = explode('-', $piece, 2); | |
if (count($piece) === 2) $repeatPiece = array_merge($repeatPiece, range($piece[0], $piece[1])); | |
else $repeatPiece[] = $piece[0]; | |
} | |
$repeat = $repeatPiece; | |
} | |
if ($every > 1) foreach ($repeat AS $key => $piece){ | |
if ($piece%$every !== 0) unset($repeat[$key]); | |
} | |
} | |
return $parameters; | |
} | |
/** | |
* Parsing and filtering PHPDoc comments. | |
* | |
* @param string $comment Raw PHPDoc comment | |
* @return array List of valid tags | |
*/ | |
protected function parseDocComment($comment){ | |
if (empty($comment)) return array(); | |
//Forming pattern based on $this->tagPrefix | |
$pattern = '#^\s*\*\s+@('.$this->tagPrefix.'(-(\w+))?)\s*(.*?)\s*$#im'; | |
//Miss tags: | |
//cron, cron-tags, cron-args, cron-strout, cron-stderr | |
if (preg_match_all($pattern, $comment, $matches, PREG_SET_ORDER)){ | |
foreach ($matches AS $match) $return[$match[3]?$match[3]:0] = $match[4]; | |
if (isset($return[0])){ | |
$return['_raw'] = preg_split('#\s+#', $return[0], 5); | |
$return[0] = $this->transformDatePieces($return['_raw']); | |
//Getting tag list. If empty, string "default" will be used. | |
$return['tags'] = isset($return['tags'])?preg_split('#\W+#', $return['tags']):array('default'); | |
return $return; | |
} | |
} | |
} | |
/** | |
* OS-independent background command execution . | |
* | |
* @param string $command | |
* @param string $stdout path to file for writing stdout | |
* @param string $stderr path to file for writing stderr | |
*/ | |
protected function runCommandBackground($command, $stdout, $stderr){ | |
try { | |
if (!file_exists(dirname($stderr))) { | |
FileHelper::createDirectory(dirname($stderr),0777); | |
} | |
}catch (\Exception $exception){ | |
} | |
$concat = ($this->updateLogFile) ? ' >>' : ' >'; | |
$command = | |
$this->interpreterPath.' '. | |
$command. | |
$concat . escapeshellarg($stdout). | |
' 2>'.(($stdout === $stderr)?'&1':escapeshellarg($stderr)); | |
if ($this->isWindowsOS()){ | |
//Windows OS | |
pclose(popen('start /B "Yii run command" '.$command, 'r')); | |
} | |
else{ | |
//nix based OS | |
system($command.' &'); | |
} | |
} | |
/** | |
* Checking is windows family OS | |
* | |
* @return boolean return true if script running under windows OS | |
*/ | |
protected function isWindowsOS(){ | |
return strncmp(PHP_OS, 'WIN', 3) === 0; | |
} | |
/** | |
* Running actions associated with {@link PHPDocCrontab} runner and matched with timestamp. | |
* | |
* @param array $args List of run-tags to running actions (if empty, only "default" run-tag will be runned). | |
*/ | |
public function actionRun($args = array()){ | |
$tags = &$args; | |
$tags[] = 'default'; | |
//Getting timestamp will be used as current | |
$time = strtotime($this->timestamp); | |
if ($time === false) throw new Exception('Bad timestamp format'); | |
$now = explode(' ', date('i G j n w', $time)); | |
$runned = 0; | |
foreach ($this->prepareActions() as $task) { | |
if (array_intersect($tags, $task['docs']['tags'])){ | |
foreach ($now AS $key => $piece){ | |
//Checking current datetime on timestamp piece array. | |
if (!in_array($piece, $task['docs'][0][$key])) continue 2; | |
} | |
//Forming command to run | |
$command = $this->bootstrapScript.' '.$task['command'].'/'.$task['action']; | |
if (isset($task['docs']['args'])) $command .= ' '.escapeshellcmd($task['docs']['args']); | |
//Setting default stdout & stderr | |
if (isset($task['docs']['stdout'])) $stdout = $task['docs']['stdout']; | |
else $stdout = $this->logFileName; | |
$stdout = $this->formatFileName($stdout, $task); | |
if(!is_writable($this->logsDir)) { | |
$stdout = '/dev/null'; | |
} | |
$stderr = isset($task['docs']['stderr'])?$this->formatFileName($task['docs']['stderr'], $task):$stdout; | |
if(!is_writable($stderr)) { | |
$stdout = '/dev/null'; | |
} | |
$this->runCommandBackground($command, $stdout, $stderr); | |
Yii::info('Running task ['.(++$runned).']: '.$task['command'].' '.$task['action'], self::CATEGORY_LOGS); | |
} | |
} | |
if ($runned > 0){ | |
Yii::info('Runned '.$runned.' task(s) at '.date('r', $time), self::CATEGORY_LOGS); | |
} else { | |
Yii::info('No task on '.date('r', $time), self::CATEGORY_LOGS); | |
} | |
} | |
/** | |
* Show actions associated with {@link PHPDocCrontab} runner. | |
* | |
* @param $args array List of run-tags for filtering action list (if empty, show all). | |
*/ | |
public function actionView($args = array()){ | |
$tags = &$args; | |
foreach ($this->prepareActions() as $task) { | |
if (!$tags || array_intersect($tags, $task['docs']['tags'])){ | |
//Forming to using with printf function | |
$times = $task['docs']['_raw']; | |
array_unshift($times, $task['command'].'.'.$task['action']); | |
array_unshift($times, "Action %-40s on %6s %6s %6s %6s %6s %s\n"); | |
array_push($times, empty($task['docs']['tags'])?'':(' ('.implode(', ', $task['docs']['tags']).')')); | |
call_user_func_array('printf', $times); | |
} | |
} | |
} | |
protected function formatFileName($pattern, $task){ | |
$pattern = str_replace( | |
array('%L', '%C', '%A', '%P'), | |
array($this->logsDir, $task['command'], $task['action'], getmypid()), | |
$pattern | |
); | |
return preg_replace_callback('#%D\((.+)\)#U', function($str){ return date($str[1]);}, $pattern); | |
} | |
/** | |
* Help command. Show command usage. | |
*/ | |
public function actionHelp(){ | |
echo $this->getHelp(); | |
} | |
/** | |
* Getting tasklist. | |
* | |
* @return array List of command actions associated with {@link PHPDocCrontab} runner. | |
*/ | |
protected function prepareActions() | |
{ | |
$actions = array(); | |
try { | |
$methods = Yii::$app->params['cronJobs']; | |
}catch (yii\base\ErrorException $e) { | |
throw new yii\base\ErrorException('Empty param cronJobs in params. ',8); | |
} | |
if (!empty($methods)) { | |
foreach ($methods as $runCommand => $runSettings) { | |
$runCommand = explode('/', $runCommand); | |
if (count($runCommand) == 2) { | |
$actions[] = array( | |
'command' => $runCommand[0], | |
'action' => $runCommand[1], | |
'docs' => $this->parseDocComment($this->arrayToDocComment($runSettings)) | |
); | |
} | |
if (count($runCommand) == 3) { | |
$actions[] = array( | |
'command' => $runCommand[0] . '/' . $runCommand[1], | |
'action' => $runCommand[2], | |
'docs' => $this->parseDocComment($this->arrayToDocComment($runSettings)) | |
); | |
} | |
if (count($runCommand) == 4) { | |
$actions[] = array( | |
'command' => $runCommand[0] . '/' . $runCommand[1] . '/' . $runCommand[2], | |
'action' => $runCommand[3], | |
'docs' => $this->parseDocComment($this->arrayToDocComment($runSettings)) | |
); | |
} | |
if(empty($actions)) { | |
continue; | |
} | |
} | |
} | |
return $actions; | |
} | |
protected function arrayToDocComment(array $runSettings) | |
{ | |
$result = "/**\n"; | |
foreach ($runSettings as $key => $setting) { | |
$result .= '* @' . $key . ' ' . $setting . "\n"; | |
} | |
$result .= "*/\n"; | |
return $result; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
* * * * * php /var/www/app/yii cron |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// console/config/main.php | |
$params = array_merge( | |
require __DIR__ . '/../../common/config/params.php', | |
require __DIR__ . '/../../common/config/params-local.php', | |
require __DIR__ . '/params.php', | |
require __DIR__ . '/params-local.php' | |
); | |
return [ | |
'id' => 'app-console', | |
'language' => 'ar', //for sending messages and notifications to be in arabic language | |
'basePath' => dirname(__DIR__), | |
'bootstrap' => ['log'], | |
'controllerNamespace' => 'console\controllers', | |
'controllerMap' => [ | |
'cron' => [ | |
'class' => 'console\controllers\CronController', | |
'logsDir' => '/var/log/cron/' | |
], | |
], | |
... | |
... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// console/config/params.php | |
return [ | |
'cronJobs' => [ | |
'test/hello' => [ | |
'cron' => '* * * * *', | |
], | |
], | |
]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment