ahmed-bhs/doctrine-doctor
Doctrine Doctor is a runtime analysis tool for Doctrine ORM integrated into the Symfony Web Profiler. It detects real-world issues like N+1 queries, slow queries, missing indexes, hydration overhead, and injection risks, with actionable backtraces and suggestions.
Guide pratique pour ajouter un analyzer compatible avec l'API actuelle de Doctrine Doctor.
Un analyzer doit:
AnalyzerInterface (query-based) ou MetadataAnalyzerInterface (metadata-based)analyze(QueryDataCollection) ou analyzeMetadata() selon le contratdoctrine_doctor.analyzerLes analyzers metadata (Integrity, Configuration, Security) utilisent MetadataAnalyzerInterface + MetadataAnalyzerTrait pour ne pas recevoir un QueryDataCollection inutile.
Références:
src/Analyzer/AnalyzerInterface.phpsrc/Analyzer/MetadataAnalyzerInterface.phpsrc/Analyzer/Concern/MetadataAnalyzerTrait.phpsrc/Collection/QueryDataCollection.phpsrc/Collection/IssueCollection.php<?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:
QueryData expose des propriétés ($queryData->sql, $queryData->backtrace, etc.)critical, warning, infoIssueCategory (performance, security, integrity, configuration)# config/services.yaml
services:
App\Analyzer\LargeOffsetAnalyzer:
arguments:
$offsetThreshold: 10000
tags:
- { name: 'doctrine_doctor.analyzer' }
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
Ajouter au minimum:
IssueData::datadoctrine_doctor.analyzerdocs/user-guide/analyzers.md + exemples si utile)← Development Setup | [Configuration →]({{ site.baseurl }}/user-guide/configuration)
How can I help you explore Laravel packages today?