diff --git a/src/ArcadiaGenesisClient.php b/src/ArcadiaGenesisClient.php deleted file mode 100644 index 12b2540..0000000 --- a/src/ArcadiaGenesisClient.php +++ /dev/null @@ -1,40 +0,0 @@ -apiConfig - ); - - $this->container = $builder->build(); - } - - public function employees(): EmployeeService - { - return $this->container->get(EmployeeService::class); - } - - public function incoSheets(): IncoService - { - return $this->container->get(IncoService::class); - } -} diff --git a/src/Di/PackageDi.php b/src/Di/PackageDi.php deleted file mode 100644 index 702049b..0000000 --- a/src/Di/PackageDi.php +++ /dev/null @@ -1,86 +0,0 @@ - $containerBuilder - */ - public static function initDi( - ContainerBuilder $containerBuilder, - ApiConfig $apiConfig - ): void { - $containerBuilder->addDefinitions([ - ApiConfig::class => fn () => $apiConfig, - Client::class => fn (Container $container) => new Client([ - 'base_uri' => $container->get(ApiConfig::class)->endpoint, - 'timeout' => $container->get(ApiConfig::class)->curlTimeOut, - ]), - ApiCaller::class => fn (Container $container) => new ApiCaller( - $container->get(Client::class), - $container->get(ApiConfig::class), - $container->get(RequestFactory::class), - ), - - EmployeeDtoFactory::class => fn (Container $container) => new EmployeeDtoFactory( - $container->get(EmployeeMapper::class) - ), - IncoDtoFactory::class => fn (Container $container) => new IncoDtoFactory( - $container->get(IncoMapper::class) - ), - IncoShortDtoFactory::class => fn (Container $container) => new IncoShortDtoFactory( - $container->get(IncoShortMapper::class) - ), - IncoCompoDtoFactory::class => fn (Container $container) => new IncoCompoDtoFactory( - $container->get(IncoCompoMapper::class) - ), - - EmployeeRepository::class => fn (Container $container) => new EmployeeGenesisRepository( - $container->get(ApiCaller::class), - $container->get(EmployeeDtoFactory::class), - $container->get(EmployeeMapper::class), - ), - IncoRepository::class => fn (Container $container) => new IncoGenesisRepository( - $container->get(ApiCaller::class), - $container->get(IncoDtoFactory::class), - $container->get(IncoMapper::class), - ), - IncoShortRepository::class => fn (Container $container) => new IncoShortGenesisRepository( - $container->get(ApiCaller::class), - $container->get(IncoShortDtoFactory::class), - $container->get(IncoMapper::class), - ), - IncoCompoRepository::class => fn (Container $container) => new IncoCompoGenesisRepository( - $container->get(ApiCaller::class), - $container->get(IncoCompoDtoFactory::class), - $container->get(IncoCompoMapper::class), - ), - ]); - } -} diff --git a/src/Enum/ArcadiaGenesisApiErrorCode.php b/src/Enum/ArcadiaGenesisApiErrorCode.php deleted file mode 100644 index 9842a33..0000000 --- a/src/Enum/ArcadiaGenesisApiErrorCode.php +++ /dev/null @@ -1,96 +0,0 @@ - 'Vous n\'êtes pas autorisé à faire cette action.', - // phpcs:ignore Generic.Files.LineLength.TooLong - self::ERR_UNAUTHORIZED_PARAMETER => 'Cette clé API n\'a pas le droit de lire les données de toutes les colonnes sélectionnées pour cette table.', - self::ERR_INVALID_API_KEY => 'La clé API n\'est pas valide.', - self::ERR_INVALID_API_SIGNATURE => 'La signature n\'est pas valide.', - self::ERR_LIST_SIZE_MISMATCH => 'Le nombre de colonnes, d\'opérateurs et de valeurs ne correspond pas.', - self::ERR_METAEXCEPTION => 'Une MetaException a été levée. Une erreur s\'est produite.', - self::ERR_SORT_LIST_SIZE_MISMATCH => 'La liste de colonnes de tri n\'est pas de la même taille que la liste d\'opérateurs de tri.', - self::ERR_TABLE_NAME_MISSING => 'Le nom de la table n\'est pas fourni.', - self::ERR_UNAUTHORIZED_OPERATOR => 'La liste d\'opérateurs contient des opérateurs non autorisés.', - self::ERR_BAD_DATE_FORMAT => 'La date doit être fournie au format ISO 8601 (ex : 2025-10-21T14:30:00Z).', - self::ERR_PAGE_TYPE_MISMATCH => 'Le type pour le paramètre \'page\' doit être un entier supérieur à 0.', - self::ERR_PAGE_SIZE_TYPE_MISMATCH => 'Le type pour le paramètre \'page_size\' doit être un entier supérieur à 0.', - self::ERR_TOO_MANY_RECORDS => 'Le nombre d\'enregistrements retournés doit être <= 10 000.', - self::ERR_TOO_MANY_REQUESTS => 'Le nombre maximal de requêtes autorisées a été atteint. Veuillez réessayer ultérieurement.', - self::UNKNOWN => 'Message d\'erreur inconnu', - default => 'Message d\'erreur inconnu', - }; - } - - public function httpStatusCode(): int - { - return match ($this) { - self::ERR_DEFAULT, - self::ERR_UNAUTHORIZED_PARAMETER, - self::ERR_LIST_SIZE_MISMATCH, - self::ERR_METAEXCEPTION, - self::ERR_SORT_LIST_SIZE_MISMATCH, - self::ERR_TABLE_NAME_MISSING, - self::ERR_UNAUTHORIZED_OPERATOR, - self::ERR_BAD_DATE_FORMAT, - self::ERR_PAGE_TYPE_MISMATCH, - self::ERR_PAGE_SIZE_TYPE_MISMATCH, - self::ERR_TOO_MANY_RECORDS => 400, - self::ERR_INVALID_API_KEY, - self::ERR_INVALID_API_SIGNATURE => 401, - self::ERR_TOO_MANY_REQUESTS => 429, - self::UNKNOWN => 0, - default => 0, - }; - } -} diff --git a/src/Exception/ApiCallFailed.php b/src/Exception/ApiCallFailed.php deleted file mode 100644 index 8b77a4b..0000000 --- a/src/Exception/ApiCallFailed.php +++ /dev/null @@ -1,9 +0,0 @@ -apiError = $apiError; - - parent::__construct( - $apiError->description(), - $apiError->httpStatusCode(), - $previous - ); - } - - /** - * @param array $error - */ - public static function fromResponseError(array $error): self - { - // FIXME: Patch degueu en attendant une correction de leur part. - - // * Si l'api retourne de multiples erreurs, ce qu'elle n'est pas sensée faire alors on prend la derniere du tableau. - if (count($error) > 1 && is_int(array_keys($error)[0] ?? null)) { - $error = array_pop($error); - } - - $code = ArcadiaGenesisApiErrorCode::tryFrom($error['code']) ?? ArcadiaGenesisApiErrorCode::UNKNOWN; - - return new self($code); - } -} diff --git a/src/Exception/ArcadiaGenesisApiException.php b/src/Exception/ArcadiaGenesisApiException.php deleted file mode 100644 index 1df3277..0000000 --- a/src/Exception/ArcadiaGenesisApiException.php +++ /dev/null @@ -1,9 +0,0 @@ -apiConfig->endpoint, - $queryPath - ); - - $request = $this->requestFactory->buildRequest( - HttpMethod::GET, - $uri, - $query->toArray() - ); - - return $this->callWithRetry($request); - } - - protected function callWithRetry(RequestInterface $request): ApiResponse - { - $maxAttempts = $this->apiConfig->maxRetries + 1; - $lastException = new \RuntimeException("Max attempts '{$maxAttempts}' reached on api call"); - - for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) { - try { - return $this->call($request); - } catch (ApiCallFailed $e) { - $lastException = $e; - - if ($attempt >= $maxAttempts) { - break; - } - - $this->delay($attempt); - } catch (ApiErrorException $e) { - if ($e->apiError !== ArcadiaGenesisApiErrorCode::ERR_TOO_MANY_REQUESTS) { - throw $e; - } - - $lastException = $e; - - if ($attempt >= $maxAttempts) { - break; - } - - $this->delay($attempt); - } - } - - throw $lastException; - } - - protected function call(RequestInterface $request): ApiResponse - { - try { - $response = $this->client->sendRequest($request); - - $this->assertResponseHasExpectedHeaders($response); - - $body = $this->parseResponseBody($response); - $body = $this->assertApiResponseFormIsAsExpected($body); - - if ($this->apiResponseIsError($body)) { - throw ApiErrorException::fromResponseError($body['error']); - } - - return ApiResponse::fromBody($body); - } catch (ConnectException $e) { - throw new ApiCallFailed( - 'Failed to connect to Api backend: ' . $e->getMessage(), - $e->getCode(), - $e - ); - } catch (ClientException $e) { - throw new ApiCallFailed( - 'Failed to call Api backend seeing a ClientException', - $e->getCode(), - $e - ); - } catch (ServerException $e) { - $response = $e->getResponse(); - - $body = $this->parseResponseBody($response); - - if ($this->apiResponseIsError($body)) { - throw ApiErrorException::fromResponseError($body['error']); - } - - throw new ApiErrorException( - ArcadiaGenesisApiErrorCode::UNKNOWN, - $e - ); - } - } - - /** - * @throws \Uwil\ArcadiaApiPhpClient\Exception\InvalidApiResponse - */ - protected function parseResponseBody(ResponseInterface $response): mixed - { - $bodyContent = $response->getBody()->getContents(); - - if (! json_validate($bodyContent)) { - $jsonErrorMessage = json_last_error_msg(); - throw new InvalidApiResponse( - "Invalid Json message '{$jsonErrorMessage}'" - ); - } - - return json_decode($bodyContent, true); - } - - /** - * @throws \Uwil\ArcadiaApiPhpClient\Exception\InvalidApiResponse - */ - protected function assertResponseHasExpectedHeaders(ResponseInterface $response): void - { - $headerIsJsonContent = in_array( - $response->getHeaders()['Content-Type'][0] ?? '', - [ - 'application/json;charset=UTF-8', - 'application/json', - ], - true - ); - - if (! $headerIsJsonContent) { - throw new InvalidApiResponse( - 'Expected Json header in Content-Type' - ); - } - } - - /** - * @return array - * - * @throws \Uwil\ArcadiaApiPhpClient\Exception\InvalidApiResponse - */ - protected function assertApiResponseFormIsAsExpected(mixed $body): array - { - $e = new InvalidApiResponse( - 'Body content shape cannot be recognized' - ); - - if (! is_array($body) || ! array_key_exists('success', $body)) { - throw $e; - } - - if (! $this->apiResponseIsError($body) && ! $this->apiResponseContainsData($body)) { - throw $e; - } - - return $body; - } - - /** - * @param array $responseData - */ - protected function apiResponseIsError(array $responseData): bool - { - return array_key_exists('error', $responseData) - && is_array($responseData['error']) - && ! empty($responseData['error']); - } - - /** - * @param array $responseData - */ - protected function apiResponseContainsData(array $responseData): bool - { - return array_key_exists('data', $responseData) - && is_array($responseData['data']); - } - - protected function delay(int $attempt): void - { - $delayMs = $this->apiConfig->retryDelayMs * $attempt; - usleep($delayMs * 1000); - } -} diff --git a/src/Lib/ApiDto/Factory/AbstractDtoFactory.php b/src/Lib/ApiDto/Factory/AbstractDtoFactory.php deleted file mode 100644 index 894ebd9..0000000 --- a/src/Lib/ApiDto/Factory/AbstractDtoFactory.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -abstract class AbstractDtoFactory implements DtoFactory -{ - protected readonly Mapper $model; - - public function __construct(Mapper $model) - { - $this->model = $model; - } - - public function model(): Mapper - { - return $this->model; - } - - protected function paginationDtoFrom(Pagination $pagination): PaginationDto - { - return new PaginationDto( - $pagination->pageNumber(), - $pagination->pageSize(), - 0, - 0, - ); - } - - protected function toPaginationDto( - ApiResponse $response - ): ?PaginationDto { - $apiPagination = $response->pagination; - - if ($apiPagination === null) { - return null; - } - - return new PaginationDto( - $apiPagination->page, - $apiPagination->pageSize, - $apiPagination->totalItems, - $apiPagination->totalPages - ); - } -} diff --git a/src/Lib/ApiDto/Factory/DtoFactory.php b/src/Lib/ApiDto/Factory/DtoFactory.php deleted file mode 100644 index 8b53bf5..0000000 --- a/src/Lib/ApiDto/Factory/DtoFactory.php +++ /dev/null @@ -1,35 +0,0 @@ -) - * - * @phpstan-return ($pagination is null ? D : PageOf) - */ - public function build(ApiResponse $response, ?Pagination $pagination = null): null | Dto | PageOf; -} diff --git a/src/Lib/ApiDto/Factory/EmployeeDtoFactory.php b/src/Lib/ApiDto/Factory/EmployeeDtoFactory.php deleted file mode 100644 index 4bdd1e8..0000000 --- a/src/Lib/ApiDto/Factory/EmployeeDtoFactory.php +++ /dev/null @@ -1,59 +0,0 @@ -model = $model; - } - - /** @return Employee|PageOf|null */ - public function build(ApiResponse $response, ?Pagination $pagination = null): Dto | PageOf | null - { - $paginationDto = $this->toPaginationDto($response); - - // FIXME: L'api ne retourne pas de pagination alors qu'on a fait une demande paginé. - if ($paginationDto === null && $pagination !== null) { - // NOTE: Hack here. - $paginationDto = $this->paginationDtoFrom($pagination); - - return new PageOf($paginationDto, []); - } - - // * Single result - if ($paginationDto === null) { - if ($response->isEmpty()) { - return null; - } - - return $this->model->toDto($response->data); - } - - return new PageOf( - $paginationDto, - $response->mapData(fn (array $data): Employee => $this->model->toDto($data)) - ); - } -} diff --git a/src/Lib/ApiDto/Factory/IncoCompoDtoFactory.php b/src/Lib/ApiDto/Factory/IncoCompoDtoFactory.php deleted file mode 100644 index 6970669..0000000 --- a/src/Lib/ApiDto/Factory/IncoCompoDtoFactory.php +++ /dev/null @@ -1,54 +0,0 @@ -model = $model; - } - - /** - * @return \Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo|PageOf<\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo>|null - */ - public function build(ApiResponse $response, ?Pagination $pagination = null): Dto | PageOf | null - { - $paginationDto = $this->toPaginationDto($response); - - // FIXME: L'api ne retourne pas de pagination alors qu'on a fait une demande paginé. - if ($paginationDto === null && $pagination !== null) { - // NOTE: Hack here. - $paginationDto = $this->paginationDtoFrom($pagination); - - return new PageOf($paginationDto, []); - } - - // * Single result - if ($paginationDto === null) { - if ($response->isEmpty()) { - return null; - } - - return $this->model->toDto($response->data); - } - - throw new \InvalidArgumentException('Pagination not supported'); - } -} diff --git a/src/Lib/ApiDto/Factory/IncoDtoFactory.php b/src/Lib/ApiDto/Factory/IncoDtoFactory.php deleted file mode 100644 index 45faeb3..0000000 --- a/src/Lib/ApiDto/Factory/IncoDtoFactory.php +++ /dev/null @@ -1,56 +0,0 @@ -model = $model; - } - - /** @return Inco|PageOf */ - public function build(ApiResponse $response, ?Pagination $pagination = null): Dto | PageOf | null - { - $paginationDto = $this->toPaginationDto($response); - - // FIXME: L'api ne retourne pas de pagination alors qu'on a fait une demande paginé. - if ($paginationDto === null && $pagination !== null) { - // NOTE: Hack here. - $paginationDto = $this->paginationDtoFrom($pagination); - - return new PageOf($paginationDto, []); - } - - // * Single result - if ($paginationDto === null) { - if ($response->isEmpty()) { - return null; - } - - return $this->model->toDto($response->data); - } - - return new PageOf( - $paginationDto, - $response->mapData(fn (array $data): Inco => $this->model->toDto($data)) - ); - } -} diff --git a/src/Lib/ApiDto/Factory/IncoShortDtoFactory.php b/src/Lib/ApiDto/Factory/IncoShortDtoFactory.php deleted file mode 100644 index 0414d1e..0000000 --- a/src/Lib/ApiDto/Factory/IncoShortDtoFactory.php +++ /dev/null @@ -1,56 +0,0 @@ -model = $model; - } - - /** @return \Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoShort|PageOf<\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoShort> */ - public function build(ApiResponse $response, ?Pagination $pagination = null): Dto | PageOf | null - { - $paginationDto = $this->toPaginationDto($response); - - // FIXME: L'api ne retourne pas de pagination alors qu'on a fait une demande paginé. - if ($paginationDto === null && $pagination !== null) { - // NOTE: Hack here. - $paginationDto = $this->paginationDtoFrom($pagination); - - return new PageOf($paginationDto, []); - } - - // * Single result - if ($paginationDto === null) { - if ($response->isEmpty()) { - return null; - } - - return $this->model->toDto($response->data); - } - - return new PageOf( - $paginationDto, - $response->mapData(fn (array $data): IncoShort => $this->model->toDto($data)) - ); - } -} diff --git a/src/Lib/ApiParser/IncoApiParser.php b/src/Lib/ApiParser/IncoApiParser.php deleted file mode 100644 index a67cb80..0000000 --- a/src/Lib/ApiParser/IncoApiParser.php +++ /dev/null @@ -1,987 +0,0 @@ - $mappedValues - */ - public static function toBoolean(array $mappedValues, string $field, bool $default): bool - { - $fieldValue = self::getArrayKeyValue($mappedValues, $field); - - if (! is_scalar($fieldValue)) { - return false; - } - - if (is_string($fieldValue)) { - return ctype_digit($fieldValue) ? (bool) intval($fieldValue) : $default; - } - - return (bool) intval($fieldValue); - } - - /** @param array $mappedValues */ - public static function toDateTimeFrom(array $mappedValues, string $field): ?\DateTimeInterface - { - $fieldValue = self::getArrayKeyValue($mappedValues, $field); - - return self::toDateTime(is_string($fieldValue) ? $fieldValue : null); - } - - /** @param array $mappedValues */ - public static function toBrand(array $mappedValues): Brand - { - $brand = self::getArrayKeyValue($mappedValues, 'brand'); - - if (! is_array($brand)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco brand to be an array : %s', - json_encode($brand) - ) - ); - } - - $libelle = self::getArrayKeyValue($brand, 'libelle'); - $libelle = IncoApiStringParser::toPhrase(is_string($libelle) ? $libelle : ''); - $code = self::getArrayKeyValue($brand, 'code'); - $code = self::toInt(is_scalar($code) ? (string) $code : ''); - $code = new BrandCode($code); - - return new Brand( - $code, - IncoApiStringParser::protectiveDataFilter($libelle) ?? '' - ); - } - - /** - * @param array $mappedValues - * @return Allergen[] - */ - public static function toAllergens(array $mappedValues): array - { - return array_map( - function ($allergen): Allergen { - if (! is_array($allergen)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco allergen to be an array : %s', - json_encode($allergen) - ) - ); - } - - $libelle = self::getArrayKeyValue($allergen, 'libelle'); - $libelle = IncoApiStringParser::toPhrase(is_string($libelle) ? $libelle : ''); - $code = self::getArrayKeyValue($allergen, 'code'); - $code = self::toInt(is_scalar($code) ? (string) $code : ''); - - return new Allergen( - $code, - IncoApiStringParser::protectiveDataFilter($libelle) ?? '' - ); - }, - self::toArray($mappedValues['allergens']) - ); - } - - /** - * @param array $mappedValues - */ - public static function toAllegations(array $mappedValues): Allegations - { - $allegations = [ - 'allegations' => $mappedValues['allegations'], - 'other' => $mappedValues['allegations_other'], - 'nutrition' => $mappedValues['allegations_nutrition'], - 'health' => $mappedValues['allegations_health'], - ]; - - foreach ($allegations as $cat => $allegation) { - $delimiters = '–,'; - $tok = StringUtils::mbStrtok($delimiters, $allegation); - /** @var string[] */ - $parts = []; - - while ($tok !== false) { - $parts[] = $tok; - $tok = StringUtils::mbStrtok($delimiters); - } - - // * Remove items that start with "aucun" (case-insensitive) - $parts = array_values(array_filter($parts, function (string $allegation): bool { - $lower = mb_strtolower($allegation); - - return strpos($lower, 'aucun') !== 0; - })); - - // * map each item: squish (collapse whitespace), lowercase then ucfirst, ensure ends with '.' then remove last character - $parts = array_map(function (string $allegation): string { - $value = IncoApiStringParser::toPhrase($allegation); - - return IncoApiStringParser::protectiveDataFilter($value) ?? ''; - }, $parts); - - $parts = array_values($parts); - - $allegations[$cat] = $parts; - } - - return new Allegations( - $allegations['allegations'], - $allegations['health'], - $allegations['nutrition'], - $allegations['other'], - ); - } - - /** @param array $mappedValues */ - public static function toIncoCompoId(array $mappedValues): ?IncoCompoId - { - $compo = self::getArrayKeyValue($mappedValues, 'compo'); - - if (! is_array($compo)) { - return null; - } - - $code = self::getArrayKeyValue($compo, 'code'); - - return new IncoCompoId(is_scalar($code) ? (string) $code : ''); - } - - /** - * NOTE: Ici osef du type de portion si aucune valeur numerique n'est donnée pour portion. - * - * @param array $mappedValues - * - * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh - */ - public static function toPortionAmount(array $mappedValues): ?PortionAmount - { - // phpcs:enable - - $raw = self::getArrayKeyValue($mappedValues, 'portions'); - $rawType = self::getArrayKeyValue($mappedValues, 'portion_type'); - - if (is_null($raw) || empty($raw)) { - return null; - } - - $normalized = self::normalizePortions(is_array($raw) ? null : $raw); - $type = self::parsePortionType(is_string($rawType) ? $rawType : null); - - $tokens = self::tokenize($normalized); - $numeric = self::extractNumericParts($tokens); - - if (count($numeric) > 2) { - throw new UnexpectedErrorException('Parts has too many numbers in it ' . json_encode($tokens)); - } - - $low = $numeric[0] ?? null; - $high = $numeric[1] ?? null; - - if (is_numeric($low) && is_numeric($high)) { - if ((float) $low === (float) $high) { - $high = null; - } else { - if ((float) $low > (float) $high) { - [$low, $high] = [$high, $low]; - } - $type = PortionType::approximative; - } - } - - if (is_null($type) && is_numeric($low) && is_null($high)) { - $type = PortionType::exact; - } - - if (! is_null($low)) { - if (is_null($type)) { - throw new UnexpectedErrorException("PortionType was not set with low '{$low}' and high '{$high}'"); - } - - try { - return new PortionAmount( - (float) $low, - $high !== null ? (float) $high : 0.0, - $type === PortionType::approximative - ); - } catch (\InvalidArgumentException) { - // NOTE: PortionAmount may not be correct here. - return null; - } - } - - if (! preg_match('/\d/', $normalized)) { - return null; - } - - throw new UnexpectedErrorException( - "portions was not set with low '{$low}' and high '{$high}' on " . json_encode($normalized) - ); - } - - /** @param array $mappedValues - * @return null|string|int|float|array - */ - public static function getArrayKeyValue(array $mappedValues, string $field): null | string | int | float | array - { - $fieldValue = $mappedValues[$field] ?? null; - - if (! array_key_exists($field, $mappedValues)) { - throw new UnexpectedErrorException( - sprintf('Expected inco field %s to exists', $field) - ); - } - - return $fieldValue; - } - - /** - * @param array $mappedValues - */ - public static function toStorageCondition(array $mappedValues): ?StorageCondition - { - $raw = self::getArrayKeyValue($mappedValues, 'conditConserv'); - - if (is_null($raw) || is_array($raw) || trim((string) $raw) === '') { - return null; - } - - $cleaned = IncoApiStringParser::protectiveDataFilter(trim((string) $raw)); - - if (is_null($cleaned) || trim($cleaned) === '') { - return null; - } - - return new StorageCondition($cleaned); - } - - /** - * @param array $mappedValues - */ - public static function toSellFamilly(array $mappedValues): ?string - { - $sellFamilly = self::getArrayKeyValue($mappedValues, 'familleVte'); - - if (is_null($sellFamilly)) { - return null; - } - - if (! is_array($sellFamilly)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco sell family to be an array : %s', - json_encode($sellFamilly) - ) - ); - } - - $libelle = self::getArrayKeyValue($sellFamilly, 'libelle'); - - return IncoApiStringParser::toPhrase(is_string($libelle) ? $libelle : ''); - } - - /** - * @param array $mappedValues - * @return Additive[] - */ - public static function toAdditives(array $mappedValues): array - { - $raw = self::getArrayKeyValue($mappedValues, 'lstAdditif'); - - if (is_null($raw) || empty($raw)) { - return []; - } - - if (! is_array($raw)) { - throw new UnexpectedErrorException( - sprintf('Expected lstAdditif to be an array, got: %s', json_encode($raw)) - ); - } - - return array_values(array_map(function (mixed $entry): Additive { - if (! is_array($entry)) { - throw new UnexpectedErrorException( - sprintf('Expected lstAdditif entry to be an array, got: %s', json_encode($entry)) - ); - } - - $code = self::getArrayKeyValue($entry, 'code'); - $label = self::getArrayKeyValue($entry, 'libelle'); - - if (! is_string($code) || ! is_string($label)) { - throw new UnexpectedErrorException( - sprintf('Expected lstAdditif code and libelle to be strings, got: %s', json_encode($entry)) - ); - } - - $label = IncoApiStringParser::toPhrase($label); - $label = (string) IncoApiStringParser::protectiveDataFilter($label); - - $code = mb_strtoupper(trim($code)); - /** @var string $code */ - $code = preg_replace('/^A(.*)/', 'E$1', $code) ?? ''; // FIX pour faux E-code. - $code = strtoupper(trim(str_replace(' ', '', $code))); - - // Remettre en minuscules uniquement le suffixe alpha après les chiffres - // ex: "E160AI" -> "E160ai", "E160A(II)" -> "E160a(ii)" - $code = (string) preg_replace_callback( - '/^(E\d+)([A-Z]+(?:\([IVX]+\))?)$/', - static fn (array $m): string => $m[1] . strtolower($m[2]), - $code, - ); - - return new Additive( - $code, - $label, - ); - }, $raw)); - } - - /** - * @param array $mappedValues - */ - public static function toNutriscore(array $mappedValues): ?Nutriscore - { - $raw = self::getArrayKeyValue($mappedValues, 'nutriscore'); - - if (is_null($raw) || is_array($raw) || trim((string) $raw) === '') { - return null; - } - - $raw = mb_strtoupper(trim((string) $raw)); - - while (strlen($raw) > 1) { - if (in_array($raw[0], Nutriscore::VALID_GRADES)) { - $raw = $raw[0]; - - continue; - } - - $raw = substr($raw, 1); - } - - return new Nutriscore($raw); - } - - /** - * @param array $mappedValues - */ - public static function toIncoWeight(array $mappedValues): ?IncoWeight - { - $netRaw = self::getArrayKeyValue($mappedValues, 'pdsNetUvc'); - $brutRaw = self::getArrayKeyValue($mappedValues, 'pdsBrutUvc'); - $moyRaw = self::getArrayKeyValue($mappedValues, 'pdsMoyTheo'); - - if (is_null($netRaw) && is_null($brutRaw) && is_null($moyRaw)) { - return null; - } - - return new IncoWeight( - netUvc: self::toNonNegativeInt($netRaw, 'pdsNetUvc'), - brutUvc: self::toNonNegativeInt($brutRaw, 'pdsBrutUvc'), - moyTheo: self::toNonNegativeInt($moyRaw, 'pdsMoyTheo'), - ); - } - - protected static function toNonNegativeInt(mixed $value, string $field): int - { - if (is_string($value) && ! strlen($value)) { - return 0; - } - - if (! is_string($value) || ! ctype_digit(trim($value))) { - throw new UnexpectedErrorException( - "Expected field '{$field}' to be a non-negative integer string, got: " . json_encode($value) - ); - } - - return (int) $value; - } - - /** - * Parse and format an ingredient list string into a structured array. - * - * Returns a list of ingredient groups. Each group is either: - * - an indexed entry (list of ingredient strings, no category label), or - * - a named entry keyed by the category label (e.g. "Ingrédients", "Traces possibles"). - * - * Ingredients listed under a "Traces possibles" heading are uppercased. - * - * @param array $mappedValues - * - * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh - */ - public static function parseIngredients(array $mappedValues): Ingredients - { - // phpcs:enable - - $ingredientString = strip_tags((string) ($mappedValues['ingredients'] ?? '')); - - if (empty($ingredientString) || mb_strlen($ingredientString) < 2) { - return new Ingredients([]); - } - - // * Replace float numbers with , as sep points (ACK chr 6) - $floatingPointEsc = chr(6); - $ingredientString = (string) preg_replace( - '/(?:([0-9]+),([0-9]+))/', - '$1' . $floatingPointEsc . '$2', - $ingredientString - ); - // * Escape float numbers with . as sep points - $ingredientString = (string) preg_replace( - '/(?:([0-9]+)\.([0-9]+))/', - '$1' . $floatingPointEsc . '$2', - $ingredientString - ); - - // * Fix string (close parentheses if not closed) - try { - $ingredientString = self::fixParentheses($ingredientString); - } catch (\RuntimeException) { - // * If failed, try with smaller parts - $parts = array_filter(explode('.', $ingredientString)); - $fixedParts = array_map( - fn (string $part) => self::removeNonOppennedParentheses(self::fixParentheses($part . ')')), - $parts - ); - $ingredientString = implode('.', $fixedParts); - } - - // * Split into a structured array (token by '.') - $tokenizedString = self::buildTokenizedString($ingredientString); - - $actualKey = null; - $triaged = []; - - foreach ($tokenizedString as $index => $tokenArray) { - if (is_string($index)) { - $actualKey = $index; - } - if (is_null($actualKey)) { - $triaged[] = $tokenArray; - - continue; - } - if (empty($triaged[$actualKey])) { - $triaged[$actualKey] = []; - } - $triaged[$actualKey] = array_merge($triaged[$actualKey], $tokenArray); - } - - // * Reset floating points to original - array_walk_recursive($triaged, function (string &$ingredient) use ($floatingPointEsc): void { - $ingredient = str_replace($floatingPointEsc, '.', $ingredient); - }); - - // * Remove non-opened parentheses - array_walk_recursive($triaged, function (string &$ingredient): void { - $ingredient = self::removeNonOppennedParentheses($ingredient); - }); - - // * Replace "Informations en GRAS" -> "Informations en MAJUSCULE" - array_walk_recursive($triaged, function (string &$ingredient): void { - $ingredient = str_ireplace( - 'Informations en GRAS', - 'Informations en MAJUSCULE', - $ingredient - ); - }); - - // * Uppercase ingredients under any "Traces..." heading - $triaged = self::mapWithKeys( - $triaged, - function (array $value, int | string $index): array { - if (is_string($index) && preg_match('/^Traces?/ui', $index)) { - $value = array_map(fn (string $line) => mb_strtoupper($line), $value); - } - - return [$index => $value]; - } - ); - - // * Extract lines that must be moved to their own top-level entry - /** @var array */ - $getOut = []; - $triaged = self::mapWithKeys( - $triaged, - function (array $value, int | string $index) use (&$getOut): array { - if (count($value) < 2) { - return [$index => $value]; - } - - $outValue = []; - - foreach ($value as $line) { - $ascii = mb_strtolower((string) iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $line)); - - if ( - str_starts_with($ascii, 'fabrique dans') - || str_starts_with($ascii, 'certifie') - || str_starts_with($ascii, 'a consommer') - || str_starts_with($ascii, 'produit fabrique') - ) { - $getOut[] = (object) ['index' => $index, 'value' => $line]; - - continue; - } - - $outValue[] = $line; - } - - return [$index => $outValue]; - } - ); - - foreach ($getOut as $insert) { - $keys = array_keys($triaged); - $pos = (int) array_search($insert->index, $keys, true); - $before = array_splice($triaged, 0, $pos + 1); - $triaged = $before + [$insert->value => []] + $triaged; - } - - return new Ingredients($triaged); - } - - protected static function removeNonOppennedParentheses(string $ingredientPart): string - { - $outString = ''; - $inParentheses = 0; - - foreach (mb_str_split($ingredientPart) as $char) { - if ($char === '(') { - $inParentheses++; - } - if ($char === ')') { - if ($inParentheses === 0) { - continue; - } - $inParentheses--; - } - $outString .= $char; - } - - return $outString; - } - - /** - * Close any unclosed parentheses in an ingredient string. - * - * @throws \RuntimeException If the string is seriously malformed (more than 1 unclosed level). - */ - protected static function fixParentheses(string $ingredientPart): string - { - $inParentheses = 0; - $lastCloseIndex = 0; - $chars = mb_str_split($ingredientPart); - - foreach ($chars as $index => $char) { - if ($char === '(') { - $inParentheses++; - } - if ($char === ')') { - $inParentheses = $inParentheses === 0 ? 0 : $inParentheses - 1; - $lastCloseIndex = (($chars[$index + 1] ?? null) === '.' ? $index + 2 : $index); - } - } - - if ($inParentheses > 1) { - throw new \RuntimeException("Seriously mal formed in : {$ingredientPart}"); - } - - if ($inParentheses) { - $closeAfter = mb_strpos($ingredientPart, '.', $lastCloseIndex); - if ($closeAfter === false) { - $closeAfter = mb_strlen($ingredientPart); - } - $ingredientPart = mb_substr($ingredientPart, 0, (int) $closeAfter) - . ')' - . mb_substr($ingredientPart, (int) $closeAfter); - } - - return $ingredientPart; - } - - /** - * Tokenize an ingredient part string on $charToEscape, ignoring separators inside parentheses. - * - * @return string[] - */ - protected static function tokenIzeIngredientPart( - string $ingredientPartString, - string $charToEscape = ',', - int $limit = -1 - ): array { - // * NAK chr(21) - escape floating-point separators so they are not split - $floatingPointEsc = chr(21); - $ingredientPartString = (string) preg_replace( - '/(?:([0-9]+),([0-9]+))/', - '$1' . $floatingPointEsc . '$2', - $ingredientPartString - ); - $ingredientPartString = (string) preg_replace( - '/(?:([0-9]+)\.([0-9]+))/', - '$1' . $floatingPointEsc . '$2', - $ingredientPartString - ); - - // * DEL chr(127) - escape separator characters that are inside parentheses or brackets - $escapeChar = chr(127); - $inParentheses = 0; - $chars = mb_str_split($ingredientPartString); - $result = []; - - foreach ($chars as $char) { - if (in_array($char, ['(', '['], true)) { - $inParentheses++; - } - if (in_array($char, [')', ']'], true)) { - $inParentheses = $inParentheses === 0 ? 0 : $inParentheses - 1; - } - // * Inside parentheses: escape the split character AND also ':' to avoid - // sub-tokenization treating "gélifiant: carraghénanes" as a section label - if ($inParentheses > 0 && $char === $charToEscape) { - $result[] = $escapeChar; - } else { - $result[] = $char; - } - } - - $ingredientPartString = implode('', $result); - - $delimiters = "\n\t{$charToEscape}"; - $tok = StringUtils::mbStrtok($delimiters, $ingredientPartString); - /** @var string[] */ - $parts = []; - - while ($tok !== false) { - $parts[] = $tok; - $tok = StringUtils::mbStrtok($delimiters); - } - - // * ESC chr(27) - apply $limit by joining/re-splitting - $limitChar = chr(27); - if ($limit !== -1) { - $joined = implode($limitChar, $parts); - $split = explode($limitChar, $joined, $limit); - $parts = array_map( - fn (string $part) => str_replace([$limitChar, $floatingPointEsc], ['', '.'], $part), - $split - ); - } - - // * Final cleanup: restore escaped separators and floating points, squish, ucfirst - $parts = array_filter($parts, fn (string $p) => trim($p) !== ''); - $parts = array_map(function (string $ingredient) use ($escapeChar, $charToEscape, $floatingPointEsc): string { - $ingredient = str_replace($escapeChar, $charToEscape, $ingredient); - $ingredient = str_replace($floatingPointEsc, '.', $ingredient); - // squish - $ingredient = trim((string) preg_replace('/\s+/u', ' ', $ingredient)); - // remove trailing dot - $ingredient = rtrim($ingredient, '.'); - - // ucfirst on first word character - return self::ucFirstOnWordChar($ingredient); - }, $parts); - - return array_values($parts); - } - - /** - * Apply mb-aware ucfirst on the first word character of a string, - * but only if that character is NOT immediately preceded by a digit. - * This prevents "105g" from becoming "105G". - */ - protected static function ucFirstOnWordChar(string $string): string - { - if (preg_match('/\p{L}/u', $string, $matches, PREG_OFFSET_CAPTURE) !== 1) { - return $string; - } - - $match = $matches[0]; - $byteOffset = $match[1]; - - // * Do not uppercase a letter that immediately follows a digit (e.g. "105g") - if ($byteOffset > 0 && ctype_digit(mb_substr($string, $byteOffset - 1, 1))) { - return $string; - } - - $firstChar = mb_strtoupper($match[0]); - - return mb_substr($string, 0, $byteOffset) - . $firstChar - . mb_substr($string, $byteOffset + mb_strlen($match[0])); - } - - /** - * Build the top-level tokenized structure from the full ingredient string. - * - * Returns an array where each entry is either: - * - int key -> string[] of ingredient tokens (no section label found) - * - string key -> string[] of ingredient tokens (section label used as key) - * - * @return array - */ - protected static function buildTokenizedString(string $ingredientString): array - { - $sentenceTokens = self::tokenIzeIngredientPart($ingredientString, '.'); - - $result = []; - - foreach ($sentenceTokens as $index => $tokenString) { - $elements = self::tokenIzeIngredientPart($tokenString, ':', 2); - - $labelRaw = $elements[0] ?? ''; - $labelSlug = StringUtils::slugify($labelRaw); - $hasLabel = count($elements) > 1; - - $trimmedLabel = trim($labelRaw); - $labelIsRatio = (bool) preg_match('/[\d%]/', $trimmedLabel); - $labelHasParen = (bool) preg_match('/[()]/', $trimmedLabel); - $labelIsTooLong = mb_strlen($trimmedLabel) > 40; - - $tokenSlug = StringUtils::slugify($tokenString); - $isInfoLine = str_starts_with($tokenSlug, 'fabrique dans') - || str_starts_with($tokenSlug, 'certifie') - || str_starts_with($tokenSlug, 'a consommer') - || str_starts_with($tokenSlug, 'produit fabrique'); - - if ($isInfoLine) { - continue; - } - - // * "INGREDIENTS :" header → discard the label, keep only the ingredient content - if ($hasLabel && strpos($labelSlug, 'ingredient') !== false) { - $ingredientPart = $elements[1] ?? ''; - $result[$index] = self::flattenIngredientTokens( - self::tokenIzeIngredientPart($ingredientPart) - ); - - continue; - } - - // * "Traces ..." / "Décor ..." / "Allergènes ..." → named group, keep the label as key - // but extract the first item from the value part (e.g. "LAIT" after the ':') - $isNamedSpecialGroup = $hasLabel && ( - str_starts_with($labelSlug, 'trace') || str_starts_with($labelSlug, 'decor') || str_starts_with($labelSlug, 'allergene') - ); - - if ($isNamedSpecialGroup) { - $label = trim((string) preg_replace('/\s+/u', ' ', mb_strtolower($labelRaw))); - $label = mb_strtoupper(mb_substr($label, 0, 1)) . mb_substr($label, 1); - $ingredientPart = $elements[1] ?? ''; - $result[$label] = self::flattenIngredientTokens( - self::tokenIzeIngredientPart($ingredientPart) - ); - - continue; - } - - if ( - $hasLabel && ! $labelIsRatio && ! $labelHasParen && ! $labelIsTooLong && strpos($labelSlug, 'fabrique') !== 0 - ) { - $label = trim((string) preg_replace('/\s+/u', ' ', mb_strtolower($labelRaw))); - $label = mb_strtoupper(mb_substr($label, 0, 1)) . mb_substr($label, 1); - $ingredientPart = $elements[1] ?? ''; - $result[$label] = self::flattenIngredientTokens( - self::tokenIzeIngredientPart($ingredientPart) - ); - } else { - $result[$index] = self::flattenIngredientTokens( - self::tokenIzeIngredientPart($tokenString) - ); - } - } - - return $result; - } - - /** - * Flatten orphan parenthesised sub-lists inside a token list. - * - * When a token starts with '(' it means the parser could not split it because - * the opening parenthesis was never closed (orphan sub-list). In that case we: - * 1. Strip the wrapping parentheses. - * 2. Re-tokenize the inner content as a normal comma-separated list. - * 3. Splice the resulting tokens in place of the original token. - * - * Example input tokens: - * ["Viande de dinde (origine france) : 93%", - * "(Viande de dinde : 81%, marinade : 12% (eau, acidifiants : E262-E331, sel, arômes naturels), poivrons verts : 7%)"] - * - * Example output tokens: - * ["Viande de dinde (origine france) : 93%", - * "Viande de dinde : 81%", - * "Marinade : 12% (eau, acidifiants : E262-E331, sel, arômes naturels)", - * "Poivrons verts : 7%"] - * - * @param string[] $tokens - * @return string[] - */ - protected static function flattenIngredientTokens(array $tokens): array - { - $flattened = []; - - foreach ($tokens as $token) { - $trimmed = trim($token); - - // * Detect an orphan sub-list: token starts with '(' and ends with ')' - // * (fixParentheses will have closed it, so it always ends with ')') - if ( - mb_substr($trimmed, 0, 1) === '(' && mb_substr($trimmed, -1) === ')' - ) { - // * Strip outer parentheses and re-tokenize as a normal ingredient list - $inner = mb_substr($trimmed, 1, mb_strlen($trimmed) - 2); - $subItems = self::tokenIzeIngredientPart($inner, ','); - - foreach ($subItems as $subItem) { - if (trim($subItem) !== '') { - $flattened[] = $subItem; - } - } - - continue; - } - - $flattened[] = $token; - } - - return $flattened; - } - - /** - * array_map variant that preserves and transforms keys, similar to Laravel's Arr::mapWithKeys. - * - * The callback receives ($value, $key) and must return a single-element associative array. - * - * @template TKey of int|string - * @template TValue - * - * @param array $array - * @param callable(TValue, TKey): array $callback - * @return array - */ - protected static function mapWithKeys(array $array, callable $callback): array - { - $result = []; - - foreach ($array as $key => $value) { - $assoc = $callback($value, $key); - foreach ($assoc as $k => $v) { - $result[$k] = $v; - } - } - - return $result; - } - - protected static function toInt(string $value, ?int $default = null): int - { - if (! ctype_digit($value) || trim($value) === '') { - if ($default !== null) { - return $default; - } - - throw new UnexpectedErrorException("Expected value to be an int, string given '{$value}'"); - } - - return intval($value); - } - - protected static function toDateTime(?string $datetime): ?\DateTimeInterface - { - return is_null($datetime) ? null : DateUtils::parseDateTime($datetime); - } - - /** - * @param array|string $value - * @return array - */ - protected static function toArray(string | array $value): array - { - if (is_string($value)) { - return trim($value) === '' ? [] : [$value]; - } - - return $value; - } - - protected static function parsePortionType(?string $typePortion): ?PortionType - { - if ($typePortion === null) { - return null; - } - - $needle = mb_strtolower($typePortion); - - $matches = array_values(array_filter( - PortionType::cases(), - fn (PortionType $type) => $type->description() === $needle - )); - - return $matches[0] ?? null; - } - - protected static function normalizePortions(string | int | float | null $value): string - { - if (! is_string($value)) { - $value = (string) $value; - } - $value = trim($value); - $value = str_replace(',', '.', $value); - - return $value; - } - - /** - * @return list - */ - protected static function tokenize(string $value): array - { - $delimiters = " \n\tà/\\-_de"; - $tok = StringUtils::mbStrtok($delimiters, $value); - $parts = []; - while ($tok !== false) { - $parts[] = $tok; - $tok = StringUtils::mbStrtok($delimiters); - } - - return $parts; - } - - /** - * @param list $parts - * @return list - */ - protected static function extractNumericParts(array $parts): array - { - return array_values(array_filter($parts, fn ($p) => is_numeric($p))); - } -} diff --git a/src/Lib/ApiParser/IncoApiStringParser.php b/src/Lib/ApiParser/IncoApiStringParser.php deleted file mode 100644 index 7113432..0000000 --- a/src/Lib/ApiParser/IncoApiStringParser.php +++ /dev/null @@ -1,217 +0,0 @@ -substr(0, -1)) - if (strlen($ucfirst)) { - $ucfirst = mb_substr($ucfirst, 0, mb_strlen($ucfirst) - 1); - } - - return $ucfirst; - } - - public static function beautifyName(string $name): string - { - // run beautifier (may return null) - $name = self::beautifyText($name) ?? ''; - $name = trim($name); - if ($name === '') { - return $name; - } - - // Title case (mb aware) and collapse whitespace - $name = mb_convert_case(mb_strtolower($name, 'UTF-8'), MB_CASE_TITLE, 'UTF-8'); - $name = preg_replace('/\s+/u', ' ', $name) ?? ''; - $name = trim($name); - - // Ensure sentence-like formatting: add trailing dot then remove it to mimic finish()->substr(0,-1) - if ($name !== '') { - $name = rtrim($name, " \t\n\r\0\x0B"); - if (! preg_match('/[.:!?\…;,]$/u', $name)) { - $name .= '.'; - } - // remove the final dot as original code did - $name = mb_substr($name, 0, mb_strlen($name, 'UTF-8') - 1, 'UTF-8'); - } - - // remove leading colon if present - if (str_starts_with($name, ':')) { - $name = mb_substr($name, 1, null, 'UTF-8'); - $name = trim($name); - } - - return $name; - } - - /** - * Nettoie et normalise une ligne texte (pas de HTML, pas de contrôles non désirés). - */ - public static function protectiveDataFilter(?string $line): ?string - { - if ($line === null || $line === '') { - return $line; - } - - // Decode HTML entities then strip tags - $line = html_entity_decode($line, ENT_QUOTES | ENT_HTML5, 'UTF-8'); - $line = strip_tags($line); - - // Normalize newlines, temporarily convert to
to preserve paragraph breaks - $line = str_replace(["\r\n", "\r"], "\n", $line); - $line = str_replace("\n", '
', $line); - - // Ensure valid UTF-8 - $line = StringEncoder::detectAndConvertToUtf8($line); - - // Remove astral symbols (4-byte UTF-8) that may break storage/processing - $line = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $line) ?? ''; - - // Remove control/non-printable characters but keep TAB(0x09), LF(0x0A) and CR(0x0D) if needed - $line = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F\xA0\xAD]/u', '', $line) ?? ''; - - // Revert
back to newlines and final strip_tags as double safety - $line = str_replace('
', "\n", $line); - $line = strip_tags($line); - - return $line; - } - - /** - * Beautify text - * - * @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh - */ - public static function beautifyText(?string $text): ?string - { - // phpcs:enable - if ($text === null) { - return null; - } - - $floatingPointEsc = chr(21); - $s = preg_replace('/(\d+)[,\.](\d+)/', '$1' . $floatingPointEsc . '$2', $text) ?? ''; - - $mbUcfirst = function (string $str): string { - if ($str === '') { - return $str; - } - $first = mb_substr($str, 0, 1); - $rest = mb_substr($str, 1); - - return mb_strtoupper($first) . $rest; - }; - - $squish = function (string $str): string { - return trim((string) preg_replace('/\s+/u', ' ', $str)); - }; - - $startsWith = function (string $hay, string | array $needles): bool { - foreach ((array) $needles as $n) { - if ($n !== '' && mb_strpos($hay, $n) === 0) { - return true; - } - } - - return false; - }; - - $endsWith = function (string $hay, string | array $needles): bool { - foreach ((array) $needles as $n) { - if ($n !== '' && mb_substr($hay, -mb_strlen($n)) === $n) { - return true; - } - } - - return false; - }; - - $s = preg_replace_callback( - '/(\w)\s*\.(\s*)(\w)/mu', - fn ($m) => $m[1] . '. ' . $m[2] . mb_strtoupper($m[3]), - $s - ) ?? ''; - - $s = preg_replace_callback( - '/(\w)(\s*\n+\s*)(\p{Lu})/mu', - fn ($m) => $m[1] . '.' . $m[2] . $m[3], - $s - ) ?? ''; - - $lines = preg_split('/\R/u', $s); - $out = []; - - if ($lines === false) { - return $s; - } - - foreach ($lines as $line) { - $phrases = array_filter(array_map('trim', explode('.', $line)), fn ($p) => $p !== ''); - $phrased = []; - - foreach ($phrases as $phrase) { - $p = $squish($phrase); - $p = $mbUcfirst(mb_strtolower($p)); - - if ($startsWith($p, "'") && ! $endsWith($p, "'")) { - $p = mb_substr($p, 1); - } - if (! $startsWith($p, "'") && $endsWith($p, "'")) { - $p = mb_substr($p, 0, -1); - } - - $wordCount = preg_match_all('/\p{L}+/u', $p); - if ($wordCount === 1) { - $p = preg_replace('/,\s*et\s+/u', ' et ', $p); - $phrased[] = $p; - - continue; - } - - if (! $endsWith($p, [':', ',', '!', '?', ';', '…'])) { - $p .= '.'; - } else { - $p = preg_replace('/:/u', ': ', $p) ?? ''; - } - - $p = preg_replace('/,\s*et\s+/u', ' et ', $p) ?? ''; - $p = $squish($p); - - $phrased[] = $p; - } - - $out[] = implode(' ', $phrased); - } - - $result = implode("\n", $out); - $result = str_replace($floatingPointEsc, '.', $result); - $text = $result; - - return $result; - } -} diff --git a/src/Lib/ApiParser/IncoCompoApiParser.php b/src/Lib/ApiParser/IncoCompoApiParser.php deleted file mode 100644 index ffabc9c..0000000 --- a/src/Lib/ApiParser/IncoCompoApiParser.php +++ /dev/null @@ -1,360 +0,0 @@ - $mappedValues - * @return null|string|int|float|array - */ - public static function getArrayKeyValue(array $mappedValues, string $field): null | string | int | float | array - { - if (! array_key_exists($field, $mappedValues)) { - throw new UnexpectedErrorException( - sprintf('Expected field "%s" to exist in mapped values.', $field) - ); - } - - return $mappedValues[$field]; - } - - /** - * Safely extracts a nullable string from the mapped array. - * Converts numeric values to their string representation. - * - * @param array $mapped - */ - public static function toString(array $mapped, string $field): string - { - $value = self::getArrayKeyValue($mapped, $field); - - return match (true) { - is_null($value) => '', - is_string($value) => $value, - is_int($value), is_float($value) => (string) $value, - default => throw new UnexpectedErrorException( - sprintf('Expected field "%s" to be a scalar, got %s.', $field, get_debug_type($value)) - ), - }; - } - - // ========================================================================= - // Private sub-DTO factories - // ========================================================================= - - /** @param array $m */ - public static function toIdentification(array $m): Identification - { - return new Identification( - nom: self::toString($m, 'nom'), - numero: self::toString($m, 'numero'), - source: self::toString($m, 'source'), - ); - } - - /** @param array $m */ - public static function toEnergie(array $m): Energie - { - return new Energie( - tailleReference: self::toString($m, 'tailleReference'), - valEnergKjReference: self::toString($m, 'valEnergKjReference'), - valEnergKcalReference: self::toString($m, 'valEnergKcalReference'), - taillePortion: self::toString($m, 'taillePortion'), - valEnergKjPortion: self::toString($m, 'valEnergKjPortion'), - valEnergKcalPortion: self::toString($m, 'valEnergKcalPortion'), - ); - } - - /** @param array $m */ - public static function toMacronutrimentsRef(array $m): MacronutrimentsRef - { - return new MacronutrimentsRef( - matGrassReference: self::toString($m, 'matGrassReference'), - matGrassReferencePrec: self::toString($m, 'matGrassReferencePrec'), - acideGrasSatureReference: self::toString($m, 'acideGrasSatureReference'), - acideGrasSatureReferencePrec: self::toString($m, 'acideGrasSatureReferencePrec'), - glucidesReference: self::toString($m, 'glucidesReference'), - glucidesReferencePrec: self::toString($m, 'glucidesReferencePrec'), - sucreReference: self::toString($m, 'sucreReference'), - sucreReferencePrec: self::toString($m, 'sucreReferencePrec'), - fibAlimReference: self::toString($m, 'fibAlimReference'), - fibAlimReferencePrec: self::toString($m, 'fibAlimReferencePrec'), - proteineReference: self::toString($m, 'proteineReference'), - proteineReferencePrec: self::toString($m, 'proteineReferencePrec'), - selReference: self::toString($m, 'selReference'), - selReferencePrec: self::toString($m, 'selReferencePrec'), - sodiumReference: self::toString($m, 'sodiumReference'), - ); - } - - /** @param array $m */ - public static function toMacronutrimentsPortion(array $m): MacronutrimentsPortion - { - return new MacronutrimentsPortion( - matGrassPortion: self::toString($m, 'matGrassPortion'), - matGrassPortionPrec: self::toString($m, 'matGrassPortionPrec'), - acideGrasSaturePortion: self::toString($m, 'acideGrasSaturePortion'), - acideGrasSaturePortionPrec: self::toString($m, 'acideGrasSaturePortionPrec'), - glucidesPortion: self::toString($m, 'glucidesPortion'), - glucidesPortionPrec: self::toString($m, 'glucidesPortionPrec'), - sucrePortion: self::toString($m, 'sucrePortion'), - sucrePortionPrec: self::toString($m, 'sucrePortionPrec'), - fibAlimPortion: self::toString($m, 'fibAlimPortion'), - fibAlimPortionPrec: self::toString($m, 'fibAlimPortionPrec'), - proteinePortion: self::toString($m, 'proteinePortion'), - proteinePortionPrec: self::toString($m, 'proteinePortionPrec'), - selPortion: self::toString($m, 'selPortion'), - selPortionPrec: self::toString($m, 'selPortionPrec'), - sodiumPortion: self::toString($m, 'sodiumPortion'), - ); - } - - /** @param array $m */ - public static function toFnl(array $m): FruitLegumeNoix - { - return new FruitLegumeNoix( - fruitsReference: self::toString($m, 'fruitsReference'), - fruitsCoqueSechReference: self::toString($m, 'fruitsCoqueSechReference'), - fruitsPortion: self::toString($m, 'fruitsPortion'), - fruitsCoqueSechPortion: self::toString($m, 'fruitsCoqueSechPortion'), - fruitLegumeNoixFrais: self::toString($m, 'fruitLegumeNoixFrais'), - fruitLegumeNoixFraisPortion: self::toString($m, 'fruitLegumeNoixFraisPortion'), - pourcentFruitsLegumesNoixFrais: self::toString($m, 'pourcentFruitsLegumesNoixFrais'), - pourcentFruits: self::toString($m, 'pourcentFruits'), - ); - } - - /** @param array $m */ - public static function toApportsReference(array $m): ApportsReference - { - return new ApportsReference( - pourcentApportRefE14P: self::toString($m, 'pourcentApportRefE14P'), - pourcentApportRefKjoP: self::toString($m, 'pourcentApportRefKjoP'), - pourcentApportRefFatP: self::toString($m, 'pourcentApportRefFatP'), - pourcentApportRefFatsatP: self::toString($m, 'pourcentApportRefFatsatP'), - pourcentApportRefChovalP: self::toString($m, 'pourcentApportRefChovalP'), - pourcentApportRefSugarP: self::toString($m, 'pourcentApportRefSugarP'), - pourcentApportRefProP: self::toString($m, 'pourcentApportRefProP'), - pourcentApportRefSalteqP: self::toString($m, 'pourcentApportRefSalteqP'), - apportPort: self::toString($m, 'apportPort'), - pSurL: self::toString($m, 'pSurL'), - ); - } - - /** @param array $m */ - public static function toNutriScore(array $m): NutriScore - { - return new NutriScore( - composanteN: self::toString($m, 'composanteN'), - composanteN2022: self::toString($m, 'composanteN2022'), - composanteP: self::toString($m, 'composanteP'), - composanteP2022: self::toString($m, 'composanteP2022'), - scoreNutri: self::toString($m, 'scoreNutri'), - scoreNutri2022: self::toString($m, 'scoreNutri2022'), - classement: self::toString($m, 'classement'), - nutriScore2022: self::toString($m, 'nutriScore2022'), - nutriAArt: self::toString($m, 'nutriAArt'), - viandeRouge: self::toString($m, 'viandeRouge'), - ); - } - - /** @param array $m */ - public static function toPointsNutriScore(array $m): PointsNutriScore - { - return new PointsNutriScore( - pointsEnergie: self::toString($m, 'pointsEnergie'), - pointsAcideGrasSature: self::toString($m, 'pointsAcideGrasSature'), - pointsSucres: self::toString($m, 'pointsSucres'), - pointsSel: self::toString($m, 'pointsSel'), - pointsFibres: self::toString($m, 'pointsFibres'), - pointsProteines: self::toString($m, 'pointsProteines'), - pointsFruitsLegumesNoixFrais: self::toString($m, 'pointsFruitsLegumesNoixFrais'), - ); - } - - /** @param array $m */ - public static function toMineraux(array $m): Mineraux - { - return new Mineraux( - nutCa: self::toString($m, 'nutCa'), - nutMg: self::toString($m, 'nutMg'), - nutK: self::toString($m, 'nutK'), - nutP: self::toString($m, 'nutP'), - nutCld: self::toString($m, 'nutCld'), - nutFe: self::toString($m, 'nutFe'), - nutZn: self::toString($m, 'nutZn'), - nutCu: self::toString($m, 'nutCu'), - nutMn: self::toString($m, 'nutMn'), - nutSe: self::toString($m, 'nutSe'), - nutId: self::toString($m, 'nutId'), - nutCr: self::toString($m, 'nutCr'), - nutMo: self::toString($m, 'nutMo'), - nutS6: self::toString($m, 'nutS6'), - nutNitra: self::toString($m, 'nutNitra'), - ); - } - - /** @param array $m */ - public static function toVitamines(array $m): Vitamines - { - return new Vitamines( - nutVita: self::toString($m, 'nutVita'), - nutVitb6: self::toString($m, 'nutVitb6'), - nutVitb12: self::toString($m, 'nutVitb12'), - nutVitc: self::toString($m, 'nutVitc'), - nutVitd: self::toString($m, 'nutVitd'), - nutVite: self::toString($m, 'nutVite'), - nutVitk: self::toString($m, 'nutVitk'), - nutChocal: self::toString($m, 'nutChocal'), - nutNia: self::toString($m, 'nutNia'), - nutRibf: self::toString($m, 'nutRibf'), - nutThia: self::toString($m, 'nutThia'), - nutPantac: self::toString($m, 'nutPantac'), - nutBiot: self::toString($m, 'nutBiot'), - nutFoldfe: self::toString($m, 'nutFoldfe'), - ); - } - - /** @param array $m */ - public static function toGlucidesDetail(array $m): GlucidesDetail - { - return new GlucidesDetail( - nutStarch: self::toString($m, 'nutStarch'), - nutMaltdex: self::toString($m, 'nutMaltdex'), - nutSucs: self::toString($m, 'nutSucs'), - nutGlus: self::toString($m, 'nutGlus'), - nutLacs: self::toString($m, 'nutLacs'), - nutMals: self::toString($m, 'nutMals'), - nutGalfb: self::toString($m, 'nutGalfb'), - nutGHc: self::toString($m, 'nutGHc'), - nutFrufb: self::toString($m, 'nutFrufb'), - nutFrus: self::toString($m, 'nutFrus'), - nutOlsac: self::toString($m, 'nutOlsac'), - nutPolyl: self::toString($m, 'nutPolyl'), - nutFibsol: self::toString($m, 'nutFibsol'), - nutInotl: self::toString($m, 'nutInotl'), - ); - } - - /** @param array $m */ - public static function toAcideGras(array $m): AcideGras - { - return new AcideGras( - // Lipides généraux - nutFd: self::toString($m, 'nutFd'), - nutChol: self::toString($m, 'nutChol'), - nutAgt: self::toString($m, 'nutAgt'), - // AGS - nutFformiq: self::toString($m, 'nutFformiq'), - nutFacetiq: self::toString($m, 'nutFacetiq'), - nutFpropioniq: self::toString($m, 'nutFpropioniq'), - nutFbutyriq: self::toString($m, 'nutFbutyriq'), - nutFvaleriq: self::toString($m, 'nutFvaleriq'), - nutFcaproiq: self::toString($m, 'nutFcaproiq'), - nutFenatiq: self::toString($m, 'nutFenatiq'), - nutFcapryliq: self::toString($m, 'nutFcapryliq'), - nutFpelargoniq: self::toString($m, 'nutFpelargoniq'), - nutFcapriq: self::toString($m, 'nutFcapriq'), - nutFundecyliq: self::toString($m, 'nutFundecyliq'), - nutFlauriq: self::toString($m, 'nutFlauriq'), - nutFtridecyliq: self::toString($m, 'nutFtridecyliq'), - nutFmyristiq: self::toString($m, 'nutFmyristiq'), - nutFpentadecyliq: self::toString($m, 'nutFpentadecyliq'), - nutF16d0: self::toString($m, 'nutF16d0'), - nutFmargariq: self::toString($m, 'nutFmargariq'), - nutFsteariq: self::toString($m, 'nutFsteariq'), - nutFarachidiq: self::toString($m, 'nutFarachidiq'), - nutFheneicosyliq: self::toString($m, 'nutFheneicosyliq'), - nutFbeheniq: self::toString($m, 'nutFbeheniq'), - nutFtricosiliq: self::toString($m, 'nutFtricosiliq'), - nutFlignoceriq: self::toString($m, 'nutFlignoceriq'), - nutFcarboceriq: self::toString($m, 'nutFcarboceriq'), - nutFcerotiq: self::toString($m, 'nutFcerotiq'), - nutFceroplastiq: self::toString($m, 'nutFceroplastiq'), - nutFmontaniq: self::toString($m, 'nutFmontaniq'), - nutFnonacosyliq: self::toString($m, 'nutFnonacosyliq'), - nutFmelissiq: self::toString($m, 'nutFmelissiq'), - nutFlaceroiq: self::toString($m, 'nutFlaceroiq'), - nutFhentriacontyliq: self::toString($m, 'nutFhentriacontyliq'), - nutFhextriacontyliq: self::toString($m, 'nutFhextriacontyliq'), - nutFheptatriacontyliq: self::toString($m, 'nutFheptatriacontyliq'), - nutFoctatriacontyliq: self::toString($m, 'nutFoctatriacontyliq'), - nutFnonadylique: self::toString($m, 'nutFnonadylique'), - nutFpsylliq: self::toString($m, 'nutFpsylliq'), - // AGMI - nutFpalm: self::toString($m, 'nutFpalm'), - nutFoleiq: self::toString($m, 'nutFoleiq'), - nutF18d: self::toString($m, 'nutF18d'), - nutF19d: self::toString($m, 'nutF19d'), - nutF11eico: self::toString($m, 'nutF11eico'), - nutFgeddiq: self::toString($m, 'nutFgeddiq'), - nutFeruciq: self::toString($m, 'nutFeruciq'), - nutFnervoniq: self::toString($m, 'nutFnervoniq'), - nutFhyeniq: self::toString($m, 'nutFhyeniq'), - nutFamscis: self::toString($m, 'nutFamscis'), - // AGPI - nutF18d2cn6: self::toString($m, 'nutF18d2cn6'), - nutF18d3n3: self::toString($m, 'nutF18d3n3'), - nutF20d4: self::toString($m, 'nutF20d4'), - nutF20d5n3: self::toString($m, 'nutF20d5n3'), - nutF22d6n3: self::toString($m, 'nutF22d6n3'), - nutFapun3: self::toString($m, 'nutFapun3'), - nutFapun6: self::toString($m, 'nutFapun6'), - nutFapun9: self::toString($m, 'nutFapun9'), - nutFapucis: self::toString($m, 'nutFapucis'), - nutFapulc: self::toString($m, 'nutFapulc'), - ); - } - - /** @param array $m */ - public static function toAcidesAmines(array $m): AcidesAmines - { - return new AcidesAmines( - nutAae: self::toString($m, 'nutAae'), - nutCasn: self::toString($m, 'nutCasn'), - nutWhey: self::toString($m, 'nutWhey'), - ); - } - - /** @param array $m */ - public static function toComposesSpeciaux(array $m): ComposesSpeciaux - { - return new ComposesSpeciaux( - nutCholn: self::toString($m, 'nutCholn'), - nutCarnitine: self::toString($m, 'nutCarnitine'), - nutTau: self::toString($m, 'nutTau'), - nutNucleotide: self::toString($m, 'nutNucleotide'), - ); - } -} diff --git a/src/Lib/ApiQueryBuilder/Direction.php b/src/Lib/ApiQueryBuilder/Direction.php deleted file mode 100644 index a884872..0000000 --- a/src/Lib/ApiQueryBuilder/Direction.php +++ /dev/null @@ -1,11 +0,0 @@ -'; - case GREATER_THAN_OR_EQUAL = '>='; - case LESS_THAN = '<'; - case LESS_THAN_OR_EQUAL = '<='; - case BETWEEN = 'BETWEEN'; - case NOT_BETWEEN = 'NOT BETWEEN'; - case LIKE = 'LIKE'; - case NOT_LIKE = 'NOT LIKE'; - case IN = 'IN'; - case NOT_IN = 'NOT IN'; -} diff --git a/src/Lib/ApiQueryBuilder/TableQuery.php b/src/Lib/ApiQueryBuilder/TableQuery.php deleted file mode 100644 index f3c5028..0000000 --- a/src/Lib/ApiQueryBuilder/TableQuery.php +++ /dev/null @@ -1,102 +0,0 @@ - $selectColumns - * @param list $criteria - * @param list $orderClauses - */ - public function __construct( - public string $table, - public ?string $soc = null, - public ?string $lang = null, - public array $selectColumns = [], - public array $criteria = [], - public array $orderClauses = [], - public ?int $page = null, - public ?int $pageSize = null, - ) { - } - - public function __toString(): string - { - $params = []; - - $params['table'] = $this->table; - - if ($this->soc !== null) { - $params['soc'] = $this->soc; - } - - if ($this->lang !== null) { - $params['lang'] = $this->lang; - } - - if ($this->selectColumns !== []) { - $params['s_cols'] = implode('|', $this->selectColumns); - } - - if ($this->criteria !== []) { - $params['c_cols'] = implode('|', array_column($this->criteria, 'column')); - $params['c_opes'] = implode('|', array_map( - static fn (array $c): string => $c['operator']->value, - $this->criteria, - )); - $params['c_vals'] = implode('|', array_column($this->criteria, 'value')); - } - - if ($this->orderClauses !== []) { - $params['t_cols'] = implode('|', array_column($this->orderClauses, 'column')); - $params['t_opes'] = implode('|', array_map( - static fn (array $o): string => $o['direction']->value, - $this->orderClauses, - )); - } - - if ($this->page !== null) { - $params['page'] = (string) $this->page; - } - - if ($this->pageSize !== null) { - $params['page_size'] = (string) $this->pageSize; - } - - $parts = []; - foreach ($params as $key => $value) { - $parts[] = $key . '=' . $value; - } - - return implode('&', $parts); - } - - /** - * @return array - */ - public function toArray(): array - { - return array_filter([ - 'table' => $this->table, - 'soc' => $this->soc, - 'lang' => $this->lang, - 's_cols' => $this->selectColumns !== [] ? implode('|', $this->selectColumns) : null, - 'c_cols' => $this->criteria !== [] ? implode('|', array_column($this->criteria, 'column')) : null, - 'c_opes' => $this->criteria !== [] ? implode('|', array_map( - static fn (array $c): string => $c['operator']->value, - $this->criteria, - )) : null, - 'c_vals' => $this->criteria !== [] ? implode('|', array_column($this->criteria, 'value')) : null, - 't_cols' => $this->orderClauses !== [] ? implode('|', array_column($this->orderClauses, 'column')) : null, - 't_opes' => $this->orderClauses !== [] ? implode('|', array_map( - static fn (array $o): string => $o['direction']->value, - $this->orderClauses, - )) : null, - 'page' => $this->page, - 'page_size' => $this->pageSize, - ], static fn (mixed $v): bool => $v !== null); - } -} diff --git a/src/Lib/ApiQueryBuilder/TableQueryBuildStep.php b/src/Lib/ApiQueryBuilder/TableQueryBuildStep.php deleted file mode 100644 index b2c3704..0000000 --- a/src/Lib/ApiQueryBuilder/TableQueryBuildStep.php +++ /dev/null @@ -1,12 +0,0 @@ - */ - private array $selectColumns = []; - - /** @var list */ - private array $criteria = []; - - /** @var list */ - private array $orderClauses = []; - - private ?Pagination $pagination = null; - - protected function __construct() - { - // PRIVATE - } - - public static function query(): TableQueryFromStep - { - return new self(); - } - - // * FromStep - - public function from(string $table): TableQuerySelectStep - { - if (trim($table) === '') { - throw new \InvalidArgumentException('Table name must not be empty.'); - } - - $this->table = $table; - - return $this; - } - - // * SelectStep - - public function society(string $soc): static - { - $this->soc = $soc; - - return $this; - } - - public function language(string $lang): static - { - $this->lang = $lang; - - return $this; - } - - public function select(string ...$columns): TableQueryWhereStep - { - $this->selectColumns = array_values($columns); - - return $this; - } - - public function selectAll(): TableQueryWhereStep - { - $this->selectColumns = []; - - return $this; - } - - // * WhereStep - - public function where(string $column, Operator $operator, string $value): TableQueryWhereStep - { - $this->criteria[] = [ - 'column' => $column, - 'operator' => $operator, - 'value' => $value, - ]; - - return $this; - } - - public function whereEqual(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::EQUAL, $value); - } - - public function whereNotEqual(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::NOT_EQUAL, $value); - } - - public function whereGreaterThan(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::GREATER_THAN, $value); - } - - public function whereGreaterThanOrEqual(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::GREATER_THAN_OR_EQUAL, $value); - } - - public function whereLessThan(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::LESS_THAN, $value); - } - - public function whereLessThanOrEqual(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::LESS_THAN_OR_EQUAL, $value); - } - - public function whereBetween(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::BETWEEN, $value); - } - - public function whereNotBetween(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::NOT_BETWEEN, $value); - } - - public function whereLike(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::LIKE, $value); - } - - public function whereNotLike(string $column, string $value): TableQueryWhereStep - { - return $this->where($column, Operator::NOT_LIKE, $value); - } - - public function whereIn(string $column, string ...$value): TableQueryWhereStep - { - return $this->where($column, Operator::IN, implode(',', $value)); - } - - public function whereNotIn(string $column, string ...$value): TableQueryWhereStep - { - return $this->where($column, Operator::NOT_IN, implode(',', $value)); - } - - // * OrderByStep - - public function orderBy(string $column, Direction $direction = Direction::ASC): TableQueryOrderByStep - { - $this->orderClauses[] = [ - 'column' => $column, - 'direction' => $direction, - ]; - - return $this; - } - - public function orderByAsc(string $column): TableQueryOrderByStep - { - return $this->orderBy($column, Direction::ASC); - } - - public function orderByDesc(string $column): TableQueryOrderByStep - { - return $this->orderBy($column, Direction::DESC); - } - - // * PaginateStep - - public function paginate(Pagination $pagination): TableQueryBuildStep - { - $this->pagination = $pagination; - - return $this; - } - - // * BuildStep - - public function build(): TableQuery - { - return new TableQuery( - table: $this->table, - soc: $this->soc, - lang: $this->lang, - selectColumns: $this->selectColumns, - criteria: $this->criteria, - orderClauses: $this->orderClauses, - page: $this->pagination?->pageNumber(), - pageSize: $this->pagination?->pageSize(), - ); - } - - public function toQueryString(): string - { - return (string) $this->build(); - } -} diff --git a/src/Lib/ApiQueryBuilder/TableQueryFromStep.php b/src/Lib/ApiQueryBuilder/TableQueryFromStep.php deleted file mode 100644 index b4dbab5..0000000 --- a/src/Lib/ApiQueryBuilder/TableQueryFromStep.php +++ /dev/null @@ -1,10 +0,0 @@ - $links - */ - public static function fromArray(array $links): self - { - /** @var Uri[] $links */ - $links = array_map([self::class, 'toUri'], $links); - - return new self( - $links['first'], - $links['last'], - $links['next'] ?? null, - $links['previous'] ?? null, - ); - } - - protected static function toUri(string $value): Uri - { - return new Uri($value); - } -} diff --git a/src/Lib/ApiResponse/ApiResponse.php b/src/Lib/ApiResponse/ApiResponse.php deleted file mode 100644 index 307977c..0000000 --- a/src/Lib/ApiResponse/ApiResponse.php +++ /dev/null @@ -1,184 +0,0 @@ - */ - public array $data { - get { - return Utils::deepCopy($this->data); - } - } - - /** - * @param array $responseData - */ - public function __construct( - array $responseData - ) { - $this->pagination = $this->toPagination($responseData); - $this->data = $this->toData( - $responseData, - $this->pagination !== null - ); - } - - public function isEmpty(): bool - { - return count($this->data) === 0; - } - - /** - * @param array $data - */ - public static function fromBody(array $data): self - { - return new ApiResponse($data); - } - - /** - * @template T of mixed - * - * NOTE: Can only be call when reponse is paginated. - * - * @param callable(array):T $hook - * @return T[] - */ - public function mapData(callable $hook): array - { - if ($this->pagination === null) { - throw new \LogicException('Response is not paginated.'); - } - - return array_map(fn (array $item) => $hook($item), $this->data); - } - - /** - * @param array $responseData - * @return array - */ - protected function toData(array $responseData, bool $isPaginated): array - { - if ( - ! array_key_exists(self::DATA_KEY, $responseData) - || ! is_array($responseData[self::DATA_KEY]) - ) { - throw new InvalidApiResponse( - sprintf( - 'Missing key %s form in response', - self::DATA_KEY - ) - ); - } - - /** @var array $output */ - $output = $responseData[self::DATA_KEY]; - - if ($isPaginated && count($output)) { - // NOTE: When there is only one result output is not a list but a record (Not documentend, seen on 30/03/26). - $keys = array_keys($output); - - /** @var array */ - return (array_shift($keys) === 0 || ! count($output)) ? - $output : [$output]; - } - - // * Make string any string is encoded in proper UTF-8 string. - $this->encodeCallOutputToUtf8($output); - - if (! is_array($output)) { - throw new UnexpectedValueException('Output shall be ann array here'); - } - - return $output; - } - - /** - * @param array $data - */ - protected function toPagination(array $data): ?ApiPagination - { - if ( - ! array_key_exists(self::PAGINATION_KEY, $data) - || ! is_array($data[self::PAGINATION_KEY]) - ) { - return null; - } - - $skel = [ - 'page' => 1, - 'pageSize' => 1, - 'totalItems' => 0, - 'totalPages' => 0, - 'links' => [ - 'first' => 'https://none.example.ldc.fr', - 'last' => 'https://none.example.ldc.fr', - ], - ]; - - $expectedKeys = array_keys($skel); - $actualKeys = array_keys($data[self::PAGINATION_KEY] ?? []); - - $missing = array_diff($expectedKeys, $actualKeys); - $extra = array_diff($actualKeys, $expectedKeys); - - if ($missing || $extra) { - throw new InvalidApiResponse( - sprintf( - 'Unexpected %s form in response, mssing keys: %s, extra keys : %s', - self::PAGINATION_KEY, - json_encode($missing), - json_encode($extra), - ) - ); - } - - $page = intval($data[self::PAGINATION_KEY]['page']); - $pageSize = intval($data[self::PAGINATION_KEY]['pageSize']); - $totalItems = intval($data[self::PAGINATION_KEY]['totalItems']); - $totalPages = intval($data[self::PAGINATION_KEY]['totalPages']); - $links = ApiPaginationLinks::fromArray($data[self::PAGINATION_KEY]['links']); - - return new ApiPagination( - $page, - $pageSize, - $totalItems, - $totalPages, - $links, - ); - } - - /** @param array $output */ - private function encodeCallOutputToUtf8(null | string | array &$output): void - { - if (is_array($output)) { - array_walk_recursive($output, function (null | string | array &$nestedOutput): void { - $this->encodeCallOutputToUtf8($nestedOutput); - }); - } - - if (! is_string($output)) { - return; - } - - $output = StringEncoder::detectAndConvertToUtf8($output); - } -} diff --git a/src/Lib/ApiSorter/AbstractSorter.php b/src/Lib/ApiSorter/AbstractSorter.php deleted file mode 100644 index 9a7340b..0000000 --- a/src/Lib/ApiSorter/AbstractSorter.php +++ /dev/null @@ -1,20 +0,0 @@ - $data - * @param array $headersMap - */ - public function buildRequest( - HttpMethod $method, - UriInterface $uri, - array $data = [], - array $headersMap = [], - ): RequestInterface { - - $scheme = $uri->getScheme(); - if ($scheme !== 'http' && $scheme !== 'https') { - throw new \InvalidArgumentException('Only http(s) is allowed'); - } - - foreach ($headersMap as $headerName => $value) { - if (! is_string($headerName)) { - throw new \InvalidArgumentException('Header must be a string'); - } - if (! is_string($value)) { - throw new \InvalidArgumentException("Header value '{$headerName}' must be a string"); - } - } - - // ! Make sure parameters are on queries when HTTP GET - if ($method === HttpMethod::GET) { - $this->putAllParamsOnQuery($uri, $data); - } else { - $this->putAllParamInData($uri, $data); - } - - $timestamp = Utils::getNow(); - - $request = new Psr7Request( - \strtoupper($method->value), - $uri, - $headersMap - ); - - // NOTE: Dont prodive body content on GET method. - $jsonEncodedData = $method !== HttpMethod::GET ? GuzzleHttpUtils::jsonEncode($data) : ''; - $alteredUri = $this->transformUriInOrderToMatchLabProxy($uri); - - $signature = Utils::calculateSignature( - $method, - $alteredUri, - $jsonEncodedData, - $this->apiConfig->secret, - $timestamp - ); - $fullApiHeaders = $this->addApiHeaders( - $headersMap, - $signature, - $timestamp - ); - - return Psr7Utils::modifyRequest($request, [ - 'set_headers' => $fullApiHeaders, - 'body' => $jsonEncodedData, - ]); - } - - /** - * @param array $headers - * @return array - */ - protected function addApiHeaders( - array $headers, - string $signature, - \DateTimeInterface $timestamp - ): array { - return array_merge( - $headers, - [ - 'Content-Type' => 'application/json', - 'X-API-Key' => $this->apiConfig->apiKey, - 'X-API-Signature' => $signature, - 'X-Timestamp' => Utils::toRfc3339($timestamp), - ] - ); - } - - /** - * @param array $data - */ - protected function putAllParamsOnQuery(UriInterface &$uri, array &$data): void - { - $existingQuery = $uri->getQuery(); - $queryArray = []; - if ($existingQuery !== '') { - parse_str($existingQuery, $queryArray); - } - - $merged = array_merge($queryArray, $data); - - $newQuery = $merged ? http_build_query($merged) : ''; - $uri = $uri->withQuery($newQuery); - - $data = []; - } - - /** - * @param array $data - */ - protected function putAllParamInData(UriInterface &$uri, array &$data): void - { - $existingQuery = $uri->getQuery(); - if ($existingQuery === '') { - return; - } - - $queryArray = []; - parse_str($existingQuery, $queryArray); - - $data = array_merge($queryArray, $data); - - $uri = $uri->withQuery(''); - } - - /** - * FIXME: This should be removed, temp fix - */ - protected function transformUriInOrderToMatchLabProxy(UriInterface $uri): UriInterface - { - $scheme = $uri->getScheme(); - $authority = str_replace('lesidc', 'svlidc', $uri->getAuthority()); - $path = '/gaston/api' . preg_replace('/^\/gaston\/api/', '', $uri->getPath()); - $query = ! empty($uri->getQuery()) ? "?{$uri->getQuery()}" : ''; - $fragment = ! empty($uri->getFragment()) ? "#{$uri->getFragment()}" : ''; - - return new Uri( - "{$scheme}://{$authority}{$path}{$query}{$fragment}" - ); - } -} diff --git a/src/Mapper/EmployeeMapper.php b/src/Mapper/EmployeeMapper.php deleted file mode 100644 index abd7f3d..0000000 --- a/src/Mapper/EmployeeMapper.php +++ /dev/null @@ -1,150 +0,0 @@ - NOM_COLONNE (API/BDD). - * - * @var array - */ - protected array $definition = [ - 'matricule' => 'MATRICULE_FIC', - 'lastname' => 'NOM_FIC', - 'firstname' => 'PRENOM_FIC', - 'start_date' => 'DATE_DEB_FIC', - 'end_date' => 'DATE_FIN_FIC', - 'planned_end_date' => 'DATE_EFFET_FIC', - 'establishment' => 'ETAB_FIC', - 'site' => 'SITE_FIC', - 'service' => 'SERVICE_FIC', - 'created_at' => 'DATE_EFFET_FIC', - 'updated_at' => 'DAT_MAJ', - ]; - - public function getTable(): string - { - return $this->table; - } - - public function getTableKey(): string - { - return $this->definition[$this->tableKey]; - } - - public function getPropertiesMap(): array - { - return $this->definition; - } - - public function getValueFor(string | SortColumn $column): string - { - if (is_string($column)) { - return array_key_exists($column, $this->definition) ? - $this->definition[$column] : - throw new \InvalidArgumentException("No matching column for {$column}"); - } - - if (! ($column instanceof EmployeeSortColumn)) { - throw new \InvalidArgumentException( - 'Expected ' . EmployeeSortColumn::class . - ' given ' . get_class($column) - ); - } - - return $this->mapToArcadiaColumn($column); - } - - public function toDto(array $data): Employee - { - $columnsMapper = $this->getPropertiesMap(); - - /** @var array */ - $mapped = []; - - foreach ($columnsMapper as $modelName => $columnName) { - // FIXME: Ceci devrait être ok au lieu de cela l'API ne respecte pas le contrat du select quand un champ est null. - // if (! array_key_exists($columnName, $data)) { - // throw new UnexpectedErrorException("Column '{$columnName}' not found in data " . json_encode($data)); - // } - - // FIXME: Supprimer ceci quand le bug d'api aura été corrigé, et decommenter le code au dessus. - if (! array_key_exists($columnName, $data)) { - // NOTE: On ne devrais pas avoir a faire cela. - $mapped[$modelName] = null; - - continue; - } - - $mapped[$modelName] = $data[$columnName]; - - if (is_string($mapped[$modelName])) { - $mapped[$modelName] = IncoApiStringParser::protectiveDataFilter($mapped[$modelName]); - } - } - - $employeeId = new ArcadiaEmployeeId($mapped['matricule']); - $siteeId = new SiteId($mapped['site']); - $serviceId = new ServiceId($mapped['service']); - $establishmentCode = new EstablishmentCode($mapped['establishment']); - - $startDate = $this->toDateTime($mapped['start_date']); - if ($startDate === null) { - throw new DomainException('Missing start date'); - } - - $createdAt = $this->toDateTime($mapped['start_date']); - if ($createdAt === null) { - throw new DomainException('Missing created at'); - } - - return new Employee( - $employeeId, - $mapped['lastname'], - $mapped['firstname'], - $startDate, - $this->toDateTime($mapped['end_date']), - $this->toDateTime($mapped['planned_end_date']), - $establishmentCode, - $siteeId, - $serviceId, - $createdAt, - $this->toDateTime($mapped['updated_at']) - ); - } - - protected function toDateTime(?string $datetime): ?\DateTimeInterface - { - return is_null($datetime) ? null : DateUtils::parseDateTime($datetime); - } - - protected function mapToArcadiaColumn(EmployeeSortColumn $column): string - { - return match ($column) { - EmployeeSortColumn::arcadiaId => $this->definition['matricule'], - EmployeeSortColumn::startDate => $this->definition['start_date'], - EmployeeSortColumn::endDate => $this->definition['end_date'], - EmployeeSortColumn::plannedEndDate => $this->definition['planned_end_date'], - EmployeeSortColumn::createdAt => $this->definition['created_at'], - EmployeeSortColumn::updatedAt => $this->definition['updated_at'] - }; - } -} diff --git a/src/Mapper/IncoCompoMapper.php b/src/Mapper/IncoCompoMapper.php deleted file mode 100644 index 551c318..0000000 --- a/src/Mapper/IncoCompoMapper.php +++ /dev/null @@ -1,435 +0,0 @@ - NOM_COLONNE (API/BDD). - * - * @var array - */ - private array $definition = [ - 'id' => 'ID', - 'idInco' => 'ID_INCO', - - // ===================================================================== - // ÉNERGIE - // ===================================================================== - - // * Par 100g/ml (référence _R) - 'tailleReference' => 'TAILLE_REF', - 'valEnergKjReference' => 'VAL_ENERG_KJ_R', - // ! MISSING VAL_ENERG_KJ_NUT - 'valEnergKcalReference' => 'VAL_ENERG_KCAL_R', - // ! MISSING VAL_ENERG_KCAL_NUT - // ! MISSING ENER_E14_R - // ! MISSING ENER_KJO_R - // ! MISSING POURCENT_APPORT_REF_E14_R - // ! MISSING POURCENT_APPORT_REF_KJO_R - - // * Par portion (_P) - 'taillePortion' => 'TAILLE_PORT', - // ! MISSING FNL_FRAIS_PORTION (présent en BDD mais absent ici - voir section FNL) - 'valEnergKjPortion' => 'VAL_ENERG_KJ_P', - 'valEnergKcalPortion' => 'VAL_ENERG_KCAL_P', - // ! MISSING ENER_E14_P - // ! MISSING ENER_KJO_P - // ! MISSING POURCENT_APPORT_REF_E14_P ← présent en API mais dans $definition d'origine comme POURCENT_APPORT_REF_E14_P - 'pourcentApportRefE14P' => 'POURCENT_APPORT_REF_E14_P', - 'pourcentApportRefKjoP' => 'POURCENT_APPORT_REF_KJO_P', - - // ===================================================================== - // MACRONUTRIMENTS - Par 100g (_R) - // ===================================================================== - - 'matGrassReference' => 'MAT_GRASS_R', - // ! MISSING MAT_GRASS_NUT - 'matGrassReferencePrec' => 'MAT_GRASS_R_PREC', - // ! MISSING FATSAT_R - // ! MISSING POURCENT_APPORT_REF_FATSAT_R - 'acideGrasSatureReference' => 'AGS_R', - // ! MISSING AGS_NUT - 'acideGrasSatureReferencePrec' => 'AGS_R_PREC', - 'glucidesReference' => 'GLUCIDES_R', - 'glucidesReferencePrec' => 'GLUCIDES_R_PREC', - // ! MISSING CHOVAL_R - // ! MISSING POURCENT_APPORT_REF_CHOVAL_R - 'sucreReference' => 'SUCRE_R', - // ! MISSING SUCRE_NUT - 'sucreReferencePrec' => 'SUCRE_R_PREC', - // ! MISSING SUGAR_R - // ! MISSING POURCENT_APPORT_REF_SUGAR_R - 'fibAlimReference' => 'FIB_ALIM_R', - // ! MISSING FIB_ALIM_NUT - 'fibAlimReferencePrec' => 'FIB_ALIM_R_PREC', - // ! MISSING FIBTG_R - // ! MISSING POURCENT_APPORT_REF_FIBTG_R - 'proteineReference' => 'PROTEINE_R', - // ! MISSING PROTEINE_NUT - 'proteineReferencePrec' => 'PROTEINE_R_PREC', - // ! MISSING PRO_R - // ! MISSING POURCENT_APPORT_REF_PRO_R - 'selReference' => 'SEL_R', - // ! MISSING SEL_NUT - 'selReferencePrec' => 'SEL_R_PREC', - // ! MISSING SALTEQ_R - // ! MISSING POURCENT_APPORT_REF_SALTEQ_R - 'sodiumReference' => 'SODIUM_R', - // ! MISSING SODIUM_R_PREC - - // ===================================================================== - // MACRONUTRIMENTS - Par portion (_P) - // ===================================================================== - - 'matGrassPortion' => 'MAT_GRASS_P', - 'matGrassPortionPrec' => 'MAT_GRASS_P_PREC', - // ! MISSING FAT_P - 'pourcentApportRefFatP' => 'POURCENT_APPORT_REF_FAT_P', - // ! MISSING FATSAT_P - 'pourcentApportRefFatsatP' => 'POURCENT_APPORT_REF_FATSAT_P', - 'acideGrasSaturePortion' => 'AGS_P', - 'acideGrasSaturePortionPrec' => 'AGS_P_PREC', - 'glucidesPortion' => 'GLUCIDES_P', - 'glucidesPortionPrec' => 'GLUCIDES_P_PREC', - // ! MISSING CHOVAL_P - 'pourcentApportRefChovalP' => 'POURCENT_APPORT_REF_CHOVAL_P', - 'sucrePortion' => 'SUCRE_P', - 'sucrePortionPrec' => 'SUCRE_P_PREC', - // ! MISSING SUGAR_P - 'pourcentApportRefSugarP' => 'POURCENT_APPORT_REF_SUGAR_P', - 'fibAlimPortion' => 'FIB_ALIM_P', - 'fibAlimPortionPrec' => 'FIB_ALIM_P_PREC', - // ! MISSING FIBTG_P - // ! MISSING POURCENT_APPORT_REF_FIBTG_P - 'proteinePortion' => 'PROTEINE_P', - 'proteinePortionPrec' => 'PROTEINE_P_PREC', - // ! MISSING PRO_P - 'pourcentApportRefProP' => 'POURCENT_APPORT_REF_PRO_P', - 'selPortion' => 'SEL_P', - 'selPortionPrec' => 'SEL_P_PREC', - // ! MISSING SALTEQ_P - 'pourcentApportRefSalteqP' => 'POURCENT_APPORT_REF_SALTEQ_P', - 'sodiumPortion' => 'SODIUM_P', - - // ===================================================================== - // APPORTS DE RÉFÉRENCE - Divers - // ===================================================================== - - // ! MISSING APPORT_REF (apport de référence pour 100g) - 'apportPort' => 'APPORT_PORT', // * présent dans l'API : "Apport de référence pour un adulte type (8400Kj-2000Kcal)" - // ! MISSING PART_P - // ! MISSING PART_G - // ! MISSING PART_L - 'pSurL' => 'P_SUR_L', - // ! MISSING POURCENT_AGS - // ! MISSING POURCENT_AGI - // ! MISSING POURCENT_SS - // ! MISSING POURCENT_AMIDON - - // ===================================================================== - // FRUITS, NOIX, LÉGUMES (FNL) - // ===================================================================== - - 'fruitsReference' => 'FRUITS_R', - // ! MISSING FRUITS_R_PREC - 'fruitsPortion' => 'FRUITS_P', - 'fruitsCoqueSechReference' => 'FRUITS_COQUE_SECH_R', - // ! MISSING FRUITS_COQUE_SECH_R_PREC - 'fruitsCoqueSechPortion' => 'FRUITS_COQUE_SECH_P', - 'fruitLegumeNoixFrais' => 'FNL_FRAIS', - 'fruitLegumeNoixFraisPortion' => 'FNL_FRAIS_PORTION', - 'pourcentFruitsLegumesNoixFrais' => 'POURCENT_FNL_FRAIS', - 'pourcentFruits' => 'POURCENT_FRUITS', - - // ===================================================================== - // NUTRI-SCORE - // ===================================================================== - - // * Composantes de calcul - 'composanteN' => 'COMPOSANTE_N', - 'composanteN2022' => 'COMPOSANTE_N_2022', - 'composanteP' => 'COMPOSANTE_P', - 'composanteP2022' => 'COMPOSANTE_P_2022', - // * Scores bruts - 'scoreNutri' => 'SCORE_NUTRI', - 'scoreNutri2022' => 'SCORE_NUTRI_2022', - // * Classement final (lettre A-E) - // ! NOTE : CLASSEMENT n'est pas censé être dans INCO_COMPO selon la spec - 'classement' => 'CLASSEMENT', - 'nutriScore2022' => 'NUTRI_SCORE_2022', - 'nutriAArt' => 'NUTRI_A_ART', - // * Indicateur viande rouge (algorithme 2022) - 'viandeRouge' => 'VIANDE_ROUGE', - - // ===================================================================== - // POINTS NUTRI-SCORE - // ===================================================================== - - 'pointsEnergie' => 'POINTS_ENERGIE', - 'pointsAcideGrasSature' => 'POINTS_AGS', - 'pointsSucres' => 'POINTS_SUCRES', - 'pointsSel' => 'POINTS_SEL', - 'pointsFibres' => 'POINTS_FIBRES', - 'pointsProteines' => 'POINTS_PROTEINES', - 'pointsFruitsLegumesNoixFrais' => 'POINTS_FNL_FRAIS', - - // ===================================================================== - // IDENTIFICATION / MÉTADONNÉES - // ===================================================================== - - 'nom' => 'NOM', - 'numero' => 'NUMERO', - 'source' => 'SOURCE', - - // ===================================================================== - // MINÉRAUX & OLIGO-ÉLÉMENTS - // ===================================================================== - - 'nutCa' => 'NUT_CA', - 'nutMg' => 'NUT_MG', - 'nutK' => 'NUT_K', - 'nutP' => 'NUT_P', - 'nutCld' => 'NUT_CLD', - 'nutFe' => 'NUT_FE', - 'nutZn' => 'NUT_ZN', - 'nutCu' => 'NUT_CU', - 'nutMn' => 'NUT_MN', - 'nutSe' => 'NUT_SE', - 'nutId' => 'NUT_ID', - 'nutCr' => 'NUT_CR', - 'nutMo' => 'NUT_MO', - 'nutS6' => 'NUT_S6', - 'nutNitra' => 'NUT_NITRA', - - // ===================================================================== - // VITAMINES - // ===================================================================== - - 'nutVita' => 'NUT_VITA', - 'nutVitb6' => 'NUT_VITB6', - 'nutVitb12' => 'NUT_VITB12', - 'nutVitc' => 'NUT_VITC', - 'nutVitd' => 'NUT_VITD', - 'nutVite' => 'NUT_VITE', - 'nutVitk' => 'NUT_VITK', - 'nutChocal' => 'NUT_CHOCAL', - 'nutNia' => 'NUT_NIA', - 'nutRibf' => 'NUT_RIBF', - 'nutThia' => 'NUT_THIA', - 'nutPantac' => 'NUT_PANTAC', - 'nutBiot' => 'NUT_BIOT', - 'nutFoldfe' => 'NUT_FOLDFE', - - // ===================================================================== - // GLUCIDES DÉTAILLÉS - // ===================================================================== - - 'nutStarch' => 'NUT_STARCH', - 'nutMaltdex' => 'NUT_MALTDEX', - 'nutSucs' => 'NUT_SUCS', - 'nutGlus' => 'NUT_GLUS', - 'nutLacs' => 'NUT_LACS', - 'nutMals' => 'NUT_MALS', - 'nutGalfb' => 'NUT_GALFB', - 'nutGHc' => 'NUT_G_HC', - 'nutFrufb' => 'NUT_FRUFB', - 'nutFrus' => 'NUT_FRUS', - 'nutOlsac' => 'NUT_OLSAC', - 'nutPolyl' => 'NUT_POLYL', - 'nutFibsol' => 'NUT_FIBSOL', - 'nutInotl' => 'NUT_INOTL', - - // ===================================================================== - // LIPIDES / ACIDES GRAS - // ===================================================================== - - // * Lipides généraux - 'nutFd' => 'NUT_FD', - 'nutChol' => 'NUT_CHOL', - 'nutAgt' => 'NUT_AGT', - - // * AGS - chaîne courte (C1-C10) - 'nutFformiq' => 'NUT_FFORMIQ', // C1:0 - 'nutFacetiq' => 'NUT_FACETIQ', // C2:0 - 'nutFpropioniq' => 'NUT_FPROPIONIQ', // C3:0 - 'nutFbutyriq' => 'NUT_FBUTYRIQ', // C4:0 - 'nutFvaleriq' => 'NUT_FVALERIQ', // C5:0 - 'nutFcaproiq' => 'NUT_FCAPROIQ', // C6:0 - 'nutFenatiq' => 'NUT_FENATIQ', // C7:0 - 'nutFcapryliq' => 'NUT_FCAPRYLIQ', // C8:0 - 'nutFpelargoniq' => 'NUT_FPELARGONIQ', // C9:0 - 'nutFcapriq' => 'NUT_FCAPRIQ', // C10:0 - - // * AGS - chaîne moyenne à longue (C11-C38) - 'nutFundecyliq' => 'NUT_FUNDECYLIQ', // C11:0 - 'nutFlauriq' => 'NUT_FLAURIQ', // C12:0 - 'nutFtridecyliq' => 'NUT_FTRIDECYLIQ', // C13:0 - 'nutFmyristiq' => 'NUT_FMYRISTIQ', // C14:0 - 'nutFpentadecyliq' => 'NUT_FPENTADECYLIQ', // C15:0 - 'nutF16d0' => 'NUT_F16D0', // C16:0 palmitique - 'nutFmargariq' => 'NUT_FMARGARIQ', // C17:0 - 'nutFsteariq' => 'NUT_FSTEARIQ', // C18:0 - 'nutFarachidiq' => 'NUT_FARACHIDIQ', // C20:0 - 'nutFheneicosyliq' => 'NUT_FHENEICOSYLIQ', // C21:0 - 'nutFbeheniq' => 'NUT_FBEHENIQ', // C22:0 - 'nutFtricosiliq' => 'NUT_FTRICOSILIQ', // C23:0 - 'nutFlignoceriq' => 'NUT_FLIGNOCERIQ', // C24:0 - 'nutFcarboceriq' => 'NUT_FCARBOCERIQ', // C26:0 - 'nutFcerotiq' => 'NUT_FCEROTIQ', // C26:0 alt. - 'nutFceroplastiq' => 'NUT_FCEROPLASTIQ', - 'nutFmontaniq' => 'NUT_FMONTANIQ', // C28:0 - 'nutFnonacosyliq' => 'NUT_FNONACOSYLIQ', // C29:0 - 'nutFmelissiq' => 'NUT_FMELISSIQ', // C30:0 - 'nutFlaceroiq' => 'NUT_FLACEROIQ', // C32:0 - 'nutFhentriacontyliq' => 'NUT_FHENTRIACONTYLIQ', // C31:0 - 'nutFhextriacontyliq' => 'NUT_FHEXTRIACONTYLIQ', // C36:0 - 'nutFheptatriacontyliq' => 'NUT_FHEPTATRIACONTYLIQ', // C37:0 - 'nutFoctatriacontyliq' => 'NUT_FOCTATRIACONTYLIQ', // C38:0 - 'nutFnonadylique' => 'NUT_FNONADYLIQUE', - 'nutFpsylliq' => 'NUT_FPSYLLIQ', - - // * AGMI - 'nutFpalm' => 'NUT_FPALM', // C16:1 - 'nutFoleiq' => 'NUT_FOLEIQ', // C18:1 ω-9 - 'nutF18d' => 'NUT_F18D', // C18 insaturé générique - 'nutF19d' => 'NUT_F19D', // C19 insaturé - 'nutF11eico' => 'NUT_F11EICO', // C20:1 - 'nutFgeddiq' => 'NUT_FGEDDIQ', // C20:1 alt. - 'nutFeruciq' => 'NUT_FERUCIQ', // C22:1 - 'nutFnervoniq' => 'NUT_FNERVONIQ', // C24:1 - 'nutFhyeniq' => 'NUT_FHYENIQ', - 'nutFamscis' => 'NUT_FAMSCIS', // AGMI cis total - - // * AGPI - 'nutF18d2cn6' => 'NUT_F18D2CN6', // C18:2 ω-6 linoléique - 'nutF18d3n3' => 'NUT_F18D3N3', // C18:3 ω-3 α-linolénique - 'nutF20d4' => 'NUT_F20D4', // C20:4 arachidonique - 'nutF20d5n3' => 'NUT_F20D5N3', // C20:5 ω-3 EPA - 'nutF22d6n3' => 'NUT_F22D6N3', // C22:6 ω-3 DHA - 'nutFapun3' => 'NUT_FAPUN3', // AGPI ω-3 total - 'nutFapun6' => 'NUT_FAPUN6', // AGPI ω-6 total - 'nutFapun9' => 'NUT_FAPUN9', // AGPI ω-9 total - 'nutFapucis' => 'NUT_FAPUCIS', // AGPI cis total - 'nutFapulc' => 'NUT_FAPULC', // AGPI longue chaîne total - - // ===================================================================== - // ACIDES AMINÉS & PROTÉINES SPÉCIFIQUES - // ===================================================================== - - 'nutAae' => 'NUT_AAE', // acides aminés essentiels total - 'nutCasn' => 'NUT_CASN', // caséines - 'nutWhey' => 'NUT_WHEY', // protéines lactosérum - - // ===================================================================== - // COMPOSÉS BIOACTIFS & SPÉCIAUX - // ===================================================================== - - 'nutCholn' => 'NUT_CHOLN', - 'nutCarnitine' => 'NUT_CARNITINE', - 'nutTau' => 'NUT_TAU', - 'nutNucleotide' => 'NUT_NUCLEOTIDE', - ]; - - public function getTable(): string - { - return $this->table; - } - - public function getTableKey(): string - { - return $this->definition[$this->tableKey]; - } - - public function getPropertiesMap(): array - { - return $this->definition; - } - - public function getValueFor(string | SortColumn $column): string - { - if (is_string($column)) { - return array_key_exists($column, $this->definition) ? - $this->definition[$column] : - throw new \InvalidArgumentException("No matching column for {$column}"); - } - - throw new \InvalidArgumentException('Sort column is not supported for IncoCompo'); - } - - public function toDto(array $data): IncoCompo - { - $mapped = $this->mapColumns($data); - - $id = IncoCompoApiParser::getArrayKeyValue($mapped, 'id'); - $id = new IncoCompoId(is_scalar($id) ? (string) $id : ''); - - $incoId = IncoCompoApiParser::getArrayKeyValue($mapped, 'idInco'); - if (! is_array($incoId)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id to be an array : %s', - json_encode($incoId) - ) - ); - } - $codeArray = IncoCompoApiParser::getArrayKeyValue($incoId, 'code'); - $incoId = new IncoId(is_scalar($codeArray) ? (string) $codeArray : ''); - - return new IncoCompo( - id: $id, - idInco: $incoId, - identification: IncoCompoApiParser::toIdentification($mapped), - energie: IncoCompoApiParser::toEnergie($mapped), - macronutrimentsRef: IncoCompoApiParser::toMacronutrimentsRef($mapped), - macronutrimentsPortion: IncoCompoApiParser::toMacronutrimentsPortion($mapped), - fnl: IncoCompoApiParser::toFnl($mapped), - apportsReference: IncoCompoApiParser::toApportsReference($mapped), - nutriScore: IncoCompoApiParser::toNutriScore($mapped), - pointsNutriScore: IncoCompoApiParser::toPointsNutriScore($mapped), - mineraux: IncoCompoApiParser::toMineraux($mapped), - vitamines: IncoCompoApiParser::toVitamines($mapped), - glucidesDetail: IncoCompoApiParser::toGlucidesDetail($mapped), - acideGras: IncoCompoApiParser::toAcideGras($mapped), - acidesAmines: IncoCompoApiParser::toAcidesAmines($mapped), - composesSpeciaux: IncoCompoApiParser::toComposesSpeciaux($mapped), - ); - } - - /** - * @param array $data - * @return array - */ - protected function mapColumns(array $data): array - { - /** @var array */ - $mapped = []; - - foreach ($this->definition as $propertyName => $columnName) { - // FIXME: Remove null fallback once the API respects its select contract. - $mapped[$propertyName] = array_key_exists($columnName, $data) - ? $data[$columnName] - : null; - - if (is_string($mapped[$propertyName])) { - $mapped[$propertyName] = IncoApiStringParser::protectiveDataFilter($mapped[$propertyName]); - } - } - - return $mapped; - } -} diff --git a/src/Mapper/IncoMapper.php b/src/Mapper/IncoMapper.php deleted file mode 100644 index 33be2ce..0000000 --- a/src/Mapper/IncoMapper.php +++ /dev/null @@ -1,217 +0,0 @@ - NOM_COLONNE (API/BDD). - * - * @var array - */ - private array $definition = [ - 'id' => 'NO_ART', - 'name' => 'DENOM_LEG', - 'description' => 'DESCRIPTIF', - 'tips' => 'ASTUCES', - 'preparation_advices' => 'CONSEIL_PREPA', - 'sell_arguments' => 'ARG_VENTE', - 'allegations' => 'ALLEGATIONS', - 'allegations_health' => 'ALLEGAT_SANTE', - 'allegations_nutrition' => 'ALLEGAT_NUTRIT', - 'allegations_other' => 'ALLEGAT_AUTRE', - 'ingredients' => 'LST_ING', - 'portions' => 'NB_PORTION', - 'portion_type' => 'TYP_PORTION', - 'allergens' => 'LST_ALLERGENE', - 'brand' => 'MARQUE', - 'nutriscore' => 'NUTRISCORE_ON_PACK', - 'pdsNetUvc' => 'PDS_NET_UVC', - 'pdsBrutUvc' => 'PDS_BRUT_UVC', - 'pdsMoyTheo' => 'PDS_MOY_THEO', - 'conditConserv' => 'CONDIT_CONSERV', - 'lstAdditif' => 'LST_ADDITIF', - 'familleVte' => 'FAMILLE_VTE', - - // 'numero' => 'NUMERO', - // 'taille_ref' => 'TAILLE_REF', - 'is_seasonal' => 'PROD_SAISON', - 'compo' => 'REF_COMPO', - 'created_at' => 'DAT_CRE', - 'updated_at' => 'DAT_MAJ', - ]; - - public function getTable(): string - { - return $this->table; - } - - public function getTableKey(): string - { - return $this->definition[$this->tableKey]; - } - - public function getPropertiesMap(): array - { - return $this->definition; - } - - public function getValueFor(string | SortColumn $column): string - { - if (! is_string($column)) { - if (! ($column instanceof IncoSortColumn)) { - throw new \InvalidArgumentException( - 'Expected ' . IncoSortColumn::class . - ' given ' . get_class($column) - ); - } - - return match ($column) { - IncoSortColumn::id => $this->definition['id'], - IncoSortColumn::createdAt => $this->definition['created_at'], - IncoSortColumn::updatedAt => $this->definition['updated_at'] - }; - } - - if (array_key_exists($column, $this->definition)) { - return $this->definition[$column]; - } - - throw new \InvalidArgumentException("No matching column for {$column}"); - } - - /** @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh */ - public function toDto(array $data): Inco - { - // phpcs:enable - - $columnsMapper = $this->getPropertiesMap(); - - /** @var array */ - $mapped = []; - - foreach ($columnsMapper as $modelName => $columnName) { - // FIXME: Ceci devrait être ok au lieu de cela l'API ne respecte pas le contrat du select quand un champ est null. - // if (! array_key_exists($columnName, $data)) { - // throw new UnexpectedErrorException("Column '{$columnName}' not found in data " . json_encode($data)); - // } - - // FIXME: Supprimer ceci quand le bug d'api aura été corrigé, et decommenter le code au dessus. - if (! array_key_exists($columnName, $data)) { - // NOTE: On ne devrais pas avoir a faire cela. - $mapped[$modelName] = null; - - continue; - } - - $mapped[$modelName] = $data[$columnName]; - - if (is_string($mapped[$modelName])) { - $mapped[$modelName] = IncoApiStringParser::protectiveDataFilter($mapped[$modelName]); - } - } - - $incoId = IncoApiParser::getArrayKeyValue($mapped, 'id'); - - if (! is_array($incoId)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id to be an array : %s', - json_encode($incoId) - ) - ); - } - - $codeArray = IncoApiParser::getArrayKeyValue($incoId, 'code'); - - if (! is_string($codeArray)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id code to be a string : %s', - json_encode($codeArray) - ) - ); - } - - $libelle = IncoApiParser::getArrayKeyValue($incoId, 'libelle'); - - if (! is_string($libelle)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id libelle to be a string : %s', - json_encode($libelle) - ) - ); - } - - $incoId = new IncoId($codeArray); - $brand = IncoApiParser::toBrand($mapped); - $allegations = IncoApiParser::toAllegations($mapped); - $allergens = IncoApiParser::toAllergens($mapped); - $portionAmount = IncoApiParser::toPortionAmount($mapped); - $compoId = IncoApiParser::toIncoCompoId($mapped); - $ingredients = IncoApiParser::parseIngredients($mapped); - $isSeasonal = IncoApiParser::toBoolean($mapped, 'is_seasonal', false); - $weight = IncoApiParser::toIncoWeight($mapped); - $nutriscore = IncoApiParser::toNutriscore($mapped); - $storageCondition = IncoApiParser::toStorageCondition($mapped); - $additives = IncoApiParser::toAdditives($mapped); - $sellFamilly = IncoApiParser::toSellFamilly($mapped); - - $libelle = IncoApiStringParser::beautifyName($libelle); - $tips = IncoApiStringParser::beautifyText($mapped['tips']); - $description = IncoApiStringParser::beautifyText($mapped['description']) ?? ''; - $sellArguments = IncoApiStringParser::beautifyText($mapped['sell_arguments']) ?? ''; - $preparationAdvices = IncoApiStringParser::beautifyText($mapped['preparation_advices']) ?? ''; - - $createdAt = IncoApiParser::toDateTimeFrom($mapped, 'created_at'); - $updatedAt = IncoApiParser::toDateTimeFrom($mapped, 'updated_at'); - - $name = IncoApiParser::getArrayKeyValue($mapped, 'name'); - $nameStr = is_string($name) ? $name : null; - if (empty($nameStr) || mb_strlen($nameStr) <= 1) { - $nameStr = (! empty($description) && mb_strlen($description) > 0) ? - StringUtils::words($description, 4, '') : "Produit Inco {$incoId->value}"; - } - $name = IncoApiStringParser::beautifyText($nameStr) ?? ''; - - return new Inco( - $incoId, - $name, - $libelle, - $description, - $allegations, - $sellArguments, - $preparationAdvices, - $tips, - $sellFamilly, - $ingredients, - $portionAmount, - $allergens, - $brand, - $isSeasonal, - $weight, - $nutriscore, - $storageCondition, - $additives, - $compoId, - $createdAt, - $updatedAt - ); - } -} diff --git a/src/Mapper/IncoShortMapper.php b/src/Mapper/IncoShortMapper.php deleted file mode 100644 index bcfa2f2..0000000 --- a/src/Mapper/IncoShortMapper.php +++ /dev/null @@ -1,163 +0,0 @@ - NOM_COLONNE (API/BDD). - * - * @var array - */ - private array $definition = [ - 'id' => 'NO_ART', - 'name' => 'DENOM_LEG', - 'description' => 'DESCRIPTIF', - 'created_at' => 'DAT_CRE', - 'updated_at' => 'DAT_MAJ', - ]; - - public function getTable(): string - { - return $this->table; - } - - public function getTableKey(): string - { - return $this->definition[$this->tableKey]; - } - - public function getPropertiesMap(): array - { - return $this->definition; - } - - public function getValueFor(string | SortColumn $column): string - { - if (! is_string($column)) { - if (! ($column instanceof IncoSortColumn)) { - throw new \InvalidArgumentException( - 'Expected ' . IncoSortColumn::class . - ' given ' . get_class($column) - ); - } - - return match ($column) { - IncoSortColumn::id => $this->definition['id'], - IncoSortColumn::createdAt => $this->definition['created_at'], - IncoSortColumn::updatedAt => $this->definition['updated_at'] - }; - } - - if (array_key_exists($column, $this->definition)) { - return $this->definition[$column]; - } - - throw new \InvalidArgumentException("No matching column for {$column}"); - } - - /** @phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh */ - public function toDto(array $data): IncoShort - { - // phpcs:enable - - $columnsMapper = $this->getPropertiesMap(); - - /** @var array */ - $mapped = []; - - foreach ($columnsMapper as $modelName => $columnName) { - // FIXME: Ceci devrait être ok au lieu de cela l'API ne respecte pas le contrat du select quand un champ est null. - // if (! array_key_exists($columnName, $data)) { - // throw new UnexpectedErrorException("Column '{$columnName}' not found in data " . json_encode($data)); - // } - - // FIXME: Supprimer ceci quand le bug d'api aura été corrigé, et decommenter le code au dessus. - if (! array_key_exists($columnName, $data)) { - // NOTE: On ne devrais pas avoir a faire cela. - $mapped[$modelName] = null; - - continue; - } - - $mapped[$modelName] = $data[$columnName]; - - if (is_string($mapped[$modelName])) { - $mapped[$modelName] = IncoApiStringParser::protectiveDataFilter($mapped[$modelName]); - } - } - - $incoId = IncoApiParser::getArrayKeyValue($mapped, 'id'); - - if (! is_array($incoId)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id to be an array : %s', - json_encode($incoId) - ) - ); - } - - $codeArray = IncoApiParser::getArrayKeyValue($incoId, 'code'); - - if (! is_string($codeArray)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id code to be a string : %s', - json_encode($codeArray) - ) - ); - } - - $libelle = IncoApiParser::getArrayKeyValue($incoId, 'libelle'); - - if (! is_string($libelle)) { - throw new UnexpectedErrorException( - sprintf( - 'Expected inco id libelle to be a string : %s', - json_encode($libelle) - ) - ); - } - - $incoId = new IncoId($codeArray); - - $libelle = IncoApiStringParser::beautifyName($libelle); - $description = IncoApiStringParser::beautifyText($mapped['description']) ?? ''; - - $createdAt = IncoApiParser::toDateTimeFrom($mapped, 'created_at'); - $updatedAt = IncoApiParser::toDateTimeFrom($mapped, 'updated_at'); - - $name = IncoApiParser::getArrayKeyValue($mapped, 'name'); - $nameStr = is_string($name) ? $name : null; - if (empty($nameStr) || mb_strlen($nameStr) <= 1) { - $nameStr = (! empty($description) && mb_strlen($description) > 0) ? - StringUtils::words($description, 4, '') : "Produit Inco {$incoId->value}"; - } - $name = IncoApiStringParser::beautifyText($nameStr) ?? ''; - - return new IncoShort( - $incoId, - $name, - $libelle, - $description, - $createdAt, - $updatedAt - ); - } -} diff --git a/src/Mapper/Mapper.php b/src/Mapper/Mapper.php deleted file mode 100644 index 1cfdc2e..0000000 --- a/src/Mapper/Mapper.php +++ /dev/null @@ -1,27 +0,0 @@ - */ - public function getPropertiesMap(): array; - - public function getValueFor(string | SortColumn $column): string; - - /** - * @param array $data - * - * @throws \Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException If mapping fails. - */ - public function toDto(array $data): Dto; -} diff --git a/src/Repository/AbstractGenesisRepository.php b/src/Repository/AbstractGenesisRepository.php deleted file mode 100644 index 451ed8d..0000000 --- a/src/Repository/AbstractGenesisRepository.php +++ /dev/null @@ -1,138 +0,0 @@ - $dtoFactory - */ - public function __construct( - private readonly ApiCaller $apiCaller, - protected readonly DtoFactory $dtoFactory, - protected readonly Mapper $mapper - ) { - } - - /** - * @param (callable(TableQueryWhereStep):TableQueryWhereStep) $filters - * @return PageOf - */ - protected function paginatedQuery( - Pagination $pagination, - ?AbstractSorter $sorter = null, - callable ...$filters - ): PageOf { - - $query = TableQueryBuilder::query() - ->from($this->mapper->getTable()) - ->select(...$this->mapper->getPropertiesMap()); - - foreach ($filters as $filter) { - $query = $filter($query); - } - - if ($sorter !== null) { - $query = $query->orderBy( - $sorter->sortBy, - $sorter->direction - ); - } - - $queryData = $query - ->paginate($pagination) - ->build(); - - $apiResponse = $this->apiCaller->callApi( - $this->queryPath, - $queryData - ); - - // NOTE: Forcement non null ici - // @phpstan-ignore return.type - return $this->dtoFactory->build($apiResponse, $pagination); - } - - /** - * @return D|null - */ - protected function findByColumn(string $column, string $value): ?Dto - { - $queryData = TableQueryBuilder::query() - ->from($this->mapper->getTable()) - ->select(...$this->mapper->getPropertiesMap()) - ->whereEqual($column, $value) - ->build(); - - $apiResponse = $this->apiCaller->callApi( - $this->queryPath, - $queryData - ); - - return $this->dtoFactory->build($apiResponse); - } - - /** - * @return callable(TableQueryWhereStep):TableQueryWhereStep - */ - protected function sinceFilter(string $column, \DateTimeInterface $since): callable - { - return fn (TableQueryWhereStep $q): TableQueryWhereStep => $q->whereGreaterThanOrEqual( - $column, - DateUtils::toRfc3339($since) - ); - } - - /** - * @param array $values - * @return callable(TableQueryWhereStep):TableQueryWhereStep - */ - protected function inFilter(string $column, array $values): callable - { - /** @var array $stringValues */ - $stringValues = array_map(fn (object $v): string => (string) $v->value, $values); - - return fn (TableQueryWhereStep $q): TableQueryWhereStep => $q->whereIn($column, ...$stringValues); - } - - /** - * @return callable(TableQueryWhereStep):TableQueryWhereStep - */ - protected function whereLikeFilter(string $column, string $stringValue): callable - { - return fn (TableQueryWhereStep $q): TableQueryWhereStep => $q->whereLike($column, $stringValue); - } - - /** - * @param callable(TableQueryWhereStep):TableQueryWhereStep $filters - * @return callable(TableQueryWhereStep):TableQueryWhereStep - */ - protected function composeFilters(callable ...$filters): callable - { - return function (TableQueryWhereStep $query) use ($filters): TableQueryWhereStep { - foreach ($filters as $filter) { - $query = $filter($query); - } - - return $query; - }; - } -} diff --git a/src/Repository/EmployeeGenesisRepository.php b/src/Repository/EmployeeGenesisRepository.php deleted file mode 100644 index c6775b2..0000000 --- a/src/Repository/EmployeeGenesisRepository.php +++ /dev/null @@ -1,164 +0,0 @@ - - */ -final class EmployeeGenesisRepository extends AbstractGenesisRepository implements EmployeeRepository -{ - /** @return PageOf */ - public function selectAll( - Pagination $pagination, - ?EmployeeSorter $sorter = null - ): PageOf { - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->mapSorter($sorter) - ); - } - - return $this->paginatedQuery( - $pagination, - ); - } - - /** @return PageOf */ - public function selectAllSince( - Pagination $pagination, - \DateTimeInterface $since, - Direction $sortDirection = Direction::DESC - ): PageOf { - $sortBy = EmployeeSortColum::updatedAt; - $sortByColumn = $this->mapper->getValueFor($sortBy); - - return $this->paginatedQuery( - $pagination, - $this->toSorter($sortBy), - $this->sinceFilter($sortByColumn, $since), - ); - } - - /** @return PageOf */ - public function selectAllHavingSitesSince( - Pagination $pagination, - \DateTimeInterface $since, - array $sites, - Direction $sortDirection = Direction::DESC - ): PageOf { - $sortBy = EmployeeSortColum::updatedAt; - $sortByColumn = $this->mapper->getValueFor($sortBy); - $filterByColumn = $this->mapper->getValueFor('site'); - - return $this->paginatedQuery( - $pagination, - $this->toSorter($sortBy), - $this->composeFilters( - $this->sinceFilter($sortByColumn, $since), - $this->inFilter($filterByColumn, $sites), - ), - ); - } - - /** @return PageOf */ - public function selectAllHavingSites( - Pagination $pagination, - array $sites, - ?EmployeeSorter $sorter = null - ): PageOf { - $filterColumn = $this->mapper->getValueFor('site'); - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->toSorter($sorter->sortBy), - $this->inFilter($filterColumn, $sites), - ); - } - - return $this->paginatedQuery( - $pagination, - null, - $this->inFilter($filterColumn, $sites), - ); - } - - /** - * @param array<\Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ServiceId> $services - * @return PageOf - */ - public function selectAllHavingServicesSince( - Pagination $pagination, - \DateTimeInterface $since, - array $services, - Direction $sortDirection = Direction::DESC - ): PageOf { - $sortBy = EmployeeSortColum::updatedAt; - $sortByColumn = $this->mapper->getValueFor($sortBy); - $filterColumn = $this->mapper->getValueFor('service'); - - return $this->paginatedQuery( - $pagination, - $this->toSorter($sortBy, $sortDirection), - $this->composeFilters( - $this->sinceFilter($sortByColumn, $since), - $this->inFilter($filterColumn, $services), - ), - ); - } - - /** - * @param array<\Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ServiceId> $services - * @return PageOf - */ - public function selectAllHavingServices( - Pagination $pagination, - array $services, - ?EmployeeSorter $sorter = null - ): PageOf { - $filterColumn = $this->mapper->getValueFor('service'); - - return $this->paginatedQuery( - $pagination, - $sorter ? $this->toSorter($sorter->sortBy) : null, - $this->inFilter($filterColumn, $services), - ); - } - - public function find(ArcadiaEmployeeId $id): ?Employee - { - return $this->findByColumn( - $this->mapper->getTableKey(), - $id->value - ); - } - - private function toSorter(EmployeeSortColum $sortBy, ?Direction $direction = null): AbstractSorter - { - return new AbstractSorter( - $this->mapper->getValueFor($sortBy), - $direction ?? Direction::DESC - ); - } - - private function mapSorter(EmployeeSorter $sorter): AbstractSorter - { - return new AbstractSorter( - $this->mapper->getValueFor($sorter->sortBy), - $sorter->direction - ); - } -} diff --git a/src/Repository/IncoCompoGenesisRepository.php b/src/Repository/IncoCompoGenesisRepository.php deleted file mode 100644 index 5003ad2..0000000 --- a/src/Repository/IncoCompoGenesisRepository.php +++ /dev/null @@ -1,31 +0,0 @@ - - */ -final class IncoCompoGenesisRepository extends AbstractGenesisRepository implements IncoCompoRepository -{ - public function find(IncoCompoId $id): ?IncoCompo - { - return $this->findByColumn( - $this->mapper->getTableKey(), - $id->value - ); - } - - public function findFor(IncoId $id): ?IncoCompo - { - return $this->findByColumn( - $this->mapper->getValueFor('idInco'), - $id->value - ); - } -} diff --git a/src/Repository/IncoGenesisRepository.php b/src/Repository/IncoGenesisRepository.php deleted file mode 100644 index 602983f..0000000 --- a/src/Repository/IncoGenesisRepository.php +++ /dev/null @@ -1,143 +0,0 @@ - - */ -final class IncoGenesisRepository extends AbstractGenesisRepository implements IncoRepository -{ - public function selectAll( - Pagination $pagination, - ?IncoSorter $sorter = null - ): PageOf { - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->mapSorter($sorter) - ); - } - - return $this->paginatedQuery( - $pagination, - ); - } - - public function selectAllSince( - Pagination $pagination, - \DateTimeInterface $since, - Direction $sortDirection = Direction::DESC - ): PageOf { - $sortBy = IncoSortColumn::updatedAt; - $sortByColumn = $this->mapper->getValueFor($sortBy); - - return $this->paginatedQuery( - $pagination, - $this->toSorter($sortBy, $sortDirection), - $this->sinceFilter($sortByColumn, $since), - ); - } - - public function selectAllHavingBrandsSince( - Pagination $pagination, - \DateTimeInterface $since, - array $brands, - Direction $sortDirection = Direction::DESC, - ): PageOf { - $sortBy = IncoSortColumn::updatedAt; - $sortByColumn = $this->mapper->getValueFor($sortBy); - $filterByColumn = $this->mapper->getValueFor('brand'); - - return $this->paginatedQuery( - $pagination, - $this->toSorter($sortBy, $sortDirection), - $this->composeFilters( - $this->sinceFilter($sortByColumn, $since), - $this->inFilter($filterByColumn, $brands), - ), - ); - } - - public function selectAllHavingBrands( - Pagination $pagination, - array $brands, - ?IncoSorter $sorter = null - ): PageOf { - $filterColumn = $this->mapper->getValueFor('brand'); - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->toSorter($sorter->sortBy), - $this->inFilter($filterColumn, $brands), - ); - } - - return $this->paginatedQuery( - $pagination, - null, - $this->inFilter($filterColumn, $brands), - ); - } - - public function findWhere( - string $incoIdLike, - array $inBrands, - Pagination $pagination, - ?IncoSorter $sorter = null - ): PageOf { - $whereLikeColumn = $this->mapper->getValueFor('id'); - $filterColumn = $this->mapper->getValueFor('brand'); - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->toSorter($sorter->sortBy), - $this->inFilter($filterColumn, $inBrands), - ); - } - - return $this->paginatedQuery( - $pagination, - null, - $this->whereLikeFilter($whereLikeColumn, $incoIdLike), - $this->inFilter($filterColumn, $inBrands), - ); - } - - public function find(IncoId $id): ?Inco - { - return $this->findByColumn( - $this->mapper->getTableKey(), - $id->value - ); - } - - private function toSorter(IncoSortColumn $sortBy, ?Direction $direction = null): AbstractSorter - { - return new AbstractSorter( - $this->mapper->getValueFor($sortBy), - $direction ?? Direction::DESC - ); - } - - private function mapSorter(IncoSorter $sorter): AbstractSorter - { - return new AbstractSorter( - $this->mapper->getValueFor($sorter->sortBy), - $sorter->direction - ); - } -} diff --git a/src/Repository/IncoShortGenesisRepository.php b/src/Repository/IncoShortGenesisRepository.php deleted file mode 100644 index 8affac0..0000000 --- a/src/Repository/IncoShortGenesisRepository.php +++ /dev/null @@ -1,77 +0,0 @@ - - */ -final class IncoShortGenesisRepository extends AbstractGenesisRepository implements IncoShortRepository -{ - /** - * @param \Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\BrandCode[] $brands - * @return PageOf<\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoShort> - */ - public function selectAllHavingBrands( - Pagination $pagination, - array $brands, - ?IncoSorter $sorter = null - ): PageOf { - $filterColumn = $this->mapper->getValueFor('brand'); - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->toSorter($sorter->sortBy), - $this->inFilter($filterColumn, $brands), - ); - } - - return $this->paginatedQuery( - $pagination, - null, - $this->inFilter($filterColumn, $brands), - ); - } - - public function findWhere( - string $incoIdLike, - array $inBrands, - Pagination $pagination, - ?IncoSorter $sorter = null - ): PageOf { - $whereLikeColumn = $this->mapper->getValueFor('id'); - $filterColumn = $this->mapper->getValueFor('brand'); - - if ($sorter !== null) { - return $this->paginatedQuery( - $pagination, - $this->toSorter($sorter->sortBy), - $this->inFilter($filterColumn, $inBrands), - ); - } - - return $this->paginatedQuery( - $pagination, - null, - $this->whereLikeFilter($whereLikeColumn, $incoIdLike), - $this->inFilter($filterColumn, $inBrands), - ); - } - - private function toSorter(IncoSortColumn $sortBy, ?Direction $direction = null): AbstractSorter - { - return new AbstractSorter( - $this->mapper->getValueFor($sortBy), - $direction ?? Direction::DESC - ); - } -} diff --git a/src/Utils/DateUtils.php b/src/Utils/DateUtils.php deleted file mode 100644 index 64e48ff..0000000 --- a/src/Utils/DateUtils.php +++ /dev/null @@ -1,89 +0,0 @@ -format(\DATE_RFC3339); - } - - public static function parseDateTime(string $datetime): \DateTimeInterface - { - $datetime = trim($datetime); - - /** @var \DateTimeInterface|null */ - $output = null; - - switch (true) { - case (bool) preg_match(self::FRENCH_DATE_REGEX, $datetime): - $output = self::parseFrench($datetime); - break; - case (bool) preg_match(self::RFC3339_REGEX, $datetime): - $output = self::parseRfc3339($datetime); - break; - case (bool) preg_match(self::ISO8601_REGEX, $datetime): - $output = self::parseIso8601($datetime); - break; - default: - try { - $output = new \DateTimeImmutable($datetime); - } catch (\Exception $e) { - throw new \InvalidArgumentException('Unrecognized date/time format: ' . $datetime); - } - } - - return $output; - } - - protected static function parseFrench(string $datetime): \DateTimeInterface - { - $dt = \DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $datetime . ' 00:00:00'); - - return $dt ?: self::parseFallback($datetime); - } - - protected static function parseRfc3339(string $datetime): \DateTimeInterface - { - $dt = \DateTimeImmutable::createFromFormat(\DateTime::RFC3339, $datetime); - if ($dt !== false) { - return $dt; - } - - $dt = \DateTimeImmutable::createFromFormat('Y-m-d\TH:i:s.uP', $datetime); - - return $dt ?: self::parseFallback($datetime); - } - - protected static function parseIso8601(string $datetime): \DateTimeInterface - { - $dt = \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $datetime); - - return $dt ?: self::parseFallback($datetime); - } - - protected static function parseFallback(string $datetime): \DateTimeInterface - { - try { - return new \DateTimeImmutable($datetime); - } catch (\Throwable $e) { - throw new \InvalidArgumentException("Unrecognized date/time format: {$datetime}"); - } - } -} diff --git a/src/Utils/StringEncoder.php b/src/Utils/StringEncoder.php deleted file mode 100644 index 6597117..0000000 --- a/src/Utils/StringEncoder.php +++ /dev/null @@ -1,325 +0,0 @@ - - */ - private const array CANONICAL_ENCODINGS = [ - 'UTF-8', - 'Windows-1252', - 'ISO-8859-15', - ]; - - /** - * Lowercase alias -> canonical name. - * - * Windows-1252 absorbs ISO-8859-1/Latin-1 because it is a strict superset: - * bytes 0xA0-0xFF are identical, and 0x80-0x9F are defined in Win-1252 - * but undefined (and useless) in ISO-8859-1. - * - * @var array - */ - private const array ENCODING_ALIASES = [ - // UTF-8 - 'utf8' => 'UTF-8', - 'utf-8' => 'UTF-8', - - // Windows-1252 (absorbs Latin-1 / ISO-8859-1) - 'win1252' => 'Windows-1252', - 'windows1252' => 'Windows-1252', - 'cp1252' => 'Windows-1252', - 'latin1' => 'Windows-1252', - 'latin-1' => 'Windows-1252', - 'iso-8859-1' => 'Windows-1252', - - // ISO-8859-15 (Latin-9) - 'latin9' => 'ISO-8859-15', - 'latin-9' => 'ISO-8859-15', - 'iso-8859-15' => 'ISO-8859-15', - ]; - - /** - * Windows-1252 specific codepoints (0x80-0x9F) absent from ISO-8859-1. - * - * Bytes 0x81, 0x8D, 0x8F, 0x90, 0x9D are officially undefined in Windows-1252. - * They are intentionally absent - encountering them triggers the - * Latin-1 fallback (Unicode codepoint = byte value). - * - * @var array Byte value -> UTF-8 character. - */ - private const array WIN1252_SPECIAL_BYTES = [ - 0x80 => "\u{20AC}", // € - 0x82 => "\u{201A}", // ‚ - 0x83 => "\u{0192}", // ƒ - 0x84 => "\u{201E}", // „ - 0x85 => "\u{2026}", // … - 0x86 => "\u{2020}", // † - 0x87 => "\u{2021}", // ‡ - 0x88 => "\u{02C6}", // ˆ - 0x89 => "\u{2030}", // ‰ - 0x8A => "\u{0160}", // Š - 0x8B => "\u{2039}", // ‹ - 0x8C => "\u{0152}", // Œ - 0x8E => "\u{017D}", // Ž - 0x91 => "\u{2018}", // ' - 0x92 => "\u{2019}", // ' - 0x93 => "\u{201C}", // " - 0x94 => "\u{201D}", // " - 0x95 => "\u{2022}", // • - 0x96 => "\u{2013}", // – - 0x97 => "\u{2014}", // — - 0x98 => "\u{02DC}", // ˜ - 0x99 => "\u{2122}", // ™ - 0x9A => "\u{0161}", // š - 0x9B => "\u{203A}", // › - 0x9C => "\u{0153}", // œ - 0x9E => "\u{017E}", // ž - 0x9F => "\u{0178}", // Ÿ - ]; - - /** - * ISO-8859-15 bytes that differ from ISO-8859-1. - * Only 8 codepoints differ between Latin-1 and Latin-9. - * - * @var array Byte value -> UTF-8 character. - */ - private const array LATIN9_OVERRIDES = [ - 0xA4 => "\u{20AC}", // € (replaces ¤) - 0xA6 => "\u{0160}", // Š (replaces ¦) - 0xA8 => "\u{0161}", // š (replaces ¨) - 0xB4 => "\u{017D}", // Ž (replaces ´) - 0xB8 => "\u{017E}", // ž (replaces ¸) - 0xBC => "\u{0152}", // Œ (replaces ¼) - 0xBD => "\u{0153}", // œ (replaces ½) - 0xBE => "\u{0178}", // Ÿ (replaces ¾) - ]; - - /** - * Converts a string from a known encoding to UTF-8. - * - * @param bool $stripBom Remove UTF-8 BOM from the result if present. - * - * @throws \ValueError If encoding is unsupported or the string is invalid. - */ - public static function toUtf8( - string $string, - string $sourceEncoding, - bool $stripBom = true, - ): string { - if ($string === '') { - return ''; - } - - $canonical = self::resolveEncoding($sourceEncoding); - - $result = match ($canonical) { - 'UTF-8' => self::ensureValidUtf8($string), - 'Windows-1252' => self::fromSingleByte($string, self::WIN1252_SPECIAL_BYTES), - 'ISO-8859-15' => self::fromSingleByte($string, self::LATIN9_OVERRIDES), - default => throw new \ValueError(sprintf( - 'Unsupported encoding "%s". Supported: %s', - $canonical, - implode(', ', self::supportedEncodings()), - )) - }; - - return $stripBom ? self::stripUtf8Bom($result) : $result; - } - - /** - * Attempts to detect the encoding and convert to UTF-8. - * - * Detection strategy: - * 1. If the string is valid UTF-8, it is treated as UTF-8. - * 2. Otherwise, the configured fallback encoding is used. - * - * This two-step approach is more reliable than mb_detect_encoding - * for single-byte encodings (which accept any byte sequence). - * - * @param string $fallbackEncoding Encoding to assume when the string is not valid UTF-8. - * Defaults to 'Windows-1252' (covers most Western European legacy data). - * @param bool $stripBom Remove UTF-8 BOM from the result if present. - * - * @throws \ValueError If fallbackEncoding is unsupported or is 'UTF-8'. - */ - public static function detectAndConvertToUtf8( - string $string, - string $fallbackEncoding = 'Windows-1252', - bool $stripBom = true, - ): string { - if ($string === '') { - return ''; - } - - $canonical = self::resolveEncoding($fallbackEncoding); - - if ($canonical === 'UTF-8') { - throw new \ValueError( - 'Fallback encoding cannot be UTF-8. Use toUtf8() for explicit UTF-8 validation.', - ); - } - - // UTF-8 is the only encoding we can reliably detect: - // it has strict byte sequence rules that reject invalid sequences. - if (mb_check_encoding($string, 'UTF-8')) { - $result = $string; - } else { - $result = self::toUtf8($string, $canonical, stripBom: false); - } - - return $stripBom ? self::stripUtf8Bom($result) : $result; - } - - /** - * Converts an array of strings to UTF-8 from a known encoding. - * - * @param list $strings - * @return list - */ - public static function arrayToUtf8( - array $strings, - string $sourceEncoding, - bool $stripBom = true, - ): array { - return array_map( - static fn (string $s): string => self::toUtf8($s, $sourceEncoding, $stripBom), - $strings, - ); - } - - /** - * Returns all supported encoding names (canonical + aliases). - * - * @return list - */ - public static function supportedEncodings(): array - { - return array_values(array_unique([ - ...self::CANONICAL_ENCODINGS, - ...array_keys(self::ENCODING_ALIASES), - ])); - } - - /** - * Resolves a user-supplied encoding name to its canonical form. - * - * @throws \ValueError If encoding is unknown. - */ - private static function resolveEncoding(string $encoding): string - { - $normalized = strtolower(trim($encoding)); - - if (isset(self::ENCODING_ALIASES[$normalized])) { - return self::ENCODING_ALIASES[$normalized]; - } - - $match = array_find( - self::CANONICAL_ENCODINGS, - static fn (string $canonical): bool => strtolower($canonical) === $normalized, - ); - - if ($match !== null) { - return $match; - } - - throw new \ValueError(sprintf( - 'Unsupported encoding "%s". Supported: %s', - $encoding, - implode(', ', self::supportedEncodings()), - )); - } - - /** - * Validates that a string is well-formed UTF-8. - * - * @throws \ValueError If the string contains invalid UTF-8 sequences. - */ - private static function ensureValidUtf8(string $string): string - { - if (! mb_check_encoding($string, 'UTF-8')) { - throw new \ValueError('String is not valid UTF-8.'); - } - - return $string; - } - - /** - * Converts a single-byte encoded string to UTF-8. - * - * All supported single-byte encodings share the same base: - * - 0x00-0x7F: ASCII (identity) - * - 0x80-0xFF: Encoding-specific - use overrides map first, then Latin-1 identity. - * - * @param array $overrides Byte -> UTF-8 mapping for encoding-specific codepoints. - * For Windows-1252: the 0x80-0x9F special range. - * For ISO-8859-15: the 8 bytes that differ from Latin-1. - */ - private static function fromSingleByte(string $string, array $overrides): string - { - /** @var array $bytes keyed from 1 (unpack behavior) */ - $bytes = unpack('C*', $string); - $parts = []; - - foreach ($bytes as $byte) { - $parts[] = match (true) { - $byte <= 0x7F => chr($byte), - isset($overrides[$byte]) => $overrides[$byte], - default => self::encodeLatin1ByteAsUtf8($byte), - }; - } - - return implode('', $parts); - } - - /** - * Encodes a single byte (0x80-0xFF) as a two-byte UTF-8 sequence. - * - * For Latin-1 / ISO-8859-1, the Unicode codepoint equals the byte value, - * so the UTF-8 encoding is: 110xxxxx 10xxxxxx. - * - * @throws \ValueError If the byte is outside the expected range. - */ - private static function encodeLatin1ByteAsUtf8(int $byte): string - { - if ($byte < 0x80 || $byte > 0xFF) { - throw new \ValueError( - sprintf('Byte must be in range 0x80-0xFF, got 0x%02X.', $byte), - ); - } - - return chr(0xC0 | ($byte >> 6)) . chr(0x80 | ($byte & 0x3F)); - } - - /** - * Strips the UTF-8 BOM (EF BB BF) if present at the start of the string. - */ - private static function stripUtf8Bom(string $string): string - { - if (str_starts_with($string, self::UTF8_BOM)) { - return substr($string, 3); - } - - return $string; - } -} diff --git a/src/Utils/StringUtils.php b/src/Utils/StringUtils.php deleted file mode 100644 index 548ab0a..0000000 --- a/src/Utils/StringUtils.php +++ /dev/null @@ -1,78 +0,0 @@ -getPath(); - - if ($basePath === '' || $basePath === '/') { - $newPath = $path; - } else { - $baseEndsWithSlash = substr($basePath, -1) === '/'; - $pathStartsWithSlash = substr($path, 0, 1) === '/'; - - if ($baseEndsWithSlash && $pathStartsWithSlash) { - $newPath = $basePath . ltrim($path, '/'); - } elseif (! $baseEndsWithSlash && ! $pathStartsWithSlash) { - $newPath = $basePath . '/' . $path; - } else { - $newPath = $basePath . $path; - } - } - - $newPath = preg_replace('#/+#', '/', $newPath) ?? $newPath; - - return $uri->withPath($newPath); - } - - public static function isHttpScheme(UriInterface $uri): bool - { - return in_array(strtolower($uri->getScheme()), ['http', 'https']); - } - - public static function calculateSignature( - HttpMethod $httpMethod, - #[\SensitiveParameter] - UriInterface $uri, - #[\SensitiveParameter] - string $body, - #[\SensitiveParameter] - string $secret, - \DateTimeInterface $timestamp - ): string { - $method = \strtoupper($httpMethod->value); - - $query = self::parseQueryString($uri->getQuery()); - $parametersMap = self::flattenAndSortParameters($query); - - $normalizedBody = ! empty($body) ? self::normalizeBody($body) : ''; - - $contentSha256 = $normalizedBody !== '' - ? base64_encode(hash('sha256', $normalizedBody, true)) - : ''; - - $stringToSign = implode("\n", [ - $method, - strtolower($uri->getHost()), - $uri->getPath(), - http_build_query($parametersMap, '', '&', \PHP_QUERY_RFC3986), - self::toRfc3339($timestamp), - $contentSha256, - ]); - - return base64_encode(hash_hmac( - 'sha256', - $stringToSign, - $secret, - true - )); - } - - public static function getNow(): \DateTimeInterface - { - return new \DateTimeImmutable('now', new \DateTimeZone('Europe/Paris')); - } - - public static function toRfc3339(\DateTimeInterface $date): string - { - return $date->format(\DATE_RFC3339); - } - - public static function deepCopy(mixed $value): mixed - { - $seen = []; - - return self::recursiveDeepCopy($value, $seen); - } - - /** - * @param array $seen - */ - protected static function recursiveDeepCopy(mixed $value, array &$seen): mixed // NOSONAR - { - if (is_array($value)) { - $out = []; - foreach ($value as $k => $v) { - $out[$k] = self::recursiveDeepCopy($v, $seen); - } - - return $out; - } - - if (is_object($value)) { - if ($value instanceof \Closure) { - return $value; // keep closure - } - - $id = spl_object_id($value); - if (isset($seen[$id])) { - return $seen[$id]; - } - - // try clone; if not clonable, keep original - $clone = clone $value; - - $seen[$id] = $clone; - - $ref = new \ReflectionObject($value); - - do { - foreach ($ref->getProperties() as $prop) { - $prop->setAccessible(true); // NOSONAR - $prop->setValue( // NOSONAR - $clone, - self::recursiveDeepCopy( - $prop->getValue($value), // NOSONAR - $seen - ) - ); - } - $ref = $ref->getParentClass(); - } while ($ref !== false); - - return $clone; - } - - // resources and scalars: return as-is - return $value; - } - - /** - * @param array|string> $parametersMap - * @return array - */ - protected static function flattenAndSortParameters( - #[\SensitiveParameter] - array $parametersMap, - ): array { - // NOTE: Ceci n'est surement pas pris en compte coté API (profondeur dans les parametres query), néanmois c'est géré pour l'instant - $flattenRequestArray = function (array $array) use (&$flattenRequestArray) { - $out = []; - foreach ($array as $key => $value) { - if (is_array($value)) { - $subArray = $flattenRequestArray($value); - foreach ($subArray as $subKey => $subValue) { - $out["$key.$subKey"] = $subValue; - } - - continue; - } - $out[$key] = $value; - } - - return $out; - }; - - $parametersMap = $flattenRequestArray($parametersMap); - - // ! Natural sort by keys. - ksort($parametersMap, \SORT_STRING); - - return $parametersMap; - } - - /** - * @return array|string> - */ - protected static function parseQueryString(string $query): array - { - $output = []; - parse_str($query, $output); - - return $output; - } - - /** Removes UTF-8 BOM et last CRLF */ - protected static function normalizeBody(string $bodyContent): string - { - if (str_starts_with($bodyContent, "\xEF\xBB\xBF")) { - $bodyContent = substr($bodyContent, 3); - } - - if ($bodyContent !== '' && str_ends_with($bodyContent, "\r\n")) { - return substr($bodyContent, 0, -2); - } - - if ( - $bodyContent !== '' && ( - str_ends_with($bodyContent, "\n") || str_ends_with($bodyContent, "\r")) - ) { - return substr($bodyContent, 0, -1); - } - - return $bodyContent; - } -} diff --git a/tests/Unit/ApiQueryBuilderTest.php b/tests/Unit/ApiQueryBuilderTest.php deleted file mode 100644 index d06a316..0000000 --- a/tests/Unit/ApiQueryBuilderTest.php +++ /dev/null @@ -1,67 +0,0 @@ -throws(\Error::class); - - it('can select * from table', function (): void { - // GIVEN - $table = 'someTable'; - $qBuilder = TableQueryBuilder::query(); - - // WHEN - $queryString = $qBuilder - ->from($table) - ->selectAll() - ->toQueryString(); - $queryString2 = $qBuilder - ->from($table) - ->toQueryString(); - - // THEN - expect($queryString)->toBe("table={$table}"); - expect($queryString2)->toBe("table={$table}"); - }); - - it('can select columns', function (): void { - // GIVEN - $table = 'someTable'; - $columns = ['col1', 'col2']; - $qBuilder = TableQueryBuilder::query(); - - // WHEN - $queryString = $qBuilder - ->from($table) - ->select(...$columns) - ->toQueryString(); - - // THEN - expect($queryString)->toBe("table={$table}&s_cols={$columns[0]}|{$columns[1]}"); - }); - - it('can paginate', function (): void { - // GIVEN - $table = 'someTable'; - $qBuilder = TableQueryBuilder::query(); - $pageNumber = 2; - $pageSize = 3; - $pagination = Pagination::ofPage($pageNumber, $pageSize); - - // WHEN - $queryString = $qBuilder - ->from($table) - ->selectAll() - ->paginate($pagination) - ->toQueryString(); - - // THEN - expect($queryString)->toBe("table={$table}&page={$pageNumber}&page_size={$pageSize}"); - }); -}); diff --git a/tests/Unit/Repository/EmployeeGenesisRepositoryTest.php b/tests/Unit/Repository/EmployeeGenesisRepositoryTest.php deleted file mode 100644 index d80e41a..0000000 --- a/tests/Unit/Repository/EmployeeGenesisRepositoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ -client = \Mockery::mock(ClientInterface::class); - /** @disregard P1056 */ - $this->requestFactory = \Mockery::mock(RequestFactory::class); - /** @disregard P1056 */ - $this->apiConfig = \Mockery::mock(ApiConfig::class); -}); - -afterEach(function () { - \Mockery::close(); -}); - -describe('Employee Genesis Repository', function (): void { - - it('can instanciate', function (): void { - // GIVEN - $apiCaller = \Mockery::mock(ApiCaller::class); - $dtoFactory = \Mockery::mock(DtoFactory::class); - $model = new EmployeeMapper(); - - // WHEN / THEN - new EmployeeGenesisRepository( - $apiCaller, - $dtoFactory, - $model - ); - })->throwsNoExceptions(); -}); diff --git a/tests/Unit/Repository/IncoCompoGenesisRepositoryTest.php b/tests/Unit/Repository/IncoCompoGenesisRepositoryTest.php deleted file mode 100644 index 500d987..0000000 --- a/tests/Unit/Repository/IncoCompoGenesisRepositoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ -client = \Mockery::mock(ClientInterface::class); - /** @disregard P1056 */ - $this->requestFactory = \Mockery::mock(RequestFactory::class); - /** @disregard P1056 */ - $this->apiConfig = \Mockery::mock(ApiConfig::class); -}); - -afterEach(function () { - \Mockery::close(); -}); - -describe('Inco Compo Genesis Repository', function (): void { - - it('can instanciate', function (): void { - // GIVEN - $apiCaller = \Mockery::mock(ApiCaller::class); - $dtoFactory = \Mockery::mock(DtoFactory::class); - $model = new IncoCompoMapper(); - - // WHEN / THEN - new IncoCompoGenesisRepository( - $apiCaller, - $dtoFactory, - $model - ); - })->throwsNoExceptions(); -}); diff --git a/tests/Unit/Repository/IncoGenesisRepositoryTest.php b/tests/Unit/Repository/IncoGenesisRepositoryTest.php deleted file mode 100644 index 2143669..0000000 --- a/tests/Unit/Repository/IncoGenesisRepositoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ -client = \Mockery::mock(ClientInterface::class); - /** @disregard P1056 */ - $this->requestFactory = \Mockery::mock(RequestFactory::class); - /** @disregard P1056 */ - $this->apiConfig = \Mockery::mock(ApiConfig::class); -}); - -afterEach(function () { - \Mockery::close(); -}); - -describe('Inco Genesis Repository', function (): void { - - it('can instanciate', function (): void { - // GIVEN - $apiCaller = \Mockery::mock(ApiCaller::class); - $dtoFactory = \Mockery::mock(DtoFactory::class); - $model = new IncoMapper(); - - // WHEN / THEN - new IncoGenesisRepository( - $apiCaller, - $dtoFactory, - $model - ); - })->throwsNoExceptions(); -}); diff --git a/tests/Unit/Service/EmployeeServiceTest.php b/tests/Unit/Service/EmployeeServiceTest.php deleted file mode 100644 index f644667..0000000 --- a/tests/Unit/Service/EmployeeServiceTest.php +++ /dev/null @@ -1,17 +0,0 @@ -throwsNoExceptions(); -}); diff --git a/tests/Unit/Service/IncoServiceTest.php b/tests/Unit/Service/IncoServiceTest.php deleted file mode 100644 index 41e350f..0000000 --- a/tests/Unit/Service/IncoServiceTest.php +++ /dev/null @@ -1,25 +0,0 @@ -throwsNoExceptions(); -}); diff --git a/tests/Unit/SignatureCalculationTest.php b/tests/Unit/SignatureCalculationTest.php deleted file mode 100644 index 4273d6f..0000000 --- a/tests/Unit/SignatureCalculationTest.php +++ /dev/null @@ -1,37 +0,0 @@ - 'NOM_FIC', - 'table' => 'RU_PERSONNE_SIRH', - ]; - $queryString = http_build_query($data, '', '&', \PHP_QUERY_RFC3986); - $method = HttpMethod::GET; - $uri = new Uri("https://{$host}/get?{$queryString}"); - $timestamp = new \DateTimeImmutable('2026-01-01 00:00:00', new \DateTimeZone('CEST')); - - // When - - $signature = Utils::calculateSignature( - $method, - $uri, - '', - $secret, - $timestamp - ); - - // Then - expect($signature)->toBe('IUwJIBCsmV4n5ahm09uaWQFwFMomjV08KtTDvVjFeTc='); - }); -}); diff --git a/tests/Unit/Utils/StringEncoderTest.php b/tests/Unit/Utils/StringEncoderTest.php deleted file mode 100644 index 5e318d0..0000000 --- a/tests/Unit/Utils/StringEncoderTest.php +++ /dev/null @@ -1,350 +0,0 @@ -toBe(''); - }); - - it('passes through a valid UTF-8 string with multibyte characters', function (): void { - // Given - $input = 'Héllo café résumé'; - - // When - $result = StringEncoder::toUtf8($input, 'UTF-8'); - - // Then - expect($result)->toBe('Héllo café résumé'); - }); - - it('throws ValueError for an invalid UTF-8 string', function (): void { - // Given - $input = "Invalid \xC3\x28 sequence"; - - // When / Then - StringEncoder::toUtf8($input, 'UTF-8'); - })->throws(\ValueError::class, 'not valid UTF-8'); - - it('strips the UTF-8 BOM by default', function (): void { - // Given - $input = "\xEF\xBB\xBFBonjour"; - - // When - $result = StringEncoder::toUtf8($input, 'UTF-8'); - - // Then - expect($result)->toBe('Bonjour'); - }); - - it('preserves the UTF-8 BOM when stripBom is false', function (): void { - // Given - $input = "\xEF\xBB\xBFBonjour"; - - // When - $result = StringEncoder::toUtf8($input, 'UTF-8', stripBom: false); - - // Then - expect($result)->toBe("\xEF\xBB\xBFBonjour"); - }); - - it('converts Windows-1252 special characters to UTF-8', function (int $byte, string $expectedUtf8): void { - // Given - $input = chr($byte); - - // When - $result = StringEncoder::toUtf8($input, 'Windows-1252'); - - // Then - expect($result)->toBe($expectedUtf8); - })->with([ - 'euro sign (0x80)' => [0x80, '€'], - 'single low-9 quote (0x82)' => [0x82, '‚'], - 'latin small f (0x83)' => [0x83, 'ƒ'], - 'double low-9 quote (0x84)' => [0x84, '„'], - 'ellipsis (0x85)' => [0x85, '…'], - 'dagger (0x86)' => [0x86, '†'], - 'double dagger (0x87)' => [0x87, '‡'], - 'circumflex (0x88)' => [0x88, 'ˆ'], - 'per mille (0x89)' => [0x89, '‰'], - 'S caron (0x8A)' => [0x8A, 'Š'], - 'left single guillemet (0x8B)' => [0x8B, '‹'], - 'OE ligature (0x8C)' => [0x8C, 'Œ'], - 'Z caron (0x8E)' => [0x8E, 'Ž'], - 'left single quote (0x91)' => [0x91, "\u{2018}"], - 'right single quote (0x92)' => [0x92, "\u{2019}"], - 'left double quote (0x93)' => [0x93, "\u{201C}"], - 'right double quote (0x94)' => [0x94, "\u{201D}"], - 'bullet (0x95)' => [0x95, '•'], - 'en dash (0x96)' => [0x96, '–'], - 'em dash (0x97)' => [0x97, '—'], - 'tilde (0x98)' => [0x98, '˜'], - 'trademark (0x99)' => [0x99, '™'], - 's caron (0x9A)' => [0x9A, 'š'], - 'right single guillemet (0x9B)' => [0x9B, '›'], - 'oe ligature (0x9C)' => [0x9C, 'œ'], - 'z caron (0x9E)' => [0x9E, 'ž'], - 'Y diaeresis (0x9F)' => [0x9F, 'Ÿ'], - ]); - - it('converts Windows-1252 high bytes (0xA0-0xFF) as ISO-8859-1 identity', function (): void { - // Given - $input = chr(0xE9); - - // When - $result = StringEncoder::toUtf8($input, 'Windows-1252'); - - // Then the result is the UTF-8 encoded é - expect($result)->toBe('é'); - }); - - it('converts a full Windows-1252 sentence with mixed ranges', function (): void { - // Given - $input = "B\x9Cuf \xE0 5\x80"; - - // When - $result = StringEncoder::toUtf8($input, 'Windows-1252'); - - // Then - expect($result)->toBe('Bœuf à 5€'); - }); - - it('handles undefined Windows-1252 bytes (0x81, 0x8D, 0x8F, 0x90, 0x9D) via Latin-1 fallback', function (int $byte): void { - // Given - $input = chr($byte); - - // When - $result = StringEncoder::toUtf8($input, 'Windows-1252'); - - // Then - expect(mb_check_encoding($result, 'UTF-8'))->toBeTrue() - ->and(mb_strlen($result, 'UTF-8'))->toBe(1); - })->with([0x81, 0x8D, 0x8F, 0x90, 0x9D]); - - it('converts ISO-8859-15 euro sign to UTF-8', function (): void { - // Given - $input = chr(0xA4); - - // When - $result = StringEncoder::toUtf8($input, 'ISO-8859-15'); - - // Then - expect($result)->toBe('€'); - }); - - it('accepts encoding aliases', function (string $alias, string $expected): void { - // Given - $input = "caf\xE9"; // é in Latin-1/Win-1252 - - // When - $result = StringEncoder::toUtf8($input, $alias); - - // Then - expect($result)->toBe($expected); - })->with([ - 'latin1' => ['latin1', 'café'], - 'latin-1' => ['latin-1', 'café'], - 'win1252' => ['win1252', 'café'], - 'windows1252' => ['windows1252', 'café'], - 'cp1252' => ['cp1252', 'café'], - ]); - - it('throws ValueError for an unsupported encoding', function (): void { - // Given / When / Then - StringEncoder::toUtf8('hello', 'EBCDIC-42'); - })->throws(\ValueError::class, 'Unsupported encoding'); -}); - -describe('detectAndConvertToUtf8()', function (): void { - - it('returns an empty string when given an empty string', function (): void { - // Given - $input = ''; - - // When - $result = StringEncoder::detectAndConvertToUtf8($input); - - // Then - expect($result)->toBe(''); - }); - - it('detects and passes through valid UTF-8 with a hint', function (): void { - // Given - $input = 'Héllo world 🌍'; - - // When - $result = StringEncoder::detectAndConvertToUtf8($input, 'Windows-1252'); - - // Then - expect($result)->toBe('Héllo world 🌍'); - }); - - it('strips the UTF-8 BOM from auto-detected UTF-8 strings', function (): void { - // Given - $input = "\xEF\xBB\xBFBonjour"; - - // When - $result = StringEncoder::detectAndConvertToUtf8($input); - - // Then - expect($result)->toBe('Bonjour'); - }); - - it('uses encoding hints to guide detection', function (): void { - // Given - $input = "5\x80"; - - // When - $result = StringEncoder::detectAndConvertToUtf8($input, 'Windows-1252'); - - // Then - expect($result)->toBe('5€'); - }); - - it('produces valid UTF-8 output regardless of detection result', function (): void { - // Given - $input = "\xFE\x01\xFE\x01"; - - // When / Then - try { - $result = StringEncoder::detectAndConvertToUtf8($input); - expect(mb_check_encoding($result, 'UTF-8'))->toBeTrue(); - } catch (\RuntimeException $e) { - expect($e->getMessage())->toContain('Unable to detect encoding'); - } - }); -}); - -describe('arrayToUtf8()', function (): void { - - it('handles an empty array', function (): void { - // Given - $input = []; - - // When - $result = StringEncoder::arrayToUtf8($input, 'UTF-8'); - - // Then - expect($result)->toBe([]); - }); - - it('converts an array of Windows-1252 strings with special characters', function (): void { - // Given - $input = ["5\x80", "B\x9Cuf"]; - - // When - $result = StringEncoder::arrayToUtf8($input, 'Windows-1252'); - - // Then - expect($result)->toBe(['5€', 'Bœuf']); - }); -}); - -describe('supportedEncodings()', function (): void { - - it('returns a non-empty list of strings', function (): void { - // Given / When - $result = StringEncoder::supportedEncodings(); - - // Then - expect($result) - ->toBeArray() - ->not->toBeEmpty() - ->each->toBeString(); - }); - - it('includes all canonical encodings', function (): void { - // Given - $expected = ['UTF-8', 'ISO-8859-15', 'Windows-1252']; - - // When - $result = StringEncoder::supportedEncodings(); - - // Then - foreach ($expected as $encoding) { - expect($result)->toContain($encoding); - } - }); - - it('includes common aliases', function (): void { - // Given - $aliases = ['latin1', 'latin-1', 'latin9', 'cp1252', 'utf8', 'utf-8']; - - // When - $result = StringEncoder::supportedEncodings(); - - // Then - foreach ($aliases as $alias) { - expect($result)->toContain($alias); - } - }); - - it('contains no duplicates', function (): void { - // Given - $result = StringEncoder::supportedEncodings(); - - // When / Then - expect($result)->toBe(array_values(array_unique($result))); - }); -}); - -describe('encoding resolution (via toUtf8)', function (): void { - - it('resolves case-insensitively', function (string $variant): void { - // Given - $input = 'hello'; - - // When - $result = StringEncoder::toUtf8($input, $variant); - - // Then - expect($result)->toBe('hello'); - })->with(['utf-8', 'UTF-8', 'Utf-8', 'UTF8', 'utf8']); - - it('trims whitespace around encoding names', function (): void { - // Given - $input = 'hello'; - - // When - $result = StringEncoder::toUtf8($input, ' utf-8 '); - - // Then - expect($result)->toBe('hello'); - }); -}); - -describe('regressions', function (): void { - - it('correctly converts "Bœuf" from Windows-1252 (œ = 0x9C)', function (): void { - // Given - $input = "B\x9Cuf"; - - // When - $result = StringEncoder::toUtf8($input, 'Windows-1252'); - - // Then - expect($result)->toBe('Bœuf') - ->and(mb_check_encoding($result, 'UTF-8'))->toBeTrue(); - }); - - it('round-trips UTF-8 → identity without corruption', function (): void { - // Given - $input = 'Héllo 世界 🎵 Ÿ œ € ™'; - - // When - $result = StringEncoder::toUtf8($input, 'UTF-8'); - - // Then - expect($result)->toBe($input); - }); -});