# Création d'endpoints GET (GetOne / GetAll)

> Ce guide décrit la mise en place d'endpoints en suivant une architecture **CQRS** (Command Query Responsibility Segregation) : les opérations de lecture sont portées par des objets Query dédiés, séparés de toute logique d'écriture.

Ce guide décrit comment créer des endpoints `GetOne` et `GetAll` en utilisant les abstractions fournies par `NoahVet\Reef`. L'exemple s'appuie sur l'entité `Application` du projet marketplace-back.

---

## Architecture

```
Infrastructure/Http/{Context}/{Entity}/
├── ControllerInterface.php   ← constante SNAKE_CASE_PLURAL_NAME
├── GetOneController.php      ← endpoint GET /{version}/{plural}/{id}
└── GetAllController.php      ← endpoint GET /{version}/{plural}

Domain/Serialization/Output/EntityToArray/
└── EntityETAAttribute.php    ← champs à sérialiser

Domain/Serialization/Normalizer/
├── EntityNormalizerInterface.php
└── EntityNormalizer.php      ← postNormalize (facultatif)
```

> Il n'y a **pas** de classe Query spécifique par entité : les classes génériques `GetOneQuery` et `GetAllQuery` (du package `noahvet/reef-api-client`) remplacent le boilerplate habituel.

### Flux GetOne

```
Request → Route → ParamConverter (Entity depuis DB) → Controller
  → GetOneQueryServiceInterface (fetch + sérialisation)
    → GetOneQuery (object, normalizationAttributes, janeModelFQCN, locale?, postNormalize?)
  → JsonResponse 200
```

### Flux GetAll

```
Request → Route → Controller
  → GetAllQueryServiceInterface (fetch via filters/sorting/permissions)
    → GetAllQuery (entityFQCN, context)
  → GetAllServiceInterface (sérialisation de chaque entité)
    → ETAAttribute + Jane model + postNormalize
  → JsonResponse 200 / 206 (pagination)
```

---

## Étape 1 — ControllerInterface

Définit la constante `SNAKE_CASE_PLURAL_NAME` utilisée pour construire les routes et les noms de routes. À partager entre tous les controllers d'un même contexte/entité.

```php
// src/Infrastructure/Http/Core/Application/ControllerInterface.php

namespace App\Infrastructure\Http\Core\Application;

interface ControllerInterface
{
    public const string SNAKE_CASE_PLURAL_NAME = 'applications';
}
```

---

## Étape 2 — ETAAttribute (champs à sérialiser)

Déclare les champs (et sous-champs) à extraire de l'entité lors de la normalisation. Des constantes séparées par action permettent de contrôler finement ce qui est exposé.

```php
// src/Domain/Serialization/Output/EntityToArray/ApplicationETAAttribute.php

namespace App\Domain\Serialization\Output\EntityToArray;

final class ApplicationETAAttribute
{
    public const array CREATE  = self::COMMON;
    public const array GET     = self::COMMON;
    public const array GET_ALL = self::COMMON;
    public const array UPDATE  = self::COMMON;

    public const array TRANSLATION_FIELDS = [
        'name',
        'description',
        'summary',
        'links' => [
            'homepageUrl',
            'tosUrl',
        ],
    ];

    private const array COMMON = [
        'id',
        'createdDate',
        'updatedDate',
        'code',
        'state',
        'translations' => self::TRANSLATION_FIELDS,
    ];
}
```

> Les clés d'un tableau imbriqué (`'links' => ['homepageUrl', ...]`) indiquent une relation à traverser lors de la normalisation.

---

## Étape 3 — Normalizer (facultatif)

Le `postNormalize` est une étape appliquée après la normalisation. Il est typiquement utilisé pour aplatir les traductions et supprimer la clé `translations` du résultat final.

### Interface

```php
// src/Domain/Serialization/Normalizer/ApplicationNormalizerInterface.php

namespace App\Domain\Serialization\Normalizer;

interface ApplicationNormalizerInterface extends NormalizerInterface
{
    /** @param array<string, mixed> $data */
    public function postNormalizeEntityToArray(array &$data, string $locale): void;
}
```

### Implémentation

