テンプレートエンジンにフォーム値の設定機能を付けた
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>https://github.com/nishimura/laiz-template/wiki/ja-readmeフォーム制御
基本的に何もしませんが
<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 $vars->items = array('選択1', '選択2', '選択3'); $vars->selectedItem = '選択2';HTML
<select name="selectedItem" laiz:form="items"> <option value=""> - </option> </select>
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でやろうと思った場合は…
- プロジェクトのログイン処理のクラスやフィルターのクラスをエイリアスとして登録し、AuthenticationServiceをインジェクション
- AuthenticationServiceをエイリアスで登録し、AdapterとStorageをエイリアスで登録して
- ZendのAdapterまたは自作Adapterをエイリアスとして登録
- Storageの識別子をコンストラクタインジェクションで設定
- アクションクラス(ページクラス?)でログイン済みかどうかを判定する必要がある場合は、そのクラスに対して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; } } }
今まで使っていた自作フレームワークと今回のフレームワークの違い
- 旧バージョン
- 全体的な考え方はMaple + guesswork
- 自作DI+α
- 1アクション1クラス
- ディレクトリ階層ごとの設定ファイル
- アクションごとの設定ファイル
- URLはフレームワークベース
- 独自ORM(joinなし)
- HTML_Template_Flexy(改造版)
- 全体的な考え方はMaple + guesswork
- 新バージョン
主な設定(?)と機能
- アクション|ページクラスは一つのnamespace以下に収める
- @var アノテーションを書くと型変換する
- @Converter アノテーションで変換
- trimとか
- @Validatorアノテーションで入力チェック
- 実体はiniファイルに書く
- validかどうかは三値で取得する。null=動作なし、false=invalid、true=valid
- アクションフィルター的なものはURLに対する正規表現で書く
- 一ヶ所に集約
- Zend\Di に対する設定はdi.iniに書く
その他
- モデルやサービス的なことは何もしない
- 自分でApp\Modelなどのnamespaceを作る
- メールとかファイルとか権限管理はまだ無い
- ログイン処理はサンプルとしてiniファイル版とDB版がある
- Wicketのように例外でリダイレクトする
- アクションフォワード的な機能は無し
現状はこんなところ。
yasnippet の php-mode を更新した
https://github.com/nishimura/minimal-yasnippet-php-mode
今更ながらPHP5.3 の namespace に対応した。
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