2012年11月5日月曜日

複雑なInteractionを関数で実装する

先週は、dci(data, context, and interaction)の、特にインターアクションという
言葉からの連想で15行程度の関数を書いた。

そこから、継続とかの難しい言葉を覚えて、更に書き換えていたのだが…
難しい。

ウェブサイトのページをコントロールするので、
ページの順番、入力のバリデーション、DB登録の制御、特にCSRF対策、などをきっちりと行おうと思うと、それなりに複雑になってしまった。

せいぜいif文で2レベルの深さにも関わらず、だ。

で、そのコード。
関数、ではなくて、あるクラス内のメソード。

entityというデータについて、
 ・form1:フォーム1の表示、load1:入力の読み取り、バリデーション、
 ・form2:フォーム2の表示、load2:入力の読み取り、バリデーション、
 ・confirm:確認画面の表示、
 ・save:そしてDBに登録する処理。

ただし確認画面の表示で、入力内容が気に入らなければ、フォーム1あるいは2を再表示して再入力して、入力に問題がなければ確認画面に戻る、という仕様を実装してみた。
(まだ実行してないので動くかどうかは知らない)
($viewやcontextとは何かとかも気にしない)


/**
 * @param string $control
 * @param view $view
 * @return \view
 */
function entityAdd( $control, $view )
{
    // get entity
    $entity = $this->restore( 'entity' );
    $state  = $this->getState();
    if( !$state ) {
        $entity = $this->contextGet( 'entity' );
        $this->register( 'entity', $entity );
        $this->setState( array( 'form1', 'form2', 'confirm', 'save', 'done' ) );
    }
    $role = $this->applyContext( $entity, 'loadable' );
    // form1
    if( $control == 'form1' || $state == 'form1' ) {
        $this->nextStateIf( 'form1' );
        return $view->showForm1( $entity );
    }
    // load1
    if( $control == 'load1' ) $role->load( 'load1' );
    
    if( !$role->verify( 'load1' ) ) return $view->showForm1( $entity );
    // form2
    if( $control == 'form2' || $state == 'form2' ) {
        $this->nextStateIf( 'form2' );
        return $view->showForm2( $entity );
    }
    // load2
    if( $control == 'load2' ) $role->load( 'load2' );
    
    if( !$role->verify( 'load2' ) ) return $view->showForm1( $entity );

    if( $control == 'save' && $state == 'confirm' ) $this->nextState();
    
    // confirm
    if( $state == 'confirm' ) {
        return $view->showConfirm( $entity );
    }
    
    // save
    if( $state == 'save' ) {
        $role = $this->applyContext( $entity, 'active' );
        $role->insert();
        $this->nextState();
    }
    // done
    return $view->showDone( $entity );
}

複雑な理由は、制御変数が2つもあるから。
ひとつは$state、セッションに保持していて「〜〜まで実行」を制御する。
一方は、$control、フォームからの入力値で「〜〜から実行」あるいは「〜〜を実行」を制御する。

ステートが出てくるので、ステートパターンの出番だと思うが、
最初の発想が、処理が複数のクラスの分割されてしまって理解が難しくなる、というOOの問題の提起があって、対処方法として関数で逐次処理するという提案だった。なので、複雑なのでOOでクラスに分割して・・・では解決にならないw

とはいえ、理解しやすいコードとは到底言えない。

2012年11月1日木曜日

PHPUnitからPostgreSQLを使ってテストしたら「FATAL: remaining connection slots are reserved...」

PHPUnitでPostgreSQLを使ったテストを書いてみた。
個々のテストは動くのだけど、テストを一つにまとめてSuiteにすると

FATAL:  remaining connection slots are reserved for non-replication superuser connections
とエラーが続出した。

Googleで調べてみると、Rails、Hibernateなどでも似たような症状が報告されていたけど、解決法が見つからない。

「max_connectionを増やす」と良い、
と書いてあったのを見かけたが、増やすと今度は別のエラーで通らない。というかPostgreSQLが起動しなかったので論外。

9.0から9.2系にアップグレードしたけど、効果なし。
phpPgAdminが未対応で、そこでまた修正が出てきたり。

しかたがないのでPHPUnitの書き方を変更して対応。
単に、setupでPdoを生成してたのを、setUpBeforeClass内で行うように変更した。staticメソードなので、ちょっと修正は必要だったが。

class TestUsingPgSql extends \PHPUnit_Framework_TestCase {
   function setUp() {
        $pdo = new \Pdo( 'some dsn' );
    }
}

class TestUsingPgSql extends \PHPUnit_Framework_TestCase
{
   static function setUpBeforeClass() {
        $pdo = new \Pdo( 'some dsn' );
    }
    public function setUp() {
    }
}

