jQuery の Deferred と Promise/A+

jQuery の Deferred といわゆる Promise/A+ について、ごっちゃになってたのでメモ。

then/catch で発生した例外は reject された Promise になる

jQuery 3 から? Promise/A+ 互換になったため then/catch で発生した例外は reject された Promise として次のチェインに渡る。

$.Deferred().resolve(1).promise()
    .then((v) => {
        console.log('then', v); // then 1
        throw 2;
    })
    .catch((v) => {
        console.log('catch', v); // catch 2
    })

なので catch で処理しない例外は静かに無視される。

$.Deferred().resolve(1).promise()
    .then((v) => {
        console.log('then', v); // then 1
        throw 2;
    })
    .then((v) => {
        console.log('then', v); // never
    })

Node.js の Promise ならそういう状況では以下のような警告が表示される。

(node:87126) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): 2
(node:87126) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

そうではない処理系のために Promise のライブラリによっては Promise#done というメソッドが設けられていることがあるらしい。

要するにチェインの最後で .done() しておけばキャッチされない例外は Promise の外まで飛んで行くようになる。

一方で jQuery の done/fail/always で例外が発生した場合はそのまま Promise の外まで飛んで行く。done の動きは↑の説明とぜんぜん違うので注意(done は Promise/A+ の仕様ではないので jQuery が特別おかしいわけではない)。

$.Deferred().resolve(1).promise()
    .done((v) => {
        console.log('done', v); // done 1
        throw 2;                // uncaught exception: 2
    })
    .fail((v) => {
        console.log('fail', v); // never
    })
    .always((v) => {
        console.log('always', v); // never
    })

jQuery で then しつつ catch されない例外を Promise の外に持っていきたければチェインの最後で次のようにすると良いだろう。

$.Deferred().resolve(1).promise()
    .then((v) => {
        console.log('then', v); // then 1
        throw 2;
    })
    .then((v) => {
        console.log('then', v); // never
    })
    .fail((err) => {
        throw err;              // uncaught exception: 2
    })

jQuery の done/fail/always の戻り値は Promise の状態を変えない

thencatch はコールバック関数が返した値で解決された新たな Promise を返す(コールバックが Promise を返したならその Promise の状態と値を持つ新たな Promise が返る)。

$.Deferred().reject(1).promise()
    .catch((v) => {
        console.log('catch', v); // catch 1
        return 2;
    })
    .then((v) => {
        console.log('then', v); // then 2
        return 3;
    })
    .then((v) => {
        console.log('then', v); // then 3
        return $.Deferred().reject(4).promise();
    })
    .catch((v) => {
        console.log('catch', v); // catch 4
    })

一方で done/fail/always の戻り値は元の Promise のまま。

var promise = $.Deferred().resolve(1).promise();
console.log(promise === promise.done(() => {})); // true
console.log(promise === promise.then(() => {})); // false

そのためコールバックが何を返してもチェインされる Promise の状態や値は変わらない。

$.Deferred().reject(1).promise()
    .always((v) => {
        console.log('always', v); // always 1
        return 0;
    })
    .fail((v) => {
        console.log('fail', v); // fail 1
        return 2;
    })
    .done((v) => {
        console.log('done', v); // never
        return 3;
    })
    .done((v) => {
        console.log('done', v); // never
        return $.Deferred().reject(4).promise();
    })
    .fail((v) => {
        console.log('fail', v); // fail 1
    })

jQuery の Promise は複数の値を持てる

本来 Promise/A+ では1つの値しか持てない。

Promise.resolve(1, 2, 3)
    .then((a, b, c) => {
        console.log('then', a, b, c); // then 1 undefined undefined
    })

jQuery の Promise は複数の値を持てる。

$.Deferred().resolve(1, 2, 3).promise()
    .then((a, b, c)=>{
        console.log('then', a, b, c); // then 1 2 3
    })

thencatch をチェインするときに次の Promise に複数の値を持たせたいときは複数の値を持つ jQuery の Promise を返せば良い。

$.Deferred().resolve(1, 2).promise()
    .then((a, b)=>{
        console.log(a, b); // 1 2
    })
    .then((a, b)=>{
        console.log(a, b); // undefined undefined
        return $.Deferred().resolve(3, 4)
    })
    .then((a, b)=>{
        console.log(a, b); // [3, 4]
    })

