applyScope($queryBuilder, $queryNameGenerator, $resourceClass); } public function applyToItem( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, array $identifiers, ?Operation $operation = null, array $context = [], ): void { $this->applyScope($queryBuilder, $queryNameGenerator, $resourceClass); } /** * Ajoute la clause `WHERE .site = :currentSite` au query builder * si les 3 conditions d'application sont remplies. No-op sinon. */ private function applyScope( QueryBuilder $queryBuilder, QueryNameGeneratorInterface $queryNameGenerator, string $resourceClass, ): void { // 1) Filtrer uniquement les resources qui ont opt-in via l'interface. // `is_subclass_of` gere a la fois `implements` direct et herite. if (!is_subclass_of($resourceClass, SiteAwareInterface::class)) { return; } // 2) Admin ou user avec bypass explicite : visibilite globale. // is_granted('sites.bypass_scope') retourne true pour les admins // (bypass total via isAdmin) meme sans permission explicite. if ($this->security->isGranted('sites.bypass_scope')) { return; } // 3) Pas de site courant -> no-op plutot que collection vide. // Decision assumee (cf. ticket 4 spec Risque 1) : un user sans // currentSite voit tout. L'alternative "collection vide" est // rejetee car elle rendrait l'app inutilisable pour un user // mal configure. $currentSite = $this->currentSiteProvider->get(); if (null === $currentSite) { return; } // Application du filtre : alias racine du QueryBuilder, parametre // genere pour eviter les collisions avec d'autres extensions. $rootAlias = $queryBuilder->getRootAliases()[0]; $parameterName = $queryNameGenerator->generateParameterName('currentSite'); $queryBuilder ->andWhere(sprintf('%s.site = :%s', $rootAlias, $parameterName)) ->setParameter($parameterName, $currentSite) ; } }