より良い環境を求めて このページをアンテナに追加 RSSフィード

2015-07-28

[][] ぼんやり振り返り

顧客は本当に欲しいものは知らない、と言うけれど同じような症状だ。

かゆいところに手が届かない。



自作フレームワークがつぎはぎになってきたのでカッとなって作ったやつ。

PHPの小規模用テンプレートエンジンを作った - より良い環境を求めて


なんかアノテーションマイブームだったっぽい。

PHPのフレームワークを作った - より良い環境を求めて

ただ利用しているZendのアノテーション解析が、あるバージョンでとても遅くなったので、アノテーションを使うことに不安を覚える。

言語レベルでやることをPHPで頑張ってやるべきじゃないな、などと思い直す。

@varアノテーションによるリクエスト変数のcastは結構便利だったんだけど。



ふと、Haskellっぽく書きたいよなー、前にそういうこと書いたよなーと思い出す。

バリデーターあれこれ - より良い環境を求めて

やっぱり書いてた。

ロジックの中の方でDBを呼び出すのは良くないよね、という話。


バリデーター繋がりで、Scalaでも何か書いてたなーと思い出す。

LiftでWebアプリ(4): フォーム用共通処理を作る 1 - より良い環境を求めて

この投稿の最後の方のコード、今見てもこんな風に書きたいなーと思える。

演算子が定義できないとどうしようもない…かな?


例えば、こんなの。

<?php
class SomePage{
  public function somePost(Request $req, Db $db){
    $form = $req->fragment('form1');
    $form->key('mail')->name('メールアドレス')
      ->trim()->required('{name}を入力してください')
      ->email('{name}の形式{input}が正しくありません');
    $form->key('body')->required('本文を入力してください')
      ->maxLength(1000, '{param0}文字以上は送信できません');
    $form->key('zip1')->key('zip2')->name('郵便番号')
      ->toHalf()->required('{name}を入力してください')
      ->length([3, 4])->numeric('{name}の形式が正しくありません');

    try {
      $values = $form->get();
      $account = $db->from('Account')->eq(['mail' => $values->mail])->getOne();
      ....
    }catch (IoException $e){
      $errors = $e->getErrors();
      ....
    }
  }
}

万能RequestクラスをTraitの寄せ集めで作って、Eitherの変わりに例外にして…。うーん…。

やっぱり演算子や結合がないとタイプ量が多い。


で、PSR-7もあるのでどうしようとか。



アプリは、Input => App => Output っていうのは確定で、これは

Request => App => Response に置き換えるとちょっとまずい。Outputがそのまま次のInputになった方がシンプルになりそう。

正確には Context & Request => App => Request | Response かなあ。

アプリがRequestを返したらリダイレクトなどをして、Responseを返したら終了。

いや、でもテンプレートエンジンを使うだろうしやっぱりResponseを返すのは特殊な場合だけになるのか。





まとまらないけど眠いので一旦投稿。

2013-06-28

[][][] PHPの自作フレームワーク現状まとめ

https://github.com/nishimura/laiz2

基本方針

  • DB設計最優先
  • HTML構造優先
  • コード量を少なく

大抵の処理はPage(Action)クラスに書く。いわゆるAction肥大化。

もし「この二つのテーブルは大抵joinして取得している」という状況になったらModelを作っても良いが、大抵の場合は一画面ごとに処理がユニークなので共通化できない。

アプリ側はフレームワークの何かを継承してクラスを作ることが基本的にないのでテストする場合はActionクラスをテストすれば良い。


プレゼンテーションロジックがほとんどでビジネスロジックはあまりないという考えに基づく。

DB設計と画面遷移でほぼシステムの設計は決まっていて、コードに書くビジネスロジック的なものはおまけ。


アプリを作る流れ

ふつう
  1. ER図を書く
  2. SQLを書いて初期DBを作る
  3. HTMLを書く
  4. PHPを書く
  1. ER図を変更する
  2. migrationでDBを変更する
  3. PHPHTMLを変更する

DB設計優先。