さいごに

昨今はバベれば async/await が非常に書きやすいので、jQuery 独特の仕様に依存しないよう $.ajax をラップした関数で Promise が1つの値だけを持つようにして、async/await 前提で書くのが良いと思う。

function ajaxGet(url, params) {

    return $.ajax(/* ... */)
        .then(
            (data, textStatus, jqXHR) => {

                // data を見て(必要なら jqXHR とかも)リクエストの成否を判断する

                if (err) {
                    // リクエストが失敗しているなら失敗の理由を示すなにかを例外として投げる
                    throw err;

                    // こっちのほうが良いかも
                    return Promise.reject(err);
                }

                // リクエストが成功したならその結果を返す
                return data;
            },
            (jqXHR, textStatus, errorThrown) => {
                
                // リクエストの失敗を示すなにかを例外として投げる
                throw err;
            }
        )
}

async function handler() {

    try {
        const data = await ajaxGet('/path/to/api', {});

        // .done() の処理

    } catch (err) {

        // .fail() の処理

        // 握りつぶさないように再送
        throw err;

    } finally {

        // .always() の処理 
    }
}

バベれないときは thencatch で発生した例外が闇の彼方に葬られないように気をつける必要がある。 ↑の方で書いたように thencatch の後で fail で例外を throw するか、catchsetTimeout(function () { throw err }, 0) とかだろうか。

Node.js で jQuery を使う

jQuery の DOM とは関係のない Deferred の動きを確認するために jQuery を Node.js で動かしたかったのだけど、下記によるとそういう場合でも window オブジェクトが必要とのこと。

ただ、この通りにやってもなんか動きませんでした。

下記のどちらかで動かせられました。

jsdom

yarn add jquery jsdom
const { JSDOM } = require('jsdom')
const $ = require('jquery')(new JSDOM().window)

$.Deferred().resolve(1).promise()
    .then((v) => { console.log(v) })

jsdom-no-contextify

yarn add jquery jsdom-no-contextify
const $ = require('jquery')(require('jsdom-no-contextify').jsdom().parentWindow)

$.Deferred().resolve(1).promise()
    .then((v) => { console.log(v) })

jQuery で data アトリビュートから文字列を取り出すときは .attr() を使うべき

PHP から JS に値を渡したいとき、PHP から適当な要素の data アトリビュートに書き出して jQuery の .data() メソッドで取り出していたんですけど・・・

http://api.jquery.com/data/ ... When the data attribute is an object (starts with '{') or array (starts with '[') then jQuery.parseJSON is used to parse the string; it must follow valid JSON syntax including quoted property names. If the value isn't parseable as a JavaScript value, it is left as a string. ...

なんと .data() メソッドで data アトリビュートから値が取られるとき、値が JSON としてパースできるならパースした結果が返り、できなければ属性値がそのまま文字列として返る仕様でした。

ので、PHP から JS に文字列を渡したいときに jQuery の .data() メソッドで取っていると、文字列がたまたま JSON っぽかったときに文字列ではなく配列やオブジェクトになってしまうことがあります。

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
    <title></title>
    <script src="//code.jquery.com/jquery-3.2.1.min.js"></script>
</head>
<body>
    <script id="data" data-aaa="[1, 2, 3]" data-bbb="[1, 2, 3]x">
        console.log($('#data').data('aaa'));      // Array [ 1, 2, 3 ]
        console.log($('#data').data('bbb'));      // "[1, 2, 3]x"
        console.log($('#data').attr('data-aaa')); // "[1, 2, 3]"
        console.log($('#data').attr('data-bbb')); // "[1, 2, 3]x"
    </script>
</body>
</html>

jQuery の .data() は元々は HTML の data アトリビュートから値を取るためのものではなく jQuery で独自に要素とデータを関連付けるためのもので(要素のプロパティとして直接データを持たせることによる DOM と JS オブジェクトを跨る循環参照よるメモリリークの問題を解消するため?)、data アトリビュートからも値が取れるのは後付けの機能だったと思うので、単に data アトリビュートから文字列を取りたいときは .data() ではなく .attr() を使うべき、ということですね。

ちなみにこんな感じに判定されているようです。