```php
// src/Domain/Serialization/Normalizer/ApplicationNormalizer.php

namespace App\Domain\Serialization\Normalizer;

use App\Domain\Serialization\Output\EntityToArray\ApplicationETAAttribute;

final readonly class ApplicationNormalizer extends AbstractNormalizer implements ApplicationNormalizerInterface
{
    #[\Override]
    public function postNormalizeEntityToArray(array &$data, string $locale): void
    {
        foreach (ApplicationETAAttribute::TRANSLATION_FIELDS as $key => $field) {
            $resolvedKey = \is_string($field) ? $field : $key;
            $data[$resolvedKey] = $data['translations'][$locale][$resolvedKey];
        }

        unset($data['translations']);
    }
}
```

> Si l'entité n'a pas de traductions, le `postNormalize` n'est pas nécessaire et peut simplement ne pas être passé à la Query.

---

## Endpoint GetOne

La classe `GetOneQuery` est fournie par `NoahVet\Reef\Application\Query\GetOneQuery`. Elle implémente `GetOneQueryInterface` et reçoit directement toutes les informations nécessaires à la sérialisation.

### Controller

L'entité est résolue automatiquement par le ParamConverter Symfony grâce au type-hint `Application $entity`.

> Les services injectés (`GetOneQueryServiceInterface`, `GetAllQueryServiceInterface`, `GetAllServiceInterface`) sont implémentés dans `NoahVet\Reef\Infrastructure\Http\Get\`.

```php
// src/Infrastructure/Http/Core/Application/GetOneController.php

namespace App\Infrastructure\Http\Core\Application;

use App\Domain\Serialization\Normalizer\ApplicationNormalizerInterface;
use App\Domain\Serialization\Output\EntityToArray\ApplicationETAAttribute;
use App\Entity\Core\Application;
use App\Generated\V1\Jane\Model\V1ApplicationsIdGetResponse200;
use NoahVet\Reef\Application\Port\GetOneQueryServiceInterface;
use NoahVet\Reef\Application\Query\GetOneQuery;
use NoahVet\Reef\Controller\Common\AbstractGetController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

/**
 * @extends AbstractGetController<Application, V1ApplicationsIdGetResponse200>
 */
#[AsController]
final class GetOneController extends AbstractGetController implements ControllerInterface
{
    #[Route(
        path: '/{version}/'.self::SNAKE_CASE_PLURAL_NAME.'/{id}',
        name: self::SNAKE_CASE_PLURAL_NAME.'_get_one',
        requirements: [
            'id'      => '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}',
            'version' => "v\d+",
        ],
        methods: Request::METHOD_GET,
    )]
    public function __invoke(
        ApplicationNormalizerInterface $applicationNormalizer,
        Application $entity,
        GetOneQueryServiceInterface $queryService,
    ): Response {
        return $this->getResponse(
            data: $queryService(new GetOneQuery(
                object: $entity,
                normalizationAttributes: ApplicationETAAttribute::GET,
                janeModelFQCN: V1ApplicationsIdGetResponse200::class,
                locale: $this->request->getLocale(),
                postNormalize: static function (array &$item, ?string $locale) use ($applicationNormalizer): void {
                    $applicationNormalizer->postNormalizeEntityToArray(data: $item, locale: (string) $locale);
                },
            )),
        );
    }
}
```

**Route générée :** `GET /{version}/applications/{id}` (ex: `GET /v1/applications/550e8400-e29b-41d4-a716-446655440000`)

### Sans postNormalize ni locale

Pour une entité sans traductions :

```php
return $this->getResponse(
    data: $queryService(new GetOneQuery(
        object: $entity,
        normalizationAttributes: ConnectedOfferETAAttribute::GET,
        janeModelFQCN: V1ConnectedOffersIdGetResponse200::class,
    )),
);
```

---

## Endpoint GetAll

La classe `GetAllQuery` est fournie par `NoahVet\Reef\Application\Query\GetAllQuery`. Elle implémente `GetAllQueryInterface` et reçoit l'FQCN de l'entité ainsi que le contexte de la requête.

### Controller

Le controller récupère les entités via `GetAllQueryServiceInterface`, puis les sérialise via `GetAllServiceInterface`. La pagination est gérée automatiquement par `AbstractListController` (header `Content-Range`, HTTP 206 si paginé).

```php
// src/Infrastructure/Http/Core/Application/GetAllController.php