HTML優先。

PHPDBHTMLつなぐ手段。


デザイン優先
  1. htmlcss、画像などのセットをもらう
  2. PHPを書く、htmlに変数を埋める

htmlpathなどの構造は変えない。

元々ある構造にPHPを割り当てていく。



細かい話

  • DB設計最優先:フレームワークの制約でDB設計が変わることのないように
    • 主キー自由
    • リレーション自由
    • DB変更はPHPコードで&バージョン管理
  • HTML構造優先:フレームワークの制約でHTML構造が変わることのないように
  • コード量を少なく:自動で出来るところは自動でやる
    • インスタンスの生成はタイプヒンティングから自動で
    • DBのテーブル情報をPHPに対応させるところは自動で
    • フォームから送られてくる各データをオブジェクトへ詰め替えるのは自動で
    • checkbox、selectboxなどの初期値設定は自動で


次に実装するかもしれない機能

  • 自動化を進めて更にコード量を少なく
    • DB制約から最低限のバリデーションを自動で
    • HTML5のフォーム入力チェック構文からバリデーションを自動で
  • 構造を変える
    • フレームワーク自体の構造を綺麗にする
    • フォーム処理の流れがまだしっくり来ない
      • もっと単純に簡単にしたい

2013-04-16

[][][] 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(改造版)
  • 新バージョン
    • 全体的な考え方はDI+アノテーション
      • Zend\DI
      • 特に参考にしたフレームワーク無し
      • Zend Framework2 を使っているので、そういう感じの設定になる
      • composer.phar を使っているので、割とマシな名前空間に強制される
    • CRUD 1クラス
      • または1アクション1クラス
    • 全体的な設定ファイルがいくつか
    • URLはhtmlメイン
      • 例えば、デザイナーがアップロードした画像やCSSHTMLの構成をそのまま使う
    • 独自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のように例外でリダイレクトする
    • アクションフォワード的な機能は無し

現状はこんなところ。

2013-04-09

[][] PHPフレームワーク再考

PHPフレームワークを考えている。


2006年からLaizを作り始めたわけだけれど、普通に利用する機能と実験的機能がごちゃ混ぜでフレームワークが肥大化している。

特に、PHP5.3用に変更したときにPHP4のコードをそのまま持ってきたり新しい仕組みを取り入れたりしてちょっと見づらいコードになった。


既に受託案件などで何度も使っているので主要機能についてはバグがあるとか動作がおかしいとかいうことはない。

でもいくつか使いにくい点というかもっと効率のいい方法があるんじゃないかと思うようになってきた。

変えたい部分は以下

  • 設定を柔軟にしすぎた。結局使い方は2パターンくらいになる。
    • 完全独自のURIにするか、HTMLファイル構造メインのURLにするか
    • hidden値の書き出しは使わない。フレームワークが自動で設定するよりHTMLで見れた方が何となく落ち着く。
    • 無駄にインターフェースが多い
      • Javaをやる前だったので設計が変
      • PHPはインターフェースのメリットがあまり無い
  • 最初にPHPの全ファイルを読み込む
    • 構文チェックや特定インターフェースの実装クラスをイテレーターで全て取得するときには使えるが、富豪的だし思ったほど利用しなかった
  • 配列からオブジェクト
    • PHP4の配列メインの構造からPHP5のオブジェクトメインの構造へ完全に変更できていない

で、結局これらを変更しようと思うと全体的な構成を変えないといけない。


もっと単純に、必要な機能だけを実装してシンプルでも使い勝手の良いものを作りたい。

フレームワークにやってほしいことって何だろうと改めて考えつつ書き出す。


  • URLマッピング
  • リクエスト変換
  • フォーム入力チェック
  • 任意のURLでグループ化可能な共通処理

この四つ、これだけあればいい。

