ahmed-bhs/doctrine-doctor
Doctrine Doctor is a runtime analysis tool for Doctrine ORM that plugs into the Symfony Web Profiler. It ships 90+ analyzers to catch N+1 queries, missing indexes, slow queries, injection risks, and other performance, security, and integrity issues.
SuggestionMetadata carries the Severity, but render() only ever receives the context array, so every template hardcoded its own alert-danger / alert-warning / alert-info class. FlushInLoopAnalyzer shows the cost most clearly: it picks critical, warning or info from the flush count, while the template always drew the critical red banner, so three flushes in a loop looked exactly as severe as sixty. SuggestionFactory now seeds the context with the metadata severity, leaving an explicit value untouched, and a severityAlertClass() helper maps it to the alert class. Applied to the twelve templates whose banner contradicted the severity they were rendered with.Performance/index raised a TypeError as soon as any key was absent, and others rendered placeholder text such as Rename table from '' to ''. Every context read now has an explicit default and a cast, and Integrity/code_suggestion no longer returns an empty description.$e(), but ucfirst($fieldName), strtolower() and substr() were echoed raw in 20 templates — 88 occurrences. These values come from Doctrine metadata rather than request input, so this was not exploitable, but the escaping was structurally absent rather than deliberately skipped.<ul><li> markup by a markdown conversion, 46 templates carried emoji in the profiler output, and 54 opened documentation links with target="_blank" and no rel.src/Template/helpers.php now own it, and the naming_convention cluster is rewritten on top of them as the worked example, from 165 lines to 53. Visible text is escaped with ENT_NOQUOTES so apostrophes stay readable in the panel, while attributes keep full quote escaping. Rendered output of all 113 templates was snapshotted before the change and compared after: no drift in rendered text or descriptions, and no CSS class or HTML tag lost.src/Template/Suggestions/ is no longer excluded from ECS and PHPStan. The exclusion was documented as covering "template files with PHP interpolation", but the templates pass both, level 8 included. Two test classes and five validator checks now guard the directory: every template renders with an empty context without raising, the banner follows the severity at all three levels, the layout helpers escape markup, and the validator rejects emoji, missing rel attributes, context destructuring without defaults, and echoing the result of a function that is not a known escaper.EXAMPLE_SECURE_TEMPLATE shipped as documentation for the SafeContext auto-escaping API but was never referenced by any analyzer, factory or test, while still being picked up as a renderable template. docs/advanced/template-security.md covers the same ground, including the same example.setMaxResults() with Collection Join alert now carries the facts of the query that triggered it instead of a fixed block of theory: the LIMIT value and the number of fetch-joined tables, the row count when the profiler reports one, and an explicit note when a result set reached its LIMIT and was therefore very likely truncated in the middle of a root entity. When the query is offset past the first page, the alert also warns that a batch loop bounded by a root entity count can exit early and never read the remaining entities — whole entities lost, not just collection items. Both remediations are given, paginating on identifiers and Paginator, with the trade-off between them. Every enrichment degrades to the base message when no row count is available, so the alert stays complete either way. Detection logic is unchanged.composer.lock is not tracked, so every run resolves afresh, and CVE-2026-53965 (reported 2026-08-14) covers mcp/sdk >=0.5.0,<0.7.1. symfony/ai-symfony-mate-extension was pinned to ^0.10, whose ai-mate v0.10 requires mcp/sdk ^0.6 exclusively, leaving no resolvable set. The constraint now allows ^0.11|^0.12, which accept mcp/sdk ^0.6|^0.7 and reach the patched v0.7.1.phpmd/phpmd is required as 3.x-dev, and PHPMD 3 applies ShortVariable to closure parameters where 2.15 did not. Renamed rather than widening the ruleset exceptions.case 'not_snake_case' in the naming convention test's severity switch was unreachable dead code, reported by newer PHPStan as switch.duplicateCase. Removed; behaviour is unchanged.hasOffset() reported an offset for every LIMIT query. The SQL parser exposes an offset of 0 for a bare LIMIT, and the guard tested it with '' !== (string) $offset, which holds for "0"; the standalone-OFFSET regex fallback matched OFFSET 0 for the same reason. It now compares against 0 and ignores a zero offset, so it answers whether the query is actually past the first page. PaginationWithoutOrderByAnalyzer derives its severity from this call, so a plain LIMIT without ORDER BY was reported as LIMIT/OFFSET pagination at warning instead of LIMIT at info; both cases are now reported correctly. OrderByWithoutLimitAnalyzer is unaffected. The method had no test coverage; a case per pagination form was added, including MySQL comma syntax and explicit zero offsets.Export JSON button on the Doctrine Doctor panel heading downloads the current report (issues, stats, and grouped query timings) as a .json file. The payload is rendered into the panel and saved client-side via a Blob, so the feature needs no route and no routing configuration in the host application. Query entries are whitelisted field by field to drop the non-serializable firstQuery payload (backtrace, connection). Contributed by [@makomweb](https://github.com/makomweb) in #120.JSON_HEX_TAG, escaping < and >. Without it, a captured SQL string containing </script> could close the panel's <script> block early, and the <!--<script sequence could flip the HTML parser into double-escaped state where a later </script> no longer closes the element — both turning collected queries into executable markup in the profiler page.OrderByNullableLeadingColumnAnalyzer (Performance/Integrity): flags ORDER BY on a nullable leading column combined with LIMIT. NULL placement (first or last) is platform- and configuration-dependent, so a query like ORDER BY requested_at LIMIT 1 can silently skip rows whose sort column is NULL depending on the database engine. Info severity, flag-only, never auto-fixed since NULL-first/last is sometimes intentional. Configurable via doctrine_doctor.analyzers.order_by_nullable_leading_column.enabled.FlushInLoopAnalyzer / FlushInLoopAnalyzerModern: both were tagged doctrine_doctor.analyzer via the same glob registration, producing duplicate/conflicting findings for the same flush-in-loop pattern. FlushInLoopAnalyzerModern is now excluded from the tag glob while staying registered and autowireable.GetReferenceAnalyzer: no longer flags Doctrine's own optimistic-lock version-check re-reads (SELECT version FROM table WHERE id = ?) or PHP 8.4 native lazy-ghost object initialization as find()-instead-of-getReference() candidates. Added ProxyFactory::createLazyInitializer / EntityPersister::loadById to the lazy-loading backtrace markers alongside the legacy Proxy::__load / __CG__:: ones.DoctrineDoctorDataCollector: expensive runAnalysis() work now runs in lateCollect() (after the HTTP response is sent) instead of collect() (before), on runtimes where this is safe. Auto-detected via function_exists('fastcgi_finish_request') so it works correctly on both php-fpm and persistent/worker-mode runtimes (FrankenPHP/RoadRunner/Swoole). No config flag needed.CollectionJoinDetector::isForeignKeyInJoinedTable(): compared SQL column names against getIdentifierFieldNames() (PHP field paths) instead of getIdentifierColumnNames() (actual DB column names). Broke for entities with embedded value-object identifiers, causing ManyToOne joins to fall through to an overly-broad table-wide fallback and get misclassified as collection joins.UnusedEagerLoadAnalyzer: carried its own copy of the now-fixed CollectionJoinDetector join-classification logic instead of reusing the shared helper, so it suffered the same value-object-identifier misclassification independently. Deduplicated to delegate to CollectionJoinDetector.DeepOffsetPaginationAnalyzer (Performance): detects deep OFFSET pagination in executed SQL — OFFSET >= 1000 is flagged as a warning, OFFSET >= 10000 as critical. Cost grows linearly with page depth because the database must read and discard every skipped row. Suggests keyset (seek) pagination via WHERE id > :lastId ORDER BY id LIMIT N or an application-level cap on the maximum offset. Handles both LIMIT n OFFSET m and MySQL LIMIT m, n forms. Thresholds are configurable via doctrine_doctor.analyzers.deep_offset_pagination.offset_warning_threshold and offset_critical_threshold.PaginationWithoutOrderByAnalyzer (Performance): detects LIMIT/OFFSET pagination without an ORDER BY clause. SQL does not guarantee row order without an explicit ORDER BY, so identical executions can return different rows and consecutive pages can return duplicates or skip rows. Suggests adding a deterministic ORDER BY on a stable, indexed column (typically the primary key) and a PK tiebreaker when sorting by a non-unique column. Skips LIMIT 1 single-row fetches where ordering is irrelevant.FunctionOnPredicateColumnAnalyzer (Performance): detects non-sargable functions wrapping a column in WHERE (LOWER, UPPER, COALESCE, IFNULL, ISNULL, NULLIF, CAST, CONVERT, TRIM, LTRIM, RTRIM, SUBSTRING, SUBSTR, CONCAT, ABS, ROUND, FLOOR, CEIL, CEILING). Wrapping a filtered column in a function defeats any standard index on that column and forces a full scan with per-row evaluation. Suggests rewriting the predicate so the column appears bare on one side, normalizing values at write time, or creating a functional/expression index. Date functions remain handled by YearFunctionOptimizationAnalyzer. Threshold configurable via doctrine_doctor.analyzers.function_on_predicate_column.min_execution_time_ms (default 10ms).NotInSubqueryAnalyzer (Performance): detects <column> NOT IN (SELECT ...) patterns that silently return zero rows whenever the subquery yields any NULL value, due to SQL three-valued logic (x NOT IN (a, b, NULL) evaluates to UNKNOWN, never TRUE). A frequent source of bugs that pass in tests but break in production. Suggests NOT EXISTS, LEFT JOIN ... IS NULL, or an explicit IS NOT NULL filter inside the subquery.ImplicitTypeConversionAnalyzer (Performance): detects predicates likely to trigger implicit type conversion in the engine — numeric columns compared to quoted string literals (e.g. user_id = '42') or date/time columns compared to bare integer literals. Implicit conversion typically disables index usage on the column. Heuristics rely on column-name suffixes (_id, _count, _amount, _at, _date, _time, ...) and skip placeholders (?, :param) since the bound type is invisible from SQL text alone. Suggests binding parameters with the correct PHP type or explicit DBAL Types::*.SQLInjectionInRawQueriesAnalyzer: extended to detect tautology variants (OR 1=1, OR '1'='1', OR TRUE) and UNION SELECT injection patterns; the LIMIT/OFFSET vector is now also analyzed for concatenated injection attempts that previously slipped past the WHERE-only scan.doctrine/orm installed. A new RemoveOrmServicesPass compiler pass removes ORM-dependent services (the EntityManager decorator, EntityMetadataProvider, and every analyzer with an EntityManagerInterface dependency) when doctrine.orm.entity_manager is absent, so the container compiles cleanly and only DBAL-native analyzers stay active.NPlusOneSqlAnalyzer: pure-SQL N+1 detection that groups identical SELECT patterns from QueryDataCollection and flags any group above the threshold (default 3), without needing ORM metadata. Required so DBAL-only applications get N+1 coverage equivalent to the existing ORM-aware analyzer.MissingTransactionOnBatchAnalyzer: walks the query timeline tracking transaction state via START/BEGIN/SAVEPOINT and COMMIT/ROLLBACK/RELEASE SAVEPOINT markers, and flags N >= threshold (default 10) INSERT/UPDATE/DELETE statements executed outside any transaction. Wrapping them in a single transaction collapses N fsyncs into one for a 10-100x speedup on durable storage.YearFunctionOptimizationAnalyzer: now also detects SQLite strftime('%Y'|'%m'|'%d'|'%H'|'%M'|'%S', col) and standard SQL EXTRACT(part FROM col) patterns in WHERE clauses, mapping them back to the existing YEAR/MONTH/... reasoning. Previously the analyzer was MySQL-only.doctrine_doctor: added a profiler collector formatter, an MCP tool (doctrine-doctor-issues), and a sanitization layer for issue hints, traces, and SQL snippets so profiler findings can be consumed safely by AI agents when symfony/ai-symfony-mate-extension is installed in the host Symfony application.QueryCachingOpportunityAnalyzer: added Doctrine 2LC Opportunity detection for repeated fast SELECT entity-load patterns with varying parameter sets, plus a dedicated suggestion template and configuration thresholds for second-level cache candidates.doctrine/orm moved from require to require-dev + suggest. Installing ahmed-bhs/doctrine-doctor no longer pulls doctrine/orm transitively. Projects that depended on this transitive install must add doctrine/orm to their own composer.json. ORM-specific analyzers (N+1 via metadata, eager-loading mapping, partial objects, etc.) are silently disabled when doctrine/orm is absent.RemoveOrmServicesPass: when the host application does not configure doctrine.orm.entity_manager, also removes PartialObjectAnalyzer, whose SELECT PARTIAL u.{...} recommendation is unusable in pure DBAL and surfaced as a false positive on SELECT * queries.
TransactionBoundaryAnalyzer: now recognizes Doctrine-quoted "START TRANSACTION" / "COMMIT" markers and treats SAVEPOINT/RELEASE SAVEPOINT as begin/commit so nested- and unclosed-transaction detection works in DBAL applications that call $conn->beginTransaction() directly.
EntityManagerClearAnalyzer: now inspects query backtraces and only flags sequential INSERT/UPDATE/DELETE operations when at least one frame originates from the Doctrine ORM (EntityManager, UnitOfWork, EntityRepository, ServiceEntityRepository). Pure-DBAL batches no longer get a misleading EntityManager::clear() recommendation.
JoinTypeConsistencyAnalyzer / UnusedEagerLoadAnalyzer: only fire on aggregations / many-JOIN queries when the FROM table is mapped in the ORM metadata. This silences JOIN type may cause incorrect results and Unused eager loading alerts on pure-DBAL queries over tables that are not ORM-managed.
SQLInjectionInRawQueriesAnalyzer / InjectionPatternDetector / QueryBuilderPatternDetector: whitelist short clean LIKE / WHERE literal values (<=64 chars, no SQL meta-tokens) so hardcoded filter values like WHERE country = 'FR' or LIKE '%abc%' are no longer reported as SQL injection. Real concatenation attacks (literals containing OR/AND/UNION/SELECT/DROP/--/;, or suspiciously long values) still trigger the issue.
FindAllAnalyzer: skips queries with GROUP BY/HAVING clauses and aggregate functions (COUNT, SUM, AVG, MIN, MAX) in the SELECT list - these are analytic queries by design, not unrestricted findAll() patterns.
DoctrineCacheAnalyzer: the YAML-based scan now detects missing metadata_cache_driver, query_cache_driver, and result_cache_driver keys inside an existing when@prod section, not only the explicit type: array case. Previously, a when@prod block that omitted these keys entirely was silently ignored, causing the critical performance issue (entity metadata reparsed on every request, -50 to -80%) to go unreported in dev. The absence of when@prod altogether is still not flagged to avoid false positives on projects using split config/packages/prod/ files.
Added missing_cache_production.php suggestion template used for the new "not configured" issues, distinct from array_cache_production.php which covers the explicit array cache case.
OrderByWithoutLimitAnalyzer: fast queries (below min_execution_time_ms) with a FK equality predicate in the WHERE clause (e.g. WHERE deposit_request_id = ?) are now silently skipped. These are aggregate-child collections whose size is bounded by the parent entity lifecycle, not by data volume. If the query ever degrades (execution time exceeds the threshold), the alert re-enables automatically.
FinalEntityAnalyzer: added early-return guard when enable_native_lazy_objects (PHP 8.4 ghost objects) is active. Ghost objects decorate rather than subclass the entity, so final classes are safe — the previous behavior produced false-positive CRITICAL issues on PHP 8.4 projects.
QueryData serialization: bound parameters flagged as sensitive (passwords, tokens, API keys, secrets, etc.) are now redacted before the QueryData DTO is serialized into the profiler payload, preventing credential leakage through the Symfony Web Profiler cache and any downstream MCP/AI integrations that consume profiler data.PhpTemplateRenderer: template names are now restricted to a strict allowlist regex, and the resolved template path is confined to the bundle's Template/Suggestions/ directory via realpath() comparison. Closes a path-traversal vector where a malicious template name (e.g. ../../etc/passwd) could escape the suggestions directory.DeepOffsetPaginationAnalyzer (Performance): detects deep OFFSET pagination in executed SQL — OFFSET >= 1000 is flagged as a warning, OFFSET >= 10000 as critical. Cost grows linearly with page depth because the database must read and discard every skipped row. Suggests keyset (seek) pagination via WHERE id > :lastId ORDER BY id LIMIT N or an application-level cap on the maximum offset. Handles both LIMIT n OFFSET m and MySQL LIMIT m, n forms. Thresholds are configurable via doctrine_doctor.analyzers.deep_offset_pagination.offset_warning_threshold and offset_critical_threshold.PaginationWithoutOrderByAnalyzer (Performance): detects LIMIT/OFFSET pagination without an ORDER BY clause. SQL does not guarantee row order without an explicit ORDER BY, so identical executions can return different rows and consecutive pages can return duplicates or skip rows. Suggests adding a deterministic ORDER BY on a stable, indexed column (typically the primary key) and a PK tiebreaker when sorting by a non-unique column. Skips LIMIT 1 single-row fetches where ordering is irrelevant.FunctionOnPredicateColumnAnalyzer (Performance): detects non-sargable functions wrapping a column in WHERE (LOWER, UPPER, COALESCE, IFNULL, ISNULL, NULLIF, CAST, CONVERT, TRIM, LTRIM, RTRIM, SUBSTRING, SUBSTR, CONCAT, ABS, ROUND, FLOOR, CEIL, CEILING). Wrapping a filtered column in a function defeats any standard index on that column and forces a full scan with per-row evaluation. Suggests rewriting the predicate so the column appears bare on one side, normalizing values at write time, or creating a functional/expression index. Date functions remain handled by YearFunctionOptimizationAnalyzer. Threshold configurable via doctrine_doctor.analyzers.function_on_predicate_column.min_execution_time_ms (default 10ms).NotInSubqueryAnalyzer (Performance): detects <column> NOT IN (SELECT ...) patterns that silently return zero rows whenever the subquery yields any NULL value, due to SQL three-valued logic (x NOT IN (a, b, NULL) evaluates to UNKNOWN, never TRUE). A frequent source of bugs that pass in tests but break in production. Suggests NOT EXISTS, LEFT JOIN ... IS NULL, or an explicit IS NOT NULL filter inside the subquery.ImplicitTypeConversionAnalyzer (Performance): detects predicates likely to trigger implicit type conversion in the engine — numeric columns compared to quoted string literals (e.g. user_id = '42') or date/time columns compared to bare integer literals. Implicit conversion typically disables index usage on the column. Heuristics rely on column-name suffixes (_id, _count, _amount, _at, _date, _time, ...) and skip placeholders (?, :param) since the bound type is invisible from SQL text alone. Suggests binding parameters with the correct PHP type or explicit DBAL Types::*.SQLInjectionInRawQueriesAnalyzer: extended to detect tautology variants (OR 1=1, OR '1'='1', OR TRUE) and UNION SELECT injection patterns; the LIMIT/OFFSET vector is now also analyzed for concatenated injection attempts that previously slipped past the WHERE-only scan.doctrine/orm installed. A new RemoveOrmServicesPass compiler pass removes ORM-dependent services (the EntityManager decorator, EntityMetadataProvider, and every analyzer with an EntityManagerInterface dependency) when doctrine.orm.entity_manager is absent, so the container compiles cleanly and only DBAL-native analyzers stay active.NPlusOneSqlAnalyzer: pure-SQL N+1 detection that groups identical SELECT patterns from QueryDataCollection and flags any group above the threshold (default 3), without needing ORM metadata. Required so DBAL-only applications get N+1 coverage equivalent to the existing ORM-aware analyzer.MissingTransactionOnBatchAnalyzer: walks the query timeline tracking transaction state via START/BEGIN/SAVEPOINT and COMMIT/ROLLBACK/RELEASE SAVEPOINT markers, and flags N >= threshold (default 10) INSERT/UPDATE/DELETE statements executed outside any transaction. Wrapping them in a single transaction collapses N fsyncs into one for a 10-100x speedup on durable storage.YearFunctionOptimizationAnalyzer: now also detects SQLite strftime('%Y'|'%m'|'%d'|'%H'|'%M'|'%S', col) and standard SQL EXTRACT(part FROM col) patterns in WHERE clauses, mapping them back to the existing YEAR/MONTH/... reasoning. Previously the analyzer was MySQL-only.doctrine_doctor: added a profiler collector formatter, an MCP tool (doctrine-doctor-issues), and a sanitization layer for issue hints, traces, and SQL snippets so profiler findings can be consumed safely by AI agents when symfony/ai-symfony-mate-extension is installed in the host Symfony application.QueryCachingOpportunityAnalyzer: added Doctrine 2LC Opportunity detection for repeated fast SELECT entity-load patterns with varying parameter sets, plus a dedicated suggestion template and configuration thresholds for second-level cache candidates.doctrine/orm moved from require to require-dev + suggest. Installing ahmed-bhs/doctrine-doctor no longer pulls doctrine/orm transitively. Projects that depended on this transitive install must add doctrine/orm to their own composer.json. ORM-specific analyzers (N+1 via metadata, eager-loading mapping, partial objects, etc.) are silently disabled when doctrine/orm is absent.RemoveOrmServicesPass: when the host application does not configure doctrine.orm.entity_manager, also removes PartialObjectAnalyzer, whose SELECT PARTIAL u.{...} recommendation is unusable in pure DBAL and surfaced as a false positive on SELECT * queries.
TransactionBoundaryAnalyzer: now recognizes Doctrine-quoted "START TRANSACTION" / "COMMIT" markers and treats SAVEPOINT/RELEASE SAVEPOINT as begin/commit so nested- and unclosed-transaction detection works in DBAL applications that call $conn->beginTransaction() directly.
EntityManagerClearAnalyzer: now inspects query backtraces and only flags sequential INSERT/UPDATE/DELETE operations when at least one frame originates from the Doctrine ORM (EntityManager, UnitOfWork, EntityRepository, ServiceEntityRepository). Pure-DBAL batches no longer get a misleading EntityManager::clear() recommendation.
JoinTypeConsistencyAnalyzer / UnusedEagerLoadAnalyzer: only fire on aggregations / many-JOIN queries when the FROM table is mapped in the ORM metadata. This silences JOIN type may cause incorrect results and Unused eager loading alerts on pure-DBAL queries over tables that are not ORM-managed.
SQLInjectionInRawQueriesAnalyzer / InjectionPatternDetector / QueryBuilderPatternDetector: whitelist short clean LIKE / WHERE literal values (<=64 chars, no SQL meta-tokens) so hardcoded filter values like WHERE country = 'FR' or LIKE '%abc%' are no longer reported as SQL injection. Real concatenation attacks (literals containing OR/AND/UNION/SELECT/DROP/--/;, or suspiciously long values) still trigger the issue.
FindAllAnalyzer: skips queries with GROUP BY/HAVING clauses and aggregate functions (COUNT, SUM, AVG, MIN, MAX) in the SELECT list - these are analytic queries by design, not unrestricted findAll() patterns.
DoctrineCacheAnalyzer: the YAML-based scan now detects missing metadata_cache_driver, query_cache_driver, and result_cache_driver keys inside an existing when@prod section, not only the explicit type: array case. Previously, a when@prod block that omitted these keys entirely was silently ignored, causing the critical performance issue (entity metadata reparsed on every request, -50 to -80%) to go unreported in dev. The absence of when@prod altogether is still not flagged to avoid false positives on projects using split config/packages/prod/ files.
Added missing_cache_production.php suggestion template used for the new "not configured" issues, distinct from array_cache_production.php which covers the explicit array cache case.
OrderByWithoutLimitAnalyzer: fast queries (below min_execution_time_ms) with a FK equality predicate in the WHERE clause (e.g. WHERE deposit_request_id = ?) are now silently skipped. These are aggregate-child collections whose size is bounded by the parent entity lifecycle, not by data volume. If the query ever degrades (execution time exceeds the threshold), the alert re-enables automatically.
FinalEntityAnalyzer: added early-return guard when enable_native_lazy_objects (PHP 8.4 ghost objects) is active. Ghost objects decorate rather than subclass the entity, so final classes are safe — the previous behavior produced false-positive CRITICAL issues on PHP 8.4 projects.
QueryData serialization: bound parameters flagged as sensitive (passwords, tokens, API keys, secrets, etc.) are now redacted before the QueryData DTO is serialized into the profiler payload, preventing credential leakage through the Symfony Web Profiler cache and any downstream MCP/AI integrations that consume profiler data.PhpTemplateRenderer: template names are now restricted to a strict allowlist regex, and the resolved template path is confined to the bundle's Template/Suggestions/ directory via realpath() comparison. Closes a path-traversal vector where a malicious template name (e.g. ../../etc/passwd) could escape the suggestions directory.ColumnTypeAnalyzer: added configurable excluded_fields list (default: mimeType, contentType, mediaType, fileType) to prevent false enum opportunity alerts on MIME-type fields that match enum patterns but are not enums.OrderByWithoutLimitAnalyzer: added configurable min_execution_time_ms threshold (default: 10ms); array-result queries below the threshold are now flagged as info instead of warning, with a description warning that production data growth will degrade performance.OrderByWithoutLimitAnalyzer: improved suggestion template for bounded array-result queries (WHERE clause present) — now recommends adding an index on the ORDER BY column, adding setMaxResults, or suppressing the alert via config when the collection is guaranteed small.GetReferenceAnalyzer: removed wildcard *_id column patterns that caused false positives on FK columns (e.g. deposit_request_id). Detection is now restricted to strict id primary key columns only, since FK columns return collections and are not candidates for getReference().DoctrineDoctorDataCollector: bootstrap entry points (index.php, autoload_runtime.php, autoload.php) are now excluded when searching for the first application frame in exclude_paths filtering. Previously these files appeared at the bottom of every backtrace and short-circuited vendor exclusion for framework-internal queries (e.g. EasyAdmin entity loading).InheritanceStrategyAnalyzer family: detects invalid or risky inheritance mappings, including missing discriminator maps in STI, sparse STI tables, unsupported OneToMany associations on mapped superclasses, non-root #[InheritanceType] declarations, non-nullable subclass columns in STI, deep CTI hierarchies, and thin CTI subclasses.UniqueEntityWithoutDatabaseIndexAnalyzer: detects #[UniqueEntity] constraints that are not backed by a database UNIQUE index, including Symfony validation metadata declared with attributes, YAML, and XML.DenormalizedAggregateWithoutLockingAnalyzer: detects denormalized aggregate fields updated alongside collections without optimistic or pessimistic locking.ColumnTypeAnalyzer: flags mutable Doctrine date/time column types and suggests immutable equivalents to avoid silent state corruption.EagerLoadingMappingAnalyzer: detects associations declared with fetch: 'EAGER' in entity mapping and suggests deferring fetch strategy decisions to queries.GedmoExtensionPerformanceAnalyzer: detects entities using Gedmo Loggable or Translatable patterns that implicitly generate extra database queries.LazyGhostObjectsDisabledAnalyzer: detects Doctrine ORM configurations where enable_lazy_ghost_objects is not enabled on supported Symfony versions.ManyToManyWithExtraColumnsAnalyzer: detects ManyToMany join tables containing extra columns and recommends promoting them to an explicit join entity.MissingVersionFieldForConcurrencyAnalyzer: detects entities involved in concurrent write patterns without an #[ORM\Version] field for optimistic locking.FlushInEventListenerAnalyzer: detects flush() calls inside Doctrine lifecycle callbacks that can trigger re-entrant UnitOfWork computation or infinite loops.UniqueEntityWithoutDatabaseIndexAnalyzer now supports Symfony validation metadata declared in YAML and XML in addition to PHP attributes.HardcodedDatabaseCredentialsAnalyzer and UniqueEntityWithoutDatabaseIndexAnalyzer.MissingIndexAnalyzer: no longer reports a false positive when the relevant index is already used.LazyGhostObjectsDisabledAnalyzer.MetadataAnalyzerTrait and the split analyzer interface contract.OverprivilegedDatabaseUserAnalyzer: detects privileged, empty, or passwordless database users and suggests switching to a least-privilege account.HardcodedDatabaseCredentialsAnalyzer: detects database credentials embedded directly in DBAL configuration and suggests moving them to environment variables.NPlusOneAnalyzer: identifies repeated findBy()/findOneBy()-style lookups on non-key columns and suggests batching with IN queries or request-level caching.SensitiveDataExposureAnalyzer: now also flags public getters that expose sensitive entity fields without explicit protection.PropertyTypeMismatchAnalyzer: now attaches concrete fix suggestions for PHP/Doctrine type mismatches, including nullability mismatches.CollectionInitializationAnalyzer suggestion template now uses the actual mappedBy value when available.CollectionInitializationAnalyzer: now supports PHP constructor promotion when detecting collection initialization, fixing the false positive reported in issue #67.CollectionEmptyAccessAnalyzer in favor of the AST-based collection initialization analysis path.SQLInjectionInRawQueriesAnalyzer: now detects unparameterized literals in WHERE clauses of raw SQL queries as an injection risk, instead of only flagging active attack patterns.DQLInjectionAnalyzer: now detects Doctrine-generated SQL with concatenated literals and empty bound parameters, indicating unsafe DQL string concatenation.OneToOneInverseSideAnalyzer: detects bidirectional OneToOne mappedBy sides that silently force Doctrine to execute N+1 queries on every load, even when the relation is never accessed. Suggests flipping the owning side, going unidirectional, or using a fetch join.one_to_one_inverse_side analyzer.CompositeKeyComplexityAnalyzer: use ShortClassNameTrait, proper return types, and MappingHelper for Doctrine 2/3/4 compatibility.CompositeKeyComplexityAnalyzer: detects entities using composite primary keys that limit Doctrine ORM features (no getReference(), slower identity map, complex FK mappings). Severity: WARNING for 2 columns, CRITICAL for 3+ or when referenced by other entities.composite_key_complexity analyzer.OnDeleteCascadeMismatchAnalyzer now assigns CRITICAL severity for orm_cascade_db_setnull and orm_orphan_db_setnull mismatches (previously WARNING).on_delete_cascade_mismatch now render context-aware code snippets per mismatch type instead of a generic template.JoinColumnNonPrimaryKeyAnalyzer: detects associations where referencedColumnName points to a non-primary-key column, which causes incorrect lazy-loading proxy behavior.DuplicatePrivateFieldInHierarchyAnalyzer: detects private fields with the same name in an entity and its mapped parent classes, which triggers MappingException or unpredictable Collection filtering.join_column_non_primary_key, duplicate_private_field_in_hierarchy, overprivileged_database_user, and hardcoded_database_credentials analyzers.ORM\BatchFetch, fixed isVendorCode detection.Suggested Fix, Hide suggestion).issue-body background color to #fffefc for better visual consistency.doctrine_doctor.enabled now supports Symfony parameter placeholders (e.g. %kernel.debug%) by resolving the root enabled config before strict tree validation.%kernel.debug% placeholder handling in DI extension tests.isset.initializedProperty error: use ReflectionProperty::isInitialized() for readonly property check after unserialization.rel="noopener noreferrer" on external Doctrine documentation link (target="_blank")..alert-warning, .alert-danger, and .dd-suggestion-meta-intro blocks (less aggressive text contrast).IssueInterface and DeduplicatableIssueInterface.IssueReconstructor (#32)<pre><code> to prevent entity encodingwebmozart/assert constraint to support v2.xbitbag/coding-standard dependencydoctrine/doctrine-bundle ^3.0 (drop ^2.x)doctrine/orm ^3.0|^4.0 (drop ^2.x)webmozart/assert ^1.12PhpTemplateRenderer into IssueReconstructor so template rendering works after Symfony profiler deserializationSafeContext::offsetGet() now returns null for missing keys instead of throwing, enabling safe array destructuring in templates with optional context variablestrigger_location in eager_loading templateleft_join_with_not_null template (table_name -> entity)#[\Override], typed constants, array_find())ini_set('memory_limit') runtime manipulationdoctrine/doctrine-bundle ^2.x supportdoctrine/orm ^2.x supportHow can I help you explore Laravel packages today?