Zend_Tool_Frameworkが便利 with Doctrine_Cliなど

Zend_Tool_Framework でManifestとProviderを書いてDoctrine_CliへZend_Applicationで書いたDoctrineリソースを流して自作Doctrine_Taskを実行ってのをやってます。Provider便利!

http://twitter.com/noopable/status/7699696818

こんなことをつぶやいてしまった関係で、補足しておきたいと思います。

Zend_Tool_Frameworkの便利なところ

  • コマンドライン引数をオプション形式(GetOpt)で指定できる
  • 必須オプションが漏れているときは、インタラクティブなプロンプトで入力を促してくれる
  • オプションの指定に1文字の短い形式が利用できる
  • 情報の取得元に、.zf.iniやマニフェストから設定を取得できる
  • 簡単な入力ヘルプを自動育成してくれる

といったところです。
似たような機能は他のコマンドラインツールでも実装されているのでzfコマンドが突出しているわけではありませんが、コマンドラインツールとして面倒そうなところはかなりスマートに支援してくれているので非常に助かります。

経緯など

Zend FrameworkにはコマンドラインツールとしてZend_Toolによって提供されるzfコマンドがあります。プロジェクト用のディレクトリを作成するツールとして有名なzfコマンドですが、このzfコマンドを拡張するにはZend_Tool_Framework_Provider_Abstractを継承して実装します。zfコマンドを使用可能にする方法については、マニュアル(clitool)に詳しく出ています。
基本的な使い方についてはマニュアル(writing-provider)にある通りなんですが、おいしいところについてもいずれドキュメント化されるものと思いますが。。↓

初期化、前処理と後処理 コマンド・ライン・クライアントが動く方法についてより深く研究するには、 ソースコード を見てください。

http://framework.zend.com/manual/ja/zend.tool.framework.architecture.html#zend.tool.framework.architecture.clients

今のところ、このスライドは必見ですわ
http://www.slideshare.net/ralphschindler/extending-zendtool

Doctrine_Cliを使う

最近、いろいろ話題のORMフレームワーク Doctrineですが、これ実際ちょっとしたDBアプリなら単体でもかなりのことができてしまうようです。Symfonyとの相性がよいようですが、ZFで構築したシステムに流用することもでき、ZF界隈ではみんなこれを試し済み?な雰囲気すらありますw
個人的には、スキーマSQLで書いた方がわかりやすいと考えているのですが、まぁツールで提供されるとやっぱり楽をしたくなってしまうもので、正直言って、Doctrineすごいよと。それに、 DB <-> スキーマ <-> モデルの間で相互に変更を追えて、しかもMigrationを育成してくれるのは正直うれしい。
Doctrineでコマンドラインツールを使うときは、Doctrine_Cliを実行しますが、個々のアクションはDoctrineではタスクとよばれる単位で管理しているようです。Doctrineでカスタムタスクを作成する方法は、http://www.doctrine-project.org/upgrade/1_2#Registering%20Custom%20CLI%20Tasksにあるように、非常にシンプルですし、必要に応じて拡張できます。
Doctrine_CliZend Frameworkとの連携についてはこちらを参考にしました。

特に、symfony用のdoctrine関連のリソースは豊富で非常に参考になります。

Zend Framework with Doctrineでのポイント

  • 接続
  • モデル育成

の二つが重要になると思います。

接続連携

私の方針としては、Zend Frameworkでの接続はZend_Applicationのdbリソースで行いたいので、基本的なPDO接続はdbリソースから取得するものとしました。これをDoctrineと連携したいです。実際、Zend Frameworkの便利ライブラリではZend_Db_Adapterを使うと便利な局面が多々あり、また、同じDBに対してDoctrine経由の接続とZend_Db_Adapterの接続設定を重複で書くのはスマートではないので、連携できればと思って調べてみました。
連携していくには、Zend_Db_AdapterをDoctrineに流せればよいと思いますので、Doctrine_Adapter_Interfaceを実装してZend_Db_Adapterに委譲するアダプタアダプタを作りました。

<?php
class Doctrine_Adapter_ZF
    implements Doctrine_Adapter_Interface
{
    protected $_adapter;

    public function __construct(Zend_Db_Adapter_Pdo_Abstract $adapter)
    {
        $this->_adapter = $adapter;
    }
    public function prepare($prepareString)
    {
        return $this->_adapter->prepare($prepareString);
    }
    public function query($queryString)
    {
        return $this->_adapter->query($queryString);
    }
    public function quote($input)
    {
        return $this->_adapter->quote($input);
    }
    public function exec($statement)
    {
        return $this->_adapter->exec($statement);
    }
    public function lastInsertId()
    {
        return $this->_adapter->lastInsertId();
    }
    public function beginTransaction()
    {
        return $this->_adapter->beginTransaction();
    }
    public function commit()
    {
        return $this->_adapter->commit();
    }
    public function rollBack()
    {
        return $this->_adapter->rollBack();
    }
    public function errorCode()
    {
        return $this->_adapter->getConnection()->errorCode();
    }
    public function errorInfo()
    {
        return $this->_adapter->getConnection()->errorInfo();
    }
    public function setAttribute($attribute, $value)
    {
        return $this->_adapter->getConnection()->setAttribute($attribute, $value);
    }
    public function getAttribute($attribute)
    {
        return $this->_adapter->getConnection()->getAttribute($attribute);
    }

    public function sqliteCreateFunction($function_name, $callback, $num_args = 0)
    {
        return $this->_adapter->getConnection()->sqliteCreateFunction($function_name, $callback, $num_args);
    }

    public function sqliteCreateAggregate($function_name, $step_func, $finalize_func, $num_args = 0)
    {
        return $this->_adapter->getConnection()->sqliteCreateAggregate($function_name, $step_func, $finalize_func, $num_args);
    }

    public function __call($name, $args)
    {
        $connection = $this->_adapter->getConnection();
        if (! is_object($connection)) {
            throw new Exception("invalid connection");
        }
        elseif(! method_exists($connection, $name)) {
            throw new Exception("method $name not found in " . get_class($connection));
        }
        elseif(! is_callable(array($connection, $name))) {
            throw new Exception("uncallable method $name of " . get_class($connection));
        }
        return call_user_func_array(array($connection, $name), $args);
    }

}