それぞれ具体的に書くと

  • フロントコントローラーとURLマッピング
    • album/list、album/add のようなリクエストに対してアクションコントローラーを割り当てるアプリっぽい方式
    • album_list.html album_add.html のようなhtmlがまずあり、それにPHP処理を割り当てるHTMLメインの方式
    • 最終的なエラー・例外補足
  • リクエスト変換
  • フォーム入力チェック
    • 一般的なバリデーション
  • 共通処理
    • フィルターなどと呼ばれるもの
    • ログインやヘッダーフッターなどの共通処理をまとめる

このくらい。


おそらくURLマッピングとディレクトリ・ファイル構造がそのフレームワークの特徴の大部分を占めるのではないかと。

各種コンポーネントの取得方法や値の設定方法もそれぞれ特徴があるけれど、それは付随的な機能というかヘルパー的な、お助け機能。

まぁ大抵の場合はフレームワークの設定で任意のURLマッピングとディレクトリ構造にできるのだろうけれど。



それから必須のライブラリとして

がある。

これはTsukiyoYokazeまたはFlexyを多少変更して使おうかと。

あると嬉しいライブラリとしては

  • メール送信
  • ファイルアップロード
  • 権限管理
  • セッション管理
  • トランザクション管理
  • テスト補助

など。これらは既にあるものから選んで使うか100行くらいなら自分で書く。

多数のファイルを読み込んで様々な構成に対応するっていうのは無しの方向で。




最近のフレームワークってどうなってるんだろうと思って Zend Framework 2 のチュートリアルをやってみた。

http://framework.zend.com/manual/2.1/en/user-guide/skeleton-application.html

データベース1テーブルの操作するだけなのに、これは疲れる。

http://d.hatena.ne.jp/noopable/20121025

フレームワークの仕組みとしては、この辺を読むと使ってみたい気もする…。


http://www.nosenaoki.net/category/zf2/

こちらを読みつつ、使えるところを使いつつ薄いラッパーを書こうか考え中。

2011-10-13

[][] PHPの小規模用テンプレートエンジンを作った

今までPHPで何かを作るときはLaizを使っていたわけだけれども、レンタルサーバー的な構成だと使いづらい。

数ページのフォームとかだとPATH_INFOに対応するためにApache設定ファイルにAliasMatchを書いたりフレームワーク設定ファイルを書いたりするのが面倒になる。それにPHP5.3用に作っていたのでPHP5.2以下では動かない。

ちょっとしたフォームを作ときに、HTMLに直接PHPのコードを書くよりは少し便利な程度の単純なテンプレートエンジンが欲しくなった。


60行で作るPHP用テンプレートエンジンのようなものを作った。

最初は同じようにHTMLPHPを書くつもりでやっていたけど、オブジェクトを使うとエディタ上で -> が閉じタグに解釈されて色がバグったりする。なのでHTMLを解析するバージョンも作った。

それからLaizのデータベース接続部分のコードを単体でも使えるように移動した。


テンプレートエンジン+α

https://github.com/nishimura/Yokaze

ここに置いた。

Template.php は70行なのでここにも貼っとく。このファイルだけ単体でも使える。

<?php

