Zend_Applicationを使ったZend_Router設定

今回は、以下のことをやりたかったのです。
【http://test.com/page/3/】とういうページングURLを実現しつつ、他のルーティングは既存のまま。
【http://test.com/index/index/page/3/】の indexの部分を出したくなかったのです。

それを実現するために application.ini に追加した記述が以下です。

; ルート設定
resources.router.routes.default.type = "Zend_Controller_Router_Route_Module"

resources.router.routes.top.type = "Zend_Controller_Router_Route"
resources.router.routes.top.route = "page/:page/"
resources.router.routes.top.defaults.controller = "index"
resources.router.routes.top.defaults.action = "index"
resources.router.routes.top.defaults.page = 1

上から順番に検索していって、最後にヒットしたルートが有効になるようです。なので、defaultルートは一番上になります。

Zend_Application_Resource_Router
=> Zend_Controller_Router_Rewrite
=> Zend_Controller_Router_Route
=> Zend_Controller_Router_Route_Module

こんな順番でソースを見てたら、面白くなってきて、興味があちこちいってしまい、
動作を確認するまでに、結構時間がかかってしまいました。

Zend_Controller_Router_Rewiteの中に、各Routeクラスを設定していく構成なんですね。
今まで、基本ルールばかりを使ってたので、勉強になりました。

途中で試した Zend_Controller_Route_Regex を動作させれなかったのが気になるので、近いうちにまた実験しします。

ZendFramework1.11.1はPCREのUnicode対応が必要

ZendFramework1.10.1から、1.11.1に変えたときに、Noticeメッセージがでました。内容は、「PCREがUnicodeをサポートしてませんよ」というもの。
Niticeメッセージはでないようにしてたのにと思って調べてみたら、ZendFrameworkのZend_Locale内で、trigger_errorを使ってメッセージを出してました。なので、ちゃんとPCREをUnicode対応にしました。

参考にさせていただいたサイトは以下です。
http://junrei.dip.jp/wordpress/nagios/pnp4nagios%EF%BC%880-6-2%EF%BC%89%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB/

以下のコマンドで、PCREがUnicodeに対応してないことを確認。

# pcretest -C
PCRE version 8.02 2010-03-19
Compiled with
  UTF-8 support
  No Unicode properties support
  Newline sequence is LF
  \R matches all Unicode newlines
  Internal link size = 2
  POSIX malloc threshold = 10
  Default match limit = 10000000
  Default recursion depth limit = 10000000
  Match recursion uses stack

まずは、以下のファイルを作成。
デフォルトのリポジトリに、pcre-unicodeがないので、下記のリポジトリを追加します。私の環境だと、phpをインストールするときに、追加してました。(忘れてました。。)

# vi /etc/yum.repos.d/utterramblings.repo

[utterramblings]
name=Jason's Utter Ramblings Repo
baseurl=http://www.jasonlitka.com/media/EL$releasever/$basearch/
enabled=0
gpgcheck=1
gpgkey=http://www.jasonlitka.com/media/RPM-GPG-KEY-jlitka

そして、以下のコマンドを実行

# yum install pcre-unicode

再度、unicodeに対応しているかをコマンドで確認。

# pcretest -C
PCRE version 8.02 2010-03-19
Compiled with
  UTF-8 support
  Unicode properties support
  Newline sequence is LF
  \R matches all Unicode newlines
  Internal link size = 2
  POSIX malloc threshold = 10
  Default match limit = 10000000
  Default recursion depth limit = 10000000
  Match recursion uses stack

Unicode properties supportになったのでOK。
Noticeメッセージもでなくなりました。
ZendFrameworkを利用するときに覚えておく必要がありそうです。

ZendFrameworkの環境変数APPLICATION_ENVを設定する

ZendFrameworkでは、index.phpにもともと書いてある記述に以下の環境変数に関わるものがあります。

/ Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));

