ログ日記

作業ログと日記とメモ

テンプレートエンジンにフォーム値の設定機能を付けた

https://github.com/nishimura/laiz-template

これでデザイナーから大量のinput type="checkbox"が入ったHTMLを渡されても、正規表現置換で一瞬で対応できる。


簡単なマニュアル書いた。

if文

hasError変数がtrueの時(if ($hasError))だけタグ内を表示します。

<div style="color:red;" laiz:if="hasError">
{errors.myError}
</div>

繰り返し

タグ内をforeachで繰り返し出力します。

<li laiz:loop="arrayOrObjectVarName:innerName">
{innerName.myValue}
</li>
<li laiz:loop="arrayOrObjectVarName:index:innerName">
{index}: {innerName.myValue}
</li>

フォーム制御

基本的に何もしませんが

<input type="text" name="item[name]" value="{item.name}">
<input type="text" name="item[value]" value="{item.value}">

のように記述してPHP側で array を object に変換する方針を想定しています。
selectboxとcheckbox、radioはめんどくさいのでユーティリティがあります。

checkbox, radio

タグにlaiz:formをつけると nameとvalueを解析してcheckedを自動設定します。

<input type="checkbox" name="check1" value="ON" laiz:form/>
<input type="checkbox" name="checks[]" value="check1" laiz:form/>
<input type="checkbox" name="checks[]" value="check2" laiz:form/>

<input type="radio" name="radio1" value="value1" laiz:form/>
<input type="radio" name="radio1" value="value2" laiz:form/>
selectbox

PHP

<?php
$vars->items = array('選択1', '選択2', '選択3');
$vars->selectedItem = '選択2';

HTML

<select name="selectedItem" laiz:form="items">
  <option value=""> - </option>
</select>
https://github.com/nishimura/laiz-template/wiki/ja-readme

Zend\Di の依存関係ループを解消する

勘違いしていたので追記

SessionManager は 複数扱えないので、セッションを複数持つ場合は以下のようにして名前空間だけ分ける。

<?php
class SessionStorage{}
class SessionAdapter{
    public function __construct(SessionStorage $storage = null){
        $this->storage = $storage;
    }
}
class SessionManager{
    public function __construct(SessionAdapter $adapter){
        $this->adapter = $adapter;
    }
}

class AuthStorage{
    public function __construct(SessionManager $manager){
        $this->manager = $manager;
    }
    public function setName($name)
    {
        $this->name = $name;
    }
}
class AuthAdapter{
}
class AuthService{
    public function __construct(AuthAdapter $adapter){
        $this->adapter = $adapter;
    }
    public function setStorage(AuthStorage $storage){
        $this->storage = $storage;
    }
}

if (0):
$di = new \Zend\Di\Di();
$im = $di->instanceManager();
$im->addAlias('auth1', 'AuthService');
$im->setParameters('auth1',
                   array('storage' => 'storage1'));
$im->addAlias('storage1', 'AuthStorage');
$im->setInjections('storage1',
                   array('setName' => array('name' => 'name1')));

$im->addAlias('auth2', 'AuthService');
$im->setParameters('auth2',
                   array('storage' => 'storage2'));
$im->addAlias('storage2', 'AuthStorage');
$im->setInjections('storage2',
                   array('setName' => array('name' => 'name2')));

var_dump($di->get('auth1'));
var_dump($di->get('auth2'));

セッション名やlifetime, start, destroyは共有される。
これを分けるには、SessionManagerがPHPのsession_*関数を使ってセッション管理することをやめて独自Cookieを発行するようにならないと無理ぽい。

ここまで追記




複数セッションを使いたい場合にSessionManagerをエイリアス設定することが必要と書いた*1が、一つ一つ$di->get()せずに最初に依存関係を全部定義するやり方だとうまくいかなかった。

Uncaught exception 'Zend\Di\Exception\CircularDependencyException' with message 'Circular dependency detected: Zend\Session\SessionManager depends on Zend\Session\Storage\StorageInterface and viceversa'

エラーが出る。


これは、$instanceManager->setParameters や setInjections でパラメーター名を指定したらそれが依存関係解消中はずっと共有されることから起きる。
例えば

