[#ED-1] Ajout des API de lecture bovin (#2)
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [x] TU/TI/TF rédigée - [x] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #2 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
This commit was merged in pull request #2.
This commit is contained in:
@@ -4,10 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Api;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Malio\EdnotifBundle\Auth\TokenProvider;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\AnimalFileDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\InventoryDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\PresumedExitsDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ReturnedDossiersDto;
|
||||
use Malio\EdnotifBundle\Bovin\Mapper\AnimalFileMapper;
|
||||
use Malio\EdnotifBundle\Bovin\Mapper\InventoryMapper;
|
||||
use Malio\EdnotifBundle\Bovin\Mapper\PresumedExitsMapper;
|
||||
use Malio\EdnotifBundle\Bovin\Mapper\ReturnedDossiersMapper;
|
||||
use Malio\EdnotifBundle\Shared\Exception\EdnotifException;
|
||||
use Malio\EdnotifBundle\Shared\Soap\ZipMessageDecoder;
|
||||
use RuntimeException;
|
||||
use SoapClient;
|
||||
use SoapFault;
|
||||
@@ -18,6 +26,10 @@ final readonly class BovinApi implements BovinApiInterface
|
||||
private TokenProvider $tokenProvider,
|
||||
private SoapClient $businessClient,
|
||||
private AnimalFileMapper $bovinDossierMapper,
|
||||
private InventoryMapper $inventoryMapper,
|
||||
private ReturnedDossiersMapper $returnedDossiersMapper,
|
||||
private PresumedExitsMapper $presumedExitsMapper,
|
||||
private ZipMessageDecoder $zipMessageDecoder,
|
||||
private string $exploitationCountryCode,
|
||||
private string $exploitationNumber,
|
||||
) {}
|
||||
@@ -45,20 +57,122 @@ final readonly class BovinApi implements BovinApiInterface
|
||||
throw new RuntimeException('SOAP Fault on IpBGetDossierAnimal: '.$soapFault->getMessage(), 0, $soapFault);
|
||||
}
|
||||
|
||||
// Throw uniquement si Resultat=false (erreur métier)
|
||||
$standardResponseNode = $soapResponse->ReponseStandard ?? null;
|
||||
$isOk = is_object($standardResponseNode) && (($standardResponseNode->Resultat ?? false) === true);
|
||||
|
||||
if (!$isOk) {
|
||||
$anomalyNode = is_object($standardResponseNode) ? ($standardResponseNode->Anomalie ?? null) : null;
|
||||
|
||||
throw new EdnotifException(
|
||||
codeAnomalie: (string) ($anomalyNode->Code ?? 'UNKNOWN'),
|
||||
severite: (int) ($anomalyNode->Severite ?? 1),
|
||||
message: (string) ($anomalyNode->Message ?? 'EDNOTIF error')
|
||||
);
|
||||
}
|
||||
$this->assertSuccessfulResponse($soapResponse, 'IpBGetDossierAnimal');
|
||||
|
||||
return $this->bovinDossierMapper->map($soapResponse);
|
||||
}
|
||||
|
||||
public function getInventory(
|
||||
DateTimeInterface $startDate,
|
||||
?DateTimeInterface $endDate = null,
|
||||
bool $includeEarTagStock = false,
|
||||
): InventoryDto {
|
||||
$token = $this->tokenProvider->getToken();
|
||||
|
||||
$payload = [
|
||||
'JetonAuthentification' => $token,
|
||||
'Exploitation' => [
|
||||
'CodePays' => $this->exploitationCountryCode,
|
||||
'NumeroExploitation' => $this->exploitationNumber,
|
||||
],
|
||||
'DateDebut' => $startDate->format('Y-m-d'),
|
||||
'StockBoucles' => $includeEarTagStock,
|
||||
];
|
||||
if (null !== $endDate) {
|
||||
$payload['DateFin'] = $endDate->format('Y-m-d');
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var object $soapResponse */
|
||||
$soapResponse = $this->businessClient->__soapCall('IpBGetInventaire', [$payload]);
|
||||
} catch (SoapFault $soapFault) {
|
||||
throw new RuntimeException('SOAP Fault on IpBGetInventaire: '.$soapFault->getMessage(), 0, $soapFault);
|
||||
}
|
||||
|
||||
$this->assertSuccessfulResponse($soapResponse, 'IpBGetInventaire');
|
||||
|
||||
$messageZip = $soapResponse->ReponseSpecifique->MessageZip ?? null;
|
||||
$unzippedMessage = is_string($messageZip) && '' !== $messageZip
|
||||
? $this->zipMessageDecoder->decode($messageZip)
|
||||
: null;
|
||||
|
||||
return $this->inventoryMapper->map($soapResponse, $unzippedMessage);
|
||||
}
|
||||
|
||||
public function getReturnedDossiers(DateTimeInterface $startDate): ReturnedDossiersDto
|
||||
{
|
||||
$token = $this->tokenProvider->getToken();
|
||||
|
||||
$payload = [
|
||||
'JetonAuthentification' => $token,
|
||||
'Exploitation' => [
|
||||
'CodePays' => $this->exploitationCountryCode,
|
||||
'NumeroExploitation' => $this->exploitationNumber,
|
||||
],
|
||||
'DateDebut' => $startDate->format('Y-m-d'),
|
||||
];
|
||||
|
||||
try {
|
||||
/** @var object $soapResponse */
|
||||
$soapResponse = $this->businessClient->__soapCall('IpBGetRetourDossiers', [$payload]);
|
||||
} catch (SoapFault $soapFault) {
|
||||
throw new RuntimeException('SOAP Fault on IpBGetRetourDossiers: '.$soapFault->getMessage(), 0, $soapFault);
|
||||
}
|
||||
|
||||
$this->assertSuccessfulResponse($soapResponse, 'IpBGetRetourDossiers');
|
||||
|
||||
$messageZip = $soapResponse->ReponseSpecifique->MessageZip ?? null;
|
||||
$unzippedMessage = is_string($messageZip) && '' !== $messageZip
|
||||
? $this->zipMessageDecoder->decode($messageZip)
|
||||
: null;
|
||||
|
||||
return $this->returnedDossiersMapper->map($soapResponse, $unzippedMessage);
|
||||
}
|
||||
|
||||
public function getPresumedExits(): PresumedExitsDto
|
||||
{
|
||||
$token = $this->tokenProvider->getToken();
|
||||
|
||||
$payload = [
|
||||
'JetonAuthentification' => $token,
|
||||
'Exploitation' => [
|
||||
'CodePays' => $this->exploitationCountryCode,
|
||||
'NumeroExploitation' => $this->exploitationNumber,
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
/** @var object $soapResponse */
|
||||
$soapResponse = $this->businessClient->__soapCall('IpBGetSortiesPresumees', [$payload]);
|
||||
} catch (SoapFault $soapFault) {
|
||||
throw new RuntimeException('SOAP Fault on IpBGetSortiesPresumees: '.$soapFault->getMessage(), 0, $soapFault);
|
||||
}
|
||||
|
||||
$this->assertSuccessfulResponse($soapResponse, 'IpBGetSortiesPresumees');
|
||||
|
||||
$messageZip = $soapResponse->ReponseSpecifique->MessageZip ?? null;
|
||||
$unzippedMessage = is_string($messageZip) && '' !== $messageZip
|
||||
? $this->zipMessageDecoder->decode($messageZip)
|
||||
: null;
|
||||
|
||||
return $this->presumedExitsMapper->map($soapResponse, $unzippedMessage);
|
||||
}
|
||||
|
||||
private function assertSuccessfulResponse(object $soapResponse, string $operation): void
|
||||
{
|
||||
$standardResponseNode = $soapResponse->ReponseStandard ?? null;
|
||||
$isOk = is_object($standardResponseNode) && (($standardResponseNode->Resultat ?? false) === true);
|
||||
|
||||
if ($isOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
$anomalyNode = is_object($standardResponseNode) ? ($standardResponseNode->Anomalie ?? null) : null;
|
||||
|
||||
throw new EdnotifException(
|
||||
codeAnomalie: (string) ($anomalyNode->Code ?? 'UNKNOWN'),
|
||||
severite: (int) ($anomalyNode->Severite ?? 1),
|
||||
message: (string) ($anomalyNode->Message ?? $operation.' : EDNOTIF error')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,23 @@ declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Api;
|
||||
|
||||
use DateTimeInterface;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\AnimalFileDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\InventoryDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\PresumedExitsDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ReturnedDossiersDto;
|
||||
|
||||
interface BovinApiInterface
|
||||
{
|
||||
public function getAnimalFile(string $nationalNumber, string $countryCode = 'FR'): AnimalFileDto;
|
||||
|
||||
public function getInventory(
|
||||
DateTimeInterface $startDate,
|
||||
?DateTimeInterface $endDate = null,
|
||||
bool $includeEarTagStock = false,
|
||||
): InventoryDto;
|
||||
|
||||
public function getReturnedDossiers(DateTimeInterface $startDate): ReturnedDossiersDto;
|
||||
|
||||
public function getPresumedExits(): PresumedExitsDto;
|
||||
}
|
||||
|
||||
16
src/Bovin/Dto/AnimalSummaryDto.php
Normal file
16
src/Bovin/Dto/AnimalSummaryDto.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Dto;
|
||||
|
||||
final readonly class AnimalSummaryDto
|
||||
{
|
||||
/**
|
||||
* @param list<PresencePeriodDto> $presencePeriods
|
||||
*/
|
||||
public function __construct(
|
||||
public ?BovinIdentificationDto $identification,
|
||||
public array $presencePeriods,
|
||||
) {}
|
||||
}
|
||||
12
src/Bovin/Dto/EarTagSeriesDto.php
Normal file
12
src/Bovin/Dto/EarTagSeriesDto.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Dto;
|
||||
|
||||
final readonly class EarTagSeriesDto
|
||||
{
|
||||
public function __construct(
|
||||
public object $rawNode,
|
||||
) {}
|
||||
}
|
||||
26
src/Bovin/Dto/InventoryDto.php
Normal file
26
src/Bovin/Dto/InventoryDto.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Dto;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
|
||||
|
||||
final readonly class InventoryDto
|
||||
{
|
||||
/**
|
||||
* @param list<AnimalSummaryDto> $animals
|
||||
* @param list<EarTagSeriesDto> $earTagSeries
|
||||
*/
|
||||
public function __construct(
|
||||
public StandardResponseDto $standardResponse,
|
||||
public int $nbBovins,
|
||||
public ?DateTimeImmutable $startDate,
|
||||
public ?DateTimeImmutable $endDate,
|
||||
public ?bool $includesEarTagStock,
|
||||
public array $animals,
|
||||
public array $earTagSeries,
|
||||
public ?object $rawSoapResponse,
|
||||
) {}
|
||||
}
|
||||
15
src/Bovin/Dto/PresumedExitDto.php
Normal file
15
src/Bovin/Dto/PresumedExitDto.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Dto;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class PresumedExitDto
|
||||
{
|
||||
public function __construct(
|
||||
public ?BovinRef $bovin,
|
||||
public ?DateTimeImmutable $exitDate,
|
||||
) {}
|
||||
}
|
||||
20
src/Bovin/Dto/PresumedExitsDto.php
Normal file
20
src/Bovin/Dto/PresumedExitsDto.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Dto;
|
||||
|
||||
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
|
||||
|
||||
final readonly class PresumedExitsDto
|
||||
{
|
||||
/**
|
||||
* @param list<PresumedExitDto> $presumedExits
|
||||
*/
|
||||
public function __construct(
|
||||
public StandardResponseDto $standardResponse,
|
||||
public int $nbBovins,
|
||||
public array $presumedExits,
|
||||
public ?object $rawSoapResponse,
|
||||
) {}
|
||||
}
|
||||
22
src/Bovin/Dto/ReturnedDossiersDto.php
Normal file
22
src/Bovin/Dto/ReturnedDossiersDto.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Dto;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
|
||||
|
||||
final readonly class ReturnedDossiersDto
|
||||
{
|
||||
/**
|
||||
* @param list<AnimalSummaryDto> $animals
|
||||
*/
|
||||
public function __construct(
|
||||
public StandardResponseDto $standardResponse,
|
||||
public int $nbBovins,
|
||||
public ?DateTimeImmutable $startDate,
|
||||
public array $animals,
|
||||
public ?object $rawSoapResponse,
|
||||
) {}
|
||||
}
|
||||
@@ -4,24 +4,20 @@ declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Mapper;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\AnimalFileDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\BovinIdentificationDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\BovinRef;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\DateValueDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ExploitationRef;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\MovementDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ParentInfoDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\PresencePeriodDto;
|
||||
use Malio\EdnotifBundle\Shared\Dto\AnomalyDto;
|
||||
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
|
||||
use Throwable;
|
||||
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
|
||||
|
||||
final class AnimalFileMapper
|
||||
{
|
||||
use BovinNodeMappingTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly StandardResponseMapper $standardResponseMapper,
|
||||
) {}
|
||||
|
||||
public function map(object $soapResponse): AnimalFileDto
|
||||
{
|
||||
$standardResponse = $this->mapStandardResponse($soapResponse->ReponseStandard ?? null);
|
||||
$standardResponse = $this->standardResponseMapper->map($soapResponse->ReponseStandard ?? null);
|
||||
|
||||
$specificResponseNode = $soapResponse->ReponseSpecifique ?? null;
|
||||
$bovinNode = is_object($specificResponseNode) ? ($specificResponseNode->Bovin ?? null) : null;
|
||||
@@ -51,185 +47,4 @@ final class AnimalFileMapper
|
||||
rawSoapResponse: $soapResponse
|
||||
);
|
||||
}
|
||||
|
||||
private function mapStandardResponse(mixed $standardResponseNode): StandardResponseDto
|
||||
{
|
||||
$result = (bool) ($standardResponseNode->Resultat ?? false);
|
||||
|
||||
$anomalyNode = $standardResponseNode->Anomalie ?? null;
|
||||
$anomaly = null;
|
||||
|
||||
if (is_object($anomalyNode)) {
|
||||
$anomaly = new AnomalyDto(
|
||||
code: $this->toNullableString($anomalyNode->Code ?? null),
|
||||
severity: $this->toNullableInt($anomalyNode->Severite ?? null),
|
||||
message: $this->toNullableString($anomalyNode->Message ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
return new StandardResponseDto($result, $anomaly);
|
||||
}
|
||||
|
||||
private function mapIdentification(object $identificationNode): BovinIdentificationDto
|
||||
{
|
||||
$bovinRef = $this->mapBovinRef($identificationNode->Bovin ?? null);
|
||||
|
||||
$birthDate = null;
|
||||
$birthDateNode = $identificationNode->DateNaissance ?? null;
|
||||
if (is_object($birthDateNode)) {
|
||||
$birthDate = new DateValueDto(
|
||||
date: $this->toNullableDate($birthDateNode->Date ?? null),
|
||||
completenessFlag: $this->toNullableString($birthDateNode->TemoinCompletude ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
$motherCarrier = $this->mapParentInfo($identificationNode->MerePorteuse ?? null);
|
||||
$fatherIpg = $this->mapParentInfo($identificationNode->PereIPG ?? null);
|
||||
$birthExploitation = $this->mapExploitationRef($identificationNode->ExploitationNaissance ?? null);
|
||||
|
||||
return new BovinIdentificationDto(
|
||||
bovin: $bovinRef,
|
||||
sex: $this->toNullableString($identificationNode->Sexe ?? null),
|
||||
breedType: $this->toNullableString($identificationNode->TypeRacial ?? null),
|
||||
birthDate: $birthDate,
|
||||
workNumber: $this->toNullableString($identificationNode->NumeroTravail ?? null),
|
||||
isFilie: $this->toNullableBool($identificationNode->StatutFilie ?? null),
|
||||
motherCarrier: $motherCarrier,
|
||||
fatherIpg: $fatherIpg,
|
||||
birthExploitation: $birthExploitation,
|
||||
);
|
||||
}
|
||||
|
||||
private function mapPresencePeriod(object $presencePeriodNode): PresencePeriodDto
|
||||
{
|
||||
$entryNode = $presencePeriodNode->Entree ?? null;
|
||||
$exitNode = $presencePeriodNode->Sortie ?? null;
|
||||
|
||||
$entryMovement = is_object($entryNode) ? $this->mapMovement($entryNode, 'entry') : null;
|
||||
$exitMovement = is_object($exitNode) ? $this->mapMovement($exitNode, 'exit') : null;
|
||||
|
||||
return new PresencePeriodDto(
|
||||
entry: $entryMovement,
|
||||
exit: $exitMovement,
|
||||
);
|
||||
}
|
||||
|
||||
private function mapMovement(object $movementNode, string $direction): MovementDto
|
||||
{
|
||||
$dateValue = null;
|
||||
$causeValue = null;
|
||||
|
||||
if ('entry' === $direction) {
|
||||
// SOAP: DateEntree / CauseEntree
|
||||
$dateValue = $movementNode->DateEntree ?? ($movementNode->Date ?? ($movementNode->DateMouvement ?? null));
|
||||
$causeValue = $movementNode->CauseEntree ?? null;
|
||||
} else {
|
||||
// SOAP (souvent): DateSortie / CauseSortie
|
||||
$dateValue = $movementNode->DateSortie ?? ($movementNode->Date ?? ($movementNode->DateMouvement ?? null));
|
||||
$causeValue = $movementNode->CauseSortie ?? null;
|
||||
}
|
||||
|
||||
$exploitationRef = $this->mapExploitationRef($movementNode->Exploitation ?? null);
|
||||
|
||||
return new MovementDto(
|
||||
date: $this->toNullableDate($dateValue),
|
||||
cause: $this->toNullableString($causeValue),
|
||||
exploitation: $exploitationRef,
|
||||
);
|
||||
}
|
||||
|
||||
private function mapParentInfo(mixed $parentNode): ?ParentInfoDto
|
||||
{
|
||||
if (!is_object($parentNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bovinRef = $this->mapBovinRef($parentNode->Bovin ?? null);
|
||||
|
||||
return new ParentInfoDto(
|
||||
bovin: $bovinRef,
|
||||
breedType: $this->toNullableString($parentNode->TypeRacial ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
private function mapBovinRef(mixed $bovinNode): ?BovinRef
|
||||
{
|
||||
if (!is_object($bovinNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BovinRef(
|
||||
countryCode: $this->toNullableString($bovinNode->CodePays ?? null),
|
||||
nationalNumber: $this->toNullableString($bovinNode->NumeroNational ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
private function mapExploitationRef(mixed $exploitationNode): ?ExploitationRef
|
||||
{
|
||||
if (!is_object($exploitationNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ExploitationRef(
|
||||
countryCode: $this->toNullableString($exploitationNode->CodePays ?? null),
|
||||
exploitationNumber: $this->toNullableString($exploitationNode->NumeroExploitation ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<mixed> */
|
||||
private function normalizeToList(mixed $value): array
|
||||
{
|
||||
if (null === $value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($value) ? $value : [$value];
|
||||
}
|
||||
|
||||
private function toNullableString(mixed $value): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
$stringValue = trim((string) $value);
|
||||
|
||||
return '' === $stringValue ? null : $stringValue;
|
||||
}
|
||||
|
||||
private function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function toNullableBool(mixed $value): ?bool
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
private function toNullableDate(mixed $value): ?DateTimeImmutable
|
||||
{
|
||||
if (!is_string($value) || '' === trim($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new DateTimeImmutable($value);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
32
src/Bovin/Mapper/AnimalSummaryMapper.php
Normal file
32
src/Bovin/Mapper/AnimalSummaryMapper.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Mapper;
|
||||
|
||||
use Malio\EdnotifBundle\Bovin\Dto\AnimalSummaryDto;
|
||||
|
||||
final class AnimalSummaryMapper
|
||||
{
|
||||
use BovinNodeMappingTrait;
|
||||
|
||||
public function map(object $bovinNode): AnimalSummaryDto
|
||||
{
|
||||
$identificationNode = $bovinNode->IdentiteBovin ?? null;
|
||||
$identification = is_object($identificationNode) ? $this->mapIdentification($identificationNode) : null;
|
||||
|
||||
$presencePeriods = [];
|
||||
$presencePeriodsNode = $bovinNode->PeriodesPresences->PeriodePresence ?? null;
|
||||
foreach ($this->normalizeToList($presencePeriodsNode) as $presencePeriodNode) {
|
||||
if (!is_object($presencePeriodNode)) {
|
||||
continue;
|
||||
}
|
||||
$presencePeriods[] = $this->mapPresencePeriod($presencePeriodNode);
|
||||
}
|
||||
|
||||
return new AnimalSummaryDto(
|
||||
identification: $identification,
|
||||
presencePeriods: $presencePeriods,
|
||||
);
|
||||
}
|
||||
}
|
||||
168
src/Bovin/Mapper/BovinNodeMappingTrait.php
Normal file
168
src/Bovin/Mapper/BovinNodeMappingTrait.php
Normal file
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Mapper;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\BovinIdentificationDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\BovinRef;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\DateValueDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ExploitationRef;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\MovementDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ParentInfoDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\PresencePeriodDto;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Helpers partagés par les mappers travaillant sur des noeuds `Bovin` issus
|
||||
* d'EDNOTIF, que la source soit une réponse SOAP directe (stdClass via ext-soap)
|
||||
* ou un message XML zippé (stdClass via ZipMessageDecoder).
|
||||
*/
|
||||
trait BovinNodeMappingTrait
|
||||
{
|
||||
protected function mapIdentification(object $identificationNode): BovinIdentificationDto
|
||||
{
|
||||
$birthDate = null;
|
||||
$birthDateNode = $identificationNode->DateNaissance ?? null;
|
||||
if (is_object($birthDateNode)) {
|
||||
$birthDate = new DateValueDto(
|
||||
date: $this->toNullableDate($birthDateNode->Date ?? null),
|
||||
completenessFlag: $this->toNullableString($birthDateNode->TemoinCompletude ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
return new BovinIdentificationDto(
|
||||
bovin: $this->mapBovinRef($identificationNode->Bovin ?? null),
|
||||
sex: $this->toNullableString($identificationNode->Sexe ?? null),
|
||||
breedType: $this->toNullableString($identificationNode->TypeRacial ?? null),
|
||||
birthDate: $birthDate,
|
||||
workNumber: $this->toNullableString($identificationNode->NumeroTravail ?? null),
|
||||
isFilie: $this->toNullableBool($identificationNode->StatutFilie ?? null),
|
||||
motherCarrier: $this->mapParentInfo($identificationNode->MerePorteuse ?? null),
|
||||
fatherIpg: $this->mapParentInfo($identificationNode->PereIPG ?? null),
|
||||
birthExploitation: $this->mapExploitationRef($identificationNode->ExploitationNaissance ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
protected function mapPresencePeriod(object $presencePeriodNode): PresencePeriodDto
|
||||
{
|
||||
$entryNode = $presencePeriodNode->Entree ?? null;
|
||||
$exitNode = $presencePeriodNode->Sortie ?? null;
|
||||
|
||||
return new PresencePeriodDto(
|
||||
entry: is_object($entryNode) ? $this->mapMovement($entryNode, 'entry') : null,
|
||||
exit: is_object($exitNode) ? $this->mapMovement($exitNode, 'exit') : null,
|
||||
);
|
||||
}
|
||||
|
||||
protected function mapMovement(object $movementNode, string $direction): MovementDto
|
||||
{
|
||||
if ('entry' === $direction) {
|
||||
$dateValue = $movementNode->DateEntree ?? ($movementNode->Date ?? ($movementNode->DateMouvement ?? null));
|
||||
$causeValue = $movementNode->CauseEntree ?? null;
|
||||
} else {
|
||||
$dateValue = $movementNode->DateSortie ?? ($movementNode->Date ?? ($movementNode->DateMouvement ?? null));
|
||||
$causeValue = $movementNode->CauseSortie ?? null;
|
||||
}
|
||||
|
||||
return new MovementDto(
|
||||
date: $this->toNullableDate($dateValue),
|
||||
cause: $this->toNullableString($causeValue),
|
||||
exploitation: $this->mapExploitationRef($movementNode->Exploitation ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
protected function mapParentInfo(mixed $parentNode): ?ParentInfoDto
|
||||
{
|
||||
if (!is_object($parentNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ParentInfoDto(
|
||||
bovin: $this->mapBovinRef($parentNode->Bovin ?? null),
|
||||
breedType: $this->toNullableString($parentNode->TypeRacial ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
protected function mapBovinRef(mixed $bovinNode): ?BovinRef
|
||||
{
|
||||
if (!is_object($bovinNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new BovinRef(
|
||||
countryCode: $this->toNullableString($bovinNode->CodePays ?? null),
|
||||
nationalNumber: $this->toNullableString($bovinNode->NumeroNational ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
protected function mapExploitationRef(mixed $exploitationNode): ?ExploitationRef
|
||||
{
|
||||
if (!is_object($exploitationNode)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ExploitationRef(
|
||||
countryCode: $this->toNullableString($exploitationNode->CodePays ?? null),
|
||||
exploitationNumber: $this->toNullableString($exploitationNode->NumeroExploitation ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
/** @return list<mixed> */
|
||||
protected function normalizeToList(mixed $value): array
|
||||
{
|
||||
if (null === $value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return is_array($value) ? array_values($value) : [$value];
|
||||
}
|
||||
|
||||
protected function toNullableString(mixed $value): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
$stringValue = trim((string) $value);
|
||||
|
||||
return '' === $stringValue ? null : $stringValue;
|
||||
}
|
||||
|
||||
protected function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function toNullableBool(mixed $value): ?bool
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (bool) $value;
|
||||
}
|
||||
|
||||
protected function toNullableDate(mixed $value): ?DateTimeImmutable
|
||||
{
|
||||
if (!is_string($value) || '' === trim($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return new DateTimeImmutable($value);
|
||||
} catch (Throwable) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Bovin/Mapper/InventoryMapper.php
Normal file
68
src/Bovin/Mapper/InventoryMapper.php
Normal file
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Mapper;
|
||||
|
||||
use Malio\EdnotifBundle\Bovin\Dto\EarTagSeriesDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\InventoryDto;
|
||||
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
|
||||
|
||||
final class InventoryMapper
|
||||
{
|
||||
use BovinNodeMappingTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly AnimalSummaryMapper $animalSummaryMapper,
|
||||
private readonly StandardResponseMapper $standardResponseMapper,
|
||||
) {}
|
||||
|
||||
public function map(object $soapResponse, ?object $unzippedMessage): InventoryDto
|
||||
{
|
||||
$standardResponse = $this->standardResponseMapper->map($soapResponse->ReponseStandard ?? null);
|
||||
|
||||
$nbBovins = $this->toNullableInt($soapResponse->ReponseSpecifique->NbBovins ?? null) ?? 0;
|
||||
|
||||
$startDate = null;
|
||||
$endDate = null;
|
||||
$includesEarTagStock = null;
|
||||
$animals = [];
|
||||
$earTagSeries = [];
|
||||
|
||||
if (is_object($unzippedMessage)) {
|
||||
$infoNode = $unzippedMessage->InformationsMessage ?? null;
|
||||
if (is_object($infoNode)) {
|
||||
$startDate = $this->toNullableDate($infoNode->DateDebut ?? null);
|
||||
$endDate = $this->toNullableDate($infoNode->DateFin ?? null);
|
||||
$includesEarTagStock = $this->toNullableBool($infoNode->StockBoucles ?? null);
|
||||
}
|
||||
|
||||
$bovinsNode = $unzippedMessage->Bovins->Bovin ?? null;
|
||||
foreach ($this->normalizeToList($bovinsNode) as $bovinNode) {
|
||||
if (!is_object($bovinNode)) {
|
||||
continue;
|
||||
}
|
||||
$animals[] = $this->animalSummaryMapper->map($bovinNode);
|
||||
}
|
||||
|
||||
$seriesNode = $unzippedMessage->Boucles->SerieBoucles ?? null;
|
||||
foreach ($this->normalizeToList($seriesNode) as $serieNode) {
|
||||
if (!is_object($serieNode)) {
|
||||
continue;
|
||||
}
|
||||
$earTagSeries[] = new EarTagSeriesDto(rawNode: $serieNode);
|
||||
}
|
||||
}
|
||||
|
||||
return new InventoryDto(
|
||||
standardResponse: $standardResponse,
|
||||
nbBovins: $nbBovins,
|
||||
startDate: $startDate,
|
||||
endDate: $endDate,
|
||||
includesEarTagStock: $includesEarTagStock,
|
||||
animals: $animals,
|
||||
earTagSeries: $earTagSeries,
|
||||
rawSoapResponse: $soapResponse,
|
||||
);
|
||||
}
|
||||
}
|
||||
46
src/Bovin/Mapper/PresumedExitsMapper.php
Normal file
46
src/Bovin/Mapper/PresumedExitsMapper.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Mapper;
|
||||
|
||||
use Malio\EdnotifBundle\Bovin\Dto\PresumedExitDto;
|
||||
use Malio\EdnotifBundle\Bovin\Dto\PresumedExitsDto;
|
||||
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
|
||||
|
||||
final class PresumedExitsMapper
|
||||
{
|
||||
use BovinNodeMappingTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly StandardResponseMapper $standardResponseMapper,
|
||||
) {}
|
||||
|
||||
public function map(object $soapResponse, ?object $unzippedMessage): PresumedExitsDto
|
||||
{
|
||||
$standardResponse = $this->standardResponseMapper->map($soapResponse->ReponseStandard ?? null);
|
||||
$nbBovins = $this->toNullableInt($soapResponse->ReponseSpecifique->NbBovins ?? null) ?? 0;
|
||||
|
||||
$presumedExits = [];
|
||||
|
||||
if (is_object($unzippedMessage)) {
|
||||
$exitsNode = $unzippedMessage->SortiesPresumees->SortiePresumee ?? null;
|
||||
foreach ($this->normalizeToList($exitsNode) as $exitNode) {
|
||||
if (!is_object($exitNode)) {
|
||||
continue;
|
||||
}
|
||||
$presumedExits[] = new PresumedExitDto(
|
||||
bovin: $this->mapBovinRef($exitNode->Bovin ?? null),
|
||||
exitDate: $this->toNullableDate($exitNode->DateSortie ?? null),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return new PresumedExitsDto(
|
||||
standardResponse: $standardResponse,
|
||||
nbBovins: $nbBovins,
|
||||
presumedExits: $presumedExits,
|
||||
rawSoapResponse: $soapResponse,
|
||||
);
|
||||
}
|
||||
}
|
||||
50
src/Bovin/Mapper/ReturnedDossiersMapper.php
Normal file
50
src/Bovin/Mapper/ReturnedDossiersMapper.php
Normal file
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Bovin\Mapper;
|
||||
|
||||
use Malio\EdnotifBundle\Bovin\Dto\ReturnedDossiersDto;
|
||||
use Malio\EdnotifBundle\Shared\Mapper\StandardResponseMapper;
|
||||
|
||||
final class ReturnedDossiersMapper
|
||||
{
|
||||
use BovinNodeMappingTrait;
|
||||
|
||||
public function __construct(
|
||||
private readonly AnimalSummaryMapper $animalSummaryMapper,
|
||||
private readonly StandardResponseMapper $standardResponseMapper,
|
||||
) {}
|
||||
|
||||
public function map(object $soapResponse, ?object $unzippedMessage): ReturnedDossiersDto
|
||||
{
|
||||
$standardResponse = $this->standardResponseMapper->map($soapResponse->ReponseStandard ?? null);
|
||||
$nbBovins = $this->toNullableInt($soapResponse->ReponseSpecifique->NbBovins ?? null) ?? 0;
|
||||
|
||||
$startDate = null;
|
||||
$animals = [];
|
||||
|
||||
if (is_object($unzippedMessage)) {
|
||||
$infoNode = $unzippedMessage->InformationsMessage ?? null;
|
||||
if (is_object($infoNode)) {
|
||||
$startDate = $this->toNullableDate($infoNode->DateDebut ?? null);
|
||||
}
|
||||
|
||||
$bovinsNode = $unzippedMessage->Bovins->Bovin ?? null;
|
||||
foreach ($this->normalizeToList($bovinsNode) as $bovinNode) {
|
||||
if (!is_object($bovinNode)) {
|
||||
continue;
|
||||
}
|
||||
$animals[] = $this->animalSummaryMapper->map($bovinNode);
|
||||
}
|
||||
}
|
||||
|
||||
return new ReturnedDossiersDto(
|
||||
standardResponse: $standardResponse,
|
||||
nbBovins: $nbBovins,
|
||||
startDate: $startDate,
|
||||
animals: $animals,
|
||||
rawSoapResponse: $soapResponse,
|
||||
);
|
||||
}
|
||||
}
|
||||
53
src/Shared/Mapper/StandardResponseMapper.php
Normal file
53
src/Shared/Mapper/StandardResponseMapper.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Shared\Mapper;
|
||||
|
||||
use Malio\EdnotifBundle\Shared\Dto\AnomalyDto;
|
||||
use Malio\EdnotifBundle\Shared\Dto\StandardResponseDto;
|
||||
|
||||
final class StandardResponseMapper
|
||||
{
|
||||
public function map(mixed $standardResponseNode): StandardResponseDto
|
||||
{
|
||||
$result = (bool) ($standardResponseNode->Resultat ?? false);
|
||||
$anomalyNode = is_object($standardResponseNode) ? ($standardResponseNode->Anomalie ?? null) : null;
|
||||
|
||||
$anomaly = null;
|
||||
if (is_object($anomalyNode)) {
|
||||
$anomaly = new AnomalyDto(
|
||||
code: $this->toNullableString($anomalyNode->Code ?? null),
|
||||
severity: $this->toNullableInt($anomalyNode->Severite ?? null),
|
||||
message: $this->toNullableString($anomalyNode->Message ?? null),
|
||||
);
|
||||
}
|
||||
|
||||
return new StandardResponseDto($result, $anomaly);
|
||||
}
|
||||
|
||||
private function toNullableString(mixed $value): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
$stringValue = trim((string) $value);
|
||||
|
||||
return '' === $stringValue ? null : $stringValue;
|
||||
}
|
||||
|
||||
private function toNullableInt(mixed $value): ?int
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
if (is_int($value)) {
|
||||
return $value;
|
||||
}
|
||||
if (is_numeric($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
87
src/Shared/Soap/ZipMessageDecoder.php
Normal file
87
src/Shared/Soap/ZipMessageDecoder.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Malio\EdnotifBundle\Shared\Soap;
|
||||
|
||||
use RuntimeException;
|
||||
use ZipArchive;
|
||||
|
||||
final class ZipMessageDecoder
|
||||
{
|
||||
/**
|
||||
* Décode le binaire `MessageZip` retourné par les opérations EDNOTIF de type
|
||||
* `Get*` (Inventaire / RetourDossiers / SortiesPresumees) : le contenu est déjà
|
||||
* décodé par ext-soap depuis base64, il reste à dézipper et parser le XML.
|
||||
*/
|
||||
public function decode(string $zipBinary): object
|
||||
{
|
||||
if ('' === $zipBinary) {
|
||||
throw new RuntimeException('ZipMessageDecoder: binaire ZIP vide.');
|
||||
}
|
||||
|
||||
$tempFile = tempnam(sys_get_temp_dir(), 'ednotif_zip_');
|
||||
if (false === $tempFile) {
|
||||
throw new RuntimeException('ZipMessageDecoder: impossible de créer un fichier temporaire.');
|
||||
}
|
||||
|
||||
try {
|
||||
if (false === file_put_contents($tempFile, $zipBinary)) {
|
||||
throw new RuntimeException('ZipMessageDecoder: impossible d\'écrire le fichier temporaire.');
|
||||
}
|
||||
|
||||
$xml = $this->readFirstEntry($tempFile);
|
||||
} finally {
|
||||
@unlink($tempFile);
|
||||
}
|
||||
|
||||
$previous = libxml_use_internal_errors(true);
|
||||
|
||||
try {
|
||||
$simpleXml = simplexml_load_string($xml);
|
||||
} finally {
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previous);
|
||||
}
|
||||
|
||||
if (false === $simpleXml) {
|
||||
throw new RuntimeException('ZipMessageDecoder: XML invalide dans l\'archive ZIP.');
|
||||
}
|
||||
|
||||
$json = json_encode($simpleXml);
|
||||
if (false === $json) {
|
||||
throw new RuntimeException('ZipMessageDecoder: échec de l\'encodage JSON intermédiaire.');
|
||||
}
|
||||
|
||||
$decoded = json_decode($json, false);
|
||||
if (!is_object($decoded)) {
|
||||
throw new RuntimeException('ZipMessageDecoder: décodage JSON non objet.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private function readFirstEntry(string $filePath): string
|
||||
{
|
||||
$zip = new ZipArchive();
|
||||
$openResult = $zip->open($filePath, ZipArchive::RDONLY);
|
||||
if (true !== $openResult) {
|
||||
throw new RuntimeException(sprintf('ZipMessageDecoder: ouverture ZIP impossible (code %s).', (string) $openResult));
|
||||
}
|
||||
|
||||
try {
|
||||
if (0 === $zip->numFiles) {
|
||||
throw new RuntimeException('ZipMessageDecoder: archive ZIP vide.');
|
||||
}
|
||||
|
||||
$xml = $zip->getFromIndex(0);
|
||||
if (false === $xml) {
|
||||
throw new RuntimeException('ZipMessageDecoder: lecture de l\'entrée ZIP impossible.');
|
||||
}
|
||||
} finally {
|
||||
$zip->close();
|
||||
}
|
||||
|
||||
return $xml;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user