'justification + reference ticket/spec'` * * @internal */ final class CollectionsArePaginatedTest extends TestCase { /** * Resources API Platform dont un `GetCollection` peut desactiver la pagination. * * Laisser vide au demarrage. Pour ajouter une exception : * 'App\Module\Foo\Infrastructure\ApiPlatform\Resource\BarResource' * => 'Referentiel statique < 50 items (types de contrat). Cf. ERP-XX.', * * @var array */ private const EXCLUDED = []; public function testAllGetCollectionOperationsHavePaginationEnabled(): void { $finder = new Finder() ->files() ->in(__DIR__.'/../../src') ->name('*.php') ->contains('#[ApiResource') ; // Garde : si le scan ne trouve rien, le chemin est casse — le test // deviendrait un faux positif vert. On verifie qu'il a du grain a moudre. self::assertNotEmpty( iterator_to_array($finder), 'Aucun fichier #[ApiResource] trouve sous src/ : chemin invalide ou codebase vide.', ); foreach ($finder as $file) { $fqcn = $this->extractFqcn($file->getRealPath()); if (null === $fqcn || !class_exists($fqcn)) { continue; } $reflection = new ReflectionClass($fqcn); $apiResourceAttributes = $reflection->getAttributes(ApiResource::class); if ([] === $apiResourceAttributes) { continue; } foreach ($apiResourceAttributes as $attribute) { /** @var ApiResource $apiResource */ $apiResource = $attribute->newInstance(); $operations = $apiResource->getOperations()?->getIterator() ?? []; foreach ($operations as $operation) { if (!$operation instanceof GetCollection) { continue; } if (false !== $operation->getPaginationEnabled()) { continue; } // La pagination est explicitement desactivee : verifier // que la resource est dans la whitelist EXCLUDED. self::assertArrayHasKey( $fqcn, self::EXCLUDED, sprintf( "La resource %s desactive la pagination sur une operation GetCollection.\n" ."Regle : toute collection API Platform doit etre paginee (cf. .claude/rules/backend.md).\n" ."Si cette collection est structurellement bornee et que la desactivation est justifiee,\n" .'ajouter une entree dans CollectionsArePaginatedTest::EXCLUDED avec une justification.', $fqcn, ), ); } } } } /** * Extrait le FQCN (namespace + classe) d'un fichier PHP par lecture du * source, sans charger le fichier. */ private function extractFqcn(string $path): ?string { $source = file_get_contents($path); if (false === $source) { return null; } if ( 1 !== preg_match('/^namespace\s+([^;]+);/m', $source, $nsMatch) || 1 !== preg_match('/^(?:final\s+|abstract\s+|readonly\s+)*class\s+(\w+)/m', $source, $classMatch) ) { return null; } return trim($nsMatch[1]).'\\'.$classMatch[1]; } }