配列やオブジェクトなら PHP から json_encode した値を書き出して jQuery で .data() を使えば受け渡しが簡単にできて便利?なのかな??

超長い文字列や配列でも実体が同じなら中身の大きさには依存しない

手元にあった PHP 7.1.10 で試してます。


PHP の配列で、連想配列なのかいわゆる添字が順序通りのただの配列なのかを調べるには、下記の方法が一番手っ取り早いと思います。

array_values($arr) === $arr;

がしかし、配列の === は配列の要素の値の比較も行われるため、これだと配列の中に超でかい文字列や配列が入っているときに超遅くなります。

比較する必要があるのはキーだけなはずなのに配列の要素の値に依存して実行時間が変わるのはイケていないので array_keys でキーだけ取り出して比較したり愚直に foreach で回す方が良いですね・・・そんなふうに考えていた時期が俺にもありました。

いろんなパターンでベンチして最適な方法さがすぞー!と意気込んだところ↑のパターンが概ね良好でした(要素数が少ないときや早期にミスマッチが判断できるときは foreach のが早いこともある)。

<?php
namespace array_values {
    function isSequence($arr)
    {
        return $arr === array_values($arr);
    }
    $namespaces[] = __NAMESPACE__;
}

namespace array_keys {
    function isSequence($arr)
    {
        return array_keys($arr) === array_keys(array_keys($arr));
    }
    $namespaces[] = __NAMESPACE__;
}

namespace range {
    function isSequence($arr)
    {
        if (count($arr) === 0) {
            return true;
        } else {
            return array_keys($arr) === range(0, count($arr) - 1);
        }
    }
    $namespaces[] = __NAMESPACE__;
}

namespace foreach_ {
    function isSequence($arr)
    {
        $j = 0;
        foreach ($arr as $i => $v) {
            if ($i !== $j++) {
                return false;
            }
        }
        return true;
    }
    $namespaces[] = __NAMESPACE__;
}

namespace {
    function test($ns)
    {
        $func = "$ns\\isSequence";

        assert($func([]) === true, "$func([]) === true");
        assert($func([2, 4, 6]) === true, "$func([2, 4, 6]) === true");
        assert($func([2, 4, 'x' => 6]) === false, "$func([2, 4, 'x' => 6]) === false");
        assert($func([2 => 2, 1 => 4, 0 => 6]) === false, "$func([2 => 2, 1 => 4, 0 => 6]) === false");
    }

    $data = [
        '[]' => [],
        '[2,4,6]' => [2, 4, 6],
        '[x:9]' => ['x' => 9],
        '[1:1,0:0]' => [1 => 1, 0 => 0],
        '[0..10000]' => range(0, 10000),
        '{0..10000, x:x}' => range(0, 10000) + ['x' => 'x'],
        '[0, 0..100000]' => [0, range(0, 100000)],
        '{x:x, z: 0..100000}' => ['x' => 'x', 'z' => range(0, 100000)],
        '[0, x..x]' => [0, str_repeat("x", 100000)],
        '{x:x, z: x..x}' => ['x' => 'x', 'z' => str_repeat("x", 100000)],
    ];

    foreach ($namespaces as $ns) {
        test($ns);
    }

