Creating Custom Analyzers
Guide pratique pour ajouter un analyzer compatible avec l’API actuelle de Doctrine Doctor.
1. Contrat à respecter
Un analyzer doit:
- choisir son chemin d’exécution selon ses données d’entrée
- exposer
analyze(QueryDataCollection)ouanalyzeMetadata()selon le contrat - rester stateless (pas d’état mutable partagé)
- être enregistré avec le tag
doctrine_doctor.analyzer
| Contrat | Exécution | À utiliser pour |
|---|---|---|
AnalyzerInterface |
Profiler runtime | Les constats qui dépendent des requêtes SQL ou du contexte de la requête courante |
StaticAnalyzerInterface |
Commande CI | L’analyse du code source ou des mappings sans dépendre du SQL observé pendant une requête |
DatabaseAuditAnalyzerInterface |
Commande CI avec --with-database |
Les contrôles qui interrogent la base configurée |
MetadataAnalyzerInterface |
Commande CI | Contrat historique des contrôles de mapping; il étend StaticAnalyzerInterface |
DatabaseAuditAnalyzerInterface étend MetadataAnalyzerInterface; les audits de base de données sont donc désactivés par défaut dans la commande. MetadataAnalyzerInterface utilise MetadataAnalyzerTrait pour adapter analyzeMetadata() au contrat commun.
Pour exécuter les contrôles indépendants des requêtes du profiler dans CI:
php bin/console doctrine:doctor:analyze
php bin/console doctrine:doctor:analyze --with-database --fail-on=warning
La liste des analyzers et leurs chemins d’exécution détaille le classement actuel.
Références:
src/Analyzer/AnalyzerInterface.phpsrc/Analyzer/StaticAnalyzerInterface.phpsrc/Analyzer/DatabaseAuditAnalyzerInterface.phpsrc/Analyzer/MetadataAnalyzerInterface.phpsrc/Analyzer/Concern/MetadataAnalyzerTrait.phpsrc/Collection/QueryDataCollection.phpsrc/Collection/IssueCollection.php
2. Exemple minimal
<?php
declare(strict_types=1);
namespace App\Analyzer;
use AhmedBhs\DoctrineDoctor\Analyzer\AnalyzerInterface;
use AhmedBhs\DoctrineDoctor\Collection\IssueCollection;
use AhmedBhs\DoctrineDoctor\Collection\QueryDataCollection;
use AhmedBhs\DoctrineDoctor\DTO\IssueData;
use AhmedBhs\DoctrineDoctor\Factory\IssueFactoryInterface;
use AhmedBhs\DoctrineDoctor\Factory\SuggestionFactoryInterface;
use AhmedBhs\DoctrineDoctor\ValueObject\IssueCategory;
use AhmedBhs\DoctrineDoctor\ValueObject\IssueType;
use AhmedBhs\DoctrineDoctor\ValueObject\Severity;
final class LargeOffsetAnalyzer implements AnalyzerInterface
{
public function __construct(
private readonly IssueFactoryInterface $issueFactory,
private readonly SuggestionFactoryInterface $suggestionFactory,
private readonly int $offsetThreshold = 10000,
) {
}
public function analyze(QueryDataCollection $queryDataCollection): IssueCollection
{
return IssueCollection::fromGenerator(function () use ($queryDataCollection) {
foreach ($queryDataCollection as $queryData) {
if (!str_contains(strtoupper($queryData->sql), ' OFFSET ')) {
continue;
}
if (!preg_match('/OFFSET\s+(\d+)/i', $queryData->sql, $matches)) {
continue;
}
$offset = (int) $matches[1];
if ($offset < $this->offsetThreshold) {
continue;
}
$issueData = new IssueData(
type: IssueType::PERFORMANCE->value,
title: sprintf('Large OFFSET detected (%d)', $offset),
description: sprintf('Query uses OFFSET %d, which can be expensive on large datasets.', $offset),
severity: Severity::warning(),
category: IssueCategory::performance(),
suggestion: null,
queries: [$queryData->sql],
backtrace: $queryData->backtrace,
data: ['offset' => $offset, 'threshold' => $this->offsetThreshold],
);
yield $this->issueFactory->create($issueData);
}
});
}
}
Notes:
QueryDataexpose des propriétés ($queryData->sql,$queryData->backtrace, etc.)- les sévérités valides sont
critical,warning,info - la catégorie métier correspond aux valeurs d’
IssueCategory(performance,security,integrity,configuration)
3. Enregistrement du service
# config/services.yaml
services:
App\Analyzer\LargeOffsetAnalyzer:
arguments:
$offsetThreshold: 10000
tags:
- { name: 'doctrine_doctor.analyzer' }
4. Configuration utilisateur
Si vous exposez un seuil configurable, ajoutez une clé de config côté bundle puis documentez-la.
Exemple de consommation côté application:
doctrine_doctor:
analyzers:
large_offset:
enabled: true
offset_threshold: 10000
5. Tests recommandés
Ajouter au minimum:
- un test sans violation (aucun issue)
- un test avec violation au-dessus du seuil
- un test au seuil exact
- un test de robustesse (requête inattendue/malformée)
6. Bonnes pratiques
- un analyzer = une responsabilité claire
- éviter les faux positifs bruyants
- produire des messages actionnables
- ajouter des données utiles dans
IssueData::data - garder la logique pure et facilement testable
7. Checklist PR
- Analyzer implémenté
- Service taggé
doctrine_doctor.analyzer - Tests ajoutés
- Documentation mise à jour (
docs/user-guide/analyzers.md,docs/user-guide/execution-modes.md+ exemples si utile) - Changelog mis à jour si nécessaire