<?php
class Storage{}
class A {
    public function __construct(Storage $storage = null){
        $this->storage = $storage;
    }
}
class B {
    public function __construct(A $a){
        $this->a = $a;
    }
}
$di = new \Zend\Di\Di();
$im = $di->instanceManager();
$im->setParameters('B', array('storage' =>'Storage'));
var_dump($di->get('B'));

このように書くと、Aのstorageをインジェクションできる。
setParameterで指定しているのはBだが、Bが依存しているAに対するパラメーターも同時に指定したことになる。


これがどういうことになるかというと

<?php
class SessionStorage{}
class SessionAdapter{
    public function __construct(SessionStorage $storage = null){
        $this->storage = $storage;
    }
}
class SessionManager{
    public function __construct(SessionAdapter $adapter){
        $this->adapter = $adapter;
    }
}

class AuthStorage{
    public function __construct(SessionManager $manager){
        $this->manager = $manager;
    }
}
class AuthAdapter{
}
class AuthService{
    public function __construct(AuthAdapter $adapter){
        $this->adapter = $adapter;
    }
    public function setStorage(AuthStorage $storage){
        $this->storage = $storage;
    }
}

$di = new \Zend\Di\Di();
$im = $di->instanceManager();
$im->setInjections('AuthService',
                   array('setStorage' => array('storage' => 'AuthStorage')));
var_dump($di->get('AuthService'));

単純に書くと、これで依存関係のエラーになる。
AuthService#setStorage のパラメーターを storege = AuthStorage に設定しているつもりが、実は SessionAdapter#__construct の storageパラメーターの方まで設定されてしまうという罠。



解決方法は二通り。

  • 先に依存関係の深い部分を呼び出しておく
    • $di->get('AuthService') の前に $di->get->('SessionManager') しておく
    • するとstorage = AuthStorage が設定されていない状態で SessionManager が生成されるので依存関係が思った通りに解決する
  • setInjectionsは使わずに、setParametersでメソッド名とパラメーター番号を指定する
<?php
...
$di = new \Zend\Di\Di();
$im = $di->instanceManager();
$im->setParameters('AuthService',
                   array('AuthService::setStorage:0' => 'AuthStorage'));
var_dump($di->get('AuthService'));


この辺の動作はDi.phpの 537行付近や584行付近にある。


これはパラメーター名が同じことから起こっているので、もし自分で作ったクラスでこういうエラーが出たらパラメーター名を変えるのが手っ取り早いかもしれない。


ここから更に追記

addAliasしてエイリアスベースで設定していると、AuthService::setStorage:0 のような記述はできないようだった。
Di.phpのソースを追って出来そうだと判断したけれど、公式の使い方じゃないのかもしれない。

'Invalid instantiator of type "NULL" for "Zend\\ServiceManager\\ServiceLocatorInterface".

Zend\ValidatorPluginManager や Zend\Filter\FilterPluginManager などの PluginManager系をDIで取ろうとするとエラーになる。


同じ問題をやっている方が居た。
http://d.hatena.ne.jp/noopable/20130304
ここを詳しく読む前にやったので、全然別の方向から。


http://framework.zend.com/manual/2.1/en/modules/zend.di.instance-manager.html

  • $di->instanceManager()->addTypePreference('interface', 'realClass') でインターフェースのデフォルト実装を指定する
  • $introspectionStrategy->setInterfaceInjectionInclusionPatterns(array()) でAwareインターフェースがあればインジェクションというルールを削除する


new Zend\Di()して使い捨てる感じなら 一番目が楽。
Diを全体で使っていて、Awareインターフェース無視していいよっていうなら二番目が楽。


自分の場合はDiもDefinitionも拡張して全体で使っているので、setInterfaceInjectionInclusionPatterns(array())しておいた。
ちなみにIntrospectionStrategyはDefinitionのコンストラクタ引数で、DefinitionはDefinitionListのコンストラクタ引数で、DefinitionListはDiのコンストラクタ引数なので三階層潜らないといけない。

Zend\Di でコンストラクタインジェクションをチェーンするのは面倒くさい

一般ユーザー用画面と管理者画面があるとする。この二つで別々のセッション管理をしたい。


その場合、ここにあるようにaliasを設定すれば一応できる。

