Skip to content

feat(unique): add #[Unique] attribute for single-field and composite unique constraints#143

Merged
clementtalleu merged 1 commit into
clementtalleu:mainfrom
guillaumedelre:feature/issue-142
May 7, 2026
Merged

feat(unique): add #[Unique] attribute for single-field and composite unique constraints#143
clementtalleu merged 1 commit into
clementtalleu:mainfrom
guillaumedelre:feature/issue-142

Conversation

@guillaumedelre
Copy link
Copy Markdown
Contributor

Ref: #142

Enforce uniqueness at flush() time using Redis WATCH + MULTI/EXEC, for both single-field (property-level) and composite (class-level, IS_REPEATABLE) constraints.

Implementation:

  • New #[Unique] attribute (TARGET_PROPERTY | TARGET_CLASS | IS_REPEATABLE)
  • New UniqueConstraintViolationException with forField(), forFields(), concurrentModification() factory methods
  • RedisObjectManager.flush() split into a WATCH path (when unique constraints are present) and the original path (no constraints)
  • buildUniqueConstraintPlan() collects unique keys to WATCH, GET-check, SET, and DEL across all pending operations (persist, merge, delete)
  • Intra-flush collision detection: duplicate values in the same flush() call are caught before touching Redis
  • ObjectToPersist.previousValues stores the pre-merge snapshot so flush() can resolve which unique key to delete when a unique field changes
  • AbstractPersister.delete() now passes value: $object (was null, caused ReflectionClass crash when building the unique key on delete)
  • RedisClientInterface extended with get(), set(), watch(), unwatch(); exec() return type changed from void to bool (false = WATCH triggered)
  • Both RedisClient (ext-redis) and PredisClient implementations updated

Unique key format:
unique:{className}:{field}:{value} (single-field)
unique:{className}:{f1,f2}:{v1}:{v2} (composite, fields sorted)

Tests:

  • 17 functional tests (UniqueConstraintTest): persist/same-flush/merge/remove scenarios for both Hash and Json formats, single-field and composite
  • 6 unit tests (UniqueConstraintViolationExceptionTest)

Docs:

  • docs/mapping.md: new "Unique constraints" section (single-field, composite, merge/remove behavior, concurrency guarantee, limitations)
  • docs/advanced_usage.md: exception handling snippet with link to mapping.md

…unique constraints

Enforce uniqueness at flush() time using Redis WATCH + MULTI/EXEC, for both
single-field (property-level) and composite (class-level, IS_REPEATABLE) constraints.

Implementation:
- New #[Unique] attribute (TARGET_PROPERTY | TARGET_CLASS | IS_REPEATABLE)
- New UniqueConstraintViolationException with forField(), forFields(),
  concurrentModification() factory methods
- RedisObjectManager.flush() split into a WATCH path (when unique constraints
  are present) and the original path (no constraints)
- buildUniqueConstraintPlan() collects unique keys to WATCH, GET-check,
  SET, and DEL across all pending operations (persist, merge, delete)
- Intra-flush collision detection: duplicate values in the same flush() call
  are caught before touching Redis
- ObjectToPersist.previousValues stores the pre-merge snapshot so flush()
  can resolve which unique key to delete when a unique field changes
- AbstractPersister.delete() now passes value: $object (was null, caused
  ReflectionClass crash when building the unique key on delete)
- RedisClientInterface extended with get(), set(), watch(), unwatch();
  exec() return type changed from void to bool (false = WATCH triggered)
- Both RedisClient (ext-redis) and PredisClient implementations updated

Unique key format:
  unique:{className}:{field}:{value}           (single-field)
  unique:{className}:{f1,f2}:{v1}:{v2}        (composite, fields sorted)

Tests:
- 17 functional tests (UniqueConstraintTest): persist/same-flush/merge/remove
  scenarios for both Hash and Json formats, single-field and composite
- 6 unit tests (UniqueConstraintViolationExceptionTest)

Docs:
- docs/mapping.md: new "Unique constraints" section (single-field, composite,
  merge/remove behavior, concurrency guarantee, limitations)
- docs/advanced_usage.md: exception handling snippet with link to mapping.md
@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 79.74684% with 32 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.43%. Comparing base (5efb773) to head (d96617a).

Files with missing lines Patch % Lines
src/Om/RedisObjectManager.php 80.34% 23 Missing ⚠️
src/Client/RedisClient.php 18.18% 9 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@             Coverage Diff              @@
##               main     #143      +/-   ##
============================================
- Coverage     88.10%   87.43%   -0.67%     
- Complexity      900      949      +49     
============================================
  Files            68       70       +2     
  Lines          2076     2221     +145     
============================================
+ Hits           1829     1942     +113     
- Misses          247      279      +32     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@clementtalleu clementtalleu merged commit 3dbfb4c into clementtalleu:main May 7, 2026
3 checks passed
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.

3 participants