PHP オブジェクトを配列のように扱うためのインターフェース ItetatorAggregateやArrayAccessなど

# はじめに

PHPには、いくつかの[定義済みインターフェース](https://www.php.net/manual/ja/reserved.interfaces.php)があります。その中でも特に、以下のインターフェースを実装することで、オブジェクトを配列のように振る舞わせることが可能です。

- Traversable
- Iterator
- IteratorAggregate
- ArrayAccess
- Countable

これらのインターフェースを満足させる方法を見ていきます。

# Traversable

Traversableインターフェースは、`foreach`文でループ可能にするためのインターフェースです。しかし、Traversableはこれは内部エンジンのインターフェイスであり、PHP スクリプト内で実装することはできません。かわりにTraversableを実装したIteratorかIteratorAggregateを使用する必要があります。

```php
<?php

// Fatal error: Class SomeClass must implement interface Traversable as part of either Iterator or IteratorAggregate in Unknown on line 0
class SomeClass implements Traversable
{

}
```

## 概要

抽象インターフェースですので、メソッドはありません。
```php
Traversable {
}
```

## Iterable型

`array`またはTraversableを実装したクラスは、疑似型Iterableとして扱われます。
Iterable型は例えば関数の引数の型指定として利用できます。関数の引数の型にIterable型が指定されている場合、安全に`foreach`文で利用することができます。

```php

function foo(iterable $iterable) {
    foreach ($iterable as $value) {
        // ...
    } 
}
```

また、関数`is_iterable`は変数の型がIterableかチェックします。

# Iterator

前述の通り、Iteratorインターフェースは抽象インターフェースTraversableを継承しており、このインターフェースを実装したクラスは`foreach`文でループ可能であることを表します。
Iteratorインターフェースを満足させるために、以下の5つのメソッドを実装する必要があります。

## 概要

```php
Iterator extends Traversable {
/* メソッド */
abstract public current ( ) : mixed
abstract public key ( ) : scalar
abstract public next ( ) : void
abstract public rewind ( ) : void
abstract public valid ( ) : bool
}
```

## Iteratorを継承したクラスの例

```php
<?php

class MyIterator implements Iterator
{
  private $position = 0;
  private $values = [];

  public function __construct(...$values)
  {
    $this->values = $values;
  }

  // 参照している要素を先頭へ戻す
  public function rewind()
  {
    $this->position = 0;
  }

  // 現在参照している要素を返す
  public function current()
  {
    return $this->values[$this->position];
  }

  // 現在参照している要素のキーを返す
  public function key()
  {
    return $this->position;
  }

  // 次の要素を参照する
  public function next()
  {
    $this->position++;
  }

  // 現在参照している値が有効かどうか
  public function valid()
  {
    return isset($this->values[$this->position]);
  }
}

$iterator = new MyIterator('apple', 'banana', 'chocolate');

foreach ($iterator as $key => $value) {
  echo $key . ' ' . $value . PHP_EOL;
}

// 0 apple
// 1 banana
// 2 chocolate

```

# IteratorAggregate

Iteratorインターフェースを実装するためには必要なメソッドが多く、少々面倒です。
IteratorAggregateは同じくTraversableインターフェースを継承していますが、こちらはItetatorに処理を委譲します。
そのため、実装するめそっでゃは別のイテレータを返す`getIterator()`のみとシンプルになっています。

## 概要

```php
IteratorAggregate extends Traversable {
/* メソッド */
abstract public getIterator ( ) : Traversable
}
```

## ItetatorAggregateを実装したクラスの例

```php
<?php

class MyIterator implements IteratorAggregate
{
  private $values = [];

  public function __construct(...$values)
  {
    $this->values = $values;
  }

  public function getIterator() 
  {
    return new ArrayIterator($this->values);
  }
}

$iterator = new MyIterator('apple', 'banana', 'chocolate');

foreach ($iterator as $key => $value) {
  echo $key . ' ' . $value . PHP_EOL;
}

// 0 apple
// 1 banana
// 2 chocolate
```

`getIterator()`の戻り値は、Traversableインターフェース型である必要があります。
この例ではシンプルなイテレータであるArrayIteratorを使用しましたが、SPLには、様々なイテレータが定義されています。

- AppendIterator
- ArrayIterator
- CachingIterator
- CallbackFilterIterator
- DirectoryIterator
- EmptyIterator
- FilesystemIterator
- FilterIterator
- GlobIterator
- InfiniteIterator
- IteratorIterator
- LimitIterator
- MultipleIterator
- NoRewindIterator
- ParentIterator
- RecursiveArrayIterator
- RecursiveCachingIterator
- RecursiveCallbackFilterIterator
- RecursiveDirectoryIterator
- RecursiveFilterIterator
- RecursiveIteratorIterator
- RecursiveRegexIterator
- RecursiveTreeIterator
- RegexIterator

[https://www.php.net/manual/ja/spl.iterators.php](https://www.php.net/manual/ja/spl.iterators.php)

# ArrayAccess

ArrayAccessインターフェースは、オブジェクトに対して配列形式でアクセスを可能にするインターフェースです。つまり、`$obj['foo']`のように要素を取得したり、`$obj['foo']`のように要素に値を設定できるということです。

ArrayAccessインターフェースを満足させるために、以下の4つのメソッドを実装する必要があります。

- offsetGet
	- 角括弧`[]`で要素を取得する際に呼ばれる
- offsetSet()
	- 角括弧`[]`で要素に値を設定する歳に呼ばれる
- offsetExists()
	- `isset()`や`empty()`に渡された時に呼ばれる
- offsetUnset()
	- `unset()`に渡された時に呼ばれる

## 実装

```php
ArrayAccess {
/* メソッド */
abstract public offsetExists ( mixed $offset ) : bool
abstract public offsetGet ( mixed $offset ) : mixed
abstract public offsetSet ( mixed $offset , mixed $value ) : void
abstract public offsetUnset ( mixed $offset ) : void
}
```

## ArrayAccessを実装したクラスの例

```php
<?php

class Cart implements ArrayAccess
{
  private $items = [];

  public function __construct(array $items)
  {
    $this->items = $items;
  }

  public function offsetGet($offset)
  {
    echo 'offsetGet called!'. PHP_EOL;
    return isset($this->items[$offset]) ? $this->items[$offset] : null;
  }

  public function offsetSet($offset, $item)
  {
    echo 'offsetSet called!'. PHP_EOL;
      if (is_null($offset)) {
        $this->items[] = $item;
    } else {
        $this->items[$offset] = $item;
    }
  }

  public function offsetExists($offset)
  {
    echo 'offsetExists called!' . PHP_EOL;
    return isset($this->items[$offset]);
  }

  public function offsetUnset($offset)
  {
    echo 'offsetUnset called!' . PHP_EOL;
    unset($this->items[$offset]);
  }
}

$items = [
  'apple' => 150,
  'banana' => 100,
  'chocolate' => 200
];

$cart = new Cart($items);

echo $cart['apple'] . PHP_EOL;
$cart['doughnut'] = 120;
echo isset($cart['strawberry']) . PHP_EOL;
unset($cart['banana']);

print_r($cart);
```

実行結果

```php
offsetGet called!
150
offsetSet called!
offsetExists called!
bool(false)
offsetUnset called!
Cart Object
(
    [items:Cart:private] => Array
        (
            [apple] => 150
            [chocolate] => 200
            [doughnut] => 120
        )

)
```

# Countable

Countableインターフェースを実装したクラスは、関数`count()`で使用することができます。また、`is_countable`に渡すと`true`を返します。

Countableインターフェースを満足させるために、`count()`メソッドを実装する必要があります。

```php
Countable {
/* メソッド */
abstract public count ( ) : int
}
```

## Countableを実装したクラスの例

```php
<?php

class MyCountable implements Countable
{
  private $values = [];

  public function __construct(...$values)
  {
    $this->values = $values;
  }

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

$countable = new MyCountable('apple', 'banana', 'chocolate');

echo count($countable);
var_dump(is_countable($countable));
```
実行結果

```php
3
bool(false)
```

実際には、ArrayAccessインターフェースやIteratorAggregateインターフェースなどとセットで実装されるとよいでしょう。

# 参考

[PHP.net](https://www.php.net/)
[パーフェクトPHP](www.amazon.co.jp/dp/4774144371)
[PHP本格入門[下]](https://www.amazon.co.jp/dp/4297114704)