*/ private array $items; /** * Total row count across every page (after filtering, before * pagination). */ private int $totalItems; /** * 1-based page number echoed back from the originating query. */ private int $page; /** * Records per page echoed back from the originating query. */ private int $perPage; /** * Construct the result envelope with full validation. * * @param list $items Page slice. * @param int $totalItems Total matching rows (>= 0). * @param int $page 1-based page number (>= 1). * @param int $perPage Records per page (>= 1). * * @throws InvalidArgumentException When `$items` is not a list of * {@see ProductRecord}, when * `$totalItems` is negative, or * when `$page` / `$perPage` are * below 1. */ public function __construct( array $items, int $totalItems, int $page, int $perPage ) { $expectedIndex = 0; foreach ( $items as $key => $item ) { if ( $key !== $expectedIndex ) { $message = 'ProductListResult: items must be a 0-indexed list.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( ! $item instanceof ProductRecord ) { $message = 'ProductListResult: every items entry must be a ProductRecord.'; // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } ++$expectedIndex; } if ( $totalItems < 0 ) { $message = sprintf( 'ProductListResult: totalItems must be >= 0, got %d.', $totalItems ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( $page < 1 ) { $message = sprintf( 'ProductListResult: page must be >= 1, got %d.', $page ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } if ( $perPage < 1 ) { $message = sprintf( 'ProductListResult: perPage must be >= 1, got %d.', $perPage ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Developer-facing message, never rendered to HTML. throw new InvalidArgumentException( $message ); } /** @var list $items */ $this->items = $items; $this->totalItems = $totalItems; $this->page = $page; $this->perPage = $perPage; } /** * Page slice, in the order produced by the query's sort axis. * * @return list */ public function items(): array { return $this->items; } /** * Total row count across every page. */ public function totalItems(): int { return $this->totalItems; } /** * 1-based page number echoed back from the originating query. */ public function page(): int { return $this->page; } /** * Records per page echoed back from the originating query. */ public function perPage(): int { return $this->perPage; } /** * Total number of pages spanned by `totalItems` at `perPage` * granularity. * * Returns 0 when `totalItems` is 0 (the conventional empty-result * shape) and `(int) ceil($totalItems / $perPage)` otherwise. The * §4.2 example aligns: `135 / 20 → 7`. */ public function totalPages(): int { if ( $this->totalItems === 0 ) { return 0; } return (int) ceil( $this->totalItems / $this->perPage ); } /** * Whether the result envelope carries no rows. */ public function isEmpty(): bool { return $this->totalItems === 0; } /** * Deep equality against another result envelope. * * Items are compared element-wise via * {@see ProductRecord::toArray()}, so two records carrying the * same column payload compare equal even when they are distinct * object instances. * * @param self $other Result to compare. */ public function equals( self $other ): bool { if ( $this->totalItems !== $other->totalItems ) { return false; } if ( $this->page !== $other->page ) { return false; } if ( $this->perPage !== $other->perPage ) { return false; } if ( count( $this->items ) !== count( $other->items ) ) { return false; } foreach ( $this->items as $index => $item ) { if ( $item->toArray() !== $other->items[ $index ]->toArray() ) { return false; } } return true; } }