| layout | default |
|---|---|
| title | Architecture |
| parent | Advanced |
| nav_order | 1 |
Doctrine Doctor follows a layered architecture pattern with clear separation of concerns:
ββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation Layer β
β (Symfony Web Profiler Integration) β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββββββββββββΌββββββββββββββββββββββββββ
β Application Layer β
β (Data Collectors, Issue Reconstructors) β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββββββββββββΌββββββββββββββββββββββββββ
β Domain Layer β
β (Analyzers, Issues, Suggestions) β
ββββββββββββββββββββββ¬ββββββββββββββββββββββββββ
β
ββββββββββββββββββββββΌββββββββββββββββββββββββββ
β Infrastructure Layer β
β (Doctrine ORM, Database, Templates) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
- Symfony Profiler panel rendering
- Twig template integration
- Visual representation of analysis results
- Query data collection
- Analysis coordination
- Issue aggregation and deduplication
- Service orchestration
- Core analysis algorithms
- Business logic for issue detection
- Suggestion generation
- Domain models (Issue, Suggestion, QueryData)
- Doctrine ORM integration
- Database platform abstraction
- PHP template rendering system
- External service integrations
Each analyzer implements AnalyzerInterface (query-based) or MetadataAnalyzerInterface (metadata-based), enabling runtime composition. This pattern provides:
- Open/Closed Principle compliance - Add new analyzers without modifying existing code
- Easy addition of new analyzers - Simply implement the interface and tag the service
- Independent testing - Each analyzer can be tested in isolation
- Dynamic analyzer selection - Enable/disable analyzers via configuration
Doctrine Doctor uses Symfony's DataCollector + LateDataCollectorInterface, but analysis is currently executed in collect() for worker-mode safety.
- collect() - Capture query data and run analysis
- lateCollect() - Present for interface compatibility (no heavy work here)
- serialize()/unserialize() - Profiler storage lifecycle
Rationale: avoid stale Doctrine objects in persistent runtimes (FrankenPHP/RoadRunner/Swoole).
Each analyzer creates Issue objects with:
- Severity (
critical,warning,info) - Category (Performance, Security, Integrity, Configuration)
- Suggestion (with code examples and descriptions)
- Location (backtrace, query context)
- Metadata (queries, execution time, affected entities)
High-level modules depend on abstractions, not concrete implementations:
- Analyzers depend on
TemplateRendererInterface, not specific renderers - DoctrineDoctorDataCollector depends on
AnalyzerInterface[], not concrete analyzers - Template rendering uses
TemplateRendererInterface(implemented byPhpTemplateRendererorTwigTemplateRenderer)
This allows easy substitution of implementations without changing high-level code.
graph TB
subgraph "Symfony Application"
A[HTTP Request] --> B[Controller]
B --> C[Doctrine ORM]
end
subgraph "Doctrine Bundle"
C --> D[DoctrineDataCollector]
end
subgraph "Doctrine Doctor Bundle"
D --> E[DoctrineDoctorDataCollector]
E --> F[ServiceHolder]
F --> G[analyzeQueriesLazy]
G --> H1[Performance Analyzers]
G --> H2[Security Analyzers]
G --> H3[Integrity Analyzers]
G --> H4[Configuration Analyzers]
H1 --> I[Issues]
H2 --> I
H3 --> I
H4 --> I
I --> J[IssueDeduplicator]
J --> K[Profiler Panel]
end
style A fill:#e1f5ff
style E fill:#fff4e1
style G fill:#ffe1e1
style K fill:#e1ffe1
sequenceDiagram
participant Request
participant Symfony
participant DoctrineDC
participant DoctorDC
participant Analyzers
participant Profiler
Request->>Symfony: HTTP Request
activate Symfony
Symfony->>DoctrineDC: Execute queries
DoctrineDC->>DoctrineDC: Log queries
Symfony->>DoctorDC: collect()
activate DoctorDC
DoctorDC->>DoctrineDC: Get query data
DoctrineDC-->>DoctorDC: Query metadata
DoctorDC->>DoctorDC: Store in $data
deactivate DoctorDC
DoctorDC->>DoctorDC: Run analysis in collect()
loop For each analyzer
DoctorDC->>Analyzers: analyze(QueryDataCollection)
Analyzers->>Analyzers: Detect patterns
Analyzers-->>DoctorDC: Return IssueCollection
end
DoctorDC->>DoctorDC: Deduplicate issues
DoctorDC->>DoctorDC: Calculate statistics
deactivate DoctorDC
Symfony-->>Request: Response sent β
deactivate Symfony
Profiler->>DoctorDC: getData()
DoctorDC-->>Profiler: Analysis results
Profiler->>Profiler: Render UI
/**
* Analyzer Interface - Strategy Pattern (query-based analyzers)
*/
interface AnalyzerInterface
{
public function analyze(QueryDataCollection $queryDataCollection): IssueCollection;
}
/**
* Metadata Analyzer Interface (metadata-based analyzers)
* Extends AnalyzerInterface for backward compatibility.
* Uses MetadataAnalyzerTrait to bridge analyze() -> analyzeMetadata().
*/
interface MetadataAnalyzerInterface extends AnalyzerInterface
{
public function analyzeMetadata(): IssueCollection;
}
/**
* Template Renderer Interface - Dependency Inversion
*/
interface TemplateRendererInterface
{
/**
* Render a template with given context.
*
* @param string $templateName Name/identifier of the template
* @param array<string, mixed> $context Variables to pass to the template
* @throws \RuntimeException If template not found or rendering fails
* @return array{code: string, description: string} Rendered code and description
*/
public function render(string $templateName, array $context): array;
/**
* Check if a template exists.
*/
public function exists(string $templateName): bool;
}
/**
* Issue Interface - Domain Model
*/
interface IssueInterface
{
public function getType(): string;
public function getTitle(): string;
public function getDescription(): string;
public function getSeverity(): Severity;
public function getCategory(): string;
public function getSuggestion(): ?SuggestionInterface;
public function getBacktrace(): ?array;
public function getQueries(): array;
public function getData(): array;
public function toArray(): array;
}βββββββββββββββββββββββββββββββββββ
β DoctrineDoctorDataCollector β
β extends DataCollector β
β implements LateDataCollectorInterface β
βββββββββββββββββββββββββββββββββββ€
β - analyzers: AnalyzerInterface[]β
β - helpers: DataCollectorHelpers β
β - doctrineCollector β
βββββββββββββββββββββββββββββββββββ€
β + collect(Request, Response) β
β + lateCollect() β
β + getData(): array β
β + getName(): string β
ββββββββββββββ¬βββββββββββββββββββββ
β uses
βΌ
βββββββββββββββββββββββββββββββββββ
β DataCollectorHelpers β
βββββββββββββββββββββββββββββββββββ€
β - databaseInfoCollector β
β - issueReconstructor β
β - queryStatsCalculator β
β - dataCollectorLogger β
β - issueDeduplicator β
ββββββββββββββ¬βββββββββββββββββββββ
β coordinates
βΌ
βββββββββββββββββββββββββββββββββββ
β AnalyzerInterface βββββββββββββ
βββββββββββββββββββββββββββββββββββ€ β
β + analyze(QueryDataCollection) β β
β : IssueCollection β β
βββββββββββββββββββββββββββββββββββ β
β³ β
β implements β
βββββββββ΄βββββββββ¬ββββββββββββββββ β
β β β β
ββββββββββ ββββββββββββββββ ββββββββββ β
βN+1 β βSlowQuery β βSecurityβ β
βAnalyzerβ βAnalyzer β βAnalyzerβ β
ββββββββββ ββββββββββββββββ ββββββββββ β
β
uses β
βββββββββββββββββββββββββββββββββββ β
β TemplateRendererInterface βββββββββββββ
βββββββββββββββββββββββββββββββββββ€
β + render(name, ctx): array β
β + exists(name): bool β
βββββββββββββββββββββββββββββββββββ
β³
β implements
βββββββββ΄βββββββββ
β β
ββββββββββββββ ββββββββββββββββ
βPhpTemplate β βTwigTemplate β
βRenderer β βRenderer β
ββββββββββββββ ββββββββββββββββ
Decision: Keep LateDataCollectorInterface contract but execute analysis in collect().
Rationale:
- Worker mode compatibility (no stale EntityManager references post-response)
- Deterministic lifecycle in persistent runtimes
- Profiler data remains available via serialized collector state
Trade-offs:
- Analysis cost happens during collection phase
- Requires careful control of overhead in development
- Still cannot modify response based on analysis (acceptable for dev tool)
Decision: Use native PHP templates instead of solely Twig.
Rationale:
- Flexibility: Full PHP capabilities for complex code generation
- No Twig Dependency: Core functionality works without Twig
- Performance: No template compilation overhead
- Familiarity: Standard PHP syntax for contributors
Implementation:
// Template: left_join_with_not_null.php
<?php ob_start(); ?>
## Issue: LEFT JOIN with IS NOT NULL
Your query uses LEFT JOIN but filters with IS NOT NULL:
```sql
<?= $context->original_query ?>
```
This is contradictory. Use INNER JOIN instead.
<?php
$code = ob_get_clean();
return ['code' => $code, 'description' => 'Suggestion'];Note: Templates use SafeContext for automatic XSS protection. See Template Security Guide for details.
Decision: Each analyzer is stateless and independent.
Rationale:
- Parallel execution potential (future optimization)
- Independent testing and development
- Configurable enable/disable per analyzer
- No inter-analyzer dependencies
Implementation:
- Constructor injection of dependencies only
- No shared mutable state
- Returns
IssueCollectionfor type safety and predictability
Decision: Three-level severity system (critical, warning, info).
Rationale:
- Simple and consistent UI semantics
- Easy sorting/filtering
- Reduced ambiguity between adjacent levels
Criteria:
- Critical: Security vulnerability, data loss risk, severe runtime risk
- Warning: Significant performance/integrity/configuration concern
- Info: Improvement opportunity, non-blocking recommendation
# config/services.yaml
services:
App\Analyzer\CustomBusinessRuleAnalyzer:
arguments:
$threshold: 100
tags:
- { name: 'doctrine_doctor.analyzer' }namespace App\Infrastructure;
use AhmedBhs\DoctrineDoctor\Template\Renderer\TemplateRendererInterface;
class CustomTemplateRenderer implements TemplateRendererInterface
{
public function render(string $templateName, array $context): array
{
// Custom rendering logic (e.g., Markdown, reStructuredText)
}
}services:
App\Infrastructure\CustomTemplateRenderer: ~
AhmedBhs\DoctrineDoctor\Template\Renderer\TemplateRendererInterface:
alias: App\Infrastructure\CustomTemplateRendererExtend DataCollectorHelpers for custom issue processing:
namespace App\Service;
use AhmedBhs\DoctrineDoctor\Collector\DataCollectorHelpers;
class CustomDataCollectorHelpers extends DataCollectorHelpers
{
public function processIssues(array $issues): array
{
// Custom filtering, prioritization, or enrichment
return array_filter($issues, fn($issue) =>
$this->matchesBusinessRules($issue)
);
}
}- Symfony DataCollector Documentation
- Doctrine ORM Architecture
- Clean Architecture Principles
- Design Patterns: Elements of Reusable Object-Oriented Software
[β Back to Main Documentation]({{ site.baseurl }}/) | Configuration Reference β