class Yokaze_Template
{
    protected $templateDir = 'template';
    protected $cacheDir = 'cache';
    private $ext = 'html';
    private $vars;
    public function __construct($templateDir = null, $cacheDir = null)
    {
        if ($templateDir)
            $this->templateDir = $templateDir;
        if ($cacheDir)
            $this->cacheDir;
        $this->vars = new StdClass();
    }
    public function setExtension($ext)
    {
        $this->ext = $ext;
    }
    public function setVars($vars)
    {
        $this->vars = $vars;
    }
    public function show($vars = null)
    {
        if ($vars === null)
            $vars = $this->vars;
        $file = basename($_SERVER['SCRIPT_FILENAME'], '.php') . '.' . $this->ext;
        $tmplFile = $this->templateDir . '/' . $file;
        $cacheFile = $this->cacheDir . '/' . $file;

        $this->compile($tmplFile, $cacheFile);

        $this->showCache($cacheFile, $vars);
    }
    protected function compile($tmplFile, $cacheFile)
    {
        if (file_exists($cacheFile) && filemtime($tmplFile) <= filemtime($cacheFile)){
            return;
        }

        $tmpl = file_get_contents($tmplFile);

        // include feature
        $incPattern = '|{include:([[:alnum:]/]+\.html)}|';
        if (preg_match($incPattern, $tmpl)){
            $incReplace =
                '<?php $this->compile(\'' .
                $this->templateDir . '/$1\', \'' .
                $this->cacheDir . '/$1\'' . ');' .
                ' include \'' . $this->cacheDir . '/' . '$1\'; ?>';
            $tmpl = preg_replace($incPattern, $incReplace, $tmpl);
        }

        // simple variables
        $tmpl = preg_replace('/(\{[[:alnum:]]+)\.([[:alnum:]]+(:[a-z]+)?\})/', '$1->$2', $tmpl);
        $tmpl = preg_replace('/\{([[:alnum:]_>-]*):h\}/', '<?php echo $$1; ?>', $tmpl);
        $tmpl = preg_replace('/\{([[:alnum:]_>-]*):b\}/', '<?php echo nl2br(htmlspecialchars($$1)); ?>', $tmpl);
        $tmpl = preg_replace('/\{([[:alnum:]_>-]*)\}/', '<?php echo htmlspecialchars($$1); ?>', $tmpl);

        file_put_contents($cacheFile, $tmpl);
    }
    private function showCache($__cacheFile__, $__vars__)
    {
        foreach ($__vars__ as $k => $v)
            $$k = $v;
        include $__cacheFile__;
    }
}

特に目新しいことはしていない。Flexyに慣れているのでそんな感じに。

テンプレートに{var}と書けば htmlspecialchars($var)を表示、{var:h}と書けばそのまま表示、{var:b}と書けばnl2brを挟む。オブジェクトは{obj.prop}と書く。ただし多段{obj.obj.prop}には対応していない。正規表現がすぐ思い付かなかったので。

あと{include:header.html}などと書くとheader.htmlを読み込む。

ディレクトリの階層には非対応。というか階層分けが必要な規模なら別のちゃんとしたフレームワークを使うと思う。


使い方の例。

public_html/
    cache/
       .htaccess
    config/
       Yokaze/
       .htaccess
       config.php
    template/
       .htaccess
       form.html
       result.html
    form.php
    result.php

.htaccess にはアクセス拒否の設定を書く。またはcacheやtemplateディレクトリをpublic_htmlの外に出してもいい。


config.php

<?php
ini_set('display_errors', 'On');
error_reporting(E_ALL | E_STRICT);

require_once 'Yokaze/Template.php';
$t = new Yokaze_Template();

require_once 'Yokaze/Session.php';
$s = new Yokaze_Session();

require_once 'Yokaze/Request.php';
$r = new Yokaze_Request();
$r->message = $s->remove('message');

SessionやRequestは使わなくてもいいけど、使った方が簡単に書ける。


form.php

<?php
require_once 'config/config.php';

$r->subject = '';
$r->body = '';
$r->initPost();

function success(){
    global $r, $s;
    $s->set('message', '送信しました。');
    // ここでDB登録、メール送信など
    $r->redirect('result.php');
}
if ($r->isPost()){
    if (!$r->get('subject'))
        $r->message = 'タイトルを入力してください。';
    else if (!$r->get('body'))
        $r->message = '本文を入力してください。';
    else
        success();
}
$t->show($r);

$r->subject = ''; などが若干めんどくさいが、データベースを使う場合はより簡単になる。それは後述。

$r->initPost() は$_POSTをRequestの変数として展開する。$r->initPost($obj) と書くと $objの中に展開する。


form.html

<!doctype>
<html>
  <head>
    <meta charset="UTF-8">
    <title>フォーム</title>
  </head>
  <body>
    <div style="color:red;">{message}</div>
    <form action="form.php" method="POST">
      タイトル:<input type="text" name="subject" value="{subject}">
      <br>
      本文:<textarea name="body">{body}</textarea>
      <br>
      <input type="submit" value="送信">
    </form>
  </body>
</html>

result.php

<?php
require_once 'config/config.php';
$t->show($r);

特に何もしない。その場合でもphpは必須。

result.html

<!doctype>
<html>
  <head>
    <meta charset="UTF-8">
    <title>送信完了</title>
  </head>
  <body>
    <div style="color:red;">{message}</div>
  </body>
</html>

O/Rマッピングツール + α

O/RマッピングツールはLaizから移植した。

https://github.com/nishimura/YokazeDb

プライマリキーはテーブルごとに一つ必要。名前は何でも良い。joinには非対応。その場合はsqlファイルに書く。

PostgreSQLとSqliteに対応。Sqliteの動作確認はあまりしていない。テーブルのメタ情報を取得する関数を書けば他のDBでも対応可能。


2005年からつぎはぎしているのでコードは汚いがそれは置いておく。メソッドや引数も綺麗じゃないので気が向いたらs2jdbc風に書き直すつもり。

さきほどのフォームから送信されたデータをDBに登録するように変更する。


まずデータベースを以下のように作る。

config/tables.sql

create table form(
  form_id    serial primary key,
  subject    text not null,
  body       text not null,
  created_at timestamp not null
);
createdb yokaze_sample
psql -f config/tables.sql yokaze_sample

データベース用のコードを config.php に追記。

<?php
// ・・・省略
require_once 'YokazeDb/Factory.php';
$factory = new YokazeDb_Factory();
$dsn = 'pgsql:host=localhost;dbname=yokaze_sample;user=user;password=pass';
$factory->setDsn($dsn);

form.php を変更する。

<?php
require_once 'config/config.php';

$orm = $factory->create('form');
$form = $orm->createVo();
$r->initPost($form);
$r->form = $form;

function success(){
    global $r, $s, $form, $orm;
    if (!$orm->save($form)){
        $r->message = '送信に失敗しました。';
        return;
    }
    $s->set('message', '送信しました。');
    $r->redirect('result.php');
}
if ($r->isPost()){
    if (!$form->subject)
        $r->message = 'タイトルを入力してください。';
    else if (!$form->body)
        $r->message = '本文を入力してください。';
    else
        success();
}
$t->show($r);

$orm->createVo()はテーブル情報を読み取ってカラムに対応するフィールドを持ったクラスを生成する。

insert時にcreatedAtの値が自動挿入される。更新時はupdatedAtが自動更新される。


form.html の方は{subject}などを{form.subject}のように変更する。



登録されたデータを確認するページを作る。

list.php

<?php
require_once 'config/config.php';
$r->forms = $factory->create('forms')->setOptions(array('order' => 'createdAt'));

require_once 'config/YokazeDb/Pager.php';
$pager = new YokazeDb_Pager($r->forms, 10);
$r->pager = $pager->getHtml();
$t->show($r);

一覧表示用のイテレーター取得は1行で書ける。

表示を整形したい場合はSPLのIteratorIteratorを被せる。

このイテレーターはHTMLを表示してforeachに差し掛かったときに初めてデータベースにfetchしにいくので、HTMLに echo $PDOStatement->fetch()['name']; と書く場合に比べてそれほど遅くならないはず。


list.html

<!doctype>
<html>
  <head>
    <meta charset="UTF-8">
    <title>入力値一覧</title>
  </head>
  <body>
    <table>
      <tr>
        <th>タイトル</th>
        <th>登録時刻</th>
        <th>本文</th>
      </tr>
      <?php foreach ($forms as $form): ?>
      <tr>
        <td>{form.subject}</td>
        <td>{form.createdAt}</td>
        <td>{form.body:b}</td>
      </tr>
      <?php endforeach ?>
    </table>
    <div>
      {pager:h}
    </div>
  </body>
</html>

foreachはPHPで書く。

HTMLPHPでforeachを書くのが嫌だという場合は new Yokaze_Template() の代わりに new Yokaze_Parser() する。