namespace App\Infrastructure\Http\Core\Application;

use App\Domain\Serialization\Normalizer\ApplicationNormalizerInterface;
use App\Domain\Serialization\Output\EntityToArray\ApplicationETAAttribute;
use App\Entity\Core\Application;
use App\Generated\V1\Jane\Model\V1ApplicationsGetResponse200Item;
use NoahVet\Reef\Application\Port\GetAllQueryServiceInterface;
use NoahVet\Reef\Application\Query\GetAllQuery;
use NoahVet\Reef\Controller\Common\AbstractListController;
use NoahVet\Reef\Domain\Serialization\Service\GetAllServiceInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController;
use Symfony\Component\Routing\Attribute\Route;

/**
 * @extends AbstractListController<Application, V1ApplicationsGetResponse200Item>
 */
#[AsController]
final class GetAllController extends AbstractListController implements ControllerInterface
{
    #[Route(
        path: '/{version}/'.self::SNAKE_CASE_PLURAL_NAME,
        requirements: [
            'version' => "v\d+",
        ],
        methods: Request::METHOD_GET,
    )]
    public function __invoke(
        ApplicationNormalizerInterface $applicationNormalizer,
        GetAllQueryServiceInterface $queryService,
        GetAllServiceInterface $getAllService,
    ): Response {
        /** @var list<Application> $applications */
        $applications = $queryService(new GetAllQuery(
            entityFQCN: Application::class,
            context: $this->requestContextFactory->createContextFromRequest(
                request: $this->request,
                whitelist: $this->whitelist,
            ),
        ));

        return $this->getResponse(
            data: $getAllService->execute(
                objects: $applications,
                normalizationAttributes: ApplicationETAAttribute::GET_ALL,
                janeModelFQCN: V1ApplicationsGetResponse200Item::class,
                locale: $this->request->getLocale(),
                postNormalize: static function (array &$item, ?string $locale) use ($applicationNormalizer): void {
                    $applicationNormalizer->postNormalizeEntityToArray(data: $item, locale: (string) $locale);
                },
            ),
        );
    }
}
```

**Route générée :** `GET /{version}/applications` (ex: `GET /v1/applications`)

---

## Récapitulatif des fichiers à créer

| Fichier | Type | Obligatoire |
|---|---|---|
| `Infrastructure/Http/{Context}/{Entity}/ControllerInterface.php` | Interface | Oui |
| `Domain/Serialization/Output/EntityToArray/EntityETAAttribute.php` | Classe | Oui |
| `Infrastructure/Http/{Context}/{Entity}/GetOneController.php` | Controller | Oui (GetOne) |
| `Infrastructure/Http/{Context}/{Entity}/GetAllController.php` | Controller | Oui (GetAll) |
| `Domain/Serialization/Normalizer/EntityNormalizerInterface.php` | Interface | Non |
| `Domain/Serialization/Normalizer/EntityNormalizer.php` | Implémentation | Non |

> Les classes `GetOneQuery` et `GetAllQuery` sont fournies par le package — aucun fichier Query à créer par entité.

## Points clés

- Le **Jane model** (`V1ApplicationsIdGetResponse200`, `V1ApplicationsGetResponse200Item`) est généré depuis l'OAS et doit exister avant de créer les controllers.
- Le **ParamConverter** Symfony résout automatiquement `Application $entity` dans le `GetOneController` à partir du segment `{id}` de la route.
- Le **`postNormalize`** est une closure optionnelle pour transformer le tableau normalisé après sérialisation (typiquement : aplatir les traductions, enrichir avec des données calculées).
- La **pagination** pour `GetAll` est gérée intégralement par `AbstractListController` : le header `Content-Range` est ajouté et le code HTTP passe à `206` dès qu'il y a plusieurs pages.
- Les **`whitelist`** dans le `GetAllController` viennent de `AbstractListController::preInvoke()` qui appelle `mapQueryParamToWhitelist()` automatiquement avant l'invocation du controller.
- **Cas personnalisé** : si un endpoint nécessite une logique de filtrage/permissions sur mesure non couverte par `GetAllQuery`, implémenter `GetAllQueryInterface` manuellement reste possible (ex: `GetApplicationCatalogQuery`).
