This document explains the Doctrine and DBAL versions supported by the bundle, the differences between versions that can break compatibility, and how the bundle handles these differences.
- Supported Versions
- Important Changes Between Versions
- Bundle Compatibility Strategies
- Diagnostic Commands
- Recommendations
- Troubleshooting
- References
- Compatibility Changelog
The bundle is compatible with the following versions:
- Doctrine ORM:
^2.13 || ^3.0 - Doctrine DBAL: Included with ORM (2.x with ORM 2.x, 3.x with ORM 3.x)
- DoctrineBundle:
^2.8 || ^3.0
| Symfony | Doctrine ORM | Doctrine DBAL | DoctrineBundle | Support | Notes |
|---|---|---|---|---|---|
| 6.1+ | 2.13+ | 2.x | 2.8+ | ✅ | Uses reflection-based middleware |
| 6.1+ | 2.13+ | 2.x | 2.17.1 | ✅ | Tested - no YAML config needed |
| 6.1+ | 2.13+ | 2.x | 3.0+ | ✅ | Uses reflection-based middleware |
| 7.0+ | 2.13+ | 2.x | 2.8+ | ✅ | Uses reflection-based middleware |
| 7.0+ | 2.13+ | 2.x | 2.17.1 | ✅ | Tested - no YAML config needed |
| 7.0+ | 3.0+ | 3.x | 3.0+ | ✅ | Uses reflection-based middleware |
| 8.0+ | 3.0+ | 3.x | 3.0+ | ✅ | Uses reflection-based middleware |
Note:
- DoctrineBundle 3.0 is required for Symfony 8.0+.
- All versions use reflection-based middleware application (no YAML configuration required).
- DoctrineBundle 2.17.1 has been tested and confirmed to work with the bundle.
The bundle uses a universal reflection-based approach that works across ALL DoctrineBundle versions (2.x and 3.x).
Why? YAML middleware configuration is NOT reliably available across DoctrineBundle versions:
- Some DoctrineBundle 2.x versions (like 2.17.1) do NOT support
middlewaresin YAML configuration - Some DoctrineBundle 2.x versions (like 2.10.0+) claim to support
yamlMiddleware, but it's not recognized in all environments - DoctrineBundle 3.x completely removed YAML middleware configuration support
Solution: The bundle uses QueryTrackingConnectionSubscriber which applies middleware via reflection at runtime, avoiding all YAML configuration issues.
Supported versions: 2.8.0 - 2.17.1+ (all versions)
Tested versions:
- ✅ 2.17.1 - Does NOT support
middlewaresoryamlMiddlewarein YAML (causes "Unrecognized option" error) - ✅ 2.8.0 - 2.16.x - Some versions may support
middlewares, but it's not reliable
YAML Configuration Issues:
# ❌ This will FAIL in DoctrineBundle 2.17.1:
doctrine:
dbal:
connections:
default:
middlewares: [] # Error: Unrecognized option "middlewares"
yamlMiddleware: [] # Error: Unrecognized option "yamlMiddleware"Error message you'll see:
Unrecognized option "middlewares" under "doctrine.dbal.connections.default".
Available options are: "MultipleActiveResultSets", "application_name", ...
Supported versions: 3.0.0+
Important changes:
- ❌ Does NOT support middleware configuration in YAML
- ❌ Does NOT support
middlewaresoryamlMiddleware - ✅ Requires manual middleware application using reflection
How the bundle handles it: The bundle uses a universal approach that works across all DoctrineBundle versions:
- All versions (2.x and 3.x): Uses
QueryTrackingConnectionSubscriberwhich applies middleware via reflection at runtime - No YAML configuration required: Avoids compatibility issues with YAML middleware options
- Runtime application: Middleware is applied when the connection is first accessed (on
KernelEvents::REQUEST) - Event Subscriber: Uses Symfony's event system with
#[AsEventListener]attribute - Reflection-based: Uses PHP reflection to wrap the DBAL driver with the middleware
How it works:
QueryTrackingConnectionSubscriberlistens toKernelEvents::REQUESTevent- On each request, it attempts to apply middleware to the Doctrine connection
- Uses
QueryTrackingMiddlewareRegistry::applyMiddleware()which:- Gets the connection from the ManagerRegistry
- Uses reflection to access the private
driverproperty - Wraps the driver with
QueryTrackingMiddleware - Handles errors gracefully (retries on subsequent requests if connection isn't ready)
Relevant code:
QueryTrackingConnectionSubscriber- Event subscriber that applies middleware via reflection- Implements
EventSubscriberInterfacewithgetSubscribedEvents()method - Uses
#[AsEventListener]attribute for event registration - Applies middleware on
KernelEvents::REQUESTwith priority 4096
- Implements
QueryTrackingMiddlewareRegistry::applyMiddleware()- Handles the reflection-based middleware application- Tries multiple property names (
driver,_driver,wrappedConnection,_conn) - Handles both DBAL 2.x and 3.x connection structures
- Gracefully handles errors and connection timing issues
- Tries multiple property names (
Benefits of this approach:
- ✅ Works with ALL DoctrineBundle versions (2.8.0+ and 3.0.0+)
- ✅ No YAML configuration required
- ✅ No compatibility issues
- ✅ Automatic retry if connection isn't ready on first request
- ✅ No breaking changes when upgrading DoctrineBundle
Method to get Schema Manager:
$schemaManager = $connection->getSchemaManager();Features:
- Direct
getSchemaManager()method available - Returns
AbstractSchemaManager
Important changes:
- ❌ Does NOT have
getSchemaManager()method - ✅ New method:
createSchemaManager()
How the bundle handles it: The bundle detects which method is available and uses it:
private function getSchemaManager(\Doctrine\DBAL\Connection $connection): \Doctrine\DBAL\Schema\AbstractSchemaManager
{
// DBAL 3.x uses createSchemaManager()
if (method_exists($connection, 'createSchemaManager')) {
return $connection->createSchemaManager();
}
// DBAL 2.x uses getSchemaManager()
if (method_exists($connection, 'getSchemaManager')) {
$getSchemaManager = [$connection, 'getSchemaManager'];
return $getSchemaManager();
}
throw new \RuntimeException('Unable to get schema manager');
}Affected files:
CreateTableCommand::getSchemaManager()CreateRecordsTableCommand::getSchemaManager()TableStatusChecker::getSchemaManager()
Method to get types:
$type = \Doctrine\DBAL\Types\Type::getType('string');Features:
- Static
Type::getType()method available - Returns type instance directly
Important changes:
- ❌ Does NOT have static
Type::getType()method - ✅ New system:
Type::getTypeRegistry()->get()
How the bundle handles it: The bundle tries both methods in order:
// Try DBAL 3.x method first
if (method_exists(\Doctrine\DBAL\Types\Type::class, 'getTypeRegistry')) {
$typeRegistry = \Doctrine\DBAL\Types\Type::getTypeRegistry();
$doctrineType = $typeRegistry->get($type);
} elseif (method_exists(\Doctrine\DBAL\Types\Type::class, 'getType')) {
// DBAL 2.x method
$doctrineType = \Doctrine\DBAL\Types\Type::getType($type);
}Affected files:
CreateTableCommand::getColumnSQLType()CreateRecordsTableCommand::getColumnSQLType()
Available methods on types:
$type = Type::getType('integer');
$typeName = $type->getName(); // Returns 'integer'Important changes:
- ❌ Does NOT have
getName()method on types - ✅ Alternative: Use
getSQLDeclaration()and compare class names
How the bundle handles it:
Instead of using getName(), the bundle uses getSQLDeclaration() and compares class names:
// Instead of: $type->getName()
$typeClass = get_class($type);
$isInteger = str_contains($typeClass, 'IntegerType');Affected files:
CreateTableCommand::getColumnSQLType()CreateRecordsTableCommand::getColumnSQLType()
Method to get table name:
$tableName = $metadata->getTableName(); // Method available
// Or
$tableName = $metadata->table['name']; // Property availableImportant changes:
⚠️ getTableName()may not be available in all versions- ✅
$metadata->table['name']is always available
How the bundle handles it: The bundle checks if the method exists before using it:
$actualTableName = method_exists($metadata, 'getTableName')
? $metadata->getTableName()
: ($metadata->table['name'] ?? $fallbackName);Affected files:
CreateTableCommandCreateRecordsTableCommandTableStatusCheckerTableNameSubscriberRouteDataRecordTableNameSubscriber
Return value of getFieldMapping():
$mapping = $metadata->getFieldMapping('name');
// Returns: ['type' => 'string', 'length' => 255, 'options' => []]
// Type: arrayImportant changes:
- ✅ New return type:
FieldMappingobject instead of array ⚠️ The object can be converted to array using(array)or accessing properties
How the bundle handles it: The bundle converts the result to array if needed:
$fieldMapping = $metadata->getFieldMapping($fieldName);
// Convert to array if object
if (is_object($fieldMapping)) {
$fieldMapping = (array) $fieldMapping;
}
// Or access properties directly
$type = is_object($fieldMapping) ? $fieldMapping->type : $fieldMapping['type'];Affected files:
CreateTableCommand::updateTableSchema()CreateRecordsTableCommand::updateTableSchema()
Return value of getAssociationMapping():
$mapping = $metadata->getAssociationMapping('routeData');
// Returns: ['joinColumns' => [...], 'targetEntity' => '...']
// Type: arrayImportant changes:
- ✅ New return type:
AssociationMappingobject instead of array ⚠️ Similar togetFieldMapping(), requires conversion
How the bundle handles it:
Similar to getFieldMapping(), the bundle handles both types:
$associationMapping = $metadata->getAssociationMapping($fieldName);
// Convert to array if object
if (is_object($associationMapping)) {
$joinColumns = $associationMapping->joinColumns ?? [];
} else {
$joinColumns = $associationMapping['joinColumns'] ?? [];
}Affected files:
CreateRecordsTableCommand::updateTableSchema()
The bundle automatically detects installed versions:
// DoctrineBundle version detection
$version = QueryTrackingMiddlewareRegistry::detectDoctrineBundleVersion();
// Feature verification (NOTE: These methods now always return false)
// The bundle uses reflection-based middleware application for all versions
$supportsYamlMiddleware = QueryTrackingMiddlewareRegistry::supportsYamlMiddleware(); // Always returns false
$supportsYamlMiddlewareConfig = QueryTrackingMiddlewareRegistry::supportsYamlMiddlewareConfig(); // Always returns falseImportant: As of bundle version 0.0.5, these methods always return false because the bundle no longer uses YAML middleware configuration. Instead, it uses reflection-based middleware application via QueryTrackingConnectionSubscriber for all DoctrineBundle versions.
Detection methods:
Composer\InstalledVersions::getVersion()- Preferred method- Reading package's
composer.json - Heuristics based on available methods/classes
The bundle uses method_exists() extensively to verify method availability:
// Example: Schema Manager
if (method_exists($connection, 'createSchemaManager')) {
// DBAL 3.x
return $connection->createSchemaManager();
} elseif (method_exists($connection, 'getSchemaManager')) {
// DBAL 2.x
return $connection->getSchemaManager();
}The bundle provides fallbacks when methods are not available:
// Example: Type Registry
try {
// Try DBAL 3.x
if (method_exists(Type::class, 'getTypeRegistry')) {
$type = Type::getTypeRegistry()->get($typeName);
}
// Fallback to DBAL 2.x
elseif (method_exists(Type::class, 'getType')) {
$type = Type::getType($typeName);
}
} catch (\Exception $e) {
// Fallback to manual mapping
return $this->getFallbackType($typeName);
}In DoctrineBundle 3.x, the bundle uses reflection to apply middleware:
// Access private properties of Connection
$reflection = new \ReflectionClass($connection);
$driverProperty = $reflection->getProperty('driver');
$driverProperty->setAccessible(true);
$originalDriver = $driverProperty->getValue($connection);Relevant files:
QueryTrackingMiddlewareRegistry::applyMiddlewareViaReflection()QueryTrackingConnectionSubscriber
Symptoms:
Unrecognized option "middlewares" under "doctrine.dbal.connections.default"
Cause: Your DoctrineBundle version (like 2.17.1) does not support YAML middleware configuration.
Solution: Update to bundle version 0.0.5 or higher, which uses reflection-based middleware application instead of YAML configuration.
Verification:
composer show nowo-tech/performance-bundle
# Should show version 0.0.5 or higher
php bin/console nowo:performance:diagnose
# Should show "Registration Method: Event Subscriber (Reflection)"Symptoms:
Unrecognized option "yamlMiddleware" under "doctrine.dbal.connections.default"
Cause: Your DoctrineBundle version does not support yamlMiddleware option.
Solution: Same as above - update to bundle version 0.0.5 or higher.
Symptoms: Query tracking is not working, query count is always 0.
Diagnosis:
php bin/console nowo:performance:diagnosePossible causes:
- Bundle is disabled (
nowo_performance.enabled: false) - Query tracking is disabled (
nowo_performance.track_queries: false) - Current environment is not in the tracked environments list
- Connection name mismatch
Solution: Check the diagnose command output and verify your configuration.
The bundle uses two closely related entities for storing performance data:
RouteData: aggregate metrics per route and environment.RouteDataRecord: individual access records for temporal analysis.
RouteData is the main entity used by the dashboard (/performance) to display
per-route metrics. It stores:
- Identification:
envnamehttpMethod
- Aggregate metrics (representative values):
requestTimequeryTimetotalQueriesmemoryUsageaccessCountlastAccessedAt
- Review state:
reviewed,reviewedAt,reviewedByqueriesImproved,timeImproved
- Status codes summary:
statusCodesJSON with counts per HTTP status code
These fields are denormalized aggregates which are updated by
PerformanceMetricsService whenever a request is recorded. They are designed
to make dashboard queries fast (simple SELECT/ORDER BY on routes_data).
RouteDataRecord stores one row per access, and is used for all advanced
temporal analysis:
routeData(FK toRouteData)accessedAtstatusCoderesponseTime
Advanced statistics pages (/performance/access-statistics, heatmaps, hourly
and daily distributions) are computed against RouteDataRecord using
RouteDataRecordRepository. This avoids duplicating logic in the aggregate
entity and keeps the model compatible across DBAL/ORM versions.
If, for any reason, aggregates in RouteData become out of sync with
RouteDataRecord (for example after manual data imports), you can rebuild them:
php bin/console nowo:performance:rebuild-aggregatesOptions:
--env=dev– restrict rebuild to a specific environment.--batch-size=200– control batch size for flushing (default: 200).
For each RouteData, this command recomputes:
accessCount– from the number ofRouteDataRecordrows.lastAccessedAt– from the latestaccessedAt.statusCodes– from the distribution ofstatusCodevalues in records.
The bundle includes a command to diagnose Doctrine configuration:
php bin/console nowo:performance:diagnoseThis command shows:
- Detected DoctrineBundle version
- Method used to register middleware
- Connection status
- Schema Manager information
- Use the latest compatible version: If you're on Symfony 8, use DoctrineBundle 3.x and ORM 3.x
- Test on multiple versions: If your application must support multiple versions, test on all of them
- Check the logs: The bundle logs which method it's using to apply middleware
- Lock versions: Use
composer.jsonto lock specific versions - Monitor logs: The bundle logs compatibility errors
- Use the diagnostic command: Run
nowo:performance:diagnoseafter updating dependencies
Symptoms: Queries are not being tracked
Solution:
- Run
php bin/console nowo:performance:diagnose - Verify DoctrineBundle version
- Check logs to see which method is being used
- If you're on DoctrineBundle 3.x, verify that
QueryTrackingConnectionSubscriberis registered
Symptoms: Table creation commands fail
Solution:
- Verify you're using DBAL 2.x or 3.x
- The bundle should detect automatically, but if it fails, verify that
method_exists()is working correctly
Symptoms: Errors when creating/updating tables
Solution:
- Verify DBAL version
- The bundle should use
getTypeRegistry()in DBAL 3.x automatically - If it persists, verify that the manual fallback is working
- ✅ Initial support for Doctrine ORM 2.13+ and 3.0+
- ✅ Initial support for DoctrineBundle 2.8+ and 3.0+
- ✅ Automatic version detection
- ✅ Automatic middleware application based on version
- ✅ Compatibility with DBAL 2.x and 3.x
- ✅ Handling of metadata differences between ORM 2.x and 3.x