そうすると

    <table>
      <tr>
        <th>タイトル</th>
        <th>登録時刻</th>
        <th>本文</th>
      </tr>
      <tr class="loop:forms:form">
        <td>{form.subject}</td>
        <td>{form.createdAt}</td>
        <td>{form.body:b}</td>
      </tr>
    </table>

classやstyle、その他適当な属性値にloopを書ける。



TODOなど

  • Request は $r->message = 'foo'; なのに、Session は $s->set('message', 'foo'); になっている。どちらかに統一する。
  • タグの属性にif文が書ける。書式は class="if:$var" や style="if:$iterator.count()" や foo="if:$item.status===1" など。ちょっと見にくいので書き方を考える。
  • メール送信用ユーティリティクラスを追加する。
    • エラー表示をオフにしてエラーをメールで送信するユーティリティ的関数を書く。
  • ORMを全体的に変更

2010-09-06

[][] バリデーターあれこれ

今更ながら。

バリデーターを書く場所について悩んでいる。


  1. バリデーター(or フィルター)
  2. アクション
  3. ビジネスロジック

という階層があるとき、ビジネスロジックで何らかのエラーがあった場合はエラー画面を出したい。



具体的には、

+----------+
|ユーザーID|
+----------+
| ・・・ |
+----------+

というテーブルがあったとき、ユーザー情報の操作画面ではフィルタなりバリデーターなりでセッション情報を確認して不備があればエラー画面を出す。

これはビジネスロジックじゃなくてもよい。セッションはユーザーIDに基づいているのでセッションが正常ならそのIDに基づいたユーザー情報の操作も正常だ。

ところが

+----------+
|ユーザーID|
+----------+
| ・・・ |
+----------+
     |
+----------+
|  住所ID  |
+----------+
|ユーザーID|
| ・・・ |
+----------+

こういうテーブルがあったとき、住所情報の操作画面では、フォームからの入力値として住所IDを受け取って何らかの処理をしたいのだが、一緒にユーザーIDのチェックもしないといけない。

ここで、住所情報の操作はビジネスロジックのクラスだとすると、ユーザーIDのチェックもビジネスロジックになる。あるいは、取得した住所情報をアクションに持ってきたときにチェックするか。


どちらにせよ、まともにリンクをたどった場合は起こり得ない状況がURL直打ちなどを考慮すると起こり得るので余計な(?)チェックが必要になる。


達成したい処理は住所情報に関する操作なのだがそこにユーザーセッションの処理が加わるのは何となくしっくりこない。

フォームからの入力チェックなのでアクションに移る前にバリデーターで処理したいところだが、住所情報を取得しないことにはユーザーIDと住所IDが正しいのかどうかが分からない。住所情報を取得するためにはビジネスロジックを通過する。


何度でもDBを見に行ってもいいとするならば、バリデーターでユーザーIDと住所IDの検証をすることもできるが、これは無駄すぎる気がする。



AOP的なことをしようかっていうのも考えたが…あまりいい案も浮かばず。



書きながら、Haskellのプログラムを思い出した。

Haskellではフォーム処理やDB処理などのIOを一カ所に集めないと面倒なことになる。

Haskell的考えでいくと、そもそもビジネスロジックからDBに接続にいくのがおかしいということになるのかな。

http://www.symfony-project.org/book/1_2/02-Exploring-Symfony-s-Code

だいたいはここのFigure 2-2のように考えていた。この考えでいくと、全てがIOになるんじゃないかな。まぁそれがPHP的と言えばPHP的だけれども。


で、自分がHaskellで作った小さなWebプログラムを見直してみる。

そこでは

type Action = Context -> IO Context
type ItemAction = (Context, Item) -> IO (Context, Item)

filterItem :: ItemAction -> Action
filterItem a = do
  ... -- DB接続してItemを取得、正常に取得できなければエラー用Actionを返し
  ... -- 取得できれば a を実行して結果からItemを除いて返却する。

modifyAndView :: Action
modifyAndView = filterItem (modifyItem >=> viewItem)

modifyItem :: ItemAction
modifyItem (c, i) = do
  ...
viewItem :: ItemAction
  ...

