refactor(legacy): remove deprecated Genesis utilities, parsers, query builders, mappers and tests
This commit is contained in:
parent
eb10792328
commit
c5f7199b9c
58 changed files with 0 additions and 6188 deletions
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient;
|
||||
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use Uwil\ArcadiaApiPhpClient\Di\PackageDi;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\EmployeeService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\IncoService;
|
||||
|
||||
final class ArcadiaGenesisClient
|
||||
{
|
||||
private readonly Container $container;
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiConfig $apiConfig
|
||||
) {
|
||||
$builder = new ContainerBuilder();
|
||||
|
||||
PackageDi::initDi(
|
||||
$builder,
|
||||
$this->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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Di;
|
||||
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use GuzzleHttp\Client;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiCaller;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\EmployeeDtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\IncoCompoDtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\IncoDtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\IncoShortDtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\EmployeeMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoCompoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoShortMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\EmployeeGenesisRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\EmployeeRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoCompoGenesisRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoCompoRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoGenesisRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoShortGenesisRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoShortRepository;
|
||||
|
||||
final readonly class PackageDi
|
||||
{
|
||||
/**
|
||||
* @param \DI\ContainerBuilder<\DI\Container> $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),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Enum;
|
||||
|
||||
enum ArcadiaGenesisApiErrorCode: string
|
||||
{
|
||||
// UnDocumented
|
||||
case E_0004 = '0004';
|
||||
|
||||
// 400 - BAD_REQUEST
|
||||
case ERR_UNAUTHORIZED_PARAMETER = 'ERR_UNAUTHORIZED_PARAMETER';
|
||||
|
||||
/** Vous n'êtes pas autorisé à faire cette action. */
|
||||
case ERR_DEFAULT = 'ERR_DEFAULT';
|
||||
/** Le nombre de colonnes, d'opérateurs et de valeurs ne correspond pas. */
|
||||
case ERR_LIST_SIZE_MISMATCH = 'ERR_LIST_SIZE_MISMATCH';
|
||||
/** Une MetaException a été levée. Une erreur s'est produite. */
|
||||
case ERR_METAEXCEPTION = 'ERR_METAEXCEPTION';
|
||||
/** La liste de colonnes de tri n'est pas de la même taille que la liste d'opérateurs de tri. */
|
||||
case ERR_SORT_LIST_SIZE_MISMATCH = 'ERR_SORT_LIST_SIZE_MISMATCH';
|
||||
/** Le nom de la table n'est pas fourni. */
|
||||
case ERR_TABLE_NAME_MISSING = 'ERR_TABLE_NAME_MISSING';
|
||||
/** La liste d'opérateurs contient des opérateurs non autorisés. */
|
||||
case ERR_UNAUTHORIZED_OPERATOR = 'ERR_UNAUTHORIZED_OPERATOR';
|
||||
/** La date doit être fournie au format ISO 8601 (ex : 2025-10-21T14:30:00Z). */
|
||||
case ERR_BAD_DATE_FORMAT = 'ERR_BAD_DATE_FORMAT';
|
||||
/** Le type pour le paramètre 'page' doit être un entier supérieur à 0. */
|
||||
case ERR_PAGE_TYPE_MISMATCH = 'ERR_PAGE_TYPE_MISMATCH';
|
||||
/** Le type pour le paramètre 'page_size' doit être un entier supérieur à 0. */
|
||||
case ERR_PAGE_SIZE_TYPE_MISMATCH = 'ERR_PAGE_SIZE_TYPE_MISMATCH';
|
||||
/** Le nombre d'enregistrements retournés doit être <= 10 000. */
|
||||
case ERR_TOO_MANY_RECORDS = 'ERR_TOO_MANY_RECORDS';
|
||||
|
||||
// 401 - UNAUTHORIZED
|
||||
/** La clé API n'est pas valide. */
|
||||
case ERR_INVALID_API_KEY = 'ERR_INVALID_API_KEY';
|
||||
/** La signature n'est pas valide. */
|
||||
case ERR_INVALID_API_SIGNATURE = 'ERR_INVALID_API_SIGNATURE';
|
||||
|
||||
// 429 - TOO_MANY_REQUESTS
|
||||
/** Le nombre maximal de requêtes autorisées a été atteint. Veuillez réessayer ultérieurement. */
|
||||
case ERR_TOO_MANY_REQUESTS = 'ERR_TOO_MANY_REQUESTS';
|
||||
|
||||
// 503 - SERVICE_UNAVAILABLE
|
||||
// Pas de code d'erreur spécifique documenté
|
||||
|
||||
case UNKNOWN = 'UNKNOWN';
|
||||
|
||||
/** phpcs:disable Generic.Metrics.CyclomaticComplexity.TooHigh */
|
||||
public function description(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::ERR_DEFAULT => '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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Exception;
|
||||
|
||||
class ApiCallFailed extends \RuntimeException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Exception;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Enum\ArcadiaGenesisApiErrorCode;
|
||||
|
||||
class ApiErrorException extends \DomainException implements ArcadiaGenesisApiException
|
||||
{
|
||||
public readonly ArcadiaGenesisApiErrorCode $apiError;
|
||||
|
||||
public function __construct(ArcadiaGenesisApiErrorCode $apiError, ?\Throwable $previous = null)
|
||||
{
|
||||
$this->apiError = $apiError;
|
||||
|
||||
parent::__construct(
|
||||
$apiError->description(),
|
||||
$apiError->httpStatusCode(),
|
||||
$previous
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Exception;
|
||||
|
||||
interface ArcadiaGenesisApiException extends \Stringable
|
||||
{
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Exception;
|
||||
|
||||
class DomainException extends \DomainException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Exception;
|
||||
|
||||
class InvalidApiResponse extends \DomainException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Exception;
|
||||
|
||||
class UnexpectedErrorException extends \ErrorException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,208 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use GuzzleHttp\Exception\ConnectException;
|
||||
use GuzzleHttp\Exception\ServerException;
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Enum\ArcadiaGenesisApiErrorCode;
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\ApiCallFailed;
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\ApiErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\InvalidApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\TableQuery;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\Utils;
|
||||
|
||||
class ApiCaller
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ClientInterface $client,
|
||||
private readonly ApiConfig $apiConfig,
|
||||
private readonly RequestFactory $requestFactory
|
||||
) {
|
||||
}
|
||||
|
||||
public function callApi(string $queryPath, TableQuery $query): ApiResponse
|
||||
{
|
||||
$uri = Utils::appendPathTo(
|
||||
$this->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<string,mixed>
|
||||
*
|
||||
* @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<string, mixed> $responseData
|
||||
*/
|
||||
protected function apiResponseIsError(array $responseData): bool
|
||||
{
|
||||
return array_key_exists('error', $responseData)
|
||||
&& is_array($responseData['error'])
|
||||
&& ! empty($responseData['error']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PaginationDto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
|
||||
/**
|
||||
* @implements DtoFactory<Mapper, \Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
|
||||
/**
|
||||
* @template M of \Uwil\ArcadiaApiPhpClient\Mapper\Mapper
|
||||
* @template D of \Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto
|
||||
*/
|
||||
interface DtoFactory
|
||||
{
|
||||
/**
|
||||
* @param M $model
|
||||
*/
|
||||
public function __construct(Mapper $model);
|
||||
|
||||
/**
|
||||
* @return M $model
|
||||
*/
|
||||
public function model(): Mapper;
|
||||
|
||||
/**
|
||||
* @return ($pagination is null ? D : PageOf<D>)
|
||||
*
|
||||
* @phpstan-return ($pagination is null ? D : PageOf<D>)
|
||||
*/
|
||||
public function build(ApiResponse $response, ?Pagination $pagination = null): null | Dto | PageOf;
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Employee;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\EmployeeMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class EmployeeDtoFactory extends AbstractDtoFactory
|
||||
{
|
||||
protected readonly Mapper $model;
|
||||
|
||||
/** @param EmployeeMapper $model */
|
||||
public function __construct(Mapper $model)
|
||||
{
|
||||
if (! ($model instanceof EmployeeMapper)) {
|
||||
throw new \InvalidArgumentException('Expected model to be instance of ' . EmployeeMapper::class);
|
||||
}
|
||||
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/** @return Employee|PageOf<Employee>|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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoCompoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
|
||||
final class IncoCompoDtoFactory extends AbstractDtoFactory
|
||||
{
|
||||
protected readonly Mapper $model;
|
||||
|
||||
/** @param IncoCompoMapper $model */
|
||||
public function __construct(Mapper $model)
|
||||
{
|
||||
if (! ($model instanceof IncoCompoMapper)) {
|
||||
throw new \InvalidArgumentException('Expected model to be instance of ' . IncoCompoMapper::class);
|
||||
}
|
||||
|
||||
$this->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');
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
|
||||
final class IncoDtoFactory extends AbstractDtoFactory
|
||||
{
|
||||
protected readonly Mapper $model;
|
||||
|
||||
/** @param IncoMapper $model */
|
||||
public function __construct(Mapper $model)
|
||||
{
|
||||
if (! ($model instanceof IncoMapper)) {
|
||||
throw new \InvalidArgumentException('Expected model to be instance of ' . IncoMapper::class);
|
||||
}
|
||||
|
||||
$this->model = $model;
|
||||
}
|
||||
|
||||
/** @return Inco|PageOf<Inco> */
|
||||
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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoShort;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoShortMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
|
||||
final class IncoShortDtoFactory extends AbstractDtoFactory
|
||||
{
|
||||
protected readonly Mapper $model;
|
||||
|
||||
/** @param IncoShortMapper $model */
|
||||
public function __construct(Mapper $model)
|
||||
{
|
||||
if (! ($model instanceof IncoShortMapper)) {
|
||||
throw new \InvalidArgumentException('Expected model to be instance of ' . IncoShortMapper::class);
|
||||
}
|
||||
|
||||
$this->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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,987 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiParser;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Brand;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Additive;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Allegations;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Allergen;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\IncoWeight;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Ingredients;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\Nutriscore;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\PortionAmount;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\PortionType;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco\StorageCondition;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\DateUtils;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\StringUtils;
|
||||
|
||||
final readonly class IncoApiParser
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
throw new \Error('Abstract class');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $mappedValues
|
||||
* @return null|string|int|float|array<mixed>
|
||||
*/
|
||||
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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<string,null|mixed> $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<int, object{index: int|string, value: string}> */
|
||||
$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<int|string, string[]>
|
||||
*/
|
||||
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<TKey, TValue> $array
|
||||
* @param callable(TValue, TKey): array<int|string, TValue> $callback
|
||||
* @return array<int|string, TValue>
|
||||
*/
|
||||
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<mixed>|string $value
|
||||
* @return array<mixed>
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
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<string> $parts
|
||||
* @return list<string>
|
||||
*/
|
||||
protected static function extractNumericParts(array $parts): array
|
||||
{
|
||||
return array_values(array_filter($parts, fn ($p) => is_numeric($p)));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiParser;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\StringEncoder;
|
||||
|
||||
final readonly class IncoApiStringParser
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
throw new \Error('Abstract class');
|
||||
}
|
||||
|
||||
public static function toPhrase(string $value): string
|
||||
{
|
||||
$squished = preg_replace('/\s+/u', ' ', trim($value)) ?? '';
|
||||
|
||||
// * lowercase then uppercase the first character (multibyte-safe)
|
||||
$lower = mb_strtolower($squished);
|
||||
$firstChar = mb_strtoupper(mb_substr($lower, 0, 1));
|
||||
$rest = mb_substr($lower, 1);
|
||||
$ucfirst = $firstChar . $rest;
|
||||
|
||||
// * ensure it ends with a dot
|
||||
if (mb_substr($ucfirst, -1) !== '.') {
|
||||
$ucfirst .= '.';
|
||||
}
|
||||
|
||||
// * remove the last character (to match the original ->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 <br /> to preserve paragraph breaks
|
||||
$line = str_replace(["\r\n", "\r"], "\n", $line);
|
||||
$line = str_replace("\n", '<br />', $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 <br /> back to newlines and final strip_tags as double safety
|
||||
$line = str_replace('<br />', "\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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,360 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiParser;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\AcideGras;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\AcidesAmines;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\ApportsReference;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\ComposesSpeciaux;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\Energie;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\FruitLegumeNoix;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\GlucidesDetail;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\Identification;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\MacronutrimentsPortion;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\MacronutrimentsRef;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\Mineraux;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\NutriScore;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\PointsNutriScore;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo\Vitamines;
|
||||
|
||||
/**
|
||||
* Static factory / parser for IncoCompo-related DTOs.
|
||||
*
|
||||
* Cannot be instantiated.
|
||||
*/
|
||||
final readonly class IncoCompoApiParser
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
throw new \Error('Static class');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value for $field from $mappedValues, or throws if missing.
|
||||
*
|
||||
* @param array<string,mixed> $mappedValues
|
||||
* @return null|string|int|float|array<mixed>
|
||||
*/
|
||||
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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed> $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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
enum Direction: string
|
||||
{
|
||||
case ASC = 'asc';
|
||||
case DESC = 'desc';
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
enum Operator: string
|
||||
{
|
||||
case EQUAL = '=';
|
||||
case NOT_EQUAL = '!=';
|
||||
case GREATER_THAN = '>';
|
||||
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';
|
||||
}
|
||||
|
|
@ -1,102 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
final readonly class TableQuery implements \Stringable
|
||||
{
|
||||
/**
|
||||
* @param list<string> $selectColumns
|
||||
* @param list<array{column:string,operator:Operator,value:string}> $criteria
|
||||
* @param list<array{column:string,direction:Direction}> $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<string, string|int|null>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQueryBuildStep
|
||||
{
|
||||
public function build(): TableQuery;
|
||||
|
||||
public function toQueryString(): string;
|
||||
}
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
final class TableQueryBuilder implements
|
||||
TableQueryBuildStep,
|
||||
TableQueryFromStep,
|
||||
TableQueryOrderByStep,
|
||||
TableQueryPaginateStep,
|
||||
TableQuerySelectStep,
|
||||
TableQueryWhereStep
|
||||
{
|
||||
private string $table;
|
||||
|
||||
private ?string $soc = null;
|
||||
|
||||
private ?string $lang = null;
|
||||
|
||||
/** @var list<string> */
|
||||
private array $selectColumns = [];
|
||||
|
||||
/** @var list<array{column: string, operator: Operator, value: string}> */
|
||||
private array $criteria = [];
|
||||
|
||||
/** @var list<array{column: string, direction: Direction}> */
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQueryFromStep
|
||||
{
|
||||
public function from(string $table): TableQuerySelectStep;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQueryOrderByStep extends TableQueryPaginateStep
|
||||
{
|
||||
public function orderBy(string $column, Direction $direction = Direction::ASC): TableQueryOrderByStep;
|
||||
|
||||
public function orderByAsc(string $column): TableQueryOrderByStep;
|
||||
|
||||
public function orderByDesc(string $column): TableQueryOrderByStep;
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
interface TableQueryPaginateStep extends TableQueryBuildStep
|
||||
{
|
||||
public function paginate(Pagination $pagination): TableQueryBuildStep;
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQuerySelectStep extends TableQueryWhereStep
|
||||
{
|
||||
/**
|
||||
* Set the society code (soc parameter).
|
||||
*/
|
||||
public function society(string $soc): static;
|
||||
|
||||
/**
|
||||
* Set the language code (lang parameter).
|
||||
*/
|
||||
public function language(string $lang): static;
|
||||
|
||||
/**
|
||||
* Select specific columns (s_cols).
|
||||
*/
|
||||
public function select(string ...$columns): TableQueryWhereStep;
|
||||
|
||||
/**
|
||||
* Select all columns (no s_cols parameter will be sent).
|
||||
*/
|
||||
public function selectAll(): TableQueryWhereStep;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQueryWhereStep extends TableQueryOrderByStep
|
||||
{
|
||||
public function where(string $column, Operator $operator, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereEqual(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereNotEqual(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereGreaterThan(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereGreaterThanOrEqual(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereLessThan(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereLessThanOrEqual(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereBetween(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereNotBetween(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereLike(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereNotLike(string $column, string $value): TableQueryWhereStep;
|
||||
|
||||
public function whereIn(string $column, string ...$value): TableQueryWhereStep;
|
||||
|
||||
public function whereNotIn(string $column, string ...$value): TableQueryWhereStep;
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiResponse;
|
||||
|
||||
final readonly class ApiPagination
|
||||
{
|
||||
public function __construct(
|
||||
public int $page,
|
||||
public int $pageSize,
|
||||
public int $totalItems,
|
||||
public int $totalPages,
|
||||
public ApiPaginationLinks $links,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiResponse;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
final readonly class ApiPaginationLinks
|
||||
{
|
||||
public function __construct(
|
||||
public UriInterface $first,
|
||||
public UriInterface $last,
|
||||
public ?UriInterface $next,
|
||||
public ?UriInterface $previous
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiResponse;
|
||||
|
||||
use UnexpectedValueException;
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\InvalidApiResponse;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\StringEncoder;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\Utils;
|
||||
|
||||
// https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/734
|
||||
// Les property hook ne sont pas supportés.
|
||||
// phpcs:disable
|
||||
|
||||
final class ApiResponse
|
||||
{
|
||||
private const DATA_KEY = 'data';
|
||||
|
||||
private const PAGINATION_KEY = 'pagination';
|
||||
|
||||
public readonly ?ApiPagination $pagination;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
public array $data {
|
||||
get {
|
||||
return Utils::deepCopy($this->data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $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<string,mixed> $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<string,mixed>):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<string,mixed> $responseData
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
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<string,mixed> $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<string, mixed> */
|
||||
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<string, mixed> $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<string,mixed> $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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
|
||||
final readonly class AbstractSorter
|
||||
{
|
||||
public function __construct(
|
||||
public string $sortBy,
|
||||
public Direction $direction
|
||||
) {
|
||||
if (empty(mb_trim($sortBy))) {
|
||||
throw new DomainException('Sort by cannot be empty');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
|
||||
final readonly class EmployeeSorter
|
||||
{
|
||||
public function __construct(
|
||||
public EmployeeSortColum $sortBy,
|
||||
public Direction $direction
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
|
||||
enum HttpMethod: string
|
||||
{
|
||||
case HEAD = 'HEAD';
|
||||
case GET = 'GET';
|
||||
case POST = 'POST';
|
||||
case PUT = 'PUT';
|
||||
case PATCH = 'PATCH';
|
||||
case DELETE = 'DELETE';
|
||||
}
|
||||
|
|
@ -1,160 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\Psr7;
|
||||
|
||||
use GuzzleHttp\Psr7\Request as Psr7Request;
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\Psr7\Utils as Psr7Utils;
|
||||
use GuzzleHttp\Utils as GuzzleHttpUtils;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\HttpMethod;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\Utils;
|
||||
|
||||
final readonly class RequestFactory
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
private readonly ApiConfig $apiConfig
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array-key,mixed> $data
|
||||
* @param array<string,string> $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<string,string> $headers
|
||||
* @return array<string,string>
|
||||
*/
|
||||
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<string,mixed> $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<array-key,mixed> $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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,150 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Mapper;
|
||||
|
||||
use DomainException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Employee;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoApiStringParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ArcadiaEmployeeId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\EmployeeSortColum as EmployeeSortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\EstablishmentCode;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ServiceId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SiteId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\DateUtils;
|
||||
|
||||
final class EmployeeMapper implements Mapper
|
||||
{
|
||||
protected string $table = 'RU_PERSONNE_SIRH';
|
||||
|
||||
protected string $tableKey = 'matricule';
|
||||
|
||||
/**
|
||||
* Mapping camelCase (propriété PHP) => NOM_COLONNE (API/BDD).
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
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<string,null|mixed> */
|
||||
$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']
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,435 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Mapper;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoApiStringParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoCompoApiParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SortColumn;
|
||||
|
||||
final class IncoCompoMapper implements Mapper
|
||||
{
|
||||
private string $table = 'INCO_COMPO';
|
||||
|
||||
private string $tableKey = 'id';
|
||||
|
||||
/**
|
||||
* Mapping camelCase (propriété PHP) => NOM_COLONNE (API/BDD).
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
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<string, mixed> $data
|
||||
* @return array<string, null|mixed>
|
||||
*/
|
||||
protected function mapColumns(array $data): array
|
||||
{
|
||||
/** @var array<string,null|mixed> */
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Mapper;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoApiParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoApiStringParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoSortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\StringUtils;
|
||||
|
||||
final class IncoMapper implements Mapper
|
||||
{
|
||||
private string $table = 'INCO';
|
||||
|
||||
private string $tableKey = 'id';
|
||||
|
||||
/**
|
||||
* Mapping camelCase (propriété PHP) => NOM_COLONNE (API/BDD).
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
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<string,null|mixed> */
|
||||
$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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Mapper;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoShort;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoApiParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiParser\IncoApiStringParser;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoSortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\StringUtils;
|
||||
|
||||
final class IncoShortMapper implements Mapper
|
||||
{
|
||||
private string $table = 'INCO';
|
||||
|
||||
private string $tableKey = 'id';
|
||||
|
||||
/**
|
||||
* Mapping camelCase (propriété PHP) => NOM_COLONNE (API/BDD).
|
||||
*
|
||||
* @var array<string, string>
|
||||
*/
|
||||
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<string,null|mixed> */
|
||||
$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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Mapper;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SortColumn;
|
||||
|
||||
interface Mapper
|
||||
{
|
||||
public function getTable(): string;
|
||||
|
||||
public function getTableKey(): string;
|
||||
|
||||
/** @return array<string,string> */
|
||||
public function getPropertiesMap(): array;
|
||||
|
||||
public function getValueFor(string | SortColumn $column): string;
|
||||
|
||||
/**
|
||||
* @param array<string,null|mixed> $data
|
||||
*
|
||||
* @throws \Uwil\ArcadiaApiPhpClient\Exception\UnexpectedErrorException If mapping fails.
|
||||
*/
|
||||
public function toDto(array $data): Dto;
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Repository;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiCaller;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\DtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\TableQueryBuilder;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\TableQueryWhereStep;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\AbstractSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\Mapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\DateUtils;
|
||||
|
||||
/**
|
||||
* @template D of Dto
|
||||
*/
|
||||
abstract class AbstractGenesisRepository
|
||||
{
|
||||
private string $queryPath = '/get';
|
||||
|
||||
/**
|
||||
* @param DtoFactory<Mapper, D> $dtoFactory
|
||||
*/
|
||||
public function __construct(
|
||||
private readonly ApiCaller $apiCaller,
|
||||
protected readonly DtoFactory $dtoFactory,
|
||||
protected readonly Mapper $mapper
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param (callable(TableQueryWhereStep):TableQueryWhereStep) $filters
|
||||
* @return PageOf<D>
|
||||
*/
|
||||
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<object{value:string|int}> $values
|
||||
* @return callable(TableQueryWhereStep):TableQueryWhereStep
|
||||
*/
|
||||
protected function inFilter(string $column, array $values): callable
|
||||
{
|
||||
/** @var array<string> $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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1,164 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Repository;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Employee;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\AbstractSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ArcadiaEmployeeId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\EmployeeSortColum;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\EmployeeSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
/**
|
||||
* @extends AbstractGenesisRepository<Employee>
|
||||
*/
|
||||
final class EmployeeGenesisRepository extends AbstractGenesisRepository implements EmployeeRepository
|
||||
{
|
||||
/** @return PageOf<Employee> */
|
||||
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<Employee> */
|
||||
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<Employee> */
|
||||
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<Employee> */
|
||||
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<Employee>
|
||||
*/
|
||||
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<Employee>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Repository;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
|
||||
/**
|
||||
* @extends AbstractGenesisRepository<IncoCompo>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Repository;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\AbstractSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoSortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
/**
|
||||
* @extends AbstractGenesisRepository<Inco>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Repository;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\AbstractSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoSortColumn;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
/**
|
||||
* @extends AbstractGenesisRepository<\Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoShort>
|
||||
*/
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Utils;
|
||||
|
||||
final readonly class DateUtils
|
||||
{
|
||||
private const FRENCH_DATE_REGEX = '/^\d{4}-\d{2}-\d{2}$/';
|
||||
|
||||
private const ISO8601_REGEX = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/';
|
||||
|
||||
private const RFC3339_REGEX = '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+\-]\d{2}:\d{2})$/';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
throw new \Error('Abstract class');
|
||||
}
|
||||
|
||||
public static function toRfc3339(\DateTimeInterface | string $datetime): string
|
||||
{
|
||||
$datetime = is_string($datetime) ? self::parseDateTime($datetime) : $datetime;
|
||||
|
||||
return $datetime->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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,325 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Utils;
|
||||
|
||||
/**
|
||||
* Converts strings from Western European single-byte encodings to UTF-8.
|
||||
*
|
||||
* Supported source encodings:
|
||||
* - UTF-8 (passthrough with validation)
|
||||
* - Windows-1252 (superset of ISO-8859-1, covers Latin-1 in practice)
|
||||
* - ISO-8859-15 (Latin-9: Latin-1 + €, œ, Œ, Š, š, Ž, ž, Ÿ)
|
||||
*
|
||||
* Auto-detection is inherently unreliable for single-byte encodings.
|
||||
* Always prefer toUtf8() with an explicit encoding.
|
||||
*/
|
||||
final class StringEncoder
|
||||
{
|
||||
/** UTF-8 BOM sequence (EF BB BF). */
|
||||
private const string UTF8_BOM = "\xEF\xBB\xBF";
|
||||
|
||||
/**
|
||||
* Canonical encodings supported by this class.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
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<string, string>
|
||||
*/
|
||||
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<int, string> 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<int, string> 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<string> $strings
|
||||
* @return list<string>
|
||||
*/
|
||||
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<string>
|
||||
*/
|
||||
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<int, string> $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<int, int> $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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Utils;
|
||||
|
||||
final readonly class StringUtils
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
throw new \Error('Abstract class');
|
||||
}
|
||||
|
||||
public static function mbStrtok(string $delimiters, ?string $str = null): string | false
|
||||
{
|
||||
// * Keep track of the position on the string for each subsequent call.
|
||||
static $pos = 0;
|
||||
static $string = '';
|
||||
|
||||
// * If a new string is passed, reset the static parameters.
|
||||
if ($str !== null) {
|
||||
$pos = 0;
|
||||
$string = $str;
|
||||
}
|
||||
|
||||
// * Initialize the token.
|
||||
$token = '';
|
||||
|
||||
while ($pos < mb_strlen($string)) {
|
||||
$char = mb_substr($string, $pos, 1);
|
||||
$pos++;
|
||||
|
||||
if (mb_strpos($delimiters, $char) === false) {
|
||||
$token .= $char;
|
||||
} else {
|
||||
// * Don't return empty strings.
|
||||
if ($token !== '') {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Check whether there is a last token to return.
|
||||
if ($token !== '') {
|
||||
return $token;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* ASCII-transliterate and lowercase a string for slug-style comparisons.
|
||||
*/
|
||||
public static function slugify(string $value): string
|
||||
{
|
||||
$ascii = (string) iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $value);
|
||||
|
||||
return strtolower(trim($ascii));
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a string into words, respecting Unicode word boundaries.
|
||||
*
|
||||
* @param string $value The input string.
|
||||
* @param int $words Maximum number of words to keep (default: 100).
|
||||
* @param string $end String to append when truncated (default: ' ...').
|
||||
*/
|
||||
public static function words(string $value, int $words = 100, string $end = ' ...'): string
|
||||
{
|
||||
preg_match('/^\s*+(?:\S+\s*+){1,' . $words . '}/u', $value, $matches);
|
||||
|
||||
if (! isset($matches[0]) || mb_strlen($value) === mb_strlen($matches[0])) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return rtrim($matches[0]) . $end;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,222 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Utils;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\HttpMethod;
|
||||
|
||||
final readonly class Utils
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
throw new \Error('Abstract class');
|
||||
}
|
||||
|
||||
public static function appendPathTo(UriInterface $uri, string $path): UriInterface
|
||||
{
|
||||
if ($path === '') {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
$basePath = $uri->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<int, mixed> $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, array<mixed>|string> $parametersMap
|
||||
* @return array<string,string>
|
||||
*/
|
||||
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<array-key,array<mixed>|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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiQueryBuilder\TableQueryBuilder;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Pagination;
|
||||
|
||||
describe('Api Query Builder', function (): void {
|
||||
|
||||
it('cannot be directly instanciated', function (): void {
|
||||
new TableQueryBuilder();
|
||||
})->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}");
|
||||
});
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Tests\TestCase;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiCaller;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\DtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\EmployeeMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\EmployeeGenesisRepository;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
/** @disregard P1056 */
|
||||
$this->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();
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Tests\TestCase;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiCaller;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\DtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoCompoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoCompoGenesisRepository;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
/** @disregard P1056 */
|
||||
$this->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();
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Psr\Http\Client\ClientInterface;
|
||||
use Tests\TestCase;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiCaller;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Factory\DtoFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoGenesisRepository;
|
||||
|
||||
uses(TestCase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
/** @disregard P1056 */
|
||||
$this->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();
|
||||
});
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\EmployeeRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\EmployeeService;
|
||||
|
||||
describe('Employee Service', function (): void {
|
||||
|
||||
it('can instanciate', function (): void {
|
||||
// GIVEN
|
||||
$employeeRepository = \Mockery::mock(EmployeeRepository::class);
|
||||
|
||||
// WHEN / THEN
|
||||
new EmployeeService($employeeRepository);
|
||||
})->throwsNoExceptions();
|
||||
});
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoCompoRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoShortRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\IncoService;
|
||||
|
||||
describe('Inco Service', function (): void {
|
||||
|
||||
it('can instanciate', function (): void {
|
||||
// GIVEN
|
||||
$incoRepository = \Mockery::mock(IncoRepository::class);
|
||||
$incoShortRepository = \Mockery::mock(IncoShortRepository::class);
|
||||
$incoCompoRepository = \Mockery::mock(IncoCompoRepository::class);
|
||||
|
||||
// WHEN / THEN
|
||||
new IncoService(
|
||||
$incoRepository,
|
||||
$incoShortRepository,
|
||||
$incoCompoRepository,
|
||||
);
|
||||
})->throwsNoExceptions();
|
||||
});
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\HttpMethod;
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\Utils;
|
||||
|
||||
describe('Signature calculation', function (): void {
|
||||
|
||||
it('can calculate signature on get method', function (): void {
|
||||
// Given
|
||||
$host = 'sub.example.net';
|
||||
$secret = 'MYSUPERSECRET';
|
||||
$data = [
|
||||
'c_cols' => '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=');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,350 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Utils\StringEncoder;
|
||||
|
||||
describe('toUtf8()', function (): void {
|
||||
|
||||
it('returns an empty string when given an empty string', function (): void {
|
||||
// Given
|
||||
$input = '';
|
||||
|
||||
// When
|
||||
$result = StringEncoder::toUtf8($input, 'UTF-8');
|
||||
|
||||
// Then
|
||||
expect($result)->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);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue