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本体でも実装してくれることに期待します。

filterに使えるarray_intersect_key

第一パラメタの配列の中で、第2パラメタ以降の配列全てに存在するキーのものだけ取得してくれる。

詳しい説明はこちらで。
http://php.net/manual/ja/function.array-intersect-key.php

今まで、Zend_Db_Table_RowのsetFromArrayの存在を知らなかったので、自分でこんなやりかたでfilterしてました。

$tmp = array();     
$table = new Zend_Db_Table_Abstractを継承したクラス();
$cols = $table->info('cols');
foreach ($params as $key => $value) {
    if (in_array($key, $cols)) { $columns[$key] = $value; }
}

これが、

$table = new Zend_Db_Table_Abstractを継承したクラス();
$cols = $table->info('cols');
$tmp = array_intersect_key($params, array_flip($cols));

と書けます。これ便利ですね。

ZendFrameworkのソースに書いてありました。
なんで今まで見つけれなかったんだろう。必要としてなかったからなのか。
もっと楽な方法がないのかって思えてなかったことに反省。

phpのautoloadメソッドはスタック

Zend_Loader_Autoloaderってどういう手順で、__autoloadを設定しているのかが気になってみてみたら、spl_autoload_registerを使っていました。apl_autoload_registerの説明を見てみたら、以下の文が。

指定した関数を、spl が提供する __autoload スタックに登録します。
スタックがまだアクティブになっていない場合は、まずアクティブにします。

もしあなたのコード中に __autoload 関数が存在するのなら、 それを明示的に __autoload スタックに登録しなければなりません。 なぜなら、spl_autoload_register() は、 spl_autoload() あるいは spl_autoload_call() によって __autoload 関数のエンジンキャッシュを効率的に置き換えるからです。

スタックってことは、複数のautoloadロジックを使えるってことですよね。
これはいつか覚えててよかったと思いそうだったので、メモがわりに残します。

動的にクラスを定義する方法ないかなと思ってるんです。
Zend_Db_Table_Rowをテーブル毎に名前を変えて定義したい場合、名前とprotectedの変数の値しか変わらないのに、毎回書くのは面倒に感じてしまって。

Doctrineは、自動生成っていう方法で面倒さを回避していますが、Zend_Db_Table_Rowの拡張の場合、自動生成するような分量でもないし、自動生成のやり方を説明するところで、時間をくいそうなので、自動的ができたらなと考えています。現時点でいいアイデアは浮かんでいません。

http://www.php.net/manual/ja/function.spl-autoload-functions.php
http://www.php.net/manual/ja/function.spl-autoload-register.php
http://www.php.net/manual/ja/function.spl-autoload.php
http://www.php.net/manual/ja/function.spl-autoload-call.php

【Zend】処理中にbootstrapを取得する方法

controllerの中なら、

$bootstrap = $this->getInvokeArg('bootstrap');

で取得できる。

modelとかの中でも

$bootstrap = Zend_Controller_Front::getInstance()->getParam('bootstrap');

で取得できる

bootstrap取得して何をするかというと、application.iniで設定したリソースを取得できる。
例えば、multidbリソースを使ってたとすると

$bootstrap->getPluginResource('multidb')->getDb('hoge1');

みたいに、hoge1 DBへのDBアダプターが取得できる。

このアダプターを、各テーブルクラスのコンストラクタで、テーブルのアダプターとして設定してあげると、普段Default_Adapterを設定しているときのように、どのアダプタを使うとかを、プログラムを作るときに意識しなくていい。

== その後1 ==
生成されたPluginリソース(Zend/Application/Resourceとかにあるやつ)は、
BootstrapのContainer(中身はZend_Registryインスタンス)に入ってたので。

$boostrap->getContainer()->multidb->getDb('hoge1');

でも同様のDBアダプターが取得できる。

getPluginResource()だと、インスタンスがなかった場合作ってくれるが、
Container経由だと、なかったらnullが返ってくるはず。

どっちもpublicメソッドなので、どちらでも使える。
処理数でいうと、getContainerが少ない(チェックとかないため)ので、
最初からインスタンスがあるってわかってるものについては、getContainer()経由がいいのかも。

== その後2 ==
ソース見ても、getPluginResource()は、bootstrapの中からしか呼ばれてないし、
protectedでいいと思うんだけど、なんでだろう?

自前のBootstrap作るときも、Bootstrapクラス継承するから問題ないし。
クラス外から呼ばれることを想定していたとして、どういう場合かがわからん。

getContainer()一本でいいのに。

Zend_Loader::isReadableを使う理由

Zend_Loader::isReadableというメソッドがあるのを今日知りました。今までphpの標準関数is_readableを使っていたので必要ともしてませんでした。これの違いってなに?

マニュアルを読んでみると、こんな記述が

このメソッドは、PHP の関数 » is_readable() のラッパーです。この関数は include_path を探しませんが、 Zend_Loader::isReadable() は include_path も検索対象に含めます。

ソースはこんな感じ

    public static function isReadable($filename)
    {
        // Phar occasionally fails when using fopen()
        if (strpos($filename, 'phar://') !== false) {
            return is_readable($filename);
        }

        if (!$fh = @fopen($filename, 'r', true)) {
            return false;
        }
        @fclose($fh);
        return true;
    }

