NakajiJapan

餓鬼の超弩級日記


[zend]ZendFrameworkでCSV出力をおこなう方法

突貫工事ではありますが、controllerからデータをCSV出力させるようにさせてみた。

        // csv?
        if (isset($params['csvFlg'])) {
            $this->getResponse()
                 ->setHeader('Content-disposition','attachment; filename="data_'.date('YmdHis').'.csv"' )
                 ->setHeader('Content-type', 'test/x-csv')
                 ->sendHeaders();
            foreach($users as $user) {
                $this->getResponse()->appendBody( implode(',', $user) . "\r\n" );
            }
            $this->getResponse()->outputBody();
            exit;
        }
No Comments »

[zend]アプリ側(UTF8)とDB(EUC-JP)の文字コード変換をPDOで行う

システム開発行っていると、なるべく昔の仕様を変更せずに既存リソースを生かしながらも新しい開発行っていかな
ければいけない自体が多々ある思われる。

今回の件でいえば、文字コードの問題。

システム側からみたら文字コードなんていっそのこと統一したほうが何も考えないで楽なんだけど、そうも
いってられず昔の文字コードをどう利用していくかを考えなければいけないときがある。

理想としてはすべてUTF-8にしたかったがそうはいかない。DBがEUC-JPだからだ。
DB・アプリ間でのインターフェースでそれぞれ変換する必要がある。

対策としては、ZendFrameworkで利用しているPDOのクラスを継承して変換をさせるようにしました。
(この場合、局所的な対処ですがとりあえず目的は果たせたのでこれでいきました)

※ZendでDIコンテナ使って細かいこと実現しているのでそのへんは・・・・。

まずは

abstract class App_Db_Adapter_Pdo_Abstract extends Zend_Db_Adapter_Pdo_Abstract
{
    /**
     * Creates a PDO object and connects to the database.
     *
     * @return void
     * @throws Zend_Db_Adapter_Exception
     */
    protected function _connect()
    {
        // if we already have a PDO object, no need to re-connect.
        if ($this->_connection) {
            return;
        }

        // get the dsn first, because some adapters alter the $_pdoType
        $dsn = $this->_dsn();

        // check for PDO extension
        if (!extension_loaded('pdo')) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception('The PDO extension is required for this adapter but the extension is not loaded');
        }

        // check the PDO driver is available
        if (!in_array($this->_pdoType, PDO::getAvailableDrivers())) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception('The ' . $this->_pdoType . ' driver is not currently installed');
        }

        // create PDO connection
        $q = $this->_profiler->queryStart('connect', Zend_Db_Profiler::CONNECT);

        // add the persistence flag if we find it in our config array
        if (isset($this->_config['persistent']) && ($this->_config['persistent'] == true)) {
            $this->_config['driver_options'][PDO::ATTR_PERSISTENT] = true;
        }

        try {
            $this->_connection = new PDOCSTM(
                $dsn,
                $this->_config['username'],
                $this->_config['password'],
                $this->_config['driver_options']
            );

            $this->_profiler->queryEnd($q);

            // set the PDO connection to perform case-folding on array keys, or not
            $this->_connection->setAttribute(PDO::ATTR_CASE, $this->_caseFolding);

            // always use exceptions.
            $this->_connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

        } catch (PDOException $e) {
            /**
             * @see Zend_Db_Adapter_Exception
             */
            require_once 'Zend/Db/Adapter/Exception.php';
            throw new Zend_Db_Adapter_Exception($e->getMessage(), $e->getCode(), $e);
        }

    }

}

なんてものを作成します。

実際変更したところといえば、

# その1
$this->_connection = new PDOCSTM();

あとは、PDOを継承して実装したクラスを記述しておしまいです。
これでほぼ取得や更新両方SQLを変換することができます。

