ZF勉強会#2フォローアップ Zend Frameworkでモデルを始める前に理解しておきたいこと

Zend Framework勉強会#2GMOペパボ株式会社様の協力もあって、盛況でしたが、どうもZend_Dbに関して誤解があるような気がしているので(私も含めて)一通り確認してみようというフォローアップ記事です。

Zend Frameworkで対応しているモデル構成は、ドメインモデル+サービスレイヤーで直接的にはデータマッパーです。
CakePHPでは標準ではActiveRecordを採用していると思いますが、ここがCakePHPsymfonyで学習してきた人が一番最初に戸惑う部分ではないかと思います。また、初学者がデータマッパーの意義をいきなり理解するのは難しいような気もします。

要は、多くの初心者が“モデルって、DBテーブルのことだよね”と考えてしまうのはよくない、と。結果的にコントローラがふくれあがり、UnitTestで影響が出てしまう、という話になっています。

      • -

CakePHPのおいしい食べ方より

http://cakephp.seesaa.net/article/99302070.html

今では、初心者に限らなくなってきているように思えます。
元記事はKeeping it Simple: ActiveRecord does not suckこちらですが、Zend Frameworkのメイン開発者の方のコメントとして、Model !== Databaseだという主張が明確に述べられています。

公式ドキュメントを確認してみる

