Deprecated : Constant E_STRICT is deprecated in /home/pastorz/old-espace-client/vendor/symfony/error-handler/ErrorHandler.php on line 58
Deprecated : Constant E_STRICT is deprecated in /home/pastorz/old-espace-client/vendor/symfony/error-handler/ErrorHandler.php on line 76
Symfony Profiler
<?php
declare( strict_types = 1 );
namespace Doctrine\ORM\Mapping ;
use BackedEnum ;
use BadMethodCallException ;
use Doctrine\DBAL\Platforms\AbstractPlatform ;
use Doctrine\DBAL\Types\Type ;
use Doctrine\Deprecations\Deprecation ;
use Doctrine\Instantiator\Instantiator ;
use Doctrine\Instantiator\InstantiatorInterface ;
use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation ;
use Doctrine\ORM\EntityRepository ;
use Doctrine\ORM\Id\AbstractIdGenerator ;
use Doctrine\Persistence\Mapping\ClassMetadata ;
use Doctrine\Persistence\Mapping\ReflectionService ;
use InvalidArgumentException ;
use LogicException ;
use ReflectionClass ;
use ReflectionNamedType ;
use ReflectionProperty ;
use RuntimeException ;
use function array_diff ;
use function array_flip ;
use function array_intersect ;
use function array_keys ;
use function array_map ;
use function array_merge ;
use function array_pop ;
use function array_values ;
use function assert ;
use function class_exists ;
use function count ;
use function enum_exists ;
use function explode ;
use function gettype ;
use function in_array ;
use function interface_exists ;
use function is_array ;
use function is_subclass_of ;
use function ltrim ;
use function method_exists ;
use function spl_object_id ;
use function str_contains ;
use function str_replace ;
use function strtolower ;
use function trait_exists ;
use function trim ;
use const PHP_VERSION_ID ;
/**
* A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
* of an entity and its associations.
*
* Once populated, ClassMetadata instances are usually cached in a serialized form.
*
* <b>IMPORTANT NOTE:</b>
*
* The fields of this class are only public for 2 reasons:
* 1) To allow fast READ access.
* 2) To drastically reduce the size of a serialized instance (private/protected members
* get the whole class name, namespace inclusive, prepended to every property in
* the serialized representation).
*
* @template-covariant T of object
* @template-implements ClassMetadata<T>
* @psalm-import-type AssociationMapping from \Doctrine\ORM\Mapping\ClassMetadata
* @psalm-import-type FieldMapping from \Doctrine\ORM\Mapping\ClassMetadata
* @psalm-import-type EmbeddedClassMapping from \Doctrine\ORM\Mapping\ClassMetadata
* @psalm-import-type JoinColumnData from \Doctrine\ORM\Mapping\ClassMetadata
* @psalm-import-type DiscriminatorColumnMapping from \Doctrine\ORM\Mapping\ClassMetadata
*/
class ClassMetadataInfo implements ClassMetadata
{
/* The inheritance mapping types */
/**
* NONE means the class does not participate in an inheritance hierarchy
* and therefore does not need an inheritance mapping type.
*/
public const INHERITANCE_TYPE_NONE = 1 ;
/**
* JOINED means the class will be persisted according to the rules of
* <tt>Class Table Inheritance</tt>.
*/
public const INHERITANCE_TYPE_JOINED = 2 ;
/**
* SINGLE_TABLE means the class will be persisted according to the rules of
* <tt>Single Table Inheritance</tt>.
*/
public const INHERITANCE_TYPE_SINGLE_TABLE = 3 ;
/**
* TABLE_PER_CLASS means the class will be persisted according to the rules
* of <tt>Concrete Table Inheritance</tt>.
*
* @deprecated
*/
public const INHERITANCE_TYPE_TABLE_PER_CLASS = 4 ;
/* The Id generator types. */
/**
* AUTO means the generator type will depend on what the used platform prefers.
* Offers full portability.
*/
public const GENERATOR_TYPE_AUTO = 1 ;
/**
* SEQUENCE means a separate sequence object will be used. Platforms that do
* not have native sequence support may emulate it. Full portability is currently
* not guaranteed.
*/
public const GENERATOR_TYPE_SEQUENCE = 2 ;
/**
* TABLE means a separate table is used for id generation.
* Offers full portability (in that it results in an exception being thrown
* no matter the platform).
*
* @deprecated no replacement planned
*/
public const GENERATOR_TYPE_TABLE = 3 ;
/**
* IDENTITY means an identity column is used for id generation. The database
* will fill in the id column on insertion. Platforms that do not support
* native identity columns may emulate them. Full portability is currently
* not guaranteed.
*/
public const GENERATOR_TYPE_IDENTITY = 4 ;
/**
* NONE means the class does not have a generated id. That means the class
* must have a natural, manually assigned id.
*/
public const GENERATOR_TYPE_NONE = 5 ;
/**
* UUID means that a UUID/GUID expression is used for id generation. Full
* portability is currently not guaranteed.
*
* @deprecated use an application-side generator instead
*/
public const GENERATOR_TYPE_UUID = 6 ;
/**
* CUSTOM means that customer will use own ID generator that supposedly work
*/
public const GENERATOR_TYPE_CUSTOM = 7 ;
/**
* DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
* by doing a property-by-property comparison with the original data. This will
* be done for all entities that are in MANAGED state at commit-time.
*
* This is the default change tracking policy.
*/
public const CHANGETRACKING_DEFERRED_IMPLICIT = 1 ;
/**
* DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
* by doing a property-by-property comparison with the original data. This will
* be done only for entities that were explicitly saved (through persist() or a cascade).
*/
public const CHANGETRACKING_DEFERRED_EXPLICIT = 2 ;
/**
* NOTIFY means that Doctrine relies on the entities sending out notifications
* when their properties change. Such entity classes must implement
* the <tt>NotifyPropertyChanged</tt> interface.
*/
public const CHANGETRACKING_NOTIFY = 3 ;
/**
* Specifies that an association is to be fetched when it is first accessed.
*/
public const FETCH_LAZY = 2 ;
/**
* Specifies that an association is to be fetched when the owner of the
* association is fetched.
*/
public const FETCH_EAGER = 3 ;
/**
* Specifies that an association is to be fetched lazy (on first access) and that
* commands such as Collection#count, Collection#slice are issued directly against
* the database if the collection is not yet initialized.
*/
public const FETCH_EXTRA_LAZY = 4 ;
/**
* Identifies a one-to-one association.
*/
public const ONE_TO_ONE = 1 ;
/**
* Identifies a many-to-one association.
*/
public const MANY_TO_ONE = 2 ;
/**
* Identifies a one-to-many association.
*/
public const ONE_TO_MANY = 4 ;
/**
* Identifies a many-to-many association.
*/
public const MANY_TO_MANY = 8 ;
/**
* Combined bitmask for to-one (single-valued) associations.
*/
public const TO_ONE = 3 ;
/**
* Combined bitmask for to-many (collection-valued) associations.
*/
public const TO_MANY = 12 ;
/**
* ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks,
*/
public const CACHE_USAGE_READ_ONLY = 1 ;
/**
* Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes.
*/
public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2 ;
/**
* Read Write Attempts to lock the entity before update/delete.
*/
public const CACHE_USAGE_READ_WRITE = 3 ;
/**
* The value of this column is never generated by the database.
*/
public const GENERATED_NEVER = 0 ;
/**
* The value of this column is generated by the database on INSERT, but not on UPDATE.
*/
public const GENERATED_INSERT = 1 ;
/**
* The value of this column is generated by the database on both INSERT and UDPATE statements.
*/
public const GENERATED_ALWAYS = 2 ;
/**
* READ-ONLY: The name of the entity class.
*
* @var string
* @psalm-var class-string<T>
*/
public $name ;
/**
* READ-ONLY: The namespace the entity class is contained in.
*
* @var string
* @todo Not really needed. Usage could be localized.
*/
public $namespace ;
/**
* READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance
* hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same
* as {@link $name}.
*
* @var string
* @psalm-var class-string
*/
public $rootEntityName ;
/**
* READ-ONLY: The definition of custom generator. Only used for CUSTOM
* generator type
*
* The definition has the following structure:
* <code>
* array(
* 'class' => 'ClassName',
* )
* </code>
*
* @todo Merge with tableGeneratorDefinition into generic generatorDefinition
* @var array<string, string>|null
*/
public $customGeneratorDefinition ;
/**
* The name of the custom repository class used for the entity class.
* (Optional).
*
* @var string|null
* @psalm-var ?class-string<EntityRepository>
*/
public $customRepositoryClassName ;
/**
* READ-ONLY: Whether this class describes the mapping of a mapped superclass.
*
* @var bool
*/
public $isMappedSuperclass = false ;
/**
* READ-ONLY: Whether this class describes the mapping of an embeddable class.
*
* @var bool
*/
public $isEmbeddedClass = false ;
/**
* READ-ONLY: The names of the parent <em>entity</em> classes (ancestors), starting with the
* nearest one and ending with the root entity class.
*
* @psalm-var list<class-string>
*/
public $parentClasses = [];
/**
* READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all
* <em>entity</em> subclasses of this class. These may also be abstract classes.
*
* This list is used, for example, to enumerate all necessary tables in JTI when querying for root
* or subclass entities, or to gather all fields comprised in an entity inheritance tree.
*
* For classes that do not use STI/JTI, this list is empty.
*
* Implementation note:
*
* In PHP, there is no general way to discover all subclasses of a given class at runtime. For that
* reason, the list of classes given in the discriminator map at the root entity is considered
* authoritative. The discriminator map must contain all <em>concrete</em> classes that can
* appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract
* entity classes, users are not required to list such classes with a discriminator value.
*
* The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the
* root entity has been loaded.
*
* For subclasses of such root entities, the list can be reused/passed downwards, it only needs to
* be filtered accordingly (only keep remaining subclasses)
*
* @psalm-var list<class-string>
*/
public $subClasses = [];
/**
* READ-ONLY: The names of all embedded classes based on properties.
*
* The value (definition) array may contain, among others, the following values:
*
* - <b>'inherited'</b> (string, optional)
* This is set when this embedded-class field is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* mapping information for this field. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the embedded-class field does not appear for the first time in this class, but is originally
* declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains mapping information for this field.
*
* @psalm-var array<string, EmbeddedClassMapping>
*/
public $embeddedClasses = [];
/**
* READ-ONLY: The named queries allowed to be called directly from Repository.
*
* @psalm-var array<string, array<string, mixed>>
*/
public $namedQueries = [];
/**
* READ-ONLY: The named native queries allowed to be called directly from Repository.
*
* A native SQL named query definition has the following structure:
* <pre>
* array(
* 'name' => <query name>,
* 'query' => <sql query>,
* 'resultClass' => <class of the result>,
* 'resultSetMapping' => <name of a SqlResultSetMapping>
* )
* </pre>
*
* @psalm-var array<string, array<string, mixed>>
*/
public $namedNativeQueries = [];
/**
* READ-ONLY: The mappings of the results of native SQL queries.
*
* A native result mapping definition has the following structure:
* <pre>
* array(
* 'name' => <result name>,
* 'entities' => array(<entity result mapping>),
* 'columns' => array(<column result mapping>)
* )
* </pre>
*
* @psalm-var array<string, array{
* name: string,
* entities: mixed[],
* columns: mixed[]
* }>
*/
public $sqlResultSetMappings = [];
/**
* READ-ONLY: The field names of all fields that are part of the identifier/primary key
* of the mapped entity class.
*
* @psalm-var list<string>
*/
public $identifier = [];
/**
* READ-ONLY: The inheritance mapping type used by the class.
*
* @var int
* @psalm-var self::INHERITANCE_TYPE_*
*/
public $inheritanceType = self :: INHERITANCE_TYPE_NONE ;
/**
* READ-ONLY: The Id generator type used by the class.
*
* @var int
* @psalm-var self::GENERATOR_TYPE_*
*/
public $generatorType = self :: GENERATOR_TYPE_NONE ;
/**
* READ-ONLY: The field mappings of the class.
* Keys are field names and values are mapping definitions.
*
* The mapping definition array has the following values:
*
* - <b>fieldName</b> (string)
* The name of the field in the Entity.
*
* - <b>type</b> (string)
* The type name of the mapped field. Can be one of Doctrine's mapping types
* or a custom mapping type.
*
* - <b>columnName</b> (string, optional)
* The column name. Optional. Defaults to the field name.
*
* - <b>length</b> (integer, optional)
* The database length of the column. Optional. Default value taken from
* the type.
*
* - <b>id</b> (boolean, optional)
* Marks the field as the primary key of the entity. Multiple fields of an
* entity can have the id attribute, forming a composite key.
*
* - <b>nullable</b> (boolean, optional)
* Whether the column is nullable. Defaults to FALSE.
*
* - <b>'notInsertable'</b> (boolean, optional)
* Whether the column is not insertable. Optional. Is only set if value is TRUE.
*
* - <b>'notUpdatable'</b> (boolean, optional)
* Whether the column is updatable. Optional. Is only set if value is TRUE.
*
* - <b>columnDefinition</b> (string, optional, schema-only)
* The SQL fragment that is used when generating the DDL for the column.
*
* - <b>precision</b> (integer, optional, schema-only)
* The precision of a decimal column. Only valid if the column type is decimal.
*
* - <b>scale</b> (integer, optional, schema-only)
* The scale of a decimal column. Only valid if the column type is decimal.
*
* - <b>'unique'</b> (boolean, optional, schema-only)
* Whether a unique constraint should be generated for the column.
*
* - <b>'inherited'</b> (string, optional)
* This is set when the field is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* mapping information for this field. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* Fields initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the field does not appear for the first time in this class, but is originally
* declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains mapping information for this field.
*
* @var mixed[]
* @psalm-var array<string, FieldMapping>
*/
public $fieldMappings = [];
/**
* READ-ONLY: An array of field names. Used to look up field names from column names.
* Keys are column names and values are field names.
*
* @psalm-var array<string, string>
*/
public $fieldNames = [];
/**
* READ-ONLY: A map of field names to column names. Keys are field names and values column names.
* Used to look up column names from field names.
* This is the reverse lookup map of $_fieldNames.
*
* @deprecated 3.0 Remove this.
*
* @var mixed[]
*/
public $columnNames = [];
/**
* READ-ONLY: The discriminator value of this class.
*
* <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
* where a discriminator column is used.</b>
*
* @see discriminatorColumn
*
* @var mixed
*/
public $discriminatorValue ;
/**
* READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
*
* <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
* where a discriminator column is used.</b>
*
* @see discriminatorColumn
*
* @var array<int|string, string>
*
* @psalm-var array<int|string, class-string>
*/
public $discriminatorMap = [];
/**
* READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
* inheritance mappings.
*
* @var array<string, mixed>
* @psalm-var DiscriminatorColumnMapping|null
*/
public $discriminatorColumn ;
/**
* READ-ONLY: The primary table definition. The definition is an array with the
* following entries:
*
* name => <tableName>
* schema => <schemaName>
* indexes => array
* uniqueConstraints => array
*
* @var mixed[]
* @psalm-var array{
* name: string,
* schema?: string,
* indexes?: array,
* uniqueConstraints?: array,
* options?: array<string, mixed>,
* quoted?: bool
* }
*/
public $table ;
/**
* READ-ONLY: The registered lifecycle callbacks for entities of this class.
*
* @psalm-var array<string, list<string>>
*/
public $lifecycleCallbacks = [];
/**
* READ-ONLY: The registered entity listeners.
*
* @psalm-var array<string, list<array{class: class-string, method: string}>>
*/
public $entityListeners = [];
/**
* READ-ONLY: The association mappings of this class.
*
* The mapping definition array supports the following keys:
*
* - <b>fieldName</b> (string)
* The name of the field in the entity the association is mapped to.
*
* - <b>sourceEntity</b> (string)
* The class name of the source entity. In the case of to-many associations initially
* present in mapped superclasses, the nearest <em>entity</em> subclasses will be
* considered the respective source entities.
*
* - <b>targetEntity</b> (string)
* The class name of the target entity. If it is fully-qualified it is used as is.
* If it is a simple, unqualified class name the namespace is assumed to be the same
* as the namespace of the source entity.
*
* - <b>mappedBy</b> (string, required for bidirectional associations)
* The name of the field that completes the bidirectional association on the owning side.
* This key must be specified on the inverse side of a bidirectional association.
*
* - <b>inversedBy</b> (string, required for bidirectional associations)
* The name of the field that completes the bidirectional association on the inverse side.
* This key must be specified on the owning side of a bidirectional association.
*
* - <b>cascade</b> (array, optional)
* The names of persistence operations to cascade on the association. The set of possible
* values are: "persist", "remove", "detach", "merge", "refresh", "all" (implies all others).
*
* - <b>orderBy</b> (array, one-to-many/many-to-many only)
* A map of field names (of the target entity) to sorting directions (ASC/DESC).
* Example: array('priority' => 'desc')
*
* - <b>fetch</b> (integer, optional)
* The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
* Possible values are: ClassMetadata::FETCH_EAGER, ClassMetadata::FETCH_LAZY.
*
* - <b>joinTable</b> (array, optional, many-to-many only)
* Specification of the join table and its join columns (foreign keys).
* Only valid for many-to-many mappings. Note that one-to-many associations can be mapped
* through a join table by simply mapping the association as many-to-many with a unique
* constraint on the join table.
*
* - <b>indexBy</b> (string, optional, to-many only)
* Specification of a field on target-entity that is used to index the collection by.
* This field HAS to be either the primary key or a unique column. Otherwise the collection
* does not contain all the entities that are actually related.
*
* - <b>'inherited'</b> (string, optional)
* This is set when the association is inherited by this class from another (inheritance) parent
* <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
* this association. (If there are transient classes in the
* class hierarchy, these are ignored, so the class property may in fact come
* from a class further up in the PHP class hierarchy.)
* To-many associations initially declared in mapped superclasses are
* <em>not</em> considered 'inherited' in the nearest entity subclasses.
*
* - <b>'declared'</b> (string, optional)
* This is set when the association does not appear in the current class for the first time, but
* is initially declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
* of the topmost non-transient class that contains association information for this relationship.
*
* A join table definition has the following structure:
* <pre>
* array(
* 'name' => <join table name>,
* 'joinColumns' => array(<join column mapping from join table to source table>),
* 'inverseJoinColumns' => array(<join column mapping from join table to target table>)
* )
* </pre>
*
* @psalm-var array<string, AssociationMapping>
*/
public $associationMappings = [];
/**
* READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
*
* @var bool
*/
public $isIdentifierComposite = false ;
/**
* READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association.
*
* This flag is necessary because some code blocks require special treatment of this cases.
*
* @var bool
*/
public $containsForeignIdentifier = false ;
/**
* READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type.
*
* This flag is necessary because some code blocks require special treatment of this cases.
*
* @var bool
*/
public $containsEnumIdentifier = false ;
/**
* READ-ONLY: The ID generator used for generating IDs for this class.
*
* @var AbstractIdGenerator
* @todo Remove!
*/
public $idGenerator ;
/**
* READ-ONLY: The definition of the sequence generator of this class. Only used for the
* SEQUENCE generation strategy.
*
* The definition has the following structure:
* <code>
* array(
* 'sequenceName' => 'name',
* 'allocationSize' => '20',
* 'initialValue' => '1'
* )
* </code>
*
* @var array<string, mixed>|null
* @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
* @todo Merge with tableGeneratorDefinition into generic generatorDefinition
*/
public $sequenceGeneratorDefinition ;
/**
* READ-ONLY: The definition of the table generator of this class. Only used for the
* TABLE generation strategy.
*
* @deprecated
*
* @var array<string, mixed>
*/
public $tableGeneratorDefinition ;
/**
* READ-ONLY: The policy used for change-tracking on entities of this class.
*
* @var int
*/
public $changeTrackingPolicy = self :: CHANGETRACKING_DEFERRED_IMPLICIT ;
/**
* READ-ONLY: A Flag indicating whether one or more columns of this class
* have to be reloaded after insert / update operations.
*
* @var bool
*/
public $requiresFetchAfterChange = false ;
/**
* READ-ONLY: A flag for whether or not instances of this class are to be versioned
* with optimistic locking.
*
* @var bool
*/
public $isVersioned = false ;
/**
* READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
*
* @var string|null
*/
public $versionField ;
/** @var mixed[]|null */
public $cache ;
/**
* The ReflectionClass instance of the mapped class.
*
* @var ReflectionClass|null
*/
public $reflClass ;
/**
* Is this entity marked as "read-only"?
*
* That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance
* optimization for entities that are immutable, either in your domain or through the relation database
* (coming from a view, or a history table for example).
*
* @var bool
*/
public $isReadOnly = false ;
/**
* NamingStrategy determining the default column and table names.
*
* @var NamingStrategy
*/
protected $namingStrategy ;
/**
* The ReflectionProperty instances of the mapped class.
*
* @var array<string, ReflectionProperty|null>
*/
public $reflFields = [];
/** @var InstantiatorInterface|null */
private $instantiator ;
/** @var TypedFieldMapper $typedFieldMapper */
private $typedFieldMapper ;
/**
* Initializes a new ClassMetadata instance that will hold the object-relational mapping
* metadata of the class with the given name.
*
* @param string $entityName The name of the entity class the new instance is used for.
* @psalm-param class-string<T> $entityName
*/
public function __construct ( $entityName , ? NamingStrategy $namingStrategy = null , ? TypedFieldMapper $typedFieldMapper = null )
{
$this -> name = $entityName ;
$this -> rootEntityName = $entityName ;
$this -> namingStrategy = $namingStrategy ?? new DefaultNamingStrategy ();
$this -> instantiator = new Instantiator ();
$this -> typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper ();
}
/**
* Gets the ReflectionProperties of the mapped class.
*
* @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
* @psalm-return array<ReflectionProperty|null>
*/
public function getReflectionProperties ()
{
return $this -> reflFields ;
}
/**
* Gets a ReflectionProperty for a specific field of the mapped class.
*
* @param string $name
*
* @return ReflectionProperty
*/
public function getReflectionProperty ( $name )
{
return $this -> reflFields [ $name ];
}
/**
* Gets the ReflectionProperty for the single identifier field.
*
* @return ReflectionProperty
*
* @throws BadMethodCallException If the class has a composite identifier.
*/
public function getSingleIdReflectionProperty ()
{
if ($this -> isIdentifierComposite ) {
throw new BadMethodCallException ( 'Class ' . $this -> name . ' has a composite identifier.' );
}
return $this -> reflFields [ $this -> identifier [ 0 ]];
}
/**
* Extracts the identifier values of an entity of this class.
*
* For composite identifiers, the identifier values are returned as an array
* with the same order as the field order in {@link identifier}.
*
* @param object $entity
*
* @return array<string, mixed>
*/
public function getIdentifierValues ( $entity )
{
if ($this -> isIdentifierComposite ) {
$id = [];
foreach ($this -> identifier as $idField ) {
$value = $this -> reflFields [ $idField ]-> getValue ( $entity );
if ($value !== null ) {
$id [ $idField ] = $value ;
}
}
return $id ;
}
$id = $this -> identifier [ 0 ];
$value = $this -> reflFields [ $id ]-> getValue ( $entity );
if ($value === null ) {
return [];
}
return [$id => $value ];
}
/**
* Populates the entity identifier of an entity.
*
* @param object $entity
* @psalm-param array<string, mixed> $id
*
* @return void
*
* @todo Rename to assignIdentifier()
*/
public function setIdentifierValues ( $entity , array $id )
{
foreach ($id as $idField => $idValue ) {
$this -> reflFields [ $idField ]-> setValue ( $entity , $idValue );
}
}
/**
* Sets the specified field to the specified value on the given entity.
*
* @param object $entity
* @param string $field
* @param mixed $value
*
* @return void
*/
public function setFieldValue ( $entity , $field , $value )
{
$this -> reflFields [ $field ]-> setValue ( $entity , $value );
}
/**
* Gets the specified field's value off the given entity.
*
* @param object $entity
* @param string $field
*
* @return mixed
*/
public function getFieldValue ( $entity , $field )
{
return $this -> reflFields [ $field ]-> getValue ( $entity );
}
/**
* Creates a string representation of this instance.
*
* @return string The string representation of this instance.
*
* @todo Construct meaningful string representation.
*/
public function __toString ()
{
return self ::class . '@' . spl_object_id ( $this );
}
/**
* Determines which fields get serialized.
*
* It is only serialized what is necessary for best unserialization performance.
* That means any metadata properties that are not set or empty or simply have
* their default value are NOT serialized.
*
* Parts that are also NOT serialized because they can not be properly unserialized:
* - reflClass (ReflectionClass)
* - reflFields (ReflectionProperty array)
*
* @return string[] The names of all the fields that should be serialized.
*/
public function __sleep ()
{
// This metadata is always serialized/cached.
$serialized = [
'associationMappings' ,
'columnNames' , //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName']
'fieldMappings' ,
'fieldNames' ,
'embeddedClasses' ,
'identifier' ,
'isIdentifierComposite' , // TODO: REMOVE
'name' ,
'namespace' , // TODO: REMOVE
'table' ,
'rootEntityName' ,
'idGenerator' , //TODO: Does not really need to be serialized. Could be moved to runtime.
];
// The rest of the metadata is only serialized if necessary.
if ( $this -> changeTrackingPolicy !== self :: CHANGETRACKING_DEFERRED_IMPLICIT ) {
$serialized [] = 'changeTrackingPolicy' ;
}
if ($this -> customRepositoryClassName ) {
$serialized [] = 'customRepositoryClassName' ;
}
if ($this -> inheritanceType !== self :: INHERITANCE_TYPE_NONE ) {
$serialized [] = 'inheritanceType' ;
$serialized [] = 'discriminatorColumn' ;
$serialized [] = 'discriminatorValue' ;
$serialized [] = 'discriminatorMap' ;
$serialized [] = 'parentClasses' ;
$serialized [] = 'subClasses' ;
}
if ($this -> generatorType !== self :: GENERATOR_TYPE_NONE ) {
$serialized [] = 'generatorType' ;
if ($this -> generatorType === self :: GENERATOR_TYPE_SEQUENCE ) {
$serialized [] = 'sequenceGeneratorDefinition' ;
}
}
if ($this -> isMappedSuperclass ) {
$serialized [] = 'isMappedSuperclass' ;
}
if ($this -> isEmbeddedClass ) {
$serialized [] = 'isEmbeddedClass' ;
}
if ($this -> containsForeignIdentifier ) {
$serialized [] = 'containsForeignIdentifier' ;
}
if ($this -> containsEnumIdentifier ) {
$serialized [] = 'containsEnumIdentifier' ;
}
if ($this -> isVersioned ) {
$serialized [] = 'isVersioned' ;
$serialized [] = 'versionField' ;
}
if ($this -> lifecycleCallbacks ) {
$serialized [] = 'lifecycleCallbacks' ;
}
if ($this -> entityListeners ) {
$serialized [] = 'entityListeners' ;
}
if ($this -> namedQueries ) {
$serialized [] = 'namedQueries' ;
}
if ($this -> namedNativeQueries ) {
$serialized [] = 'namedNativeQueries' ;
}
if ($this -> sqlResultSetMappings ) {
$serialized [] = 'sqlResultSetMappings' ;
}
if ($this -> isReadOnly ) {
$serialized [] = 'isReadOnly' ;
}
if ($this -> customGeneratorDefinition ) {
$serialized [] = 'customGeneratorDefinition' ;
}
if ($this -> cache ) {
$serialized [] = 'cache' ;
}
if ($this -> requiresFetchAfterChange ) {
$serialized [] = 'requiresFetchAfterChange' ;
}
return $serialized ;
}
/**
* Creates a new instance of the mapped class, without invoking the constructor.
*
* @return object
*/
public function newInstance ()
{
return $this -> instantiator -> instantiate ( $this -> name );
}
/**
* Restores some state that can not be serialized/unserialized.
*
* @param ReflectionService $reflService
*
* @return void
*/
public function wakeupReflection ( $reflService )
{
// Restore ReflectionClass and properties
$this -> reflClass = $reflService -> getClass ( $this -> name );
$this -> instantiator = $this -> instantiator ?: new Instantiator ();
$parentReflFields = [];
foreach ($this -> embeddedClasses as $property => $embeddedClass ) {
if (isset($embeddedClass [ 'declaredField' ])) {
assert ( $embeddedClass [ 'originalField' ] !== null );
$childProperty = $this -> getAccessibleProperty (
$reflService ,
$this -> embeddedClasses [ $embeddedClass [ 'declaredField' ]][ 'class' ],
$embeddedClass [ 'originalField' ]
);
assert ( $childProperty !== null );
$parentReflFields [ $property ] = new ReflectionEmbeddedProperty (
$parentReflFields [ $embeddedClass [ 'declaredField' ]],
$childProperty ,
$this -> embeddedClasses [ $embeddedClass [ 'declaredField' ]][ 'class' ]
);
continue;
}
$fieldRefl = $this -> getAccessibleProperty (
$reflService ,
$embeddedClass [ 'declared' ] ?? $this -> name ,
$property
);
$parentReflFields [ $property ] = $fieldRefl ;
$this -> reflFields [ $property ] = $fieldRefl ;
}
foreach ($this -> fieldMappings as $field => $mapping ) {
if (isset($mapping [ 'declaredField' ]) && isset( $parentReflFields [ $mapping [ 'declaredField' ]])) {
$childProperty = $this -> getAccessibleProperty ( $reflService , $mapping [ 'originalClass' ], $mapping [ 'originalField' ]);
assert ( $childProperty !== null );
if (isset($mapping [ 'enumType' ])) {
$childProperty = new ReflectionEnumProperty (
$childProperty ,
$mapping [ 'enumType' ]
);
}
$this -> reflFields [ $field ] = new ReflectionEmbeddedProperty (
$parentReflFields [ $mapping [ 'declaredField' ]],
$childProperty ,
$mapping [ 'originalClass' ]
);
continue;
}
$this -> reflFields [ $field ] = isset( $mapping [ 'declared' ])
? $this -> getAccessibleProperty ( $reflService , $mapping [ 'declared' ], $field )
: $this -> getAccessibleProperty ( $reflService , $this -> name , $field );
if (isset($mapping [ 'enumType' ]) && $this -> reflFields [ $field ] !== null ) {
$this -> reflFields [ $field ] = new ReflectionEnumProperty (
$this -> reflFields [ $field ],
$mapping [ 'enumType' ]
);
}
}
foreach ($this -> associationMappings as $field => $mapping ) {
$this -> reflFields [ $field ] = isset( $mapping [ 'declared' ])
? $this -> getAccessibleProperty ( $reflService , $mapping [ 'declared' ], $field )
: $this -> getAccessibleProperty ( $reflService , $this -> name , $field );
}
}
/**
* Initializes a new ClassMetadata instance that will hold the object-relational mapping
* metadata of the class with the given name.
*
* @param ReflectionService $reflService The reflection service.
*
* @return void
*/
public function initializeReflection ( $reflService )
{
$this -> reflClass = $reflService -> getClass ( $this -> name );
$this -> namespace = $reflService -> getClassNamespace ( $this -> name );
if ($this -> reflClass ) {
$this -> name = $this -> rootEntityName = $this -> reflClass -> name ;
}
$this -> table [ 'name' ] = $this -> namingStrategy -> classToTableName ( $this -> name );
}
/**
* Validates Identifier.
*
* @return void
*
* @throws MappingException
*/
public function validateIdentifier ()
{
if ($this -> isMappedSuperclass || $this -> isEmbeddedClass ) {
return;
}
// Verify & complete identifier mapping
if (! $this -> identifier ) {
throw MappingException :: identifierRequired ( $this -> name );
}
if ($this -> usesIdGenerator () && $this -> isIdentifierComposite ) {
throw MappingException :: compositeKeyAssignedIdGeneratorRequired ( $this -> name );
}
}
/**
* Validates association targets actually exist.
*
* @return void
*
* @throws MappingException
*/
public function validateAssociations ()
{
foreach ($this -> associationMappings as $mapping ) {
if (
! class_exists ( $mapping [ 'targetEntity' ])
&& ! interface_exists ( $mapping [ 'targetEntity' ])
&& ! trait_exists ( $mapping [ 'targetEntity' ])
) {
throw MappingException :: invalidTargetEntityClass ( $mapping [ 'targetEntity' ], $this -> name , $mapping [ 'fieldName' ]);
}
}
}
/**
* Validates lifecycle callbacks.
*
* @param ReflectionService $reflService
*
* @return void
*
* @throws MappingException
*/
public function validateLifecycleCallbacks ( $reflService )
{
foreach ($this -> lifecycleCallbacks as $callbacks ) {
foreach ($callbacks as $callbackFuncName ) {
if (! $reflService -> hasPublicMethod ( $this -> name , $callbackFuncName )) {
throw MappingException :: lifecycleCallbackMethodNotFound ( $this -> name , $callbackFuncName );
}
}
}
}
/**
* {@inheritDoc}
*/
public function getReflectionClass ()
{
return $this -> reflClass ;
}
/**
* @psalm-param array{usage?: mixed, region?: mixed} $cache
*
* @return void
*/
public function enableCache (array $cache )
{
if (! isset($cache [ 'usage' ])) {
$cache [ 'usage' ] = self :: CACHE_USAGE_READ_ONLY ;
}
if (! isset($cache [ 'region' ])) {
$cache [ 'region' ] = strtolower ( str_replace ( '\\' , '_' , $this -> rootEntityName ));
}
$this -> cache = $cache ;
}
/**
* @param string $fieldName
* @psalm-param array{usage?: int, region?: string} $cache
*
* @return void
*/
public function enableAssociationCache ( $fieldName , array $cache )
{
$this -> associationMappings [ $fieldName ][ 'cache' ] = $this -> getAssociationCacheDefaults ( $fieldName , $cache );
}
/**
* @param string $fieldName
* @param array $cache
* @psalm-param array{usage?: int|null, region?: string|null} $cache
*
* @return int[]|string[]
* @psalm-return array{usage: int, region: string|null}
*/
public function getAssociationCacheDefaults ( $fieldName , array $cache )
{
if (! isset($cache [ 'usage' ])) {
$cache [ 'usage' ] = $this -> cache [ 'usage' ] ?? self :: CACHE_USAGE_READ_ONLY ;
}
if (! isset($cache [ 'region' ])) {
$cache [ 'region' ] = strtolower ( str_replace ( '\\' , '_' , $this -> rootEntityName )) . '__' . $fieldName ;
}
return $cache ;
}
/**
* Sets the change tracking policy used by this class.
*
* @param int $policy
*
* @return void
*/
public function setChangeTrackingPolicy ( $policy )
{
$this -> changeTrackingPolicy = $policy ;
}
/**
* Whether the change tracking policy of this class is "deferred explicit".
*
* @return bool
*/
public function isChangeTrackingDeferredExplicit ()
{
return $this -> changeTrackingPolicy === self :: CHANGETRACKING_DEFERRED_EXPLICIT ;
}
/**
* Whether the change tracking policy of this class is "deferred implicit".
*
* @return bool
*/
public function isChangeTrackingDeferredImplicit ()
{
return $this -> changeTrackingPolicy === self :: CHANGETRACKING_DEFERRED_IMPLICIT ;
}
/**
* Whether the change tracking policy of this class is "notify".
*
* @return bool
*/
public function isChangeTrackingNotify ()
{
return $this -> changeTrackingPolicy === self :: CHANGETRACKING_NOTIFY ;
}
/**
* Checks whether a field is part of the identifier/primary key field(s).
*
* @param string $fieldName The field name.
*
* @return bool TRUE if the field is part of the table identifier/primary key field(s),
* FALSE otherwise.
*/
public function isIdentifier ( $fieldName )
{
if (! $this -> identifier ) {
return false ;
}
if (! $this -> isIdentifierComposite ) {
return $fieldName === $this -> identifier [ 0 ];
}
return in_array ( $fieldName , $this -> identifier , true );
}
/**
* Checks if the field is unique.
*
* @param string $fieldName The field name.
*
* @return bool TRUE if the field is unique, FALSE otherwise.
*/
public function isUniqueField ( $fieldName )
{
$mapping = $this -> getFieldMapping ( $fieldName );
return $mapping !== false && isset( $mapping [ 'unique' ]) && $mapping [ 'unique' ];
}
/**
* Checks if the field is not null.
*
* @param string $fieldName The field name.
*
* @return bool TRUE if the field is not null, FALSE otherwise.
*/
public function isNullable ( $fieldName )
{
$mapping = $this -> getFieldMapping ( $fieldName );
return $mapping !== false && isset( $mapping [ 'nullable' ]) && $mapping [ 'nullable' ];
}
/**
* Gets a column name for a field name.
* If the column name for the field cannot be found, the given field name
* is returned.
*
* @param string $fieldName The field name.
*
* @return string The column name.
*/
public function getColumnName ( $fieldName )
{
return $this -> columnNames [ $fieldName ] ?? $fieldName ;
}
/**
* Gets the mapping of a (regular) field that holds some data but not a
* reference to another object.
*
* @param string $fieldName The field name.
*
* @return mixed[] The field mapping.
* @psalm-return FieldMapping
*
* @throws MappingException
*/
public function getFieldMapping ( $fieldName )
{
if (! isset($this -> fieldMappings [ $fieldName ])) {
throw MappingException :: mappingNotFound ( $this -> name , $fieldName );
}
return $this -> fieldMappings [ $fieldName ];
}
/**
* Gets the mapping of an association.
*
* @see ClassMetadataInfo::$associationMappings
*
* @param string $fieldName The field name that represents the association in
* the object model.
*
* @return mixed[] The mapping.
* @psalm-return AssociationMapping
*
* @throws MappingException
*/
public function getAssociationMapping ( $fieldName )
{
if (! isset($this -> associationMappings [ $fieldName ])) {
throw MappingException :: mappingNotFound ( $this -> name , $fieldName );
}
return $this -> associationMappings [ $fieldName ];
}
/**
* Gets all association mappings of the class.
*
* @psalm-return array<string, AssociationMapping>
*/
public function getAssociationMappings ()
{
return $this -> associationMappings ;
}
/**
* Gets the field name for a column name.
* If no field name can be found the column name is returned.
*
* @param string $columnName The column name.
*
* @return string The column alias.
*/
public function getFieldName ( $columnName )
{
return $this -> fieldNames [ $columnName ] ?? $columnName ;
}
/**
* Gets the named query.
*
* @see ClassMetadataInfo::$namedQueries
*
* @param string $queryName The query name.
*
* @return string
*
* @throws MappingException
*/
public function getNamedQuery ( $queryName )
{
if (! isset($this -> namedQueries [ $queryName ])) {
throw MappingException :: queryNotFound ( $this -> name , $queryName );
}
return $this -> namedQueries [ $queryName ][ 'dql' ];
}
/**
* Gets all named queries of the class.
*
* @return mixed[][]
* @psalm-return array<string, array<string, mixed>>
*/
public function getNamedQueries ()
{
return $this -> namedQueries ;
}
/**
* Gets the named native query.
*
* @see ClassMetadataInfo::$namedNativeQueries
*
* @param string $queryName The query name.
*
* @return mixed[]
* @psalm-return array<string, mixed>
*
* @throws MappingException
*/
public function getNamedNativeQuery ( $queryName )
{
if (! isset($this -> namedNativeQueries [ $queryName ])) {
throw MappingException :: queryNotFound ( $this -> name , $queryName );
}
return $this -> namedNativeQueries [ $queryName ];
}
/**
* Gets all named native queries of the class.
*
* @psalm-return array<string, array<string, mixed>>
*/
public function getNamedNativeQueries ()
{
return $this -> namedNativeQueries ;
}
/**
* Gets the result set mapping.
*
* @see ClassMetadataInfo::$sqlResultSetMappings
*
* @param string $name The result set mapping name.
*
* @return mixed[]
* @psalm-return array{name: string, entities: array, columns: array}
*
* @throws MappingException
*/
public function getSqlResultSetMapping ( $name )
{
if (! isset($this -> sqlResultSetMappings [ $name ])) {
throw MappingException :: resultMappingNotFound ( $this -> name , $name );
}
return $this -> sqlResultSetMappings [ $name ];
}
/**
* Gets all sql result set mappings of the class.
*
* @return mixed[]
* @psalm-return array<string, array{name: string, entities: array, columns: array}>
*/
public function getSqlResultSetMappings ()
{
return $this -> sqlResultSetMappings ;
}
/**
* Checks whether given property has type
*
* @param string $name Property name
*/
private function isTypedProperty ( string $name ): bool
{
return PHP_VERSION_ID >= 70400
&& isset( $this -> reflClass )
&& $this -> reflClass -> hasProperty ( $name )
&& $this -> reflClass -> getProperty ( $name )-> hasType ();
}
/**
* Validates & completes the given field mapping based on typed property.
*
* @param array{fieldName: string, type?: string} $mapping The field mapping to validate & complete.
*
* @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping.
*/
private function validateAndCompleteTypedFieldMapping (array $mapping ): array
{
$field = $this -> reflClass -> getProperty ( $mapping [ 'fieldName' ]);
$mapping = $this -> typedFieldMapper -> validateAndComplete ( $mapping , $field );
return $mapping ;
}
/**
* Validates & completes the basic mapping information based on typed property.
*
* @param array{type: self::ONE_TO_ONE|self::MANY_TO_ONE|self::ONE_TO_MANY|self::MANY_TO_MANY, fieldName: string, targetEntity?: class-string} $mapping The mapping.
*
* @return mixed[] The updated mapping.
*/
private function validateAndCompleteTypedAssociationMapping (array $mapping ): array
{
$type = $this -> reflClass -> getProperty ( $mapping [ 'fieldName' ])-> getType ();
if ($type === null || ( $mapping [ 'type' ] & self :: TO_ONE ) === 0 ) {
return $mapping ;
}
if (! isset($mapping [ 'targetEntity' ]) && $type instanceof ReflectionNamedType ) {
$mapping [ 'targetEntity' ] = $type -> getName ();
}
return $mapping ;
}
/**
* Validates & completes the given field mapping.
*
* @psalm-param array{
* fieldName?: string,
* columnName?: string,
* id?: bool,
* generated?: int,
* enumType?: class-string,
* } $mapping The field mapping to validate & complete.
*
* @return FieldMapping The updated mapping.
*
* @throws MappingException
*/
protected function validateAndCompleteFieldMapping (array $mapping ): array
{
// Check mandatory fields
if (! isset( $mapping [ 'fieldName' ]) || ! $mapping [ 'fieldName' ]) {
throw MappingException :: missingFieldName ( $this -> name );
}
if ($this -> isTypedProperty ( $mapping [ 'fieldName' ])) {
$mapping = $this -> validateAndCompleteTypedFieldMapping ( $mapping );
}
if (! isset($mapping [ 'type' ])) {
// Default to string
$mapping [ 'type' ] = 'string' ;
}
// Complete fieldName and columnName mapping
if (! isset( $mapping [ 'columnName' ])) {
$mapping [ 'columnName' ] = $this -> namingStrategy -> propertyToColumnName ( $mapping [ 'fieldName' ], $this -> name );
}
if ($mapping [ 'columnName' ][ 0 ] === '`' ) {
$mapping [ 'columnName' ] = trim ( $mapping [ 'columnName' ], '`' );
$mapping [ 'quoted' ] = true ;
}
$this -> columnNames [ $mapping [ 'fieldName' ]] = $mapping [ 'columnName' ];
if (isset($this -> fieldNames [ $mapping [ 'columnName' ]]) || ( $this -> discriminatorColumn && $this -> discriminatorColumn [ 'name' ] === $mapping [ 'columnName' ])) {
throw MappingException :: duplicateColumnName ( $this -> name , $mapping [ 'columnName' ]);
}
$this -> fieldNames [ $mapping [ 'columnName' ]] = $mapping [ 'fieldName' ];
// Complete id mapping
if (isset( $mapping [ 'id' ]) && $mapping [ 'id' ] === true ) {
if ($this -> versionField === $mapping [ 'fieldName' ]) {
throw MappingException :: cannotVersionIdField ( $this -> name , $mapping [ 'fieldName' ]);
}
if (! in_array ( $mapping [ 'fieldName' ], $this -> identifier , true )) {
$this -> identifier [] = $mapping [ 'fieldName' ];
}
// Check for composite key
if (! $this -> isIdentifierComposite && count ( $this -> identifier ) > 1 ) {
$this -> isIdentifierComposite = true ;
}
}
if (Type :: hasType ( $mapping [ 'type' ]) && Type :: getType ( $mapping [ 'type' ])-> canRequireSQLConversion ()) {
if (isset($mapping [ 'id' ]) && $mapping [ 'id' ] === true ) {
throw MappingException :: sqlConversionNotAllowedForIdentifiers ( $this -> name , $mapping [ 'fieldName' ], $mapping [ 'type' ]);
}
$mapping [ 'requireSQLConversion' ] = true ;
}
if (isset($mapping [ 'generated' ])) {
if (! in_array ( $mapping [ 'generated' ], [ self :: GENERATED_NEVER , self :: GENERATED_INSERT , self :: GENERATED_ALWAYS ])) {
throw MappingException :: invalidGeneratedMode ( $mapping [ 'generated' ]);
}
if ($mapping [ 'generated' ] === self :: GENERATED_NEVER ) {
unset($mapping [ 'generated' ]);
}
}
if (isset($mapping [ 'enumType' ])) {
if (PHP_VERSION_ID < 80100 ) {
throw MappingException :: enumsRequirePhp81 ( $this -> name , $mapping [ 'fieldName' ]);
}
if (! enum_exists ( $mapping [ 'enumType' ])) {
throw MappingException :: nonEnumTypeMapped ( $this -> name , $mapping [ 'fieldName' ], $mapping [ 'enumType' ]);
}
if (! empty($mapping [ 'id' ])) {
$this -> containsEnumIdentifier = true ;
}
}
return $mapping ;
}
/**
* Validates & completes the basic mapping information that is common to all
* association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
*
* @psalm-param array<string, mixed> $mapping The mapping.
*
* @return mixed[] The updated mapping.
* @psalm-return AssociationMapping
*
* @throws MappingException If something is wrong with the mapping.
*/
protected function _validateAndCompleteAssociationMapping (array $mapping )
{
if (! isset($mapping [ 'mappedBy' ])) {
$mapping [ 'mappedBy' ] = null ;
}
if (! isset($mapping [ 'inversedBy' ])) {
$mapping [ 'inversedBy' ] = null ;
}
$mapping [ 'isOwningSide' ] = true ; // assume owning side until we hit mappedBy
if (empty( $mapping [ 'indexBy' ])) {
unset($mapping [ 'indexBy' ]);
}
// If targetEntity is unqualified, assume it is in the same namespace as
// the sourceEntity.
$mapping [ 'sourceEntity' ] = $this -> name ;
if ($this -> isTypedProperty ( $mapping [ 'fieldName' ])) {
$mapping = $this -> validateAndCompleteTypedAssociationMapping ( $mapping );
}
if (isset($mapping [ 'targetEntity' ])) {
$mapping [ 'targetEntity' ] = $this -> fullyQualifiedClassName ( $mapping [ 'targetEntity' ]);
$mapping [ 'targetEntity' ] = ltrim ( $mapping [ 'targetEntity' ], '\\' );
}
if (($mapping [ 'type' ] & self :: MANY_TO_ONE ) > 0 && isset( $mapping [ 'orphanRemoval' ]) && $mapping [ 'orphanRemoval' ]) {
throw MappingException :: illegalOrphanRemoval ( $this -> name , $mapping [ 'fieldName' ]);
}
// Complete id mapping
if (isset( $mapping [ 'id' ]) && $mapping [ 'id' ] === true ) {
if (isset($mapping [ 'orphanRemoval' ]) && $mapping [ 'orphanRemoval' ]) {
throw MappingException :: illegalOrphanRemovalOnIdentifierAssociation ( $this -> name , $mapping [ 'fieldName' ]);
}
if (! in_array ( $mapping [ 'fieldName' ], $this -> identifier , true )) {
if (isset($mapping [ 'joinColumns' ]) && count ( $mapping [ 'joinColumns' ]) >= 2 ) {
throw MappingException :: cannotMapCompositePrimaryKeyEntitiesAsForeignId (
$mapping [ 'targetEntity' ],
$this -> name ,
$mapping [ 'fieldName' ]
);
}
$this -> identifier [] = $mapping [ 'fieldName' ];
$this -> containsForeignIdentifier = true ;
}
// Check for composite key
if (! $this -> isIdentifierComposite && count ( $this -> identifier ) > 1 ) {
$this -> isIdentifierComposite = true ;
}
if ($this -> cache && ! isset( $mapping [ 'cache' ])) {
throw NonCacheableEntityAssociation :: fromEntityAndField (
$this -> name ,
$mapping [ 'fieldName' ]
);
}
}
// Mandatory attributes for both sides
// Mandatory: fieldName, targetEntity
if (! isset( $mapping [ 'fieldName' ]) || ! $mapping [ 'fieldName' ]) {
throw MappingException :: missingFieldName ( $this -> name );
}
if (! isset($mapping [ 'targetEntity' ])) {
throw MappingException :: missingTargetEntity ( $mapping [ 'fieldName' ]);
}
// Mandatory and optional attributes for either side
if (! $mapping [ 'mappedBy' ]) {
if (isset($mapping [ 'joinTable' ]) && $mapping [ 'joinTable' ]) {
if (isset($mapping [ 'joinTable' ][ 'name' ]) && $mapping [ 'joinTable' ][ 'name' ][ 0 ] === '`' ) {
$mapping [ 'joinTable' ][ 'name' ] = trim ( $mapping [ 'joinTable' ][ 'name' ], '`' );
$mapping [ 'joinTable' ][ 'quoted' ] = true ;
}
}
} else {
$mapping [ 'isOwningSide' ] = false ;
}
if (isset($mapping [ 'id' ]) && $mapping [ 'id' ] === true && $mapping [ 'type' ] & self :: TO_MANY ) {
throw MappingException :: illegalToManyIdentifierAssociation ( $this -> name , $mapping [ 'fieldName' ]);
}
// Fetch mode. Default fetch mode to LAZY, if not set.
if (! isset( $mapping [ 'fetch' ])) {
$mapping [ 'fetch' ] = self :: FETCH_LAZY ;
}
// Cascades
$cascades = isset( $mapping [ 'cascade' ]) ? array_map ( 'strtolower' , $mapping [ 'cascade' ]) : [];
$allCascades = [ 'remove' , 'persist' , 'refresh' , 'merge' , 'detach' ];
if (in_array ( 'all' , $cascades , true )) {
$cascades = $allCascades ;
} elseif (count ( $cascades ) !== count ( array_intersect ( $cascades , $allCascades ))) {
throw MappingException :: invalidCascadeOption (
array_diff ( $cascades , $allCascades ),
$this -> name ,
$mapping [ 'fieldName' ]
);
}
$mapping [ 'cascade' ] = $cascades ;
$mapping [ 'isCascadeRemove' ] = in_array ( 'remove' , $cascades , true );
$mapping [ 'isCascadePersist' ] = in_array ( 'persist' , $cascades , true );
$mapping [ 'isCascadeRefresh' ] = in_array ( 'refresh' , $cascades , true );
$mapping [ 'isCascadeMerge' ] = in_array ( 'merge' , $cascades , true );
$mapping [ 'isCascadeDetach' ] = in_array ( 'detach' , $cascades , true );
return $mapping ;
}
/**
* Validates & completes a one-to-one association mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
*
* @return mixed[] The validated & completed mapping.
* @psalm-return AssociationMapping
*
* @throws RuntimeException
* @throws MappingException
*/
protected function _validateAndCompleteOneToOneMapping (array $mapping )
{
$mapping = $this -> _validateAndCompleteAssociationMapping ( $mapping );
if (isset($mapping [ 'joinColumns' ]) && $mapping [ 'joinColumns' ] && ! $mapping [ 'isOwningSide' ]) {
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/10654' ,
'JoinColumn configuration is not allowed on the inverse side of one-to-one associations, and will throw a MappingException in Doctrine ORM 3.0'
);
}
if (isset($mapping [ 'joinColumns' ]) && $mapping [ 'joinColumns' ]) {
$mapping [ 'isOwningSide' ] = true ;
}
if ($mapping [ 'isOwningSide' ]) {
if (empty($mapping [ 'joinColumns' ])) {
// Apply default join column
$mapping [ 'joinColumns' ] = [
[
'name' => $this -> namingStrategy -> joinColumnName ( $mapping [ 'fieldName' ], $this -> name ),
'referencedColumnName' => $this -> namingStrategy -> referenceColumnName (),
],
];
}
$uniqueConstraintColumns = [];
foreach ($mapping [ 'joinColumns' ] as & $joinColumn ) {
if ($mapping [ 'type' ] === self :: ONE_TO_ONE && ! $this -> isInheritanceTypeSingleTable ()) {
if (count ( $mapping [ 'joinColumns' ]) === 1 ) {
if (empty($mapping [ 'id' ])) {
$joinColumn [ 'unique' ] = true ;
}
} else {
$uniqueConstraintColumns [] = $joinColumn [ 'name' ];
}
}
if (empty($joinColumn [ 'name' ])) {
$joinColumn [ 'name' ] = $this -> namingStrategy -> joinColumnName ( $mapping [ 'fieldName' ], $this -> name );
}
if (empty($joinColumn [ 'referencedColumnName' ])) {
$joinColumn [ 'referencedColumnName' ] = $this -> namingStrategy -> referenceColumnName ();
}
if ($joinColumn [ 'name' ][ 0 ] === '`' ) {
$joinColumn [ 'name' ] = trim ( $joinColumn [ 'name' ], '`' );
$joinColumn [ 'quoted' ] = true ;
}
if ($joinColumn [ 'referencedColumnName' ][ 0 ] === '`' ) {
$joinColumn [ 'referencedColumnName' ] = trim ( $joinColumn [ 'referencedColumnName' ], '`' );
$joinColumn [ 'quoted' ] = true ;
}
$mapping [ 'sourceToTargetKeyColumns' ][ $joinColumn [ 'name' ]] = $joinColumn [ 'referencedColumnName' ];
$mapping [ 'joinColumnFieldNames' ][ $joinColumn [ 'name' ]] = $joinColumn [ 'fieldName' ] ?? $joinColumn [ 'name' ];
}
if ($uniqueConstraintColumns ) {
if (! $this -> table ) {
throw new RuntimeException ( 'ClassMetadataInfo::setTable() has to be called before defining a one to one relationship.' );
}
$this -> table [ 'uniqueConstraints' ][ $mapping [ 'fieldName' ] . '_uniq' ] = [ 'columns' => $uniqueConstraintColumns ];
}
$mapping [ 'targetToSourceKeyColumns' ] = array_flip ( $mapping [ 'sourceToTargetKeyColumns' ]);
}
$mapping [ 'orphanRemoval' ] = isset( $mapping [ 'orphanRemoval' ]) && $mapping [ 'orphanRemoval' ];
$mapping [ 'isCascadeRemove' ] = $mapping [ 'orphanRemoval' ] || $mapping [ 'isCascadeRemove' ];
if ($mapping [ 'orphanRemoval' ]) {
unset($mapping [ 'unique' ]);
}
if (isset($mapping [ 'id' ]) && $mapping [ 'id' ] === true && ! $mapping [ 'isOwningSide' ]) {
throw MappingException :: illegalInverseIdentifierAssociation ( $this -> name , $mapping [ 'fieldName' ]);
}
return $mapping ;
}
/**
* Validates & completes a one-to-many association mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping to validate and complete.
*
* @return mixed[] The validated and completed mapping.
* @psalm-return AssociationMapping
*
* @throws MappingException
* @throws InvalidArgumentException
*/
protected function _validateAndCompleteOneToManyMapping (array $mapping )
{
$mapping = $this -> _validateAndCompleteAssociationMapping ( $mapping );
// OneToMany-side MUST be inverse (must have mappedBy)
if (! isset( $mapping [ 'mappedBy' ])) {
throw MappingException :: oneToManyRequiresMappedBy ( $this -> name , $mapping [ 'fieldName' ]);
}
$mapping [ 'orphanRemoval' ] = isset( $mapping [ 'orphanRemoval' ]) && $mapping [ 'orphanRemoval' ];
$mapping [ 'isCascadeRemove' ] = $mapping [ 'orphanRemoval' ] || $mapping [ 'isCascadeRemove' ];
$this -> assertMappingOrderBy ( $mapping );
return $mapping ;
}
/**
* Validates & completes a many-to-many association mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
*
* @return mixed[] The validated & completed mapping.
* @psalm-return AssociationMapping
*
* @throws InvalidArgumentException
*/
protected function _validateAndCompleteManyToManyMapping (array $mapping )
{
$mapping = $this -> _validateAndCompleteAssociationMapping ( $mapping );
if ($mapping [ 'isOwningSide' ]) {
// owning side MUST have a join table
if (! isset( $mapping [ 'joinTable' ][ 'name' ])) {
$mapping [ 'joinTable' ][ 'name' ] = $this -> namingStrategy -> joinTableName ( $mapping [ 'sourceEntity' ], $mapping [ 'targetEntity' ], $mapping [ 'fieldName' ]);
}
$selfReferencingEntityWithoutJoinColumns = $mapping [ 'sourceEntity' ] === $mapping [ 'targetEntity' ]
&& (! (isset($mapping [ 'joinTable' ][ 'joinColumns' ]) || isset( $mapping [ 'joinTable' ][ 'inverseJoinColumns' ])));
if (! isset($mapping [ 'joinTable' ][ 'joinColumns' ])) {
$mapping [ 'joinTable' ][ 'joinColumns' ] = [
[
'name' => $this -> namingStrategy -> joinKeyColumnName ( $mapping [ 'sourceEntity' ], $selfReferencingEntityWithoutJoinColumns ? 'source' : null ),
'referencedColumnName' => $this -> namingStrategy -> referenceColumnName (),
'onDelete' => 'CASCADE' ,
],
];
}
if (! isset($mapping [ 'joinTable' ][ 'inverseJoinColumns' ])) {
$mapping [ 'joinTable' ][ 'inverseJoinColumns' ] = [
[
'name' => $this -> namingStrategy -> joinKeyColumnName ( $mapping [ 'targetEntity' ], $selfReferencingEntityWithoutJoinColumns ? 'target' : null ),
'referencedColumnName' => $this -> namingStrategy -> referenceColumnName (),
'onDelete' => 'CASCADE' ,
],
];
}
$mapping [ 'joinTableColumns' ] = [];
foreach ($mapping [ 'joinTable' ][ 'joinColumns' ] as & $joinColumn ) {
if (empty($joinColumn [ 'name' ])) {
$joinColumn [ 'name' ] = $this -> namingStrategy -> joinKeyColumnName ( $mapping [ 'sourceEntity' ], $joinColumn [ 'referencedColumnName' ]);
}
if (empty($joinColumn [ 'referencedColumnName' ])) {
$joinColumn [ 'referencedColumnName' ] = $this -> namingStrategy -> referenceColumnName ();
}
if ($joinColumn [ 'name' ][ 0 ] === '`' ) {
$joinColumn [ 'name' ] = trim ( $joinColumn [ 'name' ], '`' );
$joinColumn [ 'quoted' ] = true ;
}
if ($joinColumn [ 'referencedColumnName' ][ 0 ] === '`' ) {
$joinColumn [ 'referencedColumnName' ] = trim ( $joinColumn [ 'referencedColumnName' ], '`' );
$joinColumn [ 'quoted' ] = true ;
}
if (isset($joinColumn [ 'onDelete' ]) && strtolower ( $joinColumn [ 'onDelete' ]) === 'cascade' ) {
$mapping [ 'isOnDeleteCascade' ] = true ;
}
$mapping [ 'relationToSourceKeyColumns' ][ $joinColumn [ 'name' ]] = $joinColumn [ 'referencedColumnName' ];
$mapping [ 'joinTableColumns' ][] = $joinColumn [ 'name' ];
}
foreach ($mapping [ 'joinTable' ][ 'inverseJoinColumns' ] as & $inverseJoinColumn ) {
if (empty($inverseJoinColumn [ 'name' ])) {
$inverseJoinColumn [ 'name' ] = $this -> namingStrategy -> joinKeyColumnName ( $mapping [ 'targetEntity' ], $inverseJoinColumn [ 'referencedColumnName' ]);
}
if (empty($inverseJoinColumn [ 'referencedColumnName' ])) {
$inverseJoinColumn [ 'referencedColumnName' ] = $this -> namingStrategy -> referenceColumnName ();
}
if ($inverseJoinColumn [ 'name' ][ 0 ] === '`' ) {
$inverseJoinColumn [ 'name' ] = trim ( $inverseJoinColumn [ 'name' ], '`' );
$inverseJoinColumn [ 'quoted' ] = true ;
}
if ($inverseJoinColumn [ 'referencedColumnName' ][ 0 ] === '`' ) {
$inverseJoinColumn [ 'referencedColumnName' ] = trim ( $inverseJoinColumn [ 'referencedColumnName' ], '`' );
$inverseJoinColumn [ 'quoted' ] = true ;
}
if (isset($inverseJoinColumn [ 'onDelete' ]) && strtolower ( $inverseJoinColumn [ 'onDelete' ]) === 'cascade' ) {
$mapping [ 'isOnDeleteCascade' ] = true ;
}
$mapping [ 'relationToTargetKeyColumns' ][ $inverseJoinColumn [ 'name' ]] = $inverseJoinColumn [ 'referencedColumnName' ];
$mapping [ 'joinTableColumns' ][] = $inverseJoinColumn [ 'name' ];
}
}
$mapping [ 'orphanRemoval' ] = isset( $mapping [ 'orphanRemoval' ]) && $mapping [ 'orphanRemoval' ];
$this -> assertMappingOrderBy ( $mapping );
return $mapping ;
}
/**
* {@inheritDoc}
*/
public function getIdentifierFieldNames ()
{
return $this -> identifier ;
}
/**
* Gets the name of the single id field. Note that this only works on
* entity classes that have a single-field pk.
*
* @return string
*
* @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
*/
public function getSingleIdentifierFieldName ()
{
if ($this -> isIdentifierComposite ) {
throw MappingException :: singleIdNotAllowedOnCompositePrimaryKey ( $this -> name );
}
if (! isset($this -> identifier [ 0 ])) {
throw MappingException :: noIdDefined ( $this -> name );
}
return $this -> identifier [ 0 ];
}
/**
* Gets the column name of the single id column. Note that this only works on
* entity classes that have a single-field pk.
*
* @return string
*
* @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
*/
public function getSingleIdentifierColumnName ()
{
return $this -> getColumnName ( $this -> getSingleIdentifierFieldName ());
}
/**
* INTERNAL:
* Sets the mapped identifier/primary key fields of this class.
* Mainly used by the ClassMetadataFactory to assign inherited identifiers.
*
* @psalm-param list<mixed> $identifier
*
* @return void
*/
public function setIdentifier (array $identifier )
{
$this -> identifier = $identifier ;
$this -> isIdentifierComposite = ( count ( $this -> identifier ) > 1 );
}
/**
* {@inheritDoc}
*/
public function getIdentifier ()
{
return $this -> identifier ;
}
/**
* {@inheritDoc}
*/
public function hasField ( $fieldName )
{
return isset($this -> fieldMappings [ $fieldName ]) || isset( $this -> embeddedClasses [ $fieldName ]);
}
/**
* Gets an array containing all the column names.
*
* @psalm-param list<string>|null $fieldNames
*
* @return mixed[]
* @psalm-return list<string>
*/
public function getColumnNames (?array $fieldNames = null )
{
if ($fieldNames === null ) {
return array_keys ( $this -> fieldNames );
}
return array_values ( array_map ([ $this , 'getColumnName' ], $fieldNames ));
}
/**
* Returns an array with all the identifier column names.
*
* @psalm-return list<string>
*/
public function getIdentifierColumnNames ()
{
$columnNames = [];
foreach ($this -> identifier as $idProperty ) {
if (isset($this -> fieldMappings [ $idProperty ])) {
$columnNames [] = $this -> fieldMappings [ $idProperty ][ 'columnName' ];
continue;
}
// Association defined as Id field
$joinColumns = $this -> associationMappings [ $idProperty ][ 'joinColumns' ];
$assocColumnNames = array_map (static function ( $joinColumn ) {
return $joinColumn [ 'name' ];
}, $joinColumns );
$columnNames = array_merge ( $columnNames , $assocColumnNames );
}
return $columnNames ;
}
/**
* Sets the type of Id generator to use for the mapped class.
*
* @param int $generatorType
* @psalm-param self::GENERATOR_TYPE_* $generatorType
*
* @return void
*/
public function setIdGeneratorType ( $generatorType )
{
$this -> generatorType = $generatorType ;
}
/**
* Checks whether the mapped class uses an Id generator.
*
* @return bool TRUE if the mapped class uses an Id generator, FALSE otherwise.
*/
public function usesIdGenerator ()
{
return $this -> generatorType !== self :: GENERATOR_TYPE_NONE ;
}
/** @return bool */
public function isInheritanceTypeNone ()
{
return $this -> inheritanceType === self :: INHERITANCE_TYPE_NONE ;
}
/**
* Checks whether the mapped class uses the JOINED inheritance mapping strategy.
*
* @return bool TRUE if the class participates in a JOINED inheritance mapping,
* FALSE otherwise.
*/
public function isInheritanceTypeJoined ()
{
return $this -> inheritanceType === self :: INHERITANCE_TYPE_JOINED ;
}
/**
* Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy.
*
* @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping,
* FALSE otherwise.
*/
public function isInheritanceTypeSingleTable ()
{
return $this -> inheritanceType === self :: INHERITANCE_TYPE_SINGLE_TABLE ;
}
/**
* Checks whether the mapped class uses the TABLE_PER_CLASS inheritance mapping strategy.
*
* @deprecated
*
* @return bool TRUE if the class participates in a TABLE_PER_CLASS inheritance mapping,
* FALSE otherwise.
*/
public function isInheritanceTypeTablePerClass ()
{
Deprecation :: triggerIfCalledFromOutside (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/10414/' ,
'Concrete table inheritance has never been implemented, and its stubs will be removed in Doctrine ORM 3.0 with no replacement'
);
return $this -> inheritanceType === self :: INHERITANCE_TYPE_TABLE_PER_CLASS ;
}
/**
* Checks whether the class uses an identity column for the Id generation.
*
* @return bool TRUE if the class uses the IDENTITY generator, FALSE otherwise.
*/
public function isIdGeneratorIdentity ()
{
return $this -> generatorType === self :: GENERATOR_TYPE_IDENTITY ;
}
/**
* Checks whether the class uses a sequence for id generation.
*
* @return bool TRUE if the class uses the SEQUENCE generator, FALSE otherwise.
*
* @psalm-assert-if-true !null $this->sequenceGeneratorDefinition
*/
public function isIdGeneratorSequence ()
{
return $this -> generatorType === self :: GENERATOR_TYPE_SEQUENCE ;
}
/**
* Checks whether the class uses a table for id generation.
*
* @deprecated
*
* @return false
*/
public function isIdGeneratorTable ()
{
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/9046' ,
'%s is deprecated' ,
__METHOD__
);
return false ;
}
/**
* Checks whether the class has a natural identifier/pk (which means it does
* not use any Id generator.
*
* @return bool
*/
public function isIdentifierNatural ()
{
return $this -> generatorType === self :: GENERATOR_TYPE_NONE ;
}
/**
* Checks whether the class use a UUID for id generation.
*
* @deprecated
*
* @return bool
*/
public function isIdentifierUuid ()
{
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/9046' ,
'%s is deprecated' ,
__METHOD__
);
return $this -> generatorType === self :: GENERATOR_TYPE_UUID ;
}
/**
* Gets the type of a field.
*
* @param string $fieldName
*
* @return string|null
*
* @todo 3.0 Remove this. PersisterHelper should fix it somehow
*/
public function getTypeOfField ( $fieldName )
{
return isset($this -> fieldMappings [ $fieldName ])
? $this -> fieldMappings [ $fieldName ][ 'type' ]
: null ;
}
/**
* Gets the type of a column.
*
* @deprecated 3.0 remove this. this method is bogus and unreliable, since it cannot resolve the type of a column
* that is derived by a referenced field on a different entity.
*
* @param string $columnName
*
* @return string|null
*/
public function getTypeOfColumn ( $columnName )
{
return $this -> getTypeOfField ( $this -> getFieldName ( $columnName ));
}
/**
* Gets the name of the primary table.
*
* @return string
*/
public function getTableName ()
{
return $this -> table [ 'name' ];
}
/**
* Gets primary table's schema name.
*
* @return string|null
*/
public function getSchemaName ()
{
return $this -> table [ 'schema' ] ?? null ;
}
/**
* Gets the table name to use for temporary identifier tables of this class.
*
* @return string
*/
public function getTemporaryIdTableName ()
{
// replace dots with underscores because PostgreSQL creates temporary tables in a special schema
return str_replace ( '.' , '_' , $this -> getTableName () . '_id_tmp' );
}
/**
* Sets the mapped subclasses of this class.
*
* @psalm-param list<string> $subclasses The names of all mapped subclasses.
*
* @return void
*/
public function setSubclasses (array $subclasses )
{
foreach ($subclasses as $subclass ) {
$this -> subClasses [] = $this -> fullyQualifiedClassName ( $subclass );
}
}
/**
* Sets the parent class names. Only <em>entity</em> classes may be given.
*
* Assumes that the class names in the passed array are in the order:
* directParent -> directParentParent -> directParentParentParent ... -> root.
*
* @psalm-param list<class-string> $classNames
*
* @return void
*/
public function setParentClasses (array $classNames )
{
$this -> parentClasses = $classNames ;
if (count ( $classNames ) > 0 ) {
$this -> rootEntityName = array_pop ( $classNames );
}
}
/**
* Sets the inheritance type used by the class and its subclasses.
*
* @param int $type
* @psalm-param self::INHERITANCE_TYPE_* $type
*
* @return void
*
* @throws MappingException
*/
public function setInheritanceType ( $type )
{
if (! $this -> isInheritanceType ( $type )) {
throw MappingException :: invalidInheritanceType ( $this -> name , $type );
}
$this -> inheritanceType = $type ;
}
/**
* Sets the association to override association mapping of property for an entity relationship.
*
* @param string $fieldName
* @psalm-param array<string, mixed> $overrideMapping
*
* @return void
*
* @throws MappingException
*/
public function setAssociationOverride ( $fieldName , array $overrideMapping )
{
if (! isset($this -> associationMappings [ $fieldName ])) {
throw MappingException :: invalidOverrideFieldName ( $this -> name , $fieldName );
}
$mapping = $this -> associationMappings [ $fieldName ];
if (isset($mapping [ 'inherited' ])) {
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/10470' ,
'Overrides are only allowed for fields or associations declared in mapped superclasses or traits. This is not the case for %s::%s, which was inherited from %s. This is a misconfiguration and will be an error in Doctrine ORM 3.0.' ,
$this -> name ,
$fieldName ,
$mapping [ 'inherited' ]
);
}
if (isset($overrideMapping [ 'joinColumns' ])) {
$mapping [ 'joinColumns' ] = $overrideMapping [ 'joinColumns' ];
}
if (isset($overrideMapping [ 'inversedBy' ])) {
$mapping [ 'inversedBy' ] = $overrideMapping [ 'inversedBy' ];
}
if (isset($overrideMapping [ 'joinTable' ])) {
$mapping [ 'joinTable' ] = $overrideMapping [ 'joinTable' ];
}
if (isset($overrideMapping [ 'fetch' ])) {
$mapping [ 'fetch' ] = $overrideMapping [ 'fetch' ];
}
$mapping [ 'joinColumnFieldNames' ] = null ;
$mapping [ 'joinTableColumns' ] = null ;
$mapping [ 'sourceToTargetKeyColumns' ] = null ;
$mapping [ 'relationToSourceKeyColumns' ] = null ;
$mapping [ 'relationToTargetKeyColumns' ] = null ;
switch ($mapping [ 'type' ]) {
case self :: ONE_TO_ONE :
$mapping = $this -> _validateAndCompleteOneToOneMapping ( $mapping );
break;
case self :: ONE_TO_MANY :
$mapping = $this -> _validateAndCompleteOneToManyMapping ( $mapping );
break;
case self :: MANY_TO_ONE :
$mapping = $this -> _validateAndCompleteOneToOneMapping ( $mapping );
break;
case self :: MANY_TO_MANY :
$mapping = $this -> _validateAndCompleteManyToManyMapping ( $mapping );
break;
}
$this -> associationMappings [ $fieldName ] = $mapping ;
}
/**
* Sets the override for a mapped field.
*
* @param string $fieldName
* @psalm-param array<string, mixed> $overrideMapping
*
* @return void
*
* @throws MappingException
*/
public function setAttributeOverride ( $fieldName , array $overrideMapping )
{
if (! isset($this -> fieldMappings [ $fieldName ])) {
throw MappingException :: invalidOverrideFieldName ( $this -> name , $fieldName );
}
$mapping = $this -> fieldMappings [ $fieldName ];
if (isset($mapping [ 'inherited' ])) {
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/10470' ,
'Overrides are only allowed for fields or associations declared in mapped superclasses or traits. This is not the case for %s::%s, which was inherited from %s. This is a misconfiguration and will be an error in Doctrine ORM 3.0.' ,
$this -> name ,
$fieldName ,
$mapping [ 'inherited' ]
);
//throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
}
if (isset($mapping [ 'id' ])) {
$overrideMapping [ 'id' ] = $mapping [ 'id' ];
}
if (! isset($overrideMapping [ 'type' ])) {
$overrideMapping [ 'type' ] = $mapping [ 'type' ];
}
if (! isset($overrideMapping [ 'fieldName' ])) {
$overrideMapping [ 'fieldName' ] = $mapping [ 'fieldName' ];
}
if ($overrideMapping [ 'type' ] !== $mapping [ 'type' ]) {
throw MappingException :: invalidOverrideFieldType ( $this -> name , $fieldName );
}
unset($this -> fieldMappings [ $fieldName ]);
unset($this -> fieldNames [ $mapping [ 'columnName' ]]);
unset($this -> columnNames [ $mapping [ 'fieldName' ]]);
$overrideMapping = $this -> validateAndCompleteFieldMapping ( $overrideMapping );
$this -> fieldMappings [ $fieldName ] = $overrideMapping ;
}
/**
* Checks whether a mapped field is inherited from an entity superclass.
*
* @param string $fieldName
*
* @return bool TRUE if the field is inherited, FALSE otherwise.
*/
public function isInheritedField ( $fieldName )
{
return isset($this -> fieldMappings [ $fieldName ][ 'inherited' ]);
}
/**
* Checks if this entity is the root in any entity-inheritance-hierarchy.
*
* @return bool
*/
public function isRootEntity ()
{
return $this -> name === $this -> rootEntityName ;
}
/**
* Checks whether a mapped association field is inherited from a superclass.
*
* @param string $fieldName
*
* @return bool TRUE if the field is inherited, FALSE otherwise.
*/
public function isInheritedAssociation ( $fieldName )
{
return isset($this -> associationMappings [ $fieldName ][ 'inherited' ]);
}
/**
* @param string $fieldName
*
* @return bool
*/
public function isInheritedEmbeddedClass ( $fieldName )
{
return isset($this -> embeddedClasses [ $fieldName ][ 'inherited' ]);
}
/**
* Sets the name of the primary table the class is mapped to.
*
* @deprecated Use {@link setPrimaryTable}.
*
* @param string $tableName The table name.
*
* @return void
*/
public function setTableName ( $tableName )
{
$this -> table [ 'name' ] = $tableName ;
}
/**
* Sets the primary table definition. The provided array supports the
* following structure:
*
* name => <tableName> (optional, defaults to class name)
* indexes => array of indexes (optional)
* uniqueConstraints => array of constraints (optional)
*
* If a key is omitted, the current value is kept.
*
* @psalm-param array<string, mixed> $table The table description.
*
* @return void
*/
public function setPrimaryTable (array $table )
{
if (isset($table [ 'name' ])) {
// Split schema and table name from a table name like "myschema.mytable"
if ( str_contains ( $table [ 'name' ], '.' )) {
[$this -> table [ 'schema' ], $table [ 'name' ]] = explode ( '.' , $table [ 'name' ], 2 );
}
if ($table [ 'name' ][ 0 ] === '`' ) {
$table [ 'name' ] = trim ( $table [ 'name' ], '`' );
$this -> table [ 'quoted' ] = true ;
}
$this -> table [ 'name' ] = $table [ 'name' ];
}
if (isset($table [ 'quoted' ])) {
$this -> table [ 'quoted' ] = $table [ 'quoted' ];
}
if (isset($table [ 'schema' ])) {
$this -> table [ 'schema' ] = $table [ 'schema' ];
}
if (isset($table [ 'indexes' ])) {
$this -> table [ 'indexes' ] = $table [ 'indexes' ];
}
if (isset($table [ 'uniqueConstraints' ])) {
$this -> table [ 'uniqueConstraints' ] = $table [ 'uniqueConstraints' ];
}
if (isset($table [ 'options' ])) {
$this -> table [ 'options' ] = $table [ 'options' ];
}
}
/**
* Checks whether the given type identifies an inheritance type.
*
* @return bool TRUE if the given type identifies an inheritance type, FALSE otherwise.
*/
private function isInheritanceType ( int $type ): bool
{
if ($type === self :: INHERITANCE_TYPE_TABLE_PER_CLASS ) {
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/10414/' ,
'Concrete table inheritance has never been implemented, and its stubs will be removed in Doctrine ORM 3.0 with no replacement'
);
}
return $type === self :: INHERITANCE_TYPE_NONE ||
$type === self :: INHERITANCE_TYPE_SINGLE_TABLE ||
$type === self :: INHERITANCE_TYPE_JOINED ||
$type === self :: INHERITANCE_TYPE_TABLE_PER_CLASS ;
}
/**
* Adds a mapped field to the class.
*
* @psalm-param array<string, mixed> $mapping The field mapping.
*
* @return void
*
* @throws MappingException
*/
public function mapField (array $mapping )
{
$mapping = $this -> validateAndCompleteFieldMapping ( $mapping );
$this -> assertFieldNotMapped ( $mapping [ 'fieldName' ]);
if (isset($mapping [ 'generated' ])) {
$this -> requiresFetchAfterChange = true ;
}
$this -> fieldMappings [ $mapping [ 'fieldName' ]] = $mapping ;
}
/**
* INTERNAL:
* Adds an association mapping without completing/validating it.
* This is mainly used to add inherited association mappings to derived classes.
*
* @psalm-param AssociationMapping $mapping
*
* @return void
*
* @throws MappingException
*/
public function addInheritedAssociationMapping (array $mapping /*, $owningClassName = null*/ )
{
if (isset($this -> associationMappings [ $mapping [ 'fieldName' ]])) {
throw MappingException :: duplicateAssociationMapping ( $this -> name , $mapping [ 'fieldName' ]);
}
$this -> associationMappings [ $mapping [ 'fieldName' ]] = $mapping ;
}
/**
* INTERNAL:
* Adds a field mapping without completing/validating it.
* This is mainly used to add inherited field mappings to derived classes.
*
* @psalm-param FieldMapping $fieldMapping
*
* @return void
*/
public function addInheritedFieldMapping (array $fieldMapping )
{
$this -> fieldMappings [ $fieldMapping [ 'fieldName' ]] = $fieldMapping ;
$this -> columnNames [ $fieldMapping [ 'fieldName' ]] = $fieldMapping [ 'columnName' ];
$this -> fieldNames [ $fieldMapping [ 'columnName' ]] = $fieldMapping [ 'fieldName' ];
if (isset($fieldMapping [ 'generated' ])) {
$this -> requiresFetchAfterChange = true ;
}
}
/**
* INTERNAL:
* Adds a named query to this class.
*
* @deprecated
*
* @psalm-param array<string, mixed> $queryMapping
*
* @return void
*
* @throws MappingException
*/
public function addNamedQuery (array $queryMapping )
{
if (! isset($queryMapping [ 'name' ])) {
throw MappingException :: nameIsMandatoryForQueryMapping ( $this -> name );
}
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/issues/8592' ,
'Named Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository' ,
$queryMapping [ 'name' ],
$this -> name
);
if (isset($this -> namedQueries [ $queryMapping [ 'name' ]])) {
throw MappingException :: duplicateQueryMapping ( $this -> name , $queryMapping [ 'name' ]);
}
if (! isset($queryMapping [ 'query' ])) {
throw MappingException :: emptyQueryMapping ( $this -> name , $queryMapping [ 'name' ]);
}
$name = $queryMapping [ 'name' ];
$query = $queryMapping [ 'query' ];
$dql = str_replace ( '__CLASS__' , $this -> name , $query );
$this -> namedQueries [ $name ] = [
'name' => $name ,
'query' => $query ,
'dql' => $dql ,
];
}
/**
* INTERNAL:
* Adds a named native query to this class.
*
* @deprecated
*
* @psalm-param array<string, mixed> $queryMapping
*
* @return void
*
* @throws MappingException
*/
public function addNamedNativeQuery (array $queryMapping )
{
if (! isset($queryMapping [ 'name' ])) {
throw MappingException :: nameIsMandatoryForQueryMapping ( $this -> name );
}
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/issues/8592' ,
'Named Native Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository' ,
$queryMapping [ 'name' ],
$this -> name
);
if (isset($this -> namedNativeQueries [ $queryMapping [ 'name' ]])) {
throw MappingException :: duplicateQueryMapping ( $this -> name , $queryMapping [ 'name' ]);
}
if (! isset($queryMapping [ 'query' ])) {
throw MappingException :: emptyQueryMapping ( $this -> name , $queryMapping [ 'name' ]);
}
if (! isset($queryMapping [ 'resultClass' ]) && ! isset( $queryMapping [ 'resultSetMapping' ])) {
throw MappingException :: missingQueryMapping ( $this -> name , $queryMapping [ 'name' ]);
}
$queryMapping [ 'isSelfClass' ] = false ;
if (isset($queryMapping [ 'resultClass' ])) {
if ($queryMapping [ 'resultClass' ] === '__CLASS__' ) {
$queryMapping [ 'isSelfClass' ] = true ;
$queryMapping [ 'resultClass' ] = $this -> name ;
}
$queryMapping [ 'resultClass' ] = $this -> fullyQualifiedClassName ( $queryMapping [ 'resultClass' ]);
$queryMapping [ 'resultClass' ] = ltrim ( $queryMapping [ 'resultClass' ], '\\' );
}
$this -> namedNativeQueries [ $queryMapping [ 'name' ]] = $queryMapping ;
}
/**
* INTERNAL:
* Adds a sql result set mapping to this class.
*
* @psalm-param array<string, mixed> $resultMapping
*
* @return void
*
* @throws MappingException
*/
public function addSqlResultSetMapping (array $resultMapping )
{
if (! isset($resultMapping [ 'name' ])) {
throw MappingException :: nameIsMandatoryForSqlResultSetMapping ( $this -> name );
}
if (isset($this -> sqlResultSetMappings [ $resultMapping [ 'name' ]])) {
throw MappingException :: duplicateResultSetMapping ( $this -> name , $resultMapping [ 'name' ]);
}
if (isset($resultMapping [ 'entities' ])) {
foreach ($resultMapping [ 'entities' ] as $key => $entityResult ) {
if (! isset($entityResult [ 'entityClass' ])) {
throw MappingException :: missingResultSetMappingEntity ( $this -> name , $resultMapping [ 'name' ]);
}
$entityResult [ 'isSelfClass' ] = false ;
if ($entityResult [ 'entityClass' ] === '__CLASS__' ) {
$entityResult [ 'isSelfClass' ] = true ;
$entityResult [ 'entityClass' ] = $this -> name ;
}
$entityResult [ 'entityClass' ] = $this -> fullyQualifiedClassName ( $entityResult [ 'entityClass' ]);
$resultMapping [ 'entities' ][ $key ][ 'entityClass' ] = ltrim ( $entityResult [ 'entityClass' ], '\\' );
$resultMapping [ 'entities' ][ $key ][ 'isSelfClass' ] = $entityResult [ 'isSelfClass' ];
if (isset($entityResult [ 'fields' ])) {
foreach ($entityResult [ 'fields' ] as $k => $field ) {
if (! isset($field [ 'name' ])) {
throw MappingException :: missingResultSetMappingFieldName ( $this -> name , $resultMapping [ 'name' ]);
}
if (! isset($field [ 'column' ])) {
$fieldName = $field [ 'name' ];
if (str_contains ( $fieldName , '.' )) {
[, $fieldName ] = explode ( '.' , $fieldName );
}
$resultMapping [ 'entities' ][ $key ][ 'fields' ][ $k ][ 'column' ] = $fieldName ;
}
}
}
}
}
$this -> sqlResultSetMappings [ $resultMapping [ 'name' ]] = $resultMapping ;
}
/**
* Adds a one-to-one mapping.
*
* @param array<string, mixed> $mapping The mapping.
*
* @return void
*/
public function mapOneToOne (array $mapping )
{
$mapping [ 'type' ] = self :: ONE_TO_ONE ;
$mapping = $this -> _validateAndCompleteOneToOneMapping ( $mapping );
$this -> _storeAssociationMapping ( $mapping );
}
/**
* Adds a one-to-many mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping.
*
* @return void
*/
public function mapOneToMany (array $mapping )
{
$mapping [ 'type' ] = self :: ONE_TO_MANY ;
$mapping = $this -> _validateAndCompleteOneToManyMapping ( $mapping );
$this -> _storeAssociationMapping ( $mapping );
}
/**
* Adds a many-to-one mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping.
*
* @return void
*/
public function mapManyToOne (array $mapping )
{
$mapping [ 'type' ] = self :: MANY_TO_ONE ;
// A many-to-one mapping is essentially a one-one backreference
$mapping = $this -> _validateAndCompleteOneToOneMapping ( $mapping );
$this -> _storeAssociationMapping ( $mapping );
}
/**
* Adds a many-to-many mapping.
*
* @psalm-param array<string, mixed> $mapping The mapping.
*
* @return void
*/
public function mapManyToMany (array $mapping )
{
$mapping [ 'type' ] = self :: MANY_TO_MANY ;
$mapping = $this -> _validateAndCompleteManyToManyMapping ( $mapping );
$this -> _storeAssociationMapping ( $mapping );
}
/**
* Stores the association mapping.
*
* @psalm-param AssociationMapping $assocMapping
*
* @return void
*
* @throws MappingException
*/
protected function _storeAssociationMapping (array $assocMapping )
{
$sourceFieldName = $assocMapping [ 'fieldName' ];
$this -> assertFieldNotMapped ( $sourceFieldName );
$this -> associationMappings [ $sourceFieldName ] = $assocMapping ;
}
/**
* Registers a custom repository class for the entity class.
*
* @param string|null $repositoryClassName The class name of the custom mapper.
* @psalm-param class-string<EntityRepository>|null $repositoryClassName
*
* @return void
*/
public function setCustomRepositoryClass ( $repositoryClassName )
{
$this -> customRepositoryClassName = $this -> fullyQualifiedClassName ( $repositoryClassName );
}
/**
* Dispatches the lifecycle event of the given entity to the registered
* lifecycle callbacks and lifecycle listeners.
*
* @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker
*
* @param string $lifecycleEvent The lifecycle event.
* @param object $entity The Entity on which the event occurred.
*
* @return void
*/
public function invokeLifecycleCallbacks ( $lifecycleEvent , $entity )
{
foreach ($this -> lifecycleCallbacks [ $lifecycleEvent ] as $callback ) {
$entity -> $callback ();
}
}
/**
* Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
*
* @param string $lifecycleEvent
*
* @return bool
*/
public function hasLifecycleCallbacks ( $lifecycleEvent )
{
return isset($this -> lifecycleCallbacks [ $lifecycleEvent ]);
}
/**
* Gets the registered lifecycle callbacks for an event.
*
* @param string $event
*
* @return string[]
* @psalm-return list<string>
*/
public function getLifecycleCallbacks ( $event )
{
return $this -> lifecycleCallbacks [ $event ] ?? [];
}
/**
* Adds a lifecycle callback for entities of this class.
*
* @param string $callback
* @param string $event
*
* @return void
*/
public function addLifecycleCallback ( $callback , $event )
{
if ($this -> isEmbeddedClass ) {
Deprecation :: trigger (
'doctrine/orm' ,
'https://github.com/doctrine/orm/pull/8381' ,
'Registering lifecycle callback %s on Embedded class %s is not doing anything and will throw exception in 3.0' ,
$event ,
$this -> name
);
}
if (isset($this -> lifecycleCallbacks [ $event ]) && in_array ( $callback , $this -> lifecycleCallbacks [ $event ], true )) {
return;
}
$this -> lifecycleCallbacks [ $event ][] = $callback ;
}
/**
* Sets the lifecycle callbacks for entities of this class.
* Any previously registered callbacks are overwritten.
*
* @psalm-param array<string, list<string>> $callbacks
*
* @return void
*/
public function setLifecycleCallbacks (array $callbacks )
{
$this -> lifecycleCallbacks = $callbacks ;
}
/**
* Adds a entity listener for entities of this class.
*
* @param string $eventName The entity lifecycle event.
* @param string $class The listener class.
* @param string $method The listener callback method.
*
* @return void
*
* @throws MappingException
*/
public function addEntityListener ( $eventName , $class , $method )
{
$class = $this -> fullyQualifiedClassName ( $class );
$listener = [
'class' => $class ,
'method' => $method ,
];
if (! class_exists ( $class )) {
throw MappingException :: entityListenerClassNotFound ( $class , $this -> name );
}
if (! method_exists ( $class , $method )) {
throw MappingException :: entityListenerMethodNotFound ( $class , $method , $this -> name );
}
if (isset($this -> entityListeners [ $eventName ]) && in_array ( $listener , $this -> entityListeners [ $eventName ], true )) {
throw MappingException :: duplicateEntityListener ( $class , $method , $this -> name );
}
$this -> entityListeners [ $eventName ][] = $listener ;
}
/**
* Sets the discriminator column definition.
*
* @see getDiscriminatorColumn()
*
* @param mixed[]|null $columnDef
* @psalm-param array{name: string|null, fieldName?: string, type?: string, length?: int, columnDefinition?: string|null, enumType?: class-string<BackedEnum>|null, options?: array<string, mixed>}|null $columnDef
*
* @return void
*
* @throws MappingException
*/
public function setDiscriminatorColumn ( $columnDef )
{
if ($columnDef !== null ) {
if (! isset($columnDef [ 'name' ])) {
throw MappingException :: nameIsMandatoryForDiscriminatorColumns ( $this -> name );
}
if (isset($this -> fieldNames [ $columnDef [ 'name' ]])) {
throw MappingException :: duplicateColumnName ( $this -> name , $columnDef [ 'name' ]);
}
if (! isset($columnDef [ 'fieldName' ])) {
$columnDef [ 'fieldName' ] = $columnDef [ 'name' ];
}
if (! isset($columnDef [ 'type' ])) {
$columnDef [ 'type' ] = 'string' ;
}
if (in_array ( $columnDef [ 'type' ], [ 'boolean' , 'array' , 'object' , 'datetime' , 'time' , 'date' ], true )) {
throw MappingException :: invalidDiscriminatorColumnType ( $this -> name , $columnDef [ 'type' ]);
}
$this -> discriminatorColumn = $columnDef ;
}
}
/**
* @return array<string, mixed>
* @psalm-return DiscriminatorColumnMapping
*/
final public function getDiscriminatorColumn (): array
{
if ($this -> discriminatorColumn === null ) {
throw new LogicException ( 'The discriminator column was not set.' );
}
return $this -> discriminatorColumn ;
}
/**
* Sets the discriminator values used by this class.
* Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
*
* @param array<int|string, string> $map
*
* @return void
*/
public function setDiscriminatorMap (array $map )
{
foreach ($map as $value => $className ) {
$this -> addDiscriminatorMapClass ( $value , $className );
}
}
/**
* Adds one entry of the discriminator map with a new class and corresponding name.
*
* @param int|string $name
* @param string $className
*
* @return void
*
* @throws MappingException
*/
public function addDiscriminatorMapClass ( $name , $className )
{
$className = $this -> fullyQualifiedClassName ( $className );
$className = ltrim ( $className , '\\' );
$this -> discriminatorMap [ $name ] = $className ;
if ($this -> name === $className ) {
$this -> discriminatorValue = $name ;
return;
}
if (! (class_exists ( $className ) || interface_exists ( $className ))) {
throw MappingException :: invalidClassInDiscriminatorMap ( $className , $this -> name );
}
$this -> addSubClass ( $className );
}
/** @param array<class-string> $classes */
public function addSubClasses (array $classes ): void
{
foreach ($classes as $className ) {
$this -> addSubClass ( $className );
}
}
public function addSubClass ( string $className ): void
{
// By ignoring classes that are not subclasses of the current class, we simplify inheriting
// the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata.
if ( is_subclass_of ( $className , $this -> name ) && ! in_array ( $className , $this -> subClasses , true )) {
$this -> subClasses [] = $className ;
}
}
/**
* Checks whether the class has a named query with the given query name.
*
* @param string $queryName
*
* @return bool
*/
public function hasNamedQuery ( $queryName )
{
return isset($this -> namedQueries [ $queryName ]);
}
/**
* Checks whether the class has a named native query with the given query name.
*
* @param string $queryName
*
* @return bool
*/
public function hasNamedNativeQuery ( $queryName )
{
return isset($this -> namedNativeQueries [ $queryName ]);
}
/**
* Checks whether the class has a named native query with the given query name.
*
* @param string $name
*
* @return bool
*/
public function hasSqlResultSetMapping ( $name )
{
return isset($this -> sqlResultSetMappings [ $name ]);
}
/**
* {@inheritDoc}
*/
public function hasAssociation ( $fieldName )
{
return isset($this -> associationMappings [ $fieldName ]);
}
/**
* {@inheritDoc}
*/
public function isSingleValuedAssociation ( $fieldName )
{
return isset($this -> associationMappings [ $fieldName ])
&& ($this -> associationMappings [ $fieldName ][ 'type' ] & self :: TO_ONE );
}
/**
* {@inheritDoc}
*/
public function isCollectionValuedAssociation ( $fieldName )
{
return isset($this -> associationMappings [ $fieldName ])
&& ! ($this -> associationMappings [ $fieldName ][ 'type' ] & self :: TO_ONE );
}
/**
* Is this an association that only has a single join column?
*
* @param string $fieldName
*
* @return bool
*/
public function isAssociationWithSingleJoinColumn ( $fieldName )
{
return isset($this -> associationMappings [ $fieldName ])
&& isset($this -> associationMappings [ $fieldName ][ 'joinColumns' ][ 0 ])
&& ! isset($this -> associationMappings [ $fieldName ][ 'joinColumns' ][ 1 ]);
}
/**
* Returns the single association join column (if any).
*
* @param string $fieldName
*
* @return string
*
* @throws MappingException
*/
public function getSingleAssociationJoinColumnName ( $fieldName )
{
if (! $this -> isAssociationWithSingleJoinColumn ( $fieldName )) {
throw MappingException :: noSingleAssociationJoinColumnFound ( $this -> name , $fieldName );
}
return $this -> associationMappings [ $fieldName ][ 'joinColumns' ][ 0 ][ 'name' ];
}
/**
* Returns the single association referenced join column name (if any).
*
* @param string $fieldName
*
* @return string
*
* @throws MappingException
*/
public function getSingleAssociationReferencedJoinColumnName ( $fieldName )
{
if (! $this -> isAssociationWithSingleJoinColumn ( $fieldName )) {
throw MappingException :: noSingleAssociationJoinColumnFound ( $this -> name , $fieldName );
}
return $this -> associationMappings [ $fieldName ][ 'joinColumns' ][ 0 ][ 'referencedColumnName' ];
}
/**
* Used to retrieve a fieldname for either field or association from a given column.
*
* This method is used in foreign-key as primary-key contexts.
*
* @param string $columnName
*
* @return string
*
* @throws MappingException
*/
public function getFieldForColumn ( $columnName )
{
if (isset($this -> fieldNames [ $columnName ])) {
return $this -> fieldNames [ $columnName ];
}
foreach ($this -> associationMappings as $assocName => $mapping ) {
if (
$this -> isAssociationWithSingleJoinColumn ( $assocName ) &&
$this -> associationMappings [ $assocName ][ 'joinColumns' ][ 0 ][ 'name' ] === $columnName
) {
return $assocName ;
}
}
throw MappingException :: noFieldNameFoundForColumn ( $this -> name , $columnName );
}
/**
* Sets the ID generator used to generate IDs for instances of this class.
*
* @param AbstractIdGenerator $generator
*
* @return void
*/
public function setIdGenerator ( $generator )
{
$this -> idGenerator = $generator ;
}
/**
* Sets definition.
*
* @psalm-param array<string, string|null> $definition
*
* @return void
*/
public function setCustomGeneratorDefinition (array $definition )
{
$this -> customGeneratorDefinition = $definition ;
}
/**
* Sets the definition of the sequence ID generator for this class.
*
* The definition must have the following structure:
* <code>
* array(
* 'sequenceName' => 'name',
* 'allocationSize' => 20,
* 'initialValue' => 1
* 'quoted' => 1
* )
* </code>
*
* @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
*
* @return void
*
* @throws MappingException
*/
public function setSequenceGeneratorDefinition (array $definition )
{
if (! isset($definition [ 'sequenceName' ]) || trim ( $definition [ 'sequenceName' ]) === '' ) {
throw MappingException :: missingSequenceName ( $this -> name );
}
if ($definition [ 'sequenceName' ][ 0 ] === '`' ) {
$definition [ 'sequenceName' ] = trim ( $definition [ 'sequenceName' ], '`' );
$definition [ 'quoted' ] = true ;
}
if (! isset($definition [ 'allocationSize' ]) || trim ((string) $definition [ 'allocationSize' ]) === '' ) {
$definition [ 'allocationSize' ] = '1' ;
}
if (! isset($definition [ 'initialValue' ]) || trim ((string) $definition [ 'initialValue' ]) === '' ) {
$definition [ 'initialValue' ] = '1' ;
}
$definition [ 'allocationSize' ] = (string) $definition [ 'allocationSize' ];
$definition [ 'initialValue' ] = (string) $definition [ 'initialValue' ];
$this -> sequenceGeneratorDefinition = $definition ;
}
/**
* Sets the version field mapping used for versioning. Sets the default
* value to use depending on the column type.
*
* @psalm-param array<string, mixed> $mapping The version field mapping array.
*
* @return void
*
* @throws MappingException
*/
public function setVersionMapping (array & $mapping )
{
$this -> isVersioned = true ;
$this -> versionField = $mapping [ 'fieldName' ];
$this -> requiresFetchAfterChange = true ;
if (! isset($mapping [ 'default' ])) {
if (in_array ( $mapping [ 'type' ], [ 'integer' , 'bigint' , 'smallint' ], true )) {
$mapping [ 'default' ] = 1 ;
} elseif ($mapping [ 'type' ] === 'datetime' ) {
$mapping [ 'default' ] = 'CURRENT_TIMESTAMP' ;
} else {
throw MappingException :: unsupportedOptimisticLockingType ( $this -> name , $mapping [ 'fieldName' ], $mapping [ 'type' ]);
}
}
}
/**
* Sets whether this class is to be versioned for optimistic locking.
*
* @param bool $bool
*
* @return void
*/
public function setVersioned ( $bool )
{
$this -> isVersioned = $bool ;
if ($bool ) {
$this -> requiresFetchAfterChange = true ;
}
}
/**
* Sets the name of the field that is to be used for versioning if this class is
* versioned for optimistic locking.
*
* @param string|null $versionField
*
* @return void
*/
public function setVersionField ( $versionField )
{
$this -> versionField = $versionField ;
}
/**
* Marks this class as read only, no change tracking is applied to it.
*
* @return void
*/
public function markReadOnly ()
{
$this -> isReadOnly = true ;
}
/**
* {@inheritDoc}
*/
public function getFieldNames ()
{
return array_keys ( $this -> fieldMappings );
}
/**
* {@inheritDoc}
*/
public function getAssociationNames ()
{
return array_keys ( $this -> associationMappings );
}
/**
* {@inheritDoc}
*
* @param string $assocName
*
* @return string
* @psalm-return class-string
*
* @throws InvalidArgumentException
*/
public function getAssociationTargetClass ( $assocName )
{
if (! isset($this -> associationMappings [ $assocName ])) {
throw new InvalidArgumentException ( "Association name expected, '" . $assocName . "' is not an association." );
}
return $this -> associationMappings [ $assocName ][ 'targetEntity' ];
}
/**
* {@inheritDoc}
*/
public function getName ()
{
return $this -> name ;
}
/**
* Gets the (possibly quoted) identifier column names for safe use in an SQL statement.
*
* @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
*
* @param AbstractPlatform $platform
*
* @return string[]
* @psalm-return list<string>
*/
public function getQuotedIdentifierColumnNames ( $platform )
{
$quotedColumnNames = [];
foreach ($this -> identifier as $idProperty ) {
if (isset($this -> fieldMappings [ $idProperty ])) {
$quotedColumnNames [] = isset( $this -> fieldMappings [ $idProperty ][ 'quoted' ])
? $platform -> quoteIdentifier ( $this -> fieldMappings [ $idProperty ][ 'columnName' ])
: $this -> fieldMappings [ $idProperty ][ 'columnName' ];
continue;
}
// Association defined as Id field
$joinColumns = $this -> associationMappings [ $idProperty ][ 'joinColumns' ];
$assocQuotedColumnNames = array_map (
static function ($joinColumn ) use ( $platform ) {
return isset($joinColumn [ 'quoted' ])
? $platform -> quoteIdentifier ( $joinColumn [ 'name' ])
: $joinColumn [ 'name' ];
},
$joinColumns
);
$quotedColumnNames = array_merge ( $quotedColumnNames , $assocQuotedColumnNames );
}
return $quotedColumnNames ;
}
/**
* Gets the (possibly quoted) column name of a mapped field for safe use in an SQL statement.
*
* @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
*
* @param string $field
* @param AbstractPlatform $platform
*
* @return string
*/
public function getQuotedColumnName ( $field , $platform )
{
return isset($this -> fieldMappings [ $field ][ 'quoted' ])
? $platform -> quoteIdentifier ( $this -> fieldMappings [ $field ][ 'columnName' ])
: $this -> fieldMappings [ $field ][ 'columnName' ];
}
/**
* Gets the (possibly quoted) primary table name of this class for safe use in an SQL statement.
*
* @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
*
* @param AbstractPlatform $platform
*
* @return string
*/
public function getQuotedTableName ( $platform )
{
return isset($this -> table [ 'quoted' ])
? $platform -> quoteIdentifier ( $this -> table [ 'name' ])
: $this -> table [ 'name' ];
}
/**
* Gets the (possibly quoted) name of the join table.
*
* @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
*
* @param mixed[] $assoc
* @param AbstractPlatform $platform
*
* @return string
*/
public function getQuotedJoinTableName (array $assoc , $platform )
{
return isset($assoc [ 'joinTable' ][ 'quoted' ])
? $platform -> quoteIdentifier ( $assoc [ 'joinTable' ][ 'name' ])
: $assoc [ 'joinTable' ][ 'name' ];
}
/**
* {@inheritDoc}
*/
public function isAssociationInverseSide ( $fieldName )
{
return isset($this -> associationMappings [ $fieldName ])
&& ! $this -> associationMappings [ $fieldName ][ 'isOwningSide' ];
}
/**
* {@inheritDoc}
*/
public function getAssociationMappedByTargetField ( $fieldName )
{
return $this -> associationMappings [ $fieldName ][ 'mappedBy' ];
}
/**
* @param string $targetClass
*
* @return mixed[][]
* @psalm-return array<string, array<string, mixed>>
*/
public function getAssociationsByTargetClass ( $targetClass )
{
$relations = [];
foreach ($this -> associationMappings as $mapping ) {
if ($mapping [ 'targetEntity' ] === $targetClass ) {
$relations [ $mapping [ 'fieldName' ]] = $mapping ;
}
}
return $relations ;
}
/**
* @param string|null $className
*
* @return string|null null if the input value is null
* @psalm-return class-string|null
*/
public function fullyQualifiedClassName ( $className )
{
if (empty($className )) {
return $className ;
}
if (! str_contains ( $className , '\\' ) && $this -> namespace ) {
return $this -> namespace . '\\' . $className ;
}
return $className ;
}
/**
* @param string $name
*
* @return mixed
*/
public function getMetadataValue ( $name )
{
if (isset($this -> $name )) {
return $this -> $name ;
}
return null ;
}
/**
* Map Embedded Class
*
* @psalm-param array<string, mixed> $mapping
*
* @return void
*
* @throws MappingException
*/
public function mapEmbedded (array $mapping )
{
$this -> assertFieldNotMapped ( $mapping [ 'fieldName' ]);
if (! isset($mapping [ 'class' ]) && $this -> isTypedProperty ( $mapping [ 'fieldName' ])) {
$type = $this -> reflClass -> getProperty ( $mapping [ 'fieldName' ])-> getType ();
if ($type instanceof ReflectionNamedType ) {
$mapping [ 'class' ] = $type -> getName ();
}
}
if (! (isset($mapping [ 'class' ]) && $mapping [ 'class' ])) {
throw MappingException :: missingEmbeddedClass ( $mapping [ 'fieldName' ]);
}
$fqcn = $this -> fullyQualifiedClassName ( $mapping [ 'class' ]);
assert ( $fqcn !== null );
$this -> embeddedClasses [ $mapping [ 'fieldName' ]] = [
'class' => $fqcn ,
'columnPrefix' => $mapping [ 'columnPrefix' ] ?? null ,
'declaredField' => $mapping [ 'declaredField' ] ?? null ,
'originalField' => $mapping [ 'originalField' ] ?? null ,
];
}
/**
* Inline the embeddable class
*
* @param string $property
*
* @return void
*/
public function inlineEmbeddable ( $property , ClassMetadataInfo $embeddable )
{
foreach ($embeddable -> fieldMappings as $fieldMapping ) {
$fieldMapping [ 'originalClass' ] = $fieldMapping [ 'originalClass' ] ?? $embeddable -> name ;
$fieldMapping [ 'declaredField' ] = isset( $fieldMapping [ 'declaredField' ])
? $property . '.' . $fieldMapping [ 'declaredField' ]
: $property ;
$fieldMapping [ 'originalField' ] = $fieldMapping [ 'originalField' ] ?? $fieldMapping [ 'fieldName' ];
$fieldMapping [ 'fieldName' ] = $property . '.' . $fieldMapping [ 'fieldName' ];
if (! empty($this -> embeddedClasses [ $property ][ 'columnPrefix' ])) {
$fieldMapping [ 'columnName' ] = $this -> embeddedClasses [ $property ][ 'columnPrefix' ] . $fieldMapping [ 'columnName' ];
} elseif ($this -> embeddedClasses [ $property ][ 'columnPrefix' ] !== false ) {
$fieldMapping [ 'columnName' ] = $this -> namingStrategy
-> embeddedFieldToColumnName (
$property ,
$fieldMapping [ 'columnName' ],
$this -> reflClass -> name ,
$embeddable -> reflClass -> name
);
}
$this -> mapField ( $fieldMapping );
}
}
/** @throws MappingException */
private function assertFieldNotMapped ( string $fieldName ): void
{
if (
isset($this -> fieldMappings [ $fieldName ]) ||
isset($this -> associationMappings [ $fieldName ]) ||
isset($this -> embeddedClasses [ $fieldName ])
) {
throw MappingException :: duplicateFieldMapping ( $this -> name , $fieldName );
}
}
/**
* Gets the sequence name based on class metadata.
*
* @return string
*
* @todo Sequence names should be computed in DBAL depending on the platform
*/
public function getSequenceName ( AbstractPlatform $platform )
{
$sequencePrefix = $this -> getSequencePrefix ( $platform );
$columnName = $this -> getSingleIdentifierColumnName ();
return $sequencePrefix . '_' . $columnName . '_seq' ;
}
/**
* Gets the sequence name prefix based on class metadata.
*
* @return string
*
* @todo Sequence names should be computed in DBAL depending on the platform
*/
public function getSequencePrefix ( AbstractPlatform $platform )
{
$tableName = $this -> getTableName ();
$sequencePrefix = $tableName ;
// Prepend the schema name to the table name if there is one
$schemaName = $this -> getSchemaName ();
if ($schemaName ) {
$sequencePrefix = $schemaName . '.' . $tableName ;
if (! $platform -> supportsSchemas () && $platform -> canEmulateSchemas ()) {
$sequencePrefix = $schemaName . '__' . $tableName ;
}
}
return $sequencePrefix ;
}
/** @psalm-param AssociationMapping $mapping */
private function assertMappingOrderBy (array $mapping ): void
{
if (isset($mapping [ 'orderBy' ]) && ! is_array ( $mapping [ 'orderBy' ])) {
throw new InvalidArgumentException ( "'orderBy' is expected to be an array, not " . gettype ( $mapping [ 'orderBy' ]));
}
}
/** @psalm-param class-string $class */
private function getAccessibleProperty ( ReflectionService $reflService , string $class , string $field ): ? ReflectionProperty
{
$reflectionProperty = $reflService -> getAccessibleProperty ( $class , $field );
if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty -> isReadOnly ()) {
$declaringClass = $reflectionProperty -> class ;
if ($declaringClass !== $class ) {
$reflectionProperty = $reflService -> getAccessibleProperty ( $declaringClass , $field );
}
if ($reflectionProperty !== null ) {
$reflectionProperty = new ReflectionReadonlyProperty ( $reflectionProperty );
}
}
return $reflectionProperty ;
}
}