APPLICATION_ENVが設定されてなかったら、’production’ という文字列を、APPLICATION_ENVとして設定するという内容です。このAPPLICATION_ENVはどこで使われているかというと、Bootstrap内で、application.iniを読み込む際に、パラメタとして渡されています。

application.iniは、DB接続設定等を書くファイルで、APPLICATION_ENVの値を、本番環境、テスト環境等で変えることによって、ファイルを編集することなく、DB接続先の設定を環境に合ったものに設定することができます。iniファイルの書き方は、こちらをご参照ください(http://framework.zend.com/manual/1.11/ja/zend.config.adapters.ini.html)

今回、/etc/sysconfig/httpd に以下の記述を追加すると、apacheにて環境変数を追加することができました。

export APPLICATION_ENV="development"

.htaccessを使うと、”setEnv”を使って記述することもできます。その場合の書き方は以下になります。

setEnv APPLICATION_ENV development

これまで、.htaccessを使ったやり方のみでやってきて、/etc/sysconfig/httpd を編集する方法を知りませんでした。

Zend_Mailで文字化けしなくなっていた

以前、こんな記事を書きました。
https://life.co-hey.com/2009/02/zendframework_175_1/

その後、Zend_Mailをいじることはなかったんですが、最近また利用する機会があったのですが、Zend_Mailの中身を修正しなくても、文字化けしなくなっていました。

$e = Zend_Debug::dump($errors->exception, null, false);
$mail = new Zend_Mail('iso-2022-jp');
$mail->addTo('test@co-hey.com')
     ->setFrom('error@co-hey.com')
     ->setSubject(mb_convert_encoding('例外が発生しました', 'iso-2022-jp', 'UTF-8'))
     ->setBodyText(mb_convert_encoding(strip_tags(htmlspecialchars_decode($e)), 'iso-2022-jp', 'UTF-8'))
     ->send();

ErrorController.phpの中で、上記のプログラムでエラー発生箇所をメールするという用途で使っています。バージョンアップの度に、Zend_Mailを1行修正する手間がいらなくなるのはうれしいです。

動作を試したのは、1.10.1ですが、いつから文字化けが解消されていたのかはわかりません。。

Zendを使ってZip解凍

以前、Zendを使ってZip圧縮という記事を書きましたが、今回は逆のZendを使ってZip解凍についてです。

前回は、Zend_Filter_Compressというクラスを使いましたが、今回はZend_Filter_Decompressというクラスを使います。
使い方は以下です。

$filter = new Zend_Filter_Decompress('Zip');
$filter->setArchive('圧縮ファイルのパス付ファイル名')
    ->setTarget('圧縮ファイルの解凍先ディレクトリパス');
$filter->filter('hoge');   // hogeはnullじゃなければ何でもいいです。

または

$filter = new Zend_Filter_Decompress('Zip');
$filter->setTarget('圧縮ファイルの解凍先ディレクトリパス');
$filter->filter('圧縮ファイルのパス付ファイル名');

または

$filter = new Zend_Filter_Decompress(array(
            'adapter' => 'Zip', 
            'options' => array(
                            'archive' => '圧縮ファイルの解凍先ディレクトリパス', 
                            'target'   => '圧縮ファイルの解凍先ディレクトリパス'
                         )
));
$filter->filter('hoge');   // hogeはnullじゃなければ何でもいいです。

または

$filter = new Zend_Filter_Decompress(array(
            'adapter' => 'Zip', 
            'options' => array('target'   => '圧縮ファイルの解凍先ディレクトリパス')
));
$filter->filter('圧縮ファイルのパス付ファイル名');

または

$filter = new Zend_Filter_Decompress();
$filter->setAdapter('Zip')
       ->setAdapterOptions(array(
             'archive' => '圧縮ファイルの解凍先ディレクトリパス', 
             'target'   => '圧縮ファイルの解凍先ディレクトリパス'))
       ->filter('hoge');    // hogeはnullじゃなければ何でもいいです。

解凍、圧縮共に Zip の部分を Gz, Lzf, Rar, Tar, Bz2 に変更するとそれぞれの形式で圧縮解凍ができます。
今回疑問に思ったのは(何で前回思わなかったのか。。)、なんで、こんな感じでかけないのかということ。

$filter = new Zend_Filter_Decompress('Zip');
$filter->setArchive('圧縮ファイルのパス付ファイル名')
    ->setTarget('圧縮ファイルの解凍先ディレクトリパス');
       ->filter();

または

$filter = new Zend_Filter_Decompress('Zip');
$filter->setTarget('圧縮ファイルの解凍先ディレクトリパス');
       ->filter('圧縮ファイルのパス付ファイル名');

不思議に思ってプログラムを観てみると、setTargetとかsetArchiveから返ってきているオブジェクトが、Zend_Filter_Decompressではなくて、Zend_Filter_Decompress内部で持っているZip処理のアダプターZend_Filter_Compress_Zipでした。
Zend_Filter_Compress_Zipにはfilterっていうメソッドはないので、エラーがでてたんですね。

Zend_Filter_Decompressの中で、

    public function __call($method, $options)
    {
        $adapter = $this->getAdapter();
        if (!method_exists($adapter, $method)) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception("Unknown method '{$method}'");
        }

        return call_user_func_array(array($adapter, $method), $options);
    }