Zend Framework公式ドキュメントのクィックスタートによれば(http://d.hatena.ne.jp/netjockey/20090702/p1 (日本語訳))

が今のところZend FrameworkとZend_Dbで作成するモデルの標準構成と理解していいと思います。zfツールもサポートしています。

誤解を恐れずに、端的な違いを書く

  • DoctrineやCakePHPで採用されているActiveRecordとZend_Db_Table_Row(ローデータゲートウエイ)の違い
ActiveRecord データアクセスオブジェクトにドメインロジックを含む
Row Data Gateway データアクセスオブジェクトにドメインロジックを含まない
ActiveRecord ドメインはデータソースを継承
Data Mapper ドメインとデータソースの分離を仲介

※マッパーから取得したドメインモデルにはデータアクセス機能はない

そこから言えること

ActiveRecordがデータソースを内包したビジネスロジックの実装なのに対して、データマッパーはドメインとデータソースの分離を目的にしているので、志向性が全く逆になります。

ドメインモデルの初期設計で最初に判断するべき選択は、アクティブレコードかデータマッパーのいずれを使用するかである。

            • -

PofEAAの10.3.2

データマッパーはアーキテクチャ的にはDomain-Driven Designとの相性がよいですし、軽量でクリーンなドメインを構成するのに役立ちます。
一方、ActiveRecordRDBMSを軸としたアプリケーションの実装を高速化するのに役立ちます。オブジェクトから即保存されるということで実装者から見た透明感が高いです。
クリーンなモデルを目指すならDDD、スピードを追求するならActiveRecordという図式はここから生まれるものと思いますが、一方で、DDDの実装スピードが必ずしも遅くないという点、ActiveRecordが複雑なドメインを扱えないわけではないという点で、思想の違いはあれ、できることに大きな違いがあるわけではないとは思います。
そのため、ActiveRecordかデータマッパーかという議論は哲学的で、実践的ではないという批判を受けることもあります。ただ、それはどちらかを選択して進みさえすれば、いずれでも成果物が得られることで、混同して使った場合には両方の良さを食いつぶすことは間違いないと思います。

AcriveRecord or Data Mapper

では、優劣の話ではないという前提で、Zend Frameworkでモデルを実装するならどうするべきでしょうか。
データマッパーとActiveRecordは目的の部分で大きくかい離しているので混合して使うのは筋が悪いとして、設計初期にどちらかを選択する必要があります。

Zend_Dbを生かすならデータマッパーが正解。

Zend_DbはActiveRecordをサポートしていないので、ActiveRecordを構成しようとして、Zend_Db_Table_Rowを拡張しようとすると、恐ろしくコード量が増えてしまいます。つまり、Zend FrameworkCakePHP的な実装をしようとするのは効率が悪いと思います。というか、Zend Frameworkを選ぶ理由はActiveRecord以外を使いたいからってのがトップにくるものと思ってるんですが、どうなんでしょう・・・
ですので、Zend_Dbを使うならデータマッパーが正解かと。

AcriveRecordならDoctrine

ActiveRecordで使えるシステムを作っていくには、ライブラリ側の充実が欠かせないと思います。Zend_DbはDoctrineで言えば、DBAL(Database Abstraction Layer)にすぎません。
http://www.doctrine-project.org/documentation/manual/1_2/en/introduction#basic-overview参照
そんな中でARを自作するのは、あまりにもコストが高いと思います。もちろん作るのは自由ですが、Zend FrameworkとDoctrineとの連携はさほど難しくはないので、ActiveRecordがしたければDoctrineの採用を検討するべきだと思います。

Zend Framework (日本)でData Mapperを採用するときの注意

日本のPHPフレームワーク界でCakePHPは不動の人気を誇っていますし、雄たるsymfonyもDoctrineの採用でActiveRecord側についています。そのためか日本語でのWeb経由の情報量ではモデル=ActiveRecordが圧倒的です。
データマッパーを使うなら(=Zend_Dbを使うなら)、開発者間の意識のズレを補うためActiveRecordと対比してData Mapperとはどういったものなのかを、確認しておく必要があるように思えます。データマッパー自体は難しい話ではなく、ZF公式ドキュメントのquick-startで十分だろうと思いますが、ActiveRecordとの違いは明確にしておくとよいと思います。

モデル、その他の選択肢

さて、Zend_DbはDALだという話で終わってしまうと、空洞にしてあればなんでもありか、という話になってしまいますが、意外とそうではなく・・・

  • Zend_Db_TableとTable Moduleパターン
Table Module テーブルに対する複数のドメインロジックを含む操作のカプセル化
Table Data Gateway テーブルに対するデータソース操作のカプセル化

Zend_Db_Tableはテーブルデータゲートウエイですが、それと連携するテーブルモジュールを書くと言う方向性は実は日本のWebアプリに最適だったりします。これについては、また後日。また、場合によっては、こういうくだらないアーキテクチャ論争をする必要はなく、すっぱりZend_Db_Table_Adapterと生クエリで十分ということも言えます。

Zend_Db_SelectでORM/クエリオブジェクト

こちらも後日。
でも混同はなし。

その他の

発表関係の資料はZend Framework勉強会#2 で使用した資料他 - noopな日々でリンクしていますが、下のスライドは、Zend Frameworkのモデルについて話す予定にしていて原稿も用意していたのですが、ネタがかぶるかなというのもあって没稿にしていたものです。PofEAA関連の話を流し読みできるように図解してあります。もしよろしければごらんください。

他の方の意見など

-- 勝手訳 --
Zend FrameworkActiveRecordではなく、代わりに、テーブルデータゲートウエイとローデータゲートウエイパターンを使い、ローデータゲートウエイのデータをモデルにマップするのにデータマッパーを使います。なぜなら、モデルがデータベーステーブルと1:1にマッピングしていないとActiveRecordは成立しないからです。

                  • -

Zend Framework does not use ActiveRecords but instead uses the Table Data Gateway and Row Data Gateway pattern, and uses a DataMapper to map the contents of the Row Data Gateway to the model, because ActiveRecord breaks down when your models don't have a 1:1 mapping to your database tables.

http://stackoverflow.com/questions/1016966/does-the-datamapper-pattern-break-mvc

という話もありますが、言い過ぎで、ActiveRecordを悪者にするほどZend Frameworkは傲慢ではないと思います。実際、ActiveRecordをサポートするべく、Doctrineとの連携などもやれるようになりつつあります。
また、Domain-Driven Designとの対比では、

-- 勝手訳 --
Domain-Driven Designを考慮すると、Zend Frameworkは、DDD/Repositoriesの代わりに、データマッパーを使っています。それでDDDと言えるか? マーチンハウラーによるリポジトリの解釈では、それはNOだが、Eric Evansによれば、DDDのRepositoryはシンプルにできると言っている。その最もシンプルなRepositoryはデータマッパーだと彼の著書の中に書いてあります。
The Zend Framework is using DataMappers instead of Repositories. Is this really DDD-ish? Well, Fowler's interpretation of a Repository might say no. However, Eric Evans states that a DDD Repository can be very simple. At its simplest, a Repository is a DataMapper (See DDD book).

と言う具合に、データマッパーはDDDのリポジトリとして使っても問題ないだろうという意見があり、私もそう思います。
また、データマッパーやローデータゲートウエイがRepositoryを構成する材料になっていることは間違いないので、高度なRepositoryを組むのにZend_Db関係をうまく利用するのに障害はありません。
ORMとしてActiveRecord的に実装するのがダメかと言うとそんなことはなく、Zend_Db_Table_RowをActiveRecord的に使ってもいいと思います。ただ、今のところ、Doctrineなどの外部ライブラリを使うにしても、Zend_Db_Table_Rowを使うにしても、ActiveRecord的な実装を行うにはいくつかのトリックと膨大な工数を必要としそうです。
まぁ、そこが楽しいんですけどね。

モデルもしくはサービスレイヤーに関する補足

http://events.php.gr.jp/events/show/91での発表Zend_Aclの探究の中で、サービスレイヤーについて少し触れました。
id:m_noriiさん、感想ありがとうございます。

サービスレイヤーについては、発表内容とは全然違うけど、今設計的に悩んでる部分があって、サービスレイヤーって実は2種類あるのかなぁと。

コントローラーよりのサービスと、モデルよりのサービス。

コントローラーよりのサービスは、複数コントローラーに共通する機能を提供するもの。今回のAuth、ACLもそうだし、他の人の発表でもあったけど、CSRF対策コードの埋め込みなんかもそうかと。

モデルよりのサービスは、複数モデルにまたがる1トランザクションを扱うもの。

PofEAAにある、購買トランザクションの例なんかまさにそれかと。

これらをいっしょくたに「サービス」として扱うのはまずい・・・というかわかりにくい、美しくないかなぁと思ったりしている。

・・・発表とは全然関係ない、自分の設計に対する思惑ですが^^;;;

http://d.hatena.ne.jp/m_norii/20100306/1267890641

システムで、サービスという言葉で語られる内容はいろいろあります。
私が発表で言及していたのはPofEAAでいうサービスレイヤーですが、id:m_noriiさんが言及されている

  • アプリケーション層でのサービス
  • モデル層でのサービス

という切り口で考えると、

アプリケーション層での共有サービス

Zend Framework的には

  • 小さな共有機能ではアクションヘルパー
  • アプリケーション全体の動作を横断する処理部分はフロントコントローラーのプラグイン
  • 設定やモジュールに依存する内容については、Zend_Applicationのプラグインリソース
  • アプリケーションの動作にオリジナルの仕様を入れるときは独自ライブラリもしくは、モジュール

といった選択肢を検討することになると思います。

モデル層でのサービス

一方モデル層でのサービスについては、PofEAAやDomain-Driven Designなど、まとまったアーキテクチャ毎に定義が違っていたりするので、用語的には整理する必要があります。*1
まず、OOPでは、ビジネスモデルをモデリングしたオブジェクトに、振舞いを持たせるわけですが、明らかにオブジェクトに絡まない振舞いというのもあります。*2

例えば購入トランザクション

Domain-Driven Designでは、購入トランザクションドメインロジックそのものではあるが、Entityの振舞いではない。違うみかたをすると、Entityに対する振舞い。そういうときに、要素として"サービス"を使います。これは、データベースには現れない、つまりデータ抽象を持たないビジネスロジック。DDDでは、エンティティに含まれるべき振舞いは積極的にエンティティに含め、そうでないものはサービスとして実装していきます。そして、オブジェクト間の動作を含めて、DDD/Aggregateに集約するという考え方をします。
同じくモデル層で、サービスレイヤーに似た、トランザクションスクリプトというアーキテクチャもPofEAAでのメジャーな方法論の一つだと思います。これは、複数データモデル間の振舞いを、ユースケースモデリングしていく方法論かと。
いずれにしても、DDDに限らず、ビジネストランザクションとしてのサービスもまたドメイン層の中身であるということは言えます。これはPofEAAのサービスレイヤーでいうサービスとは分けて考えます。
ただ、トランザクションスクリプトは、サービスレイヤー的に見えるというか、サービスレイヤーのつもりが、ビジネスロジックを搭載して、トランザクションスクリプトになってしまうということはあるようです。

トランザクションスクリプト*3かサービスレイヤーか

PofEAAでトランザクションスクリプトの例とサービスレイヤーの例がありますが、モデル層でのサービスでも、DDDのサービスやDCIでのロールと、PofEAAのサービスレイヤーとははっきり区別しておく必要があると思います。

日本ではサービスレイヤーよりもトランザクションスクリプトがよく用いられるようですが、これは現在のビジネス仕様をそのままシステム化しようとするためだという指摘もあります。また、やっつけ仕事みたいにコントローラーで複数モデルを操作するトランザクションスクリプトin コントローラーもよく見かけます。モデル=DBという考え方をしてMVCで考えるという軽微なアプリ向けかな。

本題のサービスレイヤーですが、

基本的には、サービスレイヤーにはドメインロジックを含めないというのがセオリーになります。
つまり、アプリケーション層とドメイン層の分離を仲介するだけの極めて薄い層。そのため、ビジネスロジックを含んでしまうトランザクションスクリプトよりも、対象と主体が明確になるサービスレイヤーの方がアクセス制御に向くという話なんだろうと思われます。

*1:"モデル"がそもそも多様な意味で使われる典型ですが

*2:振舞いを分離して実装し、コンパイル時にインジェクションするというのも含めて

*3:データベースのトランザクションのことではなく複数モデルにまたがる処理を請け負う層

Zend Framework勉強会#2 で使用した資料他

http://events.php.gr.jp/events/show/91
昨日はお疲れさまでした。懇親会は・・・*1
ベストコンディションで挑むことができず、来ていただいた皆様に申し訳なく思っております。体調はベストではなかったですが、自分としてはベストを尽くしたつもりです(何やそれw
説明が不足していた部分は、自分用としても文書化が必要だと思っているので、少しずつ分割してまとめを更新していきたいと思っています。
特に、サービスレイヤーでのアクセス制御については、今後も普及してもらいたいなぁと思っているのでしっかりまとめなおしたいと思っています。
デザインパターンリファクタリングしている部分はもう少し前提となる挙動をじっくり説明したかったのですが、実演できるハンズオン形式でないと更新コストとかメンテナンス性がどうして向上するのかまでは踏み込めないですね。デザインパターンのメリットをもう少し具体的に説明したかったのですが、少し残念。反省しています。

ust (id:maru_cc さん、ありがとうございました)

http://www.ustream.tv/channel/zf-users

↓こちらは昨日使った資料です。

スライド

サービスレイヤーでのアクセス制御にZend_Aclをどう使うかという話と、
簡易実装サンプルを、php用にアレンジしたvisitorパターン、Mediatorパターンでリファクタリングする例です

ネタを振ってもらっていた部分

id:sasezakiさんにネタを振ってもらっていた、Zend_ToolのプロバイダでZend_ApplicationのBootstrapを使う部分ですが、個別実装になりますが、環境設定スクリプトで、Bootstrapをレジストリに入れて、クラスを使わないシングルトン(謎)にしています。それをインクルードして使っています。個別実装的なものなので王道とは言えませんねw

http://github.com/noopable/Algie
ここで公開する予定にしていますが、公開できない情報との分離ができていないのでもう少し時間がかかるかもしれません。

Nullyさんの発表の質疑で出ていた話で、Twigベースのビューにした場合にZend_Layoutをどうするかの話

http://d.hatena.ne.jp/noopable/20091012/1255344554
↑で書いた、テンプレートエンジン自体をビューヘルパーとして使うという手がひとつと、私は、http://github.com/noopable/Flower/tree/master/library/Flower/で公開しているFlower_Viewを使っています。プラグインで、ビューのパースエンジンを切り替えられます。エンジンとしては、Flexy用の暫定エンジンを作っていますが、多分Twig用も作れるかもしれない・・・
エンジンだけを切り替えられるようにしないとパス設定やアサイン済みの変数、ヘルパーやフィルターの設定を引き継げないためです。
なんとなく、ネタを振っていただいていたような気もしましたが、意識がフラついていたので対応できませんでした。すみません。

追記:

Baysideさんのブログ記事
http://tech.buzoo.jp/2010/03/mixixzendframework.html

id:heavenshellさんのブログ
http://d.hatena.ne.jp/heavenshell/20100306/1267884366
発表当日が誕生日!おめでとうございます

@waraiotokoさん:実案件でのZend_Db_Table
http://www.slideshare.net/waraiotoko/zf-zend-db-by-aida

Nullyさんのブログ
http://blog.nully.org/archives/197

id:sasezakiさんのLT
http://d.hatena.ne.jp/sasezaki/20100306/p1

co-heyさんのLT資料
http://www.slideshare.net/guestd9856e/zend-db-table

*1:懇親会は個人的には出たかったのですが、家庭の事情で帰らざるをえませんでした(汗

PHP製DIコンテナ"yadif"をZend_Applicationと共に使う

Zend_Applicationはフレームワークの設定と主要インスタンスの管理に使いますが、今のところモデル用のリソースがありません。
PHP製のシンプルなDIコンテナ"yadif"は設定によるオブジェクトの生成と管理が可能で、モデルの管理にも適します。ここでは、yadifを利用してサービスリソースを作成し、アプリケーションからモデルにシンボル名でアクセスできるようにします。
Zend_Applicationでのリソースの作成その他については、公式マニュアルに十分な解説がありますが、こちら(Zend_Application (5) 実戦的使い方 - noopな日々)でも書きました。

Yadif - Yet Another Dependency Injection Framework を使う

GitHub - tsmckelvey/yadif: Yet Another (PHP) Dependency Injection Framework
GitHub - beberlei/yadif: Yet Another (PHP) Dependency Injection Framework
yadifは数ファイルで構成されたシンプルなDIフレームワークです。Zend_Applicationと使うときは、yadifの関連ファイルをincludepathに配置して、applicationのiniにautoloadernamespaces[] = Yadifを追加します。*1
yadifでできることを簡単にまとめると

  • オブジェクトを生成し、設定、依存関係を解決します。
    • コンストラクタインジェクション
    • セッターインジェクション
    • factoryオブジェクトを利用した生成
    • 手続きのモジュール化
  • インスタンスをプロパティとして保持

などで、パラメータをアプリケーション上でbindすることも設定ファイルであらかじめbindすることも可能です。
たとえば、Entity_Userの定義で認証後のIdentityをパラメータとして指定しておき、認証後にbindすることでアプリケーション上では生成過程を意識する必要がありません。また、適切なシンボル名(たとえばuser)で利用します。

ビルダーを使う

設定ファイルで作成すると集中管理しやすいですが、ビルダーオブジェクトを使えば、メソッドチェーンでの記述も可能です。

<?php
$builder = new Yadif_Builder();
$builder->bind("InterfaceName")
        ->to("Implementation")
        ->args("ConstructorDependencyA", "ConstructorDependencyB", ":paramA")
        ->param(":paramA", "foo")
        ->method("setFoo")->args("foo");
        ->method("setBar")->args("bar");
        ->scope("singleton");
$config = $builder->finalize();
$yadif = new Yadif_Container($config);

詳しくは、yadif/README.markdown at master · beberlei/yadif · GitHubがわかりやすいです。

コントローラーでコンテナを使う例

DIコンテナを使うと、たとえば、Itemモデルの育成に必要なパラメータやクラス名などをコントローラに書く必要がないので、コントローラーはモデルに何をしてほしいかを書くだけで済みます。モデルの詳細については、設定ファイル上に書きだせるので、コントローラーはモデル(サービス)のインターフェースにのみ依存させることでメンテナンス性が向上します。*2

<?php
interface item
{
    public function process($request);
    public function prepareView();
}
class BagController Extends Zend_Controller_Action
{
    /**
     * model container
     * 
     * @var Yadif_Container
     */
    protected $_container;
    
    /**
     * 
     * @return Yadif_Container
     */
    public function getContainer()
    {
        if (! isset($this->_container)) {
            $this->_container = $this->getInvokeArg('bootstrap')->services;
        }
        return $this->_container;
    }

    public function fooAction()
    {
        $item = $this->getContainer()->item;
        $result = $item->process($this->getRequest())->prepareView();
        $this->view->assign('item', $result);
    }
}

Yadif_Containerをserviceコンテナとしてリソースを作成する

<?php
class Foo_Application_Resource_Services extends Zend_Application_Resource_ResourceAbstract
{
    public function init()
    {
        return new Yadif_Container($this->getOptions());
    }
}

ここでは、Foo_Application_ResourceをPrefixPathsに加えている場合です。ResourceではgetOptionsで設定ファイルの情報を取得できるので、そのままコンテナに抽入して完成です。
これで先ほどのコントローラーでの実装にあるようにservicesでコンテナを参照できます。簡単ですね。

Zend_Applicationでアプリケーションセグメント

Zend_Applicationのリソースはデフォルトでグローバルなスコープと、モジュール別のスコープに分けることができます。同様にシステムをいくつかのセグメントに分割してリソースに管理するような入れ子のリソースを用意すれば、システム内のセグメント毎の設定や挙動の違いを統一された設定ファイルで管理し、モジュールやコントローラーを"安全に"再利用できます。より複雑なウインドウアプリケーションを構築していく場合には頼もしい存在となってくれるように思います。

Yadif_Containerの設定例

上記のようにリソースを書くと、Zend_Applicationの一連の設定ファイルと同じ管理方法でリソース用の設定を書けます。yadifのREADMEにあるような配列が育成されるように作成すると基本的なモデルクラス間の依存性はほとんど設定でかけますし、もし複雑な処理が必要であれば、yadifでは生成用モジュールとfactoryオブジェクトによる生成もサポートしているので、必要なことはほぼすべてできるとみても言い過ぎではないと思います。

resources.services.hoge.class = ArrayObject
resources.services.hoge.arguments.0 = :array
resources.services.hoge.params.:array.a = foo
resources.services.hoge.params.:array.b = bar
resources.services.hoge.scope = prototype

この例では$container->hogeでArrayObjectのインスタンスが取得できます。単純すぎて適切な例ではないかもしれませんね。yadifとZend Frameworkとの連携例は、READMEの中でFrontControllerのカスタマイズ例などが出ています。

リソースでコンテナを使うと何がうれしいか。

READMEでは、Zend_ApplicationにsetContainerする例と、FrontControllerの設定から実行までをサポートする部分までが書いてあります。yadifの使い方が実感できる部分です。
一方、コンテナをZend_Applicationのリソースに割り当てると、グローバルな使い方と違い、モジュールやコントローラーの枠組みを区分して抽象化できます。
アプリケーション内の概念上の別スコープでありながら同じモジュールを使いたいというシナリオではモジュール別ブートストラップ以外に、スコープの違いが表現されなければなりません。こういった問題をコントローラー側でパラメータを解釈して対応するよりも、セットされるコンテナで解決してあればコードやSQLを複雑にせずにすみます。
たとえば、メッセージのポストを扱うコントローラーを書いたら、どのモデルを使用し、対応するビューに何を使うかといった組み合わせをコンテナで解決してスコープ毎にアタッチします。

つまり、リソースを利用することで、モデルを提供するコンテナをスコープ毎に提供することが可能になります。Zend_Applicationの枠組みを大きく外れることなく、複雑な設定を必要とする個別モデル部分で利用でき、Zend Frameworkの手薄なところをうまくカバーできます。

*1:数ファイルなのでincludeするだけでも問題ありません

*2:DIコンテナの利用をつきつめていくとコントローラーもDIで済ませることもできますが、できることと適するかどうかは別途状況判断が必要になる問題

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

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

Zend_Cache_Frontend_Pageで、ほんの少しだけ無駄を省く

Zend_Cacheに限らず、キャッシュの管理では、IDの作成と、タグ付け、効率的なタイミングが必要になりますが、Zend_Cache_Frontend_Pageというまるっとページをキャッシュするページキャッシュは使えればかなり効率がよいもののひとつです。
1点だけ、GETなどをページの識別に利用する場合のID育成部分で、GETの順番が変わっただけでIDが変わってしまうので少し無駄があります。
Zend_Cacheに限りませんが、パラメータを元にCacheIDを育成するときはソートしましょう。(パラメータ順がコンテンツに影響しないならば)

Index: Page.php
===================================================================
--- Page.php	(revision 19290)
+++ Page.php	(working copy)
@@ -389,6 +389,7 @@
         }
         if ($bool1) {
             if ($bool2) {
+                ksort($var);
                 return serialize($var);
             }
             return '';