のように作っていた。

ここでは、Itemを操作するアクションはItemを受け取るようになっていて、何らかのエラーでItemが受け取れない場合はfilterItemによってエラー画面に遷移する。

つまりアクション実行時にはフォームやDBを含めた全てのデータが正常であることが保証されている。この構成だと本当に実装したい処理に集中できる。

ここで外部キーを使ってItemInItemテーブルを追加した場合はどうなるのだろう。たぶん

type ItemInItemAction :: (Context, ItemInItem) -> IO (Context, ItemInItem)
filterItemInItem :: ItemInItemAction -> Action
...
modifyItemInItem :: ItemInItemAction

というように作っていくのかな。filterItemInItem が ItemとItemInItemの外部キーをチェックするようにして。


まだPHPでどうするかは思い付かないが、何かこの辺にヒントがある気がする。

2010-04-22

[][] タイプヒンティングからオブジェクトの配列をインジェクションするためにネームスペースを使って動的にクラスを生成する

オートロード関数を登録する。

動的にネームスペースを生成してクラスを定義してオブジェクトを生成する。

<?php

namespace laiz\lib\aggregate;

use \laiz\autoloader\Register;
use \laiz\util\Inflector;

class Autoloader implements Register
{
    public function autoload($name)
    {
        $pattern = preg_quote(__NAMESPACE__, '/');
        if (!preg_match("/^$pattern/", $name))
            return;

        $pattern = preg_quote('\\');
        if (!preg_match("/(${pattern}[^$pattern]+)$/", $name, $matches))
            return;

        $className = $matches[1];

        $fullNamespace = str_replace($className, '', $name);
        $realNamespace = str_replace(__NAMESPACE__ , '', $fullNamespace);

        $interface = $realNamespace . Inflector::singularize($className);
        $interface = ltrim($interface, '\\');

        $className = ltrim($className, '\\');

        // fullNamespace: laiz\lib\aggregate\path\to
        // realNamespace: \path\to
        // interface    : path\to\Object
        // className:     Objects

        // debug
        //var_dump($fullNamespace, $realNamespace, $interface, $className);

        eval("
namespace $fullNamespace;
use \\ArrayObject;
use \\laiz\\builder\\Container;
class $className extends ArrayObject
{
    public function __construct(\$input = null, \$flag = 0, \$iterator = 'ArrayIterator')
    {
        if (\$input === null){
            \$input = Container::getInstance()->getComponents('$interface');
        }
        parent::__construct(\$input, \$flag, \$iterator);
    }
}
");
    }
}

Registerはオートロード関数を登録するためのインターフェースで、getComponents(interface)はインターフェースを実装しているクラスを全て読み込んで生成して配列で返すメソッド。


laiz\lib\aggregate; 以下のネームスペースを利用しようとすると、このオートロードが動く。

<?php
use \laiz\lib\aggregate\anyfeature\Filters;

$filters = new Filters();

とすると、anyfeature\Filter を実装したオブジェクトの配列(ArrayObject)を取得できる。



なんでこんなまどろっこしいことをやってるのかというと…

<?php
use \laiz\lib\aggregate\anyfeature\Filters;

class MyAction
{
    public function act(MyObject $obj, Filters $filters)
    {
        foreach ($filters as $filter){
            $filter->filter($obj)
        }
    }
}

これでタイプヒンティングからオブジェクトの配列をインジェクションできるようになった。

上のMyObjectは今までも自動生成していたが、配列の場合はタイプヒンティングがArrayになるから判別不能だった。

namespaceを使う前はやっつけで Array $Anyfeature_Filters などと書いて変数名から判別していたが、バックスラッシュが絡むとどうにもこうにも。


これ書きながら、わざわざnamespaceを隔離して新しい場所に作らない方法もあるなと思いつつ…とりあえずこれでしばらく使ってみる。




namespaceとかクロージャとかで検索したら、PHP5.3の発表前後のマニュアル的な記事が上位に上がってくる。

まだ普通に使われるところまでいってないのかな。