Skip to content

Memoize constraint resolution and attribute lookups in EntityValidator - #85

Merged
sylfabre merged 5 commits into
mainfrom
feat/memoize-entity-validator
Aug 24, 2026
Merged

Memoize constraint resolution and attribute lookups in EntityValidator#85
sylfabre merged 5 commits into
mainfrom
feat/memoize-entity-validator

Conversation

@sylfabre

Copy link
Copy Markdown
Contributor

What

EntityValidator recomputed three things on every validated entity instance, even though they only depend on the entity class (and are static for the lifetime of the process):

  1. getConstraints($class, $field) rebuilt the whole constraint array from the Doctrine mapping for every entity instance. It is now memoized per (class, field): the previous body moved to a private buildConstraints() and getConstraints() returns the cached set. The public API (getConstraints() / getConstraintsForType()) keeps its signature and behavior, so external callers benefit from the cache too.
  2. The OnlyValidateOnUpdate attribute lookup in checkIfFieldNeedsToBeValidated() performed a new \ReflectionClass() + attribute scan + parent-class recursion per field per instance. The attribute presence is now cached as a boolean per (class, field); only when the attribute is present does the (inherently per-instance) UnitOfWork changeset check still run.
  3. PropertyAccess::createPropertyAccessor() was called on every validate() call. PropertyAccessor caches property read-info per instance, so a fresh accessor forced re-resolution every time. A single lazily-created instance is now reused (no constructor/DI change).

Why

Profiling a large application using this bundle, which persists many entities per request, showed for a batch of ~720 validated entities: ~9 700 constraint-set rebuilds constructing ~15 800 constraint objects (for only ~150 unique class/field pairs), ~22 000 ReflectionClass instantiations, and ~17 900 property read-info resolutions — all recomputing values that never change within a process.

Why it is safe

  • Symfony constraints are immutable value objects, and Symfony itself reuses the constraint instances stored in class metadata across validations, so sharing instances per (class, field) is safe.
  • Whether a property carries the OnlyValidateOnUpdate attribute is a static fact of the class definition. The per-instance part (the UnitOfWork changeset lookup) is not cached.
  • PropertyAccessor is stateless apart from its internal read-info cache, which is exactly what reusing the instance is meant to leverage.

Behavior is unchanged and covered by tests:

  • repeated getConstraints() calls return the same (cached) set (assertSame),
  • validating two instances of the same class applies the same constraints to each,
  • new coverage for OnlyValidateOnUpdate: the field is skipped when not in the changeset (insert) and validated when it is (update).

Full suite + phpcs + phpstan + rector are green locally on PHP 8.4 with highest deps (the CI matrix covers the other combinations).

🤖 Generated with Claude Code

@sylfabre
sylfabre force-pushed the feat/memoize-entity-validator branch 2 times, most recently from edaba15 to 076bb05 Compare August 22, 2026 11:31
@sonarqubecloud

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

sylfabre and others added 3 commits August 23, 2026 22:49
Cache per (class, field): the resolved constraint set and the
OnlyValidateOnUpdate attribute presence, and reuse a single
PropertyAccessor instance, since all three only depend on static class
metadata.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…HPStan 2.2

The CI upgrades PHPStan to latest after the prefer-lowest install; shipmonk 4.2 predates the ExpressionResultStorage API and crashes on files using callables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…Rector

Older releases register StaticArrowFunctionRector, which current Rector rejects.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sylfabre
sylfabre force-pushed the feat/memoize-entity-validator branch from 89c0b10 to 120a028 Compare August 23, 2026 20:54
…upgrade on lowest-deps CI

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…phpstan-rules; fold them into the lowest-deps tool-upgrade step instead

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@sylfabre
sylfabre merged commit f625a06 into main Aug 24, 2026
5 checks passed
@sylfabre
sylfabre deleted the feat/memoize-entity-validator branch August 24, 2026 09:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant