OrmEntityHydrator
in package
Table of Contents
Properties
- $em : EntityManagerInterface
- $entityCache : array<string, array<string|int, object>>
- Cache of hydrated entities for quick reference access.
- $externalAdminOnlyFieldsCache : array<class-string, array<string|int, string>>
- Cache of {@see getExternalAdminOnlyFields()} per entity class; sibling of {@see $externalReadonlyFieldsCache} with the same purpose.
- $externalReadonlyFieldsCache : array<class-string, array<int, string>>
- Cache of {@see getExternalReadonlyFields()} per entity class, so the reflection over the whole hierarchy runs once per request and entity.
- $fieldAudience : FieldAudienceContext|null
- $privilegeFreeUpdatableFieldsCache : array<class-string, array<string, string|null>>
- Cache of {@see getPrivilegeFreeUpdatableFields()} per entity class; sibling of the two caches above, so the hierarchy reflection runs once per request and entity.
- $resolveReferencesViaDb : bool
- Save path (persist mode, see {@see hydrateForPersist()}): allows looking up an unresolved ToOne reference in the DB (`em->find`). Off by default; persist mode turns it on temporarily.
- $restrictedReadFieldsCache : array<class-string, array<string, Audience>>
- Reflection cache of the {@see FieldAccess} attribute: `class => ['field' => Audience]`.
- $stringTargetCache : array<class-string, array<string, array{type: string, nullable: bool, enum: bool}|null>>
- Cache of {@see resolveStringTarget()}: `class => ['field' => conversion target | null]`.
- $unitOfWorkReset : UnitOfWorkReset|null
Methods
- __construct() : mixed
- applyIdentity() : void
- Sets identifier fields on entity from result data (supports composite keys).
- clearCache() : void
- Clears internal cache of hydrated entities. Call between different hydrations to prevent data mixing.
- clearEntityManager() : void
- Clears the EntityManager (identity map/UoW) after a failed {@see hydrateForPersist()}.
- collectExternallyStrippedKeys() : array<int, string>
- Dry-run listing of what the external strip would remove from `$data`.
- debugFields() : array<string, array<int, string>>
- Debug method to see what fields Doctrine returns
- extract() : array<string, mixed>
- Extracts entity into array.
- extractAll() : array{items: array, totalCount: int|null, _references: array}
- Extracts array of entities with _references section.
- extractWithReferences() : array<string, array<string|int, mixed>>
- Extracts entity into array with _references section.
- getExternalAdminOnlyFields() : array<string|int, string>
- Returns the property names of an entity class marked {@see ExternalAdminOnly}.
- getExternalReadonlyFields() : array<string|int, string>
- Returns the property names of an entity class marked {@see ExternalReadonly}.
- getPrivilegeFreeUpdatableFields() : array<string, string|null>
- Write policy for columns without the `updateColumns` privilege.
- hydrate() : object|null
- Hydrates data into entity.
- hydrateForPersist() : T|null
- Hydration for writing (Admin/Front to API save).
- stripExternalAdminOnlyFieldsDeep() : array<string|int, mixed>
- Removes {@see ExternalAdminOnly} fields from `$data` recursively.
- stripExternalReadonlyFieldsDeep() : array<string|int, mixed>
- Removes {@see ExternalReadonly} fields from `$data` recursively across the whole owned object graph, not only the top-level entity.
- stripNobodyWritableFields() : array<string|int, mixed>
- Removes from `$data` the fields nobody may write from the payload: `#[FieldAccess]` with {@see Audience::None} for the operation (user password and salt, reservation and conversation tokens). The value is set only by the Api manager on the entity and is never taken from the payload, on the external or the internal path.
- addToCache() : void
- Adds entity to cache.
- addToReferences() : void
- Adds entity to references array.
- camelCaseToSnakeCase() : string
- childIdValueFromData() : mixed
- Returns the scalar id value of a field from the incoming child data. For an association id field (FK to another entity) it returns the id from the reference (`{id: X}` and `{id: {id: X}}`).
- childIdValueFromEntity() : mixed
- Returns the scalar id value of a field from an existing entity (for an association, the id of the linked entity).
- collectAllProperties() : array<string|int, ReflectionProperty>
- Returns all properties across the class hierarchy (descendant and parents, including Shared `MappedSuperclass`, where attributes such as {@see ExternalReadonly} often sit).
- createReferenceKeyFromId() : string
- Creates reference key from ID values (int or array for composite keys).
- diffMissingKeyPaths() : array<int, string>
- Returns key paths present in `$before` and missing in `$after` (recursing into nested arrays). A pure function over two arrays; it knows nothing about `#[External*]` attributes (see {@see collectExternallyStrippedKeys()}).
- extractAssociations() : array<string, mixed>
- Extracts associations (relations) from entity.
- extractCollection() : array<int, array<string, mixed>>
- Extracts collection of entities.
- extractEnumValue() : mixed
- extractFields() : array<string, mixed>
- Extracts regular fields (non-associations) from entity.
- extractPrimaryKeys() : array<string, mixed>
- extractSingleEntity() : mixed
- Extracts single entity.
- findExistingChild() : object|null
- Finds an existing child in the parent collection by its identity.
- findInEntityManager() : object|null
- Finds entity in EntityManager (supports composite keys).
- findManagedByScalarId() : object|null
- Finds a managed entity by a simple scalar id from the data (top-level / ToOne update).
- findProperty() : ReflectionProperty|null
- Finds property in entity (can be in parent class).
- getEnumType() : string|null
- getFromCache() : object|null
- Gets entity from cache.
- getIdentifierFromAssociation() : mixed
- Gets identifier value from association.
- getIdentifierValue() : mixed
- Gets single identifier value (handles both scalar fields and associations).
- getPropertyType() : string|null
- Gets property type.
- getPropertyValue() : mixed
- Gets property value from entity (tries getter first, then reflection).
- getReferenceKey() : string
- Creates unique key for entity based on primary keys.
- handleReference() : object|null
- Handles reference resolution.
- hydrateActionCode() : mixed
- Special handling for actionCode property.
- hydrateCollection() : ArrayCollection<string|int, mixed>
- Hydrates collection.
- hydrateDateTime() : DateTime|null
- Hydrates DateTime value.
- hydrateEnumValue() : mixed
- hydrateNestedEntity() : object|null
- Hydrates nested entity.
- hydratePersist() : object|null
- Recursive hydration for writing: builds a managed object graph from an array.
- hydratePersistAssociation() : void
- Hydrates one association on the save path: owned OneToMany collections are merged into the parent collection (update/insert + `orphanRemoval`), ManyToMany is replaced with managed references, ToOne is resolved to a managed entity.
- hydratePersistInto() : void
- Hydrates data into an already resolved entity (managed update / new). Scalars are handled by {@see hydrateProperty()}, associations by {@see hydratePersistAssociation()}.
- hydrateProperty() : void
- Hydrates single property.
- hydrateValue() : mixed
- Hydrates value based on its type.
- idKey() : string|null
- Normalizes an id value to a string for comparison; a non-scalar gives null (never matches).
- isBackendEntity() : bool
- Tells whether the entity is a backend (Api) one. Only on the backend may hydration look up missing references in the DB (`em->find`); Front/Admin consume only API responses (`_references`) and never touch the DB through Doctrine.
- isBackReference() : bool
- Checks if property is a back reference (ManyToOne with inversedBy).
- isCascadePersistAssociation() : bool
- Tells whether the association is owned by the parent (cascade persist). Such children are extracted with full data on the save path, not just as id references.
- isCollection() : bool
- Checks if property is collection (OneToMany or ManyToMany).
- isEmptyEntityData() : bool
- Checks if entity data is empty or contains only null values.
- isEmptyIdentity() : bool
- The identity is empty when no component fills it (null / '' / 0); such an object is not a stored record. Covers scalar and composite/derived identity (for an association identity Doctrine returns the linked entity, not the id).
- isEntity() : bool
- Checks if type is entity.
- isEntityInitialized() : bool
- Checks if the entire entity is initialized (not an uninitialized lazy proxy / PHP 8.4 native lazy object). `UnitOfWork::isUninitializedObject()` covers both the classic `__CG__` proxy and a native lazy object (Doctrine `lazyNativeObjects: true`, see config/Shared/doctrine.neon).
- isIdentifierAssociation() : bool
- Tells whether the association is part of the entity identity (derived identity). Such an association cannot be skipped as a back-reference in extract: it carries the child identity (e.g. translations: PK = language + association to the parent).
- isPlaceholderIdentifier() : bool
- Tells whether the identifier is a placeholder (0/'0'/null/'' in any, even nested, value): a legitimate transient state (unpersisted parent, see `getIdentifierFromAssociation()`). A placeholder reference is skipped in the owned merge branch without an exception. A zero id is never a valid FK in this schema (autoincrement starts at 1).
- isReference() : bool
- Checks if data represents a reference (only 'id' key).
- isRelationInitialized() : bool
- Checks if relation is initialized (loaded) without triggering lazy loading.
- isResurrectionCandidate() : bool
- Tells whether this miss (`findManagedByScalarId()` found nothing) is a resurrection candidate.
- isValueInitialized() : bool
- Checks if value (collection or entity) is initialized (loaded) without triggering lazy load. `UnitOfWork::isUninitializedObject()` ignores collections (handled separately); for entities it knows the state of a classic `__CG__` proxy and a PHP 8.4 native lazy object.
- matchDateTimeClass() : DateTimeInterface
- Converts a date to the class the field expects (`DateTime` or `DateTimeImmutable`); without a known target (untyped setter, no Doctrine mapping) it returns it unchanged.
- normalizeKeyValue() : string
- Converts an identifier value to a stable string for the reference key.
- normalizePropertyName() : string
- referenceIdScalar() : mixed
- Extracts a scalar id from a reference (`X`, `['id' => X]` and `['id' => ['id' => X]]`).
- resolveOwnedReferenceChild() : object|null
- Resolves a reference item (`{"id": ...}`) in the owned O2M merge branch.
- resolveStringTarget() : array{type: string, nullable: bool, enum: bool}|null
- Returns the string conversion target for a field (cached: it describes the code, not the data).
- restrictedReadFields() : array<string, Audience>
- Returns the entity fields with a restricted read audience: `['fieldName' => Audience]`.
- sameEntityIdentity() : bool
- Compares the identity of two entities of the same class by primary key (no object `==`).
- scalarizedIdentifierValues() : array<string, mixed>
- Returns the entity identity as `[fieldName => scalar]`.
- setAssociationValue() : void
- setOwnerBackReference() : void
- Sets the child's back-reference to the parent (the owning FK of a `mappedBy` collection).
- toCamelCase() : string
Properties
$em
private
EntityManagerInterface
$em
$entityCache
Cache of hydrated entities for quick reference access.
private
array<string, array<string|int, object>>
$entityCache
= []
Structure: ['EntityClass' => ['referenceKey' => $entity]]
$externalAdminOnlyFieldsCache
Cache of {@see getExternalAdminOnlyFields()} per entity class; sibling of {@see $externalReadonlyFieldsCache} with the same purpose.
private
array<class-string, array<string|int, string>>
$externalAdminOnlyFieldsCache
= []
$externalReadonlyFieldsCache
Cache of {@see getExternalReadonlyFields()} per entity class, so the reflection over the whole hierarchy runs once per request and entity.
private
array<class-string, array<int, string>>
$externalReadonlyFieldsCache
= []
Structure: ['EntityClass' => ['field1', 'field2', ...]]
$fieldAudience read-only
private
FieldAudienceContext|null
$fieldAudience
= null
$privilegeFreeUpdatableFieldsCache
Cache of {@see getPrivilegeFreeUpdatableFields()} per entity class; sibling of the two caches above, so the hierarchy reflection runs once per request and entity.
private
array<class-string, array<string, string|null>>
$privilegeFreeUpdatableFieldsCache
= []
$resolveReferencesViaDb
Save path (persist mode, see {@see hydrateForPersist()}): allows looking up an unresolved ToOne reference in the DB (`em->find`). Off by default; persist mode turns it on temporarily.
private
bool
$resolveReferencesViaDb
= false
$restrictedReadFieldsCache
Reflection cache of the {@see FieldAccess} attribute: `class => ['field' => Audience]`.
private
array<class-string, array<string, Audience>>
$restrictedReadFieldsCache
= []
Not cleared by self::clearCache(): it describes the code, not the data.
$stringTargetCache
Cache of {@see resolveStringTarget()}: `class => ['field' => conversion target | null]`.
private
array<class-string, array<string, array{type: string, nullable: bool, enum: bool}|null>>
$stringTargetCache
= []
Like the reflection caches above, it is not cleared by self::clearCache().
$unitOfWorkReset read-only
private
UnitOfWorkReset|null
$unitOfWorkReset
= null
Methods
__construct()
public
__construct(EntityManagerInterface $em[, FieldAudienceContext|null $fieldAudience = null ][, UnitOfWorkReset|null $unitOfWorkReset = null ]) : mixed
Parameters
- $em : EntityManagerInterface
- $fieldAudience : FieldAudienceContext|null = null
- $unitOfWorkReset : UnitOfWorkReset|null = null
-
central reset (DI); without it (unit tests building the hydrator by hand) clearEntityManager() clears directly
applyIdentity()
Sets identifier fields on entity from result data (supports composite keys).
public
applyIdentity(object $entity, array<string|int, mixed> $data) : void
Use after save to rehydrate auto-generated IDs.
Parameters
- $entity : object
- $data : array<string|int, mixed>
clearCache()
Clears internal cache of hydrated entities. Call between different hydrations to prevent data mixing.
public
clearCache() : void
clearEntityManager()
Clears the EntityManager (identity map/UoW) after a failed {@see hydrateForPersist()}.
public
clearEntityManager(string $reason) : void
Persist hydration mutates the managed graph directly (no copy) before the transaction/flush. When it ends with an exception (reference miss, resurrection guard), managed entities can stay partially mutated and dirty.
IMPORTANT: without this cleanup the next flush in the same request (another action
over the same EM) would write them as an unintended side effect. em->clear() is the
only reliable cleanup (detaches everything, unlike a selective refresh).
Goes through UnitOfWorkReset::abort() so the operation log can tell the reason; the direct clear is only the fallback of a hydrator built without DI.
Parameters
- $reason : string
-
why the save was aborted, e.g.
save.hydration_failed
collectExternallyStrippedKeys()
Dry-run listing of what the external strip would remove from `$data`.
public
collectExternallyStrippedKeys(array<string, mixed> $data, class-string $entityClass, bool $includeAdminOnly) : array<int, string>
Returns the key paths the external strip would take from this payload. $data is not
changed (the parameter is by value, the result is only a list of names). Used only to
measure the internal path (SaveTrait::internalActionSave()),
where nothing is stripped today and the behavior must not change.
NOTE: this is not a second implementation of the strip. It calls the same production methods (stripExternalReadonlyFieldsDeep(), stripExternalAdminOnlyFieldsDeep()) and only compares the result with the input (diffMissingKeyPaths(), a pure function that knows nothing about attributes, reflection or Doctrine metadata), so it cannot drift from the production behavior. The external path itself is unchanged.
$includeAdminOnly mirrors the tier gate of SaveTrait::internalActionSave():
#[ExternalReadonly] is always stripped, #[ExternalAdminOnly] only for a
non-privileged caller.
Numeric indexes (ToMany collection items) collapse to [] in the path
(items[].price), so the report has one line per field, not per item.
Parameters
- $data : array<string, mixed>
- $entityClass : class-string
- $includeAdminOnly : bool
Return values
array<int, string>debugFields()
Debug method to see what fields Doctrine returns
public
debugFields(object $entity) : array<string, array<int, string>>
Parameters
- $entity : object
Return values
array<string, array<int, string>>extract()
Extracts entity into array.
public
extract(object $entity[, array<string|int, mixed> &$processed = [] ][, array<string|int, mixed> &$references = [] ]) : array<string, mixed>
Automatic detection: extracts only loaded relations (no lazy loading). Includes read-only foreign keys (insertable=false, updatable=false)
Parameters
- $entity : object
- $processed : array<string|int, mixed> = []
- $references : array<string|int, mixed> = []
Return values
array<string, mixed>extractAll()
Extracts array of entities with _references section.
public
extractAll(array<string|int, mixed> $entities[, int|null $totalCount = null ]) : array{items: array, totalCount: int|null, _references: array}
Main method for API → Frontend communication (getAll). All objects (including main items) are stored in _references, items contains only references.
Output structure: {"items": [{"id": 1}], "totalCount": 42, "_references": {...}}
Parameters
- $entities : array<string|int, mixed>
- $totalCount : int|null = null
Return values
array{items: array, totalCount: int|null, _references: array}extractWithReferences()
Extracts entity into array with _references section.
public
extractWithReferences(object $entity) : array<string, array<string|int, mixed>>
Main method for API → Frontend communication (get). Main object is stored in _references, only reference is returned.
Output structure: {"data": {"id": 1}, "_references": {...}}
Parameters
- $entity : object
Return values
array<string, array<string|int, mixed>>getExternalAdminOnlyFields()
Returns the property names of an entity class marked {@see ExternalAdminOnly}.
public
getExternalAdminOnlyFields(class-string $entityClass) : array<string|int, string>
Sibling of getExternalReadonlyFields() with its own cache and implementation on
purpose: sharing a trait/method would multiply phpstan errors and risk regressions in
the already verified ExternalReadonly mechanism.
Parameters
- $entityClass : class-string
Return values
array<string|int, string>getExternalReadonlyFields()
Returns the property names of an entity class marked {@see ExternalReadonly}.
public
getExternalReadonlyFields(class-string $entityClass) : array<string|int, string>
Reflects over the whole class hierarchy (descendant and all parents; an Api leaf entity
typically inherits from a Shared MappedSuperclass where the attributes sit, including
constructor-promoted properties), like
EntityHydrator::collectProperties(). Cached per class
(see $externalReadonlyFieldsCache).
Called by SaveTrait::stripExternalReadonlyFields()
only on the external path ($meta['__external'] === true). Stripping from $data
before hydration protects UPDATE (the managed entity keeps its DB value) and INSERT
(default / prepareXxxForAdd force-override).
Parameters
- $entityClass : class-string
Return values
array<string|int, string>getPrivilegeFreeUpdatableFields()
Write policy for columns without the `updateColumns` privilege.
public
getPrivilegeFreeUpdatableFields(class-string $entityClass) : array<string, string|null>
Returns a map property name => null | 'checkMethodName':
- key present and value
null: PubliclyUpdatable (anyone may write), - key present and value a string: ConditionallyUpdatable, this Api presenter method decides about the write,
- key absent: only a holder of the privilege may write the column.
Collected the same way as getExternalReadonlyFields()
(collectAllProperties(): the whole hierarchy including Shared
MappedSuperclass and constructor-promoted properties) with the same per-request
cache, so it cannot drift from how the other hydration attributes are read.
NOTE: when a property carries both attributes, the conditional one wins as the stricter.
Parameters
- $entityClass : class-string
Return values
array<string, string|null>hydrate()
Hydrates data into entity.
public
hydrate(array<string|int, mixed> $data, class-string $entityClass) : object|null
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
Return values
object|null —Entity object or null if reference cannot be resolved
hydrateForPersist()
Hydration for writing (Admin/Front to API save).
public
hydrateForPersist(array<string, mixed> $data, T> $entityClass) : T|null
Same as hydrate() but turns on persist mode: unresolved ToOne references (the
save path has no _references) are looked up as managed entities from the DB
(em->find). Thanks to that, association identities of join entities (e.g.
NewsletterAddressRel.newsletterAddress) and ordinary foreign keys point to an entity
with a known id and the flush does not fail on EntityMissingAssignedId.
IMPORTANT: the flag is always restored after hydration. The hydrator is a shared singleton and the read path must never touch the DB.
Parameters
- $data : array<string, mixed>
- $entityClass : T>
Tags
Return values
T|null —Entity object or null if reference cannot be resolved
stripExternalAdminOnlyFieldsDeep()
Removes {@see ExternalAdminOnly} fields from `$data` recursively.
public
stripExternalAdminOnlyFieldsDeep(array<string|int, mixed> $data, class-string $entityClass) : array<string|int, mixed>
Sibling of stripExternalReadonlyFieldsDeep() with the same recursion
(ClassMetadata::hasAssociation() into ToOne and ToMany, isReference() guard skips
plain {"id": X} references). Called only when the caller is not privileged
(SaveTrait::internalActionSave(),
$meta['__externalTier'] !== 'privileged'). The comparison is case-insensitive:
hydrateProperty() finds the setter via method_exists(), which ignores letter case,
so a case-sensitive comparison would let #[ExternalAdminOnly] be bypassed by a
different letter case in the key.
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
Return values
array<string|int, mixed>stripExternalReadonlyFieldsDeep()
Removes {@see ExternalReadonly} fields from `$data` recursively across the whole owned object graph, not only the top-level entity.
public
stripExternalReadonlyFieldsDeep(array<string|int, mixed> $data, class-string $entityClass) : array<string|int, mixed>
SaveTrait::stripExternalReadonlyFields() passes
only the top-level $entityClass. For aggregates with owned cascade-persist
collections (typically Order:
items/shipper/paymentMethod/vouchers) the nested #[ExternalReadonly] fields
(OrderItem::$price, OrderShipper::$price, OrderVoucher::$percents) must be
stripped too.
IMPORTANT: without the recursion an external client could send an item price/VAT
snapshot in data.items[].price and OrderCalculator::recalculate() would sum it into
order.totalPrice (the calculator takes item prices as input, it does not derive them
from the catalog).
The recursion mirrors hydratePersistAssociation() (ToOne/ToMany by
ClassMetadata) but does not hydrate: it only prunes $data before
hydrateForPersist(). Reference items ({"id": X}) are not descended into (nothing to
strip, isReference() guard).
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
Return values
array<string|int, mixed>stripNobodyWritableFields()
Removes from `$data` the fields nobody may write from the payload: `#[FieldAccess]` with {@see Audience::None} for the operation (user password and salt, reservation and conversation tokens). The value is set only by the Api manager on the entity and is never taken from the payload, on the external or the internal path.
public
stripNobodyWritableFields(array<string|int, mixed> $data, class-string $entityClass, bool $isInsert) : array<string|int, mixed>
IMPORTANT: without this, saving a user from the admin overwrites the password with an
empty string. Reading does not expose the password (read: None), so the admin entity
carries the default '', which would go back with the whole entity.
Root of the payload only; nested graphs do not carry these fields. Keys are compared
case-insensitively and without underscores (access_token), for the same reason as in
stripExternalReadonlyFieldsDeep().
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
- $isInsert : bool
Return values
array<string|int, mixed>addToCache()
Adds entity to cache.
private
addToCache(string $entityClass, string|int $referenceKey, object $entity) : void
Parameters
- $entityClass : string
- $referenceKey : string|int
- $entity : object
addToReferences()
Adds entity to references array.
private
addToReferences(array<string|int, mixed> &$references, string $entityClass, string $referenceKey, array<string|int, mixed> $extractedData) : void
Parameters
- $references : array<string|int, mixed>
- $entityClass : string
- $referenceKey : string
- $extractedData : array<string|int, mixed>
camelCaseToSnakeCase()
private
camelCaseToSnakeCase(string $input) : string
Parameters
- $input : string
Return values
stringchildIdValueFromData()
Returns the scalar id value of a field from the incoming child data. For an association id field (FK to another entity) it returns the id from the reference (`{id: X}` and `{id: {id: X}}`).
private
childIdValueFromData(array<string|int, mixed> $itemData, string $field, ClassMetadata<string|int, object> $targetMeta) : mixed
Parameters
- $itemData : array<string|int, mixed>
- $field : string
- $targetMeta : ClassMetadata<string|int, object>
childIdValueFromEntity()
Returns the scalar id value of a field from an existing entity (for an association, the id of the linked entity).
private
childIdValueFromEntity(object $entity, string $field, ClassMetadata<string|int, object> $targetMeta) : mixed
Parameters
- $entity : object
- $field : string
- $targetMeta : ClassMetadata<string|int, object>
collectAllProperties()
Returns all properties across the class hierarchy (descendant and parents, including Shared `MappedSuperclass`, where attributes such as {@see ExternalReadonly} often sit).
private
collectAllProperties(class-string $entityClass) : array<string|int, ReflectionProperty>
Same approach as EntityHydrator::collectProperties().
Parameters
- $entityClass : class-string
Return values
array<string|int, ReflectionProperty>createReferenceKeyFromId()
Creates reference key from ID values (int or array for composite keys).
private
createReferenceKeyFromId(mixed $idValue) : string
Parameters
- $idValue : mixed
Return values
stringdiffMissingKeyPaths()
Returns key paths present in `$before` and missing in `$after` (recursing into nested arrays). A pure function over two arrays; it knows nothing about `#[External*]` attributes (see {@see collectExternallyStrippedKeys()}).
private
static diffMissingKeyPaths(array<string|int, mixed> $before, array<string|int, mixed> $after[, string $prefix = '' ]) : array<int, string>
Parameters
- $before : array<string|int, mixed>
- $after : array<string|int, mixed>
- $prefix : string = ''
Return values
array<int, string>extractAssociations()
Extracts associations (relations) from entity.
private
extractAssociations(object $entity, ClassMetadata<string|int, object> $metadata, array<string|int, mixed> &$processed, array<string|int, mixed> &$references) : array<string, mixed>
Parameters
- $entity : object
- $metadata : ClassMetadata<string|int, object>
- $processed : array<string|int, mixed>
- $references : array<string|int, mixed>
Return values
array<string, mixed>extractCollection()
Extracts collection of entities.
private
extractCollection(Collection<string|int, mixed> $collection, array<string|int, mixed> &$processed, array<string|int, mixed> &$references[, bool $inlineFull = false ]) : array<int, array<string, mixed>>
Parameters
- $collection : Collection<string|int, mixed>
- $processed : array<string|int, mixed>
- $references : array<string|int, mixed>
- $inlineFull : bool = false
-
when true, returns the full child data (owned collections with cascade persist); otherwise only id references
Return values
array<int, array<string, mixed>>extractEnumValue()
private
extractEnumValue(mixed $value) : mixed
Parameters
- $value : mixed
extractFields()
Extracts regular fields (non-associations) from entity.
private
extractFields(object $entity, ClassMetadata<string|int, object> $metadata) : array<string, mixed>
Includes fields with insertable=false, updatable=false (read-only foreign keys)
Parameters
- $entity : object
- $metadata : ClassMetadata<string|int, object>
Return values
array<string, mixed>extractPrimaryKeys()
private
extractPrimaryKeys(object $entity, ClassMetadata<string|int, object> $metadata) : array<string, mixed>
Parameters
- $entity : object
- $metadata : ClassMetadata<string|int, object>
Return values
array<string, mixed>extractSingleEntity()
Extracts single entity.
private
extractSingleEntity(object|null $relatedEntity, array<string|int, mixed> &$processed, array<string|int, mixed> &$references) : mixed
Parameters
- $relatedEntity : object|null
- $processed : array<string|int, mixed>
- $references : array<string|int, mixed>
findExistingChild()
Finds an existing child in the parent collection by its identity.
private
findExistingChild(Collection<int, object> $collection, array<string|int, mixed> $itemData, ClassMetadata<string|int, object> $targetMeta, string|null $ownerInverseField) : object|null
The owner part of the identity (owner association) is the same for all children, so it
is skipped; the remaining id fields are compared: scalar (id, languageId) and
association ones (a join entity with a PK of several associations, e.g.
NewsletterAddressRel.newsletterAddress).
Parameters
- $collection : Collection<int, object>
- $itemData : array<string|int, mixed>
- $targetMeta : ClassMetadata<string|int, object>
- $ownerInverseField : string|null
Return values
object|nullfindInEntityManager()
Finds entity in EntityManager (supports composite keys).
private
findInEntityManager(mixed $entityId, class-string $entityClass, ClassMetadata<string|int, object> $metadata) : object|null
Parses a composite key string "1-2-3" into an array for the lookup.
Parameters
- $entityId : mixed
- $entityClass : class-string
- $metadata : ClassMetadata<string|int, object>
Return values
object|nullfindManagedByScalarId()
Finds a managed entity by a simple scalar id from the data (top-level / ToOne update).
private
findManagedByScalarId(array<string|int, mixed> $data, class-string $entityClass, ClassMetadata<string|int, object> $metadata) : object|null
Composite/derived identity returns null; it is resolved by merging into the parent collection (findExistingChild()).
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
- $metadata : ClassMetadata<string|int, object>
Return values
object|nullfindProperty()
Finds property in entity (can be in parent class).
private
findProperty(object $entity, string $propertyName) : ReflectionProperty|null
Parameters
- $entity : object
- $propertyName : string
Return values
ReflectionProperty|nullgetEnumType()
private
getEnumType(ClassMetadata<string|int, object> $metadata, string $fieldName) : string|null
Parameters
- $metadata : ClassMetadata<string|int, object>
- $fieldName : string
Return values
string|nullgetFromCache()
Gets entity from cache.
private
getFromCache(string $entityClass, string|int $referenceKey) : object|null
Parameters
- $entityClass : string
- $referenceKey : string|int
Return values
object|nullgetIdentifierFromAssociation()
Gets identifier value from association.
private
getIdentifierFromAssociation(object $entity, string $fieldName, ClassMetadata<string|int, object> $metadata) : mixed
Parameters
- $entity : object
- $fieldName : string
- $metadata : ClassMetadata<string|int, object>
getIdentifierValue()
Gets single identifier value (handles both scalar fields and associations).
private
getIdentifierValue(object $entity, string $fieldName, ClassMetadata<string|int, object> $metadata) : mixed
Parameters
- $entity : object
- $fieldName : string
- $metadata : ClassMetadata<string|int, object>
getPropertyType()
Gets property type.
private
getPropertyType(ReflectionClass<string|int, object> $reflection, string $propertyName) : string|null
Parameters
- $reflection : ReflectionClass<string|int, object>
- $propertyName : string
Return values
string|nullgetPropertyValue()
Gets property value from entity (tries getter first, then reflection).
private
getPropertyValue(object $entity, string $propertyName) : mixed
Parameters
- $entity : object
- $propertyName : string
getReferenceKey()
Creates unique key for entity based on primary keys.
private
getReferenceKey(object $entity, ClassMetadata<string|int, object> $metadata) : string
Examples: id=3: "3" | pageId=5,languageId=2: "5_2" | with relation: "1_1_5"
Parameters
- $entity : object
- $metadata : ClassMetadata<string|int, object>
Return values
stringhandleReference()
Handles reference resolution.
private
handleReference(array<string|int, mixed> $data, class-string $entityClass, ClassMetadata<string|int, object> $metadata) : object|null
Supports both the new key format (dash) and the legacy one (underscore) for cache lookup.
Read path (outside persist mode): graceful degradation, returns null and logs a warning.
Persist path: an unresolved reference is a silent destructive write (a ToOne is nulled,
an M2M item vanishes from the link, orphanRemoval would delete an owned child), so a
miss throws ReferenceNotFoundException, except for a placeholder identifier
(0/null/'': an unpersisted parent, a legitimate transient state of
getIdentifierFromAssociation()), which stays a graceful null.
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
- $metadata : ClassMetadata<string|int, object>
Tags
Return values
object|null —Entity object, or null (read miss / persist placeholder miss).
hydrateActionCode()
Special handling for actionCode property.
private
hydrateActionCode(mixed $value, ClassMetadata<string|int, object> $metadata, string $propertyName) : mixed
Parameters
- $value : mixed
- $metadata : ClassMetadata<string|int, object>
- $propertyName : string
hydrateCollection()
Hydrates collection.
private
hydrateCollection(mixed $value, string|null $propertyType) : ArrayCollection<string|int, mixed>
Parameters
- $value : mixed
- $propertyType : string|null
Return values
ArrayCollection<string|int, mixed>hydrateDateTime()
Hydrates DateTime value.
private
hydrateDateTime(mixed $value) : DateTime|null
Parameters
- $value : mixed
Return values
DateTime|nullhydrateEnumValue()
private
hydrateEnumValue(mixed $value, string|null $propertyType) : mixed
Parameters
- $value : mixed
- $propertyType : string|null
hydrateNestedEntity()
Hydrates nested entity.
private
hydrateNestedEntity(mixed $value, class-string $propertyType) : object|null
Parameters
- $value : mixed
- $propertyType : class-string
Return values
object|nullhydratePersist()
Recursive hydration for writing: builds a managed object graph from an array.
private
hydratePersist(array<string|int, mixed> $data, class-string $entityClass, object|null $owner, string|null $ownerInverseField) : object|null
For each entity in the graph (the ORM metadata gives the type of nested objects):
- existing (id in the data): finds the managed entity in the EM and hydrates into it (UPDATE), so even multi-level owned collections with derived identity sit on a managed parent and the flush resolves the insert order,
- new (no id):
new, - owned collections (OneToMany cascade): merged into the parent collection
(update/insert;
orphanRemovaldeletes the missing ones), - ManyToMany / ToOne references (id only): managed references from the EM.
The read path hydrate() is unchanged.
Parameters
- $data : array<string|int, mixed>
- $entityClass : class-string
- $owner : object|null
-
parent (when hydrating a collection child)
- $ownerInverseField : string|null
-
child field with the back-reference to the parent (mappedBy)
Return values
object|nullhydratePersistAssociation()
Hydrates one association on the save path: owned OneToMany collections are merged into the parent collection (update/insert + `orphanRemoval`), ManyToMany is replaced with managed references, ToOne is resolved to a managed entity.
private
hydratePersistAssociation(object $entity, string $assoc, mixed $value, ClassMetadata<string|int, object> $metadata) : void
Parameters
- $entity : object
- $assoc : string
- $value : mixed
- $metadata : ClassMetadata<string|int, object>
hydratePersistInto()
Hydrates data into an already resolved entity (managed update / new). Scalars are handled by {@see hydrateProperty()}, associations by {@see hydratePersistAssociation()}.
private
hydratePersistInto(object $entity, array<string|int, mixed> $data, class-string $entityClass, ClassMetadata<string|int, object> $metadata, object|null $owner, string|null $ownerInverseField) : void
Parameters
- $entity : object
- $data : array<string|int, mixed>
- $entityClass : class-string
- $metadata : ClassMetadata<string|int, object>
- $owner : object|null
- $ownerInverseField : string|null
hydrateProperty()
Hydrates single property.
private
hydrateProperty(object $entity, string $key, mixed $value, ReflectionClass<string|int, object> $reflection, ClassMetadata<string|int, object> $metadata) : void
Parameters
- $entity : object
- $key : string
- $value : mixed
- $reflection : ReflectionClass<string|int, object>
- $metadata : ClassMetadata<string|int, object>
Tags
hydrateValue()
Hydrates value based on its type.
private
hydrateValue(mixed $value, string $propertyName, string|null $propertyType, ClassMetadata<string|int, object> $metadata) : mixed
Parameters
- $value : mixed
- $propertyName : string
- $propertyType : string|null
- $metadata : ClassMetadata<string|int, object>
idKey()
Normalizes an id value to a string for comparison; a non-scalar gives null (never matches).
private
idKey(mixed $value) : string|null
Parameters
- $value : mixed
Return values
string|nullisBackendEntity()
Tells whether the entity is a backend (Api) one. Only on the backend may hydration look up missing references in the DB (`em->find`); Front/Admin consume only API responses (`_references`) and never touch the DB through Doctrine.
private
isBackendEntity(string $entityClass) : bool
Parameters
- $entityClass : string
Return values
boolisBackReference()
Checks if property is a back reference (ManyToOne with inversedBy).
private
isBackReference(ClassMetadata<string|int, object> $metadata, string $propertyName) : bool
Used in extract(), not in hydrate().
Why skip in extract(): 1) Prevents N+1 queries 2) Prevents cycles 3) Parent already has children loaded Why not skip in hydrate(): receives clean JSON data (no lazy proxies), back refs already removed in extract()
Parameters
- $metadata : ClassMetadata<string|int, object>
- $propertyName : string
Return values
boolisCascadePersistAssociation()
Tells whether the association is owned by the parent (cascade persist). Such children are extracted with full data on the save path, not just as id references.
private
isCascadePersistAssociation(ClassMetadata<string|int, object> $metadata, string $associationName) : bool
Parameters
- $metadata : ClassMetadata<string|int, object>
- $associationName : string
Return values
boolisCollection()
Checks if property is collection (OneToMany or ManyToMany).
private
isCollection(ClassMetadata<string|int, object> $metadata, string $propertyName) : bool
Parameters
- $metadata : ClassMetadata<string|int, object>
- $propertyName : string
Return values
boolisEmptyEntityData()
Checks if entity data is empty or contains only null values.
private
isEmptyEntityData(array<string|int, mixed> $data) : bool
Parameters
- $data : array<string|int, mixed>
Return values
boolisEmptyIdentity()
The identity is empty when no component fills it (null / '' / 0); such an object is not a stored record. Covers scalar and composite/derived identity (for an association identity Doctrine returns the linked entity, not the id).
private
isEmptyIdentity(array<string, mixed> $idValues) : bool
Parameters
- $idValues : array<string, mixed>
Return values
boolisEntity()
Checks if type is entity.
private
isEntity(string|null $type) : bool
NOTE: class_exists() returns true for an enum too (an enum is a class under the
hood), so the enum is excluded explicitly; otherwise an enum property would be
processed as a nested entity and hydrateNestedEntity() would return null for a scalar value.
Parameters
- $type : string|null
Tags
Return values
boolisEntityInitialized()
Checks if the entire entity is initialized (not an uninitialized lazy proxy / PHP 8.4 native lazy object). `UnitOfWork::isUninitializedObject()` covers both the classic `__CG__` proxy and a native lazy object (Doctrine `lazyNativeObjects: true`, see config/Shared/doctrine.neon).
private
isEntityInitialized(object $entity) : bool
Parameters
- $entity : object
Return values
boolisIdentifierAssociation()
Tells whether the association is part of the entity identity (derived identity). Such an association cannot be skipped as a back-reference in extract: it carries the child identity (e.g. translations: PK = language + association to the parent).
private
isIdentifierAssociation(ClassMetadata<string|int, object> $metadata, string $propertyName) : bool
Parameters
- $metadata : ClassMetadata<string|int, object>
- $propertyName : string
Return values
boolisPlaceholderIdentifier()
Tells whether the identifier is a placeholder (0/'0'/null/'' in any, even nested, value): a legitimate transient state (unpersisted parent, see `getIdentifierFromAssociation()`). A placeholder reference is skipped in the owned merge branch without an exception. A zero id is never a valid FK in this schema (autoincrement starts at 1).
private
isPlaceholderIdentifier(mixed $entityId) : bool
Parameters
- $entityId : mixed
Return values
boolisReference()
Checks if data represents a reference (only 'id' key).
private
isReference(array<string|int, mixed> $data) : bool
Parameters
- $data : array<string|int, mixed>
Return values
boolisRelationInitialized()
Checks if relation is initialized (loaded) without triggering lazy loading.
private
isRelationInitialized(object $entity, string $associationName) : bool
Parameters
- $entity : object
- $associationName : string
Return values
boolisResurrectionCandidate()
Tells whether this miss (`findManagedByScalarId()` found nothing) is a resurrection candidate.
private
isResurrectionCandidate(array<string|int, mixed> $data, ClassMetadata<string|int, object> $metadata) : bool
Same eligibility as findManagedByScalarId() (a simple scalar, non-association id
field with a concrete, non-empty value in the data), plus the entity must use a
generator (identity/sequence/auto). An entity with an assigned id (usesIdGenerator()
false: derived identity, manual PK) is a legitimate insert with an explicit id.
Parameters
- $data : array<string|int, mixed>
- $metadata : ClassMetadata<string|int, object>
Return values
boolisValueInitialized()
Checks if value (collection or entity) is initialized (loaded) without triggering lazy load. `UnitOfWork::isUninitializedObject()` ignores collections (handled separately); for entities it knows the state of a classic `__CG__` proxy and a PHP 8.4 native lazy object.
private
isValueInitialized(mixed $value) : bool
Parameters
- $value : mixed
Return values
boolmatchDateTimeClass()
Converts a date to the class the field expects (`DateTime` or `DateTimeImmutable`); without a known target (untyped setter, no Doctrine mapping) it returns it unchanged.
private
matchDateTimeClass(DateTimeInterface $value, ReflectionClass<string|int, object> $reflection, string $setter, string $propertyName, ClassMetadata<string|int, object> $metadata) : DateTimeInterface
Parameters
- $value : DateTimeInterface
- $reflection : ReflectionClass<string|int, object>
- $setter : string
- $propertyName : string
- $metadata : ClassMetadata<string|int, object>
Return values
DateTimeInterfacenormalizeKeyValue()
Converts an identifier value to a stable string for the reference key.
private
normalizeKeyValue(mixed $value) : string
A DateTime (e.g. a composite PK containing a date column) cannot be cast to string directly – it is formatted to a fixed textual form instead. Must stay consistent with createReferenceKeyFromId() so extract/hydrate match.
Parameters
- $value : mixed
Return values
stringnormalizePropertyName()
private
normalizePropertyName(string|int $propertyName) : string
Parameters
- $propertyName : string|int
Return values
stringreferenceIdScalar()
Extracts a scalar id from a reference (`X`, `['id' => X]` and `['id' => ['id' => X]]`).
private
referenceIdScalar(mixed $reference) : mixed
Parameters
- $reference : mixed
resolveOwnedReferenceChild()
Resolves a reference item (`{"id": ...}`) in the owned O2M merge branch.
private
resolveOwnedReferenceChild(array<string|int, mixed> $itemData, class-string $target, ClassMetadata<string|int, object> $targetMeta, Collection<int, object> $collection, object $owner, string|null $ownerInverseField) : object|null
A reference item never reaches hydratePersistInto() (it would end in a
setId(array) TypeError); it is resolved separately:
(a) the child is already in the owner collection (instance identity guaranteed by the
EM identity map): kept in the keep-list unchanged,
(b) the child is not in the collection but its mappedBy getter points to this owner:
it was only missing from the not yet initialized/filtered collection, kept,
(b') the mappedBy getter points to another owner: a foreign child, exception,
(c) the reference was not found: handleReference() in persist mode throws
ReferenceNotFoundException for every non-placeholder miss; null is returned only
for a placeholder identifier (unpersisted parent), which is skipped silently.
IMPORTANT: a child must never drop out of the keep-list silently otherwise, because
orphanRemoval would delete the existing child from the DB.
Parameters
- $itemData : array<string|int, mixed>
- $target : class-string
- $targetMeta : ClassMetadata<string|int, object>
- $collection : Collection<int, object>
- $owner : object
- $ownerInverseField : string|null
Tags
Return values
object|nullresolveStringTarget()
Returns the string conversion target for a field (cached: it describes the code, not the data).
private
resolveStringTarget(ReflectionClass<string|int, object> $reflection, string $setter, string $propertyName, ClassMetadata<string|int, object> $metadata) : array{type: string, nullable: bool, enum: bool}|null
Parameters
- $reflection : ReflectionClass<string|int, object>
- $setter : string
- $propertyName : string
- $metadata : ClassMetadata<string|int, object>
Return values
array{type: string, nullable: bool, enum: bool}|nullrestrictedReadFields()
Returns the entity fields with a restricted read audience: `['fieldName' => Audience]`.
private
restrictedReadFields(class-string $entityClass) : array<string, Audience>
Cached per class: attribute reflection is expensive and extractAll() walks hundreds of
entities of the same class. An empty array (the vast majority of classes) is a valid
cached result.
NOTE: the parent hierarchy is read too: App\UI\Api\... entities inherit properties
from App\Core\Base\Shared\..., where the attributes sit.
Parameters
- $entityClass : class-string
Return values
array<string, Audience>sameEntityIdentity()
Compares the identity of two entities of the same class by primary key (no object `==`).
private
sameEntityIdentity(object $a, object $b) : bool
Parameters
- $a : object
- $b : object
Return values
boolscalarizedIdentifierValues()
Returns the entity identity as `[fieldName => scalar]`.
private
scalarizedIdentifierValues(object $entity, ClassMetadata<string|int, object> $metadata) : array<string, mixed>
An association-aware replacement for the raw ClassMetadata::getIdentifierValues(),
which returns objects for a composite association identity (e.g. ParameterRuleSource
= rule + parameter, both #[ORM\Id]).
IMPORTANT: use it everywhere a ['id' => ...] reference is emitted into a wire payload.
An uninitialized native lazy proxy serializes to JSON empty, giving a stub
{"id":{"rule":[],...}}; the consumer then builds the key '-', hits "Reference not
found" and silently drops the collection item (dead parameter rules). Whether it worked
depended on whether the request identity map had already initialized the proxy.
Parameters
- $entity : object
- $metadata : ClassMetadata<string|int, object>
Return values
array<string, mixed>setAssociationValue()
private
setAssociationValue(object $entity, string $field, object|null $value) : void
Parameters
- $entity : object
- $field : string
- $value : object|null
setOwnerBackReference()
Sets the child's back-reference to the parent (the owning FK of a `mappedBy` collection).
private
setOwnerBackReference(object $entity, string $field, object $owner, ClassMetadata<string|int, object> $metadata) : void
Unlike associations from the payload it is always wanted: without it the child INSERT ends with a null FK. When the entity has no setter, it is set through the Doctrine metadata instead of being skipped silently.
Parameters
- $entity : object
- $field : string
- $owner : object
- $metadata : ClassMetadata<string|int, object>
toCamelCase()
private
toCamelCase(string $fieldName) : string
Parameters
- $fieldName : string