yii2 cron job 插件使用

1.当我们的系统比较庞大,有很多脚本的时候,如果都写到crontab中会比较大,而且在程序方面不是很友好,现在介绍的是在程序中实现cron

插件的地址为:https://github.com/DenisOgr/yii2-cronjobs

安装:

composer  --prefer-dist denisogr/yii2-cronjobs "dev-master"

2配置:

在console/config/bootstrap.php中加入配置:设置别名

<?php
Yii::setAlias('@runnerScript', dirname(dirname(dirname(__FILE__))) .'/yii');

在console/config/main.php中加入配置:设置cron的controller

'controllerMap' => [
       'cron' => [
           'class' => 'denisog\cronjobs\CronController'
       ],
   ],

加完配置后的main.php文件如下:

<?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',
    'basePath' => dirname(__DIR__),
    'bootstrap' => ['log'],
    'controllerNamespace' => 'console\controllers',
    'components' => [
        //'log' => [
        //    'targets' => [
        //        [
        //            'class' => 'yii\log\FileTarget',
        //            'levels' => ['error', 'warning'],
        //        ],
        //    ],
       // ],
  
  'errorHandler' => [
//           'maxSourceLines' => 30,

        ],
    ],

'controllerMap' => [
       'cron' => [
           'class' => 'denisog\cronjobs\CronController'
       ],
   ],
    'params' => $params,
];

3.配置log,这个是为了输出cron整体的log

\common\config\main.php组件中加入log的配置:也即是配置log categories:cron_controller_logs

'log' =>[
      # 追踪级别
      # 消息跟踪级别
      # 在开发的时候,通常希望看到每个日志消息来自哪里。这个是能够被实现的,通过配置 log 组件的 yii\log\Dispatcher::traceLevel 属性, 就像下面这样:
      'traceLevel' => YII_DEBUG ? 3 : 0,
      
      # 通过 yii\log\Logger 对象,日志消息被保存在一个数组里。为了这个数组的内存消耗, 当数组积累了一定数量的日志消息,日志对象每次都将刷新被记录的消息到 log targets 中。 你可以通过配置 log 组件的 yii\log\Dispatcher::flushInterval 属性来自定义数量
      'flushInterval' => 1,
      
            'targets' => [
                'file1' =>[
          //'levels' => ['warning'],

          'categories' => ['cron_controller_logs'],
                    'class' => 'yii\log\FileTarget',
          
          # 当 yii\log\Logger 对象刷新日志消息到 log targets 的时候,它们并 不能立即获取导出的消息。相反,消息导出仅仅在一个日志目标累积了一定数量的过滤消息的时候才会发生。你可以通过配置 个别的 log targets 的 yii\log\Target::exportInterval 属性来 自定义这个数量,就像下面这样:
                    'exportInterval' => 1,
          # 输出文件
          'logFile' => '@app/runtime/logs/cron_controller_logs.log',
          
          # 你可以通过配置 yii\log\Target::prefix 的属性来自定义格式,这个属性是一个PHP可调用体返回的自定义消息前缀
          'prefix' => function ($message) {
            
            return $message;
          },
          # 除了消息前缀以外,日志目标也可以追加一些上下文信息到每组日志消息中。 默认情况下,这些全局的PHP变量的值被包含在:$_GET, $_POST, $_FILES, $_COOKIE,$_SESSION 和 $_SERVER 中。 你可以通过配置 yii\log\Target::logVars 属性适应这个行为,这个属性是你想要通过日志目标包含的全局变量名称。 举个例子,下面的日志目标配置指明了只有 $_SERVER 变量的值将被追加到日志消息中。
          # 你可以将 logVars 配置成一个空数组来完全禁止上下文信息包含。或者假如你想要实现你自己提供上下文信息的方式, 你可以重写 yii\log\Target::getContextMessage() 方法。
           'logVars' => [],
                ],
        
            ],
        ],

创建log文件,设置写权限  :

touch console/runtime/logs/cron_controller_logs.log
chmod 777 console/runtime/logs/cron_controller_logs.log

4.我们就要开始添加我们的cron任务了

在console/config/params.php中加入配置:

'cronJobs' =>[
        'test/example1' => [
            'cron'      => '* * * * *', 
      'cron-stdout'=> '/tmp/ExampleCommand.log',
      'cron-stderr'=> '/tmp/ExampleCommandError.log',			
        ],
    'test/example2' => [
            'cron'      => '10 * * * *',            
        ],

    ],

更详细的规则参看:

https://github.com/Yiivgeny/Yii-PHPDocCrontab/blob/master/examples/ExampleRuCommand.php

 

也就是加入了两个cron任务,一个是test/example1 这个任务是每分钟执行一次,这个任务的输出文件为cron-stdout对应的log文件,错误输出文件为cron-stderr对应的log文件。  另外一个任务是  test/example2

下面我们需要创建log文件:

touch  /tmp/ExampleCommand.log
touch /tmp/ExampleCommandError.log
chmod 777  /tmp/ExampleCommand.log
chmod 777  /tmp/ExampleCommandError.log

然后创建controller:

<?php
namespace console\controllers;
use Yii;
use yii\console\Controller;

class TestController extends Controller
{

   public function actionExample11(){
                echo 1;exit;

        }



}

然后执行:

./yii cron/run
tail -f /tmp/ExampleCommandError.log

可以看到错误输出

Error: Unknown command "test/example1".

因为上面我们的controller的action是actionExample11  ,两个1,去掉一个,在执行

vim /tmp/ExampleCommand.log

 

可以看到文件内容如下:

1

由于这个插件默认是覆盖写,所以需要修改一下:

170行附近的代码:

$command =
           $this->interpreterPath.' '.
           $command.
           $concat . escapeshellarg($stdout).
           ' 2>'.(($stdout === $stderr)?'&1':escapeshellarg($stderr));
       if ($this->isWindowsOS()){

改成:

$command =
           $this->interpreterPath.' '.
           $command.
           $concat . escapeshellarg($stdout).
           ' 2>>'.(($stdout === $stderr)?'&1':escapeshellarg($stderr));
       if ($this->isWindowsOS()){

30行左右的代码:

public $updateLogFile = false;

改为:

public $updateLogFile = true;

 

这就就可以覆盖写了,修改后的CronController.php文件内容如下:

<?php
/**
 * User: Denis Porplenko <denis.porplenko@gmail.com>
 * Date: 16.07.14
 * Time: 14:51
 */
namespace denisog\cronjobs;
use Yii;
use yii\console\Controller;
use yii\console\Exception;
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->getName();
        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 {
                $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){
        $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 CException('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($stdout)) {
                    $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', create_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;
    }
}

然后我们需要设置cron ,让 ./yii cron/run 可以每分钟运行。

在console/config/params.php文件配置cron

每隔2分钟运行一次

'cron'      => '*\2 * * * *',

分:每1025,26,27,28,29,3040分钟

时:每天第2(偶数)小时

天:15至21和从23至27的数

月:一月至六月,偶数月

周:不管第几周

'cron' => '10,25-30,40 *\2 15-21,23-27 1-6\2 *',