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.
Doctrine Doctor implements 90+ specialized analyzers organized into four categories that detect Doctrine ORM anti-patterns and performance issues.
| Severity | Impact | Examples |
|---|---|---|
| Critical | Security/data-loss/severe runtime risk | SQL injection, dangerous cascade |
| Warning | Important performance/integrity/config issues | N+1 patterns, missing indexes |
| Info | Optimization and maintainability recommendations | Naming and design improvements |

Performance analyzers detect patterns that degrade application responsiveness, increase database load, or consume excessive system resources.
Total: 19 analyzers Average Impact: 10-1000x performance improvement when resolved
N x M result multiplicationNote: Some analyzer classes exist in
src/Analyzer/but are not part of the default registered analyzer set.
| Analyzer ID | Detection Method | Typical Impact | Configuration |
|---|---|---|---|
| NPlusOneAnalyzer | Query signature matching | 90-99% query reduction | threshold: 5 |
| MissingIndexAnalyzer | EXPLAIN analysis | 10-1000x speedup | slow_query_threshold: 50 |
| SlowQueryAnalyzer | Execution time | Direct | threshold: 100 (ms) |
| HydrationAnalyzer | Result set size | 50-80% memory reduction | row_threshold: 99 |
| FlushInLoopAnalyzer | Trace analysis | 10-100x | flush_count_threshold: 5 |
| EagerLoadingAnalyzer | JOIN count | Query optimization | join_threshold: 4 |
| LazyLoadingAnalyzer | Proxy initialization | Query reduction | threshold: 10 |
| DTOHydrationAnalyzer | Hydration mode | Memory + performance | — |
| BulkOperationAnalyzer | Entity count | 100-1000x | threshold: 20 |
| QueryCachingOpportunityAnalyzer | Cache statistics | 50-90% reduction | — |
| EntityManagerClearAnalyzer | Memory usage | Memory leak prevention | batch_size_threshold: 20 |
| JoinOptimizationAnalyzer | JOIN complexity | Query simplification | max_joins_recommended: 5, max_joins_critical: 8 |
| CartesianProductAnalyzer | Multi-collection JOIN analysis | Prevent row explosion | n1_collection_threshold: 3 |
| SetMaxResultsWithCollectionJoinAnalyzer | LIMIT + JOIN | Incorrect results | — |
| OrderByWithoutLimitAnalyzer | ORDER BY + full scan | Resource usage | — |
| FindAllAnalyzer | Unfiltered queries | Memory exhaustion | threshold: 99 |
| YearFunctionOptimizationAnalyzer | Function in WHERE | Index usage | — |
| IneffectiveLikeAnalyzer | Leading wildcard | Full table scan | — |
Internal Parser Utilities (not directly user-facing): | SqlAggregationAnalyzer | Aggregation function analysis | Query optimization | Internal | | SqlConditionAnalyzer | WHERE/ON clause analysis | Index effectiveness | Internal | | SqlPerformanceAnalyzer | SQL pattern analysis | Performance insights | Internal |
Security analyzers detect vulnerabilities aligned with OWASP Top 10 and Doctrine-specific attack vectors.
Total: 4 analyzers OWASP Coverage: A02:2021 (Cryptographic Failures), A03:2021 (Injection), A05:2021 (Security Misconfiguration)
rand() in security contextsIntegrity analyzers detect code smells, anti-patterns, and violations of best practices that affect maintainability, readability, and adherence to Doctrine ORM conventions.
Total: 35 analyzers Focus: Type safety, relationship consistency, lifecycle management, naming conventions
Description: Single unified analyzer for all cascade-related issues following Single Responsibility Principle.
Detects:
cascade="all" usage (highest priority - most dangerous)cascade="remove" on independent entities (potential data loss)cascade="persist" on independent entities (wrong aggregate boundaries)Benefits:
Example Violation:
/**
* [@ORM](https://github.com/ORM)\ManyToOne(targetEntity="Tag")
* [@ORM](https://github.com/ORM)\JoinColumn(cascade={"remove"}) // ❌ Tag is independent!
*/
private Tag $tag;
Issue: Deleting article would delete shared tag → data loss
Description: Validates consistency between ORM cascade operations and database foreign key constraints.
Violation Example:
/**
* [@ORM](https://github.com/ORM)\OneToMany(targetEntity="Item", mappedBy="order", cascade={"remove"})
*/
private Collection $items;
// Database: ON DELETE SET NULL (mismatch!)
Issue: ORM expects cascade delete, database sets NULL → inconsistent state
Description: Ensures symmetric mapping in bidirectional relationships.
Violation:
class Order {
/** [@ORM](https://github.com/ORM)\ManyToOne(targetEntity="Customer", inversedBy="orders") */
private Customer $customer;
}
class Customer {
/** [@ORM](https://github.com/ORM)\OneToMany(targetEntity="Order", mappedBy="wrongField") */
// ↑ Should be "customer"
private Collection $orders;
}
| Analyzer | Focus Area | Violation Type | Impact |
|---|---|---|---|
| BidirectionalConsistencyAnalyzer | Relationship symmetry | Mapping error | ORM malfunction |
| CascadeConfigurationAnalyzer | Aggregate consistency | ORM/DB mismatch | Data corruption |
| CascadeAllAnalyzer | Explicit design | Over-automation | Unintended side effects |
| CascadePersistOnIndependentEntityAnalyzer | Aggregate boundaries | Wrong cascade scope | Data integrity |
| CascadeRemoveOnIndependentEntityAnalyzer | Entity independence | Improper deletion | Data loss |
| OrphanRemovalWithoutCascadeRemoveAnalyzer | Lifecycle management | Configuration inconsistency | Memory leak |
| MissingOrphanRemovalOnCompositionAnalyzer | Composition pattern | Missing cleanup | Orphaned records |
| OnDeleteCascadeMismatchAnalyzer | Layer consistency | ORM vs DB conflict | Undefined behavior |
| ForeignKeyMappingAnalyzer | Referential integrity | Primitive FK exposure | Architecture violation |
| TransactionBoundaryAnalyzer | ACID compliance | Transaction scope | Data inconsistency |
| EntityStateConsistencyAnalyzer | UnitOfWork pattern | State management | Sync issues |
| FinalEntityAnalyzer | Proxy compatibility | Non-final entities | Proxy failures |
| EmbeddableMutabilityAnalyzer | Value object | Mutable embeddables | Side effects |
| EmbeddableWithoutValueObjectAnalyzer | Value object pattern | Missing VO semantics | Design smell |
| MissingEmbeddableOpportunityAnalyzer | Cohesion | Scattered value objects | Maintainability |
| DecimalPrecisionAnalyzer | Type system | Precision loss | Financial errors |
| FloatForMoneyAnalyzer | Type system | Floating-point rounding | Calculation errors |
| FloatInMoneyEmbeddableAnalyzer | Value objects | Incorrect money handling | Financial bugs |
| PropertyTypeMismatchAnalyzer | Type safety | PHP↔DB type mismatch | Runtime errors |
| ColumnTypeAnalyzer | Column definitions | Wrong type usage | Data loss |
| CollectionInitializationAnalyzer | Object lifecycle | Uninitialized collections | Null pointer exceptions |
| GetReferenceAnalyzer | Performance | Unnecessary queries | Database overhead |
| PrimaryKeyStrategyAnalyzer | ID generation | Inefficient strategy | Performance issues |
| QueryBuilderBestPracticesAnalyzer | Code quality | Bad QueryBuilder patterns | Maintainability |
| EntityManagerInEntityAnalyzer | Architecture | Dependency injection | Architecture violation |
| TypeHintMismatchAnalyzer | Type safety | Type inconsistency | Runtime errors |
| NamingConventionAnalyzer | Code standards | Naming violations | Readability issues |
createdAt/updatedAt fields are mapped and lifecycle updates are consistentdeletedAt mapping, and query expectations across the apputf8mb4 to avoid truncation and multi-byte character lossutf8mb4_general_ci vs utf8mb4_unicode_ci mismatches"C" collation issues, libc vs ICU differences, FK collation mismatches| Focus Area | Analyzers | Key Recommendations |
|---|---|---|
| Timezone | TimeZoneAnalyzer | Use UTC + DateTimeImmutable |
| Gedmo Traits | 3 analyzers | Proper trait configuration |
| Database Setup | 4 analyzers | UTF8MB4 charset + strict mode + InnoDB |
doctrine_doctor:
enabled: true
profiler:
show_in_toolbar: true
show_debug_info: false
doctrine_doctor:
analyzers:
n_plus_one:
enabled: true
threshold: 5
slow_query:
enabled: true
threshold: 100 # milliseconds
missing_index:
enabled: true
slow_query_threshold: 50
doctrine_doctor:
analyzers:
n_plus_one:
enabled: true
dql_injection:
enabled: true
strict_mode:
enabled: true
Create custom analyzers by implementing AnalyzerInterface (query-based) or MetadataAnalyzerInterface (metadata-based):
// Query-based analyzer
use AhmedBhs\DoctrineDoctor\Analyzer\AnalyzerInterface;
final class CustomQueryAnalyzer implements AnalyzerInterface
{
public function analyze(QueryDataCollection $queries): IssueCollection
{
// Detection logic based on captured SQL queries
}
}
// Metadata-based analyzer
use AhmedBhs\DoctrineDoctor\Analyzer\Concern\MetadataAnalyzerTrait;
use AhmedBhs\DoctrineDoctor\Analyzer\MetadataAnalyzerInterface;
final class CustomMetadataAnalyzer implements MetadataAnalyzerInterface
{
use MetadataAnalyzerTrait;
public function analyzeMetadata(): IssueCollection
{
// Detection logic based on Doctrine metadata or database connection
}
}
services:
App\Analyzer\CustomAnalyzer:
tags:
- { name: 'doctrine_doctor.analyzer' }
[← Back to Main Documentation]({{ site.baseurl }}/) | Configuration →
How can I help you explore Laravel packages today?