<?php
// add aliases for specific instances
$im->addAlias('dbadapter-readonly', 'MyLibrary\DbAdapter', array(
    'username' => $config->db->readAdapter->username,
    'password' => $config->db->readAdapter->password,
));
$im->addAlias('dbadapter-readwrite', 'MyLibrary\DbAdapter', array(
    'username' => $config->db->readWriteAdapter->username,
    'password' => $config->db->readWriteAdapter->password,
));

// set a default type to use, pointing to an alias
$im->addTypePreference('MyLibrary\DbAdapter', 'dbadapter-readonly');

$movieListerRead = $di->get('MyMovieApp\MovieLister');
$movieListerReadWrite = $di->get('MyMovieApp\MovieLister', array('dbAdapter' => 'dbadapter-readwrite'));
http://framework.zend.com/manual/2.1/en/modules/zend.di.instance-manager.html

これをZend\Authentication\AuthenticationServiceでやろうと思った場合は…

  1. プロジェクトのログイン処理のクラスやフィルターのクラスをエイリアスとして登録し、AuthenticationServiceをインジェクション
  2. AuthenticationServiceをエイリアスで登録し、AdapterとStorageをエイリアスで登録して
  3. ZendのAdapterまたは自作Adapterをエイリアスとして登録
  4. Storageの識別子をコンストラクタインジェクションで設定
  5. アクションクラス(ページクラス?)でログイン済みかどうかを判定する必要がある場合は、そのクラスに対してAuthenticationServiceのエイリアスをインジェクション

これで一連のセッション処理がエイリアスチェーンで登録できる。
同じことをもう1セットやると、同一パッケージで別々のセッションを持てる。


しかしこれはめんどくさい…。
セッションが一種類だけの場合は、ただAuthenticationServiceにAdapterを設定するだけで使えるのにね。
何か他に良いやり方があるんだろうか。


4/18 追記

追記

このままではSessionManagerが共有されてしまうので、セッション破棄やlifetime設定などが全て共通となる。
なのでSessionManagerもエイリアス設定しないといけない。
これはZend\Authentication\Storage\Sessionのコンストラクタで指定する。


ちょっと勘違いしていた。
SessionManagerはPHPのsession_*関数をそのまま呼び出すので、複数持つことはできなかった。
詳細は次の記事へ。

PHPのフレームワークを作った

ソースはここ: https://github.com/nishimura/laiz2

composerの使い方を見るのも兼ねてサンプルアプリを置いたのですぐにインストールできるはず。
https://github.com/nishimura/laiz-sample-task

composer.phar create-project laiz/laiz-sample-task laiz-sample-task
cd laiz-sample-task
mkdir logs cache
chmod o+w logs cache


やりたかったことは
https://github.com/nishimura/laiz-sample-task/blob/master/src/Laiz/Sample/Task/Page/Dir/Information.php
ほぼこのファイルに集約した。

<?php
...
class Information
{
    /**
     * @Converter(["upper", PlusLengthConverter])
     */
    public $plainText;

    /** @var bool */
    public $flag;

    /**
     * @var MyModel
     * @Converter(["name" => "wordseparatortocamelcase",
     *             "value" => ["wordseparatortodash", "upper"]])
     */
    public $model;
...

アノテーションによるフィルターで型変換。
フォームとDBの操作は
https://github.com/nishimura/laiz-sample-task/blob/master/src/Laiz/Sample/Task/Page/Task.php
こんな感じ。

<?php
...
class Task
{
    public $action;
    public $pager;
    public $TASKS;

    /**
     * @var TransactionToken
     * @Validator("transactiontoken.ini")
     */
    public $transaction;

    /**
     * @var Vo_Task
     * @Validator("task.ini")
     */
    public $task;

    /**
     * @Validator("check.ini")
     * @var bool
     */
    public $check;

    public function index(Db $db)
    {
        $iterator = $db->from('Task')
            ->order(array('subject', 'taskId'))
            ->iterator();

        $pager = new Pager($iterator, 5);
        $this->pager = $pager->getHtml();
        $this->TASKS = $iterator;
    }

    public function info()
    {
        // nothing
    }

