Compare commits
No commits in common. "main" and "init-jeremy" have entirely different histories.
main
...
init-jerem
215 changed files with 13566 additions and 11671 deletions
6
.gitattributes
vendored
6
.gitattributes
vendored
|
|
@ -1,6 +0,0 @@
|
|||
/tests export-ignore
|
||||
/.gitlab-ci.yml export-ignore
|
||||
/packages export-ignore
|
||||
/phpstan.neon.dist export-ignore
|
||||
.gitignore export-ignore
|
||||
.gitattributes export-ignore
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -1,4 +1,3 @@
|
|||
/packages/
|
||||
/vendor/
|
||||
.php-cs-fixer.cache
|
||||
junit.xml
|
||||
|
|
@ -6,5 +5,3 @@ junit.xml
|
|||
coverage
|
||||
coverage.xml
|
||||
test.php
|
||||
.scannerwork/
|
||||
/composer.lock
|
||||
|
|
|
|||
|
|
@ -1,59 +1,59 @@
|
|||
include:
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "lint/phpcs.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "lint/phpstan.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "security/composer.yml"
|
||||
- project: "ldc-web/shared-ci"
|
||||
ref: "main"
|
||||
file: "test/test.yml"
|
||||
|
||||
stages:
|
||||
- lint
|
||||
- security
|
||||
- test
|
||||
- publish
|
||||
- project: 'ldc-web/shared-ci'
|
||||
ref: 'main'
|
||||
file: 'lint/phpcs.yml'
|
||||
- project: 'ldc-web/shared-ci'
|
||||
ref: 'main'
|
||||
file: 'lint/phpstan.yml'
|
||||
- project: 'ldc-web/shared-ci'
|
||||
ref: 'main'
|
||||
file: 'security/composer.yml'
|
||||
- project: 'ldc-web/shared-ci'
|
||||
ref: 'main'
|
||||
file: 'test/test.yml'
|
||||
|
||||
phplint:
|
||||
stage: lint
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-qa-8.5:latest
|
||||
variables:
|
||||
TEXT_REPORT: php-l-report.txt
|
||||
before_script:
|
||||
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
||||
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
|
||||
script:
|
||||
- composer run-script lint:php > "$TEXT_REPORT" 2>&1; status=$?
|
||||
- rm -f "$TEXT_REPORT"
|
||||
- touch "$TEXT_REPORT"
|
||||
- bash -c 'composer run-script lint:php 2>&1 | tee "$TEXT_REPORT"; status=${PIPESTATUS[0]}; if [ $status -ne 0 ]; then echo "composer lint command failed (exit $status)"; fi; exit $status' || true
|
||||
- cat "$TEXT_REPORT"
|
||||
- if [ $status -ne 0 ] || grep -qE "Parse error|Errors parsing" "$TEXT_REPORT"; then echo "PHP lint failed"; exit 1; fi
|
||||
- if grep -qE "Parse error|Errors parsing" "$TEXT_REPORT"; then echo "PHP lint failed"; exit 1; fi
|
||||
artifacts:
|
||||
when: always
|
||||
expose_as: "php-l report"
|
||||
expose_as: 'php-l report'
|
||||
paths:
|
||||
- php-l-report.txt
|
||||
reports:
|
||||
junit: php-l-report.txt
|
||||
allow_failure: false
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
pint:
|
||||
stage: lint
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-qa-8.5:latest
|
||||
variables:
|
||||
PINT_REPORT: pint-report.txt
|
||||
before_script:
|
||||
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
||||
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
|
||||
script:
|
||||
- vendor/bin/pint --test > "$PINT_REPORT" 2>&1; status=$?
|
||||
- rm -f "$PINT_REPORT"
|
||||
- touch "$PINT_REPORT"
|
||||
- bash -c 'vendor/bin/pint --test 2>&1 | tee "$PINT_REPORT"; status=${PIPESTATUS[0]}; exit $status'
|
||||
- cat "$PINT_REPORT"
|
||||
- if [ $status -ne 0 ] || grep -qE "ERROR|ERRORS|ERROR:" "$PINT_REPORT"; then echo "Pint reported errors"; exit 1; fi
|
||||
- if [ -f "$PINT_REPORT" ] && grep -qE "ERROR|ERRORS|ERROR:" "$PINT_REPORT"; then echo "Pint reported errors"; exit 1; fi
|
||||
artifacts:
|
||||
when: always
|
||||
expose_as: "pint report"
|
||||
expose_as: 'pint report'
|
||||
paths:
|
||||
- pint-report.txt
|
||||
allow_failure: false
|
||||
|
|
@ -62,7 +62,7 @@ pint:
|
|||
|
||||
pest:
|
||||
stage: test
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-ci-8.5:latest
|
||||
image: $CI_REGISTRY/ldc-web/projets/ldc-web-images-docker/uwil-qa-8.5:latest
|
||||
variables:
|
||||
JUNIT_REPORT: junit.xml
|
||||
TEXT_REPORT: test-output.txt
|
||||
|
|
@ -71,14 +71,14 @@ pest:
|
|||
- composer config gitlab-token.lab.s-web.fr $CI_DEPLOY_USER $CI_DEPLOY_PASSWORD
|
||||
- composer install --no-scripts --no-interaction --no-ansi --no-progress --prefer-dist
|
||||
script:
|
||||
- rm -f "$JUNIT_REPORT" "$COVERAGE_REPORT"
|
||||
- composer run-script test:coverage -- --min=85 > "$TEXT_REPORT" 2>&1; status=$?
|
||||
- rm -f "$JUNIT_REPORT" "$TEXT_REPORT" "$COVERAGE_REPORT"
|
||||
- touch "$TEXT_REPORT"
|
||||
- bash -c 'composer run-script test:coverage 2>&1 | tee "$TEXT_REPORT"; status=${PIPESTATUS[0]}; exit $status'
|
||||
- cat "$TEXT_REPORT"
|
||||
- if [ -f "$COVERAGE_REPORT" ]; then echo "Coverage report generated $COVERAGE_REPORT"; else echo "No coverage report found"; fi
|
||||
- exit $status
|
||||
artifacts:
|
||||
when: always
|
||||
expose_as: "pest report"
|
||||
expose_as: 'pest report'
|
||||
paths:
|
||||
- junit.xml
|
||||
- test-output.txt
|
||||
|
|
@ -89,22 +89,7 @@ pest:
|
|||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
|
||||
|
||||
publish:
|
||||
stage: publish
|
||||
image: curlimages/curl:latest
|
||||
variables:
|
||||
URL: "$CI_SERVER_PROTOCOL://$CI_SERVER_HOST:$CI_SERVER_PORT/api/v4/projects/$CI_PROJECT_ID/packages/composer?job_token=$CI_JOB_TOKEN"
|
||||
script:
|
||||
- version=$(echo "tag=$CI_COMMIT_TAG")
|
||||
- insecure=$([ "$CI_SERVER_PROTOCOL" = "http" ] && echo "--insecure" || echo "")
|
||||
- response=$(curl -s -w "\n%{http_code}" $insecure --data $version $URL)
|
||||
- code=$(echo "$response" | tail -n 1)
|
||||
- body=$(echo "$response" | head -n 1)
|
||||
- if [ "$code" -eq 201 ]; then
|
||||
echo "Package created - Code $code - $body";
|
||||
else
|
||||
echo "Could not create package - Code $code - $body";
|
||||
exit 1;
|
||||
fi
|
||||
rules:
|
||||
- if: '$CI_COMMIT_TAG =~ /^\d+.\d+.\d+/'
|
||||
stages:
|
||||
- lint
|
||||
- security
|
||||
- test
|
||||
|
|
|
|||
6
.vscode/launch.json
vendored
6
.vscode/launch.json
vendored
|
|
@ -1,12 +1,6 @@
|
|||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Listen for Xdebug",
|
||||
"type": "php",
|
||||
"request": "launch",
|
||||
"port": 9003
|
||||
},
|
||||
{
|
||||
"name": "Listen for Xdebug (Docker)",
|
||||
"type": "php",
|
||||
|
|
|
|||
45
.vscode/settings.json
vendored
45
.vscode/settings.json
vendored
|
|
@ -1,7 +1,15 @@
|
|||
{
|
||||
"sonarlint.analysisExcludes": "vendor/**,packages/**,junit.xml,coverage/**",
|
||||
"php.version": "8.5.0",
|
||||
"intelephense.environment.phpVersion": "8.5.0",
|
||||
"php.executables": {
|
||||
"php84": {
|
||||
"binary": "/usr/local/bin/php84",
|
||||
"paths": {
|
||||
"/app": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
},
|
||||
"php.executablePath": "/usr/local/bin/php84",
|
||||
"php.version": "8.4.0",
|
||||
"intelephense.environment.phpVersion": "8.4.0",
|
||||
"intelephense.environment.includePaths": [
|
||||
"./vendor/pestphp",
|
||||
"./vendor/phpunit/phpunit",
|
||||
|
|
@ -17,6 +25,9 @@
|
|||
"${workspaceFolder}/vendor/bin/phpstan"
|
||||
],
|
||||
"phpstan.configFile": "phpstan.neon.dist",
|
||||
"phpstan.paths": {
|
||||
"${workspaceFolder}": "/app"
|
||||
},
|
||||
"files.associations": { "*.module": "php" },
|
||||
"php.suggest.basic": false,
|
||||
"laravel-pint.enable": true,
|
||||
|
|
@ -79,33 +90,5 @@
|
|||
"cmd": "${workspaceFolder}/vendor/bin/pint ${file}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"workbench.colorCustomizations": {
|
||||
"activityBar.activeBackground": "#045a4a",
|
||||
"activityBar.background": "#045a4a",
|
||||
"activityBar.foreground": "#e7e7e7",
|
||||
"activityBar.inactiveForeground": "#e7e7e799",
|
||||
"activityBarBadge.background": "#d029f5",
|
||||
"activityBarBadge.foreground": "#e7e7e7",
|
||||
"activityBarTop.activeBackground": "#045a4a",
|
||||
"activityBarTop.background": "#045a4a",
|
||||
"activityBarTop.foreground": "#e7e7e7",
|
||||
"activityBarTop.inactiveForeground": "#e7e7e799",
|
||||
"commandCenter.border": "#e7e7e799",
|
||||
"commandCenter.foreground": "#e7e7e7",
|
||||
"sash.hoverBorder": "#045a4a",
|
||||
"statusBar.background": "#022922",
|
||||
"statusBar.debuggingBackground": "#022922",
|
||||
"statusBar.debuggingForeground": "#e7e7e7",
|
||||
"statusBar.foreground": "#e7e7e7",
|
||||
"statusBarItem.hoverBackground": "#045a4a",
|
||||
"statusBarItem.remoteBackground": "#9709b6",
|
||||
"statusBarItem.remoteForeground": "#e7e7e7",
|
||||
"titleBar.activeBackground": "#022922",
|
||||
"titleBar.activeForeground": "#e7e7e7",
|
||||
"titleBar.inactiveBackground": "#02292299",
|
||||
"titleBar.inactiveForeground": "#e7e7e799",
|
||||
"activityBar.activeBorder": "#e7e7e7",
|
||||
"activityBarTop.activeBorder": "#e7e7e7"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
337
README.md
337
README.md
|
|
@ -1,325 +1,42 @@
|
|||
# Arcadia API PHP Client
|
||||
# Arcadia Genesis PHP Client
|
||||
|
||||
[](https://www.php.net/)
|
||||
[](LICENSE)
|
||||
[](https://pestphp.com/)
|
||||
[](https://phpstan.org/)
|
||||
## Mappers
|
||||
|
||||
Client PHP haut niveau, fortement typé et orienté domaine pour l'API Arcadia.
|
||||
Il s'appuie sur le client OpenAPI généré par Kiota (`uwil/arcadia-kiota-open-api-client`) et fournit :
|
||||
- Une couche de services métier (`EmployeeService`, `IncoService`, `BrandService`, `ApiService`).
|
||||
- Des Value Objects stricts (`ArcadiaEmployeeId`, `IncoId`, `IncoCompoId`, `BrandCode`, `SiteId`, etc.).
|
||||
- Des DTOs riches (`Employee`, `Inco`, `IncoCompo`, `IncoShort`, `Brand`) avec typage complet et conformité `JsonSerializable`.
|
||||
- Une gestion unifiée de la pagination (`PageOf`, `Pagination`) et du parcours continu par flux (`StreamOf`).
|
||||
- L'authentification automatique avec signature HMAC (`HMAC-SHA256`) et gestion des nonces.
|
||||
- Le support de la journalisation (PSR-3 `LoggerInterface`) et de l'observabilité (OpenTelemetry).
|
||||
- Une gestion explicite des erreurs via la hiérarchie `ApiError`.
|
||||
### Employee
|
||||
|
||||
---
|
||||
|
||||
## Sommaire
|
||||
|
||||
- [Prérequis](#prérequis)
|
||||
- [Installation](#installation)
|
||||
- [Initialisation](#initialisation)
|
||||
- [Services et Utilisation](#services-et-utilisation)
|
||||
- [Vérification de l'API (`apiService`)](#vérification-de-lapi-apiservice)
|
||||
- [Gestion des Collaborateurs (`employees`)](#gestion-des-collaborateurs-employees)
|
||||
- [Fiches INCO & Compositions (`incoSheets`)](#fiches-inco--compositions-incosheets)
|
||||
- [Gestion des Marques (`brands`)](#gestion-des-marques-brands)
|
||||
- [Pagination et Streaming](#pagination-et-streaming)
|
||||
- [Gestion des Erreurs](#gestion-des-erreurs)
|
||||
- [Tests et Qualité](#tests-et-qualité)
|
||||
- [Licence](#licence)
|
||||
|
||||
---
|
||||
|
||||
## Prérequis
|
||||
|
||||
- **PHP 8.5+**
|
||||
- Extensions PHP : `ext-curl`, `ext-mbstring`
|
||||
- Accès au registre GitLab Composer de LDC Digital
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Comme cette bibliothèque et sa sous-dépendance Kiota sont hébergées sur le registre privé GitLab de LDC, configurez le dépôt Composer dans le `composer.json` de votre projet :
|
||||
|
||||
```json
|
||||
{
|
||||
"repositories": [
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Puis installez le client via Composer :
|
||||
|
||||
```bash
|
||||
composer require uwil/arcadia-api-php-client
|
||||
```
|
||||
|
||||
Assurez-vous d'avoir configuré vos identifiants d'accès GitLab pour Composer (via `auth.json` ou variable d'environnement CI) :
|
||||
|
||||
```bash
|
||||
composer config gitlab-token.lab.s-web.fr <deploy-token-user> <deploy-token-password>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Initialisation
|
||||
|
||||
L'entrée principale de la bibliothèque est [`ArcadiaApiClient`](src/ArcadiaApiClient.php), configurée avec [`ApiConfig`](src/Lib/ApiConfig.php) :
|
||||
#### Props
|
||||
|
||||
```php
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Uwil\ArcadiaApiPhpClient\ArcadiaApiClient;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ClientIdentifier;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ClientSecret;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
|
||||
$config = new ApiConfig(
|
||||
endpoint: Utils::uriFor('https://arcadia-api.domain.tld/api'),
|
||||
apiKey: new ClientIdentifier('VOTRE_API_KEY'),
|
||||
secret: new ClientSecret('VOTRE_SECRET_KEY'),
|
||||
curlTimeOut: 20.0, // secondes (optionnel, défaut: 20.0)
|
||||
maxRetries: 3, // tentatives en cas d'erreur réseau (optionnel, défaut: 0)
|
||||
retryDelaySeconds: 1, // délai entre les retries (optionnel, défaut: 1)
|
||||
ignoreSelfSignedCert: false, // pour environnements locaux de test (défaut: false)
|
||||
logger: null, // ?Psr\Log\LoggerInterface
|
||||
tracer: null, // ?OpenTelemetry\API\Trace\TracerInterface
|
||||
);
|
||||
|
||||
$client = new ArcadiaApiClient($config);
|
||||
public string $matricule;
|
||||
public string $lastname;
|
||||
public string $firstname;
|
||||
public string $start_date;
|
||||
public string|null $end_date;
|
||||
public string|null $planned_end_date;
|
||||
public string $establishment;
|
||||
public string $site;
|
||||
public string $service;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Services et Utilisation
|
||||
|
||||
### Vérification de l'API (`apiService`)
|
||||
|
||||
Permet de vérifier la disponibilité et la santé de l'API Arcadia :
|
||||
#### Usage
|
||||
|
||||
```php
|
||||
$isAlive = $client->apiService()->isAlive();
|
||||
<?php
|
||||
|
||||
if ($isAlive) {
|
||||
echo "L'API Arcadia est joignable et opérationnelle.\n";
|
||||
}
|
||||
use Uwil\ArcadiaGenesisPhpClient\Datas\EmployeesDatas;
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
$employee = EmployeesDatas::getById("0115590");
|
||||
|
||||
var_dump($employee);
|
||||
?>
|
||||
```
|
||||
|
||||
---
|
||||
### INCO
|
||||
|
||||
### Gestion des Collaborateurs (`employees`)
|
||||
TODO
|
||||
|
||||
Le service [`EmployeeService`](src/Service/EmployeeService.php) permet de consulter et rechercher les salariés.
|
||||
## TESTS
|
||||
|
||||
#### Récupérer par identifiant
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ArcadiaEmployeeId;
|
||||
use Uwil\ArcadiaApiPhpClient\Error\ApiError;
|
||||
|
||||
$employee = $client->employees()->findBy(new ArcadiaEmployeeId('0115590'));
|
||||
|
||||
if ($employee instanceof ApiError) {
|
||||
echo "Erreur API: " . $employee->getMessage();
|
||||
} elseif ($employee === null) {
|
||||
echo "Collaborateur non trouvé (404).\n";
|
||||
} else {
|
||||
echo "Salarié: {$employee->firstname} {$employee->lastname} (Site: {$employee->site})\n";
|
||||
}
|
||||
```
|
||||
|
||||
#### Récupérer par site(s)
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SiteId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiSorter\EmployeeSorter;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiSorter\EmployeeSortColumn;
|
||||
|
||||
// Pour un site unique avec tri par matricule
|
||||
$page = $client->employees()->getAllOfSites(
|
||||
new SiteId('01'),
|
||||
new EmployeeSorter(EmployeeSortColumn::MATRICULE, 'asc')
|
||||
);
|
||||
|
||||
// Pour plusieurs sites modifiés depuis une date
|
||||
$page = $client->employees()->getAllOfSitesSince(
|
||||
[new SiteId('01'), new SiteId('02')],
|
||||
new DateTimeImmutable('2026-01-01')
|
||||
);
|
||||
```
|
||||
|
||||
#### Streaming de tous les collaborateurs
|
||||
Le streaming parcourt automatiquement toutes les pages de façon transparente grâce à un générateur PHP :
|
||||
|
||||
```php
|
||||
foreach ($client->employees()->streamAll() as $employee) {
|
||||
echo "{$employee->matricule} - {$employee->lastname}\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Fiches INCO & Compositions (`incoSheets`)
|
||||
|
||||
Le service [`IncoService`](src/Service/IncoService.php) gère les fiches techniques produit (INCO) et leurs compositions nutritionnelles détaillées.
|
||||
|
||||
#### Consulter une fiche INCO
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
|
||||
$inco = $client->incoSheets()->findBy(new IncoId('12345'));
|
||||
|
||||
if ($inco !== null && !($inco instanceof ApiError)) {
|
||||
echo "Fiche: {$inco->commercialLabel}\n";
|
||||
echo "Nutri-Score: {$inco->nutriscore?->grade}\n";
|
||||
}
|
||||
```
|
||||
|
||||
#### Consulter la composition nutritionnelle
|
||||
Vous pouvez interroger la composition par son `IncoCompoId` ou directement par l'`IncoId` :
|
||||
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
|
||||
// Directement via IncoCompoId
|
||||
$compo = $client->incoSheets()->findCompo(new IncoCompoId('98765'));
|
||||
|
||||
// Ou via IncoId
|
||||
$compo = $client->incoSheets()->findCompo(new IncoId('12345'));
|
||||
|
||||
if ($compo !== null && !($compo instanceof ApiError)) {
|
||||
echo "Énergie (kJ): {$compo->energy?->kJ?->value}\n";
|
||||
echo "Acides gras saturés: {$compo->fattyAcids?->saturatedFattyAcids?->value}\n";
|
||||
echo "Points Nutri-Score: {$compo->nutriScorePoints?->totalPoints}\n";
|
||||
}
|
||||
```
|
||||
|
||||
#### Recherche et listage par marque
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
|
||||
// Liste courte (IncoShort) pour optimiser les performances d'affichage
|
||||
$shortsPage = $client->incoSheets()->getAllShortOfBrands(new BrandCode('GAULOIS'));
|
||||
|
||||
// Recherche par motif d'identifiant
|
||||
$results = $client->incoSheets()->findByIncoIdLike('POULET%', new BrandCode('GAULOIS'));
|
||||
|
||||
// Streaming de toutes les fiches d'une marque
|
||||
foreach ($client->incoSheets()->streamAllOfBrands(new BrandCode('LEGAULOIS')) as $sheet) {
|
||||
echo "Fiche #{$sheet->id->value}: {$sheet->commercialLabel}\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Gestion des Marques (`brands`)
|
||||
|
||||
Le service [`BrandService`](src/Service/BrandService.php) donne accès aux marques référencées :
|
||||
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
|
||||
// Récupérer une marque
|
||||
$brand = $client->brands()->findBy(new BrandCode('GAULOIS'));
|
||||
|
||||
// Parcourir toutes les marques
|
||||
foreach ($client->brands()->streamAll() as $brand) {
|
||||
echo "Marque [{$brand->code->value}] : {$brand->label}\n";
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pagination et Streaming
|
||||
|
||||
### Objet `PageOf`
|
||||
|
||||
Toutes les méthodes de liste paginées retournent un objet [`PageOf<T>`](src/Lib/ApiDto/Dto/PageOf.php) contenant les éléments et les métadonnées de pagination :
|
||||
|
||||
```php
|
||||
$page = $client->employees()->getAll();
|
||||
|
||||
// Éléments de la page courante
|
||||
$employees = $page->items;
|
||||
|
||||
// Métadonnées
|
||||
echo "Page actuelle : " . $page->pagination->pageNumber->value . "\n";
|
||||
echo "Taille de page : " . $page->pagination->pageSize->value . "\n";
|
||||
echo "Total éléments : " . $page->pagination->totalItems . "\n";
|
||||
echo "Total pages : " . $page->pagination->totalPages . "\n";
|
||||
```
|
||||
|
||||
### Objet `StreamOf`
|
||||
|
||||
Toutes les méthodes `stream*()` retournent un [`StreamOf<T>`](src/Lib/Pagination/StreamOf.php) qui implémente `\IteratorAggregate`. Il effectue les requêtes de page suivante automatiquement à la demande :
|
||||
|
||||
```php
|
||||
$stream = $client->employees()->streamAll();
|
||||
|
||||
foreach ($stream as $employee) {
|
||||
// Les pages suivantes sont chargées en arrière-plan au fur et à mesure de l'itération
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Gestion des Erreurs
|
||||
|
||||
Les méthodes du client ne lancent pas d'exceptions inattendues pour les erreurs HTTP ou de communication ; elles renvoient une instance de [`ApiError`](src/Error/ApiError.php) (ou ses sous-classes spécialisées) :
|
||||
|
||||
```php
|
||||
use Uwil\ArcadiaApiPhpClient\Error\AuthenticationError;
|
||||
use Uwil\ArcadiaApiPhpClient\Error\ValidationError;
|
||||
use Uwil\ArcadiaApiPhpClient\Error\NetworkError;
|
||||
use Uwil\ArcadiaApiPhpClient\Error\RateLimitError;
|
||||
use Uwil\ArcadiaApiPhpClient\Error\ServerError;
|
||||
use Uwil\ArcadiaApiPhpClient\Error\ApiError;
|
||||
|
||||
$result = $client->employees()->findBy(new ArcadiaEmployeeId('0115590'));
|
||||
|
||||
match (true) {
|
||||
$result instanceof AuthenticationError => logAuthFailure($result),
|
||||
$result instanceof ValidationError => logValidationDetails($result->getViolations()),
|
||||
$result instanceof NetworkError => retryLater($result),
|
||||
$result instanceof RateLimitError => handleRateLimit($result),
|
||||
$result instanceof ServerError => notifyAdmin($result),
|
||||
$result instanceof ApiError => logGenericError($result),
|
||||
$result === null => handleNotFound(),
|
||||
default => handleEmployee($result),
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tests et Qualité
|
||||
|
||||
Le projet est rigoureusement testé avec une suite complète de tests unitaires et d'architecture.
|
||||
|
||||
```bash
|
||||
# Lancer les tests unitaires
|
||||
composer test
|
||||
|
||||
# Vérifier la couverture de code (seuil minimal 85% configuré)
|
||||
composer test:coverage
|
||||
|
||||
# Analyse statique PHPStan (niveau 10)
|
||||
composer lint:phpstan
|
||||
|
||||
# Vérifier le style du code (Laravel Pint)
|
||||
composer lint:pint
|
||||
|
||||
# Formater le code automatiquement
|
||||
composer format
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Licence
|
||||
|
||||
Ce projet est sous licence MIT. Consultez le fichier [LICENSE](LICENSE) pour plus d'informations.
|
||||
[coverage](./coverage/index.html)
|
||||
|
|
|
|||
5
TODO.md
Normal file
5
TODO.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# TODO
|
||||
|
||||
- add pest tests
|
||||
- map more info on INCO
|
||||
- add Nutritioanl Values call when authorized
|
||||
|
|
@ -1,15 +1,11 @@
|
|||
{
|
||||
"name": "uwil/arcadia-api-php-client",
|
||||
"description": "PHP Client implementation for Arcadia API",
|
||||
"name": "uwil/arcadia-genesis-php-client",
|
||||
"description": "PHP Client implementation for Arcadia Genesis API",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Uwil\\ArcadiaApiPhpClient\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Uwil\\ArcadiaGenesisPhpClient\\": "src/",
|
||||
"Tests\\": "tests/"
|
||||
}
|
||||
},
|
||||
|
|
@ -20,21 +16,19 @@
|
|||
}
|
||||
],
|
||||
"scripts": {
|
||||
"analyse": "vendor/bin/phpstan analyse",
|
||||
"format": "vendor/bin/pint",
|
||||
"lint:php": "find . -path './vendor' -prune -o -type f -name '*.php' -print0 | xargs -0 -n1 -P4 -I{} php -l \"{}\"",
|
||||
"lint:phpcs": "vendor/bin/phpcs",
|
||||
"lint:phpstan": "vendor/bin/phpstan analyse",
|
||||
"lint:pint": "vendor/bin/pint --no-interaction --quiet",
|
||||
"test": "vendor/bin/pest --log-junit=junit.xml",
|
||||
"test:coverage": "vendor/bin/pest --log-junit=junit.xml --coverage --coverage-html=coverage --coverage-clover=coverage.xml"
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": "^8.4",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=8.3",
|
||||
"guzzlehttp/guzzle": "^7.10",
|
||||
"uwil/arcadia-kiota-open-api-client": "^0.1.0",
|
||||
"symfony/polyfill-php86": "^1.41"
|
||||
"php-di/php-di": "^7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpstan/phpstan": "^2.1",
|
||||
|
|
@ -46,18 +40,10 @@
|
|||
"mockery/mockery": "^1.6",
|
||||
"dg/bypass-finals": "^1.9"
|
||||
},
|
||||
"repositories": [
|
||||
{
|
||||
"type": "composer",
|
||||
"url": "https://lab.s-web.fr/api/v4/group/23/-/packages/composer/packages.json"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"pestphp/pest-plugin": true,
|
||||
"php-http/discovery": true,
|
||||
"tbachert/spi": true
|
||||
"pestphp/pest-plugin": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6850
composer.lock
generated
Normal file
6850
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,7 +2,8 @@ parameters:
|
|||
paths:
|
||||
- src
|
||||
|
||||
level: 10
|
||||
# Level 9 is the highest level
|
||||
level: 8
|
||||
|
||||
|
||||
# Enforce strict types analysis
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
sonar.projectKey=arcadia-genesis-php-client
|
||||
sonar.projectName=Arcadia Genesis PHP Client
|
||||
sonar.sources=src
|
||||
sonar.tests=tests
|
||||
sonar.sourceEncoding=UTF-8
|
||||
sonar.exclusions=**/vendor/**,**/packages/**,**/coverage/**,junit.xml
|
||||
sonar.php.exclusions=**/vendor/**,**/packages/**
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Factory\ApiClientFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Factory\ServiceFactory;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\ApiService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\BrandService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\EmployeeService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\IncoService;
|
||||
|
||||
class ArcadiaApiClient
|
||||
{
|
||||
private readonly ServiceFactory $serviceFactory;
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiConfig $apiConfig
|
||||
) {
|
||||
$apiClientFactory = new ApiClientFactory($this->apiConfig);
|
||||
$apiClient = $apiClientFactory->createClient();
|
||||
|
||||
$this->serviceFactory = new ServiceFactory(
|
||||
$apiClient,
|
||||
$this->apiConfig
|
||||
);
|
||||
}
|
||||
|
||||
public function apiService(): ApiService
|
||||
{
|
||||
return $this->serviceFactory->createApiService();
|
||||
}
|
||||
|
||||
public function employees(): EmployeeService
|
||||
{
|
||||
return $this->serviceFactory->createEmployeeService();
|
||||
}
|
||||
|
||||
public function incoSheets(): IncoService
|
||||
{
|
||||
return $this->serviceFactory->createIncoService();
|
||||
}
|
||||
|
||||
public function brands(): BrandService
|
||||
{
|
||||
return $this->serviceFactory->createBrandService();
|
||||
}
|
||||
}
|
||||
40
src/ArcadiaGenesisClient.php
Normal file
40
src/ArcadiaGenesisClient.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient;
|
||||
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Di\PackageDi;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Service\EmployeeService;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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,100 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
use GuzzleHttp\Promise\PromiseInterface;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Uwil\ArcadiaApiPhpClient\Enum\HttpMethod;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\LibMiddleware;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ApiMandatoryHeadersMiddleware implements LibMiddleware
|
||||
{
|
||||
public const string HANDLER_NAME = 'Arcadia API request Signature';
|
||||
|
||||
public const string HEADER_CLIENT_IDENTIFIER = 'X-Client-Identifier';
|
||||
|
||||
public const string HEADER_BODY_HASH = 'X-Body-Hash';
|
||||
|
||||
public const string HEADER_TIMESTAMP = 'X-Timestamp';
|
||||
|
||||
public const string HEADER_SIGNATURE = 'X-Signature';
|
||||
|
||||
public const string HEADER_NONCE = 'X-Nonce';
|
||||
|
||||
public static function signRequest(
|
||||
ClientIdentifier $apiKey,
|
||||
ClientSecret $withSecret,
|
||||
RequestSignatureCalculator $signatureCalculator,
|
||||
): callable {
|
||||
return static function (callable $handler) use ($apiKey, $withSecret, $signatureCalculator): self {
|
||||
return new self(
|
||||
$handler,
|
||||
$apiKey,
|
||||
$withSecret,
|
||||
$signatureCalculator
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callable(RequestInterface, array<string, mixed>): PromiseInterface $nextHandler
|
||||
*/
|
||||
public function __construct(
|
||||
private $nextHandler,
|
||||
private readonly ClientIdentifier $apiKey,
|
||||
private readonly ClientSecret $withSecret,
|
||||
private readonly RequestSignatureCalculator $signatureCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RequestInterface $request, array $options): PromiseInterface
|
||||
{
|
||||
$fn = $this->nextHandler;
|
||||
|
||||
return $fn($this->sign($request), $options);
|
||||
}
|
||||
|
||||
private function sign(RequestInterface $request): RequestInterface
|
||||
{
|
||||
$body = $request->getBody();
|
||||
$body->rewind();
|
||||
$bodyContents = $body->getContents();
|
||||
|
||||
$timestamp = new \DateTimeImmutable('now', new \DateTimeZone('UTC'));
|
||||
$nonce = new Nonce(value: Uuid::uuid7());
|
||||
|
||||
$signatureResult = $this->signatureCalculator->sign(
|
||||
new RequestSignaturePayload(
|
||||
method: HttpMethod::from($request->getMethod()),
|
||||
uri: $request->getUri(),
|
||||
timestamp: $timestamp,
|
||||
nonce: $nonce,
|
||||
body: $bodyContents,
|
||||
),
|
||||
$this->withSecret,
|
||||
);
|
||||
|
||||
$headers = [
|
||||
self::HEADER_CLIENT_IDENTIFIER => $this->apiKey->value,
|
||||
self::HEADER_TIMESTAMP => $timestamp->format(\DateTimeInterface::RFC3339),
|
||||
self::HEADER_NONCE => $nonce->value->toString(),
|
||||
self::HEADER_BODY_HASH => $signatureResult->bodyHash,
|
||||
self::HEADER_SIGNATURE => $signatureResult->value,
|
||||
];
|
||||
|
||||
return Utils::modifyRequest($request, [
|
||||
'set_headers' => array_filter(
|
||||
$headers,
|
||||
static fn (string $name): bool => ! $request->hasHeader($name),
|
||||
ARRAY_FILTER_USE_KEY
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
final readonly class ClientIdentifier
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
public string $value,
|
||||
) {
|
||||
if (empty(mb_trim($value))) {
|
||||
throw new \InvalidArgumentException('ClientIdentifier cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
final readonly class ClientSecret
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
public string $value,
|
||||
) {
|
||||
if (empty(mb_trim($value))) {
|
||||
throw new \InvalidArgumentException('ClientSecret cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class Nonce
|
||||
{
|
||||
public function __construct(
|
||||
public readonly UuidInterface $value,
|
||||
) {
|
||||
if (! Uuid::isValid($value->toString())) {
|
||||
throw new \InvalidArgumentException('Nonce value is not a valid UUID');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
final readonly class RequestSignature
|
||||
{
|
||||
public function __construct(
|
||||
public string $value,
|
||||
public string $bodyHash,
|
||||
) {
|
||||
if (empty(mb_trim($value))) {
|
||||
throw new \InvalidArgumentException('Signature value cannot be empty.');
|
||||
}
|
||||
if (empty(mb_trim($bodyHash))) {
|
||||
throw new \InvalidArgumentException('Body hash cannot be empty.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
class RequestSignatureCalculator
|
||||
{
|
||||
private const DATE_FORMAT = \DateTimeInterface::RFC3339;
|
||||
|
||||
private const HASH_ALGORITHM = 'sha256';
|
||||
|
||||
public function sign(RequestSignaturePayload $payload, ClientSecret $withSecret): RequestSignature
|
||||
{
|
||||
$bodyHash = hash(self::HASH_ALGORITHM, $payload->body);
|
||||
|
||||
$payloadToSign = [
|
||||
$payload->method->value,
|
||||
$this->normalizePathAndQuery($payload->uri),
|
||||
$payload->timestamp->format(self::DATE_FORMAT),
|
||||
$payload->nonce->value->toString(),
|
||||
$bodyHash,
|
||||
];
|
||||
|
||||
$signature = hash_hmac(
|
||||
self::HASH_ALGORITHM,
|
||||
implode('|', $payloadToSign),
|
||||
$withSecret->value
|
||||
);
|
||||
|
||||
return new RequestSignature(
|
||||
value: $signature,
|
||||
bodyHash: $bodyHash,
|
||||
);
|
||||
}
|
||||
|
||||
private function normalizePathAndQuery(UriInterface $uri): string
|
||||
{
|
||||
$path = $uri->getPath();
|
||||
$query = $uri->getQuery();
|
||||
|
||||
if ($query === '') {
|
||||
return $path;
|
||||
}
|
||||
|
||||
parse_str($query, $queryParams);
|
||||
ksort($queryParams);
|
||||
$sortedQuery = http_build_query($queryParams);
|
||||
|
||||
return "{$path}?{$sortedQuery}";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Auth;
|
||||
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Enum\HttpMethod;
|
||||
|
||||
final readonly class RequestSignaturePayload
|
||||
{
|
||||
public function __construct(
|
||||
public HttpMethod $method,
|
||||
public UriInterface $uri,
|
||||
public \DateTimeInterface $timestamp,
|
||||
public Nonce $nonce,
|
||||
public string $body,
|
||||
) {
|
||||
}
|
||||
}
|
||||
86
src/Di/PackageDi.php
Normal file
86
src/Di/PackageDi.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Di;
|
||||
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use GuzzleHttp\Client;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiCaller;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory\EmployeeDtoFactory;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory\IncoCompoDtoFactory;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory\IncoDtoFactory;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory\IncoShortDtoFactory;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\EmployeeMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\IncoCompoMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\IncoMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\IncoShortMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\EmployeeGenesisRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\EmployeeRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\IncoCompoGenesisRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\IncoCompoRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\IncoGenesisRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\IncoRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Repository\IncoShortGenesisRepository;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
96
src/Enum/ArcadiaGenesisApiErrorCode.php
Normal file
96
src/Enum/ArcadiaGenesisApiErrorCode.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Enum;
|
||||
|
||||
enum HttpMethod: string
|
||||
{
|
||||
case GET = 'GET';
|
||||
case POST = 'POST';
|
||||
case PUT = 'PUT';
|
||||
case PATCH = 'PATCH';
|
||||
case DELETE = 'DELETE';
|
||||
case HEAD = 'HEAD';
|
||||
case OPTIONS = 'OPTIONS';
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
abstract readonly class AbstractApiError implements ApiError
|
||||
{
|
||||
public function __construct(
|
||||
public string $message,
|
||||
public int $statusCode = 0,
|
||||
public ?\Throwable $previous = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public function message(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function statusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
|
||||
public function previous(): ?\Throwable
|
||||
{
|
||||
return $this->previous;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
interface ApiError
|
||||
{
|
||||
public function message(): string;
|
||||
|
||||
public function statusCode(): int;
|
||||
|
||||
public function previous(): ?\Throwable;
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
final readonly class AuthenticationError extends AbstractApiError
|
||||
{
|
||||
}
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
final readonly class NetworkError extends AbstractApiError
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
final readonly class RateLimitError extends AbstractApiError
|
||||
{
|
||||
public function __construct(
|
||||
string $message,
|
||||
public ?int $retryAfter = null,
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, 429, $previous);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
final readonly class ServerError extends AbstractApiError
|
||||
{
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
final readonly class UnexpectedApiError extends AbstractApiError
|
||||
{
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Error;
|
||||
|
||||
final readonly class ValidationError extends AbstractApiError
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $violations Map of invalid parameter => reason
|
||||
*/
|
||||
public function __construct(
|
||||
string $message,
|
||||
public array $violations = [],
|
||||
?\Throwable $previous = null,
|
||||
) {
|
||||
parent::__construct($message, 422, $previous);
|
||||
}
|
||||
}
|
||||
9
src/Exception/ApiCallFailed.php
Normal file
9
src/Exception/ApiCallFailed.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Exception;
|
||||
|
||||
class ApiCallFailed extends \RuntimeException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
40
src/Exception/ApiErrorException.php
Normal file
40
src/Exception/ApiErrorException.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Exception;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\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);
|
||||
}
|
||||
}
|
||||
9
src/Exception/ArcadiaGenesisApiException.php
Normal file
9
src/Exception/ArcadiaGenesisApiException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Exception;
|
||||
|
||||
interface ArcadiaGenesisApiException extends \Stringable
|
||||
{
|
||||
}
|
||||
9
src/Exception/DomainException.php
Normal file
9
src/Exception/DomainException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Exception;
|
||||
|
||||
class DomainException extends \DomainException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
9
src/Exception/InvalidApiResponse.php
Normal file
9
src/Exception/InvalidApiResponse.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Exception;
|
||||
|
||||
class InvalidApiResponse extends \DomainException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
9
src/Exception/UnexpectedErrorException.php
Normal file
9
src/Exception/UnexpectedErrorException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Exception;
|
||||
|
||||
class UnexpectedErrorException extends \ErrorException implements ArcadiaGenesisApiException
|
||||
{
|
||||
}
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Factory;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Microsoft\Kiota\Bundle\DefaultRequestAdapter;
|
||||
use Microsoft\Kiota\Http\KiotaClientFactory;
|
||||
use Microsoft\Kiota\Http\Middleware\KiotaMiddleware;
|
||||
use Microsoft\Kiota\Http\Middleware\Options\RetryOption;
|
||||
use Microsoft\Kiota\Http\Middleware\Options\UserAgentHandlerOption;
|
||||
use Microsoft\Kiota\Http\Middleware\RetryHandler;
|
||||
use Microsoft\Kiota\Http\Middleware\UserAgentHandler;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ApiMandatoryHeadersMiddleware;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\RequestSignatureCalculator;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\LibProperties;
|
||||
use Uwil\ArcadiaApiPhpClient\Logger\ApiLoggerMiddleware;
|
||||
use Uwil\ArcadiaApiPhpClient\Observability\ObservabilityOption;
|
||||
use Uwil\ArcadiaKiotaOpenApiClient\ApiClient;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ApiClientFactory
|
||||
{
|
||||
public function __construct(
|
||||
private ApiConfig $apiConfig
|
||||
) {
|
||||
}
|
||||
|
||||
public function createClient(): ApiClient
|
||||
{
|
||||
$signatureCalculator = new RequestSignatureCalculator();
|
||||
$libProperties = new LibProperties();
|
||||
$uaOptions = new UserAgentHandlerOption();
|
||||
$uaOptions->setProductName($libProperties->productName);
|
||||
$uaOptions->setProductVersion($libProperties->productVersion);
|
||||
|
||||
$handlerStack = KiotaClientFactory::getDefaultHandlerStack();
|
||||
|
||||
if ($this->apiConfig->logger !== null) {
|
||||
$handlerStack->push(
|
||||
ApiLoggerMiddleware::log($this->apiConfig->logger),
|
||||
ApiLoggerMiddleware::HANDLER_NAME
|
||||
);
|
||||
}
|
||||
|
||||
$handlerStack->push(
|
||||
ApiMandatoryHeadersMiddleware::signRequest(
|
||||
apiKey: $this->apiConfig->apiKey,
|
||||
withSecret: $this->apiConfig->secret,
|
||||
signatureCalculator: $signatureCalculator
|
||||
),
|
||||
ApiMandatoryHeadersMiddleware::HANDLER_NAME
|
||||
);
|
||||
|
||||
$handlerStack->push(
|
||||
KiotaMiddleware::userAgent($uaOptions),
|
||||
UserAgentHandler::HANDLER_NAME
|
||||
);
|
||||
|
||||
$handlerStack->push(
|
||||
KiotaMiddleware::retry(
|
||||
new RetryOption(
|
||||
maxRetries: $this->apiConfig->maxRetries,
|
||||
delaySeconds: $this->apiConfig->retryDelaySeconds
|
||||
)
|
||||
),
|
||||
RetryHandler::HANDLER_NAME
|
||||
);
|
||||
|
||||
$guzzleClient = new Client([
|
||||
'base_uri' => $this->apiConfig->endpoint,
|
||||
'timeout' => $this->apiConfig->curlTimeOut,
|
||||
'connect_timeout' => min(5.0, $this->apiConfig->curlTimeOut),
|
||||
'handler' => $handlerStack,
|
||||
'verify' => ! $this->apiConfig->ignoreSelfSignedCert,
|
||||
]);
|
||||
|
||||
$observabilityOption = new ObservabilityOption(
|
||||
$libProperties,
|
||||
$this->apiConfig->tracer
|
||||
);
|
||||
|
||||
$adapter = DefaultRequestAdapter::builder()
|
||||
->withGuzzleClient($guzzleClient)
|
||||
->withObservabilityOption($observabilityOption)
|
||||
->build();
|
||||
|
||||
$adapter->setBaseUrl($this->apiConfig->endpoint->__toString());
|
||||
|
||||
return new ApiClient($adapter);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Factory;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiConfig;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\BrandMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\EmployeeMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoCompoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Mapper\IncoShortMapper;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\BrandApiRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\EmployeeApiRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoApiRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoCompoApiRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Repository\IncoShortApiRepository;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\ApiService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\ArcadiaApiService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\ArcadiaBrandService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\ArcadiaEmployeeService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\ArcadiaIncoService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\BrandService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\EmployeeService;
|
||||
use Uwil\ArcadiaApiPhpClient\Service\IncoService;
|
||||
use Uwil\ArcadiaKiotaOpenApiClient\ApiClient;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class ServiceFactory
|
||||
{
|
||||
private ?ApiService $apiService = null;
|
||||
|
||||
private ?EmployeeService $employeeService = null;
|
||||
|
||||
private ?IncoService $incoService = null;
|
||||
|
||||
private ?BrandService $brandService = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly ApiClient $apiClient,
|
||||
private readonly ApiConfig $apiConfig
|
||||
) {
|
||||
}
|
||||
|
||||
public function createApiService(): ApiService
|
||||
{
|
||||
return $this->apiService ??= new ArcadiaApiService(
|
||||
$this->apiConfig
|
||||
);
|
||||
}
|
||||
|
||||
public function createEmployeeService(): EmployeeService
|
||||
{
|
||||
return $this->employeeService ??= new ArcadiaEmployeeService(
|
||||
new EmployeeApiRepository(
|
||||
$this->apiClient,
|
||||
new EmployeeMapper()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function createIncoService(): IncoService
|
||||
{
|
||||
return $this->incoService ??= new ArcadiaIncoService(
|
||||
new IncoApiRepository(
|
||||
$this->apiClient,
|
||||
new IncoMapper()
|
||||
),
|
||||
new IncoShortApiRepository(
|
||||
$this->apiClient,
|
||||
new IncoShortMapper()
|
||||
),
|
||||
new IncoCompoApiRepository(
|
||||
$this->apiClient,
|
||||
new IncoCompoMapper()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function createBrandService(): BrandService
|
||||
{
|
||||
return $this->brandService ??= new ArcadiaBrandService(
|
||||
new BrandApiRepository(
|
||||
$this->apiClient,
|
||||
new BrandMapper()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function employeeService(): EmployeeService
|
||||
{
|
||||
return $this->createEmployeeService();
|
||||
}
|
||||
|
||||
public function incoService(): IncoService
|
||||
{
|
||||
return $this->createIncoService();
|
||||
}
|
||||
|
||||
public function brandService(): BrandService
|
||||
{
|
||||
return $this->createBrandService();
|
||||
}
|
||||
}
|
||||
208
src/Lib/ApiCaller.php
Normal file
208
src/Lib/ApiCaller.php
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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\ArcadiaGenesisPhpClient\Enum\ArcadiaGenesisApiErrorCode;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\ApiCallFailed;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\ApiErrorException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder\TableQuery;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Psr7\RequestFactory;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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\ArcadiaGenesisPhpClient\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\ArcadiaGenesisPhpClient\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\ArcadiaGenesisPhpClient\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);
|
||||
}
|
||||
}
|
||||
|
|
@ -2,41 +2,28 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib;
|
||||
|
||||
use OpenTelemetry\API\Trace\TracerInterface;
|
||||
use Psr\Http\Message\UriInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ClientIdentifier;
|
||||
use Uwil\ArcadiaApiPhpClient\Auth\ClientSecret;
|
||||
|
||||
final readonly class ApiConfig
|
||||
readonly class ApiConfig
|
||||
{
|
||||
public function __construct(
|
||||
#[\SensitiveParameter]
|
||||
public UriInterface $endpoint,
|
||||
public readonly UriInterface $endpoint,
|
||||
#[\SensitiveParameter]
|
||||
public ClientIdentifier $apiKey,
|
||||
public readonly string $apiKey,
|
||||
#[\SensitiveParameter]
|
||||
public ClientSecret $secret,
|
||||
public float $curlTimeOut = 20.0,
|
||||
public int $maxRetries = 0,
|
||||
public int $retryDelaySeconds = 1,
|
||||
public bool $ignoreSelfSignedCert = false,
|
||||
public ?LoggerInterface $logger = null,
|
||||
public ?TracerInterface $tracer = null,
|
||||
public readonly string $secret,
|
||||
public readonly float $curlTimeOut = 20.0,
|
||||
public readonly int $maxRetries = 0,
|
||||
public readonly int $retryDelayMs = 500,
|
||||
) {
|
||||
if (! in_array(strtolower($endpoint->getScheme()), ['http', 'https'])) {
|
||||
throw new \InvalidArgumentException('endpoint must have http or https scheme');
|
||||
}
|
||||
if ($curlTimeOut <= 0) {
|
||||
throw new \InvalidArgumentException('curlTimeOut must be positive');
|
||||
}
|
||||
if ($maxRetries < 0) {
|
||||
throw new \InvalidArgumentException('maxRetries must be non-negative');
|
||||
}
|
||||
if ($retryDelaySeconds < 0) {
|
||||
throw new \InvalidArgumentException('retryDelaySeconds must be non-negative');
|
||||
if ($retryDelayMs < 0) {
|
||||
throw new \InvalidArgumentException('retryDelayMs must be non-negative');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto;
|
||||
|
||||
interface Dto extends \JsonSerializable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,21 +2,20 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
|
||||
final readonly class Brand implements Dto
|
||||
{
|
||||
public function __construct(
|
||||
public BrandCode $code,
|
||||
public string $label,
|
||||
public ?string $arcadiaId = null,
|
||||
public ?\DateTimeInterface $createdAt = null,
|
||||
public readonly BrandCode $code,
|
||||
public readonly string $label
|
||||
) {
|
||||
if (empty(mb_trim($label))) {
|
||||
throw new \DomainException(
|
||||
if (empty(trim($label))) {
|
||||
throw new DomainException(
|
||||
"Invalid label '{$label}' should not be empty"
|
||||
);
|
||||
}
|
||||
|
|
@ -25,10 +24,8 @@ final readonly class Brand implements Dto
|
|||
public function jsonSerialize(): mixed
|
||||
{
|
||||
return [
|
||||
'code' => $this->code->value,
|
||||
'label' => $this->label,
|
||||
'arcadiaId' => $this->arcadiaId,
|
||||
'createdAt' => $this->createdAt?->format(\DateTimeInterface::ATOM),
|
||||
'code' => $this->code->value,
|
||||
'label' => $this->label,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ArcadiaEmployeeId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\EstablishmentCode;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\ServiceId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\SiteId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\ArcadiaEmployeeId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\EstablishmentCode;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\ServiceId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\SiteId;
|
||||
|
||||
final readonly class Employee implements Dto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,19 +2,20 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
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\StorageCondition;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Additive;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Allegations;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Allergen;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\IncoWeight;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Ingredients;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Nutriscore;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\PortionAmount;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\StorageCondition;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\IncoId;
|
||||
|
||||
/**
|
||||
* Represents an Inco sheet (ingredients / recipe).
|
||||
|
|
@ -45,17 +46,17 @@ final readonly class Inco implements Dto
|
|||
public ?StorageCondition $storageCondition,
|
||||
public array $additives,
|
||||
public ?IncoCompoId $compo,
|
||||
public ?\DateTimeInterface $createdAt,
|
||||
public ?\DateTimeInterface $updatedAt,
|
||||
public ?\DateTimeInterface $created_at,
|
||||
public ?\DateTimeInterface $updated_at,
|
||||
) {
|
||||
foreach ($this->allergens as $allergen) {
|
||||
if (! is_object($allergen) || ! ($allergen instanceof Allergen)) {
|
||||
throw new \InvalidArgumentException('Allergens shall be instance of ' . Allergen::class);
|
||||
throw new UnexpectedErrorException('Allergens shall be instance of ' . Allergen::class);
|
||||
}
|
||||
}
|
||||
foreach ($this->additives as $additive) {
|
||||
if (! is_object($additive) || ! ($additive instanceof Additive)) {
|
||||
throw new \InvalidArgumentException('Additives shall be instance of ' . Additive::class);
|
||||
throw new UnexpectedErrorException('Additives shall be instance of ' . Additive::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -85,8 +86,8 @@ final readonly class Inco implements Dto
|
|||
'storageCondition' => $this->storageCondition,
|
||||
'additives' => $this->additives,
|
||||
'compo' => $this->compo?->value,
|
||||
'created_at' => $this->createdAt,
|
||||
'updated_at' => $this->updatedAt,
|
||||
'created_at' => $this->created_at,
|
||||
'updated_at' => $this->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* Represents a food additive identified by its E-number code.
|
||||
|
|
@ -24,16 +25,16 @@ final readonly class Additive implements Dto
|
|||
public readonly string $label,
|
||||
) {
|
||||
$this->code = $code;
|
||||
$trimmedLabel = mb_trim($label);
|
||||
$trimmedLabel = trim($label);
|
||||
|
||||
if (! preg_match('/^E\d+(?:[a-z]+(?:\([ivx]+\))?)?$/', $this->code)) {
|
||||
throw new \DomainException(
|
||||
throw new DomainException(
|
||||
"Invalid additive code '{$code}': must match E-number format (e.g. E300, E471a, E160ai, E160a(ii), E1404)"
|
||||
);
|
||||
}
|
||||
|
||||
if ($trimmedLabel === '') {
|
||||
throw new \DomainException(
|
||||
throw new DomainException(
|
||||
"Invalid label for additive '{$code}': must not be empty"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
final readonly class Allegations implements Dto
|
||||
{
|
||||
|
|
@ -35,10 +35,10 @@ final readonly class Allegations implements Dto
|
|||
array $nutrition = [],
|
||||
array $other = []
|
||||
) {
|
||||
$this->allegations = \array_filter($allegations, fn (string $el) => ! empty($el));
|
||||
$this->health = \array_filter($health, fn (string $el) => ! empty($el));
|
||||
$this->nutrition = \array_filter($nutrition, fn (string $el) => ! empty($el));
|
||||
$this->other = \array_filter($other, fn (string $el) => ! empty($el));
|
||||
$this->allegations = \array_filter($allegations, fn ($el) => ! empty($el));
|
||||
$this->health = \array_filter($health, fn ($el) => ! empty($el));
|
||||
$this->nutrition = \array_filter($nutrition, fn ($el) => ! empty($el));
|
||||
$this->other = \array_filter($other, fn ($el) => ! empty($el));
|
||||
}
|
||||
|
||||
// * Methods
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
final readonly class Allergen implements Dto
|
||||
{
|
||||
|
|
@ -13,13 +14,13 @@ final readonly class Allergen implements Dto
|
|||
public readonly string $label
|
||||
) {
|
||||
if ($code <= 0) {
|
||||
throw new \DomainException(
|
||||
throw new DomainException(
|
||||
"Invalid code '{$code}' shall be greater than 0"
|
||||
);
|
||||
}
|
||||
|
||||
if (empty(mb_trim($label))) {
|
||||
throw new \DomainException(
|
||||
if (empty(trim($label))) {
|
||||
throw new DomainException(
|
||||
"Invalid label '{$label}' should not be empty"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
final readonly class IncoWeight implements Dto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* Represents a structured and parsed ingredient list.
|
||||
|
|
@ -26,20 +27,20 @@ final readonly class Ingredients implements Dto
|
|||
) {
|
||||
foreach ($groups as $key => $lines) {
|
||||
if (! is_array($lines)) {
|
||||
throw new \DomainException(
|
||||
throw new DomainException(
|
||||
"Invalid ingredient group at key '{$key}': value must be an array of strings"
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($lines as $i => $line) {
|
||||
if (! is_string($line)) {
|
||||
throw new \DomainException(
|
||||
throw new DomainException(
|
||||
"Invalid ingredient at group '{$key}', index '{$i}': value must be a string"
|
||||
);
|
||||
}
|
||||
|
||||
if (empty(mb_trim($line))) {
|
||||
throw new \DomainException(
|
||||
if (empty(trim($line))) {
|
||||
throw new DomainException(
|
||||
"Invalid ingredient at group '{$key}', index '{$i}': string should not be empty"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* Represents the Nutri-Score grade of a product as displayed on pack.
|
||||
|
|
@ -19,7 +20,7 @@ final readonly class Nutriscore implements Dto
|
|||
public readonly string $grade,
|
||||
) {
|
||||
if (! in_array($this->grade, self::VALID_GRADES, true)) {
|
||||
throw new \DomainException(
|
||||
throw new DomainException(
|
||||
sprintf(
|
||||
"Invalid Nutri-Score grade '%s': must be one of %s",
|
||||
$grade,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,16 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* Si le type de portion est exact, seul la partie basse est utilisée,
|
||||
* Si le type de portion est environ, la partie basse seule ou les deux peuvent
|
||||
* etre utilisés.
|
||||
*/
|
||||
final readonly class PortionAmount implements \Stringable, Dto
|
||||
final readonly class PortionAmount implements \JsonSerializable, \Stringable, Dto
|
||||
{
|
||||
/**
|
||||
* Valeur minimale, ou exacte
|
||||
|
|
@ -197,6 +197,6 @@ final readonly class PortionAmount implements \Stringable, Dto
|
|||
$decimals = self::ALLOWED_DECIMAL_SIZE;
|
||||
$format = number_format($value, $decimals, '.', '');
|
||||
|
||||
return mb_rtrim(mb_rtrim($format, '0'), '.');
|
||||
return rtrim(rtrim($format, '0'), '.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
enum PortionType: int implements Dto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* Represents the storage/conservation conditions of a product.
|
||||
|
|
@ -16,8 +17,8 @@ final readonly class StorageCondition implements Dto
|
|||
public function __construct(
|
||||
public readonly string $instructions,
|
||||
) {
|
||||
if (empty(mb_trim($this->instructions))) {
|
||||
throw new \DomainException(
|
||||
if (empty(trim($this->instructions))) {
|
||||
throw new DomainException(
|
||||
'Invalid instructions: must not be empty'
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,25 +2,25 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
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;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\AcideGras;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\AcidesAmines;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\ApportsReference;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\ComposesSpeciaux;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Energie;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\FruitLegumeNoix;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\GlucidesDetail;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Identification;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\MacronutrimentsPortion;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\MacronutrimentsRef;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Mineraux;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\NutriScore;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\PointsNutriScore;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Vitamines;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\IncoId;
|
||||
|
||||
/**
|
||||
* Agrégat racine regroupant toutes les sous-sections nutritionnelles d'une fiche INCO.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
/**
|
||||
* Base implementation for NutriDto.
|
||||
|
|
@ -36,13 +36,13 @@ abstract readonly class AbstractIncoCompoPartDto implements IncoCompoPartDto
|
|||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
/** @var array<string, float|string|null> $result */
|
||||
// Reflection on public properties of the concrete child class.
|
||||
$ref = new \ReflectionObject($this);
|
||||
$result = [];
|
||||
|
||||
foreach (get_object_vars($this) as $name => $value) {
|
||||
if (\is_float($value) || \is_string($value) || $value === null) {
|
||||
$result[(string) $name] = $value;
|
||||
}
|
||||
foreach ($ref->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
|
||||
$name = $property->getName();
|
||||
$result[$name] = $property->getValue($this);
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class AcideGras extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class AcidesAmines extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class ApportsReference extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class ComposesSpeciaux extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class Energie extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class FruitLegumeNoix extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class GlucidesDetail extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class Identification extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* Interface for nutritional DTOs that can provide units, human-readable labels,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class MacronutrimentsPortion extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class MacronutrimentsRef extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class Mineraux extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class NutriScore extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class PointsNutriScore extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo;
|
||||
|
||||
final readonly class Vitamines extends AbstractIncoCompoPartDto
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiValueObject\IncoId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\IncoId;
|
||||
|
||||
/**
|
||||
* Represents an Inco sheet resumed info
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
/**
|
||||
* A page of items with pagination metadata.
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
use Uwil\ArcadiaApiPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
|
||||
final readonly class PaginationDto implements Dto
|
||||
{
|
||||
|
|
|
|||
55
src/Lib/ApiDto/Factory/AbstractDtoFactory.php
Normal file
55
src/Lib/ApiDto/Factory/AbstractDtoFactory.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\PaginationDto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\Mapper;
|
||||
|
||||
/**
|
||||
* @implements DtoFactory<Mapper, \Uwil\ArcadiaGenesisPhpClient\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
|
||||
);
|
||||
}
|
||||
}
|
||||
35
src/Lib/ApiDto/Factory/DtoFactory.php
Normal file
35
src/Lib/ApiDto/Factory/DtoFactory.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\Mapper;
|
||||
|
||||
/**
|
||||
* @template M of \Uwil\ArcadiaGenesisPhpClient\Mapper\Mapper
|
||||
* @template D of \Uwil\ArcadiaGenesisPhpClient\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;
|
||||
}
|
||||
59
src/Lib/ApiDto/Factory/EmployeeDtoFactory.php
Normal file
59
src/Lib/ApiDto/Factory/EmployeeDtoFactory.php
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Employee;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\EmployeeMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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))
|
||||
);
|
||||
}
|
||||
}
|
||||
54
src/Lib/ApiDto/Factory/IncoCompoDtoFactory.php
Normal file
54
src/Lib/ApiDto/Factory/IncoCompoDtoFactory.php
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\IncoCompoMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo|PageOf<\Uwil\ArcadiaGenesisPhpClient\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');
|
||||
}
|
||||
}
|
||||
56
src/Lib/ApiDto/Factory/IncoDtoFactory.php
Normal file
56
src/Lib/ApiDto/Factory/IncoDtoFactory.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\IncoMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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))
|
||||
);
|
||||
}
|
||||
}
|
||||
56
src/Lib/ApiDto/Factory/IncoShortDtoFactory.php
Normal file
56
src/Lib/ApiDto/Factory/IncoShortDtoFactory.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Factory;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoShort;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\PageOf;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse\ApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Mapper\IncoShortMapper;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoShort|PageOf<\Uwil\ArcadiaGenesisPhpClient\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))
|
||||
);
|
||||
}
|
||||
}
|
||||
987
src/Lib/ApiParser/IncoApiParser.php
Normal file
987
src/Lib/ApiParser/IncoApiParser.php
Normal file
|
|
@ -0,0 +1,987 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiParser;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Brand;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Additive;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Allegations;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Allergen;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\IncoWeight;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Ingredients;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\Nutriscore;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\PortionAmount;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\PortionType;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\Inco\StorageCondition;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\BrandCode;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject\IncoCompoId;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Utils\DateUtils;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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)));
|
||||
}
|
||||
}
|
||||
217
src/Lib/ApiParser/IncoApiStringParser.php
Normal file
217
src/Lib/ApiParser/IncoApiStringParser.php
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiParser;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\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;
|
||||
}
|
||||
}
|
||||
360
src/Lib/ApiParser/IncoCompoApiParser.php
Normal file
360
src/Lib/ApiParser/IncoCompoApiParser.php
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiParser;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\UnexpectedErrorException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\AcideGras;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\AcidesAmines;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\ApportsReference;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\ComposesSpeciaux;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Energie;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\FruitLegumeNoix;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\GlucidesDetail;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Identification;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\MacronutrimentsPortion;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\MacronutrimentsRef;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\Mineraux;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\NutriScore;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiDto\Dto\IncoCompo\PointsNutriScore;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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'),
|
||||
);
|
||||
}
|
||||
}
|
||||
11
src/Lib/ApiQueryBuilder/Direction.php
Normal file
11
src/Lib/ApiQueryBuilder/Direction.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
enum Direction: string
|
||||
{
|
||||
case ASC = 'asc';
|
||||
case DESC = 'desc';
|
||||
}
|
||||
21
src/Lib/ApiQueryBuilder/Operator.php
Normal file
21
src/Lib/ApiQueryBuilder/Operator.php
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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';
|
||||
}
|
||||
102
src/Lib/ApiQueryBuilder/TableQuery.php
Normal file
102
src/Lib/ApiQueryBuilder/TableQuery.php
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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);
|
||||
}
|
||||
}
|
||||
12
src/Lib/ApiQueryBuilder/TableQueryBuildStep.php
Normal file
12
src/Lib/ApiQueryBuilder/TableQueryBuildStep.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQueryBuildStep
|
||||
{
|
||||
public function build(): TableQuery;
|
||||
|
||||
public function toQueryString(): string;
|
||||
}
|
||||
211
src/Lib/ApiQueryBuilder/TableQueryBuilder.php
Normal file
211
src/Lib/ApiQueryBuilder/TableQueryBuilder.php
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\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();
|
||||
}
|
||||
}
|
||||
10
src/Lib/ApiQueryBuilder/TableQueryFromStep.php
Normal file
10
src/Lib/ApiQueryBuilder/TableQueryFromStep.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
interface TableQueryFromStep
|
||||
{
|
||||
public function from(string $table): TableQuerySelectStep;
|
||||
}
|
||||
14
src/Lib/ApiQueryBuilder/TableQueryOrderByStep.php
Normal file
14
src/Lib/ApiQueryBuilder/TableQueryOrderByStep.php
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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;
|
||||
}
|
||||
12
src/Lib/ApiQueryBuilder/TableQueryPaginateStep.php
Normal file
12
src/Lib/ApiQueryBuilder/TableQueryPaginateStep.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\Pagination;
|
||||
|
||||
interface TableQueryPaginateStep extends TableQueryBuildStep
|
||||
{
|
||||
public function paginate(Pagination $pagination): TableQueryBuildStep;
|
||||
}
|
||||
28
src/Lib/ApiQueryBuilder/TableQuerySelectStep.php
Normal file
28
src/Lib/ApiQueryBuilder/TableQuerySelectStep.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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;
|
||||
}
|
||||
34
src/Lib/ApiQueryBuilder/TableQueryWhereStep.php
Normal file
34
src/Lib/ApiQueryBuilder/TableQueryWhereStep.php
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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;
|
||||
}
|
||||
17
src/Lib/ApiResponse/ApiPagination.php
Normal file
17
src/Lib/ApiResponse/ApiPagination.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse;
|
||||
|
||||
final readonly class ApiPagination
|
||||
{
|
||||
public function __construct(
|
||||
public int $page,
|
||||
public int $pageSize,
|
||||
public int $totalItems,
|
||||
public int $totalPages,
|
||||
public ApiPaginationLinks $links,
|
||||
) {
|
||||
}
|
||||
}
|
||||
40
src/Lib/ApiResponse/ApiPaginationLinks.php
Normal file
40
src/Lib/ApiResponse/ApiPaginationLinks.php
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\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);
|
||||
}
|
||||
}
|
||||
184
src/Lib/ApiResponse/ApiResponse.php
Normal file
184
src/Lib/ApiResponse/ApiResponse.php
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiResponse;
|
||||
|
||||
use UnexpectedValueException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\InvalidApiResponse;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Utils\StringEncoder;
|
||||
use Uwil\ArcadiaGenesisPhpClient\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);
|
||||
}
|
||||
}
|
||||
20
src/Lib/ApiSorter/AbstractSorter.php
Normal file
20
src/Lib/ApiSorter/AbstractSorter.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Exception\DomainException;
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
|
||||
final readonly class AbstractSorter
|
||||
{
|
||||
public function __construct(
|
||||
public string $sortBy,
|
||||
public Direction $direction
|
||||
) {
|
||||
if (empty(trim($sortBy))) {
|
||||
throw new DomainException('Sort by cannot be empty');
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/Lib/ApiSorter/EmployeSorter.php
Normal file
16
src/Lib/ApiSorter/EmployeSorter.php
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject;
|
||||
|
||||
use Uwil\ArcadiaGenesisPhpClient\Lib\ApiQueryBuilder\Direction;
|
||||
|
||||
final readonly class EmployeeSorter
|
||||
{
|
||||
public function __construct(
|
||||
public EmployeeSortColum $sortBy,
|
||||
public Direction $direction
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Uwil\ArcadiaApiPhpClient\Lib\ApiSorter;
|
||||
namespace Uwil\ArcadiaGenesisPhpClient\Lib\ApiValueObject;
|
||||
|
||||
enum EmployeeSortColumn implements SortColumn
|
||||
enum EmployeeSortColum implements SortColumn
|
||||
{
|
||||
case arcadiaId;
|
||||
case startDate;
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue