目次

  1. はじめに
  2. ポイント
  3. おまけ

はじめに

以前、CakePHP1.3(ひょっとしたら1.2?)のときにもCronの使い方をメモしたのですが、あちらは通常の、ブラウザから渡されたURLに基づくアクションを疑似的に実行するという、どちらかというと裏技的なやり方であり、CakePHPの本来のShellの使い方ではなかったようです。(なお、基本的なシェルアプリの作り方も書きましたので興味のある方はご覧ください。)

今回2.xを使い始めたこともあり、改めてCronの使い方について学習したのでメモします。1.3とは結構変更があったようで、ConsoleというディレクトリがApp内に追加されて自作シェルの使用がより簡単になったっぽいです。なお以下の手順のバージョンはCakePHP2.1.1です。

基本はControllerがShellに変わったような感じで、かなりスッキリ作れます。

ポイント

CakePHPにおける Cron 使用のポイントは、クラスファイルの作成の方法と、Shellアクション呼び出しの方法だと思います。以下、自分があとで見なおして便利なように作ったチェックポイントです。

  1. 自作のShellクラスを記述したPHPファイル(A)、Console/cake.phpを起動させるシェルスクリプト(B)、シェルスクリプトを定時実行させるためのCronコマンド(C)、以上のA,B,C3つの設置・設定が必要。
  2. 自作Shellクラス名は先頭にシェル名、続いて末尾に “Shell” をつける。
  3. 自作Shellクラスは AppShell を継承する。
  4. 自作Shell クラスファイルの保存場所は app/Console/Command/
  5. ファイル名はクラス名+”.php”
  6. 無名メソッド(Controller の index のようなもの)を使いたいときは main を用いる。
  7. メソッドのシグネチャはパラメータをとれない。コマンドで渡した引数は $this -> args でアクセス。
  8. シェルスクリプトから呼び出すのは app/Console/cake.php
  9. シェルから cake.php に渡すアクション引数は “Shell” をつけない。
  10. Shell / Web の判定は定数 php_sapi_name() を用いる。
  11. アクション共通の前処理は Shell::startup() をオーバーライド。後処理のコールバックは…無い。

と言うわけで、Webの場合の”Controller”とかなり共通点が多いと思います。
ただ、シェルスクリプトの設置やコマンドの打ち方など、直接CakePHPに関係ない部分もあるのでそちらの方で躓くことが多いかも知れない。

実際に例を挙げるとこんな具合

A:自作のShellクラス

/**
 * app/Console/Command/MyShell.php
 */
class MyShell extends AppShell {

    var $uses = array( 'MyModel' ); // モデルを使用する場合はここに記述すれば楽。

    function main() {} // メソッドを無名にする場合は "main" を用いる

    function myMethod() { // メソッド名を指定する場合。
        $arg0 = $this -> args[0]; // コマンドで渡した引数は $this -> args に配列でセットされている。
    }

    /** このようなパラメータの渡し方は出来ない
    function myInvalidMethod( $arg ) {
    }
    */

}

B:シェルスクリプト

  1. php へのパス
  2. cake.php へのパス
  3. シェル名(=Shell クラスの名前から末尾の”Shell”を除いたもの)
  4. メソッド名
  5. 引数(省略/複数可能。クォータで囲むのが無難。)
  6. コマンド”-app”
  7. appまでのパス

のそれぞれを順番通りにスペースで区切って保存。

パラメータを指定する場合は、クォータでくくっておいた方が無難。特に先頭が”-“ハイフンから始まるものは -app 以外であっても特別な意味を持つ可能性があるので要注意。

実行権限は755以上にする。

あと、Windows環境で作成したファイルの場合は改行コードを必ずLFとする。これ、忘れないように。

#! /bin/sh
/usr/bin/php /*********/app/Console/cake.php My myMethod myArg0 myArg1 myArg2 -app /*********/app

C: ジョブコマンド

※ B のファイルまでの(ルートから見た)パスを記述。例えば

/home/user/private/myjob.sh

な、感じ。

以上の A、B、Cを滞りなく設定すればOKなはず。

Linuxとかで普段からスクリプト書いてる人には後半の記述は当たり前のことがばかりではないかと思うのですが、私のようにそういうのが大変な人は参考にしてください。
そういえば最初、Windows しか触ったことが無かったんで”root” とユーザーの違いが分かりにくかったですわ。


おまけ

コマンドから柔軟に引数を渡す

以上の書き方はシェルスクリプトに引数を固定した場合。これをコマンドから柔軟に指定したい場合は次のようにすればOK。ただ、なんかもっとスマートな書き方があると思います(^^;)

B:シェル

