php で配列やオブジェクトをアクセスキーにする

# php で配列やオブジェクトをアクセスキーにする

[ArrayAccess](http://php.net/manual/ja/class.arrayaccess.php) を読みつつ色々試していて気づいたんですが、どうも `$offset` 引数があらゆる型を受け入れてくれるみたいです。
それでふと思い至って下記のクラスを作ってみました。

```php

class Map implements \ArrayAccess, \Iterator, \Countable
{
    private $keys = [];

    private $vals = [];

    private $position = 0;

    private function getKey($offset)
    {
        foreach ($this->keys as $n => $key) {
            // ここを書き換えれば同値判定を差し替えられる
            if ($key === $offset) {
                return $n;
            }
        }
        return null;
    }

    public function offsetExists($offset)
    {
        $key = $this->getKey($offset);
        return $key !== null;
    }

    public function offsetGet($offset)
    {
        $key = $this->getKey($offset);
        if ($key !== null) {
            return $this->vals[$key];
        }
        trigger_error("Undefined index: $offset");
    }

    public function offsetSet($offset, $value)
    {
        // 追加時の null もキー扱いとする
        $key = $this->getKey($offset);
        if ($key === null) {
            $this->keys[] = $offset;
            $this->vals[] = $value;
        }
        else {
            $this->keys[$key] = $offset;
            $this->vals[$key] = $value;
        }
    }

    public function offsetUnset($offset)
    {
        $key = $this->getKey($offset);
        if ($key !== null) {
            unset($this->keys[$key]);
            unset($this->vals[$key]);
        }
    }

    public function current()
    {
        return $this->vals[$this->position];
    }

    public function next()
    {
        $this->position++;
    }

    public function key()
    {
        return $this->keys[$this->position];
    }

    public function valid()
    {
        return isset($this->keys[$this->position]);
    }

    public function rewind()
    {
        $this->position = 0;
        // unset すると歯抜けができるはずなので連番振り直し
        $this->keys = array_values($this->keys);
        $this->vals = array_values($this->vals);
    }

    public function generate()
    {
        foreach ($this->keys as $n => $key) {
            yield $key => $this->vals[$n];
        }
    }

    public function count()
    {
        return count($this->keys);
    }
}
```

試してみたら動いてしまいました。
下記のように使います。

```php
$map = new Map();

echo "# ACCESS\n";

// 配列をキーにできる
$key = ['akey'];
$map[$key] = 'hoge';
printf("array key value: %s\n", $map[$key]);

// オブジェクトをキーにできる
$key = new stdClass();
$map[$key] = 'fuga';
printf("object key value: %s\n", $map[$key]);

// 文字列をキーにできる
$key = 'key';
$map[$key] = 'piyo';
printf("string key value: %s\n", $map[$key]);

echo "\n# FOREACH\n";

// foreach で回せる
foreach ($map as $k => $v) {
    printf("key type: %s=%s\n", gettype($k), json_encode($k));
    printf("val: %s\n", $v);
}

echo "\n# GENERATOR\n";

// generator で回せる
foreach ($map->generate() as $k => $v) {
    printf("key type: %s=%s\n", gettype($k), json_encode($k));
    printf("val: %s\n", $v);
}

echo "\n# COUNT\n";

// count を取れる
printf("element count: %s\n", count($map));
```

上の例の出力は以下となります。

```
# ACCESS
array key value: hoge
object key value: fuga
string key value: piyo

# FOREACH
key type: array=["akey"]
val: hoge
key type: object={}
val: fuga
key type: string="key"
val: piyo

# GENERATOR
key type: array=["akey"]
val: hoge
key type: object={}
val: fuga
key type: string="key"
val: piyo

# COUNT
element count: 3
```

配列やオブジェクトをキーとして利用したい状況はまれによくあるため、そういうときに使えるかもしれません。
ただし下記に注意です。

- ドキュメントに言及がない
  - 現状「たまたまそうなっている」とも言えるので良い実装ではない
  - `$map[] = 123;` で null が来るので多分それ関係？
- 線形探索になる
  - 要素の数に比例して探索が遅くなる
  - serialize とかしてキーにしてもいいが、それなら普通に配列を使えばいい
- 文字列と整数キーは別扱いになる
  - getKey を修正すれば同一視できると思う
- キーは内部で保持されるため GC の対象にならない
  - [spl_object_hash](http://php.net/manual/ja/function.spl-object-hash.php) に重複が無ければ…！
- `$map[] = 123;` で追加ではなく null キーになる
  - 多分両立できないので `offsetSet` を弄るしかないと思う
- phpstorm に激しく怒られる
  - 「Illegal array key type」とかそういうの

一番上が割と致命的なので無理に ArrayAccess なんて使わないで普通にメソッドベースでいいと思うよ。