vendor/doctrine/orm/src/Mapping/ClassMetadata.php line 948

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ORM\Mapping;
  4. use BackedEnum;
  5. use BadMethodCallException;
  6. use Doctrine\DBAL\Platforms\AbstractPlatform;
  7. use Doctrine\DBAL\Types\Types;
  8. use Doctrine\Deprecations\Deprecation;
  9. use Doctrine\Instantiator\Instantiator;
  10. use Doctrine\Instantiator\InstantiatorInterface;
  11. use Doctrine\ORM\Cache\Exception\NonCacheableEntityAssociation;
  12. use Doctrine\ORM\EntityRepository;
  13. use Doctrine\ORM\Id\AbstractIdGenerator;
  14. use Doctrine\ORM\Mapping\PropertyAccessors\EmbeddablePropertyAccessor;
  15. use Doctrine\ORM\Mapping\PropertyAccessors\EnumPropertyAccessor;
  16. use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessor;
  17. use Doctrine\ORM\Mapping\PropertyAccessors\PropertyAccessorFactory;
  18. use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
  19. use Doctrine\Persistence\Mapping\ReflectionService;
  20. use InvalidArgumentException;
  21. use LogicException;
  22. use ReflectionClass;
  23. use ReflectionNamedType;
  24. use ReflectionProperty;
  25. use Stringable;
  26. use function array_column;
  27. use function array_count_values;
  28. use function array_diff;
  29. use function array_filter;
  30. use function array_flip;
  31. use function array_intersect;
  32. use function array_key_exists;
  33. use function array_keys;
  34. use function array_map;
  35. use function array_merge;
  36. use function array_pop;
  37. use function array_values;
  38. use function assert;
  39. use function class_exists;
  40. use function count;
  41. use function defined;
  42. use function enum_exists;
  43. use function explode;
  44. use function implode;
  45. use function in_array;
  46. use function interface_exists;
  47. use function is_string;
  48. use function is_subclass_of;
  49. use function ltrim;
  50. use function method_exists;
  51. use function spl_object_id;
  52. use function sprintf;
  53. use function str_contains;
  54. use function str_replace;
  55. use function strtolower;
  56. use function trait_exists;
  57. use function trim;
  58. /**
  59. * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
  60. * of an entity and its associations.
  61. *
  62. * Once populated, ClassMetadata instances are usually cached in a serialized form.
  63. *
  64. * <b>IMPORTANT NOTE:</b>
  65. *
  66. * The fields of this class are only public for 2 reasons:
  67. * 1) To allow fast READ access.
  68. * 2) To drastically reduce the size of a serialized instance (private/protected members
  69. * get the whole class name, namespace inclusive, prepended to every property in
  70. * the serialized representation).
  71. *
  72. * @phpstan-type ConcreteAssociationMapping = OneToOneOwningSideMapping|OneToOneInverseSideMapping|ManyToOneAssociationMapping|OneToManyAssociationMapping|ManyToManyOwningSideMapping|ManyToManyInverseSideMapping
  73. * @template-covariant T of object
  74. * @template-implements PersistenceClassMetadata<T>
  75. */
  76. class ClassMetadata implements PersistenceClassMetadata, Stringable
  77. {
  78. use GetReflectionClassImplementation;
  79. /* The inheritance mapping types */
  80. /**
  81. * NONE means the class does not participate in an inheritance hierarchy
  82. * and therefore does not need an inheritance mapping type.
  83. */
  84. public const INHERITANCE_TYPE_NONE = 1;
  85. /**
  86. * JOINED means the class will be persisted according to the rules of
  87. * <tt>Class Table Inheritance</tt>.
  88. */
  89. public const INHERITANCE_TYPE_JOINED = 2;
  90. /**
  91. * SINGLE_TABLE means the class will be persisted according to the rules of
  92. * <tt>Single Table Inheritance</tt>.
  93. */
  94. public const INHERITANCE_TYPE_SINGLE_TABLE = 3;
  95. /* The Id generator types. */
  96. /**
  97. * AUTO means the generator type will depend on what the used platform prefers.
  98. * Offers full portability.
  99. */
  100. public const GENERATOR_TYPE_AUTO = 1;
  101. /**
  102. * SEQUENCE means a separate sequence object will be used. Platforms that do
  103. * not have native sequence support may emulate it. Full portability is currently
  104. * not guaranteed.
  105. */
  106. public const GENERATOR_TYPE_SEQUENCE = 2;
  107. /**
  108. * IDENTITY means an identity column is used for id generation. The database
  109. * will fill in the id column on insertion. Platforms that do not support
  110. * native identity columns may emulate them. Full portability is currently
  111. * not guaranteed.
  112. */
  113. public const GENERATOR_TYPE_IDENTITY = 4;
  114. /**
  115. * NONE means the class does not have a generated id. That means the class
  116. * must have a natural, manually assigned id.
  117. */
  118. public const GENERATOR_TYPE_NONE = 5;
  119. /**
  120. * CUSTOM means that customer will use own ID generator that supposedly work
  121. */
  122. public const GENERATOR_TYPE_CUSTOM = 7;
  123. /**
  124. * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
  125. * by doing a property-by-property comparison with the original data. This will
  126. * be done for all entities that are in MANAGED state at commit-time.
  127. *
  128. * This is the default change tracking policy.
  129. */
  130. public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
  131. /**
  132. * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
  133. * by doing a property-by-property comparison with the original data. This will
  134. * be done only for entities that were explicitly saved (through persist() or a cascade).
  135. */
  136. public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
  137. /**
  138. * Specifies that an association is to be fetched when it is first accessed.
  139. */
  140. public const FETCH_LAZY = 2;
  141. /**
  142. * Specifies that an association is to be fetched when the owner of the
  143. * association is fetched.
  144. */
  145. public const FETCH_EAGER = 3;
  146. /**
  147. * Specifies that an association is to be fetched lazy (on first access) and that
  148. * commands such as Collection#count, Collection#slice are issued directly against
  149. * the database if the collection is not yet initialized.
  150. */
  151. public const FETCH_EXTRA_LAZY = 4;
  152. /**
  153. * Identifies a one-to-one association.
  154. */
  155. public const ONE_TO_ONE = 1;
  156. /**
  157. * Identifies a many-to-one association.
  158. */
  159. public const MANY_TO_ONE = 2;
  160. /**
  161. * Identifies a one-to-many association.
  162. */
  163. public const ONE_TO_MANY = 4;
  164. /**
  165. * Identifies a many-to-many association.
  166. */
  167. public const MANY_TO_MANY = 8;
  168. /**
  169. * Combined bitmask for to-one (single-valued) associations.
  170. */
  171. public const TO_ONE = 3;
  172. /**
  173. * Combined bitmask for to-many (collection-valued) associations.
  174. */
  175. public const TO_MANY = 12;
  176. /**
  177. * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks,
  178. */
  179. public const CACHE_USAGE_READ_ONLY = 1;
  180. /**
  181. * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes.
  182. */
  183. public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2;
  184. /**
  185. * Read Write Attempts to lock the entity before update/delete.
  186. */
  187. public const CACHE_USAGE_READ_WRITE = 3;
  188. /**
  189. * The value of this column is never generated by the database.
  190. */
  191. public const GENERATED_NEVER = 0;
  192. /**
  193. * The value of this column is generated by the database on INSERT, but not on UPDATE.
  194. */
  195. public const GENERATED_INSERT = 1;
  196. /**
  197. * The value of this column is generated by the database on both INSERT and UDPATE statements.
  198. */
  199. public const GENERATED_ALWAYS = 2;
  200. /**
  201. * READ-ONLY: The namespace the entity class is contained in.
  202. *
  203. * @todo Not really needed. Usage could be localized.
  204. */
  205. public string|null $namespace = null;
  206. /**
  207. * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance
  208. * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same
  209. * as {@link $name}.
  210. *
  211. * @phpstan-var class-string
  212. */
  213. public string $rootEntityName;
  214. /**
  215. * READ-ONLY: The definition of custom generator. Only used for CUSTOM
  216. * generator type
  217. *
  218. * The definition has the following structure:
  219. * <code>
  220. * array(
  221. * 'class' => 'ClassName',
  222. * )
  223. * </code>
  224. *
  225. * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
  226. * @var array<string, string>|null
  227. */
  228. public array|null $customGeneratorDefinition = null;
  229. /**
  230. * The name of the custom repository class used for the entity class.
  231. * (Optional).
  232. *
  233. * @phpstan-var ?class-string<EntityRepository>
  234. */
  235. public string|null $customRepositoryClassName = null;
  236. /**
  237. * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
  238. */
  239. public bool $isMappedSuperclass = false;
  240. /**
  241. * READ-ONLY: Whether this class describes the mapping of an embeddable class.
  242. */
  243. public bool $isEmbeddedClass = false;
  244. /**
  245. * READ-ONLY: The names of the parent <em>entity</em> classes (ancestors), starting with the
  246. * nearest one and ending with the root entity class.
  247. *
  248. * @phpstan-var list<class-string>
  249. */
  250. public array $parentClasses = [];
  251. /**
  252. * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all
  253. * <em>entity</em> subclasses of this class. These may also be abstract classes.
  254. *
  255. * This list is used, for example, to enumerate all necessary tables in JTI when querying for root
  256. * or subclass entities, or to gather all fields comprised in an entity inheritance tree.
  257. *
  258. * For classes that do not use STI/JTI, this list is empty.
  259. *
  260. * Implementation note:
  261. *
  262. * In PHP, there is no general way to discover all subclasses of a given class at runtime. For that
  263. * reason, the list of classes given in the discriminator map at the root entity is considered
  264. * authoritative. The discriminator map must contain all <em>concrete</em> classes that can
  265. * appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract
  266. * entity classes, users are not required to list such classes with a discriminator value.
  267. *
  268. * The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the
  269. * root entity has been loaded.
  270. *
  271. * For subclasses of such root entities, the list can be reused/passed downwards, it only needs to
  272. * be filtered accordingly (only keep remaining subclasses)
  273. *
  274. * @phpstan-var list<class-string>
  275. */
  276. public array $subClasses = [];
  277. /**
  278. * READ-ONLY: The names of all embedded classes based on properties.
  279. *
  280. * @phpstan-var array<string, EmbeddedClassMapping>
  281. */
  282. public array $embeddedClasses = [];
  283. /**
  284. * READ-ONLY: The field names of all fields that are part of the identifier/primary key
  285. * of the mapped entity class.
  286. *
  287. * @phpstan-var list<string>
  288. */
  289. public array $identifier = [];
  290. /**
  291. * READ-ONLY: The inheritance mapping type used by the class.
  292. *
  293. * @phpstan-var self::INHERITANCE_TYPE_*
  294. */
  295. public int $inheritanceType = self::INHERITANCE_TYPE_NONE;
  296. /**
  297. * READ-ONLY: The Id generator type used by the class.
  298. *
  299. * @phpstan-var self::GENERATOR_TYPE_*
  300. */
  301. public int $generatorType = self::GENERATOR_TYPE_NONE;
  302. /**
  303. * READ-ONLY: The field mappings of the class.
  304. * Keys are field names and values are FieldMapping instances
  305. *
  306. * @var array<string, FieldMapping>
  307. */
  308. public array $fieldMappings = [];
  309. /**
  310. * READ-ONLY: An array of field names. Used to look up field names from column names.
  311. * Keys are column names and values are field names.
  312. *
  313. * @phpstan-var array<string, string>
  314. */
  315. public array $fieldNames = [];
  316. /**
  317. * READ-ONLY: A map of field names to column names. Keys are field names and values column names.
  318. * Used to look up column names from field names.
  319. * This is the reverse lookup map of $_fieldNames.
  320. *
  321. * @deprecated 3.0 Remove this.
  322. *
  323. * @var mixed[]
  324. */
  325. public array $columnNames = [];
  326. /**
  327. * READ-ONLY: The discriminator value of this class.
  328. *
  329. * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
  330. * where a discriminator column is used.</b>
  331. *
  332. * @see discriminatorColumn
  333. */
  334. public mixed $discriminatorValue = null;
  335. /**
  336. * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
  337. *
  338. * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
  339. * where a discriminator column is used.</b>
  340. *
  341. * @see discriminatorColumn
  342. *
  343. * @var array<int|string, string>
  344. *
  345. * @phpstan-var array<int|string, class-string>
  346. */
  347. public array $discriminatorMap = [];
  348. /**
  349. * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
  350. * inheritance mappings.
  351. */
  352. public DiscriminatorColumnMapping|null $discriminatorColumn = null;
  353. /**
  354. * READ-ONLY: The primary table definition.
  355. *
  356. * "quoted" indicates whether the table name is quoted (with backticks) or not
  357. *
  358. * @var mixed[]
  359. * @phpstan-var array{
  360. * name: string,
  361. * schema?: string,
  362. * indexes?: array,
  363. * uniqueConstraints?: array,
  364. * options?: array<string, mixed>,
  365. * quoted?: bool
  366. * }
  367. */
  368. public array $table;
  369. /**
  370. * READ-ONLY: The registered lifecycle callbacks for entities of this class.
  371. *
  372. * @phpstan-var array<string, list<string>>
  373. */
  374. public array $lifecycleCallbacks = [];
  375. /**
  376. * READ-ONLY: The registered entity listeners.
  377. *
  378. * @phpstan-var array<string, list<array{class: class-string, method: string}>>
  379. */
  380. public array $entityListeners = [];
  381. /**
  382. * READ-ONLY: The association mappings of this class.
  383. *
  384. * A join table definition has the following structure:
  385. * <pre>
  386. * array(
  387. * 'name' => <join table name>,
  388. * 'joinColumns' => array(<join column mapping from join table to source table>),
  389. * 'inverseJoinColumns' => array(<join column mapping from join table to target table>)
  390. * )
  391. * </pre>
  392. *
  393. * @phpstan-var array<string, ConcreteAssociationMapping>
  394. */
  395. public array $associationMappings = [];
  396. /**
  397. * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
  398. */
  399. public bool $isIdentifierComposite = false;
  400. /**
  401. * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association.
  402. *
  403. * This flag is necessary because some code blocks require special treatment of this cases.
  404. */
  405. public bool $containsForeignIdentifier = false;
  406. /**
  407. * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type.
  408. *
  409. * This flag is necessary because some code blocks require special treatment of this cases.
  410. */
  411. public bool $containsEnumIdentifier = false;
  412. /**
  413. * READ-ONLY: The ID generator used for generating IDs for this class.
  414. *
  415. * @todo Remove!
  416. */
  417. public AbstractIdGenerator $idGenerator;
  418. /**
  419. * READ-ONLY: The definition of the sequence generator of this class. Only used for the
  420. * SEQUENCE generation strategy.
  421. *
  422. * The definition has the following structure:
  423. * <code>
  424. * array(
  425. * 'sequenceName' => 'name',
  426. * 'allocationSize' => '20',
  427. * 'initialValue' => '1'
  428. * )
  429. * </code>
  430. *
  431. * @var array<string, mixed>|null
  432. * @phpstan-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
  433. * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
  434. */
  435. public array|null $sequenceGeneratorDefinition = null;
  436. /**
  437. * READ-ONLY: The policy used for change-tracking on entities of this class.
  438. */
  439. public int $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
  440. /**
  441. * READ-ONLY: A Flag indicating whether one or more columns of this class
  442. * have to be reloaded after insert / update operations.
  443. */
  444. public bool $requiresFetchAfterChange = false;
  445. /**
  446. * READ-ONLY: A flag for whether or not instances of this class are to be versioned
  447. * with optimistic locking.
  448. */
  449. public bool $isVersioned = false;
  450. /**
  451. * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
  452. */
  453. public string|null $versionField = null;
  454. /** @var mixed[]|null */
  455. public array|null $cache = null;
  456. /**
  457. * The ReflectionClass instance of the mapped class.
  458. *
  459. * @var ReflectionClass<T>|null
  460. */
  461. public ReflectionClass|null $reflClass = null;
  462. /**
  463. * Is this entity marked as "read-only"?
  464. *
  465. * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance
  466. * optimization for entities that are immutable, either in your domain or through the relation database
  467. * (coming from a view, or a history table for example).
  468. */
  469. public bool $isReadOnly = false;
  470. /**
  471. * NamingStrategy determining the default column and table names.
  472. */
  473. protected NamingStrategy $namingStrategy;
  474. /**
  475. * The ReflectionProperty instances of the mapped class.
  476. *
  477. * @deprecated Use $propertyAccessors instead.
  478. *
  479. * @var LegacyReflectionFields|array<string, ReflectionProperty>
  480. */
  481. public LegacyReflectionFields|array $reflFields = [];
  482. /** @var array<string, PropertyAccessors\PropertyAccessor> */
  483. public array $propertyAccessors = [];
  484. private InstantiatorInterface|null $instantiator = null;
  485. private readonly TypedFieldMapper $typedFieldMapper;
  486. /**
  487. * Initializes a new ClassMetadata instance that will hold the object-relational mapping
  488. * metadata of the class with the given name.
  489. *
  490. * @param string $name The name of the entity class the new instance is used for.
  491. * @phpstan-param class-string<T> $name
  492. */
  493. public function __construct(public string $name, NamingStrategy|null $namingStrategy = null, TypedFieldMapper|null $typedFieldMapper = null)
  494. {
  495. $this->rootEntityName = $name;
  496. $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy();
  497. $this->instantiator = new Instantiator();
  498. $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper();
  499. }
  500. /**
  501. * Gets the ReflectionProperties of the mapped class.
  502. *
  503. * @deprecated Use getPropertyAccessors() instead.
  504. *
  505. * @return LegacyReflectionFields|ReflectionProperty[] An array of ReflectionProperty instances.
  506. * @phpstan-return LegacyReflectionFields|array<string, ReflectionProperty>
  507. */
  508. public function getReflectionProperties(): array|LegacyReflectionFields
  509. {
  510. return $this->reflFields;
  511. }
  512. /**
  513. * Gets the ReflectionProperties of the mapped class.
  514. *
  515. * @return PropertyAccessor[] An array of PropertyAccessor instances.
  516. */
  517. public function getPropertyAccessors(): array
  518. {
  519. return $this->propertyAccessors;
  520. }
  521. /**
  522. * Gets a ReflectionProperty for a specific field of the mapped class.
  523. *
  524. * @deprecated Use getPropertyAccessor() instead.
  525. */
  526. public function getReflectionProperty(string $name): ReflectionProperty|null
  527. {
  528. return $this->reflFields[$name];
  529. }
  530. public function getPropertyAccessor(string $name): PropertyAccessor|null
  531. {
  532. return $this->propertyAccessors[$name] ?? null;
  533. }
  534. /**
  535. * @deprecated Use getPropertyAccessor() instead.
  536. *
  537. * @throws BadMethodCallException If the class has a composite identifier.
  538. */
  539. public function getSingleIdReflectionProperty(): ReflectionProperty|null
  540. {
  541. if ($this->isIdentifierComposite) {
  542. throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
  543. }
  544. return $this->reflFields[$this->identifier[0]];
  545. }
  546. /** @throws BadMethodCallException If the class has a composite identifier. */
  547. public function getSingleIdPropertyAccessor(): PropertyAccessor|null
  548. {
  549. if ($this->isIdentifierComposite) {
  550. throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
  551. }
  552. return $this->propertyAccessors[$this->identifier[0]];
  553. }
  554. /**
  555. * Extracts the identifier values of an entity of this class.
  556. *
  557. * For composite identifiers, the identifier values are returned as an array
  558. * with the same order as the field order in {@link identifier}.
  559. *
  560. * @return array<string, mixed>
  561. */
  562. public function getIdentifierValues(object $entity): array
  563. {
  564. if ($this->isIdentifierComposite) {
  565. $id = [];
  566. foreach ($this->identifier as $idField) {
  567. $value = $this->propertyAccessors[$idField]->getValue($entity);
  568. if ($value !== null) {
  569. $id[$idField] = $value;
  570. }
  571. }
  572. return $id;
  573. }
  574. $id = $this->identifier[0];
  575. $value = $this->propertyAccessors[$id]->getValue($entity);
  576. if ($value === null) {
  577. return [];
  578. }
  579. return [$id => $value];
  580. }
  581. /**
  582. * Populates the entity identifier of an entity.
  583. *
  584. * @phpstan-param array<string, mixed> $id
  585. *
  586. * @todo Rename to assignIdentifier()
  587. */
  588. public function setIdentifierValues(object $entity, array $id): void
  589. {
  590. foreach ($id as $idField => $idValue) {
  591. $this->propertyAccessors[$idField]->setValue($entity, $idValue);
  592. }
  593. }
  594. /**
  595. * Sets the specified field to the specified value on the given entity.
  596. */
  597. public function setFieldValue(object $entity, string $field, mixed $value): void
  598. {
  599. $this->propertyAccessors[$field]->setValue($entity, $value);
  600. }
  601. /**
  602. * Gets the specified field's value off the given entity.
  603. */
  604. public function getFieldValue(object $entity, string $field): mixed
  605. {
  606. return $this->propertyAccessors[$field]->getValue($entity);
  607. }
  608. /**
  609. * Creates a string representation of this instance.
  610. *
  611. * @return string The string representation of this instance.
  612. *
  613. * @todo Construct meaningful string representation.
  614. */
  615. public function __toString(): string
  616. {
  617. return self::class . '@' . spl_object_id($this);
  618. }
  619. /**
  620. * Determines which fields get serialized.
  621. *
  622. * It is only serialized what is necessary for best unserialization performance.
  623. * That means any metadata properties that are not set or empty or simply have
  624. * their default value are NOT serialized.
  625. *
  626. * Parts that are also NOT serialized because they can not be properly unserialized:
  627. * - reflClass (ReflectionClass)
  628. * - reflFields (ReflectionProperty array)
  629. *
  630. * @return string[] The names of all the fields that should be serialized.
  631. */
  632. public function __sleep(): array
  633. {
  634. // This metadata is always serialized/cached.
  635. $serialized = [
  636. 'associationMappings',
  637. 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName']
  638. 'fieldMappings',
  639. 'fieldNames',
  640. 'embeddedClasses',
  641. 'identifier',
  642. 'isIdentifierComposite', // TODO: REMOVE
  643. 'name',
  644. 'namespace', // TODO: REMOVE
  645. 'table',
  646. 'rootEntityName',
  647. 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime.
  648. ];
  649. // The rest of the metadata is only serialized if necessary.
  650. if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
  651. $serialized[] = 'changeTrackingPolicy';
  652. }
  653. if ($this->customRepositoryClassName) {
  654. $serialized[] = 'customRepositoryClassName';
  655. }
  656. if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) {
  657. $serialized[] = 'inheritanceType';
  658. $serialized[] = 'discriminatorColumn';
  659. $serialized[] = 'discriminatorValue';
  660. $serialized[] = 'discriminatorMap';
  661. $serialized[] = 'parentClasses';
  662. $serialized[] = 'subClasses';
  663. }
  664. if ($this->generatorType !== self::GENERATOR_TYPE_NONE) {
  665. $serialized[] = 'generatorType';
  666. if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) {
  667. $serialized[] = 'sequenceGeneratorDefinition';
  668. }
  669. }
  670. if ($this->isMappedSuperclass) {
  671. $serialized[] = 'isMappedSuperclass';
  672. }
  673. if ($this->isEmbeddedClass) {
  674. $serialized[] = 'isEmbeddedClass';
  675. }
  676. if ($this->containsForeignIdentifier) {
  677. $serialized[] = 'containsForeignIdentifier';
  678. }
  679. if ($this->containsEnumIdentifier) {
  680. $serialized[] = 'containsEnumIdentifier';
  681. }
  682. if ($this->isVersioned) {
  683. $serialized[] = 'isVersioned';
  684. $serialized[] = 'versionField';
  685. }
  686. if ($this->lifecycleCallbacks) {
  687. $serialized[] = 'lifecycleCallbacks';
  688. }
  689. if ($this->entityListeners) {
  690. $serialized[] = 'entityListeners';
  691. }
  692. if ($this->isReadOnly) {
  693. $serialized[] = 'isReadOnly';
  694. }
  695. if ($this->customGeneratorDefinition) {
  696. $serialized[] = 'customGeneratorDefinition';
  697. }
  698. if ($this->cache) {
  699. $serialized[] = 'cache';
  700. }
  701. if ($this->requiresFetchAfterChange) {
  702. $serialized[] = 'requiresFetchAfterChange';
  703. }
  704. return $serialized;
  705. }
  706. /**
  707. * Creates a new instance of the mapped class, without invoking the constructor.
  708. */
  709. public function newInstance(): object
  710. {
  711. return $this->instantiator->instantiate($this->name);
  712. }
  713. /**
  714. * Restores some state that can not be serialized/unserialized.
  715. */
  716. public function wakeupReflection(ReflectionService $reflService): void
  717. {
  718. // Restore ReflectionClass and properties
  719. $this->reflClass = $reflService->getClass($this->name);
  720. /** @phpstan-ignore property.deprecated */
  721. $this->reflFields = new LegacyReflectionFields($this, $reflService);
  722. $this->instantiator = $this->instantiator ?: new Instantiator();
  723. $parentAccessors = [];
  724. foreach ($this->embeddedClasses as $property => $embeddedClass) {
  725. if (isset($embeddedClass->declaredField)) {
  726. assert($embeddedClass->originalField !== null);
  727. $childAccessor = PropertyAccessorFactory::createPropertyAccessor(
  728. $this->embeddedClasses[$embeddedClass->declaredField]->class,
  729. $embeddedClass->originalField,
  730. );
  731. $parentAccessors[$property] = new EmbeddablePropertyAccessor(
  732. $parentAccessors[$embeddedClass->declaredField],
  733. $childAccessor,
  734. $this->embeddedClasses[$embeddedClass->declaredField]->class,
  735. );
  736. continue;
  737. }
  738. $accessor = PropertyAccessorFactory::createPropertyAccessor(
  739. $embeddedClass->declared ?? $this->name,
  740. $property,
  741. );
  742. $parentAccessors[$property] = $accessor;
  743. $this->propertyAccessors[$property] = $accessor;
  744. }
  745. foreach ($this->fieldMappings as $field => $mapping) {
  746. if (isset($mapping->declaredField) && isset($parentAccessors[$mapping->declaredField])) {
  747. assert($mapping->originalField !== null);
  748. assert($mapping->originalClass !== null);
  749. $accessor = PropertyAccessorFactory::createPropertyAccessor($mapping->originalClass, $mapping->originalField);
  750. if ($mapping->enumType !== null) {
  751. $accessor = new EnumPropertyAccessor(
  752. $accessor,
  753. $mapping->enumType,
  754. );
  755. }
  756. $this->propertyAccessors[$field] = new EmbeddablePropertyAccessor(
  757. $parentAccessors[$mapping->declaredField],
  758. $accessor,
  759. $mapping->originalClass,
  760. );
  761. continue;
  762. }
  763. $this->propertyAccessors[$field] = isset($mapping->declared)
  764. ? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field)
  765. : PropertyAccessorFactory::createPropertyAccessor($this->name, $field);
  766. if ($mapping->enumType !== null) {
  767. $this->propertyAccessors[$field] = new EnumPropertyAccessor(
  768. $this->propertyAccessors[$field],
  769. $mapping->enumType,
  770. );
  771. }
  772. }
  773. foreach ($this->associationMappings as $field => $mapping) {
  774. $this->propertyAccessors[$field] = isset($mapping->declared)
  775. ? PropertyAccessorFactory::createPropertyAccessor($mapping->declared, $field)
  776. : PropertyAccessorFactory::createPropertyAccessor($this->name, $field);
  777. }
  778. }
  779. /**
  780. * Initializes a new ClassMetadata instance that will hold the object-relational mapping
  781. * metadata of the class with the given name.
  782. *
  783. * @param ReflectionService $reflService The reflection service.
  784. */
  785. public function initializeReflection(ReflectionService $reflService): void
  786. {
  787. $this->reflClass = $reflService->getClass($this->name);
  788. $this->namespace = $reflService->getClassNamespace($this->name);
  789. if ($this->reflClass) {
  790. $this->name = $this->rootEntityName = $this->reflClass->name;
  791. }
  792. $this->table['name'] = $this->namingStrategy->classToTableName($this->name);
  793. }
  794. /**
  795. * Validates Identifier.
  796. *
  797. * @throws MappingException
  798. */
  799. public function validateIdentifier(): void
  800. {
  801. if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
  802. return;
  803. }
  804. // Verify & complete identifier mapping
  805. if (! $this->identifier) {
  806. throw MappingException::identifierRequired($this->name);
  807. }
  808. if ($this->usesIdGenerator() && $this->isIdentifierComposite) {
  809. throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name);
  810. }
  811. }
  812. /**
  813. * Validates association targets actually exist.
  814. *
  815. * @throws MappingException
  816. */
  817. public function validateAssociations(): void
  818. {
  819. foreach ($this->associationMappings as $mapping) {
  820. if (
  821. ! class_exists($mapping->targetEntity)
  822. && ! interface_exists($mapping->targetEntity)
  823. && ! trait_exists($mapping->targetEntity)
  824. ) {
  825. throw MappingException::invalidTargetEntityClass($mapping->targetEntity, $this->name, $mapping->fieldName);
  826. }
  827. }
  828. }
  829. /**
  830. * Validates lifecycle callbacks.
  831. *
  832. * @throws MappingException
  833. */
  834. public function validateLifecycleCallbacks(ReflectionService $reflService): void
  835. {
  836. foreach ($this->lifecycleCallbacks as $callbacks) {
  837. foreach ($callbacks as $callbackFuncName) {
  838. if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) {
  839. throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName);
  840. }
  841. }
  842. }
  843. }
  844. /** @phpstan-param array{usage?: mixed, region?: mixed} $cache */
  845. public function enableCache(array $cache): void
  846. {
  847. if (! isset($cache['usage'])) {
  848. $cache['usage'] = self::CACHE_USAGE_READ_ONLY;
  849. }
  850. if (! isset($cache['region'])) {
  851. $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName));
  852. }
  853. $this->cache = $cache;
  854. }
  855. /** @phpstan-param array{usage?: int, region?: string} $cache */
  856. public function enableAssociationCache(string $fieldName, array $cache): void
  857. {
  858. $this->associationMappings[$fieldName]->cache = $this->getAssociationCacheDefaults($fieldName, $cache);
  859. }
  860. /**
  861. * @phpstan-param array{usage?: int, region?: string|null} $cache
  862. *
  863. * @return int[]|string[]
  864. * @phpstan-return array{usage: int, region: string|null}
  865. */
  866. public function getAssociationCacheDefaults(string $fieldName, array $cache): array
  867. {
  868. if (! isset($cache['usage'])) {
  869. $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY;
  870. }
  871. if (! isset($cache['region'])) {
  872. $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName;
  873. }
  874. return $cache;
  875. }
  876. /**
  877. * Sets the change tracking policy used by this class.
  878. */
  879. public function setChangeTrackingPolicy(int $policy): void
  880. {
  881. $this->changeTrackingPolicy = $policy;
  882. }
  883. /**
  884. * Whether the change tracking policy of this class is "deferred explicit".
  885. */
  886. public function isChangeTrackingDeferredExplicit(): bool
  887. {
  888. return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
  889. }
  890. /**
  891. * Whether the change tracking policy of this class is "deferred implicit".
  892. */
  893. public function isChangeTrackingDeferredImplicit(): bool
  894. {
  895. return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
  896. }
  897. /**
  898. * Checks whether a field is part of the identifier/primary key field(s).
  899. */
  900. public function isIdentifier(string $fieldName): bool
  901. {
  902. if (! $this->identifier) {
  903. return false;
  904. }
  905. if (! $this->isIdentifierComposite) {
  906. return $fieldName === $this->identifier[0];
  907. }
  908. return in_array($fieldName, $this->identifier, true);
  909. }
  910. public function isUniqueField(string $fieldName): bool
  911. {
  912. $mapping = $this->getFieldMapping($fieldName);
  913. return $mapping !== false && isset($mapping->unique) && $mapping->unique;
  914. }
  915. public function isNullable(string $fieldName): bool
  916. {
  917. $mapping = $this->getFieldMapping($fieldName);
  918. return $mapping !== false && isset($mapping->nullable) && $mapping->nullable;
  919. }
  920. public function isIndexed(string $fieldName): bool
  921. {
  922. $mapping = $this->getFieldMapping($fieldName);
  923. return isset($mapping->index) && $mapping->index;
  924. }
  925. /**
  926. * Gets a column name for a field name.
  927. * If the column name for the field cannot be found, the given field name
  928. * is returned.
  929. */
  930. public function getColumnName(string $fieldName): string
  931. {
  932. // @phpstan-ignore property.deprecated
  933. return $this->columnNames[$fieldName] ?? $fieldName;
  934. }
  935. /**
  936. * Gets the mapping of a (regular) field that holds some data but not a
  937. * reference to another object.
  938. *
  939. * @throws MappingException
  940. */
  941. public function getFieldMapping(string $fieldName): FieldMapping
  942. {
  943. if (! isset($this->fieldMappings[$fieldName])) {
  944. throw MappingException::mappingNotFound($this->name, $fieldName);
  945. }
  946. return $this->fieldMappings[$fieldName];
  947. }
  948. /**
  949. * Gets the mapping of an association.
  950. *
  951. * @see ClassMetadata::$associationMappings
  952. *
  953. * @param string $fieldName The field name that represents the association in
  954. * the object model.
  955. *
  956. * @throws MappingException
  957. */
  958. public function getAssociationMapping(string $fieldName): AssociationMapping
  959. {
  960. if (! isset($this->associationMappings[$fieldName])) {
  961. throw MappingException::mappingNotFound($this->name, $fieldName);
  962. }
  963. return $this->associationMappings[$fieldName];
  964. }
  965. /**
  966. * Gets all association mappings of the class.
  967. *
  968. * @phpstan-return array<string, AssociationMapping>
  969. */
  970. public function getAssociationMappings(): array
  971. {
  972. return $this->associationMappings;
  973. }
  974. /**
  975. * Gets the field name for a column name.
  976. * If no field name can be found the column name is returned.
  977. *
  978. * @return string The column alias.
  979. */
  980. public function getFieldName(string $columnName): string
  981. {
  982. return $this->fieldNames[$columnName] ?? $columnName;
  983. }
  984. /**
  985. * Checks whether given property has type
  986. */
  987. private function isTypedProperty(string $name): bool
  988. {
  989. return isset($this->reflClass)
  990. && $this->reflClass->hasProperty($name)
  991. && $this->reflClass->getProperty($name)->hasType();
  992. }
  993. /**
  994. * Validates & completes the given field mapping based on typed property.
  995. *
  996. * @param array{fieldName: string, type?: string} $mapping The field mapping to validate & complete.
  997. *
  998. * @return array{fieldName: string, enumType?: class-string<BackedEnum>, type?: string} The updated mapping.
  999. */
  1000. private function validateAndCompleteTypedFieldMapping(array $mapping): array
  1001. {
  1002. $field = $this->reflClass->getProperty($mapping['fieldName']);
  1003. return $this->typedFieldMapper->validateAndComplete($mapping, $field);
  1004. }
  1005. /**
  1006. * Validates & completes the basic mapping information based on typed property.
  1007. *
  1008. * @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.
  1009. *
  1010. * @return mixed[] The updated mapping.
  1011. */
  1012. private function validateAndCompleteTypedAssociationMapping(array $mapping): array
  1013. {
  1014. $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
  1015. if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) {
  1016. return $mapping;
  1017. }
  1018. if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) {
  1019. $mapping['targetEntity'] = $type->getName();
  1020. }
  1021. return $mapping;
  1022. }
  1023. /**
  1024. * Validates & completes the given field mapping.
  1025. *
  1026. * @phpstan-param array{
  1027. * fieldName?: string,
  1028. * columnName?: string,
  1029. * id?: bool,
  1030. * generated?: self::GENERATED_*,
  1031. * enumType?: class-string,
  1032. * } $mapping The field mapping to validate & complete.
  1033. *
  1034. * @return FieldMapping The updated mapping.
  1035. *
  1036. * @throws MappingException
  1037. */
  1038. protected function validateAndCompleteFieldMapping(array $mapping): FieldMapping
  1039. {
  1040. // Check mandatory fields
  1041. if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
  1042. throw MappingException::missingFieldName($this->name);
  1043. }
  1044. if ($this->isTypedProperty($mapping['fieldName'])) {
  1045. $mapping = $this->validateAndCompleteTypedFieldMapping($mapping);
  1046. }
  1047. if (! isset($mapping['type'])) {
  1048. // Default to string
  1049. $mapping['type'] = 'string';
  1050. }
  1051. // Complete fieldName and columnName mapping
  1052. if (! isset($mapping['columnName'])) {
  1053. $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name);
  1054. }
  1055. $mapping = FieldMapping::fromMappingArray($mapping);
  1056. if ($mapping->columnName[0] === '`') {
  1057. $mapping->columnName = trim($mapping->columnName, '`');
  1058. $mapping->quoted = true;
  1059. }
  1060. // @phpstan-ignore property.deprecated
  1061. $this->columnNames[$mapping->fieldName] = $mapping->columnName;
  1062. if (isset($this->fieldNames[$mapping->columnName]) || ($this->discriminatorColumn && $this->discriminatorColumn->name === $mapping->columnName)) {
  1063. throw MappingException::duplicateColumnName($this->name, $mapping->columnName);
  1064. }
  1065. $this->fieldNames[$mapping->columnName] = $mapping->fieldName;
  1066. // Complete id mapping
  1067. if (isset($mapping->id) && $mapping->id === true) {
  1068. if ($this->versionField === $mapping->fieldName) {
  1069. throw MappingException::cannotVersionIdField($this->name, $mapping->fieldName);
  1070. }
  1071. if (! in_array($mapping->fieldName, $this->identifier, true)) {
  1072. $this->identifier[] = $mapping->fieldName;
  1073. }
  1074. // Check for composite key
  1075. if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
  1076. $this->isIdentifierComposite = true;
  1077. }
  1078. }
  1079. if (isset($mapping->generated)) {
  1080. if (! in_array($mapping->generated, [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) {
  1081. throw MappingException::invalidGeneratedMode($mapping->generated);
  1082. }
  1083. if ($mapping->generated === self::GENERATED_NEVER) {
  1084. unset($mapping->generated);
  1085. }
  1086. }
  1087. if (isset($mapping->enumType)) {
  1088. if (! enum_exists($mapping->enumType)) {
  1089. throw MappingException::nonEnumTypeMapped($this->name, $mapping->fieldName, $mapping->enumType);
  1090. }
  1091. if (! empty($mapping->id)) {
  1092. $this->containsEnumIdentifier = true;
  1093. }
  1094. if (
  1095. defined('Doctrine\DBAL\Types\Types::ENUM')
  1096. && $mapping->type === Types::ENUM
  1097. && ! isset($mapping->options['values'])
  1098. ) {
  1099. $mapping->options['values'] = array_column($mapping->enumType::cases(), 'value');
  1100. }
  1101. }
  1102. return $mapping;
  1103. }
  1104. /**
  1105. * Validates & completes the basic mapping information that is common to all
  1106. * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
  1107. *
  1108. * @phpstan-param array<string, mixed> $mapping The mapping.
  1109. *
  1110. * @return ConcreteAssociationMapping
  1111. *
  1112. * @throws MappingException If something is wrong with the mapping.
  1113. */
  1114. protected function _validateAndCompleteAssociationMapping(array $mapping): AssociationMapping
  1115. {
  1116. if (array_key_exists('mappedBy', $mapping) && $mapping['mappedBy'] === null) {
  1117. unset($mapping['mappedBy']);
  1118. }
  1119. if (array_key_exists('inversedBy', $mapping) && $mapping['inversedBy'] === null) {
  1120. unset($mapping['inversedBy']);
  1121. }
  1122. if (array_key_exists('joinColumns', $mapping) && in_array($mapping['joinColumns'], [null, []], true)) {
  1123. unset($mapping['joinColumns']);
  1124. }
  1125. $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
  1126. if (empty($mapping['indexBy'])) {
  1127. unset($mapping['indexBy']);
  1128. }
  1129. // If targetEntity is unqualified, assume it is in the same namespace as
  1130. // the sourceEntity.
  1131. $mapping['sourceEntity'] = $this->name;
  1132. if ($this->isTypedProperty($mapping['fieldName'])) {
  1133. $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
  1134. }
  1135. if (isset($mapping['targetEntity'])) {
  1136. $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']);
  1137. $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
  1138. }
  1139. if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
  1140. throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']);
  1141. }
  1142. // Complete id mapping
  1143. if (isset($mapping['id']) && $mapping['id'] === true) {
  1144. if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
  1145. throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']);
  1146. }
  1147. if (! in_array($mapping['fieldName'], $this->identifier, true)) {
  1148. if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) {
  1149. throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
  1150. $mapping['targetEntity'],
  1151. $this->name,
  1152. $mapping['fieldName'],
  1153. );
  1154. }
  1155. assert(is_string($mapping['fieldName']));
  1156. $this->identifier[] = $mapping['fieldName'];
  1157. $this->containsForeignIdentifier = true;
  1158. }
  1159. // Check for composite key
  1160. if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
  1161. $this->isIdentifierComposite = true;
  1162. }
  1163. if ($this->cache && ! isset($mapping['cache'])) {
  1164. throw NonCacheableEntityAssociation::fromEntityAndField(
  1165. $this->name,
  1166. $mapping['fieldName'],
  1167. );
  1168. }
  1169. }
  1170. // Mandatory attributes for both sides
  1171. // Mandatory: fieldName, targetEntity
  1172. if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
  1173. throw MappingException::missingFieldName($this->name);
  1174. }
  1175. if (! isset($mapping['targetEntity'])) {
  1176. throw MappingException::missingTargetEntity($mapping['fieldName']);
  1177. }
  1178. // Mandatory and optional attributes for either side
  1179. if (! isset($mapping['mappedBy'])) {
  1180. if (isset($mapping['joinTable'])) {
  1181. if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') {
  1182. $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`');
  1183. $mapping['joinTable']['quoted'] = true;
  1184. }
  1185. }
  1186. } else {
  1187. $mapping['isOwningSide'] = false;
  1188. }
  1189. if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
  1190. throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']);
  1191. }
  1192. // Fetch mode. Default fetch mode to LAZY, if not set.
  1193. if (! isset($mapping['fetch'])) {
  1194. $mapping['fetch'] = self::FETCH_LAZY;
  1195. }
  1196. // Cascades
  1197. $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
  1198. $allCascades = ['remove', 'persist', 'refresh', 'detach'];
  1199. if (in_array('all', $cascades, true)) {
  1200. $cascades = $allCascades;
  1201. } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
  1202. throw MappingException::invalidCascadeOption(
  1203. array_diff($cascades, $allCascades),
  1204. $this->name,
  1205. $mapping['fieldName'],
  1206. );
  1207. }
  1208. $mapping['cascade'] = $cascades;
  1209. switch ($mapping['type']) {
  1210. case self::ONE_TO_ONE:
  1211. if (isset($mapping['joinColumns']) && $mapping['joinColumns'] && ! $mapping['isOwningSide']) {
  1212. throw MappingException::joinColumnNotAllowedOnOneToOneInverseSide(
  1213. $this->name,
  1214. $mapping['fieldName'],
  1215. );
  1216. }
  1217. return $mapping['isOwningSide'] ?
  1218. OneToOneOwningSideMapping::fromMappingArrayAndName(
  1219. $mapping,
  1220. $this->namingStrategy,
  1221. $this->name,
  1222. $this->table ?? null,
  1223. $this->isInheritanceTypeSingleTable(),
  1224. ) :
  1225. OneToOneInverseSideMapping::fromMappingArrayAndName($mapping, $this->name);
  1226. case self::MANY_TO_ONE:
  1227. return ManyToOneAssociationMapping::fromMappingArrayAndName(
  1228. $mapping,
  1229. $this->namingStrategy,
  1230. $this->name,
  1231. $this->table ?? null,
  1232. $this->isInheritanceTypeSingleTable(),
  1233. );
  1234. case self::ONE_TO_MANY:
  1235. return OneToManyAssociationMapping::fromMappingArrayAndName($mapping, $this->name);
  1236. case self::MANY_TO_MANY:
  1237. if (isset($mapping['joinColumns'])) {
  1238. unset($mapping['joinColumns']);
  1239. }
  1240. return $mapping['isOwningSide'] ?
  1241. ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy($mapping, $this->namingStrategy) :
  1242. ManyToManyInverseSideMapping::fromMappingArray($mapping);
  1243. default:
  1244. throw MappingException::invalidAssociationType(
  1245. $this->name,
  1246. $mapping['fieldName'],
  1247. $mapping['type'],
  1248. );
  1249. }
  1250. }
  1251. /**
  1252. * {@inheritDoc}
  1253. */
  1254. public function getIdentifierFieldNames(): array
  1255. {
  1256. return $this->identifier;
  1257. }
  1258. /**
  1259. * Gets the name of the single id field. Note that this only works on
  1260. * entity classes that have a single-field pk.
  1261. *
  1262. * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
  1263. */
  1264. public function getSingleIdentifierFieldName(): string
  1265. {
  1266. if ($this->isIdentifierComposite) {
  1267. throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name);
  1268. }
  1269. if (! isset($this->identifier[0])) {
  1270. throw MappingException::noIdDefined($this->name);
  1271. }
  1272. return $this->identifier[0];
  1273. }
  1274. /**
  1275. * Gets the column name of the single id column. Note that this only works on
  1276. * entity classes that have a single-field pk.
  1277. *
  1278. * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
  1279. */
  1280. public function getSingleIdentifierColumnName(): string
  1281. {
  1282. return $this->getColumnName($this->getSingleIdentifierFieldName());
  1283. }
  1284. /**
  1285. * INTERNAL:
  1286. * Sets the mapped identifier/primary key fields of this class.
  1287. * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
  1288. *
  1289. * @phpstan-param list<mixed> $identifier
  1290. */
  1291. public function setIdentifier(array $identifier): void
  1292. {
  1293. $this->identifier = $identifier;
  1294. $this->isIdentifierComposite = (count($this->identifier) > 1);
  1295. }
  1296. /**
  1297. * {@inheritDoc}
  1298. */
  1299. public function getIdentifier(): array
  1300. {
  1301. return $this->identifier;
  1302. }
  1303. public function hasField(string $fieldName): bool
  1304. {
  1305. return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]);
  1306. }
  1307. /**
  1308. * Gets an array containing all the column names.
  1309. *
  1310. * @phpstan-param list<string>|null $fieldNames
  1311. *
  1312. * @return mixed[]
  1313. * @phpstan-return list<string>
  1314. */
  1315. public function getColumnNames(array|null $fieldNames = null): array
  1316. {
  1317. if ($fieldNames === null) {
  1318. return array_keys($this->fieldNames);
  1319. }
  1320. return array_values(array_map($this->getColumnName(...), $fieldNames));
  1321. }
  1322. /**
  1323. * Returns an array with all the identifier column names.
  1324. *
  1325. * @phpstan-return list<string>
  1326. */
  1327. public function getIdentifierColumnNames(): array
  1328. {
  1329. $columnNames = [];
  1330. foreach ($this->identifier as $idProperty) {
  1331. if (isset($this->fieldMappings[$idProperty])) {
  1332. $columnNames[] = $this->fieldMappings[$idProperty]->columnName;
  1333. continue;
  1334. }
  1335. // Association defined as Id field
  1336. assert($this->associationMappings[$idProperty]->isToOneOwningSide());
  1337. $joinColumns = $this->associationMappings[$idProperty]->joinColumns;
  1338. $assocColumnNames = array_map(static fn (JoinColumnMapping $joinColumn): string => $joinColumn->name, $joinColumns);
  1339. $columnNames = array_merge($columnNames, $assocColumnNames);
  1340. }
  1341. return $columnNames;
  1342. }
  1343. /**
  1344. * Sets the type of Id generator to use for the mapped class.
  1345. *
  1346. * @phpstan-param self::GENERATOR_TYPE_* $generatorType
  1347. */
  1348. public function setIdGeneratorType(int $generatorType): void
  1349. {
  1350. $this->generatorType = $generatorType;
  1351. }
  1352. /**
  1353. * Checks whether the mapped class uses an Id generator.
  1354. */
  1355. public function usesIdGenerator(): bool
  1356. {
  1357. return $this->generatorType !== self::GENERATOR_TYPE_NONE;
  1358. }
  1359. public function isInheritanceTypeNone(): bool
  1360. {
  1361. return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
  1362. }
  1363. /**
  1364. * Checks whether the mapped class uses the JOINED inheritance mapping strategy.
  1365. *
  1366. * @return bool TRUE if the class participates in a JOINED inheritance mapping,
  1367. * FALSE otherwise.
  1368. */
  1369. public function isInheritanceTypeJoined(): bool
  1370. {
  1371. return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED;
  1372. }
  1373. /**
  1374. * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy.
  1375. *
  1376. * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping,
  1377. * FALSE otherwise.
  1378. */
  1379. public function isInheritanceTypeSingleTable(): bool
  1380. {
  1381. return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE;
  1382. }
  1383. /**
  1384. * Checks whether the class uses an identity column for the Id generation.
  1385. */
  1386. public function isIdGeneratorIdentity(): bool
  1387. {
  1388. return $this->generatorType === self::GENERATOR_TYPE_IDENTITY;
  1389. }
  1390. /**
  1391. * Checks whether the class uses a sequence for id generation.
  1392. *
  1393. * @phpstan-assert-if-true !null $this->sequenceGeneratorDefinition
  1394. */
  1395. public function isIdGeneratorSequence(): bool
  1396. {
  1397. return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE;
  1398. }
  1399. /**
  1400. * Checks whether the class has a natural identifier/pk (which means it does
  1401. * not use any Id generator.
  1402. */
  1403. public function isIdentifierNatural(): bool
  1404. {
  1405. return $this->generatorType === self::GENERATOR_TYPE_NONE;
  1406. }
  1407. /**
  1408. * Gets the type of a field.
  1409. *
  1410. * @todo 3.0 Remove this. PersisterHelper should fix it somehow
  1411. */
  1412. public function getTypeOfField(string $fieldName): string|null
  1413. {
  1414. return isset($this->fieldMappings[$fieldName])
  1415. ? $this->fieldMappings[$fieldName]->type
  1416. : null;
  1417. }
  1418. /**
  1419. * Gets the name of the primary table.
  1420. */
  1421. public function getTableName(): string
  1422. {
  1423. return $this->table['name'];
  1424. }
  1425. /**
  1426. * Gets primary table's schema name.
  1427. */
  1428. public function getSchemaName(): string|null
  1429. {
  1430. return $this->table['schema'] ?? null;
  1431. }
  1432. /**
  1433. * Gets the table name to use for temporary identifier tables of this class.
  1434. */
  1435. public function getTemporaryIdTableName(): string
  1436. {
  1437. // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
  1438. return str_replace('.', '_', $this->getTableName() . '_id_tmp');
  1439. }
  1440. /**
  1441. * Sets the mapped subclasses of this class.
  1442. *
  1443. * @phpstan-param list<string> $subclasses The names of all mapped subclasses.
  1444. */
  1445. public function setSubclasses(array $subclasses): void
  1446. {
  1447. foreach ($subclasses as $subclass) {
  1448. $this->subClasses[] = $this->fullyQualifiedClassName($subclass);
  1449. }
  1450. }
  1451. /**
  1452. * Sets the parent class names. Only <em>entity</em> classes may be given.
  1453. *
  1454. * Assumes that the class names in the passed array are in the order:
  1455. * directParent -> directParentParent -> directParentParentParent ... -> root.
  1456. *
  1457. * @phpstan-param list<class-string> $classNames
  1458. */
  1459. public function setParentClasses(array $classNames): void
  1460. {
  1461. $this->parentClasses = $classNames;
  1462. if (count($classNames) > 0) {
  1463. $this->rootEntityName = array_pop($classNames);
  1464. }
  1465. }
  1466. /**
  1467. * Sets the inheritance type used by the class and its subclasses.
  1468. *
  1469. * @phpstan-param self::INHERITANCE_TYPE_* $type
  1470. *
  1471. * @throws MappingException
  1472. */
  1473. public function setInheritanceType(int $type): void
  1474. {
  1475. if (! $this->isInheritanceType($type)) {
  1476. throw MappingException::invalidInheritanceType($this->name, $type);
  1477. }
  1478. $this->inheritanceType = $type;
  1479. }
  1480. /**
  1481. * Sets the association to override association mapping of property for an entity relationship.
  1482. *
  1483. * @phpstan-param array{joinColumns?: array, inversedBy?: ?string, joinTable?: array, fetch?: ?string, cascade?: string[]} $overrideMapping
  1484. *
  1485. * @throws MappingException
  1486. */
  1487. public function setAssociationOverride(string $fieldName, array $overrideMapping): void
  1488. {
  1489. if (! isset($this->associationMappings[$fieldName])) {
  1490. throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
  1491. }
  1492. $mapping = $this->associationMappings[$fieldName]->toArray();
  1493. if (isset($mapping['inherited'])) {
  1494. throw MappingException::illegalOverrideOfInheritedProperty(
  1495. $this->name,
  1496. $fieldName,
  1497. $mapping['inherited'],
  1498. );
  1499. }
  1500. if (isset($overrideMapping['joinColumns'])) {
  1501. $mapping['joinColumns'] = $overrideMapping['joinColumns'];
  1502. }
  1503. if (isset($overrideMapping['inversedBy'])) {
  1504. $mapping['inversedBy'] = $overrideMapping['inversedBy'];
  1505. }
  1506. if (isset($overrideMapping['joinTable'])) {
  1507. $mapping['joinTable'] = $overrideMapping['joinTable'];
  1508. }
  1509. if (isset($overrideMapping['fetch'])) {
  1510. $mapping['fetch'] = $overrideMapping['fetch'];
  1511. }
  1512. if (isset($overrideMapping['cascade'])) {
  1513. $mapping['cascade'] = $overrideMapping['cascade'];
  1514. }
  1515. switch ($mapping['type']) {
  1516. case self::ONE_TO_ONE:
  1517. case self::MANY_TO_ONE:
  1518. $mapping['joinColumnFieldNames'] = [];
  1519. $mapping['sourceToTargetKeyColumns'] = [];
  1520. break;
  1521. case self::MANY_TO_MANY:
  1522. $mapping['relationToSourceKeyColumns'] = [];
  1523. $mapping['relationToTargetKeyColumns'] = [];
  1524. break;
  1525. }
  1526. $this->associationMappings[$fieldName] = $this->_validateAndCompleteAssociationMapping($mapping);
  1527. }
  1528. /**
  1529. * Sets the override for a mapped field.
  1530. *
  1531. * @phpstan-param array<string, mixed> $overrideMapping
  1532. *
  1533. * @throws MappingException
  1534. */
  1535. public function setAttributeOverride(string $fieldName, array $overrideMapping): void
  1536. {
  1537. if (! isset($this->fieldMappings[$fieldName])) {
  1538. throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
  1539. }
  1540. $mapping = $this->fieldMappings[$fieldName];
  1541. if (isset($mapping->inherited)) {
  1542. throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName, $mapping->inherited);
  1543. }
  1544. if (isset($mapping->id)) {
  1545. $overrideMapping['id'] = $mapping->id;
  1546. }
  1547. if (isset($mapping->declared)) {
  1548. $overrideMapping['declared'] = $mapping->declared;
  1549. }
  1550. if (! isset($overrideMapping['type'])) {
  1551. $overrideMapping['type'] = $mapping->type;
  1552. }
  1553. if (! isset($overrideMapping['fieldName'])) {
  1554. $overrideMapping['fieldName'] = $mapping->fieldName;
  1555. }
  1556. if ($overrideMapping['type'] !== $mapping->type) {
  1557. throw MappingException::invalidOverrideFieldType($this->name, $fieldName);
  1558. }
  1559. unset($this->fieldMappings[$fieldName]);
  1560. unset($this->fieldNames[$mapping->columnName]);
  1561. // @phpstan-ignore property.deprecated
  1562. unset($this->columnNames[$mapping->fieldName]);
  1563. $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
  1564. $this->fieldMappings[$fieldName] = $overrideMapping;
  1565. }
  1566. /**
  1567. * Checks whether a mapped field is inherited from an entity superclass.
  1568. */
  1569. public function isInheritedField(string $fieldName): bool
  1570. {
  1571. return isset($this->fieldMappings[$fieldName]->inherited);
  1572. }
  1573. /**
  1574. * Checks if this entity is the root in any entity-inheritance-hierarchy.
  1575. */
  1576. public function isRootEntity(): bool
  1577. {
  1578. return $this->name === $this->rootEntityName;
  1579. }
  1580. /**
  1581. * Checks whether a mapped association field is inherited from a superclass.
  1582. */
  1583. public function isInheritedAssociation(string $fieldName): bool
  1584. {
  1585. return isset($this->associationMappings[$fieldName]->inherited);
  1586. }
  1587. public function isInheritedEmbeddedClass(string $fieldName): bool
  1588. {
  1589. return isset($this->embeddedClasses[$fieldName]->inherited);
  1590. }
  1591. /**
  1592. * Sets the name of the primary table the class is mapped to.
  1593. *
  1594. * @deprecated Use {@link setPrimaryTable}.
  1595. */
  1596. public function setTableName(string $tableName): void
  1597. {
  1598. $this->table['name'] = $tableName;
  1599. }
  1600. /**
  1601. * Sets the primary table definition. The provided array supports the
  1602. * following structure:
  1603. *
  1604. * name => <tableName> (optional, defaults to class name)
  1605. * indexes => array of indexes (optional)
  1606. * uniqueConstraints => array of constraints (optional)
  1607. *
  1608. * If a key is omitted, the current value is kept.
  1609. *
  1610. * @phpstan-param array<string, mixed> $table The table description.
  1611. */
  1612. public function setPrimaryTable(array $table): void
  1613. {
  1614. if (isset($table['name'])) {
  1615. // Split schema and table name from a table name like "myschema.mytable"
  1616. if (str_contains($table['name'], '.')) {
  1617. [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2);
  1618. }
  1619. if ($table['name'][0] === '`') {
  1620. $table['name'] = trim($table['name'], '`');
  1621. $this->table['quoted'] = true;
  1622. }
  1623. $this->table['name'] = $table['name'];
  1624. }
  1625. if (isset($table['quoted'])) {
  1626. $this->table['quoted'] = $table['quoted'];
  1627. }
  1628. if (isset($table['schema'])) {
  1629. $this->table['schema'] = $table['schema'];
  1630. }
  1631. if (isset($table['indexes'])) {
  1632. $this->table['indexes'] = $table['indexes'];
  1633. }
  1634. if (isset($table['uniqueConstraints'])) {
  1635. $this->table['uniqueConstraints'] = $table['uniqueConstraints'];
  1636. }
  1637. if (isset($table['options'])) {
  1638. $this->table['options'] = $table['options'];
  1639. }
  1640. }
  1641. /**
  1642. * Checks whether the given type identifies an inheritance type.
  1643. */
  1644. private function isInheritanceType(int $type): bool
  1645. {
  1646. return $type === self::INHERITANCE_TYPE_NONE ||
  1647. $type === self::INHERITANCE_TYPE_SINGLE_TABLE ||
  1648. $type === self::INHERITANCE_TYPE_JOINED;
  1649. }
  1650. /**
  1651. * Adds a mapped field to the class.
  1652. *
  1653. * @phpstan-param array<string, mixed> $mapping The field mapping.
  1654. *
  1655. * @throws MappingException
  1656. */
  1657. public function mapField(array $mapping): void
  1658. {
  1659. $mapping = $this->validateAndCompleteFieldMapping($mapping);
  1660. $this->assertFieldNotMapped($mapping->fieldName);
  1661. if (isset($mapping->generated)) {
  1662. $this->requiresFetchAfterChange = true;
  1663. }
  1664. $this->fieldMappings[$mapping->fieldName] = $mapping;
  1665. }
  1666. /**
  1667. * INTERNAL:
  1668. * Adds an association mapping without completing/validating it.
  1669. * This is mainly used to add inherited association mappings to derived classes.
  1670. *
  1671. * @param ConcreteAssociationMapping $mapping
  1672. *
  1673. * @throws MappingException
  1674. */
  1675. public function addInheritedAssociationMapping(AssociationMapping $mapping/*, $owningClassName = null*/): void
  1676. {
  1677. if (isset($this->associationMappings[$mapping->fieldName])) {
  1678. throw MappingException::duplicateAssociationMapping($this->name, $mapping->fieldName);
  1679. }
  1680. $this->associationMappings[$mapping->fieldName] = $mapping;
  1681. }
  1682. /**
  1683. * INTERNAL:
  1684. * Adds a field mapping without completing/validating it.
  1685. * This is mainly used to add inherited field mappings to derived classes.
  1686. */
  1687. public function addInheritedFieldMapping(FieldMapping $fieldMapping): void
  1688. {
  1689. $this->fieldMappings[$fieldMapping->fieldName] = $fieldMapping;
  1690. // @phpstan-ignore property.deprecated
  1691. $this->columnNames[$fieldMapping->fieldName] = $fieldMapping->columnName;
  1692. $this->fieldNames[$fieldMapping->columnName] = $fieldMapping->fieldName;
  1693. if (isset($fieldMapping->generated)) {
  1694. $this->requiresFetchAfterChange = true;
  1695. }
  1696. }
  1697. /**
  1698. * Adds a one-to-one mapping.
  1699. *
  1700. * @param array<string, mixed> $mapping The mapping.
  1701. */
  1702. public function mapOneToOne(array $mapping): void
  1703. {
  1704. $mapping['type'] = self::ONE_TO_ONE;
  1705. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1706. $this->_storeAssociationMapping($mapping);
  1707. }
  1708. /**
  1709. * Adds a one-to-many mapping.
  1710. *
  1711. * @phpstan-param array<string, mixed> $mapping The mapping.
  1712. */
  1713. public function mapOneToMany(array $mapping): void
  1714. {
  1715. $mapping['type'] = self::ONE_TO_MANY;
  1716. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1717. $this->_storeAssociationMapping($mapping);
  1718. }
  1719. /**
  1720. * Adds a many-to-one mapping.
  1721. *
  1722. * @phpstan-param array<string, mixed> $mapping The mapping.
  1723. */
  1724. public function mapManyToOne(array $mapping): void
  1725. {
  1726. $mapping['type'] = self::MANY_TO_ONE;
  1727. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1728. $this->_storeAssociationMapping($mapping);
  1729. }
  1730. /**
  1731. * Adds a many-to-many mapping.
  1732. *
  1733. * @phpstan-param array<string, mixed> $mapping The mapping.
  1734. */
  1735. public function mapManyToMany(array $mapping): void
  1736. {
  1737. $mapping['type'] = self::MANY_TO_MANY;
  1738. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1739. $this->_storeAssociationMapping($mapping);
  1740. }
  1741. /**
  1742. * Stores the association mapping.
  1743. *
  1744. * @param ConcreteAssociationMapping $assocMapping
  1745. *
  1746. * @throws MappingException
  1747. */
  1748. protected function _storeAssociationMapping(AssociationMapping $assocMapping): void
  1749. {
  1750. $sourceFieldName = $assocMapping->fieldName;
  1751. $this->assertFieldNotMapped($sourceFieldName);
  1752. $this->associationMappings[$sourceFieldName] = $assocMapping;
  1753. }
  1754. /**
  1755. * Registers a custom repository class for the entity class.
  1756. *
  1757. * @param string|null $repositoryClassName The class name of the custom mapper.
  1758. * @phpstan-param class-string<EntityRepository>|null $repositoryClassName
  1759. */
  1760. public function setCustomRepositoryClass(string|null $repositoryClassName): void
  1761. {
  1762. if ($repositoryClassName === null) {
  1763. $this->customRepositoryClassName = null;
  1764. return;
  1765. }
  1766. $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
  1767. }
  1768. /**
  1769. * Dispatches the lifecycle event of the given entity to the registered
  1770. * lifecycle callbacks and lifecycle listeners.
  1771. *
  1772. * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker
  1773. *
  1774. * @param string $lifecycleEvent The lifecycle event.
  1775. */
  1776. public function invokeLifecycleCallbacks(string $lifecycleEvent, object $entity): void
  1777. {
  1778. foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) {
  1779. $entity->$callback();
  1780. }
  1781. }
  1782. /**
  1783. * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
  1784. */
  1785. public function hasLifecycleCallbacks(string $lifecycleEvent): bool
  1786. {
  1787. return isset($this->lifecycleCallbacks[$lifecycleEvent]);
  1788. }
  1789. /**
  1790. * Gets the registered lifecycle callbacks for an event.
  1791. *
  1792. * @return string[]
  1793. * @phpstan-return list<string>
  1794. */
  1795. public function getLifecycleCallbacks(string $event): array
  1796. {
  1797. return $this->lifecycleCallbacks[$event] ?? [];
  1798. }
  1799. /**
  1800. * Adds a lifecycle callback for entities of this class.
  1801. */
  1802. public function addLifecycleCallback(string $callback, string $event): void
  1803. {
  1804. if ($this->isEmbeddedClass) {
  1805. throw MappingException::illegalLifecycleCallbackOnEmbeddedClass($callback, $this->name);
  1806. }
  1807. if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
  1808. return;
  1809. }
  1810. $this->lifecycleCallbacks[$event][] = $callback;
  1811. }
  1812. /**
  1813. * Sets the lifecycle callbacks for entities of this class.
  1814. * Any previously registered callbacks are overwritten.
  1815. *
  1816. * @phpstan-param array<string, list<string>> $callbacks
  1817. */
  1818. public function setLifecycleCallbacks(array $callbacks): void
  1819. {
  1820. $this->lifecycleCallbacks = $callbacks;
  1821. }
  1822. /**
  1823. * Adds a entity listener for entities of this class.
  1824. *
  1825. * @param string $eventName The entity lifecycle event.
  1826. * @param string $class The listener class.
  1827. * @param string $method The listener callback method.
  1828. *
  1829. * @throws MappingException
  1830. */
  1831. public function addEntityListener(string $eventName, string $class, string $method): void
  1832. {
  1833. $class = $this->fullyQualifiedClassName($class);
  1834. $listener = [
  1835. 'class' => $class,
  1836. 'method' => $method,
  1837. ];
  1838. if (! class_exists($class)) {
  1839. throw MappingException::entityListenerClassNotFound($class, $this->name);
  1840. }
  1841. if (! method_exists($class, $method)) {
  1842. throw MappingException::entityListenerMethodNotFound($class, $method, $this->name);
  1843. }
  1844. if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
  1845. throw MappingException::duplicateEntityListener($class, $method, $this->name);
  1846. }
  1847. $this->entityListeners[$eventName][] = $listener;
  1848. }
  1849. /**
  1850. * Sets the discriminator column definition.
  1851. *
  1852. * @see getDiscriminatorColumn()
  1853. *
  1854. * @param DiscriminatorColumnMapping|mixed[]|null $columnDef
  1855. * @phpstan-param DiscriminatorColumnMapping|array{
  1856. * name: string|null,
  1857. * fieldName?: string|null,
  1858. * type?: string|null,
  1859. * length?: int|null,
  1860. * columnDefinition?: string|null,
  1861. * enumType?: class-string<BackedEnum>|null,
  1862. * options?: array<string, mixed>|null
  1863. * }|null $columnDef
  1864. *
  1865. * @throws MappingException
  1866. */
  1867. public function setDiscriminatorColumn(DiscriminatorColumnMapping|array|null $columnDef): void
  1868. {
  1869. if ($columnDef instanceof DiscriminatorColumnMapping) {
  1870. $this->discriminatorColumn = $columnDef;
  1871. return;
  1872. }
  1873. if ($columnDef !== null) {
  1874. if (! isset($columnDef['name'])) {
  1875. throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name);
  1876. }
  1877. if (isset($this->fieldNames[$columnDef['name']])) {
  1878. throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
  1879. }
  1880. $columnDef['fieldName'] ??= $columnDef['name'];
  1881. $columnDef['type'] ??= 'string';
  1882. $columnDef['options'] ??= [];
  1883. if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
  1884. throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
  1885. }
  1886. $this->discriminatorColumn = DiscriminatorColumnMapping::fromMappingArray($columnDef);
  1887. }
  1888. }
  1889. final public function getDiscriminatorColumn(): DiscriminatorColumnMapping
  1890. {
  1891. if ($this->discriminatorColumn === null) {
  1892. throw new LogicException('The discriminator column was not set.');
  1893. }
  1894. return $this->discriminatorColumn;
  1895. }
  1896. /**
  1897. * Sets the discriminator values used by this class.
  1898. * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
  1899. *
  1900. * @param array<int|string, string> $map
  1901. */
  1902. public function setDiscriminatorMap(array $map): void
  1903. {
  1904. if (count(array_flip($map)) !== count($map)) {
  1905. Deprecation::trigger(
  1906. 'doctrine/orm',
  1907. 'https://github.com/doctrine/orm/issues/3519',
  1908. <<<'DEPRECATION'
  1909. Mapping a class to multiple discriminator values is deprecated,
  1910. and the discriminator mapping of %s contains duplicate values
  1911. for the following discriminator values: %s.
  1912. DEPRECATION,
  1913. $this->name,
  1914. implode(', ', array_keys(array_filter(array_count_values($map), static function (int $value): bool {
  1915. return $value > 1;
  1916. }))),
  1917. );
  1918. }
  1919. foreach ($map as $value => $className) {
  1920. $this->addDiscriminatorMapClass($value, $className);
  1921. }
  1922. }
  1923. /**
  1924. * Adds one entry of the discriminator map with a new class and corresponding name.
  1925. *
  1926. * @throws MappingException
  1927. */
  1928. public function addDiscriminatorMapClass(int|string $name, string $className): void
  1929. {
  1930. $className = $this->fullyQualifiedClassName($className);
  1931. $className = ltrim($className, '\\');
  1932. $this->discriminatorMap[$name] = $className;
  1933. if ($this->name === $className) {
  1934. $this->discriminatorValue = $name;
  1935. return;
  1936. }
  1937. if (! (class_exists($className) || interface_exists($className))) {
  1938. throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
  1939. }
  1940. $this->addSubClass($className);
  1941. }
  1942. /** @param array<class-string> $classes */
  1943. public function addSubClasses(array $classes): void
  1944. {
  1945. foreach ($classes as $className) {
  1946. $this->addSubClass($className);
  1947. }
  1948. }
  1949. public function addSubClass(string $className): void
  1950. {
  1951. // By ignoring classes that are not subclasses of the current class, we simplify inheriting
  1952. // the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata.
  1953. if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
  1954. $this->subClasses[] = $className;
  1955. }
  1956. }
  1957. public function hasAssociation(string $fieldName): bool
  1958. {
  1959. return isset($this->associationMappings[$fieldName]);
  1960. }
  1961. public function isSingleValuedAssociation(string $fieldName): bool
  1962. {
  1963. return isset($this->associationMappings[$fieldName])
  1964. && ($this->associationMappings[$fieldName]->isToOne());
  1965. }
  1966. public function isCollectionValuedAssociation(string $fieldName): bool
  1967. {
  1968. return isset($this->associationMappings[$fieldName])
  1969. && ! $this->associationMappings[$fieldName]->isToOne();
  1970. }
  1971. /**
  1972. * Is this an association that only has a single join column?
  1973. */
  1974. public function isAssociationWithSingleJoinColumn(string $fieldName): bool
  1975. {
  1976. return isset($this->associationMappings[$fieldName])
  1977. && isset($this->associationMappings[$fieldName]->joinColumns[0])
  1978. && ! isset($this->associationMappings[$fieldName]->joinColumns[1]);
  1979. }
  1980. /**
  1981. * Returns the single association join column (if any).
  1982. *
  1983. * @throws MappingException
  1984. */
  1985. public function getSingleAssociationJoinColumnName(string $fieldName): string
  1986. {
  1987. if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
  1988. throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
  1989. }
  1990. $assoc = $this->associationMappings[$fieldName];
  1991. assert($assoc->isToOneOwningSide());
  1992. return $assoc->joinColumns[0]->name;
  1993. }
  1994. /**
  1995. * Returns the single association referenced join column name (if any).
  1996. *
  1997. * @throws MappingException
  1998. */
  1999. public function getSingleAssociationReferencedJoinColumnName(string $fieldName): string
  2000. {
  2001. if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
  2002. throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
  2003. }
  2004. $assoc = $this->associationMappings[$fieldName];
  2005. assert($assoc->isToOneOwningSide());
  2006. return $assoc->joinColumns[0]->referencedColumnName;
  2007. }
  2008. /**
  2009. * Used to retrieve a fieldname for either field or association from a given column.
  2010. *
  2011. * This method is used in foreign-key as primary-key contexts.
  2012. *
  2013. * @throws MappingException
  2014. */
  2015. public function getFieldForColumn(string $columnName): string
  2016. {
  2017. if (isset($this->fieldNames[$columnName])) {
  2018. return $this->fieldNames[$columnName];
  2019. }
  2020. foreach ($this->associationMappings as $assocName => $mapping) {
  2021. if (
  2022. $this->isAssociationWithSingleJoinColumn($assocName) &&
  2023. assert($this->associationMappings[$assocName]->isToOneOwningSide()) &&
  2024. $this->associationMappings[$assocName]->joinColumns[0]->name === $columnName
  2025. ) {
  2026. return $assocName;
  2027. }
  2028. }
  2029. throw MappingException::noFieldNameFoundForColumn($this->name, $columnName);
  2030. }
  2031. /**
  2032. * Sets the ID generator used to generate IDs for instances of this class.
  2033. */
  2034. public function setIdGenerator(AbstractIdGenerator $generator): void
  2035. {
  2036. $this->idGenerator = $generator;
  2037. }
  2038. /**
  2039. * Sets definition.
  2040. *
  2041. * @phpstan-param array<string, string|null> $definition
  2042. */
  2043. public function setCustomGeneratorDefinition(array $definition): void
  2044. {
  2045. $this->customGeneratorDefinition = $definition;
  2046. }
  2047. /**
  2048. * Sets the definition of the sequence ID generator for this class.
  2049. *
  2050. * The definition must have the following structure:
  2051. * <code>
  2052. * array(
  2053. * 'sequenceName' => 'name',
  2054. * 'allocationSize' => 20,
  2055. * 'initialValue' => 1
  2056. * 'quoted' => 1
  2057. * )
  2058. * </code>
  2059. *
  2060. * @phpstan-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
  2061. *
  2062. * @throws MappingException
  2063. */
  2064. public function setSequenceGeneratorDefinition(array $definition): void
  2065. {
  2066. if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') {
  2067. throw MappingException::missingSequenceName($this->name);
  2068. }
  2069. if ($definition['sequenceName'][0] === '`') {
  2070. $definition['sequenceName'] = trim($definition['sequenceName'], '`');
  2071. $definition['quoted'] = true;
  2072. }
  2073. if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') {
  2074. $definition['allocationSize'] = '1';
  2075. }
  2076. if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') {
  2077. $definition['initialValue'] = '1';
  2078. }
  2079. $definition['allocationSize'] = (string) $definition['allocationSize'];
  2080. $definition['initialValue'] = (string) $definition['initialValue'];
  2081. $this->sequenceGeneratorDefinition = $definition;
  2082. }
  2083. /**
  2084. * Sets the version field mapping used for versioning. Sets the default
  2085. * value to use depending on the column type.
  2086. *
  2087. * @phpstan-param array<string, mixed> $mapping The version field mapping array.
  2088. *
  2089. * @throws MappingException
  2090. */
  2091. public function setVersionMapping(array &$mapping): void
  2092. {
  2093. $this->isVersioned = true;
  2094. $this->versionField = $mapping['fieldName'];
  2095. $this->requiresFetchAfterChange = true;
  2096. if (! isset($mapping['default'])) {
  2097. if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
  2098. $mapping['default'] = 1;
  2099. } elseif ($mapping['type'] === 'datetime') {
  2100. $mapping['default'] = 'CURRENT_TIMESTAMP';
  2101. } else {
  2102. throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
  2103. }
  2104. }
  2105. }
  2106. /**
  2107. * Sets whether this class is to be versioned for optimistic locking.
  2108. */
  2109. public function setVersioned(bool $bool): void
  2110. {
  2111. $this->isVersioned = $bool;
  2112. if ($bool) {
  2113. $this->requiresFetchAfterChange = true;
  2114. }
  2115. }
  2116. /**
  2117. * Sets the name of the field that is to be used for versioning if this class is
  2118. * versioned for optimistic locking.
  2119. */
  2120. public function setVersionField(string|null $versionField): void
  2121. {
  2122. $this->versionField = $versionField;
  2123. }
  2124. /**
  2125. * Marks this class as read only, no change tracking is applied to it.
  2126. */
  2127. public function markReadOnly(): void
  2128. {
  2129. $this->isReadOnly = true;
  2130. }
  2131. /**
  2132. * {@inheritDoc}
  2133. */
  2134. public function getFieldNames(): array
  2135. {
  2136. return array_keys($this->fieldMappings);
  2137. }
  2138. /**
  2139. * {@inheritDoc}
  2140. */
  2141. public function getAssociationNames(): array
  2142. {
  2143. return array_keys($this->associationMappings);
  2144. }
  2145. /**
  2146. * {@inheritDoc}
  2147. *
  2148. * @phpstan-return class-string
  2149. *
  2150. * @throws InvalidArgumentException
  2151. */
  2152. public function getAssociationTargetClass(string $assocName): string
  2153. {
  2154. return $this->associationMappings[$assocName]->targetEntity
  2155. ?? throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
  2156. }
  2157. public function getName(): string
  2158. {
  2159. return $this->name;
  2160. }
  2161. public function isAssociationInverseSide(string $assocName): bool
  2162. {
  2163. return isset($this->associationMappings[$assocName])
  2164. && ! $this->associationMappings[$assocName]->isOwningSide();
  2165. }
  2166. public function getAssociationMappedByTargetField(string $assocName): string
  2167. {
  2168. $assoc = $this->getAssociationMapping($assocName);
  2169. if (! $assoc instanceof InverseSideMapping) {
  2170. throw new LogicException(sprintf(
  2171. <<<'EXCEPTION'
  2172. Context: Calling %s() with "%s", which is the owning side of an association.
  2173. Problem: The owning side of an association has no "mappedBy" field.
  2174. Solution: Call %s::isAssociationInverseSide() to check first.
  2175. EXCEPTION,
  2176. __METHOD__,
  2177. $assocName,
  2178. self::class,
  2179. ));
  2180. }
  2181. return $assoc->mappedBy;
  2182. }
  2183. /**
  2184. * @param C $className
  2185. *
  2186. * @return string|null null if and only if the input value is null
  2187. * @phpstan-return (C is class-string ? class-string : (C is string ? string : null))
  2188. *
  2189. * @template C of string|null
  2190. */
  2191. public function fullyQualifiedClassName(string|null $className): string|null
  2192. {
  2193. if ($className === null) {
  2194. Deprecation::trigger(
  2195. 'doctrine/orm',
  2196. 'https://github.com/doctrine/orm/pull/11294',
  2197. 'Passing null to %s is deprecated and will not be supported in Doctrine ORM 4.0',
  2198. __METHOD__,
  2199. );
  2200. return null;
  2201. }
  2202. if (! str_contains($className, '\\') && $this->namespace) {
  2203. return $this->namespace . '\\' . $className;
  2204. }
  2205. return $className;
  2206. }
  2207. public function getMetadataValue(string $name): mixed
  2208. {
  2209. return $this->$name ?? null;
  2210. }
  2211. /**
  2212. * Map Embedded Class
  2213. *
  2214. * @phpstan-param array{
  2215. * fieldName: string,
  2216. * class?: class-string,
  2217. * declaredField?: string,
  2218. * columnPrefix?: string|false|null,
  2219. * originalField?: string
  2220. * } $mapping
  2221. *
  2222. * @throws MappingException
  2223. */
  2224. public function mapEmbedded(array $mapping): void
  2225. {
  2226. $this->assertFieldNotMapped($mapping['fieldName']);
  2227. if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) {
  2228. $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
  2229. if ($type instanceof ReflectionNamedType) {
  2230. $mapping['class'] = $type->getName();
  2231. }
  2232. }
  2233. if (! (isset($mapping['class']) && $mapping['class'])) {
  2234. throw MappingException::missingEmbeddedClass($mapping['fieldName']);
  2235. }
  2236. $this->embeddedClasses[$mapping['fieldName']] = EmbeddedClassMapping::fromMappingArray([
  2237. 'class' => $this->fullyQualifiedClassName($mapping['class']),
  2238. 'columnPrefix' => $mapping['columnPrefix'] ?? null,
  2239. 'declaredField' => $mapping['declaredField'] ?? null,
  2240. 'originalField' => $mapping['originalField'] ?? null,
  2241. ]);
  2242. }
  2243. /**
  2244. * Inline the embeddable class
  2245. */
  2246. public function inlineEmbeddable(string $property, ClassMetadata $embeddable): void
  2247. {
  2248. foreach ($embeddable->fieldMappings as $originalFieldMapping) {
  2249. $fieldMapping = (array) $originalFieldMapping;
  2250. $fieldMapping['originalClass'] ??= $embeddable->name;
  2251. $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
  2252. ? $property . '.' . $fieldMapping['declaredField']
  2253. : $property;
  2254. $fieldMapping['originalField'] ??= $fieldMapping['fieldName'];
  2255. $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName'];
  2256. if (! empty($this->embeddedClasses[$property]->columnPrefix)) {
  2257. $fieldMapping['columnName'] = $this->embeddedClasses[$property]->columnPrefix . $fieldMapping['columnName'];
  2258. } elseif ($this->embeddedClasses[$property]->columnPrefix !== false) {
  2259. assert($this->reflClass !== null);
  2260. assert($embeddable->reflClass !== null);
  2261. $fieldMapping['columnName'] = $this->namingStrategy
  2262. ->embeddedFieldToColumnName(
  2263. $property,
  2264. $fieldMapping['columnName'],
  2265. $this->reflClass->name,
  2266. $embeddable->reflClass->name,
  2267. );
  2268. }
  2269. $this->mapField($fieldMapping);
  2270. }
  2271. }
  2272. /** @throws MappingException */
  2273. private function assertFieldNotMapped(string $fieldName): void
  2274. {
  2275. if (
  2276. isset($this->fieldMappings[$fieldName]) ||
  2277. isset($this->associationMappings[$fieldName]) ||
  2278. isset($this->embeddedClasses[$fieldName])
  2279. ) {
  2280. throw MappingException::duplicateFieldMapping($this->name, $fieldName);
  2281. }
  2282. }
  2283. /**
  2284. * Gets the sequence name based on class metadata.
  2285. *
  2286. * @todo Sequence names should be computed in DBAL depending on the platform
  2287. */
  2288. public function getSequenceName(AbstractPlatform $platform): string
  2289. {
  2290. $sequencePrefix = $this->getSequencePrefix($platform);
  2291. $columnName = $this->getSingleIdentifierColumnName();
  2292. return $sequencePrefix . '_' . $columnName . '_seq';
  2293. }
  2294. /**
  2295. * Gets the sequence name prefix based on class metadata.
  2296. *
  2297. * @todo Sequence names should be computed in DBAL depending on the platform
  2298. */
  2299. public function getSequencePrefix(AbstractPlatform $platform): string
  2300. {
  2301. $tableName = $this->getTableName();
  2302. $sequencePrefix = $tableName;
  2303. // Prepend the schema name to the table name if there is one
  2304. $schemaName = $this->getSchemaName();
  2305. if ($schemaName) {
  2306. $sequencePrefix = $schemaName . '.' . $tableName;
  2307. }
  2308. return $sequencePrefix;
  2309. }
  2310. }