ってなっている部分を

    public function __call($method, $options)
    {
        $adapter = $this->getAdapter();
        if (!method_exists($adapter, $method)) {
            require_once 'Zend/Filter/Exception.php';
            throw new Zend_Filter_Exception("Unknown method '{$method}'");
        }

        call_user_func_array(array($adapter, $method), $options);
        return $this;
    }

ってしたら、やりたい書き方ができそうだけど、どんな弊害があるのかは細かく調べてないのでわかりません。ざっとみたところなさそう。アダプター使いたいなら、getAdapterで取得できるので、不具合もなさそう。なんで、アダプターを返すような作りにしたんだろう?読み取れてない意図があるのかな。

ZendでDbのMeta情報をキャッシュする

なんのこっちゃ?って感じですよね。僕もそう思います。

ZendFrameworkを利用する場合、Zend_Db_Table(Row、Rowsetも)を利用することが多いと思います。Zend_Db_Table_Abstractを継承して、テーブル名と他テーブルとのリレーションを定義するだけで、DBの登録、更新、削除ができてしまう優れものです。個人的にはこれだけ、ZendFrameworkを使ってみようってきっかけになりました。

これらがテーブル名とリレーションだけしか定義してないのに、存在しないカラム名を指定して処理しようとすると、”そんなカラムはないよ!”ってエラーを出してくれます。賢いです。これができるのって、テーブル関連の処理をするたびに”describe”が実行されて、テーブルの情報を取得しているからなのです。気がきいてます。

ただ1点、”describe”が実行される量が多すぎるんです。これが処理が遅くなる原因になることがあります。ここででてくるのがDbのMeta情報のキャッシュ。DbのMeta情報というのは、”describe”の実行結果です。なので、これをキャッシュとして保持しておくと、”describe”をしなくても、情報が手に入るため余計なSQLをDBに実行しなくてすみます。

たかが”describe”ですが、実行回数がほんとに多いので、だいぶ変わります。
Zend_Db_Table使うときは、セットで設定するくらいでいいのではと思っています。

Bootstrap.phpの中にこんな感じで書いておくと使えます。

    protected function _initDbCache()
    {
        $frontendOptions = array('automatic_serialization' => true);
        $backendOptions = array('cache_dir' => APPLICATION_PATH . 'cacheディレクトリへの相対パス');
        $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
        Pb_Db_Table_Abstract::setDefaultMetadataCache($cache);
    }

Zend_Db_Selectのcolumnsメソッドではまった