    foreach ($namespaces as $ns) {
        $results = [];
        foreach ($data as $name => $arr) {
            $func = "$ns\\isSequence";
            for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
                $func($arr);
            }
            printf("%-16s%-24s%d\n", $ns, $name, $i);
            $results[] = $i;
        }
        printf("%-16s%-24s%d ... %d\n", $ns, "", min($results), max($results));
    }
}
array_values    []                      3947641
array_values    [2,4,6]                 3383804
array_values    [x:9]                   3956602
array_values    [1:1,0:0]               3893512
array_values    [0..10000]              5912
array_values    {0..10000, x:x}         6079
array_values    [0, 0..100000]          3246093
array_values    {x:x, z: 0..100000}     3820617
array_values    [0, x..x]               3392908
array_values    {x:x, z: x..x}          3945126
array_values                            5912 ... 3956602
array_keys      []                      3104513
array_keys      [2,4,6]                 2405964
array_keys      [x:9]                   2596304
array_keys      [1:1,0:0]               2524770
array_keys      [0..10000]              3455
array_keys      {0..10000, x:x}         3429
array_keys      [0, 0..100000]          2456517
array_keys      {x:x, z: 0..100000}     2541887
array_keys      [0, x..x]               2523583
array_keys      {x:x, z: x..x}          2530141
array_keys                              3429 ... 3104513
range           []                      5123887
range           [2,4,6]                 1966400
range           [x:9]                   2065069
range           [1:1,0:0]               2093647
range           [0..10000]              4480
range           {0..10000, x:x}         4243
range           [0, 0..100000]          1963215
range           {x:x, z: 0..100000}     2149248
range           [0, x..x]               2077303
range           {x:x, z: x..x}          2039283
range                                   4243 ... 5123887
foreach_        []                      5173692
foreach_        [2,4,6]                 3569323
foreach_        [x:9]                   4653941
foreach_        [1:1,0:0]               4681362
foreach_        [0..10000]              4177
foreach_        {0..10000, x:x}         4102
foreach_        [0, 0..100000]          4032628
foreach_        {x:x, z: 0..100000}     4661640
foreach_        [0, x..x]               4007409
foreach_        {x:x, z: x..x}          4776680

実体が同じなら文字列や配列の中身は比較されない

ということのようです。

<?php
function func($a, $b, $name)
{
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $a === $b;
    }
    printf("%10d ... %s\n", $i, $name);
}

$a = str_repeat('x', 1024*1024*10);
$b = $a;
func($a, $b, 'same huge str');

$a = str_repeat('x', 1024*1024*10);
$b = str_repeat('x', 1024*1024*10);
func($a, $b, 'diff huge str');

$a = 'x';
$b = 'x';
func($a, $b, 'diff tiny str');
  11970864 ... same huge str
       782 ... diff huge str
  11892552 ... diff tiny str

最初のパターンはちょうでかい文字列ですけど実体が同じ変数の比較なので1文字の文字列の比較と差がありません。 一方で2番目のパターンは同じ文字列ではあるものの実体が異なるので超遅いです。

配列でも同様です。

<?php
function func($a, $b, $name)
{
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $a === $b;
    }
    printf("%10d ... %s\n", $i, $name);
}

$a = range(1, 1000000);
$b = $a;
func($a, $b, 'same huge arr');

$a = range(1, 1000000);
$b = range(1, 1000000);
func($a, $b, 'diff huge arr');

$a = [];
$b = [];
func($a, $b, 'diff tiny arr');
  11500321 ... same huge arr
        82 ... diff huge arr
  11197598 ... diff tiny arr

PHP の変数は Copy on Write になっており単に = でコピーしただけなら実際に確保されている文字列や配列のためのメモリ領域は共有されており、なんらかの変更を行ったときに実際の領域がコピーされます。

そして、2つの変数を比較するとき、変数の中身が同じ実体で同じ領域を指しているのであれば、中身を比較するまでもなく「一致」と判断することができるのでしょう(ソースは見ていないけどたぶん)。

配列のキーも文字列のサイズには依存しない

PHP の配列はハッシュテーブルなので文字列をキーにする場合は文字列を元にハッシュ値を計算する必要があります。なので、あまりに巨大な文字列をキーにすると計算のコストが増大して性能が劣化する・・・そんなふうに考えていた時期が俺にもありました。

<?php
function func($arr, $key, $name)
{
    $arr[$key] = 1;
    for ($i=0, $t=microtime(true)+1; microtime(true)<$t; $i++) {
        $v = $arr[$key];
    }
    printf("%10d ... %s\n", $i, $name);
}

$key = str_repeat('x', 1024*1024*10);
$arr[$key] = 1;
func($arr, $key, 'same huge str');

$key = str_repeat('x', 1024*1024*10);
$arr[$key . ''] = 1;
func($arr, $key, 'diff huge str');

$key = 'x';
$arr[$key . ''] = 1;
func($arr, $key, 'diff tiny str');
  11339929 ... same huge str
       781 ... diff huge str
  11241169 ... diff tiny str

最初のパターンは超でかい文字列をキーにしているのでハッシュ値の計算のために性能が劣化しそうなものですが、キーが短いときと代わりありません。

文字列自体が配列のキーとして使うときのためのハッシュ値を持っているとでもいうの・・・

