* @license MIT */ declare(strict_types=1); namespace BitBadger\PDODocument; use BitBadger\PDODocument\Mapper\Mapper; use Generator; use PDO; use PDOStatement; /** * A lazy iterator of results in a list; implementations will create new connections to the database and close/dispose * them as required once the results have been exhausted. * * @template TDoc The domain class for items returned by this list */ class DocumentList { /** @var TDoc|null $first The first item from the results */ private mixed $first = null; /** @var bool $isConsumed This is set to true once the generator has been exhausted */ private bool $isConsumed = false; /** * Constructor * * @param PDOStatement|null $result The result of the query * @param Mapper $mapper The mapper to deserialize JSON */ private function __construct(private ?PDOStatement &$result, private readonly Mapper $mapper) { if (!is_null($this->result)) { if ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { $this->first = $this->mapper->map($row); } else { $this->result = null; } } } /** @var bool True if there are items still to be retrieved from the list, false if not */ public bool $hasItems { get => !is_null($this->result); } /** * @var Generator The items from the document list * @throws DocumentException If this is called once the generator has been consumed */ public Generator $items { get { if (!$this->result) { if ($this->isConsumed) { throw new DocumentException('Cannot call items() multiple times'); } $this->isConsumed = true; return; } if (!$this->first) { $this->isConsumed = true; $this->result = null; return; } yield $this->first; while ($row = $this->result->fetch(PDO::FETCH_ASSOC)) { yield $this->mapper->map($row); } $this->isConsumed = true; $this->result = null; } } /** * Map items by consuming the generator * * @template U The type to which each item should be mapped * @param callable(TDoc): U $map The mapping function * @return Generator The result of the mapping function * @throws DocumentException If this is called once the generator has been consumed */ public function map(callable $map): Generator { foreach ($this->items as $item) { yield $map($item); } } /** * Iterate the generator, running the given function for each item * * @param callable(TDoc): void $f The function to run for each item * @throws DocumentException If this is called once the generator has been consumed */ public function iter(callable $f): void { foreach ($this->items as $item) { $f($item); } } /** * Iterate the generator, extracting key/value pairs returned as an associative array * * @template TValue The type for the mapped value * @param callable(TDoc): (int|string) $keyFunc The function to extract a key from the document * @param callable(TDoc): TValue $valueFunc The function to extract a value from the document * @return TValue[] An associative array of values, keyed by the extracted keys * @throws DocumentException If this is called once the generator has been consumed */ public function mapToArray(callable $keyFunc, callable $valueFunc): array { $results = []; foreach ($this->items as $item) { $results[$keyFunc($item)] = $valueFunc($item); } return $results; } /** * Ensure the statement is destroyed if the generator is not exhausted */ public function __destruct() { if (!is_null($this->result)) $this->result = null; } /** * Construct a new document list * * @param string $query The query to run to retrieve results * @param array $parameters An associative array of parameters for the query * @param Mapper $mapper A mapper to deserialize JSON documents * @return self The document list instance * @throws DocumentException If any is encountered */ public static function create(string $query, array $parameters, Mapper $mapper): self { $stmt = &Custom::runQuery($query, $parameters); return new self($stmt, $mapper); } }