feat : ajout de l'auth et du guichet pour la récupération d'info bovin

This commit is contained in:
2026-01-23 10:03:14 +01:00
commit b279f1ac47
25 changed files with 4565 additions and 0 deletions

112
src/Api/BovinApi.php Normal file
View File

@@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Api;
use Malio\EdnotifBundle\Auth\TokenProvider;
use Malio\EdnotifBundle\Dto\DossierAnimalDto;
use Malio\EdnotifBundle\Exception\EdnotifException;
use SoapClient;
use SoapFault;
final class BovinApi implements BovinApiInterface
{
public function __construct(
private TokenProvider $tokenProvider,
private SoapClient $metierClient,
) {
}
public function getDossierAnimal(string $exploitationNumero, string $numeroNational, string $codePays = 'FR'): DossierAnimalDto
{
$token = $this->tokenProvider->getToken();
$payload = [[
'JetonAuthentification' => $token,
'Exploitation' => [
'CodePays' => $codePays,
'NumeroExploitation' => $exploitationNumero,
],
'Bovin' => [
'CodePays' => $codePays,
'NumeroNational' => $numeroNational,
],
]];
try {
/** @var object $response */
$response = $this->metierClient->__soapCall('IpBGetDossierAnimal', $payload);
} catch (SoapFault $e) {
// Si cest un souci de jeton, tu peux invalider et retenter une fois (optionnel)
throw new \RuntimeException('SOAP Fault lors de IpBGetDossierAnimal: ' . $e->getMessage(), 0, $e);
}
$rs = $response->ReponseStandard ?? null;
$ok = is_object($rs) && (($rs->Resultat ?? false) === true);
if (!$ok) {
$anom = $rs->Anomalie ?? null;
$code = (string)($anom->Code ?? 'UNKNOWN');
$sev = (int)($anom->Severite ?? 1);
$msg = (string)($anom->Message ?? 'Appel EDNOTIF refusé');
throw new EdnotifException($code, $sev, $msg);
}
$identite = [];
$periodes = [];
$bovinNode = $response->ReponseSpecifique->Bovin ?? null;
if (is_object($bovinNode)) {
$identiteObj = $bovinNode->IdentiteBovin ?? null;
if (is_object($identiteObj)) {
$identite = $this->objectToArray($identiteObj);
}
$pp = $bovinNode->PeriodesPresences->PeriodePresence ?? null;
foreach ($this->normalizeList($pp) as $periode) {
if (!is_object($periode)) {
continue;
}
$entree = is_object($periode->Entree ?? null) ? $this->objectToArray($periode->Entree) : [];
$sortie = is_object($periode->Sortie ?? null) ? $this->objectToArray($periode->Sortie) : null;
$row = ['entree' => $entree];
if ($sortie !== null) {
$row['sortie'] = $sortie;
}
$periodes[] = $row;
}
}
return new DossierAnimalDto(
numeroNational: $numeroNational,
identiteBovin: $identite,
periodesPresence: $periodes,
rawResponse: $response,
);
}
/**
* @return list<mixed>
*/
private function normalizeList(mixed $value): array
{
if ($value === null) {
return [];
}
if (is_array($value)) {
return $value;
}
return [$value];
}
/**
* @return array<string,mixed>
*/
private function objectToArray(object $obj): array
{
// conversion simple (suffisante pour démarrer)
return json_decode(json_encode($obj, JSON_THROW_ON_ERROR), true, 512, JSON_THROW_ON_ERROR);
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Api;
use Malio\EdnotifBundle\Dto\DossierAnimalDto;
interface BovinApiInterface
{
public function getDossierAnimal(
string $exploitationNumero,
string $numeroNational,
string $codePays = 'FR'
): DossierAnimalDto;
}

View File

@@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Auth;
use Malio\EdnotifBundle\Exception\EdnotifException;
use Psr\Cache\CacheItemPoolInterface;
use SoapClient;
use SoapFault;
final class TokenProvider
{
public function __construct(
private SoapClient $guichetClient,
private string $entreprise,
private ?string $zone,
private ?string $application,
private string $login,
private string $password,
private int $tokenTtlSeconds,
private CacheItemPoolInterface $cachePool,
) {
}
public function getToken(): string
{
$cacheKey = $this->getCacheKey();
$item = $this->cachePool->getItem($cacheKey);
if ($item->isHit()) {
$token = $item->get();
if (is_string($token) && $token !== '') {
return $token;
}
}
$token = $this->createToken();
$item->set($token);
$item->expiresAfter($this->tokenTtlSeconds);
$this->cachePool->save($item);
return $token;
}
public function invalidateToken(): void
{
$this->cachePool->deleteItem($this->getCacheKey());
}
private function createToken(): string
{
$profil = array_filter([
'Entreprise' => $this->entreprise,
'Zone' => $this->zone,
'Application' => $this->application,
], static fn ($v) => $v !== null && $v !== '');
$payload = [
'Identification' => [
'UserId' => $this->login,
'Password' => $this->password,
'Profil' => $profil,
],
];
try {
/** @var object $response */
$response = $this->guichetClient->__soapCall('tkCreateIdentification', [$payload]);
} catch (SoapFault $e) {
throw new \RuntimeException('SOAP Fault lors de tkCreateIdentification: ' . $e->getMessage(), 0, $e);
}
$rs = $response->ReponseStandard ?? null;
$ok = is_object($rs) && (($rs->Resultat ?? false) === true);
if (!$ok) {
$anom = $rs->Anomalie ?? null;
$code = (string)($anom->Code ?? 'UNKNOWN');
$sev = (int)($anom->Severite ?? 1);
$msg = (string)($anom->Message ?? 'Authentification refusée');
throw new EdnotifException($code, $sev, $msg);
}
$token = $response->Jeton ?? null;
if (!is_string($token) || $token === '') {
throw new \RuntimeException('Guichet: réponse OK mais Jeton absent.');
}
return $token;
}
private function getCacheKey(): string
{
return 'ednotif.token.' . hash('sha256', $this->entreprise . '|' . $this->login);
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
final class Configuration implements ConfigurationInterface
{
public function getConfigTreeBuilder(): TreeBuilder
{
$treeBuilder = new TreeBuilder('ednotif');
$root = $treeBuilder->getRootNode();
$root
->children()
->scalarNode('guichet_wsdl')->cannotBeEmpty()->isRequired()->end()
->scalarNode('metier_wsdl')->cannotBeEmpty()->isRequired()->end()
->scalarNode('entreprise')->cannotBeEmpty()->isRequired()->end()
->scalarNode('zone')->defaultNull()->end()
->scalarNode('application')->defaultNull()->end()
->scalarNode('login')->cannotBeEmpty()->isRequired()->end()
->scalarNode('password')->cannotBeEmpty()->isRequired()->end()
->integerNode('token_ttl_seconds')->min(30)->defaultValue(900)->end()
->arrayNode('soap_options')
->addDefaultsIfNotSet()
->children()
->booleanNode('trace')->defaultFalse()->end()
->booleanNode('exceptions')->defaultTrue()->end()
->integerNode('connection_timeout')->min(1)->defaultValue(15)->end()
->integerNode('cache_wsdl')->defaultValue(\WSDL_CACHE_BOTH)->end()
->integerNode('features')->defaultValue(\SOAP_SINGLE_ELEMENT_ARRAYS)->end()
->end()
->end()
->end();
return $treeBuilder;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
final class EdnotifExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container): void
{
$configuration = new Configuration();
/** @var array{
* guichet_wsdl:string,
* metier_wsdl:string,
* entreprise:string,
* zone:?string,
* application:?string,
* login:string,
* password:string,
* token_ttl_seconds:int,
* soap_options:array<string,mixed>
* } $config
*/
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('ednotif.guichet_wsdl', $config['guichet_wsdl']);
$container->setParameter('ednotif.metier_wsdl', $config['metier_wsdl']);
$container->setParameter('ednotif.entreprise', $config['entreprise']);
$container->setParameter('ednotif.zone', $config['zone']);
$container->setParameter('ednotif.application', $config['application']);
$container->setParameter('ednotif.login', $config['login']);
$container->setParameter('ednotif.password', $config['password']);
$container->setParameter('ednotif.token_ttl_seconds', $config['token_ttl_seconds']);
$container->setParameter('ednotif.soap_options', $config['soap_options']);
$loader = new PhpFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
$loader->load('services.php');
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Dto;
final readonly class DossierAnimalDto
{
/**
* @param array<string,mixed> $identiteBovin
* @param list<array{entree: array<string,mixed>, sortie?: array<string,mixed>}> $periodesPresence
*/
public function __construct(
public string $numeroNational,
public array $identiteBovin,
public array $periodesPresence,
public object $rawResponse,
) {
}
}

11
src/EdnotifBundle.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
final class EdnotifBundle extends Bundle
{
}

View File

@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Exception;
use RuntimeException;
final class EdnotifException extends RuntimeException
{
public function __construct(
public readonly string $codeAnomalie,
public readonly int $severite,
string $message
) {
parent::__construct($message);
}
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Malio\EdnotifBundle\Soap;
use SoapClient;
final class SoapClientFactory
{
/**
* @param array<string,mixed> $soapOptions
*/
public function __construct(private array $soapOptions = [])
{
}
public function create(string $wsdl): SoapClient
{
$options = $this->soapOptions;
// Petites valeurs sûres par défaut
$options['encoding'] ??= 'UTF-8';
$options['exceptions'] ??= true;
return new SoapClient($wsdl, $options);
}
}