PHPで作るFirstClassCollectionの雛形

【自分用のメモ】
POPO（Plain Old PHP Object）でそれっぽいものを作ってみたいな〜と思っていたところ、[IteratorAggeregate](https://www.php.net/manual/ja/class.iteratoraggregate.php)を使えば簡単にできるよ！と教えていただいたので、下記の記事を参考にそれっぽいものを作ってみました。

参考：[Iteratorをimplementsする奴は情弱。IteratorAggregateを使え](https://qiita.com/Hiraku/items/14722922441f9ed3fbbb)
## 基底クラス
```PHP
use ArrayIterator;
use InvalidArgumentException;
use IteratorAggregate;
use Traversable;

/**
 * Foundation class for first class collection
 * @template T
 */
abstract class Collection implements IteratorAggregate
{
    /**
     * @var array<T>
     */
    public readonly array $items;

    /**
     * @param array<T> $items
     */
    public function __construct(array $items)
    {
        $this->items = $this->validate($items);
    }

    /**
     * Validate items before setting it to instance variable
     *
     * @param array<T> $items
     * @return array<T> $items
     */
    private function validate(array $items)
    {
        $rules = $this->collectionRules();
        // if no set rules, return all items as validated
        if (count($rules) <= 0) {
            return $items;
        }
        // apply rules
        foreach($rules as $key => $rule) {
            if (!$rule($items)) {
                throw new InvalidArgumentException("Rule $key violated");
            }
        }
        return $items;
    }

    /**
     * returns sets of rules to be applied to values given
     *
     * @return array<string,Callable>
     */
    abstract protected function collectionRules(): array;

    /**
     * @inheritDoc
     */
    public function getIterator(): Traversable
    {
        return new ArrayIterator($this->items);
    }
}
```
## 上記の基底クラスを利用したサンプル
```PHP
use Collection;

class SampleCollection extends Collection
{
    private const MIN = 1;
    private const MAX = 4;

    /**
     * @inheritDoc
     */
    protected function collectionRules(): array
    {
        return [
            'is_valid_class' => function(array $items) {
                foreach($items as $item) {
                    if ($item instanceof Sample) {
                        continue;
                    }
                    return false;
                }
                return true;
            },
            'is_enough' => function(array $items) {
                return count($items) >= self::MIN;
            },
            'is_not_too_many' => function(array $items) {
                return count($items) <= self::MAX;
            }
        ];
    }
}
```
こんな感じかな？という雰囲気はなんとなくつかめたかな...？
実際に使うとなるとおそらくCollectionに対するいろんな操作がクラスに集まってきそうな気もするので、その場合は生成時のルールは別オブジェクトに切り出したい気もする？

こんな感じで切り出してあげればルールだけでユニットテストが書けそうだな？
```PHP
class SampleCollectionRule
{
    private const MIN = 1;
    private const MAX = 4;

    public function __invoke(array $items): bool
    {
        if ($this->isNotValidClass($items)) {
            return false;
        }
        if ($this->isNotEnough($items)) {
            return false;
        }
        if ($this->isTooMany($items)) {
            return false;
        }
        return true;
    }

    private function isNotValidClass(array $items)
    {
        foreach($items as $item) {
            if ($item instanceof Sample) {
                continue;
            }
            return true;
        }
        return false;
    }

    private function isNotEnough(array $items)
    {
        return count($items) < self::MIN;
    }

    private function isTooMany(array $items)
    {
        return count($items) > self::MAX;
    }
}
```
こんな感じで切り出してあげればCollectionの方は実際の操作ロジックをメインにできそう
でもこれだとドメインロジックが別のクラスになってしまう？
Psalmを入れるなどしてPackage Privateな仕組みがないとだめか...？

この辺のバランス感覚が難しいなぁ...