Zend_Db_Selectのメソッドにcolumnsというものがあります。
selectする対象を後付で追加できるメソッドです。使い方はこんな感じ。

※ $adapterは、Zend_Db_Adapter_Pdo_Mysql。

$adapter->select()
        ->from(array("t1" => "hoge_table"), array("id", "account"))
        ->columns(array("password", "update_date"))
        ->where("account = ?", "testest");
        ->query()->fetchAll();

今回やっていたのは、2つのSQL文をUNIONで結合するという内容でした。
取得するカラムが、”id”, “account”, “state”, “update_date”として、UNIONで結合するのですが、結合する2つのSQL文の対象となるテーブルに、”state”っていうカラムがないために、後付で”0″をつけるっていうことをやっていました。(実際は複数のテーブルを結合していて、複雑なので、簡略化してます。)

$select1 = $adapter->select()
                   ->from(array("t1" => "hoge_table"))
                   ->from(array("t2" => "hogehogehoge_table"), array("name"))
                   ->where("t1.id = t2.account_id")

$select2 = $adapter->select()
                   ->from(array("t1" => "hogehoge_table"))
                   ->from(array("t2" => "hogehogehoge_table"), array("name"))
                   ->columns(array("state" => new Zend_Db_Expr(0)));

$adapter->select()->union(array($select1, $select2))->query()->fetchAll();

テーブルのイメージはこう。

create table hoge_table (
    id int , 
    account varchar(16), 
    state varchar(8), 
    update_date timestamp, 

    primary key `id`
);

create table hogehoge_table (
    id int, 
    account varchar(4), 
    update_date timestamp, 

    primary key `id`
);

create table hogehogehoge_table (
    id int auto_increment, 
    account_id int, 
    name varchar(128),  

    primary key `id`
);

こういう場合、columnsで追加したカラムは、取得結果でも、後ろに追加した形ででてきます。

--------------------------------------------
| id | account | update_date | name | state | 
--------------------------------------------
| 1  |  test   | 2010-11-12  | hoge  | valid  |
---------------------------------------------

しかし、もともとテーブルに含まれるものについては、Zend_Db_Selectを利用しているためか、SQL文が作成される際に、並び替えられていて、stateがどの位置にくるか分かりません。(ここが問題の原因となります。)
しかも、カラム名、カラム数が一致しているのでUNIONにもエラーがでません。

columnsを利用しなかったSQL文の取得結果が、

--------------------------------------------
| id | account | update_date | state | name | 
--------------------------------------------
| 3  |  test   | 2010-11-15  | valid  | aaaa  |
---------------------------------------------

という順番で取得された場合、UNIONの結果は、

--------------------------------------------
| id | account | update_date | state | name | 
--------------------------------------------
| 3  |  test   | 2010-11-12  | valid  | aaaa  |
---------------------------------------------
| 1  |  test   | 2010-11-15  | hoge  | valid  |
---------------------------------------------

となって取得されます。

項目名と違う値が入ってくるのです。今回は、互い違いになったのが数値と日付で、それをsumして合計値を取得してたために、全く予測できない値が取得されてしまいました。

解決策は、片方でcolumnsを使って、カラム名を追加した場合は、UNIONで結合するSQLは全て、同じカラム名は、columnsを使って追加する方法をとるということです。

今回の例だと。

$select1 = $adapter->select()
                   ->from(array("t1" => "hoge_table"), 
                          array("id", "account", "update_date"))
                   ->from(array("t2" => "hogehogehoge_table"), array("name"))
                   ->columns(array("state" => "t1.state"))
                   ->where("t1.id = t2.account_id")

$select2 = $adapter->select()
                   ->from(array("t1" => "hogehoge_table"))
                   ->from(array("t2" => "hogehogehoge_table"), array("name"))
                   ->columns(array("state" => new Zend_Db_Expr(0)));

$adapter->select()->union(array($select1, $select2))->query()->fetchAll();

と書くことです。または、columnsを使わない形で、このような書き方もできると思います。