#! /bin/sh
/usr/bin/php /*********/app/Console/cake.php $1 $2 $3 $4 $5 -app /*********/app

C:コマンド

/home/user/private/myjob.sh My myFunction myArg0 myArg1 myArg2

呼び出しがShellであるかの是非を知る

Model などで、現在のプロセスがShellなのかそうでないのかを判別する方法として、class ShellDispatcher で定義される定数 ‘CAKEPHP_SHELL’ の使用があります。ちなみに bootstrap.php の読み込みはこの後に実行されるので、そこでもこのフラグをつかえます。

※ 定数 CAKEPHP_SHELL は 2.4.0 で非推奨となり、CakePHP3 では削除されることになりました。代わりに php_sapi_name() を用いて下さい。コマンドライン実行時のこの結果は ‘cli’ になります。

/**
 * lib/Cake/Console/ShellDispatcher.php

ShellDispatcher::run( $argv ); #Console/cake.php
    ↓
new ShellDispatcher( $argv ); #ShellDispatcher::run
    ↓
ShellDispatcher::_initConstants(); #ShellDispatcher::__construct

 */
protected function _initConstants() {
    if (function_exists('ini_set')) {
        ini_set('html_errors', false);
        ini_set('implicit_flush', true);
        ini_set('max_execution_time', 0);
    }

    if (!defined('CAKE_CORE_INCLUDE_PATH')) {
        define('DS', DIRECTORY_SEPARATOR);
        define('CAKE_CORE_INCLUDE_PATH', dirname(dirname(dirname(__FILE__))));
        define('CAKEPHP_SHELL', true);  // ←これ!
        if (!defined('CORE_PATH')) {
            define('CORE_PATH', CAKE_CORE_INCLUDE_PATH . DS);
        }
    }
}

現時点ではこの定数に false を定義するコードは見当たりませんでしたので defined(‘CAKEPHP_SHELL’) でチェック可能ですが、一応将来の改訂も見越した無難な使い方はこんな感じでしょうか。 ※2.4.0以降は非推奨

if (defined('CAKEPHP_SHELL') && CAKEPHP_SHELL) {
    // Shell 実行中です。
}else{
    // Shell じゃないです。
}

上述したように、2.4.0 以降は CAKEPHP_SHELL 定数は使用せずに、以下のように php_sapi_name() を利用しましょう

if (php_sapi_name() === 'cli') {
    // Shell 実行中です。
} else {
    // Shell じゃないです。
}

メソッド実行後のコールバックメソッドは・・・「無い」

Controller でいうところの afterFilter のようなコールバックはありません。共通の前処理を行うには AppShell::startup() メソッドをオーバーライドすればいいのですが、後処理のそれはなく、見たところコアに触れざるを得ないようですので、実質的に共通の後処理は個別に書いていくしかない。

このあたり CakePHP はきっちり実装してる印象があるだけにちょっと残念。。。


なんとなくコンソール系が苦手な人

Cronをはじめとして、Bakeだとか、AclだとかのUnix系のお約束や「黒い画面」が嫌だな~と感じてる人は、一度ドットインストールのローカル環境の構築あたりを一通りおやりになると宜しいかも知れません。

私みたいな趣味でいじってる人の多くはLinuxってなんとなくやった方が良いんだろうけどめんどくさそうだなと敬遠してる場合も少なくないのではないかと思います。しかし、さわりだけでも知っておくと何かと便利で、いろんなブログ記事などを読む際にもそのあたりを常識という前提で書かれている記事も多いので早めに取りかかっておいて損はないです。

ドットインストール

これなんかがおすすめ→ローカル開発環境の構築

百式管理人さんの小気味良い解説で、あっという間にレッスンが終了します。お金を払っても惜しくないほどの内容なのに全部無料。


そのほかありがちなトラブル

preg_replace_callback() 関連のエラーが出て怒られる

v.2.1.4 で修正済み(参考)

Warning: preg_replace_callback(): Compilation failed: unrecognized character after (?< at offset 4 in /****/cake/lib/Cake/Console/ConsoleOutput.php on line 186
PHP Warning:  preg_replace_callback(): Compilation failed: unrecognized character after (?< at offset 4 in /****/cake/lib/Cake/Console/ConsoleOutput.php on line 186

この場合は正規表現パターンの記述を修正する。

'/<(?<tag>[a-z0-9-_]+)>(?<text>.*?)<\/(\1)>/ims' (変更前)
'/<(?P<tag>[a-z0-9-_]+)>(?P<text>.*?)<\/(\1)>/ims' (変更後)

↓こちらを参考にしてください!

ブログ三郎:CakePHP2系でConsoleプログラム実行時のエラー対応