コマンドラインとタスクの活用 (パート 1)
Geoffrey Bachelet 著
以前の pake ベースのタスクシステムに代わり、モダンで強力で柔軟なコマンドラインシステムが symfony 1.1 で導入されました。 このタスクシステムはバージョンアップと改良が行われ、現在に至っています。
多くの Web 開発者はタスクを使うことの価値や、コマンドラインの便利さにも気づいていないかもしれません。この章では、タスクの高度な使用方法から始めて、日々の作業の効率化やタスクの最大限の活用方法といったタスクの詳細に触れます。
タスクの概要
タスクとは、プロジェクトのルートでコマンドラインから symfony
PHP スクリプトを使って実行するコード片です。シェルで次のように実行して、よく知られた cache:clear
タスク(または cc
)を実行したことがあるはずです:
$ php symfony cc
symfony では、さまざま場面で使える汎用的な組み込みタスクが提供されています。symfony
スクリプトを引数やオプションをつけずに実行すると、利用可能なタスクの一覧が表示されます:
$ php symfony
次のような一覧が表示されます(内容は省略してあります):
Usage:
symfony [options] task_name [arguments]
Options:
--help -H Display this help message.
--quiet -q Do not log messages to standard output.
--trace -t Turn on invoke/execute tracing, enable full backtrace.
--version -V Display the program version.
--color Forces ANSI color output.
--xml To output help as XML
Available tasks:
:help Displays help for a task (h)
:list Lists tasks
app
:routes Displays current routes for an application
cache
:clear Clears the cache (cc, clear-cache)
すでにお気づきの方もいらっしゃるかと思いますが、タスクはグループ化されています。タスクのグループを名前空間と呼び、タスクの名前は一般的には名前空間とタスク名で構成されます。 (help
タスクと list
タスクは例外で、名前空間がありません) この命名規則によりタスクを簡単に分類でき、独自に作ったタスクに意味のある名前空間を使うことができます。
独自のタスクを記述する
symfony でタスクの開発を始めるのはとても簡単です。タスクを作り、名前をつけ、何らかのロジックを記述するだけで、初めてのカスタムタスクを実行できます。例として、とても単純な Hello, World! タスクを lib/task/sayHelloTask.class.php
に作ります:
class sayHelloTask extends sfBaseTask
{
public function configure()
{
$this->namespace = 'say';
$this->name = 'hello';
}
public function execute($arguments = array(), $options = array())
{
echo 'Hello, World!';
}
}
このタスクを、次のコマンドで実行します:
$ php symfony say:hello
このタスクでは Hello, World! と表示されるだけですが、最初の例としては十分です。実際のタスクでは、echo
や print
文を使って直接内容を表示する必要はありません。sfBaseTask
を継承すると、たとえば内容の表示を行う log()
メソッドのような、便利ないくつかのメソッドを使えます:
public function execute($arguments = array(), $options = array())
{
$this->log('Hello, World!');
}
1 度のタスクの実行で複数行の内容を表示したい場合、logSection()
メソッドを使うと便利です:
public function execute($arguments = array(), $options = array())
{
$this->logSection('say', 'Hello, World!');
}
execute()
メソッドに 2 つの引数 $arguments
と $options
が渡されています。これらの引数には、実行時に指定されたすべての引数とオプションが格納されています。引数とオプションについては、後の節で詳細に説明します。ここでは単純に、タスクで挨拶する相手を指定できるようにしてみましょう:
public function configure()
{
$this->addArgument('who', sfCommandArgument::OPTIONAL, 'Who to say hello to?', 'World');
}
public function execute($arguments = array(), $options = array())
{
$this->logSection('say', 'Hello, '.$arguments['who'].'!');
}
次のようにコマンドを実行します:
$ php symfony say:hello Geoffrey
すると、次のように表示されます:
>> say Hello, Geoffrey!
簡単ですね!
ところで、たとえばタスクの処理内容といった簡単なメタデータをタスクに記述したい場合があります。このような場合、briefDescription
プロパティや description
プロパティを設定します:
public function configure()
{
$this->namespace = 'say';
$this->name = 'hello';
$this->briefDescription = 'Simple hello world';
$this->detailedDescription = <<<EOF
The [say:hello|INFO] task is an implementation of the classical
Hello World example using symfony's task system.
[./symfony say:hello|INFO]
Use this task to greet yourself, or somebody else using
the [--who|COMMENT] argument.
EOF;
$this->addArgument('who', sfCommandArgument::OPTIONAL, 'Who to say hello to?', 'World');
}
この例のように、基本的なマークアップを使って説明文を装飾できます。symfony のタスクヘルプシステムを使って表示を確認してください:
$ php symfony help say:hello
オプションシステム
symfony のタスクでは、オプションは 2 つの別々の集合、オプションと引数にまとめられます。
オプション
オプションはハイフン付きで指定します。オプションは、コマンドラインに任意の順序で指定できます。オプションの値の指定は任意ですが、値を指定し ない場合はブール値として動作します。オプションには、短い形式と長い形式の両方が用意されていることがよくあります。長い形式は通常 2 つのハイフンを使って指定され、短い形式は 1 つのハイフンで指定されます。
よく使われるオプションの例として、ヘルプ用のスイッチ(--help
または-h
)、冗長表示のスイッチ(--quiet
または-q
)、バージョン情報のスイッチ(--version
または-V
)があります。
オプションは sfCommandOption
クラスで定義され、sfCommandOptionSet
クラスに保存されます。
引数
引数はコマンドラインに追加する短いデータです。引数は、定義された順に指定し、引数のデータに空白がある場合は引用符で囲うか、空白をエスケープ する必要があります。引数の値の指定は任意ですが、値を指定しない場合に使われるデフォルト値を引数の定義で指定しなければなりません。
引数は sfCommandArgument
クラスで定義され、sfCommandArgumentSet
クラスに保存されます。
デフォルトセット
symfony のすべてのタスクには、デフォルトで次のオプションと引数があります:
--help
(-H
): このヘルプメッセージを表示する
--quiet
(-q
): 標準出力にメッセージを出力しない
--trace
(-t
): トレースの呼び出し/実行をオンにし、フルバックトレースを有効にする
--version
(-V
): プログラムのバージョンを表示する
--color
: ANSI カラー出力を強制する
特別なオプション
symfony のタスクシステムでは、2 つの特別なオプション application
と env
を使えます。
application
オプションは、sfProjectConfiguration
ではなく sfApplicationConfiguration
のインスタンスにアクセスしたい場合に指定します。これは、たとえばルーティングから URL を生成する場合、ルーティングは特定のアプリケーションに関連付けられているために必要になります。
application
オプションがタスクに指定されると symfony によって自動的に検出され、デフォルトの sfProjectConfiguration
オブジェクトの代わりに指定されたアプリケーションに対応する sfApplicationConfiguration
オブジェクトが作られます。このオプションのデフォルト値を設定することも可能で、デフォルト値を設定すればタスクを実行するたびにオプションでアプリケーションを指定する必要はなくなります。
env
オプションで、タスクを実行する環境を制御できます。環境を指定しない場合は、デフォルトで test
環境が使われます。application
と同様に env
オプションのデフォルト値を設定すると、タスクの実行時に自動的に適用されます。
application
と env
はデフォルトオプションセットに含まれていないので、次のようにタスクに手作業で追加する必要があります:
public function configure()
{
$this->addOptions(array(
new sfCommandOption('application', null, sfCommandOption::PARAMETER_REQUIRED, 'The application name', 'frontend'),
new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environment', 'dev'),
));
}
この例では、frontend
アプリケーションが自動的に使われ、別の環境が指定されなければ dev
環境でタスクが実行されます。
データベースへのアクセス
symfony のタスクからデータベースにアクセスするには、sfDatabaseManager
をインスタンス化します:
public function execute($arguments = array(), $options = array())
{
$databaseManager = new sfDatabaseManager($this->configuration);
}
ORM のコネクションオブジェクトにも直接アクセスできます:
public function execute($arguments = array(), $options = array())
{
$databaseManager = new sfDatabaseManager($this->configuration);
$connection = $databaseManager->getDatabase()->getConnection();
}
databases.yml
に複数のコネクションが定義されている場合はどうするのでしょうか?この場合は、たとえば次のように connection
オプションをタスクに追加します:
public function configure()
{
$this->addOption('connection', sfCommandOption::PARAMETER_REQUIRED, 'The connection name', 'doctrine');
}
public function execute($arguments = array(), $options = array())
{
$databaseManager = new sfDatabaseManager($this->configuration);
$connection = $databaseManager->getDatabase(isset($options['connection']) ? $options['connection'] : null)->getConnection();
}
通常は、このオプションにデフォルト値を設定しておくと便利でしょう。
これだけで、通常の symfonyアプリケーションと同様にモデルの操作などを行えます!
ORM オブジェクトを使ってバッチ処理を行う場合は注意が必要です。Propel と Doctrine のいずれの場合でも、よく知られた PHP の循環参照とガベージコレクターのバグによりメモリーリークが発生する問題の影響を受けます。この問題は PHP 5.3 で部分的に解消されています。
メールの送信
タスクのよくある使用法の1つに、メールの送信があります。symfony 1.3 まではメールの送信は単純な作業ではありませんでした。symfony 1.3 からは高機能な PHP メーラーライブラリ Swift Mailer が完全に統合されたので、これを使ってみましょう!
symfony のタスクシステムは sfCommandApplicationTask::getMailer()
メソッドでメーラーオブジェクトを公開しています。これを使って簡単にメーラーオブジェクトにアクセスしてメールを送信できます:
public function execute($arguments = array(), $options = array())
{
$mailer = $this->getMailer();
$mailer->composeAndSend($from, $recipient, $subject, $messageBody);
}
メーラーのコンフィギュレーションはアプリケーションコンフィギュレーションから読み込まれるので、メーラーオブジェクトを使うにはタスクで application オプションを受け取る必要があります。
メーラーのスプールを有効にしている場合は、project:send-emails
タスクを実行するまでメールは送信されません。
多くの場合、送りたいメッセージの内容がすでに魔法の変数 $messageBody
に設定されて送信を待っていることはないので、何らかの方法で生成します。symfony でメールの内容を生成する推奨の方法はありませんが、以降でいくつかの TIPS を紹介します。
内容の生成を委譲する
例として、送信するメールの内容を返す protected なメソッドをタスクに作ります:
public function execute($arguments = array(), $options = array())
{
$this->getMailer()->composeAndsend($from, $recipient, $subject, $this->getMessageBody());
}
protected function getMessageBody()
{
return 'Hello, World';
}
Swift Mailer の Decorator プラグインを使う
Swift Mailer にはシンプルで効果的なテンプレートエンジンである Decorator
プラグインがあり、宛先ごとに置き換え値の組み合わせを使えるシンプルなテンプレートエンジンを使って、送信するすべてのメールに置き換えを適用できます。
詳細は Swift Mailerのドキュメント を参照してください。
外部のテンプレートライブラリーを使う
サードパーティのテンプレートライブラリーを使うのも簡単です。たとえば、Symfony Components Project の 1 つとしてリリースされた新しいテンプレートコンポーネントを使えます。コンポーネントのコードをプロジェクトのどこか(lib/vendor/templating/
がよいでしょう)に配置し、次のコードをタスクに追加します:
protected function getMessageBody($template, $vars = array())
{
$engine = $this->getTemplateEngine();
return $engine->render($template, $vars);
}
protected function getTemplateEngine()
{
if (is_null($this->templateEngine))
{
$loader = new sfTemplateLoaderFilesystem(sfConfig::get('sf_app_dir').'/templates/emails/%s.php');
$this->templateEngine = new sfTemplateEngine($loader);
}
return $this->templateEngine;
}
2 つのテンプレートシステムのよい点を使う
もう少しメーラーについて考えてみましょう。Swift Mailer の Decorator
プラグインは宛先ごとの置き換えを管理できるのでとても便利です。つまり、各宛先に対して一連の置き換えを定義すると、Swift Mailer により、トークンが、送信されるメールの宛先に応じた適切な値に置き換えられます。これをテンプレートコンポーネントと組み合わせてみます:
public function execute($arguments = array(), $options = array())
{
$message = Swift_Message::newInstance();
// ユーザーの一覧を取得する
foreach($users as $user)
{
$replacements[$user->getEmail()] = array(
'{username}' => $user->getEmail(),
'{specific_data}' => $user->getSomeUserSpecificData(),
);
$message->addTo($user->getEmail());
}
$this->registerDecorator($replacements);
$message
->setSubject('User specific data for {username}!')
->setBody($this->getMessageBody('user_specific_data'));
$this->getMailer()->send($message);
}
protected function registerDecorator($replacements)
{
$this->getMailer()->registerPlugin(new Swift_Plugins_DecoratorPlugin($replacements));
}
protected function getMessageBody($template, $vars = array())
{
$engine = $this->getTemplateEngine();
return $engine->render($template, $vars);
}
protected function getTemplateEngine($replacements = array())
{
if (is_null($this->templateEngine))
{
$loader = new sfTemplateLoaderFilesystem(sfConfig::get('sf_app_template_dir').'/emails/%s.php');
$this->templateEngine = new sfTemplateEngine($loader);
}
return $this->templateEngine;
}
apps/frontend/templates/emails/user_specific_data.php
ファイルには次のコードを記述します:
Hi {username}!
We just wanted to let you know your specific data:
{specific_data}
これだけで、完全に機能するメール本文用のテンプレートエンジンを使えるようになりました!
続く...