class PDOCSTM extends PDO {
    public function __construct($dsn, $username="", $password="", $driver_options=array()) {
        parent::__construct($dsn, $username, $password, $driver_options);
        $this->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('STMT', array($this)));
    }
}
class STMT extends PDOStatement {
    public $dbh;
    protected function __construct($dbh) {
        $this->dbh = $dbh;
    }
    function fetch($option=PDO::FETCH_ASSOC) {
        $row = parent::fetch($option);
        foreach ($row as $key=>$val) {
            # その1
            $row[$key] = mb_convert_encoding($val, "UTF-8", "EUC-JP");
        }
        return $row;
    }
    function fetchAll($option=PDO::FETCH_ASSOC) {
        $rows = parent::fetchAll($option);
        foreach ($rows as $key => $row) {
            foreach ($row as $crm => $val) {
                # その2
                $rows[$key][$crm] = mb_convert_encoding($val, "UTF-8", "EUC-JP");
            }
        }
        return $rows;
    }
    function execute($params=array()) {
        if (APPLICATION_ENV !== "production") {
            if (!preg_match("/SELECT/", $this->queryString)) {
                error_log("[SQL] " . $this->queryString . " Array::" . print_r($params,true));
            }
            else {
                error_log("[SQL] " . $this->queryString);
            }
            if (!empty($params)) {
                # その3
                $params = App_Array::getEncodedData($params, "EUC-JP", "UTF-8");
            }
        }
        return parent::execute($params);
    }
}

ちなみに「App_Array()」は配列のデータを再帰的に文字コード変換させてます。

                # その3
                $params = App_Array::getEncodedData($params, "EUC-JP", "UTF-8");
No Comments »

Zend_Application に Autoloderの設定を行う

昔のバージョンだとZendのソースを書き換えなくちゃいけなかったけど1.10からは自分でZend_Application利用してカスタマイズできるようになったみたいです。

まずはPHP.iniを以下のように設定します。
設定項目としては、どのクラスが宣言された場合はどのディレクトリを読み込みにいくかという設定です。

やりかたはいろいろあると思いますが、とりあえずこんな感じでいいかなレベルです。

;--------------------
; loader
;--------------------
; resources.autoloader.lists.(設定名) = "(どのクラス),(読み込み先)"
resources.autoloader.lists.validate = "Validate,models/Validate"
resources.autoloader.lists.dbtable = "Model_DbTable,models/DbTable"
resources.autoloader.lists.mappers = "Model_Mapper,models/mappers"

リソースのプログラムは以下のように実装します。


/*
 * My_Application_Resource_Autoloader
 */
class My_Application_Resource_Autoloader extends Zend_Application_Resource_ResourceAbstract
{
    /**
     *
     * @return My_Application_Resource_Autoloader
     */
    public function init()
    {
        // init
        $router = null;

        // オプション情報取得
        $options = $this->getOptions();

        $moduleLoader = new Zend_Application_Module_Autoloader(array(
            'basePath' => APPLICATION_PATH,
            'namespace' => '',
        ));

        foreach ($options['lists'] as $key => $resource ) {
            $data = explode(',', $resource);
            $moduleLoader->addResourceType($key, $data[1], $data[0]);
        }

        return ;
    }
}

これでいけるはず!!!

■参考URL
http://d.hatena.ne.jp/sasezaki……0090415/p1

No Comments »

[zendframework][smarty]PC/Mobile版切替時にSmartyの設定で気をつけること

最近ハマったこと。

携帯版とPC版でテンプレートを切り分けているときにちゃんとテンプレートキャッシュも別で保存しておかないと
最初に表示した画面の状態をずっと保持し続けてしまう。たとえば、PC版で表示したら携帯版で表示しても
PC版で表示されてしまう。

当たり前のことだけど、別にしないとだめだよね。

Smartyのコンパイルファイル(templates_c側)を一緒のディレクトリ・ファイルにしないようにします。
なので携帯版はSMARTY_COMPILE_IDに「_mb」を追加して別管理にしました。

以下がディレクトリ構造。