$select1 = $adapter->select()
                   ->from(array("t1" => "hoge_table"))
                   ->from(array("t2" => "hogehogehoge_table"), array("name"))
                   ->where("t1.id = t2.account_id")

$select2 = $adapter->select()
                   ->from(array("t1" => "hogehoge_table"), 
                          array("id", "account", "state" => new Zend_Db_Expr(0), "update_date")))
                   ->from(array("t2" => "hogehogehoge_table"), array("name"))
                   ->columns(array("state" => new Zend_Db_Expr(0)));

$adapter->select()->union(array($select1, $select2))->query()->fetchAll();

テーブルのカラム数が多いとcolumnsで後付した方が、記述量が少なくなるので、どちらを使うかはその時のテーブルによるかなと思います。重要なのはcolumnsの利用の有無を揃えること。

この説明、うまくやるの難しいですね。。。

ZendFrameworkを使ってBasic認証を行う

ZendFrameworkで構築したサイトの一部にbasic認証をかけたいけど、どうしたらできるか知ってる?と聞かれたのでやってみました。
考えたことなかったですが、.htaccess使うとindex.phpと同じ階層に置くことになって、サイト全体にしかかけれないのかもしれません。試してはないので推測です。

やりたかった内容は、特定のURLにbasic認証をかけて、認証NGな場合は他の画面を表示し、認証OKな場合は、そのまま指定されたURLの画面を表示するというものです。

サンプルソースは、以下になります。
今回は、controller内に全部書きました。

    public function testAction()
    {

        $config = array(
            'accept_schemes'  => 'basic',    // basic認証指定
            'realm'           => 'aaaaa',    // realm(passwd.txtの内容と一致させる必要あり)
            'digest_domains'  => '/login/',  // このURL以下には全てbasic認証要
            'nonce_timeout'   => 3600
        );

        $resolver = new Zend_Auth_Adapter_Http_Resolver_File();
        $resolver->setFile('passwd.txt');  // index.phpがある階層と同じところに置いてます。

        $adapter  = new Zend_Auth_Adapter_Http($config);
        $adapter->setBasicResolver($resolver)
                ->setRequest($this->getRequest())
                ->setResponse($this->getResponse());


        $result = $adapter->authenticate();
        if (!$result->isValid()) {
            // 認証エラーの場合
            return $this->_forward('test1');
        }

        // 認証OK
    }

passwd.txtの中身はこちら

test:aaaaa:hogehoge

左から、アカウント、realm、パスワードです。パスワードは生パスワードでOKです。
realmは、はプログラム内で指定したものと一致する必要があります。
アカウントとrealmが一対となっていて、それに対してパスワードが存在するイメージです。

basic認証の画面は、authenticate()の部分で生成され、Zend_Controller_Response_Httpに設定されています。
authenticate()の部分で実行されるということは、認証画面が表示される際は、必ず “認証エラー” のルートを通ることになります。
なので、このルートを単なるエラールートと捉えない方がいいです。例えば、Exceptionをthrowしたりしたら、必ずthrowされます。

あと、はまった点が1つありました。”認証エラー”のルートに $this->_redirect()を利用すると、期待する動きはしません。
basic認証画面の出力より、リダイレクト処理が優先されてしまい、常にリダイレクトされることになります。
そのため、上記サンプルでは $this->_forward()を利用しています。これなら、認証画面にてキャンセルを押した場合にforward先の画面が出力されます。

Zendのソースを見つつ、認証画面にてキャンセルを押された場合を検出しようとしたんですが、判別の方法がわかりませんでした。
認証画面をキャンセルした際に、サーバ側にリクエストがきてないようだったので、サーバ側で判別するのは無理と判断しました。
今回は、Responseにbasic認証の情報と、forwardの情報を両方設定し、basic認証がキャンセルされたら、forwardが実行される形になりました。

Zendを使ってZip圧縮