なるほど、include_path先も見てくれるとなると便利そうと感じる。
include_path関連で、シンボリック等を使ったパスを渡すと実際の絶対パスを返してくれる、realpathというphpのメソッドも覚えておくと、使うときがありそうです。

Zend_Http_Clientでbitly APIを利用する

URL短縮サービス http://bitly.net/ は、APIが準備されているので、自分のプログラムの中に組み込むことができます。

実際に組み込む前に、http://bitly.net でsign up(アカウント登録)して、APIを利用するのに必要なapikeyをメモっておいてくださいね。
英語のページですが、英語がほとんどわからない僕でも登録できたので、そんなに手間取らずにできると思います。

setParameterGet('version', '2.0.1')
       ->setParameterGet('login', 'アカウント')
       ->setParameterGet('apiKey', 'apiKey')    // ログイン後の画面に書いてあるやつR_xxxとか
       ->setParameterGet('longUrl', $url);
$res = $client->request();   // 変換実行

if ($res->isSuccessful()) {
    // 結果を stdClass形式で受け取る
    $body = Zend_Json::decode($res->getBody(), Zend_Json::TYPE_OBJECT);
    $bitlyUrl = $body->results->{$url}->shortUrl;
}
?>

Zend_Db_Tableを利用して複数DBサーバを参照する

DBサーバの負荷分散のために、select文を発行する先のDBを複数用意して運用することがあると思います。マスター(更新&参照)1台に、スレーブ(参照)1台とか。

今回、マスター1台、スレーブ1台の構成で、selectクエリを振り分けるプログラムを作りました。Zend_Db_TableのfetchRow、fetchAll等が呼ばれる度に、クエリを実行するDBサーバを決定します。

まずは、DB接続先の設定をiniファイルに定義します。
接続先情報は、ダミーです。

database.adapter         = Pdo_Mysql
database.params.host     = master.test.com
database.params.username = dbuser
database.params.password = hogehoge
database.params.dbname   = master

# 参照DBhost
database.reference.host  = slave1

次に、Zend_Db_Table_Abstractと各テーブルクラスの間に入れるクラス。
Zend_Db_Table_Abstractクラスの拡張クラスです。

abstract class My_Db_Table_Abstract extends Zend_Db_Table_Abstract
{
    public function fetchAll($where=null, $order=null, $count=null, $offset=null)
    {
        // 2回に1回はmasterに接続
        if ((rand() % 2) == 0) {
            return parent::fetchAll($where, $order, $count, $offset);
        }
        
        // 2回に1回はslaveに接続
        $adapter = $this->getAdapter();
        $this->_setAdapter($this->getReferenceAdapter());
        $rows = parent::fetchAll($where, $order, $count, $offset);
        $this->_setAdapter($adapter);
        
        return $rows;
    }

    public function fetchRow($where=null, $order=null)
    {
        // 2回に1回はmasterに接続
        if ((rand() % 2) == 0) {
            return parent::fetchRow($where, $order);
        }
        
        // 2回に1回はslaveに接続
        $adapter = $this->getAdapter();
        $this->_setAdapter($this->getReferenceAdapter());
        $row = parent::fetchRow($where, $order);
        $this->_setAdapter($adapter);
        
        return $row;
    }
    
    public function getReferenceAdapter()
    {
        if (Zend_Registry::isRegistered('Ref_Db_Adapter')) {
            return Zend_Registry::get('Ref_Db_Adapter');
        }
        
        $config = new Zend_Config_Ini('↑で作ったconfigファイル名');
        $params = $config->database->params->toArray();
        $params['host'] = $config->database->reference->host;
        $db = Zend_Db::factory($config->database->adapter, $params);
        $db->setFetchMode(Zend_Db::FETCH_ASSOC);
        Zend_Registry::set('Ref_Db_Adapter', $db);
        
        return $db;
    }
}

テーブルクラスの定義は以下のように

class Users extends My_Db_Table_Abstract
{
    protected $_name    = 'users';
    protected $_primary = 'user_id';
}

これで、fetchRow、fetchAllを使う際は、masterとslaveにクエリが分散されます

Plugin内のExceptionで、処理はとまらない

ActionHelperやPluginを有効利用しようと模索していますが、
今回Pluginでチェック処理を行った際に、気づいたことがあります。

Plugin内でExceptionを発生させても、処理は止まらないということ。
Zend_Controller_Action内の処理で発生した場合は止まるのに。(※1)

※1
Zend_Controller_Action内とは、ActionHelper、Controller内で
利用しているmodelの処理を指しています。

調べてみると、PluginでのExceptionはPluginBroker
(Pluginの処理を統合してるとこ)の中で全てcatchされているために、
PluginBrokerを呼び出しているFrontControllerでは認識できません。
そのために、FrontControllerはPlugin内でExceptionが発生しても、
その後の処理をどんどん実行していくというわけです。

チェック次第で処理を中断するという内容の処理を記述するなら、
Zend_Controller_Action内(※1)でってことですね。

HelperとPluginのこういう違いがわかっていくと、使い分けの
目安がなんとなくできるようになっていきますね。