php:``数组''
#php #tips #doctrine

PHP : The fall of  raw `array` endraw

array是PHP中非常普遍的类型,但通常被过度使用。

我会尝试解释为什么您不再键入提示阵列了!

目录

  1. The problems
  2. First steps
  3. Using koude1
  4. Using koude2
  5. Using koude3
    1. The hard way (implementing koude4)
    2. The easy way (implementing koude5)
    3. But what do I gain ?
  6. Conclusion

问题

  1. businness逻辑

    让我们首先谈论为什么我们需要替换array

    在声明像array $products这样的简单参数时,我们通常需要在其中应用一些域逻辑:groupByCategoriesfilterNonUnavailablecalculateTotalPrice,...但是我们应该在哪里放置所有这些逻辑?我经常看到它在代码中的多个位置或具有命名或结构不一致的辅助类中重复。

  2. 记忆消耗

    已知阵列在PHP中是内存密集的(请参阅下面的Nikita Popov的推文)。


    此外,它不能飞行生成(vs Generator,我们将在稍后讨论)。

  3. ISP

    It always provides 3 different features : koude1, koude3, koude2 which you may not always have a need for.

  4. Keys must be koude15 or koude16

    When using an array you MUST use either a string or an int as key (⚠️ koude17s are automatically converted to koude16 as well as numeric koude15).

    Sometimes you may want to have a koude20 as key or any objects. You even may want to have duplicated keys !

  5. Pass by value

    When you use an array variable to call a method or a function, PHP will actually duplicate that array everytime (pass by value). Object however are pass by reference.

第一步

如果您已经做到了这么远,您可能会害怕可能需要的所有重构!让我首先向您保证,然后解释一下我们如何从中改进。这是一个简单的类,与array完全相同:

/** @extends ArrayIterator<string, Product> */
class ProductCollection extends ArrayIterator
{
    /**
      * @param array<string, Product> $products
      */
    public function __construct(array $products) 
    {
        parent::__construct($products);
    }
}

现在,您可以到处键入ProductCollection $products,而无需更改其他任何内容。它可以以所有类似的方式使用,例如array和Phpstorm知道$productCollection['someId']将返回Product类。

编辑:对象 array_*兼容。您需要使用iterator_to_array($productCollection)将其应用于array_*功能。要么在对象上创建一种将使用内部值的方法。


似乎足够了吗?好吧,我说了第一步。现在让我们看看我们如何以及为什么可以进一步改进。

使用Countable

这个非常轻,通常与其他两个结合使用。如果您只需要检查数组中的数字,您可以简单地说您的班级是Countable这样的:

class ProductCollection implements Countable
{
    public function count(): int
    {
        return count($this->products);
    }
}

本课程本身不是很有用。让我们潜入其他两个。

使用ArrayAccess

假设您现在想提供与$productCollection[$productId]相同的语法以访问数据,但是您想提供一个像不变的数组。

这是一个示例:

/** @implements ArrayAccess<string, Product> */
class FrozenProductCollection implements ArrayAccess
{
    /**
      * @param array<string, Product> $products
      */
    public function __construct(private readonly array $products) 
    {
    }

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->products);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->products[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        throw new Exception('Cannot add a product to a FrozenProductCollection');
    }

    public function offsetUnset(mixed $offset): void
    {
        throw new Exception('Cannot remove a product to a FrozenProductCollection');
    }
}

使用此功能您可以访问数据,但您无法删除或添加新产品。

  • offsetExists(mixed $offset): bool:如果存在位置$offset的输入(由isset??,...
  • 使用),返回true。
  • offsetGet(mixed $offset): mixed:返回与偏移相关的条目。
  • offsetSet(mixed $offset, mixed $value): void:在位置添加 /替代条目$offset。< / li>
  • offsetUnset(mixed $offset): void:卸下位于$offset位置的条目

请注意,所有这些方法都使用mixed作为$offset。因此,是的

您甚至可以将其与基本类相结合:

/** @extends ArrayIterator<string, Product> */
class ProductCollection extends ArrayIterator
{
    /**
      * @param array<string, Product> $products
      */
    public function __construct(array $products) 
    {
        parent::__construct($products);
    }
}

