vendor/doctrine/orm/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php line 1283

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\Type;
  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\Persistence\Mapping\ClassMetadata;
  15. use Doctrine\Persistence\Mapping\ReflectionService;
  16. use InvalidArgumentException;
  17. use LogicException;
  18. use ReflectionClass;
  19. use ReflectionNamedType;
  20. use ReflectionProperty;
  21. use RuntimeException;
  22. use function array_diff;
  23. use function array_flip;
  24. use function array_intersect;
  25. use function array_keys;
  26. use function array_map;
  27. use function array_merge;
  28. use function array_pop;
  29. use function array_values;
  30. use function assert;
  31. use function class_exists;
  32. use function count;
  33. use function enum_exists;
  34. use function explode;
  35. use function gettype;
  36. use function in_array;
  37. use function interface_exists;
  38. use function is_array;
  39. use function is_subclass_of;
  40. use function ltrim;
  41. use function method_exists;
  42. use function spl_object_id;
  43. use function str_contains;
  44. use function str_replace;
  45. use function strtolower;
  46. use function trait_exists;
  47. use function trim;
  48. use const PHP_VERSION_ID;
  49. /**
  50. * A <tt>ClassMetadata</tt> instance holds all the object-relational mapping metadata
  51. * of an entity and its associations.
  52. *
  53. * Once populated, ClassMetadata instances are usually cached in a serialized form.
  54. *
  55. * <b>IMPORTANT NOTE:</b>
  56. *
  57. * The fields of this class are only public for 2 reasons:
  58. * 1) To allow fast READ access.
  59. * 2) To drastically reduce the size of a serialized instance (private/protected members
  60. * get the whole class name, namespace inclusive, prepended to every property in
  61. * the serialized representation).
  62. *
  63. * @template-covariant T of object
  64. * @template-implements ClassMetadata<T>
  65. * @psalm-type FieldMapping = array{
  66. * type: string,
  67. * fieldName: string,
  68. * columnName: string,
  69. * length?: int,
  70. * id?: bool,
  71. * nullable?: bool,
  72. * notInsertable?: bool,
  73. * notUpdatable?: bool,
  74. * generated?: int,
  75. * enumType?: class-string<BackedEnum>,
  76. * columnDefinition?: string,
  77. * precision?: int,
  78. * scale?: int,
  79. * unique?: bool,
  80. * inherited?: class-string,
  81. * originalClass?: class-string,
  82. * originalField?: string,
  83. * quoted?: bool,
  84. * requireSQLConversion?: bool,
  85. * declared?: class-string,
  86. * declaredField?: string,
  87. * options?: array<string, mixed>
  88. * }
  89. * @psalm-type JoinColumnData = array{
  90. * name: string,
  91. * referencedColumnName: string,
  92. * unique?: bool,
  93. * quoted?: bool,
  94. * fieldName?: string,
  95. * onDelete?: string,
  96. * columnDefinition?: string,
  97. * nullable?: bool,
  98. * }
  99. * @psalm-type AssociationMapping = array{
  100. * cache?: array,
  101. * cascade: array<string>,
  102. * declared?: class-string,
  103. * fetch: mixed,
  104. * fieldName: string,
  105. * id?: bool,
  106. * inherited?: class-string,
  107. * indexBy?: string,
  108. * inversedBy: string|null,
  109. * isCascadeRemove: bool,
  110. * isCascadePersist: bool,
  111. * isCascadeRefresh: bool,
  112. * isCascadeMerge: bool,
  113. * isCascadeDetach: bool,
  114. * isOnDeleteCascade?: bool,
  115. * isOwningSide: bool,
  116. * joinColumns?: array<JoinColumnData>,
  117. * joinColumnFieldNames?: array<string, string>,
  118. * joinTable?: array,
  119. * joinTableColumns?: list<mixed>,
  120. * mappedBy: string|null,
  121. * orderBy?: array,
  122. * originalClass?: class-string,
  123. * originalField?: string,
  124. * orphanRemoval?: bool,
  125. * relationToSourceKeyColumns?: array,
  126. * relationToTargetKeyColumns?: array,
  127. * sourceEntity: class-string,
  128. * sourceToTargetKeyColumns?: array<string, string>,
  129. * targetEntity: class-string,
  130. * targetToSourceKeyColumns?: array<string, string>,
  131. * type: int,
  132. * unique?: bool,
  133. * }
  134. * @psalm-type DiscriminatorColumnMapping = array{
  135. * name: string,
  136. * fieldName: string,
  137. * type: string,
  138. * length?: int,
  139. * columnDefinition?: string|null,
  140. * enumType?: class-string<BackedEnum>|null,
  141. * }
  142. */
  143. class ClassMetadataInfo implements ClassMetadata
  144. {
  145. /* The inheritance mapping types */
  146. /**
  147. * NONE means the class does not participate in an inheritance hierarchy
  148. * and therefore does not need an inheritance mapping type.
  149. */
  150. public const INHERITANCE_TYPE_NONE = 1;
  151. /**
  152. * JOINED means the class will be persisted according to the rules of
  153. * <tt>Class Table Inheritance</tt>.
  154. */
  155. public const INHERITANCE_TYPE_JOINED = 2;
  156. /**
  157. * SINGLE_TABLE means the class will be persisted according to the rules of
  158. * <tt>Single Table Inheritance</tt>.
  159. */
  160. public const INHERITANCE_TYPE_SINGLE_TABLE = 3;
  161. /**
  162. * TABLE_PER_CLASS means the class will be persisted according to the rules
  163. * of <tt>Concrete Table Inheritance</tt>.
  164. */
  165. public const INHERITANCE_TYPE_TABLE_PER_CLASS = 4;
  166. /* The Id generator types. */
  167. /**
  168. * AUTO means the generator type will depend on what the used platform prefers.
  169. * Offers full portability.
  170. */
  171. public const GENERATOR_TYPE_AUTO = 1;
  172. /**
  173. * SEQUENCE means a separate sequence object will be used. Platforms that do
  174. * not have native sequence support may emulate it. Full portability is currently
  175. * not guaranteed.
  176. */
  177. public const GENERATOR_TYPE_SEQUENCE = 2;
  178. /**
  179. * TABLE means a separate table is used for id generation.
  180. * Offers full portability (in that it results in an exception being thrown
  181. * no matter the platform).
  182. *
  183. * @deprecated no replacement planned
  184. */
  185. public const GENERATOR_TYPE_TABLE = 3;
  186. /**
  187. * IDENTITY means an identity column is used for id generation. The database
  188. * will fill in the id column on insertion. Platforms that do not support
  189. * native identity columns may emulate them. Full portability is currently
  190. * not guaranteed.
  191. */
  192. public const GENERATOR_TYPE_IDENTITY = 4;
  193. /**
  194. * NONE means the class does not have a generated id. That means the class
  195. * must have a natural, manually assigned id.
  196. */
  197. public const GENERATOR_TYPE_NONE = 5;
  198. /**
  199. * UUID means that a UUID/GUID expression is used for id generation. Full
  200. * portability is currently not guaranteed.
  201. *
  202. * @deprecated use an application-side generator instead
  203. */
  204. public const GENERATOR_TYPE_UUID = 6;
  205. /**
  206. * CUSTOM means that customer will use own ID generator that supposedly work
  207. */
  208. public const GENERATOR_TYPE_CUSTOM = 7;
  209. /**
  210. * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
  211. * by doing a property-by-property comparison with the original data. This will
  212. * be done for all entities that are in MANAGED state at commit-time.
  213. *
  214. * This is the default change tracking policy.
  215. */
  216. public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
  217. /**
  218. * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
  219. * by doing a property-by-property comparison with the original data. This will
  220. * be done only for entities that were explicitly saved (through persist() or a cascade).
  221. */
  222. public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
  223. /**
  224. * NOTIFY means that Doctrine relies on the entities sending out notifications
  225. * when their properties change. Such entity classes must implement
  226. * the <tt>NotifyPropertyChanged</tt> interface.
  227. */
  228. public const CHANGETRACKING_NOTIFY = 3;
  229. /**
  230. * Specifies that an association is to be fetched when it is first accessed.
  231. */
  232. public const FETCH_LAZY = 2;
  233. /**
  234. * Specifies that an association is to be fetched when the owner of the
  235. * association is fetched.
  236. */
  237. public const FETCH_EAGER = 3;
  238. /**
  239. * Specifies that an association is to be fetched lazy (on first access) and that
  240. * commands such as Collection#count, Collection#slice are issued directly against
  241. * the database if the collection is not yet initialized.
  242. */
  243. public const FETCH_EXTRA_LAZY = 4;
  244. /**
  245. * Identifies a one-to-one association.
  246. */
  247. public const ONE_TO_ONE = 1;
  248. /**
  249. * Identifies a many-to-one association.
  250. */
  251. public const MANY_TO_ONE = 2;
  252. /**
  253. * Identifies a one-to-many association.
  254. */
  255. public const ONE_TO_MANY = 4;
  256. /**
  257. * Identifies a many-to-many association.
  258. */
  259. public const MANY_TO_MANY = 8;
  260. /**
  261. * Combined bitmask for to-one (single-valued) associations.
  262. */
  263. public const TO_ONE = 3;
  264. /**
  265. * Combined bitmask for to-many (collection-valued) associations.
  266. */
  267. public const TO_MANY = 12;
  268. /**
  269. * ReadOnly cache can do reads, inserts and deletes, cannot perform updates or employ any locks,
  270. */
  271. public const CACHE_USAGE_READ_ONLY = 1;
  272. /**
  273. * Nonstrict Read Write Cache doesn’t employ any locks but can do inserts, update and deletes.
  274. */
  275. public const CACHE_USAGE_NONSTRICT_READ_WRITE = 2;
  276. /**
  277. * Read Write Attempts to lock the entity before update/delete.
  278. */
  279. public const CACHE_USAGE_READ_WRITE = 3;
  280. /**
  281. * The value of this column is never generated by the database.
  282. */
  283. public const GENERATED_NEVER = 0;
  284. /**
  285. * The value of this column is generated by the database on INSERT, but not on UPDATE.
  286. */
  287. public const GENERATED_INSERT = 1;
  288. /**
  289. * The value of this column is generated by the database on both INSERT and UDPATE statements.
  290. */
  291. public const GENERATED_ALWAYS = 2;
  292. /**
  293. * READ-ONLY: The name of the entity class.
  294. *
  295. * @var string
  296. * @psalm-var class-string<T>
  297. */
  298. public $name;
  299. /**
  300. * READ-ONLY: The namespace the entity class is contained in.
  301. *
  302. * @var string
  303. * @todo Not really needed. Usage could be localized.
  304. */
  305. public $namespace;
  306. /**
  307. * READ-ONLY: The name of the entity class that is at the root of the mapped entity inheritance
  308. * hierarchy. If the entity is not part of a mapped inheritance hierarchy this is the same
  309. * as {@link $name}.
  310. *
  311. * @var string
  312. * @psalm-var class-string
  313. */
  314. public $rootEntityName;
  315. /**
  316. * READ-ONLY: The definition of custom generator. Only used for CUSTOM
  317. * generator type
  318. *
  319. * The definition has the following structure:
  320. * <code>
  321. * array(
  322. * 'class' => 'ClassName',
  323. * )
  324. * </code>
  325. *
  326. * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
  327. * @var array<string, string>|null
  328. */
  329. public $customGeneratorDefinition;
  330. /**
  331. * The name of the custom repository class used for the entity class.
  332. * (Optional).
  333. *
  334. * @var string|null
  335. * @psalm-var ?class-string<EntityRepository>
  336. */
  337. public $customRepositoryClassName;
  338. /**
  339. * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
  340. *
  341. * @var bool
  342. */
  343. public $isMappedSuperclass = false;
  344. /**
  345. * READ-ONLY: Whether this class describes the mapping of an embeddable class.
  346. *
  347. * @var bool
  348. */
  349. public $isEmbeddedClass = false;
  350. /**
  351. * READ-ONLY: The names of the parent classes (ancestors).
  352. *
  353. * @psalm-var list<class-string>
  354. */
  355. public $parentClasses = [];
  356. /**
  357. * READ-ONLY: For classes in inheritance mapping hierarchies, this field contains the names of all
  358. * <em>entity</em> subclasses of this class. These may also be abstract classes.
  359. *
  360. * This list is used, for example, to enumerate all necessary tables in JTI when querying for root
  361. * or subclass entities, or to gather all fields comprised in an entity inheritance tree.
  362. *
  363. * For classes that do not use STI/JTI, this list is empty.
  364. *
  365. * Implementation note:
  366. *
  367. * In PHP, there is no general way to discover all subclasses of a given class at runtime. For that
  368. * reason, the list of classes given in the discriminator map at the root entity is considered
  369. * authoritative. The discriminator map must contain all <em>concrete</em> classes that can
  370. * appear in the particular inheritance hierarchy tree. Since there can be no instances of abstract
  371. * entity classes, users are not required to list such classes with a discriminator value.
  372. *
  373. * The possibly remaining "gaps" for abstract entity classes are filled after the class metadata for the
  374. * root entity has been loaded.
  375. *
  376. * For subclasses of such root entities, the list can be reused/passed downwards, it only needs to
  377. * be filtered accordingly (only keep remaining subclasses)
  378. *
  379. * @psalm-var list<class-string>
  380. */
  381. public $subClasses = [];
  382. /**
  383. * READ-ONLY: The names of all embedded classes based on properties.
  384. *
  385. * The value (definition) array may contain, among others, the following values:
  386. *
  387. * - <b>'inherited'</b> (string, optional)
  388. * This is set when this embedded-class field is inherited by this class from another (inheritance) parent
  389. * <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
  390. * mapping information for this field. (If there are transient classes in the
  391. * class hierarchy, these are ignored, so the class property may in fact come
  392. * from a class further up in the PHP class hierarchy.)
  393. * Fields initially declared in mapped superclasses are
  394. * <em>not</em> considered 'inherited' in the nearest entity subclasses.
  395. *
  396. * - <b>'declared'</b> (string, optional)
  397. * This is set when the embedded-class field does not appear for the first time in this class, but is originally
  398. * declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
  399. * of the topmost non-transient class that contains mapping information for this field.
  400. *
  401. * @psalm-var array<string, mixed[]>
  402. */
  403. public $embeddedClasses = [];
  404. /**
  405. * READ-ONLY: The named queries allowed to be called directly from Repository.
  406. *
  407. * @psalm-var array<string, array<string, mixed>>
  408. */
  409. public $namedQueries = [];
  410. /**
  411. * READ-ONLY: The named native queries allowed to be called directly from Repository.
  412. *
  413. * A native SQL named query definition has the following structure:
  414. * <pre>
  415. * array(
  416. * 'name' => <query name>,
  417. * 'query' => <sql query>,
  418. * 'resultClass' => <class of the result>,
  419. * 'resultSetMapping' => <name of a SqlResultSetMapping>
  420. * )
  421. * </pre>
  422. *
  423. * @psalm-var array<string, array<string, mixed>>
  424. */
  425. public $namedNativeQueries = [];
  426. /**
  427. * READ-ONLY: The mappings of the results of native SQL queries.
  428. *
  429. * A native result mapping definition has the following structure:
  430. * <pre>
  431. * array(
  432. * 'name' => <result name>,
  433. * 'entities' => array(<entity result mapping>),
  434. * 'columns' => array(<column result mapping>)
  435. * )
  436. * </pre>
  437. *
  438. * @psalm-var array<string, array{
  439. * name: string,
  440. * entities: mixed[],
  441. * columns: mixed[]
  442. * }>
  443. */
  444. public $sqlResultSetMappings = [];
  445. /**
  446. * READ-ONLY: The field names of all fields that are part of the identifier/primary key
  447. * of the mapped entity class.
  448. *
  449. * @psalm-var list<string>
  450. */
  451. public $identifier = [];
  452. /**
  453. * READ-ONLY: The inheritance mapping type used by the class.
  454. *
  455. * @var int
  456. * @psalm-var self::INHERITANCE_TYPE_*
  457. */
  458. public $inheritanceType = self::INHERITANCE_TYPE_NONE;
  459. /**
  460. * READ-ONLY: The Id generator type used by the class.
  461. *
  462. * @var int
  463. * @psalm-var self::GENERATOR_TYPE_*
  464. */
  465. public $generatorType = self::GENERATOR_TYPE_NONE;
  466. /**
  467. * READ-ONLY: The field mappings of the class.
  468. * Keys are field names and values are mapping definitions.
  469. *
  470. * The mapping definition array has the following values:
  471. *
  472. * - <b>fieldName</b> (string)
  473. * The name of the field in the Entity.
  474. *
  475. * - <b>type</b> (string)
  476. * The type name of the mapped field. Can be one of Doctrine's mapping types
  477. * or a custom mapping type.
  478. *
  479. * - <b>columnName</b> (string, optional)
  480. * The column name. Optional. Defaults to the field name.
  481. *
  482. * - <b>length</b> (integer, optional)
  483. * The database length of the column. Optional. Default value taken from
  484. * the type.
  485. *
  486. * - <b>id</b> (boolean, optional)
  487. * Marks the field as the primary key of the entity. Multiple fields of an
  488. * entity can have the id attribute, forming a composite key.
  489. *
  490. * - <b>nullable</b> (boolean, optional)
  491. * Whether the column is nullable. Defaults to FALSE.
  492. *
  493. * - <b>'notInsertable'</b> (boolean, optional)
  494. * Whether the column is not insertable. Optional. Is only set if value is TRUE.
  495. *
  496. * - <b>'notUpdatable'</b> (boolean, optional)
  497. * Whether the column is updatable. Optional. Is only set if value is TRUE.
  498. *
  499. * - <b>columnDefinition</b> (string, optional, schema-only)
  500. * The SQL fragment that is used when generating the DDL for the column.
  501. *
  502. * - <b>precision</b> (integer, optional, schema-only)
  503. * The precision of a decimal column. Only valid if the column type is decimal.
  504. *
  505. * - <b>scale</b> (integer, optional, schema-only)
  506. * The scale of a decimal column. Only valid if the column type is decimal.
  507. *
  508. * - <b>'unique'</b> (boolean, optional, schema-only)
  509. * Whether a unique constraint should be generated for the column.
  510. *
  511. * - <b>'inherited'</b> (string, optional)
  512. * This is set when the field is inherited by this class from another (inheritance) parent
  513. * <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
  514. * mapping information for this field. (If there are transient classes in the
  515. * class hierarchy, these are ignored, so the class property may in fact come
  516. * from a class further up in the PHP class hierarchy.)
  517. * Fields initially declared in mapped superclasses are
  518. * <em>not</em> considered 'inherited' in the nearest entity subclasses.
  519. *
  520. * - <b>'declared'</b> (string, optional)
  521. * This is set when the field does not appear for the first time in this class, but is originally
  522. * declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
  523. * of the topmost non-transient class that contains mapping information for this field.
  524. *
  525. * @var mixed[]
  526. * @psalm-var array<string, FieldMapping>
  527. */
  528. public $fieldMappings = [];
  529. /**
  530. * READ-ONLY: An array of field names. Used to look up field names from column names.
  531. * Keys are column names and values are field names.
  532. *
  533. * @psalm-var array<string, string>
  534. */
  535. public $fieldNames = [];
  536. /**
  537. * READ-ONLY: A map of field names to column names. Keys are field names and values column names.
  538. * Used to look up column names from field names.
  539. * This is the reverse lookup map of $_fieldNames.
  540. *
  541. * @deprecated 3.0 Remove this.
  542. *
  543. * @var mixed[]
  544. */
  545. public $columnNames = [];
  546. /**
  547. * READ-ONLY: The discriminator value of this class.
  548. *
  549. * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
  550. * where a discriminator column is used.</b>
  551. *
  552. * @see discriminatorColumn
  553. *
  554. * @var mixed
  555. */
  556. public $discriminatorValue;
  557. /**
  558. * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
  559. *
  560. * <b>This does only apply to the JOINED and SINGLE_TABLE inheritance mapping strategies
  561. * where a discriminator column is used.</b>
  562. *
  563. * @see discriminatorColumn
  564. *
  565. * @var array<int|string, string>
  566. *
  567. * @psalm-var array<int|string, class-string>
  568. */
  569. public $discriminatorMap = [];
  570. /**
  571. * READ-ONLY: The definition of the discriminator column used in JOINED and SINGLE_TABLE
  572. * inheritance mappings.
  573. *
  574. * @var array<string, mixed>
  575. * @psalm-var DiscriminatorColumnMapping|null
  576. */
  577. public $discriminatorColumn;
  578. /**
  579. * READ-ONLY: The primary table definition. The definition is an array with the
  580. * following entries:
  581. *
  582. * name => <tableName>
  583. * schema => <schemaName>
  584. * indexes => array
  585. * uniqueConstraints => array
  586. *
  587. * @var mixed[]
  588. * @psalm-var array{
  589. * name: string,
  590. * schema?: string,
  591. * indexes?: array,
  592. * uniqueConstraints?: array,
  593. * options?: array<string, mixed>,
  594. * quoted?: bool
  595. * }
  596. */
  597. public $table;
  598. /**
  599. * READ-ONLY: The registered lifecycle callbacks for entities of this class.
  600. *
  601. * @psalm-var array<string, list<string>>
  602. */
  603. public $lifecycleCallbacks = [];
  604. /**
  605. * READ-ONLY: The registered entity listeners.
  606. *
  607. * @psalm-var array<string, list<array{class: class-string, method: string}>>
  608. */
  609. public $entityListeners = [];
  610. /**
  611. * READ-ONLY: The association mappings of this class.
  612. *
  613. * The mapping definition array supports the following keys:
  614. *
  615. * - <b>fieldName</b> (string)
  616. * The name of the field in the entity the association is mapped to.
  617. *
  618. * - <b>sourceEntity</b> (string)
  619. * The class name of the source entity. In the case of to-many associations initially
  620. * present in mapped superclasses, the nearest <em>entity</em> subclasses will be
  621. * considered the respective source entities.
  622. *
  623. * - <b>targetEntity</b> (string)
  624. * The class name of the target entity. If it is fully-qualified it is used as is.
  625. * If it is a simple, unqualified class name the namespace is assumed to be the same
  626. * as the namespace of the source entity.
  627. *
  628. * - <b>mappedBy</b> (string, required for bidirectional associations)
  629. * The name of the field that completes the bidirectional association on the owning side.
  630. * This key must be specified on the inverse side of a bidirectional association.
  631. *
  632. * - <b>inversedBy</b> (string, required for bidirectional associations)
  633. * The name of the field that completes the bidirectional association on the inverse side.
  634. * This key must be specified on the owning side of a bidirectional association.
  635. *
  636. * - <b>cascade</b> (array, optional)
  637. * The names of persistence operations to cascade on the association. The set of possible
  638. * values are: "persist", "remove", "detach", "merge", "refresh", "all" (implies all others).
  639. *
  640. * - <b>orderBy</b> (array, one-to-many/many-to-many only)
  641. * A map of field names (of the target entity) to sorting directions (ASC/DESC).
  642. * Example: array('priority' => 'desc')
  643. *
  644. * - <b>fetch</b> (integer, optional)
  645. * The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
  646. * Possible values are: ClassMetadata::FETCH_EAGER, ClassMetadata::FETCH_LAZY.
  647. *
  648. * - <b>joinTable</b> (array, optional, many-to-many only)
  649. * Specification of the join table and its join columns (foreign keys).
  650. * Only valid for many-to-many mappings. Note that one-to-many associations can be mapped
  651. * through a join table by simply mapping the association as many-to-many with a unique
  652. * constraint on the join table.
  653. *
  654. * - <b>indexBy</b> (string, optional, to-many only)
  655. * Specification of a field on target-entity that is used to index the collection by.
  656. * This field HAS to be either the primary key or a unique column. Otherwise the collection
  657. * does not contain all the entities that are actually related.
  658. *
  659. * - <b>'inherited'</b> (string, optional)
  660. * This is set when the association is inherited by this class from another (inheritance) parent
  661. * <em>entity</em> class. The value is the FQCN of the topmost entity class that contains
  662. * this association. (If there are transient classes in the
  663. * class hierarchy, these are ignored, so the class property may in fact come
  664. * from a class further up in the PHP class hierarchy.)
  665. * To-many associations initially declared in mapped superclasses are
  666. * <em>not</em> considered 'inherited' in the nearest entity subclasses.
  667. *
  668. * - <b>'declared'</b> (string, optional)
  669. * This is set when the association does not appear in the current class for the first time, but
  670. * is initially declared in another parent <em>entity or mapped superclass</em>. The value is the FQCN
  671. * of the topmost non-transient class that contains association information for this relationship.
  672. *
  673. * A join table definition has the following structure:
  674. * <pre>
  675. * array(
  676. * 'name' => <join table name>,
  677. * 'joinColumns' => array(<join column mapping from join table to source table>),
  678. * 'inverseJoinColumns' => array(<join column mapping from join table to target table>)
  679. * )
  680. * </pre>
  681. *
  682. * @psalm-var array<string, AssociationMapping>
  683. */
  684. public $associationMappings = [];
  685. /**
  686. * READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
  687. *
  688. * @var bool
  689. */
  690. public $isIdentifierComposite = false;
  691. /**
  692. * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one foreign key association.
  693. *
  694. * This flag is necessary because some code blocks require special treatment of this cases.
  695. *
  696. * @var bool
  697. */
  698. public $containsForeignIdentifier = false;
  699. /**
  700. * READ-ONLY: Flag indicating whether the identifier/primary key contains at least one ENUM type.
  701. *
  702. * This flag is necessary because some code blocks require special treatment of this cases.
  703. *
  704. * @var bool
  705. */
  706. public $containsEnumIdentifier = false;
  707. /**
  708. * READ-ONLY: The ID generator used for generating IDs for this class.
  709. *
  710. * @var AbstractIdGenerator
  711. * @todo Remove!
  712. */
  713. public $idGenerator;
  714. /**
  715. * READ-ONLY: The definition of the sequence generator of this class. Only used for the
  716. * SEQUENCE generation strategy.
  717. *
  718. * The definition has the following structure:
  719. * <code>
  720. * array(
  721. * 'sequenceName' => 'name',
  722. * 'allocationSize' => '20',
  723. * 'initialValue' => '1'
  724. * )
  725. * </code>
  726. *
  727. * @var array<string, mixed>|null
  728. * @psalm-var array{sequenceName: string, allocationSize: string, initialValue: string, quoted?: mixed}|null
  729. * @todo Merge with tableGeneratorDefinition into generic generatorDefinition
  730. */
  731. public $sequenceGeneratorDefinition;
  732. /**
  733. * READ-ONLY: The definition of the table generator of this class. Only used for the
  734. * TABLE generation strategy.
  735. *
  736. * @deprecated
  737. *
  738. * @var array<string, mixed>
  739. */
  740. public $tableGeneratorDefinition;
  741. /**
  742. * READ-ONLY: The policy used for change-tracking on entities of this class.
  743. *
  744. * @var int
  745. */
  746. public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
  747. /**
  748. * READ-ONLY: A Flag indicating whether one or more columns of this class
  749. * have to be reloaded after insert / update operations.
  750. *
  751. * @var bool
  752. */
  753. public $requiresFetchAfterChange = false;
  754. /**
  755. * READ-ONLY: A flag for whether or not instances of this class are to be versioned
  756. * with optimistic locking.
  757. *
  758. * @var bool
  759. */
  760. public $isVersioned = false;
  761. /**
  762. * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
  763. *
  764. * @var string|null
  765. */
  766. public $versionField;
  767. /** @var mixed[]|null */
  768. public $cache;
  769. /**
  770. * The ReflectionClass instance of the mapped class.
  771. *
  772. * @var ReflectionClass|null
  773. */
  774. public $reflClass;
  775. /**
  776. * Is this entity marked as "read-only"?
  777. *
  778. * That means it is never considered for change-tracking in the UnitOfWork. It is a very helpful performance
  779. * optimization for entities that are immutable, either in your domain or through the relation database
  780. * (coming from a view, or a history table for example).
  781. *
  782. * @var bool
  783. */
  784. public $isReadOnly = false;
  785. /**
  786. * NamingStrategy determining the default column and table names.
  787. *
  788. * @var NamingStrategy
  789. */
  790. protected $namingStrategy;
  791. /**
  792. * The ReflectionProperty instances of the mapped class.
  793. *
  794. * @var array<string, ReflectionProperty|null>
  795. */
  796. public $reflFields = [];
  797. /** @var InstantiatorInterface|null */
  798. private $instantiator;
  799. /** @var TypedFieldMapper $typedFieldMapper */
  800. private $typedFieldMapper;
  801. /**
  802. * Initializes a new ClassMetadata instance that will hold the object-relational mapping
  803. * metadata of the class with the given name.
  804. *
  805. * @param string $entityName The name of the entity class the new instance is used for.
  806. * @psalm-param class-string<T> $entityName
  807. */
  808. public function __construct($entityName, ?NamingStrategy $namingStrategy = null, ?TypedFieldMapper $typedFieldMapper = null)
  809. {
  810. $this->name = $entityName;
  811. $this->rootEntityName = $entityName;
  812. $this->namingStrategy = $namingStrategy ?? new DefaultNamingStrategy();
  813. $this->instantiator = new Instantiator();
  814. $this->typedFieldMapper = $typedFieldMapper ?? new DefaultTypedFieldMapper();
  815. }
  816. /**
  817. * Gets the ReflectionProperties of the mapped class.
  818. *
  819. * @return ReflectionProperty[]|null[] An array of ReflectionProperty instances.
  820. * @psalm-return array<ReflectionProperty|null>
  821. */
  822. public function getReflectionProperties()
  823. {
  824. return $this->reflFields;
  825. }
  826. /**
  827. * Gets a ReflectionProperty for a specific field of the mapped class.
  828. *
  829. * @param string $name
  830. *
  831. * @return ReflectionProperty
  832. */
  833. public function getReflectionProperty($name)
  834. {
  835. return $this->reflFields[$name];
  836. }
  837. /**
  838. * Gets the ReflectionProperty for the single identifier field.
  839. *
  840. * @return ReflectionProperty
  841. *
  842. * @throws BadMethodCallException If the class has a composite identifier.
  843. */
  844. public function getSingleIdReflectionProperty()
  845. {
  846. if ($this->isIdentifierComposite) {
  847. throw new BadMethodCallException('Class ' . $this->name . ' has a composite identifier.');
  848. }
  849. return $this->reflFields[$this->identifier[0]];
  850. }
  851. /**
  852. * Extracts the identifier values of an entity of this class.
  853. *
  854. * For composite identifiers, the identifier values are returned as an array
  855. * with the same order as the field order in {@link identifier}.
  856. *
  857. * @param object $entity
  858. *
  859. * @return array<string, mixed>
  860. */
  861. public function getIdentifierValues($entity)
  862. {
  863. if ($this->isIdentifierComposite) {
  864. $id = [];
  865. foreach ($this->identifier as $idField) {
  866. $value = $this->reflFields[$idField]->getValue($entity);
  867. if ($value !== null) {
  868. $id[$idField] = $value;
  869. }
  870. }
  871. return $id;
  872. }
  873. $id = $this->identifier[0];
  874. $value = $this->reflFields[$id]->getValue($entity);
  875. if ($value === null) {
  876. return [];
  877. }
  878. return [$id => $value];
  879. }
  880. /**
  881. * Populates the entity identifier of an entity.
  882. *
  883. * @param object $entity
  884. * @psalm-param array<string, mixed> $id
  885. *
  886. * @return void
  887. *
  888. * @todo Rename to assignIdentifier()
  889. */
  890. public function setIdentifierValues($entity, array $id)
  891. {
  892. foreach ($id as $idField => $idValue) {
  893. $this->reflFields[$idField]->setValue($entity, $idValue);
  894. }
  895. }
  896. /**
  897. * Sets the specified field to the specified value on the given entity.
  898. *
  899. * @param object $entity
  900. * @param string $field
  901. * @param mixed $value
  902. *
  903. * @return void
  904. */
  905. public function setFieldValue($entity, $field, $value)
  906. {
  907. $this->reflFields[$field]->setValue($entity, $value);
  908. }
  909. /**
  910. * Gets the specified field's value off the given entity.
  911. *
  912. * @param object $entity
  913. * @param string $field
  914. *
  915. * @return mixed
  916. */
  917. public function getFieldValue($entity, $field)
  918. {
  919. return $this->reflFields[$field]->getValue($entity);
  920. }
  921. /**
  922. * Creates a string representation of this instance.
  923. *
  924. * @return string The string representation of this instance.
  925. *
  926. * @todo Construct meaningful string representation.
  927. */
  928. public function __toString()
  929. {
  930. return self::class . '@' . spl_object_id($this);
  931. }
  932. /**
  933. * Determines which fields get serialized.
  934. *
  935. * It is only serialized what is necessary for best unserialization performance.
  936. * That means any metadata properties that are not set or empty or simply have
  937. * their default value are NOT serialized.
  938. *
  939. * Parts that are also NOT serialized because they can not be properly unserialized:
  940. * - reflClass (ReflectionClass)
  941. * - reflFields (ReflectionProperty array)
  942. *
  943. * @return string[] The names of all the fields that should be serialized.
  944. */
  945. public function __sleep()
  946. {
  947. // This metadata is always serialized/cached.
  948. $serialized = [
  949. 'associationMappings',
  950. 'columnNames', //TODO: 3.0 Remove this. Can use fieldMappings[$fieldName]['columnName']
  951. 'fieldMappings',
  952. 'fieldNames',
  953. 'embeddedClasses',
  954. 'identifier',
  955. 'isIdentifierComposite', // TODO: REMOVE
  956. 'name',
  957. 'namespace', // TODO: REMOVE
  958. 'table',
  959. 'rootEntityName',
  960. 'idGenerator', //TODO: Does not really need to be serialized. Could be moved to runtime.
  961. ];
  962. // The rest of the metadata is only serialized if necessary.
  963. if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
  964. $serialized[] = 'changeTrackingPolicy';
  965. }
  966. if ($this->customRepositoryClassName) {
  967. $serialized[] = 'customRepositoryClassName';
  968. }
  969. if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE) {
  970. $serialized[] = 'inheritanceType';
  971. $serialized[] = 'discriminatorColumn';
  972. $serialized[] = 'discriminatorValue';
  973. $serialized[] = 'discriminatorMap';
  974. $serialized[] = 'parentClasses';
  975. $serialized[] = 'subClasses';
  976. }
  977. if ($this->generatorType !== self::GENERATOR_TYPE_NONE) {
  978. $serialized[] = 'generatorType';
  979. if ($this->generatorType === self::GENERATOR_TYPE_SEQUENCE) {
  980. $serialized[] = 'sequenceGeneratorDefinition';
  981. }
  982. }
  983. if ($this->isMappedSuperclass) {
  984. $serialized[] = 'isMappedSuperclass';
  985. }
  986. if ($this->isEmbeddedClass) {
  987. $serialized[] = 'isEmbeddedClass';
  988. }
  989. if ($this->containsForeignIdentifier) {
  990. $serialized[] = 'containsForeignIdentifier';
  991. }
  992. if ($this->containsEnumIdentifier) {
  993. $serialized[] = 'containsEnumIdentifier';
  994. }
  995. if ($this->isVersioned) {
  996. $serialized[] = 'isVersioned';
  997. $serialized[] = 'versionField';
  998. }
  999. if ($this->lifecycleCallbacks) {
  1000. $serialized[] = 'lifecycleCallbacks';
  1001. }
  1002. if ($this->entityListeners) {
  1003. $serialized[] = 'entityListeners';
  1004. }
  1005. if ($this->namedQueries) {
  1006. $serialized[] = 'namedQueries';
  1007. }
  1008. if ($this->namedNativeQueries) {
  1009. $serialized[] = 'namedNativeQueries';
  1010. }
  1011. if ($this->sqlResultSetMappings) {
  1012. $serialized[] = 'sqlResultSetMappings';
  1013. }
  1014. if ($this->isReadOnly) {
  1015. $serialized[] = 'isReadOnly';
  1016. }
  1017. if ($this->customGeneratorDefinition) {
  1018. $serialized[] = 'customGeneratorDefinition';
  1019. }
  1020. if ($this->cache) {
  1021. $serialized[] = 'cache';
  1022. }
  1023. if ($this->requiresFetchAfterChange) {
  1024. $serialized[] = 'requiresFetchAfterChange';
  1025. }
  1026. return $serialized;
  1027. }
  1028. /**
  1029. * Creates a new instance of the mapped class, without invoking the constructor.
  1030. *
  1031. * @return object
  1032. */
  1033. public function newInstance()
  1034. {
  1035. return $this->instantiator->instantiate($this->name);
  1036. }
  1037. /**
  1038. * Restores some state that can not be serialized/unserialized.
  1039. *
  1040. * @param ReflectionService $reflService
  1041. *
  1042. * @return void
  1043. */
  1044. public function wakeupReflection($reflService)
  1045. {
  1046. // Restore ReflectionClass and properties
  1047. $this->reflClass = $reflService->getClass($this->name);
  1048. $this->instantiator = $this->instantiator ?: new Instantiator();
  1049. $parentReflFields = [];
  1050. foreach ($this->embeddedClasses as $property => $embeddedClass) {
  1051. if (isset($embeddedClass['declaredField'])) {
  1052. $childProperty = $this->getAccessibleProperty(
  1053. $reflService,
  1054. $this->embeddedClasses[$embeddedClass['declaredField']]['class'],
  1055. $embeddedClass['originalField']
  1056. );
  1057. assert($childProperty !== null);
  1058. $parentReflFields[$property] = new ReflectionEmbeddedProperty(
  1059. $parentReflFields[$embeddedClass['declaredField']],
  1060. $childProperty,
  1061. $this->embeddedClasses[$embeddedClass['declaredField']]['class']
  1062. );
  1063. continue;
  1064. }
  1065. $fieldRefl = $this->getAccessibleProperty(
  1066. $reflService,
  1067. $embeddedClass['declared'] ?? $this->name,
  1068. $property
  1069. );
  1070. $parentReflFields[$property] = $fieldRefl;
  1071. $this->reflFields[$property] = $fieldRefl;
  1072. }
  1073. foreach ($this->fieldMappings as $field => $mapping) {
  1074. if (isset($mapping['declaredField']) && isset($parentReflFields[$mapping['declaredField']])) {
  1075. $childProperty = $this->getAccessibleProperty($reflService, $mapping['originalClass'], $mapping['originalField']);
  1076. assert($childProperty !== null);
  1077. if (isset($mapping['enumType'])) {
  1078. $childProperty = new ReflectionEnumProperty(
  1079. $childProperty,
  1080. $mapping['enumType']
  1081. );
  1082. }
  1083. $this->reflFields[$field] = new ReflectionEmbeddedProperty(
  1084. $parentReflFields[$mapping['declaredField']],
  1085. $childProperty,
  1086. $mapping['originalClass']
  1087. );
  1088. continue;
  1089. }
  1090. $this->reflFields[$field] = isset($mapping['declared'])
  1091. ? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
  1092. : $this->getAccessibleProperty($reflService, $this->name, $field);
  1093. if (isset($mapping['enumType']) && $this->reflFields[$field] !== null) {
  1094. $this->reflFields[$field] = new ReflectionEnumProperty(
  1095. $this->reflFields[$field],
  1096. $mapping['enumType']
  1097. );
  1098. }
  1099. }
  1100. foreach ($this->associationMappings as $field => $mapping) {
  1101. $this->reflFields[$field] = isset($mapping['declared'])
  1102. ? $this->getAccessibleProperty($reflService, $mapping['declared'], $field)
  1103. : $this->getAccessibleProperty($reflService, $this->name, $field);
  1104. }
  1105. }
  1106. /**
  1107. * Initializes a new ClassMetadata instance that will hold the object-relational mapping
  1108. * metadata of the class with the given name.
  1109. *
  1110. * @param ReflectionService $reflService The reflection service.
  1111. *
  1112. * @return void
  1113. */
  1114. public function initializeReflection($reflService)
  1115. {
  1116. $this->reflClass = $reflService->getClass($this->name);
  1117. $this->namespace = $reflService->getClassNamespace($this->name);
  1118. if ($this->reflClass) {
  1119. $this->name = $this->rootEntityName = $this->reflClass->getName();
  1120. }
  1121. $this->table['name'] = $this->namingStrategy->classToTableName($this->name);
  1122. }
  1123. /**
  1124. * Validates Identifier.
  1125. *
  1126. * @return void
  1127. *
  1128. * @throws MappingException
  1129. */
  1130. public function validateIdentifier()
  1131. {
  1132. if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
  1133. return;
  1134. }
  1135. // Verify & complete identifier mapping
  1136. if (! $this->identifier) {
  1137. throw MappingException::identifierRequired($this->name);
  1138. }
  1139. if ($this->usesIdGenerator() && $this->isIdentifierComposite) {
  1140. throw MappingException::compositeKeyAssignedIdGeneratorRequired($this->name);
  1141. }
  1142. }
  1143. /**
  1144. * Validates association targets actually exist.
  1145. *
  1146. * @return void
  1147. *
  1148. * @throws MappingException
  1149. */
  1150. public function validateAssociations()
  1151. {
  1152. foreach ($this->associationMappings as $mapping) {
  1153. if (
  1154. ! class_exists($mapping['targetEntity'])
  1155. && ! interface_exists($mapping['targetEntity'])
  1156. && ! trait_exists($mapping['targetEntity'])
  1157. ) {
  1158. throw MappingException::invalidTargetEntityClass($mapping['targetEntity'], $this->name, $mapping['fieldName']);
  1159. }
  1160. }
  1161. }
  1162. /**
  1163. * Validates lifecycle callbacks.
  1164. *
  1165. * @param ReflectionService $reflService
  1166. *
  1167. * @return void
  1168. *
  1169. * @throws MappingException
  1170. */
  1171. public function validateLifecycleCallbacks($reflService)
  1172. {
  1173. foreach ($this->lifecycleCallbacks as $callbacks) {
  1174. foreach ($callbacks as $callbackFuncName) {
  1175. if (! $reflService->hasPublicMethod($this->name, $callbackFuncName)) {
  1176. throw MappingException::lifecycleCallbackMethodNotFound($this->name, $callbackFuncName);
  1177. }
  1178. }
  1179. }
  1180. }
  1181. /**
  1182. * {@inheritDoc}
  1183. */
  1184. public function getReflectionClass()
  1185. {
  1186. return $this->reflClass;
  1187. }
  1188. /**
  1189. * @psalm-param array{usage?: mixed, region?: mixed} $cache
  1190. *
  1191. * @return void
  1192. */
  1193. public function enableCache(array $cache)
  1194. {
  1195. if (! isset($cache['usage'])) {
  1196. $cache['usage'] = self::CACHE_USAGE_READ_ONLY;
  1197. }
  1198. if (! isset($cache['region'])) {
  1199. $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName));
  1200. }
  1201. $this->cache = $cache;
  1202. }
  1203. /**
  1204. * @param string $fieldName
  1205. * @psalm-param array{usage?: int, region?: string} $cache
  1206. *
  1207. * @return void
  1208. */
  1209. public function enableAssociationCache($fieldName, array $cache)
  1210. {
  1211. $this->associationMappings[$fieldName]['cache'] = $this->getAssociationCacheDefaults($fieldName, $cache);
  1212. }
  1213. /**
  1214. * @param string $fieldName
  1215. * @param array $cache
  1216. * @psalm-param array{usage?: int|null, region?: string|null} $cache
  1217. *
  1218. * @return int[]|string[]
  1219. * @psalm-return array{usage: int, region: string|null}
  1220. */
  1221. public function getAssociationCacheDefaults($fieldName, array $cache)
  1222. {
  1223. if (! isset($cache['usage'])) {
  1224. $cache['usage'] = $this->cache['usage'] ?? self::CACHE_USAGE_READ_ONLY;
  1225. }
  1226. if (! isset($cache['region'])) {
  1227. $cache['region'] = strtolower(str_replace('\\', '_', $this->rootEntityName)) . '__' . $fieldName;
  1228. }
  1229. return $cache;
  1230. }
  1231. /**
  1232. * Sets the change tracking policy used by this class.
  1233. *
  1234. * @param int $policy
  1235. *
  1236. * @return void
  1237. */
  1238. public function setChangeTrackingPolicy($policy)
  1239. {
  1240. $this->changeTrackingPolicy = $policy;
  1241. }
  1242. /**
  1243. * Whether the change tracking policy of this class is "deferred explicit".
  1244. *
  1245. * @return bool
  1246. */
  1247. public function isChangeTrackingDeferredExplicit()
  1248. {
  1249. return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
  1250. }
  1251. /**
  1252. * Whether the change tracking policy of this class is "deferred implicit".
  1253. *
  1254. * @return bool
  1255. */
  1256. public function isChangeTrackingDeferredImplicit()
  1257. {
  1258. return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
  1259. }
  1260. /**
  1261. * Whether the change tracking policy of this class is "notify".
  1262. *
  1263. * @return bool
  1264. */
  1265. public function isChangeTrackingNotify()
  1266. {
  1267. return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
  1268. }
  1269. /**
  1270. * Checks whether a field is part of the identifier/primary key field(s).
  1271. *
  1272. * @param string $fieldName The field name.
  1273. *
  1274. * @return bool TRUE if the field is part of the table identifier/primary key field(s),
  1275. * FALSE otherwise.
  1276. */
  1277. public function isIdentifier($fieldName)
  1278. {
  1279. if (! $this->identifier) {
  1280. return false;
  1281. }
  1282. if (! $this->isIdentifierComposite) {
  1283. return $fieldName === $this->identifier[0];
  1284. }
  1285. return in_array($fieldName, $this->identifier, true);
  1286. }
  1287. /**
  1288. * Checks if the field is unique.
  1289. *
  1290. * @param string $fieldName The field name.
  1291. *
  1292. * @return bool TRUE if the field is unique, FALSE otherwise.
  1293. */
  1294. public function isUniqueField($fieldName)
  1295. {
  1296. $mapping = $this->getFieldMapping($fieldName);
  1297. return $mapping !== false && isset($mapping['unique']) && $mapping['unique'];
  1298. }
  1299. /**
  1300. * Checks if the field is not null.
  1301. *
  1302. * @param string $fieldName The field name.
  1303. *
  1304. * @return bool TRUE if the field is not null, FALSE otherwise.
  1305. */
  1306. public function isNullable($fieldName)
  1307. {
  1308. $mapping = $this->getFieldMapping($fieldName);
  1309. return $mapping !== false && isset($mapping['nullable']) && $mapping['nullable'];
  1310. }
  1311. /**
  1312. * Gets a column name for a field name.
  1313. * If the column name for the field cannot be found, the given field name
  1314. * is returned.
  1315. *
  1316. * @param string $fieldName The field name.
  1317. *
  1318. * @return string The column name.
  1319. */
  1320. public function getColumnName($fieldName)
  1321. {
  1322. return $this->columnNames[$fieldName] ?? $fieldName;
  1323. }
  1324. /**
  1325. * Gets the mapping of a (regular) field that holds some data but not a
  1326. * reference to another object.
  1327. *
  1328. * @param string $fieldName The field name.
  1329. *
  1330. * @return mixed[] The field mapping.
  1331. * @psalm-return FieldMapping
  1332. *
  1333. * @throws MappingException
  1334. */
  1335. public function getFieldMapping($fieldName)
  1336. {
  1337. if (! isset($this->fieldMappings[$fieldName])) {
  1338. throw MappingException::mappingNotFound($this->name, $fieldName);
  1339. }
  1340. return $this->fieldMappings[$fieldName];
  1341. }
  1342. /**
  1343. * Gets the mapping of an association.
  1344. *
  1345. * @see ClassMetadataInfo::$associationMappings
  1346. *
  1347. * @param string $fieldName The field name that represents the association in
  1348. * the object model.
  1349. *
  1350. * @return mixed[] The mapping.
  1351. * @psalm-return AssociationMapping
  1352. *
  1353. * @throws MappingException
  1354. */
  1355. public function getAssociationMapping($fieldName)
  1356. {
  1357. if (! isset($this->associationMappings[$fieldName])) {
  1358. throw MappingException::mappingNotFound($this->name, $fieldName);
  1359. }
  1360. return $this->associationMappings[$fieldName];
  1361. }
  1362. /**
  1363. * Gets all association mappings of the class.
  1364. *
  1365. * @psalm-return array<string, AssociationMapping>
  1366. */
  1367. public function getAssociationMappings()
  1368. {
  1369. return $this->associationMappings;
  1370. }
  1371. /**
  1372. * Gets the field name for a column name.
  1373. * If no field name can be found the column name is returned.
  1374. *
  1375. * @param string $columnName The column name.
  1376. *
  1377. * @return string The column alias.
  1378. */
  1379. public function getFieldName($columnName)
  1380. {
  1381. return $this->fieldNames[$columnName] ?? $columnName;
  1382. }
  1383. /**
  1384. * Gets the named query.
  1385. *
  1386. * @see ClassMetadataInfo::$namedQueries
  1387. *
  1388. * @param string $queryName The query name.
  1389. *
  1390. * @return string
  1391. *
  1392. * @throws MappingException
  1393. */
  1394. public function getNamedQuery($queryName)
  1395. {
  1396. if (! isset($this->namedQueries[$queryName])) {
  1397. throw MappingException::queryNotFound($this->name, $queryName);
  1398. }
  1399. return $this->namedQueries[$queryName]['dql'];
  1400. }
  1401. /**
  1402. * Gets all named queries of the class.
  1403. *
  1404. * @return mixed[][]
  1405. * @psalm-return array<string, array<string, mixed>>
  1406. */
  1407. public function getNamedQueries()
  1408. {
  1409. return $this->namedQueries;
  1410. }
  1411. /**
  1412. * Gets the named native query.
  1413. *
  1414. * @see ClassMetadataInfo::$namedNativeQueries
  1415. *
  1416. * @param string $queryName The query name.
  1417. *
  1418. * @return mixed[]
  1419. * @psalm-return array<string, mixed>
  1420. *
  1421. * @throws MappingException
  1422. */
  1423. public function getNamedNativeQuery($queryName)
  1424. {
  1425. if (! isset($this->namedNativeQueries[$queryName])) {
  1426. throw MappingException::queryNotFound($this->name, $queryName);
  1427. }
  1428. return $this->namedNativeQueries[$queryName];
  1429. }
  1430. /**
  1431. * Gets all named native queries of the class.
  1432. *
  1433. * @psalm-return array<string, array<string, mixed>>
  1434. */
  1435. public function getNamedNativeQueries()
  1436. {
  1437. return $this->namedNativeQueries;
  1438. }
  1439. /**
  1440. * Gets the result set mapping.
  1441. *
  1442. * @see ClassMetadataInfo::$sqlResultSetMappings
  1443. *
  1444. * @param string $name The result set mapping name.
  1445. *
  1446. * @return mixed[]
  1447. * @psalm-return array{name: string, entities: array, columns: array}
  1448. *
  1449. * @throws MappingException
  1450. */
  1451. public function getSqlResultSetMapping($name)
  1452. {
  1453. if (! isset($this->sqlResultSetMappings[$name])) {
  1454. throw MappingException::resultMappingNotFound($this->name, $name);
  1455. }
  1456. return $this->sqlResultSetMappings[$name];
  1457. }
  1458. /**
  1459. * Gets all sql result set mappings of the class.
  1460. *
  1461. * @return mixed[]
  1462. * @psalm-return array<string, array{name: string, entities: array, columns: array}>
  1463. */
  1464. public function getSqlResultSetMappings()
  1465. {
  1466. return $this->sqlResultSetMappings;
  1467. }
  1468. /**
  1469. * Checks whether given property has type
  1470. *
  1471. * @param string $name Property name
  1472. */
  1473. private function isTypedProperty(string $name): bool
  1474. {
  1475. return PHP_VERSION_ID >= 70400
  1476. && isset($this->reflClass)
  1477. && $this->reflClass->hasProperty($name)
  1478. && $this->reflClass->getProperty($name)->hasType();
  1479. }
  1480. /**
  1481. * Validates & completes the given field mapping based on typed property.
  1482. *
  1483. * @param array{fieldName: string, type?: mixed} $mapping The field mapping to validate & complete.
  1484. *
  1485. * @return array{fieldName: string, enumType?: string, type?: mixed} The updated mapping.
  1486. */
  1487. private function validateAndCompleteTypedFieldMapping(array $mapping): array
  1488. {
  1489. $field = $this->reflClass->getProperty($mapping['fieldName']);
  1490. $mapping = $this->typedFieldMapper->validateAndComplete($mapping, $field);
  1491. return $mapping;
  1492. }
  1493. /**
  1494. * Validates & completes the basic mapping information based on typed property.
  1495. *
  1496. * @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.
  1497. *
  1498. * @return mixed[] The updated mapping.
  1499. */
  1500. private function validateAndCompleteTypedAssociationMapping(array $mapping): array
  1501. {
  1502. $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
  1503. if ($type === null || ($mapping['type'] & self::TO_ONE) === 0) {
  1504. return $mapping;
  1505. }
  1506. if (! isset($mapping['targetEntity']) && $type instanceof ReflectionNamedType) {
  1507. $mapping['targetEntity'] = $type->getName();
  1508. }
  1509. return $mapping;
  1510. }
  1511. /**
  1512. * Validates & completes the given field mapping.
  1513. *
  1514. * @psalm-param array{
  1515. * fieldName?: string,
  1516. * columnName?: string,
  1517. * id?: bool,
  1518. * generated?: int,
  1519. * enumType?: class-string,
  1520. * } $mapping The field mapping to validate & complete.
  1521. *
  1522. * @return mixed[] The updated mapping.
  1523. *
  1524. * @throws MappingException
  1525. */
  1526. protected function validateAndCompleteFieldMapping(array $mapping): array
  1527. {
  1528. // Check mandatory fields
  1529. if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
  1530. throw MappingException::missingFieldName($this->name);
  1531. }
  1532. if ($this->isTypedProperty($mapping['fieldName'])) {
  1533. $mapping = $this->validateAndCompleteTypedFieldMapping($mapping);
  1534. }
  1535. if (! isset($mapping['type'])) {
  1536. // Default to string
  1537. $mapping['type'] = 'string';
  1538. }
  1539. // Complete fieldName and columnName mapping
  1540. if (! isset($mapping['columnName'])) {
  1541. $mapping['columnName'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName'], $this->name);
  1542. }
  1543. if ($mapping['columnName'][0] === '`') {
  1544. $mapping['columnName'] = trim($mapping['columnName'], '`');
  1545. $mapping['quoted'] = true;
  1546. }
  1547. $this->columnNames[$mapping['fieldName']] = $mapping['columnName'];
  1548. if (isset($this->fieldNames[$mapping['columnName']]) || ($this->discriminatorColumn && $this->discriminatorColumn['name'] === $mapping['columnName'])) {
  1549. throw MappingException::duplicateColumnName($this->name, $mapping['columnName']);
  1550. }
  1551. $this->fieldNames[$mapping['columnName']] = $mapping['fieldName'];
  1552. // Complete id mapping
  1553. if (isset($mapping['id']) && $mapping['id'] === true) {
  1554. if ($this->versionField === $mapping['fieldName']) {
  1555. throw MappingException::cannotVersionIdField($this->name, $mapping['fieldName']);
  1556. }
  1557. if (! in_array($mapping['fieldName'], $this->identifier, true)) {
  1558. $this->identifier[] = $mapping['fieldName'];
  1559. }
  1560. // Check for composite key
  1561. if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
  1562. $this->isIdentifierComposite = true;
  1563. }
  1564. }
  1565. if (Type::hasType($mapping['type']) && Type::getType($mapping['type'])->canRequireSQLConversion()) {
  1566. if (isset($mapping['id']) && $mapping['id'] === true) {
  1567. throw MappingException::sqlConversionNotAllowedForIdentifiers($this->name, $mapping['fieldName'], $mapping['type']);
  1568. }
  1569. $mapping['requireSQLConversion'] = true;
  1570. }
  1571. if (isset($mapping['generated'])) {
  1572. if (! in_array($mapping['generated'], [self::GENERATED_NEVER, self::GENERATED_INSERT, self::GENERATED_ALWAYS])) {
  1573. throw MappingException::invalidGeneratedMode($mapping['generated']);
  1574. }
  1575. if ($mapping['generated'] === self::GENERATED_NEVER) {
  1576. unset($mapping['generated']);
  1577. }
  1578. }
  1579. if (isset($mapping['enumType'])) {
  1580. if (PHP_VERSION_ID < 80100) {
  1581. throw MappingException::enumsRequirePhp81($this->name, $mapping['fieldName']);
  1582. }
  1583. if (! enum_exists($mapping['enumType'])) {
  1584. throw MappingException::nonEnumTypeMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
  1585. }
  1586. if (! empty($mapping['id'])) {
  1587. $this->containsEnumIdentifier = true;
  1588. }
  1589. }
  1590. return $mapping;
  1591. }
  1592. /**
  1593. * Validates & completes the basic mapping information that is common to all
  1594. * association mappings (one-to-one, many-ot-one, one-to-many, many-to-many).
  1595. *
  1596. * @psalm-param array<string, mixed> $mapping The mapping.
  1597. *
  1598. * @return mixed[] The updated mapping.
  1599. * @psalm-return AssociationMapping
  1600. *
  1601. * @throws MappingException If something is wrong with the mapping.
  1602. */
  1603. protected function _validateAndCompleteAssociationMapping(array $mapping)
  1604. {
  1605. if (! isset($mapping['mappedBy'])) {
  1606. $mapping['mappedBy'] = null;
  1607. }
  1608. if (! isset($mapping['inversedBy'])) {
  1609. $mapping['inversedBy'] = null;
  1610. }
  1611. $mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
  1612. if (empty($mapping['indexBy'])) {
  1613. unset($mapping['indexBy']);
  1614. }
  1615. // If targetEntity is unqualified, assume it is in the same namespace as
  1616. // the sourceEntity.
  1617. $mapping['sourceEntity'] = $this->name;
  1618. if ($this->isTypedProperty($mapping['fieldName'])) {
  1619. $mapping = $this->validateAndCompleteTypedAssociationMapping($mapping);
  1620. }
  1621. if (isset($mapping['targetEntity'])) {
  1622. $mapping['targetEntity'] = $this->fullyQualifiedClassName($mapping['targetEntity']);
  1623. $mapping['targetEntity'] = ltrim($mapping['targetEntity'], '\\');
  1624. }
  1625. if (($mapping['type'] & self::MANY_TO_ONE) > 0 && isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
  1626. throw MappingException::illegalOrphanRemoval($this->name, $mapping['fieldName']);
  1627. }
  1628. // Complete id mapping
  1629. if (isset($mapping['id']) && $mapping['id'] === true) {
  1630. if (isset($mapping['orphanRemoval']) && $mapping['orphanRemoval']) {
  1631. throw MappingException::illegalOrphanRemovalOnIdentifierAssociation($this->name, $mapping['fieldName']);
  1632. }
  1633. if (! in_array($mapping['fieldName'], $this->identifier, true)) {
  1634. if (isset($mapping['joinColumns']) && count($mapping['joinColumns']) >= 2) {
  1635. throw MappingException::cannotMapCompositePrimaryKeyEntitiesAsForeignId(
  1636. $mapping['targetEntity'],
  1637. $this->name,
  1638. $mapping['fieldName']
  1639. );
  1640. }
  1641. $this->identifier[] = $mapping['fieldName'];
  1642. $this->containsForeignIdentifier = true;
  1643. }
  1644. // Check for composite key
  1645. if (! $this->isIdentifierComposite && count($this->identifier) > 1) {
  1646. $this->isIdentifierComposite = true;
  1647. }
  1648. if ($this->cache && ! isset($mapping['cache'])) {
  1649. throw NonCacheableEntityAssociation::fromEntityAndField(
  1650. $this->name,
  1651. $mapping['fieldName']
  1652. );
  1653. }
  1654. }
  1655. // Mandatory attributes for both sides
  1656. // Mandatory: fieldName, targetEntity
  1657. if (! isset($mapping['fieldName']) || ! $mapping['fieldName']) {
  1658. throw MappingException::missingFieldName($this->name);
  1659. }
  1660. if (! isset($mapping['targetEntity'])) {
  1661. throw MappingException::missingTargetEntity($mapping['fieldName']);
  1662. }
  1663. // Mandatory and optional attributes for either side
  1664. if (! $mapping['mappedBy']) {
  1665. if (isset($mapping['joinTable']) && $mapping['joinTable']) {
  1666. if (isset($mapping['joinTable']['name']) && $mapping['joinTable']['name'][0] === '`') {
  1667. $mapping['joinTable']['name'] = trim($mapping['joinTable']['name'], '`');
  1668. $mapping['joinTable']['quoted'] = true;
  1669. }
  1670. }
  1671. } else {
  1672. $mapping['isOwningSide'] = false;
  1673. }
  1674. if (isset($mapping['id']) && $mapping['id'] === true && $mapping['type'] & self::TO_MANY) {
  1675. throw MappingException::illegalToManyIdentifierAssociation($this->name, $mapping['fieldName']);
  1676. }
  1677. // Fetch mode. Default fetch mode to LAZY, if not set.
  1678. if (! isset($mapping['fetch'])) {
  1679. $mapping['fetch'] = self::FETCH_LAZY;
  1680. }
  1681. // Cascades
  1682. $cascades = isset($mapping['cascade']) ? array_map('strtolower', $mapping['cascade']) : [];
  1683. $allCascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
  1684. if (in_array('all', $cascades, true)) {
  1685. $cascades = $allCascades;
  1686. } elseif (count($cascades) !== count(array_intersect($cascades, $allCascades))) {
  1687. throw MappingException::invalidCascadeOption(
  1688. array_diff($cascades, $allCascades),
  1689. $this->name,
  1690. $mapping['fieldName']
  1691. );
  1692. }
  1693. $mapping['cascade'] = $cascades;
  1694. $mapping['isCascadeRemove'] = in_array('remove', $cascades, true);
  1695. $mapping['isCascadePersist'] = in_array('persist', $cascades, true);
  1696. $mapping['isCascadeRefresh'] = in_array('refresh', $cascades, true);
  1697. $mapping['isCascadeMerge'] = in_array('merge', $cascades, true);
  1698. $mapping['isCascadeDetach'] = in_array('detach', $cascades, true);
  1699. return $mapping;
  1700. }
  1701. /**
  1702. * Validates & completes a one-to-one association mapping.
  1703. *
  1704. * @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
  1705. *
  1706. * @return mixed[] The validated & completed mapping.
  1707. * @psalm-return array{
  1708. * mappedBy: mixed|null,
  1709. * inversedBy: mixed|null,
  1710. * isOwningSide: bool,
  1711. * sourceEntity: class-string,
  1712. * targetEntity: string,
  1713. * fieldName: mixed,
  1714. * fetch: mixed,
  1715. * cascade: array<string>,
  1716. * isCascadeRemove: bool,
  1717. * isCascadePersist: bool,
  1718. * isCascadeRefresh: bool,
  1719. * isCascadeMerge: bool,
  1720. * isCascadeDetach: bool,
  1721. * type: int,
  1722. * originalField: string,
  1723. * originalClass: class-string,
  1724. * joinColumns?: array{0: array{name: string, referencedColumnName: string}}|mixed,
  1725. * id?: mixed,
  1726. * sourceToTargetKeyColumns?: array<string, string>,
  1727. * joinColumnFieldNames?: array<string, string>,
  1728. * targetToSourceKeyColumns?: array<string, string>,
  1729. * orphanRemoval: bool
  1730. * }
  1731. *
  1732. * @throws RuntimeException
  1733. * @throws MappingException
  1734. */
  1735. protected function _validateAndCompleteOneToOneMapping(array $mapping)
  1736. {
  1737. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1738. if (isset($mapping['joinColumns']) && $mapping['joinColumns']) {
  1739. $mapping['isOwningSide'] = true;
  1740. }
  1741. if ($mapping['isOwningSide']) {
  1742. if (empty($mapping['joinColumns'])) {
  1743. // Apply default join column
  1744. $mapping['joinColumns'] = [
  1745. [
  1746. 'name' => $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name),
  1747. 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
  1748. ],
  1749. ];
  1750. }
  1751. $uniqueConstraintColumns = [];
  1752. foreach ($mapping['joinColumns'] as &$joinColumn) {
  1753. if ($mapping['type'] === self::ONE_TO_ONE && ! $this->isInheritanceTypeSingleTable()) {
  1754. if (count($mapping['joinColumns']) === 1) {
  1755. if (empty($mapping['id'])) {
  1756. $joinColumn['unique'] = true;
  1757. }
  1758. } else {
  1759. $uniqueConstraintColumns[] = $joinColumn['name'];
  1760. }
  1761. }
  1762. if (empty($joinColumn['name'])) {
  1763. $joinColumn['name'] = $this->namingStrategy->joinColumnName($mapping['fieldName'], $this->name);
  1764. }
  1765. if (empty($joinColumn['referencedColumnName'])) {
  1766. $joinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
  1767. }
  1768. if ($joinColumn['name'][0] === '`') {
  1769. $joinColumn['name'] = trim($joinColumn['name'], '`');
  1770. $joinColumn['quoted'] = true;
  1771. }
  1772. if ($joinColumn['referencedColumnName'][0] === '`') {
  1773. $joinColumn['referencedColumnName'] = trim($joinColumn['referencedColumnName'], '`');
  1774. $joinColumn['quoted'] = true;
  1775. }
  1776. $mapping['sourceToTargetKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName'];
  1777. $mapping['joinColumnFieldNames'][$joinColumn['name']] = $joinColumn['fieldName'] ?? $joinColumn['name'];
  1778. }
  1779. if ($uniqueConstraintColumns) {
  1780. if (! $this->table) {
  1781. throw new RuntimeException('ClassMetadataInfo::setTable() has to be called before defining a one to one relationship.');
  1782. }
  1783. $this->table['uniqueConstraints'][$mapping['fieldName'] . '_uniq'] = ['columns' => $uniqueConstraintColumns];
  1784. }
  1785. $mapping['targetToSourceKeyColumns'] = array_flip($mapping['sourceToTargetKeyColumns']);
  1786. }
  1787. $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
  1788. $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
  1789. if ($mapping['orphanRemoval']) {
  1790. unset($mapping['unique']);
  1791. }
  1792. if (isset($mapping['id']) && $mapping['id'] === true && ! $mapping['isOwningSide']) {
  1793. throw MappingException::illegalInverseIdentifierAssociation($this->name, $mapping['fieldName']);
  1794. }
  1795. return $mapping;
  1796. }
  1797. /**
  1798. * Validates & completes a one-to-many association mapping.
  1799. *
  1800. * @psalm-param array<string, mixed> $mapping The mapping to validate and complete.
  1801. *
  1802. * @return mixed[] The validated and completed mapping.
  1803. * @psalm-return array{
  1804. * mappedBy: mixed,
  1805. * inversedBy: mixed,
  1806. * isOwningSide: bool,
  1807. * sourceEntity: string,
  1808. * targetEntity: string,
  1809. * fieldName: mixed,
  1810. * fetch: int|mixed,
  1811. * cascade: array<array-key,string>,
  1812. * isCascadeRemove: bool,
  1813. * isCascadePersist: bool,
  1814. * isCascadeRefresh: bool,
  1815. * isCascadeMerge: bool,
  1816. * isCascadeDetach: bool,
  1817. * orphanRemoval: bool
  1818. * }
  1819. *
  1820. * @throws MappingException
  1821. * @throws InvalidArgumentException
  1822. */
  1823. protected function _validateAndCompleteOneToManyMapping(array $mapping)
  1824. {
  1825. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1826. // OneToMany-side MUST be inverse (must have mappedBy)
  1827. if (! isset($mapping['mappedBy'])) {
  1828. throw MappingException::oneToManyRequiresMappedBy($this->name, $mapping['fieldName']);
  1829. }
  1830. $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
  1831. $mapping['isCascadeRemove'] = $mapping['orphanRemoval'] || $mapping['isCascadeRemove'];
  1832. $this->assertMappingOrderBy($mapping);
  1833. return $mapping;
  1834. }
  1835. /**
  1836. * Validates & completes a many-to-many association mapping.
  1837. *
  1838. * @psalm-param array<string, mixed> $mapping The mapping to validate & complete.
  1839. *
  1840. * @return mixed[] The validated & completed mapping.
  1841. * @psalm-return array{
  1842. * mappedBy: mixed,
  1843. * inversedBy: mixed,
  1844. * isOwningSide: bool,
  1845. * sourceEntity: class-string,
  1846. * targetEntity: string,
  1847. * fieldName: mixed,
  1848. * fetch: mixed,
  1849. * cascade: array<string>,
  1850. * isCascadeRemove: bool,
  1851. * isCascadePersist: bool,
  1852. * isCascadeRefresh: bool,
  1853. * isCascadeMerge: bool,
  1854. * isCascadeDetach: bool,
  1855. * type: int,
  1856. * originalField: string,
  1857. * originalClass: class-string,
  1858. * joinTable?: array{inverseJoinColumns: mixed}|mixed,
  1859. * joinTableColumns?: list<mixed>,
  1860. * isOnDeleteCascade?: true,
  1861. * relationToSourceKeyColumns?: array,
  1862. * relationToTargetKeyColumns?: array,
  1863. * orphanRemoval: bool
  1864. * }
  1865. *
  1866. * @throws InvalidArgumentException
  1867. */
  1868. protected function _validateAndCompleteManyToManyMapping(array $mapping)
  1869. {
  1870. $mapping = $this->_validateAndCompleteAssociationMapping($mapping);
  1871. if ($mapping['isOwningSide']) {
  1872. // owning side MUST have a join table
  1873. if (! isset($mapping['joinTable']['name'])) {
  1874. $mapping['joinTable']['name'] = $this->namingStrategy->joinTableName($mapping['sourceEntity'], $mapping['targetEntity'], $mapping['fieldName']);
  1875. }
  1876. $selfReferencingEntityWithoutJoinColumns = $mapping['sourceEntity'] === $mapping['targetEntity']
  1877. && (! (isset($mapping['joinTable']['joinColumns']) || isset($mapping['joinTable']['inverseJoinColumns'])));
  1878. if (! isset($mapping['joinTable']['joinColumns'])) {
  1879. $mapping['joinTable']['joinColumns'] = [
  1880. [
  1881. 'name' => $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $selfReferencingEntityWithoutJoinColumns ? 'source' : null),
  1882. 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
  1883. 'onDelete' => 'CASCADE',
  1884. ],
  1885. ];
  1886. }
  1887. if (! isset($mapping['joinTable']['inverseJoinColumns'])) {
  1888. $mapping['joinTable']['inverseJoinColumns'] = [
  1889. [
  1890. 'name' => $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $selfReferencingEntityWithoutJoinColumns ? 'target' : null),
  1891. 'referencedColumnName' => $this->namingStrategy->referenceColumnName(),
  1892. 'onDelete' => 'CASCADE',
  1893. ],
  1894. ];
  1895. }
  1896. $mapping['joinTableColumns'] = [];
  1897. foreach ($mapping['joinTable']['joinColumns'] as &$joinColumn) {
  1898. if (empty($joinColumn['name'])) {
  1899. $joinColumn['name'] = $this->namingStrategy->joinKeyColumnName($mapping['sourceEntity'], $joinColumn['referencedColumnName']);
  1900. }
  1901. if (empty($joinColumn['referencedColumnName'])) {
  1902. $joinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
  1903. }
  1904. if ($joinColumn['name'][0] === '`') {
  1905. $joinColumn['name'] = trim($joinColumn['name'], '`');
  1906. $joinColumn['quoted'] = true;
  1907. }
  1908. if ($joinColumn['referencedColumnName'][0] === '`') {
  1909. $joinColumn['referencedColumnName'] = trim($joinColumn['referencedColumnName'], '`');
  1910. $joinColumn['quoted'] = true;
  1911. }
  1912. if (isset($joinColumn['onDelete']) && strtolower($joinColumn['onDelete']) === 'cascade') {
  1913. $mapping['isOnDeleteCascade'] = true;
  1914. }
  1915. $mapping['relationToSourceKeyColumns'][$joinColumn['name']] = $joinColumn['referencedColumnName'];
  1916. $mapping['joinTableColumns'][] = $joinColumn['name'];
  1917. }
  1918. foreach ($mapping['joinTable']['inverseJoinColumns'] as &$inverseJoinColumn) {
  1919. if (empty($inverseJoinColumn['name'])) {
  1920. $inverseJoinColumn['name'] = $this->namingStrategy->joinKeyColumnName($mapping['targetEntity'], $inverseJoinColumn['referencedColumnName']);
  1921. }
  1922. if (empty($inverseJoinColumn['referencedColumnName'])) {
  1923. $inverseJoinColumn['referencedColumnName'] = $this->namingStrategy->referenceColumnName();
  1924. }
  1925. if ($inverseJoinColumn['name'][0] === '`') {
  1926. $inverseJoinColumn['name'] = trim($inverseJoinColumn['name'], '`');
  1927. $inverseJoinColumn['quoted'] = true;
  1928. }
  1929. if ($inverseJoinColumn['referencedColumnName'][0] === '`') {
  1930. $inverseJoinColumn['referencedColumnName'] = trim($inverseJoinColumn['referencedColumnName'], '`');
  1931. $inverseJoinColumn['quoted'] = true;
  1932. }
  1933. if (isset($inverseJoinColumn['onDelete']) && strtolower($inverseJoinColumn['onDelete']) === 'cascade') {
  1934. $mapping['isOnDeleteCascade'] = true;
  1935. }
  1936. $mapping['relationToTargetKeyColumns'][$inverseJoinColumn['name']] = $inverseJoinColumn['referencedColumnName'];
  1937. $mapping['joinTableColumns'][] = $inverseJoinColumn['name'];
  1938. }
  1939. }
  1940. $mapping['orphanRemoval'] = isset($mapping['orphanRemoval']) && $mapping['orphanRemoval'];
  1941. $this->assertMappingOrderBy($mapping);
  1942. return $mapping;
  1943. }
  1944. /**
  1945. * {@inheritDoc}
  1946. */
  1947. public function getIdentifierFieldNames()
  1948. {
  1949. return $this->identifier;
  1950. }
  1951. /**
  1952. * Gets the name of the single id field. Note that this only works on
  1953. * entity classes that have a single-field pk.
  1954. *
  1955. * @return string
  1956. *
  1957. * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
  1958. */
  1959. public function getSingleIdentifierFieldName()
  1960. {
  1961. if ($this->isIdentifierComposite) {
  1962. throw MappingException::singleIdNotAllowedOnCompositePrimaryKey($this->name);
  1963. }
  1964. if (! isset($this->identifier[0])) {
  1965. throw MappingException::noIdDefined($this->name);
  1966. }
  1967. return $this->identifier[0];
  1968. }
  1969. /**
  1970. * Gets the column name of the single id column. Note that this only works on
  1971. * entity classes that have a single-field pk.
  1972. *
  1973. * @return string
  1974. *
  1975. * @throws MappingException If the class doesn't have an identifier or it has a composite primary key.
  1976. */
  1977. public function getSingleIdentifierColumnName()
  1978. {
  1979. return $this->getColumnName($this->getSingleIdentifierFieldName());
  1980. }
  1981. /**
  1982. * INTERNAL:
  1983. * Sets the mapped identifier/primary key fields of this class.
  1984. * Mainly used by the ClassMetadataFactory to assign inherited identifiers.
  1985. *
  1986. * @psalm-param list<mixed> $identifier
  1987. *
  1988. * @return void
  1989. */
  1990. public function setIdentifier(array $identifier)
  1991. {
  1992. $this->identifier = $identifier;
  1993. $this->isIdentifierComposite = (count($this->identifier) > 1);
  1994. }
  1995. /**
  1996. * {@inheritDoc}
  1997. */
  1998. public function getIdentifier()
  1999. {
  2000. return $this->identifier;
  2001. }
  2002. /**
  2003. * {@inheritDoc}
  2004. */
  2005. public function hasField($fieldName)
  2006. {
  2007. return isset($this->fieldMappings[$fieldName]) || isset($this->embeddedClasses[$fieldName]);
  2008. }
  2009. /**
  2010. * Gets an array containing all the column names.
  2011. *
  2012. * @psalm-param list<string>|null $fieldNames
  2013. *
  2014. * @return mixed[]
  2015. * @psalm-return list<string>
  2016. */
  2017. public function getColumnNames(?array $fieldNames = null)
  2018. {
  2019. if ($fieldNames === null) {
  2020. return array_keys($this->fieldNames);
  2021. }
  2022. return array_values(array_map([$this, 'getColumnName'], $fieldNames));
  2023. }
  2024. /**
  2025. * Returns an array with all the identifier column names.
  2026. *
  2027. * @psalm-return list<string>
  2028. */
  2029. public function getIdentifierColumnNames()
  2030. {
  2031. $columnNames = [];
  2032. foreach ($this->identifier as $idProperty) {
  2033. if (isset($this->fieldMappings[$idProperty])) {
  2034. $columnNames[] = $this->fieldMappings[$idProperty]['columnName'];
  2035. continue;
  2036. }
  2037. // Association defined as Id field
  2038. $joinColumns = $this->associationMappings[$idProperty]['joinColumns'];
  2039. $assocColumnNames = array_map(static function ($joinColumn) {
  2040. return $joinColumn['name'];
  2041. }, $joinColumns);
  2042. $columnNames = array_merge($columnNames, $assocColumnNames);
  2043. }
  2044. return $columnNames;
  2045. }
  2046. /**
  2047. * Sets the type of Id generator to use for the mapped class.
  2048. *
  2049. * @param int $generatorType
  2050. * @psalm-param self::GENERATOR_TYPE_* $generatorType
  2051. *
  2052. * @return void
  2053. */
  2054. public function setIdGeneratorType($generatorType)
  2055. {
  2056. $this->generatorType = $generatorType;
  2057. }
  2058. /**
  2059. * Checks whether the mapped class uses an Id generator.
  2060. *
  2061. * @return bool TRUE if the mapped class uses an Id generator, FALSE otherwise.
  2062. */
  2063. public function usesIdGenerator()
  2064. {
  2065. return $this->generatorType !== self::GENERATOR_TYPE_NONE;
  2066. }
  2067. /** @return bool */
  2068. public function isInheritanceTypeNone()
  2069. {
  2070. return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
  2071. }
  2072. /**
  2073. * Checks whether the mapped class uses the JOINED inheritance mapping strategy.
  2074. *
  2075. * @return bool TRUE if the class participates in a JOINED inheritance mapping,
  2076. * FALSE otherwise.
  2077. */
  2078. public function isInheritanceTypeJoined()
  2079. {
  2080. return $this->inheritanceType === self::INHERITANCE_TYPE_JOINED;
  2081. }
  2082. /**
  2083. * Checks whether the mapped class uses the SINGLE_TABLE inheritance mapping strategy.
  2084. *
  2085. * @return bool TRUE if the class participates in a SINGLE_TABLE inheritance mapping,
  2086. * FALSE otherwise.
  2087. */
  2088. public function isInheritanceTypeSingleTable()
  2089. {
  2090. return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_TABLE;
  2091. }
  2092. /**
  2093. * Checks whether the mapped class uses the TABLE_PER_CLASS inheritance mapping strategy.
  2094. *
  2095. * @return bool TRUE if the class participates in a TABLE_PER_CLASS inheritance mapping,
  2096. * FALSE otherwise.
  2097. */
  2098. public function isInheritanceTypeTablePerClass()
  2099. {
  2100. return $this->inheritanceType === self::INHERITANCE_TYPE_TABLE_PER_CLASS;
  2101. }
  2102. /**
  2103. * Checks whether the class uses an identity column for the Id generation.
  2104. *
  2105. * @return bool TRUE if the class uses the IDENTITY generator, FALSE otherwise.
  2106. */
  2107. public function isIdGeneratorIdentity()
  2108. {
  2109. return $this->generatorType === self::GENERATOR_TYPE_IDENTITY;
  2110. }
  2111. /**
  2112. * Checks whether the class uses a sequence for id generation.
  2113. *
  2114. * @return bool TRUE if the class uses the SEQUENCE generator, FALSE otherwise.
  2115. *
  2116. * @psalm-assert-if-true !null $this->sequenceGeneratorDefinition
  2117. */
  2118. public function isIdGeneratorSequence()
  2119. {
  2120. return $this->generatorType === self::GENERATOR_TYPE_SEQUENCE;
  2121. }
  2122. /**
  2123. * Checks whether the class uses a table for id generation.
  2124. *
  2125. * @deprecated
  2126. *
  2127. * @return false
  2128. */
  2129. public function isIdGeneratorTable()
  2130. {
  2131. Deprecation::trigger(
  2132. 'doctrine/orm',
  2133. 'https://github.com/doctrine/orm/pull/9046',
  2134. '%s is deprecated',
  2135. __METHOD__
  2136. );
  2137. return false;
  2138. }
  2139. /**
  2140. * Checks whether the class has a natural identifier/pk (which means it does
  2141. * not use any Id generator.
  2142. *
  2143. * @return bool
  2144. */
  2145. public function isIdentifierNatural()
  2146. {
  2147. return $this->generatorType === self::GENERATOR_TYPE_NONE;
  2148. }
  2149. /**
  2150. * Checks whether the class use a UUID for id generation.
  2151. *
  2152. * @deprecated
  2153. *
  2154. * @return bool
  2155. */
  2156. public function isIdentifierUuid()
  2157. {
  2158. Deprecation::trigger(
  2159. 'doctrine/orm',
  2160. 'https://github.com/doctrine/orm/pull/9046',
  2161. '%s is deprecated',
  2162. __METHOD__
  2163. );
  2164. return $this->generatorType === self::GENERATOR_TYPE_UUID;
  2165. }
  2166. /**
  2167. * Gets the type of a field.
  2168. *
  2169. * @param string $fieldName
  2170. *
  2171. * @return string|null
  2172. *
  2173. * @todo 3.0 Remove this. PersisterHelper should fix it somehow
  2174. */
  2175. public function getTypeOfField($fieldName)
  2176. {
  2177. return isset($this->fieldMappings[$fieldName])
  2178. ? $this->fieldMappings[$fieldName]['type']
  2179. : null;
  2180. }
  2181. /**
  2182. * Gets the type of a column.
  2183. *
  2184. * @deprecated 3.0 remove this. this method is bogus and unreliable, since it cannot resolve the type of a column
  2185. * that is derived by a referenced field on a different entity.
  2186. *
  2187. * @param string $columnName
  2188. *
  2189. * @return string|null
  2190. */
  2191. public function getTypeOfColumn($columnName)
  2192. {
  2193. return $this->getTypeOfField($this->getFieldName($columnName));
  2194. }
  2195. /**
  2196. * Gets the name of the primary table.
  2197. *
  2198. * @return string
  2199. */
  2200. public function getTableName()
  2201. {
  2202. return $this->table['name'];
  2203. }
  2204. /**
  2205. * Gets primary table's schema name.
  2206. *
  2207. * @return string|null
  2208. */
  2209. public function getSchemaName()
  2210. {
  2211. return $this->table['schema'] ?? null;
  2212. }
  2213. /**
  2214. * Gets the table name to use for temporary identifier tables of this class.
  2215. *
  2216. * @return string
  2217. */
  2218. public function getTemporaryIdTableName()
  2219. {
  2220. // replace dots with underscores because PostgreSQL creates temporary tables in a special schema
  2221. return str_replace('.', '_', $this->getTableName() . '_id_tmp');
  2222. }
  2223. /**
  2224. * Sets the mapped subclasses of this class.
  2225. *
  2226. * @psalm-param list<string> $subclasses The names of all mapped subclasses.
  2227. *
  2228. * @return void
  2229. */
  2230. public function setSubclasses(array $subclasses)
  2231. {
  2232. foreach ($subclasses as $subclass) {
  2233. $this->subClasses[] = $this->fullyQualifiedClassName($subclass);
  2234. }
  2235. }
  2236. /**
  2237. * Sets the parent class names.
  2238. * Assumes that the class names in the passed array are in the order:
  2239. * directParent -> directParentParent -> directParentParentParent ... -> root.
  2240. *
  2241. * @psalm-param list<class-string> $classNames
  2242. *
  2243. * @return void
  2244. */
  2245. public function setParentClasses(array $classNames)
  2246. {
  2247. $this->parentClasses = $classNames;
  2248. if (count($classNames) > 0) {
  2249. $this->rootEntityName = array_pop($classNames);
  2250. }
  2251. }
  2252. /**
  2253. * Sets the inheritance type used by the class and its subclasses.
  2254. *
  2255. * @param int $type
  2256. * @psalm-param self::INHERITANCE_TYPE_* $type
  2257. *
  2258. * @return void
  2259. *
  2260. * @throws MappingException
  2261. */
  2262. public function setInheritanceType($type)
  2263. {
  2264. if (! $this->isInheritanceType($type)) {
  2265. throw MappingException::invalidInheritanceType($this->name, $type);
  2266. }
  2267. $this->inheritanceType = $type;
  2268. }
  2269. /**
  2270. * Sets the association to override association mapping of property for an entity relationship.
  2271. *
  2272. * @param string $fieldName
  2273. * @psalm-param array<string, mixed> $overrideMapping
  2274. *
  2275. * @return void
  2276. *
  2277. * @throws MappingException
  2278. */
  2279. public function setAssociationOverride($fieldName, array $overrideMapping)
  2280. {
  2281. if (! isset($this->associationMappings[$fieldName])) {
  2282. throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
  2283. }
  2284. $mapping = $this->associationMappings[$fieldName];
  2285. //if (isset($mapping['inherited']) && (count($overrideMapping) !== 1 || ! isset($overrideMapping['fetch']))) {
  2286. // TODO: Deprecate overriding the fetch mode via association override for 3.0,
  2287. // users should do this with a listener and a custom attribute/annotation
  2288. // TODO: Enable this exception in 2.8
  2289. //throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
  2290. //}
  2291. if (isset($overrideMapping['joinColumns'])) {
  2292. $mapping['joinColumns'] = $overrideMapping['joinColumns'];
  2293. }
  2294. if (isset($overrideMapping['inversedBy'])) {
  2295. $mapping['inversedBy'] = $overrideMapping['inversedBy'];
  2296. }
  2297. if (isset($overrideMapping['joinTable'])) {
  2298. $mapping['joinTable'] = $overrideMapping['joinTable'];
  2299. }
  2300. if (isset($overrideMapping['fetch'])) {
  2301. $mapping['fetch'] = $overrideMapping['fetch'];
  2302. }
  2303. $mapping['joinColumnFieldNames'] = null;
  2304. $mapping['joinTableColumns'] = null;
  2305. $mapping['sourceToTargetKeyColumns'] = null;
  2306. $mapping['relationToSourceKeyColumns'] = null;
  2307. $mapping['relationToTargetKeyColumns'] = null;
  2308. switch ($mapping['type']) {
  2309. case self::ONE_TO_ONE:
  2310. $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
  2311. break;
  2312. case self::ONE_TO_MANY:
  2313. $mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
  2314. break;
  2315. case self::MANY_TO_ONE:
  2316. $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
  2317. break;
  2318. case self::MANY_TO_MANY:
  2319. $mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
  2320. break;
  2321. }
  2322. $this->associationMappings[$fieldName] = $mapping;
  2323. }
  2324. /**
  2325. * Sets the override for a mapped field.
  2326. *
  2327. * @param string $fieldName
  2328. * @psalm-param array<string, mixed> $overrideMapping
  2329. *
  2330. * @return void
  2331. *
  2332. * @throws MappingException
  2333. */
  2334. public function setAttributeOverride($fieldName, array $overrideMapping)
  2335. {
  2336. if (! isset($this->fieldMappings[$fieldName])) {
  2337. throw MappingException::invalidOverrideFieldName($this->name, $fieldName);
  2338. }
  2339. $mapping = $this->fieldMappings[$fieldName];
  2340. //if (isset($mapping['inherited'])) {
  2341. // TODO: Enable this exception in 2.8
  2342. //throw MappingException::illegalOverrideOfInheritedProperty($this->name, $fieldName);
  2343. //}
  2344. if (isset($mapping['id'])) {
  2345. $overrideMapping['id'] = $mapping['id'];
  2346. }
  2347. if (! isset($overrideMapping['type'])) {
  2348. $overrideMapping['type'] = $mapping['type'];
  2349. }
  2350. if (! isset($overrideMapping['fieldName'])) {
  2351. $overrideMapping['fieldName'] = $mapping['fieldName'];
  2352. }
  2353. if ($overrideMapping['type'] !== $mapping['type']) {
  2354. throw MappingException::invalidOverrideFieldType($this->name, $fieldName);
  2355. }
  2356. unset($this->fieldMappings[$fieldName]);
  2357. unset($this->fieldNames[$mapping['columnName']]);
  2358. unset($this->columnNames[$mapping['fieldName']]);
  2359. $overrideMapping = $this->validateAndCompleteFieldMapping($overrideMapping);
  2360. $this->fieldMappings[$fieldName] = $overrideMapping;
  2361. }
  2362. /**
  2363. * Checks whether a mapped field is inherited from an entity superclass.
  2364. *
  2365. * @param string $fieldName
  2366. *
  2367. * @return bool TRUE if the field is inherited, FALSE otherwise.
  2368. */
  2369. public function isInheritedField($fieldName)
  2370. {
  2371. return isset($this->fieldMappings[$fieldName]['inherited']);
  2372. }
  2373. /**
  2374. * Checks if this entity is the root in any entity-inheritance-hierarchy.
  2375. *
  2376. * @return bool
  2377. */
  2378. public function isRootEntity()
  2379. {
  2380. return $this->name === $this->rootEntityName;
  2381. }
  2382. /**
  2383. * Checks whether a mapped association field is inherited from a superclass.
  2384. *
  2385. * @param string $fieldName
  2386. *
  2387. * @return bool TRUE if the field is inherited, FALSE otherwise.
  2388. */
  2389. public function isInheritedAssociation($fieldName)
  2390. {
  2391. return isset($this->associationMappings[$fieldName]['inherited']);
  2392. }
  2393. /**
  2394. * @param string $fieldName
  2395. *
  2396. * @return bool
  2397. */
  2398. public function isInheritedEmbeddedClass($fieldName)
  2399. {
  2400. return isset($this->embeddedClasses[$fieldName]['inherited']);
  2401. }
  2402. /**
  2403. * Sets the name of the primary table the class is mapped to.
  2404. *
  2405. * @deprecated Use {@link setPrimaryTable}.
  2406. *
  2407. * @param string $tableName The table name.
  2408. *
  2409. * @return void
  2410. */
  2411. public function setTableName($tableName)
  2412. {
  2413. $this->table['name'] = $tableName;
  2414. }
  2415. /**
  2416. * Sets the primary table definition. The provided array supports the
  2417. * following structure:
  2418. *
  2419. * name => <tableName> (optional, defaults to class name)
  2420. * indexes => array of indexes (optional)
  2421. * uniqueConstraints => array of constraints (optional)
  2422. *
  2423. * If a key is omitted, the current value is kept.
  2424. *
  2425. * @psalm-param array<string, mixed> $table The table description.
  2426. *
  2427. * @return void
  2428. */
  2429. public function setPrimaryTable(array $table)
  2430. {
  2431. if (isset($table['name'])) {
  2432. // Split schema and table name from a table name like "myschema.mytable"
  2433. if (str_contains($table['name'], '.')) {
  2434. [$this->table['schema'], $table['name']] = explode('.', $table['name'], 2);
  2435. }
  2436. if ($table['name'][0] === '`') {
  2437. $table['name'] = trim($table['name'], '`');
  2438. $this->table['quoted'] = true;
  2439. }
  2440. $this->table['name'] = $table['name'];
  2441. }
  2442. if (isset($table['quoted'])) {
  2443. $this->table['quoted'] = $table['quoted'];
  2444. }
  2445. if (isset($table['schema'])) {
  2446. $this->table['schema'] = $table['schema'];
  2447. }
  2448. if (isset($table['indexes'])) {
  2449. $this->table['indexes'] = $table['indexes'];
  2450. }
  2451. if (isset($table['uniqueConstraints'])) {
  2452. $this->table['uniqueConstraints'] = $table['uniqueConstraints'];
  2453. }
  2454. if (isset($table['options'])) {
  2455. $this->table['options'] = $table['options'];
  2456. }
  2457. }
  2458. /**
  2459. * Checks whether the given type identifies an inheritance type.
  2460. *
  2461. * @return bool TRUE if the given type identifies an inheritance type, FALSE otherwise.
  2462. */
  2463. private function isInheritanceType(int $type): bool
  2464. {
  2465. return $type === self::INHERITANCE_TYPE_NONE ||
  2466. $type === self::INHERITANCE_TYPE_SINGLE_TABLE ||
  2467. $type === self::INHERITANCE_TYPE_JOINED ||
  2468. $type === self::INHERITANCE_TYPE_TABLE_PER_CLASS;
  2469. }
  2470. /**
  2471. * Adds a mapped field to the class.
  2472. *
  2473. * @psalm-param array<string, mixed> $mapping The field mapping.
  2474. *
  2475. * @return void
  2476. *
  2477. * @throws MappingException
  2478. */
  2479. public function mapField(array $mapping)
  2480. {
  2481. $mapping = $this->validateAndCompleteFieldMapping($mapping);
  2482. $this->assertFieldNotMapped($mapping['fieldName']);
  2483. if (isset($mapping['generated'])) {
  2484. $this->requiresFetchAfterChange = true;
  2485. }
  2486. $this->fieldMappings[$mapping['fieldName']] = $mapping;
  2487. }
  2488. /**
  2489. * INTERNAL:
  2490. * Adds an association mapping without completing/validating it.
  2491. * This is mainly used to add inherited association mappings to derived classes.
  2492. *
  2493. * @psalm-param AssociationMapping $mapping
  2494. *
  2495. * @return void
  2496. *
  2497. * @throws MappingException
  2498. */
  2499. public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/)
  2500. {
  2501. if (isset($this->associationMappings[$mapping['fieldName']])) {
  2502. throw MappingException::duplicateAssociationMapping($this->name, $mapping['fieldName']);
  2503. }
  2504. $this->associationMappings[$mapping['fieldName']] = $mapping;
  2505. }
  2506. /**
  2507. * INTERNAL:
  2508. * Adds a field mapping without completing/validating it.
  2509. * This is mainly used to add inherited field mappings to derived classes.
  2510. *
  2511. * @psalm-param array<string, mixed> $fieldMapping
  2512. *
  2513. * @return void
  2514. */
  2515. public function addInheritedFieldMapping(array $fieldMapping)
  2516. {
  2517. $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
  2518. $this->columnNames[$fieldMapping['fieldName']] = $fieldMapping['columnName'];
  2519. $this->fieldNames[$fieldMapping['columnName']] = $fieldMapping['fieldName'];
  2520. }
  2521. /**
  2522. * INTERNAL:
  2523. * Adds a named query to this class.
  2524. *
  2525. * @deprecated
  2526. *
  2527. * @psalm-param array<string, mixed> $queryMapping
  2528. *
  2529. * @return void
  2530. *
  2531. * @throws MappingException
  2532. */
  2533. public function addNamedQuery(array $queryMapping)
  2534. {
  2535. if (! isset($queryMapping['name'])) {
  2536. throw MappingException::nameIsMandatoryForQueryMapping($this->name);
  2537. }
  2538. Deprecation::trigger(
  2539. 'doctrine/orm',
  2540. 'https://github.com/doctrine/orm/issues/8592',
  2541. 'Named Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository',
  2542. $queryMapping['name'],
  2543. $this->name
  2544. );
  2545. if (isset($this->namedQueries[$queryMapping['name']])) {
  2546. throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']);
  2547. }
  2548. if (! isset($queryMapping['query'])) {
  2549. throw MappingException::emptyQueryMapping($this->name, $queryMapping['name']);
  2550. }
  2551. $name = $queryMapping['name'];
  2552. $query = $queryMapping['query'];
  2553. $dql = str_replace('__CLASS__', $this->name, $query);
  2554. $this->namedQueries[$name] = [
  2555. 'name' => $name,
  2556. 'query' => $query,
  2557. 'dql' => $dql,
  2558. ];
  2559. }
  2560. /**
  2561. * INTERNAL:
  2562. * Adds a named native query to this class.
  2563. *
  2564. * @deprecated
  2565. *
  2566. * @psalm-param array<string, mixed> $queryMapping
  2567. *
  2568. * @return void
  2569. *
  2570. * @throws MappingException
  2571. */
  2572. public function addNamedNativeQuery(array $queryMapping)
  2573. {
  2574. if (! isset($queryMapping['name'])) {
  2575. throw MappingException::nameIsMandatoryForQueryMapping($this->name);
  2576. }
  2577. Deprecation::trigger(
  2578. 'doctrine/orm',
  2579. 'https://github.com/doctrine/orm/issues/8592',
  2580. 'Named Native Queries are deprecated, here "%s" on entity %s. Move the query logic into EntityRepository',
  2581. $queryMapping['name'],
  2582. $this->name
  2583. );
  2584. if (isset($this->namedNativeQueries[$queryMapping['name']])) {
  2585. throw MappingException::duplicateQueryMapping($this->name, $queryMapping['name']);
  2586. }
  2587. if (! isset($queryMapping['query'])) {
  2588. throw MappingException::emptyQueryMapping($this->name, $queryMapping['name']);
  2589. }
  2590. if (! isset($queryMapping['resultClass']) && ! isset($queryMapping['resultSetMapping'])) {
  2591. throw MappingException::missingQueryMapping($this->name, $queryMapping['name']);
  2592. }
  2593. $queryMapping['isSelfClass'] = false;
  2594. if (isset($queryMapping['resultClass'])) {
  2595. if ($queryMapping['resultClass'] === '__CLASS__') {
  2596. $queryMapping['isSelfClass'] = true;
  2597. $queryMapping['resultClass'] = $this->name;
  2598. }
  2599. $queryMapping['resultClass'] = $this->fullyQualifiedClassName($queryMapping['resultClass']);
  2600. $queryMapping['resultClass'] = ltrim($queryMapping['resultClass'], '\\');
  2601. }
  2602. $this->namedNativeQueries[$queryMapping['name']] = $queryMapping;
  2603. }
  2604. /**
  2605. * INTERNAL:
  2606. * Adds a sql result set mapping to this class.
  2607. *
  2608. * @psalm-param array<string, mixed> $resultMapping
  2609. *
  2610. * @return void
  2611. *
  2612. * @throws MappingException
  2613. */
  2614. public function addSqlResultSetMapping(array $resultMapping)
  2615. {
  2616. if (! isset($resultMapping['name'])) {
  2617. throw MappingException::nameIsMandatoryForSqlResultSetMapping($this->name);
  2618. }
  2619. if (isset($this->sqlResultSetMappings[$resultMapping['name']])) {
  2620. throw MappingException::duplicateResultSetMapping($this->name, $resultMapping['name']);
  2621. }
  2622. if (isset($resultMapping['entities'])) {
  2623. foreach ($resultMapping['entities'] as $key => $entityResult) {
  2624. if (! isset($entityResult['entityClass'])) {
  2625. throw MappingException::missingResultSetMappingEntity($this->name, $resultMapping['name']);
  2626. }
  2627. $entityResult['isSelfClass'] = false;
  2628. if ($entityResult['entityClass'] === '__CLASS__') {
  2629. $entityResult['isSelfClass'] = true;
  2630. $entityResult['entityClass'] = $this->name;
  2631. }
  2632. $entityResult['entityClass'] = $this->fullyQualifiedClassName($entityResult['entityClass']);
  2633. $resultMapping['entities'][$key]['entityClass'] = ltrim($entityResult['entityClass'], '\\');
  2634. $resultMapping['entities'][$key]['isSelfClass'] = $entityResult['isSelfClass'];
  2635. if (isset($entityResult['fields'])) {
  2636. foreach ($entityResult['fields'] as $k => $field) {
  2637. if (! isset($field['name'])) {
  2638. throw MappingException::missingResultSetMappingFieldName($this->name, $resultMapping['name']);
  2639. }
  2640. if (! isset($field['column'])) {
  2641. $fieldName = $field['name'];
  2642. if (str_contains($fieldName, '.')) {
  2643. [, $fieldName] = explode('.', $fieldName);
  2644. }
  2645. $resultMapping['entities'][$key]['fields'][$k]['column'] = $fieldName;
  2646. }
  2647. }
  2648. }
  2649. }
  2650. }
  2651. $this->sqlResultSetMappings[$resultMapping['name']] = $resultMapping;
  2652. }
  2653. /**
  2654. * Adds a one-to-one mapping.
  2655. *
  2656. * @param array<string, mixed> $mapping The mapping.
  2657. *
  2658. * @return void
  2659. */
  2660. public function mapOneToOne(array $mapping)
  2661. {
  2662. $mapping['type'] = self::ONE_TO_ONE;
  2663. $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
  2664. $this->_storeAssociationMapping($mapping);
  2665. }
  2666. /**
  2667. * Adds a one-to-many mapping.
  2668. *
  2669. * @psalm-param array<string, mixed> $mapping The mapping.
  2670. *
  2671. * @return void
  2672. */
  2673. public function mapOneToMany(array $mapping)
  2674. {
  2675. $mapping['type'] = self::ONE_TO_MANY;
  2676. $mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
  2677. $this->_storeAssociationMapping($mapping);
  2678. }
  2679. /**
  2680. * Adds a many-to-one mapping.
  2681. *
  2682. * @psalm-param array<string, mixed> $mapping The mapping.
  2683. *
  2684. * @return void
  2685. */
  2686. public function mapManyToOne(array $mapping)
  2687. {
  2688. $mapping['type'] = self::MANY_TO_ONE;
  2689. // A many-to-one mapping is essentially a one-one backreference
  2690. $mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
  2691. $this->_storeAssociationMapping($mapping);
  2692. }
  2693. /**
  2694. * Adds a many-to-many mapping.
  2695. *
  2696. * @psalm-param array<string, mixed> $mapping The mapping.
  2697. *
  2698. * @return void
  2699. */
  2700. public function mapManyToMany(array $mapping)
  2701. {
  2702. $mapping['type'] = self::MANY_TO_MANY;
  2703. $mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
  2704. $this->_storeAssociationMapping($mapping);
  2705. }
  2706. /**
  2707. * Stores the association mapping.
  2708. *
  2709. * @psalm-param array<string, mixed> $assocMapping
  2710. *
  2711. * @return void
  2712. *
  2713. * @throws MappingException
  2714. */
  2715. protected function _storeAssociationMapping(array $assocMapping)
  2716. {
  2717. $sourceFieldName = $assocMapping['fieldName'];
  2718. $this->assertFieldNotMapped($sourceFieldName);
  2719. $this->associationMappings[$sourceFieldName] = $assocMapping;
  2720. }
  2721. /**
  2722. * Registers a custom repository class for the entity class.
  2723. *
  2724. * @param string|null $repositoryClassName The class name of the custom mapper.
  2725. * @psalm-param class-string<EntityRepository>|null $repositoryClassName
  2726. *
  2727. * @return void
  2728. */
  2729. public function setCustomRepositoryClass($repositoryClassName)
  2730. {
  2731. $this->customRepositoryClassName = $this->fullyQualifiedClassName($repositoryClassName);
  2732. }
  2733. /**
  2734. * Dispatches the lifecycle event of the given entity to the registered
  2735. * lifecycle callbacks and lifecycle listeners.
  2736. *
  2737. * @deprecated Deprecated since version 2.4 in favor of \Doctrine\ORM\Event\ListenersInvoker
  2738. *
  2739. * @param string $lifecycleEvent The lifecycle event.
  2740. * @param object $entity The Entity on which the event occurred.
  2741. *
  2742. * @return void
  2743. */
  2744. public function invokeLifecycleCallbacks($lifecycleEvent, $entity)
  2745. {
  2746. foreach ($this->lifecycleCallbacks[$lifecycleEvent] as $callback) {
  2747. $entity->$callback();
  2748. }
  2749. }
  2750. /**
  2751. * Whether the class has any attached lifecycle listeners or callbacks for a lifecycle event.
  2752. *
  2753. * @param string $lifecycleEvent
  2754. *
  2755. * @return bool
  2756. */
  2757. public function hasLifecycleCallbacks($lifecycleEvent)
  2758. {
  2759. return isset($this->lifecycleCallbacks[$lifecycleEvent]);
  2760. }
  2761. /**
  2762. * Gets the registered lifecycle callbacks for an event.
  2763. *
  2764. * @param string $event
  2765. *
  2766. * @return string[]
  2767. * @psalm-return list<string>
  2768. */
  2769. public function getLifecycleCallbacks($event)
  2770. {
  2771. return $this->lifecycleCallbacks[$event] ?? [];
  2772. }
  2773. /**
  2774. * Adds a lifecycle callback for entities of this class.
  2775. *
  2776. * @param string $callback
  2777. * @param string $event
  2778. *
  2779. * @return void
  2780. */
  2781. public function addLifecycleCallback($callback, $event)
  2782. {
  2783. if ($this->isEmbeddedClass) {
  2784. Deprecation::trigger(
  2785. 'doctrine/orm',
  2786. 'https://github.com/doctrine/orm/pull/8381',
  2787. 'Registering lifecycle callback %s on Embedded class %s is not doing anything and will throw exception in 3.0',
  2788. $event,
  2789. $this->name
  2790. );
  2791. }
  2792. if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event], true)) {
  2793. return;
  2794. }
  2795. $this->lifecycleCallbacks[$event][] = $callback;
  2796. }
  2797. /**
  2798. * Sets the lifecycle callbacks for entities of this class.
  2799. * Any previously registered callbacks are overwritten.
  2800. *
  2801. * @psalm-param array<string, list<string>> $callbacks
  2802. *
  2803. * @return void
  2804. */
  2805. public function setLifecycleCallbacks(array $callbacks)
  2806. {
  2807. $this->lifecycleCallbacks = $callbacks;
  2808. }
  2809. /**
  2810. * Adds a entity listener for entities of this class.
  2811. *
  2812. * @param string $eventName The entity lifecycle event.
  2813. * @param string $class The listener class.
  2814. * @param string $method The listener callback method.
  2815. *
  2816. * @return void
  2817. *
  2818. * @throws MappingException
  2819. */
  2820. public function addEntityListener($eventName, $class, $method)
  2821. {
  2822. $class = $this->fullyQualifiedClassName($class);
  2823. $listener = [
  2824. 'class' => $class,
  2825. 'method' => $method,
  2826. ];
  2827. if (! class_exists($class)) {
  2828. throw MappingException::entityListenerClassNotFound($class, $this->name);
  2829. }
  2830. if (! method_exists($class, $method)) {
  2831. throw MappingException::entityListenerMethodNotFound($class, $method, $this->name);
  2832. }
  2833. if (isset($this->entityListeners[$eventName]) && in_array($listener, $this->entityListeners[$eventName], true)) {
  2834. throw MappingException::duplicateEntityListener($class, $method, $this->name);
  2835. }
  2836. $this->entityListeners[$eventName][] = $listener;
  2837. }
  2838. /**
  2839. * Sets the discriminator column definition.
  2840. *
  2841. * @see getDiscriminatorColumn()
  2842. *
  2843. * @param mixed[]|null $columnDef
  2844. * @psalm-param array{name: string|null, fieldName?: string, type?: string, length?: int, columnDefinition?: string|null, enumType?: class-string<BackedEnum>|null}|null $columnDef
  2845. *
  2846. * @return void
  2847. *
  2848. * @throws MappingException
  2849. */
  2850. public function setDiscriminatorColumn($columnDef)
  2851. {
  2852. if ($columnDef !== null) {
  2853. if (! isset($columnDef['name'])) {
  2854. throw MappingException::nameIsMandatoryForDiscriminatorColumns($this->name);
  2855. }
  2856. if (isset($this->fieldNames[$columnDef['name']])) {
  2857. throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
  2858. }
  2859. if (! isset($columnDef['fieldName'])) {
  2860. $columnDef['fieldName'] = $columnDef['name'];
  2861. }
  2862. if (! isset($columnDef['type'])) {
  2863. $columnDef['type'] = 'string';
  2864. }
  2865. if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
  2866. throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
  2867. }
  2868. $this->discriminatorColumn = $columnDef;
  2869. }
  2870. }
  2871. /**
  2872. * @return array<string, mixed>
  2873. * @psalm-return DiscriminatorColumnMapping
  2874. */
  2875. final public function getDiscriminatorColumn(): array
  2876. {
  2877. if ($this->discriminatorColumn === null) {
  2878. throw new LogicException('The discriminator column was not set.');
  2879. }
  2880. return $this->discriminatorColumn;
  2881. }
  2882. /**
  2883. * Sets the discriminator values used by this class.
  2884. * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
  2885. *
  2886. * @param array<int|string, string> $map
  2887. *
  2888. * @return void
  2889. */
  2890. public function setDiscriminatorMap(array $map)
  2891. {
  2892. foreach ($map as $value => $className) {
  2893. $this->addDiscriminatorMapClass($value, $className);
  2894. }
  2895. }
  2896. /**
  2897. * Adds one entry of the discriminator map with a new class and corresponding name.
  2898. *
  2899. * @param int|string $name
  2900. * @param string $className
  2901. *
  2902. * @return void
  2903. *
  2904. * @throws MappingException
  2905. */
  2906. public function addDiscriminatorMapClass($name, $className)
  2907. {
  2908. $className = $this->fullyQualifiedClassName($className);
  2909. $className = ltrim($className, '\\');
  2910. $this->discriminatorMap[$name] = $className;
  2911. if ($this->name === $className) {
  2912. $this->discriminatorValue = $name;
  2913. return;
  2914. }
  2915. if (! (class_exists($className) || interface_exists($className))) {
  2916. throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
  2917. }
  2918. $this->addSubClass($className);
  2919. }
  2920. /** @param array<class-string> $classes */
  2921. public function addSubClasses(array $classes): void
  2922. {
  2923. foreach ($classes as $className) {
  2924. $this->addSubClass($className);
  2925. }
  2926. }
  2927. public function addSubClass(string $className): void
  2928. {
  2929. // By ignoring classes that are not subclasses of the current class, we simplify inheriting
  2930. // the subclass list from a parent class at the beginning of \Doctrine\ORM\Mapping\ClassMetadataFactory::doLoadMetadata.
  2931. if (is_subclass_of($className, $this->name) && ! in_array($className, $this->subClasses, true)) {
  2932. $this->subClasses[] = $className;
  2933. }
  2934. }
  2935. /**
  2936. * Checks whether the class has a named query with the given query name.
  2937. *
  2938. * @param string $queryName
  2939. *
  2940. * @return bool
  2941. */
  2942. public function hasNamedQuery($queryName)
  2943. {
  2944. return isset($this->namedQueries[$queryName]);
  2945. }
  2946. /**
  2947. * Checks whether the class has a named native query with the given query name.
  2948. *
  2949. * @param string $queryName
  2950. *
  2951. * @return bool
  2952. */
  2953. public function hasNamedNativeQuery($queryName)
  2954. {
  2955. return isset($this->namedNativeQueries[$queryName]);
  2956. }
  2957. /**
  2958. * Checks whether the class has a named native query with the given query name.
  2959. *
  2960. * @param string $name
  2961. *
  2962. * @return bool
  2963. */
  2964. public function hasSqlResultSetMapping($name)
  2965. {
  2966. return isset($this->sqlResultSetMappings[$name]);
  2967. }
  2968. /**
  2969. * {@inheritDoc}
  2970. */
  2971. public function hasAssociation($fieldName)
  2972. {
  2973. return isset($this->associationMappings[$fieldName]);
  2974. }
  2975. /**
  2976. * {@inheritDoc}
  2977. */
  2978. public function isSingleValuedAssociation($fieldName)
  2979. {
  2980. return isset($this->associationMappings[$fieldName])
  2981. && ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
  2982. }
  2983. /**
  2984. * {@inheritDoc}
  2985. */
  2986. public function isCollectionValuedAssociation($fieldName)
  2987. {
  2988. return isset($this->associationMappings[$fieldName])
  2989. && ! ($this->associationMappings[$fieldName]['type'] & self::TO_ONE);
  2990. }
  2991. /**
  2992. * Is this an association that only has a single join column?
  2993. *
  2994. * @param string $fieldName
  2995. *
  2996. * @return bool
  2997. */
  2998. public function isAssociationWithSingleJoinColumn($fieldName)
  2999. {
  3000. return isset($this->associationMappings[$fieldName])
  3001. && isset($this->associationMappings[$fieldName]['joinColumns'][0])
  3002. && ! isset($this->associationMappings[$fieldName]['joinColumns'][1]);
  3003. }
  3004. /**
  3005. * Returns the single association join column (if any).
  3006. *
  3007. * @param string $fieldName
  3008. *
  3009. * @return string
  3010. *
  3011. * @throws MappingException
  3012. */
  3013. public function getSingleAssociationJoinColumnName($fieldName)
  3014. {
  3015. if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
  3016. throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
  3017. }
  3018. return $this->associationMappings[$fieldName]['joinColumns'][0]['name'];
  3019. }
  3020. /**
  3021. * Returns the single association referenced join column name (if any).
  3022. *
  3023. * @param string $fieldName
  3024. *
  3025. * @return string
  3026. *
  3027. * @throws MappingException
  3028. */
  3029. public function getSingleAssociationReferencedJoinColumnName($fieldName)
  3030. {
  3031. if (! $this->isAssociationWithSingleJoinColumn($fieldName)) {
  3032. throw MappingException::noSingleAssociationJoinColumnFound($this->name, $fieldName);
  3033. }
  3034. return $this->associationMappings[$fieldName]['joinColumns'][0]['referencedColumnName'];
  3035. }
  3036. /**
  3037. * Used to retrieve a fieldname for either field or association from a given column.
  3038. *
  3039. * This method is used in foreign-key as primary-key contexts.
  3040. *
  3041. * @param string $columnName
  3042. *
  3043. * @return string
  3044. *
  3045. * @throws MappingException
  3046. */
  3047. public function getFieldForColumn($columnName)
  3048. {
  3049. if (isset($this->fieldNames[$columnName])) {
  3050. return $this->fieldNames[$columnName];
  3051. }
  3052. foreach ($this->associationMappings as $assocName => $mapping) {
  3053. if (
  3054. $this->isAssociationWithSingleJoinColumn($assocName) &&
  3055. $this->associationMappings[$assocName]['joinColumns'][0]['name'] === $columnName
  3056. ) {
  3057. return $assocName;
  3058. }
  3059. }
  3060. throw MappingException::noFieldNameFoundForColumn($this->name, $columnName);
  3061. }
  3062. /**
  3063. * Sets the ID generator used to generate IDs for instances of this class.
  3064. *
  3065. * @param AbstractIdGenerator $generator
  3066. *
  3067. * @return void
  3068. */
  3069. public function setIdGenerator($generator)
  3070. {
  3071. $this->idGenerator = $generator;
  3072. }
  3073. /**
  3074. * Sets definition.
  3075. *
  3076. * @psalm-param array<string, string|null> $definition
  3077. *
  3078. * @return void
  3079. */
  3080. public function setCustomGeneratorDefinition(array $definition)
  3081. {
  3082. $this->customGeneratorDefinition = $definition;
  3083. }
  3084. /**
  3085. * Sets the definition of the sequence ID generator for this class.
  3086. *
  3087. * The definition must have the following structure:
  3088. * <code>
  3089. * array(
  3090. * 'sequenceName' => 'name',
  3091. * 'allocationSize' => 20,
  3092. * 'initialValue' => 1
  3093. * 'quoted' => 1
  3094. * )
  3095. * </code>
  3096. *
  3097. * @psalm-param array{sequenceName?: string, allocationSize?: int|string, initialValue?: int|string, quoted?: mixed} $definition
  3098. *
  3099. * @return void
  3100. *
  3101. * @throws MappingException
  3102. */
  3103. public function setSequenceGeneratorDefinition(array $definition)
  3104. {
  3105. if (! isset($definition['sequenceName']) || trim($definition['sequenceName']) === '') {
  3106. throw MappingException::missingSequenceName($this->name);
  3107. }
  3108. if ($definition['sequenceName'][0] === '`') {
  3109. $definition['sequenceName'] = trim($definition['sequenceName'], '`');
  3110. $definition['quoted'] = true;
  3111. }
  3112. if (! isset($definition['allocationSize']) || trim((string) $definition['allocationSize']) === '') {
  3113. $definition['allocationSize'] = '1';
  3114. }
  3115. if (! isset($definition['initialValue']) || trim((string) $definition['initialValue']) === '') {
  3116. $definition['initialValue'] = '1';
  3117. }
  3118. $definition['allocationSize'] = (string) $definition['allocationSize'];
  3119. $definition['initialValue'] = (string) $definition['initialValue'];
  3120. $this->sequenceGeneratorDefinition = $definition;
  3121. }
  3122. /**
  3123. * Sets the version field mapping used for versioning. Sets the default
  3124. * value to use depending on the column type.
  3125. *
  3126. * @psalm-param array<string, mixed> $mapping The version field mapping array.
  3127. *
  3128. * @return void
  3129. *
  3130. * @throws MappingException
  3131. */
  3132. public function setVersionMapping(array &$mapping)
  3133. {
  3134. $this->isVersioned = true;
  3135. $this->versionField = $mapping['fieldName'];
  3136. $this->requiresFetchAfterChange = true;
  3137. if (! isset($mapping['default'])) {
  3138. if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
  3139. $mapping['default'] = 1;
  3140. } elseif ($mapping['type'] === 'datetime') {
  3141. $mapping['default'] = 'CURRENT_TIMESTAMP';
  3142. } else {
  3143. throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
  3144. }
  3145. }
  3146. }
  3147. /**
  3148. * Sets whether this class is to be versioned for optimistic locking.
  3149. *
  3150. * @param bool $bool
  3151. *
  3152. * @return void
  3153. */
  3154. public function setVersioned($bool)
  3155. {
  3156. $this->isVersioned = $bool;
  3157. if ($bool) {
  3158. $this->requiresFetchAfterChange = true;
  3159. }
  3160. }
  3161. /**
  3162. * Sets the name of the field that is to be used for versioning if this class is
  3163. * versioned for optimistic locking.
  3164. *
  3165. * @param string|null $versionField
  3166. *
  3167. * @return void
  3168. */
  3169. public function setVersionField($versionField)
  3170. {
  3171. $this->versionField = $versionField;
  3172. }
  3173. /**
  3174. * Marks this class as read only, no change tracking is applied to it.
  3175. *
  3176. * @return void
  3177. */
  3178. public function markReadOnly()
  3179. {
  3180. $this->isReadOnly = true;
  3181. }
  3182. /**
  3183. * {@inheritDoc}
  3184. */
  3185. public function getFieldNames()
  3186. {
  3187. return array_keys($this->fieldMappings);
  3188. }
  3189. /**
  3190. * {@inheritDoc}
  3191. */
  3192. public function getAssociationNames()
  3193. {
  3194. return array_keys($this->associationMappings);
  3195. }
  3196. /**
  3197. * {@inheritDoc}
  3198. *
  3199. * @param string $assocName
  3200. *
  3201. * @return string
  3202. * @psalm-return class-string
  3203. *
  3204. * @throws InvalidArgumentException
  3205. */
  3206. public function getAssociationTargetClass($assocName)
  3207. {
  3208. if (! isset($this->associationMappings[$assocName])) {
  3209. throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
  3210. }
  3211. return $this->associationMappings[$assocName]['targetEntity'];
  3212. }
  3213. /**
  3214. * {@inheritDoc}
  3215. */
  3216. public function getName()
  3217. {
  3218. return $this->name;
  3219. }
  3220. /**
  3221. * Gets the (possibly quoted) identifier column names for safe use in an SQL statement.
  3222. *
  3223. * @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
  3224. *
  3225. * @param AbstractPlatform $platform
  3226. *
  3227. * @return string[]
  3228. * @psalm-return list<string>
  3229. */
  3230. public function getQuotedIdentifierColumnNames($platform)
  3231. {
  3232. $quotedColumnNames = [];
  3233. foreach ($this->identifier as $idProperty) {
  3234. if (isset($this->fieldMappings[$idProperty])) {
  3235. $quotedColumnNames[] = isset($this->fieldMappings[$idProperty]['quoted'])
  3236. ? $platform->quoteIdentifier($this->fieldMappings[$idProperty]['columnName'])
  3237. : $this->fieldMappings[$idProperty]['columnName'];
  3238. continue;
  3239. }
  3240. // Association defined as Id field
  3241. $joinColumns = $this->associationMappings[$idProperty]['joinColumns'];
  3242. $assocQuotedColumnNames = array_map(
  3243. static function ($joinColumn) use ($platform) {
  3244. return isset($joinColumn['quoted'])
  3245. ? $platform->quoteIdentifier($joinColumn['name'])
  3246. : $joinColumn['name'];
  3247. },
  3248. $joinColumns
  3249. );
  3250. $quotedColumnNames = array_merge($quotedColumnNames, $assocQuotedColumnNames);
  3251. }
  3252. return $quotedColumnNames;
  3253. }
  3254. /**
  3255. * Gets the (possibly quoted) column name of a mapped field for safe use in an SQL statement.
  3256. *
  3257. * @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
  3258. *
  3259. * @param string $field
  3260. * @param AbstractPlatform $platform
  3261. *
  3262. * @return string
  3263. */
  3264. public function getQuotedColumnName($field, $platform)
  3265. {
  3266. return isset($this->fieldMappings[$field]['quoted'])
  3267. ? $platform->quoteIdentifier($this->fieldMappings[$field]['columnName'])
  3268. : $this->fieldMappings[$field]['columnName'];
  3269. }
  3270. /**
  3271. * Gets the (possibly quoted) primary table name of this class for safe use in an SQL statement.
  3272. *
  3273. * @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
  3274. *
  3275. * @param AbstractPlatform $platform
  3276. *
  3277. * @return string
  3278. */
  3279. public function getQuotedTableName($platform)
  3280. {
  3281. return isset($this->table['quoted'])
  3282. ? $platform->quoteIdentifier($this->table['name'])
  3283. : $this->table['name'];
  3284. }
  3285. /**
  3286. * Gets the (possibly quoted) name of the join table.
  3287. *
  3288. * @deprecated Deprecated since version 2.3 in favor of \Doctrine\ORM\Mapping\QuoteStrategy
  3289. *
  3290. * @param mixed[] $assoc
  3291. * @param AbstractPlatform $platform
  3292. *
  3293. * @return string
  3294. */
  3295. public function getQuotedJoinTableName(array $assoc, $platform)
  3296. {
  3297. return isset($assoc['joinTable']['quoted'])
  3298. ? $platform->quoteIdentifier($assoc['joinTable']['name'])
  3299. : $assoc['joinTable']['name'];
  3300. }
  3301. /**
  3302. * {@inheritDoc}
  3303. */
  3304. public function isAssociationInverseSide($fieldName)
  3305. {
  3306. return isset($this->associationMappings[$fieldName])
  3307. && ! $this->associationMappings[$fieldName]['isOwningSide'];
  3308. }
  3309. /**
  3310. * {@inheritDoc}
  3311. */
  3312. public function getAssociationMappedByTargetField($fieldName)
  3313. {
  3314. return $this->associationMappings[$fieldName]['mappedBy'];
  3315. }
  3316. /**
  3317. * @param string $targetClass
  3318. *
  3319. * @return mixed[][]
  3320. * @psalm-return array<string, array<string, mixed>>
  3321. */
  3322. public function getAssociationsByTargetClass($targetClass)
  3323. {
  3324. $relations = [];
  3325. foreach ($this->associationMappings as $mapping) {
  3326. if ($mapping['targetEntity'] === $targetClass) {
  3327. $relations[$mapping['fieldName']] = $mapping;
  3328. }
  3329. }
  3330. return $relations;
  3331. }
  3332. /**
  3333. * @param string|null $className
  3334. *
  3335. * @return string|null null if the input value is null
  3336. * @psalm-return class-string|null
  3337. */
  3338. public function fullyQualifiedClassName($className)
  3339. {
  3340. if (empty($className)) {
  3341. return $className;
  3342. }
  3343. if (! str_contains($className, '\\') && $this->namespace) {
  3344. return $this->namespace . '\\' . $className;
  3345. }
  3346. return $className;
  3347. }
  3348. /**
  3349. * @param string $name
  3350. *
  3351. * @return mixed
  3352. */
  3353. public function getMetadataValue($name)
  3354. {
  3355. if (isset($this->$name)) {
  3356. return $this->$name;
  3357. }
  3358. return null;
  3359. }
  3360. /**
  3361. * Map Embedded Class
  3362. *
  3363. * @psalm-param array<string, mixed> $mapping
  3364. *
  3365. * @return void
  3366. *
  3367. * @throws MappingException
  3368. */
  3369. public function mapEmbedded(array $mapping)
  3370. {
  3371. $this->assertFieldNotMapped($mapping['fieldName']);
  3372. if (! isset($mapping['class']) && $this->isTypedProperty($mapping['fieldName'])) {
  3373. $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
  3374. if ($type instanceof ReflectionNamedType) {
  3375. $mapping['class'] = $type->getName();
  3376. }
  3377. }
  3378. $this->embeddedClasses[$mapping['fieldName']] = [
  3379. 'class' => $this->fullyQualifiedClassName($mapping['class']),
  3380. 'columnPrefix' => $mapping['columnPrefix'] ?? null,
  3381. 'declaredField' => $mapping['declaredField'] ?? null,
  3382. 'originalField' => $mapping['originalField'] ?? null,
  3383. ];
  3384. }
  3385. /**
  3386. * Inline the embeddable class
  3387. *
  3388. * @param string $property
  3389. *
  3390. * @return void
  3391. */
  3392. public function inlineEmbeddable($property, ClassMetadataInfo $embeddable)
  3393. {
  3394. foreach ($embeddable->fieldMappings as $fieldMapping) {
  3395. $fieldMapping['originalClass'] = $fieldMapping['originalClass'] ?? $embeddable->name;
  3396. $fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
  3397. ? $property . '.' . $fieldMapping['declaredField']
  3398. : $property;
  3399. $fieldMapping['originalField'] = $fieldMapping['originalField'] ?? $fieldMapping['fieldName'];
  3400. $fieldMapping['fieldName'] = $property . '.' . $fieldMapping['fieldName'];
  3401. if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
  3402. $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
  3403. } elseif ($this->embeddedClasses[$property]['columnPrefix'] !== false) {
  3404. $fieldMapping['columnName'] = $this->namingStrategy
  3405. ->embeddedFieldToColumnName(
  3406. $property,
  3407. $fieldMapping['columnName'],
  3408. $this->reflClass->name,
  3409. $embeddable->reflClass->name
  3410. );
  3411. }
  3412. $this->mapField($fieldMapping);
  3413. }
  3414. }
  3415. /** @throws MappingException */
  3416. private function assertFieldNotMapped(string $fieldName): void
  3417. {
  3418. if (
  3419. isset($this->fieldMappings[$fieldName]) ||
  3420. isset($this->associationMappings[$fieldName]) ||
  3421. isset($this->embeddedClasses[$fieldName])
  3422. ) {
  3423. throw MappingException::duplicateFieldMapping($this->name, $fieldName);
  3424. }
  3425. }
  3426. /**
  3427. * Gets the sequence name based on class metadata.
  3428. *
  3429. * @return string
  3430. *
  3431. * @todo Sequence names should be computed in DBAL depending on the platform
  3432. */
  3433. public function getSequenceName(AbstractPlatform $platform)
  3434. {
  3435. $sequencePrefix = $this->getSequencePrefix($platform);
  3436. $columnName = $this->getSingleIdentifierColumnName();
  3437. return $sequencePrefix . '_' . $columnName . '_seq';
  3438. }
  3439. /**
  3440. * Gets the sequence name prefix based on class metadata.
  3441. *
  3442. * @return string
  3443. *
  3444. * @todo Sequence names should be computed in DBAL depending on the platform
  3445. */
  3446. public function getSequencePrefix(AbstractPlatform $platform)
  3447. {
  3448. $tableName = $this->getTableName();
  3449. $sequencePrefix = $tableName;
  3450. // Prepend the schema name to the table name if there is one
  3451. $schemaName = $this->getSchemaName();
  3452. if ($schemaName) {
  3453. $sequencePrefix = $schemaName . '.' . $tableName;
  3454. if (! $platform->supportsSchemas() && $platform->canEmulateSchemas()) {
  3455. $sequencePrefix = $schemaName . '__' . $tableName;
  3456. }
  3457. }
  3458. return $sequencePrefix;
  3459. }
  3460. /** @psalm-param array<string, mixed> $mapping */
  3461. private function assertMappingOrderBy(array $mapping): void
  3462. {
  3463. if (isset($mapping['orderBy']) && ! is_array($mapping['orderBy'])) {
  3464. throw new InvalidArgumentException("'orderBy' is expected to be an array, not " . gettype($mapping['orderBy']));
  3465. }
  3466. }
  3467. /** @psalm-param class-string $class */
  3468. private function getAccessibleProperty(ReflectionService $reflService, string $class, string $field): ?ReflectionProperty
  3469. {
  3470. $reflectionProperty = $reflService->getAccessibleProperty($class, $field);
  3471. if ($reflectionProperty !== null && PHP_VERSION_ID >= 80100 && $reflectionProperty->isReadOnly()) {
  3472. $reflectionProperty = new ReflectionReadonlyProperty($reflectionProperty);
  3473. }
  3474. return $reflectionProperty;
  3475. }
  3476. }