* @implements Collection * @implements ArrayAccess * @implements SeekableIterator */ class EntityCollection implements Collection, Iterator, Countable, ArrayAccess, SeekableIterator { private ?EntityFactory $entityFactory = null; private ?string $entityType; private int $position = 0; private bool $isFetched = false; /** @var array> */ protected array $dataList = []; /** * @param array> $dataList */ public function __construct( array $dataList = [], ?string $entityType = null, ?EntityFactory $entityFactory = null ) { $this->dataList = $dataList; $this->entityType = $entityType; $this->entityFactory = $entityFactory; } public function rewind(): void { $this->position = 0; while (!$this->valid() && $this->position <= $this->getLastValidKey()) { $this->position ++; } } /** * @return TEntity */ #[\ReturnTypeWillChange] public function current() { return $this->getEntityByOffset($this->position); } /** * @return int */ #[\ReturnTypeWillChange] public function key() { return $this->position; } public function next(): void { do { $this->position ++; $next = false; if (!$this->valid() && $this->position <= $this->getLastValidKey()) { $next = true; } } while ($next); } /** * @return int */ private function getLastValidKey() { $keys = array_keys($this->dataList); $i = end($keys); while ($i > 0) { if (isset($this->dataList[$i])) { break; } $i--; } return $i; } public function valid(): bool { return isset($this->dataList[$this->position]); } /** * @param mixed $offset */ public function offsetExists($offset): bool { return isset($this->dataList[$offset]); } /** * @param mixed $offset * @return ?TEntity */ #[\ReturnTypeWillChange] public function offsetGet($offset) { if (!isset($this->dataList[$offset])) { return null; } return $this->getEntityByOffset($offset); } /** * @param mixed $offset * @param mixed $value */ public function offsetSet($offset, $value): void { if (!($value instanceof Entity)) { throw new InvalidArgumentException('Only Entity is allowed to be added to EntityCollection.'); } /** @var TEntity $value */ if (is_null($offset)) { $this->dataList[] = $value; return; } $this->dataList[$offset] = $value; } /** * @param mixed $offset */ public function offsetUnset($offset): void { unset($this->dataList[$offset]); } public function count(): int { return count($this->dataList); } /** * @param int $offset */ public function seek($offset): void { $this->position = $offset; if (!$this->valid()) { throw new OutOfBoundsException("Invalid seek offset ($offset)."); } } /** * @param TEntity $entity */ public function append(Entity $entity): void { $this->dataList[] = $entity; } /** * @param int $offset * @return TEntity */ private function getEntityByOffset($offset): Entity { if (!array_key_exists($offset, $this->dataList)) { throw new RuntimeException(); } /** @var mixed */ $value = $this->dataList[$offset]; if ($value instanceof Entity) { /** @var TEntity */ return $value; } if (is_array($value)) { $this->dataList[$offset] = $this->buildEntityFromArray($value); return $this->dataList[$offset]; } throw new RuntimeException(); } /** * @param array $dataArray * @return TEntity */ protected function buildEntityFromArray(array $dataArray): Entity { if (!$this->entityFactory) { throw new RuntimeException("Can't build from array. EntityFactory was not passed to the constructor."); } assert($this->entityType !== null); /** @var TEntity $entity */ $entity = $this->entityFactory->create($this->entityType); $entity->set($dataArray); if ($this->isFetched) { $entity->setAsFetched(); } return $entity; } /** * Get an entity type. */ public function getEntityType(): ?string { return $this->entityType; } /** * @deprecated As of v6.0. Use `getEntityType`. * @return ?string */ public function getEntityName() { return $this->entityType; } /** * @return array> */ public function getDataList(): array { return $this->dataList; } /** * Merge with another collection. * * @param EntityCollection $collection */ public function merge(EntityCollection $collection): void { $incomingDataList = $collection->getDataList(); foreach ($incomingDataList as $v) { if (!$this->contains($v)) { $this->dataList[] = $v; } } } /** * Whether a collection contains a specific item. * * @param TEntity|array $value */ public function contains($value): bool { if ($this->indexOf($value) !== false) { return true; } return false; } /** * @param TEntity|array $value * @return false|int */ public function indexOf($value) { $index = 0; if (is_array($value)) { foreach ($this->dataList as $v) { if (is_array($v)) { if ($value['id'] == $v['id']) { return $index; } } else if ($v instanceof Entity) { if ($value['id'] == $v->getId()) { return $index; } } $index ++; } } else if ($value instanceof Entity) { foreach ($this->dataList as $v) { if (is_array($v)) { if ($value->getId() == $v['id']) { return $index; } } else if ($v instanceof Entity) { if ($value === $v) { return $index; } } $index ++; } } return false; } /** * @deprecated As of v6.0. Use `getValueMapList`. * @return array>|stdClass[] */ public function toArray(bool $itemsAsObjects = false): array { $arr = []; foreach ($this as $entity) { if ($itemsAsObjects) { $item = $entity->getValueMap(); } else { $item = $entity->toArray(); // @phpstan-ignore-line } $arr[] = $item; } return $arr; } /** * {@inheritDoc} */ public function getValueMapList(): array { /** @var stdClass[] */ return $this->toArray(true); } /** * Mark as fetched from DB. */ public function setAsFetched(): void { $this->isFetched = true; } /** * Is fetched from DB. */ public function isFetched(): bool { return $this->isFetched; } /** * Create from SthCollection. * * @param SthCollection $sthCollection * @return self */ public static function fromSthCollection(SthCollection $sthCollection): self { $entityList = []; foreach ($sthCollection as $entity) { $entityList[] = $entity; } /** @var self $obj */ $obj = new EntityCollection($entityList, $sthCollection->getEntityType()); $obj->setAsFetched(); return $obj; } }