で、dbリソースからDoctrineのconnectionに流してやる仕事はDoctrineリソースを作成して処理します。Zend_Applicationのリソースはシステムワイドに使ったりモジュール別に使ったりできるので便利です。

<?php
//Doctrineリソース内での記述(ポイントだけ)
$bootstrap->bootstrap('db');
$dbParams = $bootstrap->getPluginResource('db')->getParams();
$dsn        = $this->makeDsn($dbParams);
$dbAdapter  = new Doctrine_Adapter_ZF($bootstrap->db);
$connection = Doctrine_Manager::connection($dbAdapter, $this->_connectionId);            
if (! empty($dsn)) {
    $connection->setOption('dsn', $dsn);
}

makeDsnでは、dbリソースにセットしたパラメータからDoctrineスタイルのdsnを育成して渡します。これが必要な理由は、PDO_SQLiteでモデルを育成するときに、Doctrineスタイルのdsnを使ってくれと怒られるためです。SQLiteじゃなければ必要がないような気もします。

モデル育成

doctrineの標準タスクでgenerate-models-(yaml|db)を使うとモデルクラスを自動育成できますが、このタスクに渡すパラメータとして、スキーマの保存パスや出力先をセットしなければなりません。Zend Frameworkではモデルはモジュール別に配置するのが最近のパターンのようなので、そのルールに従うとすると、Zend Frameworkでのディレクトリ構成や現在対象にしているモジュールのスキーマとモデルパス等の設定を流す必要があります。これは通常はZend_Applicationのリソースで読み込むのが妥当と思いますので、その意味でもZF環境を起点にしてDoctrineコマンドを使うとよさそうです。
ポイントはもう一つ、Doctrineでデフォルトで吐かれるモデル配置方法はZendのAutoLoadと微妙に異なります。タスク用のオプションでpearStyleを指定するとZFのautoloadと同じ形式になりますので、これは忘れずにセットしておきたいところです。クラスプレフィックスをモジュール名_Model_にします。ここでもモジュール別のDoctrineリソースでセットすると間違いがなくてgoodです。

Doctrine_Task

個人的にDoctrineでカスタムタスクを書いたのは、モジュール別に指定されたスキーマを用途に応じて継承コピーしてモデルを自動育成するためのツールを書くためです。たとえば、部門向けモジュールを作った時に、サッカー部門とゴルフ部門に固有の実装が必要な時などに継承をyamlで書いて継承クラスをジェネレートする際、モジュール側をいじらずにカスタムモデル用のディレクトリだけが更新されるようにしたい・・・というニーズのためでした。

私家版 Zend Framework with Doctrineの構成

  • Zend_ApplicationにDoctrineリソースを作成
    • Doctrineリソース内
      • 接続はZend_ApplicationのDbリソースで作成し、アダプタを経由してDoctrineに抽入
      • Doctrine連携用の設定をDoctrineリソース用の設定から読み込む
      • getDoctrineCLIメソッドを作成し、Doctrine_Cliインスタンスを作成して設定を抽入
  • Zend_Tool_FrameworkでDoctrineプロバイダを作成(実行用)
  • Doctrineタスクは設定に合わせてタスクを実行する。

こんな感じの構成にしました。いずれgithubでソースを公開して突っ込み待ちにしたい気持ちもありますが、リリース用に調整できていないのでいましばらくかかりそうです。
Zend_Tool関係では、マニフェストをうまく使ったり、ヘルプを充実させたりといったより奥の深い使い方ができるようですので、その辺はマニュアルの充実か他の方の解説に期待してます

zfコマンドを改造するとしたら

zfコマンド自体、非常に便利なので、現状では改造するつもりはないのですが、インタラクティブにデータを取得しながら実行していく部分で、できればプロンプトに介入したいという気持ちはあります。たとえば、現在選択中のモジュール名やコントローラー名などをプロンプトに反映できれば、インタラクティブツールとしてはかなり便利だと思います。
インタラクティブモードに期待する動作としては、

zf do someprovider
zf> select foo
zf/foo> exec hoge
zf/foo> exec fuga
zf/foo> select none
zf>

みたいな。

追記)引数の扱い

冒頭で書いておいて具体性がなかったので少し追記です。Zend_Tool_Framework_Providerでは、アクションの引数をメソッドのシグネチャから自動的に管理してくれます。

<?php
    /**
     * do()
     *
     * @param string $command shortname=c
     * @param string $module  shortname=m
     * @param string $section shortname=s
     * @param string $hostid  shortname=h
     */
    public function doAction($command, $module, $section, $hostid = null, $args = array())

たとえば、プロバイダ内のアクションをこんな感じにしておくと、

zf do providername --module=foo --section=bar -h=www.example.com

のように順序を気にせずにオプション指定でき、$commandは必須オプションだが指定されていないので
$command?
zf>
てな具合にプロンプトで入力を促してくれます。
もちろん、

zf do hoge generate-models-yaml shop vendor

という風に引数を順に与えていくことも可能です。親切設計ですね。