LoginSignup
0
1

More than 1 year has passed since last update.

PHPで作るFirstClassCollectionの雛形

Posted at

【自分用のメモ】
POPO(Plain Old PHP Object)でそれっぽいものを作ってみたいな〜と思っていたところ、IteratorAggeregateを使えば簡単にできるよ!と教えていただいたので、下記の記事を参考にそれっぽいものを作ってみました。

参考:Iteratorをimplementsする奴は情弱。IteratorAggregateを使え

基底クラス

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);
    }
}

上記の基底クラスを利用したサンプル

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に対するいろんな操作がクラスに集まってきそうな気もするので、その場合は生成時のルールは別オブジェクトに切り出したい気もする?

こんな感じで切り出してあげればルールだけでユニットテストが書けそうだな?

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な仕組みがないとだめか...?

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

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1