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.
layout: default
Doctrine Doctor uses Symfony's configuration system. All configuration is placed in YAML files under config/packages/.
config/
├── packages/
│ ├── dev/
│ │ └── doctrine_doctor.yaml # Development-specific
│ └── doctrine_doctor.yaml # All environments
Environment-specific (config/packages/dev/)
↓ overrides
Global (config/packages/)
↓ overrides
Bundle defaults (DependencyInjection/Configuration.php)
doctrine_doctor:
# Master switch
enabled: true|false
analysis:
exclude_third_party_entities: true|false
exclude_paths: ['vendor/', 'var/cache/']
# Profiler integration settings
profiler:
show_in_toolbar: true|false
show_debug_info: true|false
debug:
enabled: true|false
internal_logging: true|false
# Individual analyzer settings
analyzers:
analyzer_name:
enabled: true|false
# Analyzer-specific parameters
threshold: <value>
# ... additional parameters
When no configuration file exists, Doctrine Doctor uses these defaults:
doctrine_doctor:
enabled: true
analysis:
exclude_third_party_entities: true
exclude_paths: ['vendor/']
profiler:
show_in_toolbar: true
show_debug_info: false
debug:
enabled: false
internal_logging: false
analyzers:
# Performance
n_plus_one:
enabled: true
threshold: 5
slow_query:
enabled: true
threshold: 100 # milliseconds
missing_index:
enabled: true
slow_query_threshold: 50
min_rows_scanned: 1000
explain_queries: true
# ... (see section 5 for complete list)
doctrine_doctor:
enabled: true
Type: boolean
Default: true
Description: Global switch for the entire bundle. When false, all analyzers are disabled and the profiler panel is hidden.
Use Cases:
Example:
# config/packages/prod/doctrine_doctor.yaml
doctrine_doctor:
enabled: false # Disable in production
doctrine_doctor:
analysis:
exclude_third_party_entities: true
Type: boolean
Default: true
Description: Automatically excludes entities from the vendor/ directory during analysis. This filters out third-party entities from Symfony, Doctrine, FOSUserBundle, and other vendor bundles to provide cleaner, more relevant reports focused on your application code.
How it works:
/vendor/ in file path) - 100% reliableExample: With this enabled (recommended), entities like Symfony\Component\Security\Core\User\User or FOS\UserBundle\Model\User will be excluded from analysis, while your App\Entity\User will be analyzed normally.
When to disable: Only disable if you need to analyze vendor entity mappings or debug third-party bundle configurations.
doctrine_doctor:
analysis:
exclude_paths: ['vendor/', 'var/cache/']
Type: array<string>
Default: ['vendor/']
Description: Excludes DBAL queries whose origin path contains one of these fragments.
doctrine_doctor:
profiler:
show_in_toolbar: true
Type: boolean
Default: true
Description: Controls whether the "Doctrine Doctor" panel appears in the Symfony Profiler toolbar.
doctrine_doctor:
profiler:
show_debug_info: false
Type: boolean
Default: false
Description: Displays internal debugging information (analyzer execution times, memory usage, service instances). For development of Doctrine Doctor itself only.
doctrine_doctor:
debug:
enabled: false
internal_logging: false
Type: boolean
Defaults: false
Description:
debug.enabled: Enables contributor-oriented debug behavior.debug.internal_logging: Enables internal analyzer logs (can add noticeable overhead).To see code location backtraces in Doctrine Doctor issues, enable Doctrine DBAL's backtrace collection:
# config/packages/dev/doctrine.yaml
doctrine:
dbal:
profiling_collect_backtrace: true
Type: boolean
Default: false
Description: Enables collection of stack traces for SQL queries, allowing Doctrine Doctor to show exactly where in your code each issue originates.
Note: This is a Doctrine DBAL setting, not a Doctrine Doctor configuration. Add it to your doctrine.yaml file. Recommended for development environments only (minimal performance overhead ~2-5%).
doctrine_doctor:
analyzers:
n_plus_one:
enabled: true
threshold: 5
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 5 |
Minimum duplicate query occurrences to trigger detection |
Tuning Guide:
threshold: 2 - Catch even minor N+1 issuesthreshold: 5 - Default, good signal-to-noise ratiothreshold: 10 - Only major N+1 problemsdoctrine_doctor:
analyzers:
slow_query:
enabled: true
threshold: 100
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 100 |
Execution time threshold in milliseconds |
Tuning by Environment:
# Local development (fast SSD)
threshold: 20
# Shared development server
threshold: 50
# Staging (production-like hardware)
threshold: 100
doctrine_doctor:
analyzers:
missing_index:
enabled: true
slow_query_threshold: 100
min_rows_scanned: 1000
explain_queries: true
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
slow_query_threshold |
integer | 100 |
Minimum execution time (ms) to trigger EXPLAIN analysis |
min_rows_scanned |
integer | 1000 |
Minimum rows scanned to recommend index |
explain_queries |
boolean | true |
Execute EXPLAIN queries (requires database permissions) |
Database Permission Requirements:
-- MySQL: Ensure user can execute EXPLAIN
GRANT SELECT ON database.* TO 'app_user'@'localhost';
-- PostgreSQL: EXPLAIN requires SELECT permission
-- No additional permissions needed
Performance Considerations:
explain_queries: false - Disable if database user lacks permissions or for performancemin_rows_scanned reduces false positives on small tablesdoctrine_doctor:
analyzers:
hydration:
enabled: true
row_threshold: 99
critical_threshold: 999
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
row_threshold |
integer | 99 |
Rows to trigger warning severity |
critical_threshold |
integer | 999 |
Rows to trigger "Critical" severity |
doctrine_doctor:
analyzers:
flush_in_loop:
enabled: true
flush_count_threshold: 5
time_window_ms: 1000
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
flush_count_threshold |
integer | 5 |
Minimum flush calls to detect loop pattern |
time_window_ms |
integer | 1000 |
Time window (ms) to group flush calls |
doctrine_doctor:
analyzers:
eager_loading:
enabled: true
join_threshold: 4
critical_join_threshold: 7
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
join_threshold |
integer | 4 |
Number of JOINs to trigger info/warning severity |
critical_join_threshold |
integer | 7 |
Number of JOINs to trigger critical severity |
doctrine_doctor:
analyzers:
lazy_loading:
enabled: true
threshold: 10
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 10 |
Minimum lazy load events to trigger detection |
doctrine_doctor:
analyzers:
bulk_operation:
enabled: true
threshold: 20
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 20 |
Entity count to recommend DQL bulk operations |
Recommendation:
// Below threshold: ORM is acceptable
foreach ($entities as $entity) {
$em->remove($entity);
}
$em->flush();
// Above threshold: Use DQL
$qb->delete(Entity::class, 'e')
->where('e.status = :status')
->setParameter('status', 'inactive')
->getQuery()
->execute();
doctrine_doctor:
analyzers:
entity_manager_clear:
enabled: true
batch_size_threshold: 20
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
batch_size_threshold |
integer | 20 |
Entity count to recommend clear() calls |
doctrine_doctor:
analyzers:
join_optimization:
enabled: true
max_joins_recommended: 5
max_joins_critical: 8
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
max_joins_recommended |
integer | 5 |
Recommended maximum JOINs |
max_joins_critical |
integer | 8 |
Critical threshold |
doctrine_doctor:
analyzers:
partial_object:
enabled: true
threshold: 5
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 5 |
Minimum fields loaded to suggest partial objects |
doctrine_doctor:
analyzers:
find_all:
enabled: true
threshold: 99
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 99 |
Row count to flag findAll() usage |
doctrine_doctor:
analyzers:
get_reference:
enabled: true
threshold: 2
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled |
boolean | true |
Enable/disable analyzer |
threshold |
integer | 2 |
Minimum find() calls to recommend getReference() |
All security analyzers use enabled parameter only (no thresholds):
doctrine_doctor:
analyzers:
dql_injection:
enabled: true
sql_injection_in_raw_queries:
enabled: true
sensitive_data_exposure:
enabled: true
insecure_random:
enabled: true
Security Best Practice: Keep all security analyzers enabled at all times.
Integrity analyzers typically have no configurable parameters beyond enabled:
doctrine_doctor:
analyzers:
cascade_configuration:
enabled: true
cascade_all:
enabled: true
bidirectional_consistency:
enabled: true
orphan_removal_without_cascade_remove:
enabled: true
# ... (see full list in user-guide/analyzers)
doctrine_doctor:
analyzers:
strict_mode:
enabled: true
charset:
enabled: true
inno_db_engine:
enabled: true
connection_pooling:
enabled: true
doctrine_cache:
enabled: true
Note: Some configuration analyzers are active but currently do not expose dedicated analyzers.<key> toggles in the configuration tree.
doctrine_doctor:
analyzers:
naming_convention:
enabled: true
collection_initialization:
enabled: true
missing_embeddable_opportunity:
enabled: true
blameable_trait:
enabled: true
doctrine_doctor:
analyzers:
slow_query:
threshold: 10 # Fast local database
n_plus_one:
threshold: 2 # Strict detection
missing_index:
min_rows_scanned: 500 # Detect early
doctrine_doctor:
analyzers:
slow_query:
threshold: 50
n_plus_one:
threshold: 3
missing_index:
min_rows_scanned: 1000
doctrine_doctor:
analyzers:
slow_query:
threshold: 100
n_plus_one:
threshold: 5 # Reduce noise
missing_index:
min_rows_scanned: 5000 # Production-scale data
⚠️ Note: Group-based configuration (
groups:node) is not yet implemented.To enable/disable specific analyzers, use individual analyzer configuration:
doctrine_doctor: analyzers: n_plus_one: enabled: false # Disable specific analyzer slow_query: enabled: true # Keep enabledThis feature is planned for a future release to allow bulk enable/disable by category.
# config/packages/dev/doctrine_doctor.yaml
doctrine_doctor:
enabled: true
profiler:
show_in_toolbar: true
show_debug_info: true # Show analyzer performance metrics
analyzers:
n_plus_one:
threshold: 2 # Strict
slow_query:
threshold: 20 # Fast local DB
missing_index:
explain_queries: true
# config/packages/test/doctrine_doctor.yaml
doctrine_doctor:
enabled: false # Disable to avoid test overhead
# config/packages/prod/doctrine_doctor.yaml
doctrine_doctor:
enabled: false # MUST be disabled in production
Critical: Doctrine Doctor should NEVER run in production. The bundle is excluded from production via:
composer require --devenabled: false in prod configuration# config/services.yaml
services:
App\Analyzer\CustomBusinessRuleAnalyzer:
arguments:
$threshold: 50
$templateRenderer: '[@AhmedBhs](https://github.com/AhmedBhs)\DoctrineDoctor\Template\Renderer\TemplateRendererInterface'
tags:
- { name: 'doctrine_doctor.analyzer' }
services:
App\Infrastructure\MarkdownTemplateRenderer:
arguments:
$templateDirectory: '%kernel.project_dir%/templates/doctrine_doctor'
# Override default renderer
AhmedBhs\DoctrineDoctor\Template\Renderer\TemplateRendererInterface:
alias: App\Infrastructure\MarkdownTemplateRenderer
services:
App\Decorator\EnhancedIssueFactory:
decorates: AhmedBhs\DoctrineDoctor\Factory\IssueFactoryInterface
arguments:
$inner: '@.inner'
$logger: '[@logger](https://github.com/logger)'
# config/packages/doctrine_doctor.yaml
doctrine_doctor:
enabled: '%env(bool:DOCTRINE_DOCTOR_ENABLED)%'
analyzers:
slow_query:
threshold: '%env(int:SLOW_QUERY_THRESHOLD)%'
# .env.local
DOCTRINE_DOCTOR_ENABLED=true
SLOW_QUERY_THRESHOLD=30
# Check YAML syntax
php bin/console lint:yaml config/packages/doctrine_doctor.yaml
# Validate container configuration
php bin/console debug:config doctrine_doctor
# Dump complete merged configuration
php bin/console debug:config doctrine_doctor --resolve-env
# 📢 Wrong
doctrine_doctor:
analyzers:
slow_query:
threshold: "50" # String instead of integer
# Correct
doctrine_doctor:
analyzers:
slow_query:
threshold: 50
# 📢 Wrong (typo)
doctrine_doctor:
analyzers:
n_pluss_one: # Typo
enabled: true
# Correct
doctrine_doctor:
analyzers:
n_plus_one:
enabled: true
# 📢 Incomplete
doctrine_doctor:
analyzers:
missing_index:
enabled: true
# Optional overrides omitted (defaults are used)
# Complete
doctrine_doctor:
analyzers:
missing_index:
enabled: true
slow_query_threshold: 50
min_rows_scanned: 1000
explain_queries: true
# config/packages/dev/doctrine_doctor.yaml
doctrine_doctor:
enabled: true
doctrine_doctor:
analyzers:
n_plus_one:
threshold: 2
slow_query:
threshold: 25
missing_index:
slow_query_threshold: 50
min_rows_scanned: 500
doctrine_doctor:
# Enable only security analyzers individually
analyzers:
dql_injection:
enabled: true
sql_injection_in_raw_queries:
enabled: true
sensitive_data_exposure:
enabled: true
# Disable performance analyzers
n_plus_one:
enabled: false
slow_query:
enabled: false
doctrine_doctor:
enabled: true
profiler:
show_in_toolbar: true
show_debug_info: false
analyzers:
n_plus_one:
enabled: true
threshold: 5
slow_query:
enabled: true
threshold: 100
missing_index:
enabled: true
slow_query_threshold: 50
min_rows_scanned: 1000
explain_queries: true
hydration:
enabled: true
row_threshold: 99
critical_threshold: 999
flush_in_loop:
enabled: true
flush_count_threshold: 5
time_window_ms: 1000
eager_loading:
enabled: true
join_threshold: 4
critical_join_threshold: 7
lazy_loading:
enabled: true
threshold: 10
bulk_operation:
enabled: true
threshold: 20
entity_manager_clear:
enabled: true
batch_size_threshold: 20
join_optimization:
enabled: true
max_joins_recommended: 5
max_joins_critical: 8
partial_object:
enabled: true
threshold: 5
find_all:
enabled: true
threshold: 99
get_reference:
enabled: true
threshold: 2
# Security (no thresholds)
dql_injection:
enabled: true
sql_injection_in_raw_queries:
enabled: true
sensitive_data_exposure:
enabled: true
insecure_random:
enabled: true
# Integrity (no thresholds)
cascade_configuration:
enabled: true
cascade_all:
enabled: true
bidirectional_consistency:
enabled: true
# Configuration (no thresholds)
strict_mode:...
How can I help you explore Laravel packages today?