単独でもsuite内で走らせても、問題なし。

しかし、PostgreSQLを使ったテストは3ファイルのみ。
もっとテストが増えてきたら対応できるのだろうか?


2012年10月27日土曜日

MongoDBインストール(Mac OS X Homebrew)

homebrewでmongoDbインストールしてみた。
参考:http://www.milligramme.cc/wp/archives/2994



brew install mongodb
==> Downloading http://fastdl.mongodb.org/osx/mongodb-osx-x86_64-2.2.0.tgz
######################################################################## 100.0%
==> Caveats
To have launchd start mongodb at login:
    ln -s /usr/local/opt/mongodb/*.plist ~/Library/LaunchAgents/
Then to load mongodb now:
    launchctl load -w ~/Library/LaunchAgents/homebrew.mxcl.mongodb.plist
Or, if you don't want/need launchctl, you can just run:
    mongod
==> Summary
/usr/local/Cellar/mongodb/2.2.0-x86_64: 20 files, 170M, built in 15 seconds

と一発でインストール完了。
続いて、

mongod &
mongo

> j = { name: "mongo" };
{ "name" : "mongo" }
> db.thingy.save(j);
> db.thingy.find();
{ "_id" : ObjectId("508b83edb38f2b4b29878958"), "name" : "mongo" }



動いている。

次はPHPからMongoDBを動かしてみる。
参考:http://blog.ville.jp/2012/07/18/1058

sudo pecl install mongo
php.iniに次の項目を追加。

[mongodb]
extension=mongo.so

phpinfo()で確認。

ここまで簡単でした。
しかし、上の記事ではMongoDBが散々に書かれてますねw

DCIを調べてみて、MVCについて思ったこと

Data, Context, and Interaction (DCI)というのが流行っているらしい、と聞いて調べてみた。なにか「とてつもなく面白そう」なので、何か書いて見る。

全然理解はできていないので、あくまで連想したことです。

MVCは1リクエストに対して1リスポンス

自分の理解では、ウェブ・アプリケーションでのMVCとはリクエストに対してリスポンスを返すためのデザインパターンであること。

MVCについては、MOVEやらCが太るとか言われてますが、基本的にはhttpの仕組み上、この考え方は理にかなっていると思う。

ただ、人間が操作する場合を考えると、MVCだと何かが足りない気がする。

非常に単純なケースとして、新規データの登録を考えてみる。
 1)フォームを表示(form)、
 2)入力されたデータの確認画面(confirm)、
 3)データベースに登録(insert)、
という流れで処理が行われていると思う。
実際には、もうちょっと複雑なことが多い気がする。

さて、MVCだと、上の処理を3回に分けて記述する。
一つ一つの処理は理解しやすいのだけど、流れとして見た場合に処理のつながりが追いかけにくい。と思う。

Interactionをコードにする

DCIだと、どうなるのだろう?
こういった処理(インターアクション)をコードとして記述しましょう。
という事らしい。

早速擬似コーディング。
こんなかんじかな。

function add_entity( $action, $ctrl, $view )
{
    $role = $ctrl->getRole( 'entity' 
);
    if( $action == 'form' ) {
        return $view->showForm( $role );
    }
    $ok = $ctrl->verifyInput( $role );
    if( !$ok ) {
        return $view->showForm( $role );
    }
    if( $action == 'confirm' ) {

        $view->setToken( $ctrl->getToken() );
        return $view->showConfirm( $role );
    }
    if( $action == 'insert' && $ctrl->tokenOk() ) {
        $role->insert();
        $role->vanish();
    }
    return $view->showDone( $role );
}

$ctrlや$viewが何かというのは考えないで。

実際に操作する対象は「entity」。
それをRoleで包んでいる。この例だと、単にActiveRecord化してる。


さて、$actionは「form」「confirm」「insert」と進んでゆく。
$view->showForm()からは「confirm」をアクションとして、
$view->showConfirm()からは「insert」をアクションとして、
呼び出す。

そして、すべてのアクションで同じ処理(add_entity)が走る

どうだろう?
これだと何が起きているのか、分かりやすいと思う。
まぁ、ちゃんと動くのが前提ですが。

2012年10月10日水曜日

Interfaceを使ったDIの問題点

PHPのインターフェースを使って、簡単に依存注入するコードを考えました。
が、これぐらい誰かがすでにやっているはず。

と思って探してたら、見つけた。
Symfony2: Dependency Injection Types – Update

これによると、過去にSymfonyでインターフェースを使ったインジェクションがサポートされていたらしい。で、ここのスレッドでの話し合いの結果、問題があるので機能そのものを削除することになった。

何度も読んだが、今ひとつ理解し切れないのだけど、
おそらく、

  1. インターフェースインジェクションを持つクラスAを継承して別のクラスBを作成、
  2. 別クラスBに対して「自分でインジェクション」を行う、
  3. が、SymfonyのDIコンテナがクラスAとBを混乱、
  4. クラスBに対して再びインジェクション実行
  5. 最初に自分でインジェクトした結果が上書き、

という流れのよう。

ようするに、
インターフェースは継承したクラスにも含まれるので、
依存注入のコントロールが難しい、
ということだと思う。
正直に言えば、同じインターフェースで別の依存性を注入するのが問題な気がするが、できるのだから、できるようにしないといけない。

とにかく、見つけてられて、すっきりした。
インターフェースを使った依存性の注入はやめておこう。

2012年9月10日月曜日

PHPのinterfaeを使った簡単なDiContainerを考えた

性懲りもなく、こんなことを考えてます。

DIコンテナは便利なのですが、設定を作るのが面倒です。
クラスのコード内に、必要なオブジェクトを記述して、コンテナがよしなに注入してくれる方法が楽そうです。

BEAR.SundayのDIモジュールがまさにそうです。

ただアノテーションの処理にDoctrineが必要など、それはそれで面倒な感です。そこで、PHPのインターフェースを使った簡単Diコンテナについて考えてみました。

簡単な流れは:
  1. コンテナ内で、オブジェクトを生成。
  2. オブジェクトのインターフェースを検索。
  3. インターフェース名から注入するクラスを決定。
  4. オブジェクトを生成して、適当なメソードに注入。

具体例です。
namespace Database {
  interface InjectDbaInterface {}
  class Dba {...}
}
namespace Invoice {
  class Billing implements \Database\InjectDbaInterface {
    function injectDba( $Dba ) {
      $this->Dba = $Dba;
    }
  }
  $bill = $container( 'Billing' ); // 注入済みのオブジェクト
}
ポイントは:
  • 必要とされる(であろう)Dbaのクラスと同じnamespaceに、注入用のinterfaceを前もって作っておく。
  • インターフェース名は、\name\space\Inject{クラス名}Interface、と決めておくことで、注入するクラスとネームスペースを特定できる。
  • Invoiceのクラス宣言で、注入してほしいオブジェクトのinterfaceを実装(implements)する。
なお、コンテナ側のコードは、こんな感じ。
if( $interfaces = class_implements( $object ) )
foreach( $interfaces as $interface ) { // インターフェース一覧
  if( preg_match( '/^(.*)Inject([_a-zA-Z0-9]+)Interface$/i',
    $interface, $matches ) ) {
    $className = $matches[1] . $matches[2]; // 注入するクラス
    $injector  = "inject" . $matches[2]; // 注入方式は決め打ち
    $injObj = new $className;
    $object->$injector( $injObj );
  }
}
非常に簡単に動く(はず)。
【追記:2012年9月15日】
簡単なサンプルコードを書きました(https://gist.github.com/3725826)
確かに簡単に動きました。

インターフェースを使って依存性の自動解決をするコンテナがないか気になったのですが、ちょっと探したところでは見当たりません。見つけたのはアノテーションを使っているものばかりでした。(例:Seasarの自動バインディング

2012年8月12日日曜日

ssh_exchange_identification: Connection closed by remote host

今までのように、sshでサーバーに入ろうとしたら、怒られた。
ssh_exchange_identification: Connection closed by remote host

最近、何もいじくってないのに。
いや、MacBook Airから鍵認証で入れるようにしたが・・・
サーバー側はいじってないはず。

ググると、いろいろかかれてた。
ひとつずつ、対応してみた・・・

/etc/hosts.deny


まずは、一番怪しそうなこのファイル。
中身を見たら、いろんなサイトが追加されてた。

そういえばdenyhostsをインストールしていた。
アタックしてるサイトのIPアドレスが追加されてる。

えっ、こんなにアタックがあるんだ。
ポート変更しとこう・・・

ちなみに、自分のアクセス元はありませんでした。
これが原因ではない。

/etc/ssh/sshd_config


このなかの「MaxStartups」を設定しておくのがよいらしい。
見たら、コメントアウトされてたので、「#」マークを削除。

リスタートして・・・まだ動かない。

/etc/hosts.allow


まさかね。
これじゃないよね。でも何も書いてないよな。

このページを参考にして「sshd:all」と一行追加。


動いた。


こうなると、今まで動いていたのが不思議だが、

ま、少しセキュリティも向上して、よかった、よかった。