zend_stringに関するメモ - Qiita

struct _zend_string {
        zend_refcounted_h gc;
        zend_ulong        h;                /* hash value */
        size_t            len;
        char              val[1];
};

あ、持ってるっぽい、なるほど。

PHP 5.4.16

まだまだ現役?? の PHP 5.4.16 で試してみました。

# str.php
      1801 ... same huge str
       816 ... diff huge str
   5962900 ... diff tiny str

# arr.php
   5819125 ... same huge arr
        47 ... diff huge arr
   5843144 ... diff tiny arr

# key.php
        85 ... same huge str
        84 ... diff huge str
   6210093 ... diff tiny str

配列の比較だけは PHP 7 と同じような傾向ですが、文字列の比較と配列のキーはご覧の有様でした。

さいごに

PHP 7 なめてた。

例えば PSR-7 を実装したリクエストオブジェクトのアトリビュートに何か入れるとき、名前の競合を避けるためにクラス名を使いたかったりすることあるんですけど、

$request = $request->withAttribute(HogeAttr::class, new HogeAttr($hoge));

今日日の PHP は名前空間が付いていてクラス名の完全修飾名は長くなりがちなので、リクエストオブジェクトの中でアトリビュート名が配列のキーとして持たれることを考えるとクラス名よりも短い文字列の定数とかを別に用意するのが良いだろうかと思ったのだけど・・・

$request = $request->withAttribute(HogeAttr::NAME, new HogeAttr($hoge));
// or
$request = $request->withAttribute('hoge', new HogeAttr($hoge));

そんなこと考える必要は無いということですね。むしろクラスがロードされた時点で存在していることが明らかであるクラス名の文字列の方が好ましいでしょう(::class が使えるならですけど)。

DI コンテナのオブジェクトの ID も同じです。PSR-11 で ID にはクラス名やインタフェース名を使うのを奨励、みたいなことが書かれてた気がしますが、PHP 7 ならクラス名やインタフェース名が超長くてもノーコストなのですね(コンテナの実装に依る)。

DIコンテナ使ってみて思った雑文:その2

アプリケーションにテーブルゲートウェイがあったとして、もちろんDB接続に依存している。

class UserTable
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    // select/insert/update/delete/etc...
}

テーブルゲートウェイはテーブルの数だけ存在するので、全部に同じ実装を書くのはDRYじゃないので抽象クラスを作る。

abstract class AbstractTableGateway
{
    private $connection;

    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }

    // select/insert/update/delete/etc...
}

class UserTable extends AbstractTableGateway
{
    // UserTable spesific method...
}

特定のテーブルで Connection 以外のなにかに依存する場合、コンストラクタを拡張して DI する。

class HogeTable extends AbstractTableGateway
{
    private $hoge;