    public function add(Db $db, AuthenticationService $auth, $valid = null)
    {
        $this->action = "Create New Task";
        $this->editInternal($db, $auth, $valid, 'Task was created.');
        return 'task_edit.html';
    }
    public function edit(Db $db, AuthenticationService $auth, $valid = null)
    {
        $this->action = "Edit the Task";
        if ($valid === true)
            $this->task->updatedAt = date('Y-m-d H:i:s');
        $this->editInternal($db, $auth, $valid, 'Task was edited.');
    }
    private function editInternal($db, $auth, $valid, $msg)
    {
        if ($valid === null){
            $this->task->userName = $auth->getIdentity();
        }else if ($valid === true){
            try {
                $db->save($this->task);
                throw new RedirectMessageException('/task_info.html?task[taskId]=' . $this->task->taskId,
                                                   $msg,
                                                   Message::SUCCESS);
            }catch (Exception $e){
                Message::add($e->getMessage(), Message::ERROR);
            }
        }
    }
    public function delete(Db $db)
    {
        try {
            $db->delete($this->task);
            throw new RedirectMessageException('/task.html',
                                               'Task was deleted.',
                                               Message::SUCCESS);
        }catch (Exception $e){
            Message::add($e->getMessage(), Message::ERROR);
            return;
        }
    }
}

今まで使っていた自作フレームワークと今回のフレームワークの違い

  • バージョン
  • バージョン
    • 全体的な考え方はDI+アノテーション
      • Zend\DI
      • 特に参考にしたフレームワーク無し
      • Zend Framework2 を使っているので、そういう感じの設定になる
      • composer.phar を使っているので、割とマシな名前空間に強制される
    • CRUD 1クラス
      • または1アクション1クラス
    • 全体的な設定ファイルがいくつか
    • URLはhtmlメイン
      • 例えば、デザイナーがアップロードした画像やCSSやHTMLの構成をそのまま使う
    • 独自ORM(Tsukiyo、S2JDBC風)
      • DB設計重視、複数プライマリキーの外部結合など対応
    • 独自テンプレートエンジン(Yokaze、今のところ機能縮小版Flexy、HTMLタグ解析は少しやってるのでフォームに対応したいところ)


主な設定(?)と機能

  • アクション|ページクラスは一つのnamespace以下に収める
  • @var アノテーションを書くと型変換する
    • ORMのVOの型を書くと、リクエストにプライマリーキーがある場合はDBからデータを取得し、さらにリクエスト変数で上書きする
  • @Converter アノテーションで変換
    • trimとか
  • @Validatorアノテーションで入力チェック
    • 実体はiniファイルに書く
    • validかどうかは三値で取得する。null=動作なし、false=invalid、true=valid
  • アクションフィルター的なものはURLに対する正規表現で書く
    • 一ヶ所に集約
  • Zend\Di に対する設定はdi.iniに書く


その他

  • モデルやサービス的なことは何もしない
    • 自分でApp\Modelなどのnamespaceを作る
  • メールとかファイルとか権限管理はまだ無い
    • ログイン処理はサンプルとしてiniファイル版とDB版がある
  • Wicketのように例外でリダイレクトする
    • アクションフォワード的な機能は無し


現状はこんなところ。

Zend Framework の一部を使おうとしてハマった

症状は、composer.phar で zendframework/zend-validator を入れようとすると zendframework全体がインストールされる。
原因は、zendframeworkのcomposer.jsonが良くないっぽい。



依存関係を追っていくと、どうもzendframework/zend-i18nを入れようとするとzendframeworkが入るようだった。
i18nのcomposer.jsonを見てみると、requireにintlが書いてある。試しにローカルプロジェクトのrequireにext-intlを書くと composer.phar install がエラーになる。



https://github.com/composer/composer/issues/1158
https://github.com/composer/composer/issues/1063


どうも、zend-i18nを入れようとしたらエラーになるが、zendframeworkのrequireには何もない、かつ、zendframeworkにzend-i18nが含まれているので、zendframeworkが入ってしまうみたい。

aptitude install php5-intl

intl拡張をインストールしたら無事にzendframework/zend-validatorだけインストールされた。



キャッシュを削除してチェックするコマンド下記。

# キャッシュ削除
rm composer.lock
rm -rf vendor/*
rm -rf ~/.composer/

# dry run
composer.phar install --verbose --dry-run