test(arch) : garde-fou CI 'collections paginees obligatoires'
This commit is contained in:
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\Architecture;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use ReflectionClass;
|
||||||
|
use Symfony\Component\Finder\Finder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Garde-fou architecture : toute operation `GetCollection` exposee via API Platform
|
||||||
|
* doit avoir la pagination activee (ou laisser la valeur par defaut, qui est
|
||||||
|
* activee globalement dans `config/packages/api_platform.yaml`).
|
||||||
|
*
|
||||||
|
* Interdit : `new GetCollection(paginationEnabled: false)` sans exception documentee.
|
||||||
|
*
|
||||||
|
* Raison : une collection non paginee peut retourner des milliers de lignes et
|
||||||
|
* saturer la memoire du serveur, le reseau et le navigateur. La pagination est la
|
||||||
|
* seule protection fiable contre ce risque sur un CRM a donnees croissantes.
|
||||||
|
*
|
||||||
|
* Quand ajouter une entree dans `EXCLUDED` :
|
||||||
|
* - La collection est structurellement bornee (referentiel statique, < 100 items,
|
||||||
|
* jamais alimente par des utilisateurs) ET la suppression de la pagination est
|
||||||
|
* documentee avec une justification metier explicite.
|
||||||
|
* - Format obligatoire : `FQCN => '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<class-string, string>
|
||||||
|
*/
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user