/** @implements ArrayAccess<string, Product> */
class FrozenProductCollection implements ArrayAccess
{
    public function __construct(
        private readonly ProductCollection $products,
    ) {
    }

    public function offsetExists(mixed $offset): bool
    {
        return array_key_exists($offset, $this->products);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->products[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        throw new Exception('Cannot add a product to a FrozenProductCollection');
    }

    public function offsetUnset(mixed $offset): void
    {
        throw new Exception('Cannot remove a product to a FrozenProductCollection');
    }
}

$productCollection = new FrozenProductCollection(new ProductCollection([
    '#123' => new Product(
        id: '#123',
        name: 't-shirt',
    ),
]));

$productCollection['#456'] = new Product(
    id: '#456',
    name: 'headband',
); // This is now forbidden

win的装饰器!

使用Traversable

php接口Traversable用于定义可以迭代(使用foreach)的对象。 arrayTraversable,但对象也可以!让我们看看如何实施它。您不能直接实现可遍历(用于PHP),但可以使用IteratorIteratorAggregate

艰难的方式(实施Iterator

/** @implements Iterator<string, Product> */
class ProductCollection implements Iterator
{
    private int $cursorPosition = 0;

    /** @var array<string> */
    private readonly array $keys;

    /** @var array<Product> */
    private readonly array $products;

    /**
      * @param array<string, Product> $products
      */
    public function __construct(
        array $products
    ) {
        $this->keys = array_keys($products);
        $this->products = array_values($products);
    }

    public function rewind(): void
    {
        $this->cursorPosition = 0;
    }

    public function current(): Product
    {
        return $this->products[$this->cursorPosition];
    }

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

    public function next(): void
    {
        ++$this->cursorPosition;
    }

    public function valid(): bool
    {
        return isset($this->products[$this->cursorPosition]);
    }
}

您可以看到,这是很多尚未提供比array更好的代码。尽管该语法看起来与任何Collection相同,但实际上是实现PDO生成器技术的好方法。那么,我们避免重复的代码吗?

简单的方法(实施IteratorAggregate

看这个课:

/** @implements IteratorAggregate<string, Product> */
class ProductCollection implements IteratorAggregate
{
    /**
      * @param array<string, Product> $products
      */
    public function __construct(
        private readonly array $products
    ) {
    }

    /** @return ArrayIterator<string, Product> */
    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->products);
    }
}

这两个示例都会产生相同的结果:您可以在像这样的ProductCollection实例上迭代:

$productCollection = new ProductCollection([
    '#123' => new Product(
        id: '#123',
        name: 't-shirt',
    ),
]);

foreach ($productCollection as $product) {
    echo $product->id; // will print '#123'
}

但是我会得到什么?

首先,您现在是一个打字的集合,您可以在任何地方显式typehint。您的系列本人拥有有关迭代的信息

您还可以访问类,因此您可以在此上下文中添加您认为有用的任何方法。

截至这一刻,您还没有任何PHP内存改进,因为我们仍在使用同类中的数组。让我们改进这个。

/** @implements IteratorAggregate<string, Product> */
class ProductCollection implements IteratorAggregate
{
    /**
      * @param iterable<string, Product> $products
      */
    public function __construct(
        private readonly iterable $products
    ) {
    }

    /** @return Generator<string, Product> */
    public function getIterator(): Generator
    {
        yield from $this->products;
    }
}

â€ateware,这可能是一个不可回复的生成器,这意味着您只能在$this->products上迭代一次。

现在您可以将其与对象或数组一起使用。该对象可以是生成器(使用yield创建)。这是一个使用学说的示例,该示例将在每个行上迭代,而无需立即将它们加载到内存:

class ProductRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, Product::class);
    }

    public function streamAll(): ProductCollection
    {
        $qb = $this->createQueryBuilder('product');

        return new ProductCollection($qb->getQuery()->toIterable());
    }
}

关于Iterator的还有很多话要说。根据这篇文章的反馈,我可能会在后续文章中更深入。


结论

使用array曾经提供的类别的类允许您:

  • 更容易扩展您的代码
  • 将业务逻辑保持在一起
  • typehint正是您注入方法或类别的内容
  • 让您的静态分析仪快乐!