feat(dto): update DTOs, value objects, sorters, pagination and configuration
This commit is contained in:
parent
019e7a4025
commit
bbb3acc234
24 changed files with 350 additions and 70 deletions
|
|
@ -4,31 +4,39 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
|
||||
use OpenTelemetry\API\Trace\TracerInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ClientIdentifier;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ClientSecret;
|
||||
|
||||
readonly class ApiConfig
|
||||
final readonly class ApiConfig
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
public readonly UriInterface $endpoint,
|
||||
public UriInterface $endpoint,
|
||||
#[\SensitiveParameter]
|
||||
public readonly ClientIdentifier $apiKey,
|
||||
public ClientIdentifier $apiKey,
|
||||
#[\SensitiveParameter]
|
||||
public readonly ClientSecret $secret,
|
||||
public readonly float $curlTimeOut = 20.0,
|
||||
public readonly int $maxRetries = 0,
|
||||
public readonly int $retryDelayMs = 500,
|
||||
public ClientSecret $secret,
|
||||
public float $curlTimeOut = 20.0,
|
||||
public int $maxRetries = 0,
|
||||
public int $retryDelaySeconds = 1,
|
||||
public bool $ignoreSelfSignedCert = false,
|
||||
public ?LoggerInterface $logger = null,
|
||||
public ?TracerInterface $tracer = null,
|
||||
) {
|
||||
if (! in_array(strtolower($endpoint->getScheme()), ['http', 'https'])) {
|
||||
throw new \InvalidArgumentException('endpoint must have http or https scheme');
|
||||
}
|
||||
if ($curlTimeOut < 0) {
|
||||
throw new \InvalidArgumentException('curlTimeOut must be non-negative');
|
||||
}
|
||||
if ($maxRetries < 0) {
|
||||
throw new \InvalidArgumentException('maxRetries must be non-negative');
|
||||
}
|
||||
if ($retryDelayMs < 0) {
|
||||
throw new \InvalidArgumentException('retryDelayMs must be non-negative');
|
||||
if ($retryDelaySeconds < 0) {
|
||||
throw new \InvalidArgumentException('retryDelaySeconds must be non-negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,18 +4,19 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
|
||||
final readonly class Brand implements Dto
|
||||
{
|
||||
public function __construct(
|
||||
public readonly BrandCode $code,
|
||||
public readonly string $label
|
||||
public BrandCode $code,
|
||||
public string $label,
|
||||
public ?string $arcadiaId = null,
|
||||
public ?\DateTimeInterface $createdAt = null,
|
||||
) {
|
||||
if (empty(mb_trim($label))) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid label '{$label}' should not be empty"
|
||||
);
|
||||
}
|
||||
|
|
@ -24,8 +25,10 @@ final readonly class Brand implements Dto
|
|||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return [
|
||||
'code' => $this->code->value,
|
||||
'label' => $this->label,
|
||||
'code' => $this->code->value,
|
||||
'label' => $this->label,
|
||||
'arcadiaId' => $this->arcadiaId,
|
||||
'createdAt' => $this->createdAt?->format(\DateTimeInterface::ATOM),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Additive;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Allegations;
|
||||
|
|
@ -46,17 +45,17 @@ final readonly class Inco implements Dto
|
|||
public ?StorageCondition $storageCondition,
|
||||
public array $additives,
|
||||
public ?IncoCompoId $compo,
|
||||
public ?\DateTimeInterface $created_at,
|
||||
public ?\DateTimeInterface $updated_at,
|
||||
public ?\DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt,
|
||||
) {
|
||||
foreach ($this->allergens as $allergen) {
|
||||
if (! is_object($allergen) || ! ($allergen instanceof Allergen)) {
|
||||
throw new UnexpectedErrorException('Allergens shall be instance of ' . Allergen::class);
|
||||
throw new \InvalidArgumentException('Allergens shall be instance of ' . Allergen::class);
|
||||
}
|
||||
}
|
||||
foreach ($this->additives as $additive) {
|
||||
if (! is_object($additive) || ! ($additive instanceof Additive)) {
|
||||
throw new UnexpectedErrorException('Additives shall be instance of ' . Additive::class);
|
||||
throw new \InvalidArgumentException('Additives shall be instance of ' . Additive::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -86,8 +85,8 @@ final readonly class Inco implements Dto
|
|||
'storageCondition' => $this->storageCondition,
|
||||
'additives' => $this->additives,
|
||||
'compo' => $this->compo?->value,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
'created_at' => $this->createdAt,
|
||||
'updated_at' => $this->updatedAt,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
|
|
@ -25,16 +24,16 @@ final readonly class Additive implements Dto
|
|||
public readonly string $label,
|
||||
) {
|
||||
$this->code = $code;
|
||||
$trimmedLabel = trim($label);
|
||||
$trimmedLabel = mb_trim($label);
|
||||
|
||||
if (! preg_match('/^E\d+(?:[a-z]+(?:\([ivx]+\))?)?$/', $this->code)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid additive code '{$code}': must match E-number format (e.g. E300, E471a, E160ai, E160a(ii), E1404)"
|
||||
);
|
||||
}
|
||||
|
||||
if ($trimmedLabel === '') {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid label for additive '{$code}': must not be empty"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
final readonly class Allergen implements Dto
|
||||
|
|
@ -14,13 +13,13 @@ final readonly class Allergen implements Dto
|
|||
public readonly string $label
|
||||
) {
|
||||
if ($code <= 0) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid code '{$code}' shall be greater than 0"
|
||||
);
|
||||
}
|
||||
|
||||
if (empty(mb_trim($label))) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid label '{$label}' should not be empty"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
|
|
@ -27,20 +26,20 @@ final readonly class Ingredients implements Dto
|
|||
) {
|
||||
foreach ($groups as $key => $lines) {
|
||||
if (! is_array($lines)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid ingredient group at key '{$key}': value must be an array of strings"
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
if (! is_string($line)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid ingredient at group '{$key}', index '{$i}': value must be a string"
|
||||
);
|
||||
}
|
||||
|
||||
if (empty(mb_trim($line))) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid ingredient at group '{$key}', index '{$i}': string should not be empty"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
|
|
@ -20,7 +19,7 @@ final readonly class Nutriscore implements Dto
|
|||
public readonly string $grade,
|
||||
) {
|
||||
if (! in_array($this->grade, self::VALID_GRADES, true)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
sprintf(
|
||||
"Invalid Nutri-Score grade '%s': must be one of %s",
|
||||
$grade,
|
||||
|
|
|
|||
|
|
@ -197,6 +197,6 @@ final readonly class PortionAmount implements \JsonSerializable, \Stringable, Dt
|
|||
$decimals = self::ALLOWED_DECIMAL_SIZE;
|
||||
$format = number_format($value, $decimals, '.', '');
|
||||
|
||||
return rtrim(rtrim($format, '0'), '.');
|
||||
return mb_rtrim(mb_rtrim($format, '0'), '.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
|
|
@ -18,7 +17,7 @@ final readonly class StorageCondition implements Dto
|
|||
public readonly string $instructions,
|
||||
) {
|
||||
if (empty(mb_trim($this->instructions))) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
'Invalid instructions: must not be empty'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,13 +36,13 @@ abstract readonly class AbstractIncoCompoPartDto implements IncoCompoPartDto
|
|||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
// Reflection on public properties of the concrete child class.
|
||||
$ref = new \ReflectionObject($this);
|
||||
/** @var array<string, float|string|null> $result */
|
||||
$result = [];
|
||||
|
||||
foreach ($ref->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
|
||||
$name = $property->getName();
|
||||
$result[$name] = $property->getValue($this);
|
||||
foreach (get_object_vars($this) as $name => $value) {
|
||||
if (\is_float($value) || \is_string($value) || $value === null) {
|
||||
$result[(string) $name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiSorter;
|
||||
|
||||
enum EmployeeSortColum implements SortColumn
|
||||
enum EmployeeSortColumn implements SortColumn
|
||||
{
|
||||
case arcadiaId;
|
||||
case startDate;
|
||||
14
src/Lib/ApiSorter/EmployeeSorter.php
Normal file
14
src/Lib/ApiSorter/EmployeeSorter.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiSorter;
|
||||
|
||||
final readonly class EmployeeSorter
|
||||
{
|
||||
public function __construct(
|
||||
public EmployeeSortColumn $sortBy,
|
||||
public \SortDirection $direction = \SortDirection::Descending
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiSorter;
|
||||
|
||||
enum IncoSortColumn implements SortColumn
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,15 +2,13 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiSorter;
|
||||
|
||||
final readonly class IncoSorter
|
||||
{
|
||||
public function __construct(
|
||||
public IncoSortColumn $sortBy,
|
||||
public Direction $direction
|
||||
public \SortDirection $direction = \SortDirection::Descending
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiSorter;
|
||||
|
||||
interface SortColumn extends \UnitEnum
|
||||
{
|
||||
|
|
|
|||
|
|
@ -4,18 +4,17 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ValueObjectId;
|
||||
|
||||
final readonly class ArcadiaEmployeeId implements ValueObjectId
|
||||
{
|
||||
private const PATTERN = '/^\d{7, 8}$/';
|
||||
private const PATTERN = '/^\d{7,8}$/';
|
||||
|
||||
public function __construct(
|
||||
public readonly string $value
|
||||
) {
|
||||
if (! preg_match(self::PATTERN, $value)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid value '{$value}' does not match pattern " . self::PATTERN
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,20 +4,19 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ValueObjectId;
|
||||
|
||||
/**
|
||||
* eg: 13 -> Sans marque
|
||||
* Unique public brand code (e.g. "LGL01", "ARC01").
|
||||
*/
|
||||
final readonly class BrandCode implements ValueObjectId
|
||||
{
|
||||
public function __construct(
|
||||
public readonly int $value
|
||||
public string $value
|
||||
) {
|
||||
if ($value <= 0) {
|
||||
throw new DomainException(
|
||||
"Invalid code '{$value}' shall be greater than 0"
|
||||
if (! preg_match('/^[A-Z]{3}\d{2}$/', $value)) {
|
||||
throw new \DomainException(
|
||||
"Invalid code '{$value}' does not match pattern /^[A-Z]{3}\d{2}$/"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ValueObjectId;
|
||||
|
||||
final readonly class IncoCompoId implements ValueObjectId
|
||||
|
|
@ -15,7 +14,7 @@ final readonly class IncoCompoId implements ValueObjectId
|
|||
public readonly string $value
|
||||
) {
|
||||
if (! preg_match(self::PATTERN, $value)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid value '{$value}' does not match pattern " . self::PATTERN
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ValueObjectId;
|
||||
|
||||
final readonly class IncoId implements ValueObjectId
|
||||
|
|
@ -15,7 +14,7 @@ final readonly class IncoId implements ValueObjectId
|
|||
public readonly string $value
|
||||
) {
|
||||
if (! preg_match(self::PATTERN, $value)) {
|
||||
throw new DomainException(
|
||||
throw new \DomainException(
|
||||
"Invalid value '{$value}' does not match pattern " . self::PATTERN
|
||||
);
|
||||
}
|
||||
|
|
|
|||
17
src/Lib/LibMiddleware.php
Normal file
17
src/Lib/LibMiddleware.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
interface LibMiddleware
|
||||
{
|
||||
/** @param array<string, mixed> $options */
|
||||
public function __invoke(RequestInterface $request, array $options): PromiseInterface;
|
||||
}
|
||||
30
src/Lib/LibProperties.php
Normal file
30
src/Lib/LibProperties.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
|
||||
use Composer\InstalledVersions;
|
||||
|
||||
final readonly class LibProperties
|
||||
{
|
||||
public const string PACKAGE_NAME = 'uwil/arcadia-api-php-client';
|
||||
|
||||
public string $productName;
|
||||
|
||||
public string $productVersion;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$version = InstalledVersions::getPrettyVersion(self::PACKAGE_NAME);
|
||||
|
||||
if ($version === null) {
|
||||
throw new \UnexpectedValueException(
|
||||
sprintf("Unable to determine the version for package '%s'.", self::PACKAGE_NAME)
|
||||
);
|
||||
}
|
||||
|
||||
$this->productName = self::PACKAGE_NAME;
|
||||
$this->productVersion = $version;
|
||||
}
|
||||
}
|
||||
53
src/Lib/PageSize.php
Normal file
53
src/Lib/PageSize.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
|
||||
/**
|
||||
* Value object representing a page size in pagination.
|
||||
*/
|
||||
final readonly class PageSize implements \JsonSerializable, \Stringable
|
||||
{
|
||||
public const int DEFAULT_SIZE = 50;
|
||||
|
||||
/** ERR_TOO_MANY_RECORDS fixed to 10k */
|
||||
public const int MAX_PAGE_SIZE = 10000;
|
||||
|
||||
public function __construct(
|
||||
public int $value = self::DEFAULT_SIZE,
|
||||
) {
|
||||
if ($this->value <= 0) {
|
||||
throw new \InvalidArgumentException('Page size must be greater than 0');
|
||||
}
|
||||
|
||||
if ($this->value > self::MAX_PAGE_SIZE) {
|
||||
throw new \InvalidArgumentException('Page size must be less than or equal to ' . self::MAX_PAGE_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
public static function of(int $value): self
|
||||
{
|
||||
return new self($value);
|
||||
}
|
||||
|
||||
public static function default(): self
|
||||
{
|
||||
return new self(self::DEFAULT_SIZE);
|
||||
}
|
||||
|
||||
public function value(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return (string) $this->value;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): int
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,7 +10,7 @@ namespace Uwil\ArcadiaApiPhpClient\Lib;
|
|||
final class Pagination
|
||||
{
|
||||
/** ERR_TOO_MANY_RECORDS fixed to 10k */
|
||||
public const MAX_PAGE_SIZE = 10000;
|
||||
public const int MAX_PAGE_SIZE = PageSize::MAX_PAGE_SIZE;
|
||||
|
||||
public function __construct(
|
||||
private readonly int $from,
|
||||
|
|
@ -55,17 +55,19 @@ final class Pagination
|
|||
return new self($from, $to);
|
||||
}
|
||||
|
||||
public static function ofPage(int $pageNumber, int $pageSize): self
|
||||
public static function ofPage(int $pageNumber, int | PageSize $pageSize): self
|
||||
{
|
||||
$resolvedPageSize = $pageSize instanceof PageSize ? $pageSize->value : $pageSize;
|
||||
|
||||
if ($pageNumber <= 0) {
|
||||
throw new \InvalidArgumentException('Page number must be positive and non-zero');
|
||||
}
|
||||
if ($pageSize <= 0) {
|
||||
if ($resolvedPageSize <= 0) {
|
||||
throw new \InvalidArgumentException('Page size must be positive and non-zero');
|
||||
}
|
||||
|
||||
$fromIndex = ($pageNumber - 1) * $pageSize;
|
||||
$toIndex = max($pageNumber * $pageSize - 1, 1);
|
||||
$fromIndex = ($pageNumber - 1) * $resolvedPageSize;
|
||||
$toIndex = max($pageNumber * $resolvedPageSize - 1, 1);
|
||||
|
||||
return new self($fromIndex, $toIndex);
|
||||
}
|
||||
|
|
@ -85,7 +87,7 @@ final class Pagination
|
|||
return new self($fromIndex, $toIndex);
|
||||
}
|
||||
|
||||
public static function firstPage(int $pageSize = 25): self
|
||||
public static function firstPage(int | PageSize $pageSize = new PageSize()): self
|
||||
{
|
||||
return self::ofPage(1, $pageSize);
|
||||
}
|
||||
|
|
|
|||
165
src/Lib/Pagination/StreamOf.php
Normal file
165
src/Lib/Pagination/StreamOf.php
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Error\ApiError;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PaginationDto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\PageSize;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
/**
|
||||
* Lazy stream / cursor iterator over paginated API resources.
|
||||
*
|
||||
* Provides continuous O(1) memory iteration across all pages.
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @implements \IteratorAggregate<int, T>
|
||||
*/
|
||||
final class StreamOf implements \IteratorAggregate
|
||||
{
|
||||
private ?ApiError $lastError = null;
|
||||
|
||||
private ?PaginationDto $firstPagePagination = null;
|
||||
|
||||
/**
|
||||
* @param \Closure(Pagination):(\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf<T>|ApiError) $pageFetcher
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly \Closure $pageFetcher,
|
||||
private readonly PageSize $pageSize = new PageSize(),
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over individual items of type T.
|
||||
*
|
||||
* @return \Generator<int, T, mixed, ?ApiError>
|
||||
*/
|
||||
public function getIterator(): \Generator
|
||||
{
|
||||
foreach ($this->pages() as $page) {
|
||||
foreach ($page->items() as $item) {
|
||||
yield $item;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate page by page (useful for batching operations).
|
||||
*
|
||||
* @return \Generator<int,\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf<T>, mixed, ?ApiError>
|
||||
*/
|
||||
public function pages(): \Generator
|
||||
{
|
||||
$this->lastError = null;
|
||||
$pageNumber = 1;
|
||||
|
||||
do {
|
||||
$pagination = Pagination::ofPage($pageNumber, $this->pageSize);
|
||||
$result = ($this->pageFetcher)($pagination);
|
||||
|
||||
if ($result instanceof ApiError) {
|
||||
$this->lastError = $result;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($this->firstPagePagination === null) {
|
||||
$this->firstPagePagination = $result->pagination();
|
||||
}
|
||||
|
||||
yield $result;
|
||||
|
||||
if (! $result->pagination()->hasNextPage()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$pageNumber++;
|
||||
} while (true);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a callback on each item.
|
||||
* Stops and returns the ApiError if an error occurs, or stops if the callback returns false.
|
||||
*
|
||||
* @param callable(T): (bool|void) $callback
|
||||
*/
|
||||
public function each(callable $callback): ?ApiError
|
||||
{
|
||||
foreach ($this->getIterator() as $item) {
|
||||
if ($callback($item) === false) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yield at most $limit items without requesting subsequent pages unnecessarily.
|
||||
*
|
||||
* @return \Generator<int, T>
|
||||
*/
|
||||
public function take(int $limit): \Generator
|
||||
{
|
||||
if ($limit <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
foreach ($this->getIterator() as $item) {
|
||||
yield $item;
|
||||
$count++;
|
||||
if ($count >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the first item of the stream, or null if empty, or ApiError on failure.
|
||||
*
|
||||
* @return T|ApiError|null
|
||||
*/
|
||||
public function first(): mixed
|
||||
{
|
||||
foreach ($this->take(1) as $item) {
|
||||
return $item;
|
||||
}
|
||||
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
public function lastError(): ?ApiError
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
public function hasError(): bool
|
||||
{
|
||||
return $this->lastError !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of items reported by the API (available once at least one page is fetched).
|
||||
*/
|
||||
public function totalCount(): ?int
|
||||
{
|
||||
return $this->firstPagePagination?->totalItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of pages reported by the API (available once at least one page is fetched).
|
||||
*/
|
||||
public function totalPages(): ?int
|
||||
{
|
||||
return $this->firstPagePagination?->totalPages;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue