Files
Starseed/src/Module/Technique/Domain/Repository/ProviderRepositoryInterface.php
T
Matthieu 0ca1fb159a feat(technique) : ProviderProvider + ProviderProcessor + cloisonnement site (ERP-134)
Coeur API du repertoire prestataires (M3), jumeau du M2 fournisseurs :

- ProviderProvider : liste paginee (Paginator ORM), filtres
  search/categoryCode/siteId/includeArchived, tri companyName ASC,
  exclusion archives + soft-deletes (RG-3.16). Cloisonnement par site
  pilote par l'utilisateur (RG-3.17 / § 2.13) : liste restreinte au
  currentSite avant pagination (totalItems = perimetre), detail hors
  perimetre -> 404, bypass via sites.bypass_scope.
- ProviderProcessor : normalisation companyName (RG-3.11), POST formulaire
  principal (companyName + categories + sites), PATCH partiels par groupe
  en mode strict (RG-3.15, 403 sur tout le payload), archivage
  (RG-3.13/3.14), 409 doublon de nom (RG-3.10), garde d'ecriture cloisonnee
  des sites (RG-3.03/3.17, 422 sur sites pour les users sites.read_ref).
- ProviderReadGroupContextBuilder : gating comptabilite par AJOUT du groupe
  provider:read:accounting si accounting.view (jamais par retrait).
- ProviderFieldNormalizer : miroir SupplierFieldNormalizer.
- ApiResource cable (provider + processor) sur l'entite Provider.

Tests : ProviderApiTest, ProviderListTest, ProviderRbacGatingTest,
ProviderSiteScopeTest (26 tests). Suite complete verte (612 tests).
2026-06-12 11:03:19 +02:00

99 lines
4.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Module\Technique\Domain\Repository;
use App\Module\Technique\Domain\Entity\Provider;
use Doctrine\ORM\QueryBuilder;
interface ProviderRepositoryInterface
{
public function findById(int $id): ?Provider;
public function save(Provider $provider): void;
/**
* Restreint un QueryBuilder de liste aux prestataires rattaches au site donne
* (relation DIRECTE provider.sites). Sert le cloisonnement par site pilote par
* l'utilisateur (RG-3.17, § 2.13) : le ProviderProvider resout le site courant
* (CurrentSiteProvider) puis appelle cette methode quand l'user n'a pas
* `sites.bypass_scope`. Decouple ainsi la DECISION (Provider, qui connait
* l'user) du DQL (repository, qui ne connait que l'id de site).
*
* Sous-requete IN (et non JOIN sur la M2M) pour ne pas perturber le
* DISTINCT / ORDER BY / pagination du QueryBuilder de selection — meme parti
* pris que les filtres ?categoryCode / ?siteId. Applique AVANT la pagination
* (le COUNT du Paginator reflete alors le perimetre de l'user).
*/
public function applySiteScope(QueryBuilder $qb, int $siteId): void;
/**
* Construit un QueryBuilder de liste pour le repertoire prestataires.
* - Exclut toujours les prestataires soft-deletes (deleted_at IS NOT NULL, RG-3.16).
* - Archivage (RG-3.16) :
* - $archivedOnly = true -> uniquement les archives (is_archived = true) ;
* - sinon $includeArchived = true -> actifs + archives (echappatoire) ;
* - sinon (defaut) -> uniquement les actifs (is_archived = false).
* $archivedOnly a la priorite sur $includeArchived.
* - Tri par defaut : companyName ASC (RG-3.16).
* - $search : recherche fuzzy insensible a la casse sur companyName + les
* contacts lies (firstName / lastName / email) via sous-requete.
* Metacaracteres LIKE echappes. Ignore si null/vide.
* - $categoryCodes : restreint aux prestataires possedant au moins une
* categorie dont le code est dans la liste (OR). Liste vide = pas de filtre.
* - $siteIds : restreint aux prestataires rattaches a l'un des sites donnes
* (OR — RG-3.03, relation DIRECTE provider.sites). Liste vide = pas de filtre.
*
* Filtrage centralise ICI (et non dans le provider/controller) pour que la
* liste paginee et l'export partagent strictement la meme logique de selection
* (miroir M2).
*
* Contrat = SELECTION uniquement (filtres + tri). Aucun fetch-join to-many :
* l'hydratation des collections affichees est deleguee a
* {@see self::hydrateListCollections()} pour ne pas imposer le cout d'un
* produit cartesien aux chemins non pagines (§ 2.12, cf. M1/ERP-100, M2).
*
* NB : le cloisonnement par site pilote par l'utilisateur (RG-3.17, § 2.13) est
* applique en AMONT par le ProviderProvider (ERP-134), pas par ce QueryBuilder
* (qui ne connait pas l'user courant).
*
* @param list<string> $categoryCodes
* @param list<int> $siteIds
*/
public function createListQueryBuilder(
bool $includeArchived = false,
?string $search = null,
array $categoryCodes = [],
array $siteIds = [],
bool $archivedOnly = false,
): QueryBuilder;
/**
* Hydrate en lot les collections affichees par le repertoire (categories puis
* sites — relation DIRECTE provider.sites, RG-3.03) sur un jeu de prestataires
* DEJA charges, via l'identity map Doctrine (memes instances). A appeler apres
* une selection bornee (page courante ou jeu d'export) pour eviter le N+1 a la
* serialisation, sans imposer de fetch-join au QueryBuilder de selection
* (anti N+1, § 2.12).
*
* Charge les categories et les sites en DEUX requetes distinctes (et non un
* double fetch-join) pour ne pas multiplier categories x sites en un seul
* produit cartesien.
*
* @param list<Provider> $providers
*/
public function hydrateListCollections(array $providers): void;
/**
* Hydrate en lot la collection `contacts` sur un jeu de prestataires DEJA
* charges (memes instances via l'identity map). Reservee aux chemins qui ont
* besoin du contact principal (export) : la LISTE paginee n'embarque pas les
* contacts (§ 2.12), d'ou une methode dediee plutot qu'une passe supplementaire
* dans {@see self::hydrateListCollections()}.
*
* @param list<Provider> $providers
*/
public function hydrateContacts(array $providers): void;
}