    public function __construct(Connection $connection, Hoge $hoge)
    {
        parent::__construct($connection);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

おっと、ここでテーブルのメタデータを保存するために AbstractTableGateway でキャッシュを使いたくなった。

abstract class AbstractTableGateway
{
    private $connection;
    private $cache;

    public function __construct(Connection $connection, Cache $cache)
    {
        $this->connection = $connection;
        $this->cache = $cache;
    }

    // select/insert/update/delete/etc...
}

すると HogeTable のコンストラクタも弄らなければならない。

class HogeTable extends AbstractTableGateway
{
    private $hoge;

    public function __construct(Connection $connection, Cache $cache, Hoge $hoge)
    {
        parent::__construct($connection, $cache);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

HogeTable 的には「キャッシュとか知らんがなそっちで勝手にやってくれ」なので HogeTable のコンストラクタにまで影響するのは違和感がある。

これはまあ Connection と Cache を併せたなにかを作るか(ConnectionFacade とか)、あるいは、Connection に Cache のインスタンスを持たせればスッキリする。

あるいは AbstractTableGateway のような抽象クラスを作って継承するのはやめて、委譲とかトレイトだけでどうにかするべきだろうか。

trait TableGatewayTrait
{
    abstract protected function getTableGateway();

    // select/insert/update/delete/etc...
}

class UserTable
{
    use TableGatewayTrait;

    private $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // UserTable spesific method...
}

class HogeTable
{
    use TableGatewayTrait;

    private $tableGateway;
    private $hoge;

    public function __construct(TableGateway $tableGateway, Hoge $hoge)
    {
        $this->tableGateway = $tableGateway;
        $this->hoge = $hoge;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // HogeTable spesific method...
}

コンストラクタとかで定形パターンをなんども書くのが辛ければそれらもトレイトに含めて、必要に応じてコンストラクタをオーバーライドするとか。

trait TableGatewayTrait
{
    private $tableGateway;

    public function __construct(TableGateway $tableGateway)
    {
        $this->tableGateway = $tableGateway;
    }

    protected function getTableGateway()
    {
        return $this->tableGateway;
    }

    // select/insert/update/delete/etc...
}

class UserTable
{
    use TableGatewayTrait;

    // UserTable spesific method...
}

class HogeTable
{
    use TableGatewayTrait { __construct as constructTableGateway; }

    private $hoge;

    public function __construct(TableGateway $tableGateway, Hoge $hoge)
    {
        $this->constructTableGateway($tableGateway);
        $this->hoge = $hoge;
    }

    // HogeTable spesific method...
}

コンストラクタを as で別名にするのはさすがに変か。。。


コンストラクタインジェクション前提で考えると、継承元のコンストラクタを弄ったときに派生先のすべてに影響してしまうので継承させにくいなーと思ったけどよく考えたら DI とか関係なく継承してれば当たり前のことだった。

Terraform v0.10 でいろいろ変わってた

久しぶりに Terraform を使ってたらちょこちょこ変わってたのでメモ。多分前は v0.8 とかを使ってたと思う。

Initialization

以前は terraform をインストールすると AWS とかのプロバイダのプラグインも一緒に入っていて、すぐに使えていたと思うんですが、いつからか terraform init コマンドでプロバイダのプラグインがインストールされるようになったようです。

例えば次のように AWS のリソースを tf ファイルに書いておくと。

resource "aws_vpc" "test" {
    cidr_block = "10.1.0.0/16"
    tags { Name = "test" }
}

terraform init.terraform/plugins/darwin_amd64/terraform-provider-aws_v0.1.4_x4 のような位置に AWS プロバイダのためのプラグインがインストールされます。

$ terraform init

... snip ...

* provider.aws: version = "~> 0.1"

... snip ...

$ ls .terraform/plugins/darwin_amd64/terraform-provider-aws_v0.1.4_x4
.terraform/plugins/darwin_amd64/terraform-provider-aws_v0.1.4_x4

後はこれまで通り plan とか apply とかできます。

ちなみにこのファイルはいわゆる実行ファイルなので、実行属性を落とすと動かなくなります。

Mac とかなら大丈夫なんですけど・・・普段 Windows を cifs マウントしている Linux 上で作業している自分にはその仕様はちょっとツライ・・・ mound --bind とかでどうにか乗り切ってますけど。

Remote Backends

Remote Backend の設定方法も以前から変わっているようで、以前のまま使うと「なんか古いから新しくしいや」みたいなことを言われた気がします。

以前は terrarofm remote みたいなコマンドでバックエンドを設定する必要があった気がするのですが、今は次のように tf ファイルにバックエンドの情報を書いて、

terraform {
    backend "s3" {
        bucket = "oreore-tfstate"
        key    = "oreore.tfstate"
    }
}

terraform init で設定出来るようです。

複数の作業者で共有すること考えたらこの方が良いですね。

バックエンドを変更したりローカルに戻したりも tf ファイルを修正して terraform init で良いようです。楽ちん。

DIコンテナ使ってみて思った雑文

アプリケーションの流れとして、

  1. コンテナを構築
  2. アプリを実行するなにか(パイプラインとかディスパッチャとか)をコンテナから取り出す
  3. リクエストオブジェクトを作成してアプリを実行

みたいな流れかなと思う。順番的にDIコンテナが先に作られるのでリクエストオブジェクトやリクエストに含まれる情報はDIコンテナには入らない。そもそもDIコンテナはグローバルなコンテキストのものなので、リクエストのコンテキストに属するオブジェクトを入れるべきではない。

(PHPでは出来ないけれども)もし可能なのならDIコンテナはリクエストをまたがって共有されても良い。なのでコンテナに入るオブジェクトはステートレスでなければならないし、再入可能でスレッドセーフであるべき(PHPでは関係ないけれども)。

・・・みたいなイメージ。


例えば Logger にリクエストに基づく情報が自動的に付加されるようにしてみる(IPアドレスとか)。

$logger = $logger->withContext(['ipaddr' => $ipaddr]);

// $context に 'ipaddr' が追加される
$logger->info('ほげほげ');

こうして作られた Logger はリクエストのコンテキストに属するので、グローバルなコンテキストであるところのコンテナには入らない。

サービスクラスがコンテナに入っているのだとすると、サービスクラスのコンストラクタにインジェクションできるものはコンテナに入っているものだけなので、↑のよう部分的に適用された Logger をインジェクションすることはできない。

大本の Logger がインジェクションされて、そしてサービスのメソッドにリクエストオブジェクトが渡されるようにする?(コードはイメージです)

class HogeService
{
    public function __construct($logger)
    {
        $this->logger = $logger
    }

    public function func($request)
    {
        $logger = $this->logger->withContext(['ipaddr' => $request->getRemoteAddr()]);

        // $logger を使ってログを出す
    }
}

ちがう、サービスがリクエストに依存しているわけではないので ipaddr が適用された Logger を渡すべき。

class HogeService
{
    public function func($logger)
    {
        // $logger には 'ipaddr' が適用されているけどサービスはそんなこと知らねえ
    }
}

ただ $logger をずっと引数として持ち回さなければならない。引数が Logger しかないなら良いけれども、他にもリクエストのコンテキストに属するオブジェクトをたくさん持ち回したくなったときにツラミが増してくる(Logger 以外なにがあるかぱっと思いつかないけど)。

ので、それを見越して、リクエストのコンテキスト、というオブジェクトが渡されるようにしてみる。

class HogeService
{
    public function func($requestContext)
    {
        $logger = $requestContext->getLogger();
    }
}

うーん悪手? サービスが RequestContext というよくわからないものに依存している。


Logger がコンストラクタからシュッと入ってきてそれをそのまま使えられれば良いのだけど、コンテナにリクエストに基づくモノが入っていないとすると引数で持ち回すしか無い?

あるいはコンテナにリクエストオブジェクトをぶっこんでも良いことにする?

// bootstrap.php っぽいなにか
$builder = new ContainerBuilder();
$builder->addDefinitions($definitions + [
    ServerRequestInterface::class => ServerRequestFactory::fromGlobals(),
]);

$container = $builder->build();

// コンテナにリクエストオブジェクトが入っているので logger に ipaddr が適用されている
$logger = $container->get(Logger::class);

でも PSR-7 だとミドルウェアパイプラインの中でリクエストオブジェクトがクローンされて次々に生成されるわけで、↑だとコンテナに入ったリクエストオブジェクトとコントローラーのアクションに渡ってくるリクエストオブジェクトが異なるものになってしまって違和感。

例えば X-Forwarded-For を見てリクエストが持つ RemoteAddr を本来のものに書き換えるようなミドルウェアを考えると問題がわかりやすい。

それか Logger をミュータブルにしてそういうミドルウェアが Logger に IP アドレスをセットする?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $this->logger->addContext(['ipaddr' => $request->getRemoteAddr()]);
        return $delegate->process($request);
    }

うーん、どんどんゆるふわになっていく・・


コントローラーをディスパッチするときに元のコンテナを拡張した新たなコンテナを作成して、コントローラーの依存はその新たなコンテナによって解決する?

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $container = $this->extend($this->container, [
            LoggerInterface::class => function () use ($request) {
                return (new Logger())->withContext([
                    'ipaddr' => $request->getRemoteAddr(),
                ]);
            }
        ]);

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

リクエストのアトリビュートを全部ぶっこむとかでも。

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $request = $request->withAttribute(
            LoggerInterface::class, (new Logger())->withContext([
                'ipaddr' => $request->getRemoteAddr(),
            ])
        );

        $container = $this->extend($this->container, $request->getAttributes());

        // コントローラーとコントローラーが依存するオブジェクトは新たなコンテナで依存解決される
        $result = $request->getAttribute(RouteResult::class);
        $instance = $container->get($result->getController());
    }

とくに結論はない。