ZendFramework使って、ディレクトリのzipってできますか?
と聞かれたので調べたらでてきた。しかも、filterのところで。

filterは、validateが不正データを検出するのに対して、フィルタリングするってイメージで、入力チェックのイメージが強かったから意外だった。

filterを通すことで、データを変換するってイメージならファイル圧縮がfilterにあるのも納得できる。あー、なるほどねーって思わされた。

使い方は、こんな感じ。

$filter = new Zend_Filter_Compress('Zip');
$filter->setArchive(作成したい圧縮ファイルのパス付ファイル名);
$filter->filter(圧縮したいファイルのパス付ファイル名);

他にもこんな書き方もできそう。

$filter = new Zend_Filter_Compress(array(
     'adapter' => 'Zip', 
     'options' => array('archive' => '作成したい圧縮ファイルのパス付ファイル名')
));
$filter->filter(圧縮したいファイルのパス付ファイル名);

中身は、ZipArchiveクラス使ってました。

Zend_Db_SelectでUNIONとHAVINGを同時に使えない

訂正:2010/04/09

通りすがりさんにコメントを頂き、UNIONで連結した全体にhavingをかけるのはできないということで正しいということが分かりました。

たまたま、$select2の方にだけhavingでフィルターされるデータがあったため、結果的に意図通りのデータが出力されただけで、全体にhavingが掛かっているわけではありませんでした。

サブクエリを使う方法が適切です。

    // $adapterはZend_Db_Adapterのクラス。MySQLを利用。
    $select1 = $adapter->select()->from(....)->where(....);
    $select2 = $adapter->select()->from(....)->where(....);

    $select = $adapter->select()->union(array($select1, $select2));

   echo $adapter->select()->from($select)->where(....); ←havingで書いてた条件

=========

UNIONとHAVINGを同時に使うSQLを組んでいたときに、Zend_Db_Selectから生成されたSQLからHAVING句が抜け落ちていました。

プログラムは以下のようなものです。

    // $adapterはZend_Db_Adapterのクラス。MySQLを利用。
    $select1 = $adapter->select()->from(....)->where(....);
    $select2 = $adapter->select()->from(....)->where(....);

    $select = $adapter->select()->union($select1, $select2)->having(.....);
    echo $select;   ← ここで HAVING句がなくなってる

上記でできるSQLのイメージは、これです。

   select ...... from .... where ....
   union
   select ...... from .... where ....
   having .....

このSQLをDBサーバで実行すると、動くので構文はあってます。

そこで、Zend_Db_Selectクラスのhaving句を出力するところを見たら、こうなってました。(バージョンは10.2)

    /**
     * Render HAVING clause
     *
     * @param string   $sql SQL query
     * @return string
     */
    protected function _renderHaving($sql)
    {
        if ($this->_parts[self::FROM] && $this->_parts[self::HAVING]) {
            $sql .= ' ' . self::SQL_HAVING . ' ' . implode(' ', $this->_parts[self::HAVING]);
        }

        Zend_Debug::dump($sql);
        return $sql;
    }

FROM句とHAVING句が両方ある場合に、HAVINGをつけるって処理ですね。確かにこれだと、UNIONと同時に使ったときは、FROM句がないので、HAVING句がつかないのは納得です。

というわけで、以下のように修正して、UNIONとHAVINGが同時に使えることを確認しました。

    /**
     * Render HAVING clause
     *
     * @param string   $sql SQL query
     * @return string
     */
    protected function _renderHaving($sql)
    {
        // FROMかUNIONがあればOKに変更
        if (($this->_parts[self::FROM] || $this->_parts[self::UNION]) && $this->_parts[self::HAVING]) {
            $sql .= ' ' . self::SQL_HAVING . ' ' . implode(' ', $this->_parts[self::HAVING]);
        }

        Zend_Debug::dump($sql);
        return $sql;
    }

そのうちZend本体でも実装してくれることに期待します。