---------------- 例 -------------------
[root@hoge] # tree templates_c
templates_c
|-- ref
|   |
|   `-- %%DE
|       `-- DE7
|           `-- DE7C2150%%header.html.php
`-- ref_mb
    |
    `-- %%DE
        `-- DE7
            `-- DE7C2150%%header.html.php

Smartyのキャッシュを有効にしたときはまたもう少し考えないといけないな。

例えば、日記を新しく作成した場合はどちらとも作成しないといけないね。

No Comments »

携帯でリダイレクトした際にセッションが切れてしまう件。

携帯版ではまってしまったのでメモ

携帯は基本的にはセッションはパラメータで引き回すようにしています。
しかし、リダイレクト時には自動的にはセッションIDは引き回さないようPHPの仕様でなっています。
そこで、リダイレクト時にもセッションを引き回せるように手動でセッションを引き回すように処理を
加えたのですがなぜかリダイレクト先の処理側でセッションが新しく生成されてしまう。

そう、セッションを引き回していなかったのです。

「加藤さん事件です。」(レポーター風)

一応セッション設定チェック

;----- mobile {-----
resources.session.use_only_cookies      = false
resources.session.use_cookies           = false
resources.session.use_trans_sid         = true
;----- mobile }-----

なんら問題無し。

各処理でのサーバ情報ゲット

[QUERY_STRING] =>
[REQUEST_URI] => /test.php

!?

むむむ、URI自体に問題があるのか。

そういえばリダイレクト先にパラメータとは別に「#top」なんてものも追加していた。。。


http://nakajijapan.net/test.php#top?PHPSESSID=xxxxxxxxxxxxxxx

だとだめで

http://nakajijapan.net/test.php?PHPSESSID=xxxxxxxxxxxxxxx#top

「#」が前に来るか後にくるかででパラメータを認識してくれないようですね。

RFCの決まりごと的には最初にqueryの解析を始めるからそれでもうないって認識されてしまったのかもね。

結構単純なことでした。。

■参照URL
URI – Uniform Resource Identifiers

No Comments »

ZendFrameworkでBasic認証 既にあるものを利用した編

以前、Zendでベーシック認証を自作したエントリーがありましたが、本当は
Zendに用意されているプラグインがあるのです。

なぜ利用しなかったっていうと、Zend側での必須項目としてrealmを必ず
指定しなければいけないのでです。それだとユーザ名とパスワードの認証情報
に対応しないので必ずエラーになってしまいます。

ベーシック認証としては「Zend_Auth_Adapter_Http_Resolver_File」のクラスが
当てはまるのでしょうね。このクラスに認証に必要なファイルをセットする。
Zendの認証用のAdapterを生成して、このクラスに実際の処理をさせていく感じですかね。

        $config = array(
            'accept_schemes'  => 'basic',      // basic認証
            'realm'           => 'aaaaa',      // realm(ベーシック認証のフォームに出力されます)
            'digest_domains'  => '/login',     // どのURL以下にベーシック認証をかけるかです。
            'nonce_timeout'   => 3600
        );

        $resolver = new Zend_Auth_Adapter_Http_Resolver_File();
        $resolver->setFile('passwd.txt');

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

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

passwd.txtは、ドキュメントルート(index.phpがあるところ)からのパスになります。
内容はこんな感じです

test:aaaaa:hogehoge
ユーザ名:realm:パスワード

realmはプログラム内で指定したrealmと一致している必要があります。

前回書いたものよりだいぶスマートに実装されていますね。感心感心です。
ちょっとわがままいうなら認証機構をもっと昔のにも親和性高くしてほしかったなと思います。

まぁでも勉強になったのでよしとします。

No Comments »

ZendFrameworkでベーシック認証を試みる 自作編

わけって、アプリ側(ZendFramework)でもベーシック認証できないかということで調べてみたらありました。

流れとしては、

1. 認証を行う対象のコントローラがリクエストされたら、認証用のリクエストを送信する
2. パスワードファイルは指定されたディレクトリから情報読み込み、そこから認証のチェックを行う
3. 認証できれば当該処理を終了させる
4. 認証できなければ、401にする

今回はプラグイン側で実行することにします。

/**
 * My_Controller_Plugin_HttpBasicAuth
 *
 */
class My_Controller_Plugin_HttpBasicAuth extends  Zend_Controller_Plugin_Abstract
{

    /**
     * Basic認証
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
    {
        // init
        $server         = $this->getRequest()->getServer();
        $htpasswdFile   = "/home/html/public_html/.htpasswd";

        // 処理系のアクションか?
        if (file_exists($htpasswdFile) && is_file($htpasswdFile)) {
            // init
            $response = $this->getResponse();
            $request  = $this->getRequest();

            // htpasswdから情報取得
            $htpasswds = file($htpasswdFile);

            // headerから情報取得
            $authHeader = $request->getHeader('Authorization');
            // 認証情報解析
            $cryptPass = '';
            $cryptUser = '';
            if (preg_match('/Basic (.+)/', $authHeader, $matches)) {
                $base64     = base64_decode($matches[1]);
                $userInfo   = explode(':', $base64);
                $cryptUser  = $userInfo[0];

                // パスワード情報があるまでチェックする
                foreach ($htpasswds as $line) {
                    if (empty($line)) {
                        continue;
                    }
                    $htpasswd  = My_Util::trimAll(explode(':', $line));
                    $username  = array_get($htpasswd, 0);
                    $password  = array_get($htpasswd, 1);

                    // 送信されたパスワード
                    $cryptPass  = crypt($userInfo[1], substr($password, 0, 2));

                    // 値は等しいければ終了
                    if ($password == $cryptPass && $username == $cryptUser ) {
                        return;
                    }
                }
            }

            // 認証リクエスト送信
            $response->setRawHeader('HTTP/1.1 401 Authorization Required');
            $response->setRawHeader('WWW-Authenticate: Basic realm="My User"');

            // 認証画面設定
            $response->clearBody();
            $request->setControllerName('error')
                    ->setActionName('authrequired')
                    ->setDispatched(false);
        }
    }
}

最後にapp.iniにプラグインを有効化して終了。

resources.frontController.plugins.HttpBasicAuth.class = "My_Controller_Plugin_HttpBasicAuth"

ベーシック認証の仕組みを理解してれば、容易くいけるものですね。

base64でデコードしてあげればいいだけですもの。

No Comments »

zend_captchaをいじってみた

やっぱ、コメントスパムってなくならないよねーと騒がれるこのご時世。

対策の一環として一つの施策を行いました。

そう、その名もキャプチャ認証。チャララリン。

今回はZendのコンポーネントに画像認証用のものがあったのでそれを利用してみることにしました。

下準備として以下の条件が必要になります。
・GDは利用できるかどうか
・画像を保存するディレクトリは実行権限が「7」で設定されているか

早速実装です。以下が画像と承認用のIDを生成する設処理です。

        //-----------------------------------
        // キャプチャ画像生成
        //-----------------------------------
        $imgUrl = "hogehoge";
        $imgDir = "hogehoge";
        $config = array('timeout' => 300,
                        'wordLen' => 6,
                        'width'   => 110,
                        'height'  => 50,
                        'lineNoiseLevel' => 3,
                        'dotNoiseLevel' => 3,
                        'fontSize' => 26,
                        'font'    => '/usr/share/fonts/ja/TrueType/kochi-mincho-subst.ttf',
                        'imgUrl'  => $imgUrl,
                        'imgDir'  => $imgDir
            );
        $captcha = new Zend_Captcha_Image( $config );
        $captcha->generate();   //command to generate session + create image

        $this->view->captchaId      = $captcha->getId();
        $this->view->captchaImgTag  = $captcha->render(); // generate Image Tag

簡単です、設定をちょろちょろしてからインスタンス生成、「generate()」で画像生成。これでロジック部分は終了。

次はHTML部分です。確認用のID情報はセッションに保存され、それをもとに値をチェックするようになります。

{* テンプレートエンジンはSmartyを使っています。 *}

{$captchaImgTag}

後は、入力した値のチェックを行う部分です。
チェックを行う際にはValidatorを作成してセッションに値と正しいかのチェックを行います。

    public function isValid($value)
    {
        $captchaId          = $value['id'];
        $captchaInput       = $value['input'];
        $captchaSession     = new Zend_Session_Namespace('Zend_Form_Captcha_' . $captchaId);
        $captchaIterator    = $captchaSession->getIterator();

        if( isset($captchaIterator['word'])
            && $captchaInput == $captchaIterator['word'] ) {
            return true;
        }
        else {
            return false;
        }
    }

以下のコードで複数画面操作していても別の画面で生成された値を利用しなくて済むようになります。

        $captchaSession     = new Zend_Session_Namespace('Zend_Form_Captcha_' . $captchaId);

なんか書いてみたけどチェック用のメソッドがあったのが後々になって気づきました。無念。

「Zend_Captcha_Image::validateCaptcha();」
まぁそんなかわらんからえええか

画像がこんな感じで出力されます。

まぁ、そんなこんなで簡単に実装ができるとさ。

ノイズもあるメソッドをごりごりいじればもっと複雑かつ面白くできるのだが今回はここまで。

■参考URL
Tutorial: Using Zend_Captcha_Image

No Comments »

Zend_Db_Selectの不穏な動き

最近、こう日記を更新していけども。まぁHDがいかれるやらSATAのコネクタの規格を間違って購入してることやらなんやらほんにゃら。
そろそろ、俺のサーバもクラウドかな。まだやらんけどさ。

マニュアル上ではこんな感じで記述されていてクラスでもSQL(文字列)でも埋め込める事ができるらしいのだけれど

$sql1 = $db->select();
$sql2 = "SELECT ...";

$select = $db->select()
    ->union(array($sql1, $sql2))
    ->order("id");

でも実際この通り実装してみるエラー発生、原因としてはどうやらMysql5でZendが吐いたSQLを実行すると
エラーが発生する模様。Mysql4ではどうも発生しなかったみたい。Zendの仕様なのかなと思ったので
しょうが無く文字列にしてごねごね作成することにした。

$sql1 = $db->select();
$sql2 = "SELECT ...";

$select = $db->select()
    ->union(array("(".$sql1->__toString().")", "(".$sql2.")"))
    ->order("id");

でも、この書き方腑に落ちないですね。SQLは低レイヤーで抽象化されるべきもの。しかもUNIONするときは括弧で括ったほうが
SQL的には推奨されてるはずなんだけどねーー。

Mysqlマニュアル

この辺、いじって括弧で括らせれば問題解決されるかも。でもZendのオリジナルソースいじるの気がひけるなー。
継承したいところだけど結構クラスが入り組んでて大変なことになりかねないのでソースいじったほうがてっとり早いんだろうなー。

この辺いじる。

    /**
     * Render UNION query
     *
     * @param string   $sql SQL query
     * @return string
     */
    protected function _renderUnion($sql)
    {
        if ($this->_parts[self::UNION]) {
            $parts = count($this->_parts[self::UNION]);
            foreach ($this->_parts[self::UNION] as $cnt => $union) {
                list($target, $type) = $union;
                if ($target instanceof Zend_Db_Select) {
                    $target = $target->assemble();
                }
// この辺かな
                //$sql .= $target;
                $sql .= '(' . $target . ')';
                if ($cnt < $parts - 1) {
                    $sql .= ' ' . $type . ' ';
                }
            }
        }

        return $sql;
    }
No Comments »

即興でZendFrameworkでルーターをプラグイン化

最近になってDIコンテナの意味を体で感じ始めてきた今日この頃。僕です。

今日はzendframeworkのRouerという機能をbootstrapに入れてみます。

まずRouer用のリソースを作成

/*
 * My_Application_Resource_Router
 */
class My_Application_Resource_Router extends Zend_Application_Resource_ResourceAbstract
{

    public function init()
    {
        // init
        $router = null;

        // オプション情報取得
        $options = $this->getOptions();

        // ルータを利用する
        if ( $options['use'] == true ) {
            // フロントコントローラ取得
            $front = Zend_Controller_Front::getInstance();

            // ルータ情報を保存する。
            $routes = new Zend_Config_Ini( $options['filepath'] , $options['section'] );
            $router = $front->getRouter();
            $router->addConfig($routes, $options['section']);
        }

        return $router;
    }
}

その後にapplication.iniに設定のファイルを読み込ませます。

resouorce.router.use = true
resouorce.router.filepath = "path/to/"
resouorce.router.section = "router"

一応、これでindex.phpでやってたことをapplication.iniで行えるようにできました。とさ。

No Comments »