API Platform — ressources, opérations, sécurité, sérialisation
Kohinos expose une API REST via API Platform 3.x, principalement destinée à des usages internes : future application mobile adhérent, intégrations partenaires sous contrôle, outils d'administration. Aucune opération financière sensible (transfert eMLc, cotisation, prélèvement SEPA, redistribution SSA) n'est exécutable via l'API publique : ces flux restent pilotés par les services métier et leurs contrôleurs internes, sous workflow d'admin et invariants vérifiés (cf. invariants financiers).
Ressources exposées & opérations
flowchart LR CLIENT["Client APISeules les ressources structurelles (entités d'identité, catalogue prestataire, contenu, SSA en lecture/écriture limitée) sont exposées. Tout ce qui touche au mouvement de monnaie reste hors API publique.
(mobile, partenaire)"] -->|API-AUTH-TOKEN| FW[Firewall ^/api] FW --> AUTH[ApiKeyAuthenticator] AUTH --> RES{Ressources} RES --> USR[User · GET/POST/PUT] RES --> ADH[Adherent · GET/POST/PUT] RES --> PRE[Prestataire · GET/POST/PUT] RES --> CMP[Comptoir · GET/POST/PUT] RES --> GRP[Groupe / Groupeprestataire] RES --> SOL[SolidoumeItem · GET/POST/PUT] RES --> COT[Cotisation* · GET/POST/PUT] RES --> CMS[Page / News / Faq / Rubrique] RES --> DOC[Document · GET/POST/PUT/DELETE] RES --> GEO[GeolocPrestataire · GET/POST/PUT] RES --> MAP["Map · GET /public/poi/all"] RES -.NON exposé.-> MONEY[("Flux · Operation
Sepa* · Don*")]
Catalogue des ressources
Ce tableau reflète l'état du code actuel (attributs #[ApiResource] dans
src/Entity/). La colonne sécurité liste les rôles acceptés pour la
collection ; les opérations individuelles peuvent restreindre davantage (cf. section sécurité).
| Entité | Opérations | Sécurité (collection) | Groupes |
|---|---|---|---|
User |
GET, GET coll., POST, PUT | ROLE_SONATA_USER_GERER_LIST |
read / write |
Adherent |
GET, GET coll., POST, PUT | ROLE_ADMIN_ADHERENT_GERER_LIST · auto-accès via ROLE_API & object.user == user |
read:admin / write |
Prestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST ou ROLE_API |
read:public / read:admin / write |
Comptoir |
GET, GET coll., POST, PUT | ROLE_ADMIN_COMPTOIR_GERER_LIST ou ROLE_API |
read / write (max_depth) |
Groupe |
GET, GET coll., POST, PUT | ROLE_ADMIN_GROUPE_GERER_LIST ou ROLE_API |
read / write |
Groupeprestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST ou ROLE_API |
read / write |
TypePrestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST ou ROLE_API |
read / write |
EtatPrestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST |
read / write |
ContactPrestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST |
read / write |
ContactComptoir |
GET, GET coll., POST, PUT | ROLE_ADMIN_COMPTOIR_GERER_LIST ou ROLE_API |
read (max_depth) / write |
GeolocPrestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST ou ROLE_API |
read / write · ressource-level security |
Map |
GET coll. uniquement : /public/poi/all |
ROLE_ADMIN_PRESTATAIRE_GERER_LIST ou ROLE_API |
read · contrôleur custom MapController, read: false, pagination désactivée |
SolidoumeItem |
GET, GET coll., POST, PUT | ROLE_ADMIN_SOLIDOUME_LIST · POST/PUT auto-accès object.user == user |
read / write (max_depth) |
CotisationAdherent |
GET, GET coll., POST, PUT | ROLE_ADMIN_ADHERENT_COTISATIONS_GERER_LIST |
read / write (max_depth) |
CotisationPrestataire |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_COTISATIONS_GERER_LIST |
read / write (max_depth) |
CotisationInfos |
GET, GET coll., POST, PUT | ROLE_ADMIN_ADHERENT_COTISATIONS_GERER_LIST ou ROLE_ADMIN_PRESTATAIRE_COTISATIONS_GERER_LIST |
read / write |
Page |
GET, GET coll., POST, PUT | ROLE_ADMIN_PAGE_GERER_LIST |
read / write |
News |
GET, GET coll., POST, PUT | ROLE_ADMIN_NEWS_GERER_LIST |
read / write |
Faq |
GET, GET coll., POST, PUT | ROLE_ADMIN_FAQ_GERER_LIST |
read / write |
Rubrique |
GET, GET coll., POST, PUT | ROLE_ADMIN_PRESTATAIRE_GERER_LIST ou ROLE_API |
read / write |
Document |
GET, GET coll., POST, PUT, DELETE | ROLE_ADMIN_DOCUMENT_GERER_LIST |
read / write |
Filtres déclaratifs. Aucun #[ApiFilter] n'est aujourd'hui posé sur
les entités : les filtres SQL transverses sont gérés en amont par les SQL filters
Doctrine, notamment App\Filter\EnabledFilter (auto-applique enabled = true
aux entités utilisant EnablableEntityTrait, à l'exception de Flux).
Sécurité
Firewall & authentification
Deux firewalls dans config/packages/security.yaml :
| Firewall | Pattern | Mode | Authenticators |
|---|---|---|---|
api |
^/api |
stateless | ApiKeyAuthenticator (header API-AUTH-TOKEN) · fallback HTTP Basic via api_email_user_provider |
main |
^/ |
session | LoginAuthenticator, EmailTokenAuthenticator, remember-me |
Le mode JWT n'est pas utilisé. Kohinos s'en tient à une clé API par utilisateur
(champ User::apiKey, indexé idx_user_api_key) résolue par
UserRepository::findOneByApiKey(). Cohérent avec la priorité 2 souveraineté
(cf. conventions) : pas de service d'émission de tokens tiers.
Hiérarchie de rôles
| Rôle | Hérite de | Usage API |
|---|---|---|
ROLE_API | ROLE_USER | Accès lecture aux ressources publiques (Prestataire, Comptoir, Groupe, Map…) via clé API. |
ROLE_SUPER_ADMIN | ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH, ROLE_API | Accès complet, mais doit rester un compte humain — pas un compte de service. |
Distinction importante : ROLE_API n'est pas un
sous-ensemble de ROLE_SUPER_ADMIN — c'est l'inverse. Un compte de service mobile
porte ROLE_API seul et doit rester sans privilège admin.
Décorateurs security au niveau opération
Toutes les ressources définissent security: par opération (et parfois au niveau
ressource). Exemple représentatif sur Adherent :
#[ApiResource(
operations: [
new GetCollection(
normalizationContext: ['groups' => ['read:admin'], 'enable_max_depth' => true],
security: "is_granted('ROLE_ADMIN_ADHERENT_GERER_LIST')"
),
new Post(security: "is_granted('ROLE_ADMIN_ADHERENT_GERER_EDIT')"),
new Get(
normalizationContext: ['groups' => ['read:admin'], 'enable_max_depth' => true],
security: "is_granted('ROLE_ADMIN_ADHERENT_GERER_LIST')
or (is_granted('ROLE_API') and object.user == user)"
),
new Put(
security: "is_granted('ROLE_ADMIN_ADHERENT_GERER_EDIT') or object.user == user",
securityPostDenormalize: "is_granted('ROLE_ADMIN_ADHERENT_GERER_EDIT')
or (object.user == user and previous_object.user == user)"
),
],
)]
Le securityPostDenormalize est critique : il s'exécute après
désérialisation et compare previous_object à object. Sans lui, un
adhérent pourrait via PUT modifier le champ user d'une autre fiche pour s'y
rattacher. La règle Kohinos : tout PUT sur ressource possédée par un utilisateur
doit avoir un securityPostDenormalize.
Voters Symfony
Quand la décision dépasse l'appartenance simple, on s'appuie sur des Voters dédiés :
App\Security\Voter\SsaVoter— décisions liées aux caisses SSA (lecture sensibles, exécution).App\Security\Voter\ExportVoter— autorisation des exports.App\Security\ApiSuperAdminVoter— refuse certaines opérations API même àROLE_SUPER_ADMINquand elles n'ont pas de sens via l'API (defense-in-depth).
Référencer un voter depuis API Platform : security: "is_granted('VIEW_SSA', object)".
Le subject object est l'instance, comparée par le voter.
Sérialisation — groupes
Convention de nommage observée dans le code :
| Groupe | Sens | Exposition typique |
|---|---|---|
read | Lecture standard | Champs publics non-sensibles d'une fiche. |
read:public | Lecture anonyme/api | Catalogue prestataire, géoloc, comptoirs ouverts au public. |
read:admin | Lecture admin | Champs internes (statut, audit, relations sensibles). |
write | Création/édition | denormalizationContext par défaut. |
user:read | Lecture User spécifique | Identifiant User::id. |
Pose sur les champs :
#[ORM\Column(type: 'uuid', unique: true)]
#[Groups(['user:read', 'read'])]
protected $id;
#[ORM\Column(length: 150)]
#[Groups(['read', 'write'])]
protected $name;
#[ORM\Column(name: 'iban_encrypted', type: 'text', nullable: true)]
// PAS de #[Groups(...)] — l'IBAN n'est JAMAIS sérialisé par l'API
private ?string $ibanEncrypted = null;
L'option 'enable_max_depth' => true est utilisée sur Adherent,
Comptoir, ContactComptoir, SolidoumeItem,
CotisationAdherent, CotisationPrestataire pour borner la profondeur
de sérialisation des graphes — combiner avec #[MaxDepth(n)] sur les relations.
Validation
Symfony Validator s'applique automatiquement aux POST/PUT API Platform. Les contraintes
courantes sont posées en attributs #[Assert\*] :
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
#[UniqueEntity(fields: 'email', message: 'Cet email est déjà utilisé !')]
#[UniqueEntity(fields: 'raison', message: 'Le prestataire avec cette raison existe déjà.')]
class Prestataire { /* … */ }
Les erreurs de validation remontent en 422 Unprocessable Entity avec un payload
Hydra/Problem-JSON listant les violations par propertyPath.
Pagination
Configuration globale dans config/packages/api_platform.yaml :
api_platform:
collection:
pagination:
enabled: true
defaults:
pagination_items_per_page: 30
pagination_maximum_items_per_page: 100
pagination_client_items_per_page: true
eager_loading:
max_joins: 40
Le client peut donc demander ?itemsPerPage=60 jusqu'à la borne dure de
100. Pour les ressources à fort volume (audit, historique de flux quand il
sera exposé en lecture seule), envisager la partial pagination au cas par cas :
new GetCollection(paginationPartial: true)
La Map est l'exception : paginationEnabled: false car le
contrôleur custom MapController retourne un payload géoloc compact.
State processors & providers
Aujourd'hui Kohinos s'appuie principalement sur les extensions Doctrine par
défaut d'API Platform (collection auto, item par identifiant, persist via EM). Le dossier
src/State/ n'existe pas encore.
Cas d'usage à venir pour passer en State Processor custom :
- POST sur une ressource exposée dont la création doit déclencher un service
métier transactionnel (ex : pré-inscription SEPA via API mobile → délègue à
SepaSubscriptionServiceau lieu d'écrire directement l'entité). - PUT qui doit recalculer un solde dérivé : confier le calcul au service, le processor ne fait que valider l'autorisation et orchestrer.
- DELETE soft sur
SolidoumeItem: poserarchived = now()plutôt que de réellement supprimer.
Squelette type :
final class SolidoumeItemArchiveProcessor implements ProcessorInterface
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly SolidoumeArchiveService $archiver,
) {}
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
{
// $data est l'entité désérialisée
$this->archiver->archive($data); // soft-delete daté
$this->em->flush();
return $data;
}
}
Branché sur l'opération : new Delete(processor: SolidoumeItemArchiveProcessor::class).
Cas particulier : Map & contrôleur custom
Map illustre le pattern ressource API sans Doctrine :
#[ApiResource(
operations: [
new GetCollection(
uriTemplate: '/public/poi/all',
controller: MapController::class,
paginationEnabled: false,
security: "is_granted('ROLE_ADMIN_PRESTATAIRE_GERER_LIST') or is_granted('ROLE_API')",
read: false,
),
]
)]
read: false désactive le provider Doctrine : c'est le contrôleur qui
construit la réponse (agrégation géoloc multi-tables). Pratique pour les read models
qui ne correspondent pas 1-1 à une entité.
Anti-patterns
- Exposer une entité financière sans
security:strict. Toute ressource touchant àFlux,Operation,SepaMandate,SepaTransfer,Don*doit rester hors API publique. Si lecture nécessaire (mobile), exposer un read model agrégé via State Provider, pas l'entité. - Oublier un groupe de sérialisation sur un nouveau champ sensible.
Tout
#[ORM\Column]sans#[Groups]est inerte côté API — c'est le bon défaut. À l'inverse, ajouter'read'par réflexe expose le champ à toutes les routes GET. Lire deux fois avant. - Pagination non bornée. Ne jamais désactiver
pagination_maximum_items_per_page. Aujourd'hui :100. - Modifier un champ d'intégrité via PUT.
flux.hash,operation.amount,sepa_mandate.umr,solidoume_item.lastMonthPayed— aucun ne doit être writable. À défaut de processor, ne pas mettre'write'sur leur#[Groups]. - Ne se reposer que sur
security:niveau ressource. Toujours posersecurity:au niveau de chaque opération — un attribut hérité peut sauter à une régression de version. - Renvoyer un objet possédant des relations non-bornées. Sans
enable_max_depth+#[MaxDepth], on peut sérialiser un graphe gigantesque (timeout, fuite d'info).
Tests d'API
API Platform fournit ApiPlatform\Symfony\Bundle\Test\ApiTestCase qui hérite de
WebTestCase. Règles Kohinos :
- Un seul
createClient()par classe (rappel Symfony 7.x — cf. Tests & qualité).AbstractFunctionalTestCase::setUp()l'appelle déjà. - Pour authentifier : forger un header
API-AUTH-TOKENavec la clé d'un user de fixture, ne pas instancier d'authenticator dans le test. - Tester aussi bien le chemin refusé (403) que le chemin accepté : les régressions de sécurité passent souvent inaperçues si on ne teste que le bon rôle.
final class PrestataireApiTest extends ApiTestCase
{
public function testAnonymousIsRejected(): void
{
static::createClient()->request('GET', '/api/prestataires');
$this->assertResponseStatusCodeSame(401);
}
public function testRoleApiCanListPrestataires(): void
{
$apiKey = self::getContainer()->get(UserRepository::class)
->findOneByUsername('mobile-bot')->getApiKey();
static::createClient()->request('GET', '/api/prestataires', [
'headers' => ['API-AUTH-TOKEN' => $apiKey],
]);
$this->assertResponseIsSuccessful();
$this->assertJsonContains(['@type' => 'hydra:Collection']);
}
}
Pour aller plus loin
config/packages/api_platform.yaml— config globale (pagination, formats, mapping).config/packages/security.yaml— firewallsapi+main, hiérarchie des rôles.src/Security/ApiKeyAuthenticator.php·ApiKeyUserProvider.php·ApiSuperAdminVoter.phpsrc/Security/Voter/SsaVoter.php,ExportVoter.phpsrc/Filter/EnabledFilter.php— SQL filter Doctrine, appliqueenabled = true.src/Entity/*.php— toute entité exposée porte un#[ApiResource].src/Controller/MapController.php— exemple de ressource avec contrôleur custom.- Invariants financiers — pourquoi rien de monétaire ne passe par l'API.
- Cryptographie — chiffrement IBAN, jamais sérialisé.
- Conventions — règles de souveraineté et choix d'auth.