vendor/doctrine/mongodb-odm/lib/Doctrine/ODM/MongoDB/Mapping/ClassMetadata.php line 2479

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace Doctrine\ODM\MongoDB\Mapping;
  4. use BackedEnum;
  5. use BadMethodCallException;
  6. use DateTime;
  7. use DateTimeImmutable;
  8. use Doctrine\Common\Collections\Collection;
  9. use Doctrine\Instantiator\Instantiator;
  10. use Doctrine\Instantiator\InstantiatorInterface;
  11. use Doctrine\ODM\MongoDB\Id\IdGenerator;
  12. use Doctrine\ODM\MongoDB\LockException;
  13. use Doctrine\ODM\MongoDB\Mapping\Annotations\TimeSeries;
  14. use Doctrine\ODM\MongoDB\Proxy\InternalProxy;
  15. use Doctrine\ODM\MongoDB\Types\Incrementable;
  16. use Doctrine\ODM\MongoDB\Types\Type;
  17. use Doctrine\ODM\MongoDB\Types\Versionable;
  18. use Doctrine\ODM\MongoDB\Utility\CollectionHelper;
  19. use Doctrine\Persistence\Mapping\ClassMetadata as BaseClassMetadata;
  20. use Doctrine\Persistence\Mapping\ReflectionService;
  21. use Doctrine\Persistence\Mapping\RuntimeReflectionService;
  22. use Doctrine\Persistence\Reflection\EnumReflectionProperty;
  23. use InvalidArgumentException;
  24. use LogicException;
  25. use ProxyManager\Proxy\GhostObjectInterface;
  26. use ReflectionClass;
  27. use ReflectionEnum;
  28. use ReflectionNamedType;
  29. use ReflectionProperty;
  30. use function array_filter;
  31. use function array_key_exists;
  32. use function array_keys;
  33. use function array_map;
  34. use function array_pop;
  35. use function assert;
  36. use function class_exists;
  37. use function constant;
  38. use function count;
  39. use function enum_exists;
  40. use function extension_loaded;
  41. use function in_array;
  42. use function interface_exists;
  43. use function is_array;
  44. use function is_string;
  45. use function is_subclass_of;
  46. use function ltrim;
  47. use function sprintf;
  48. use function strtolower;
  49. use function strtoupper;
  50. use function trigger_deprecation;
  51. /**
  52. * A <tt>ClassMetadata</tt> instance holds all the object-document mapping metadata
  53. * of a document and it's references.
  54. *
  55. * Once populated, ClassMetadata instances are usually cached in a serialized form.
  56. *
  57. * <b>IMPORTANT NOTE:</b>
  58. *
  59. * The fields of this class are only public for 2 reasons:
  60. * 1) To allow fast READ access.
  61. * 2) To drastically reduce the size of a serialized instance (private/protected members
  62. * get the whole class name, namespace inclusive, prepended to every property in
  63. * the serialized representation).
  64. *
  65. * @phpstan-type FieldMappingConfig array{
  66. * type?: string,
  67. * fieldName?: string,
  68. * name?: string,
  69. * strategy?: string,
  70. * association?: int,
  71. * id?: bool,
  72. * isOwningSide?: bool,
  73. * collectionClass?: class-string,
  74. * cascade?: list<string>|string,
  75. * embedded?: bool,
  76. * orphanRemoval?: bool,
  77. * options?: array<string, mixed>,
  78. * nullable?: bool,
  79. * reference?: bool,
  80. * storeAs?: string,
  81. * targetDocument?: class-string|null,
  82. * mappedBy?: string|null,
  83. * inversedBy?: string|null,
  84. * discriminatorField?: string,
  85. * defaultDiscriminatorValue?: string,
  86. * discriminatorMap?: array<string, class-string>,
  87. * repositoryMethod?: string|null,
  88. * sort?: array<string, string|int>,
  89. * limit?: int|null,
  90. * skip?: int|null,
  91. * version?: bool,
  92. * lock?: bool,
  93. * inherited?: string,
  94. * declared?: class-string,
  95. * prime?: list<string>,
  96. * sparse?: bool,
  97. * unique?: bool,
  98. * index?: bool,
  99. * index-name?: string,
  100. * criteria?: array<string, mixed>,
  101. * alsoLoadFields?: list<string>,
  102. * order?: int|string,
  103. * background?: bool,
  104. * enumType?: class-string<BackedEnum>,
  105. * }
  106. * @phpstan-type FieldMapping array{
  107. * type: string,
  108. * fieldName: string,
  109. * name: string,
  110. * isCascadeRemove: bool,
  111. * isCascadePersist: bool,
  112. * isCascadeRefresh: bool,
  113. * isCascadeMerge: bool,
  114. * isCascadeDetach: bool,
  115. * isOwningSide: bool,
  116. * isInverseSide: bool,
  117. * strategy?: string,
  118. * association?: int,
  119. * id?: bool,
  120. * collectionClass?: class-string,
  121. * cascade?: list<string>|string,
  122. * embedded?: bool,
  123. * orphanRemoval?: bool,
  124. * options?: array<string, mixed>,
  125. * nullable?: bool,
  126. * reference?: bool,
  127. * storeAs?: string,
  128. * targetDocument?: class-string|null,
  129. * mappedBy?: string|null,
  130. * inversedBy?: string|null,
  131. * discriminatorField?: string,
  132. * defaultDiscriminatorValue?: string,
  133. * discriminatorMap?: array<string, class-string>,
  134. * repositoryMethod?: string|null,
  135. * sort?: array<string, string|int>,
  136. * limit?: int|null,
  137. * skip?: int|null,
  138. * version?: bool,
  139. * lock?: bool,
  140. * notSaved?: bool,
  141. * inherited?: string,
  142. * declared?: class-string,
  143. * prime?: list<string>,
  144. * sparse?: bool,
  145. * unique?: bool,
  146. * index?: bool,
  147. * criteria?: array<string, mixed>,
  148. * alsoLoadFields?: list<string>,
  149. * enumType?: class-string<BackedEnum>,
  150. * storeEmptyArray?: bool,
  151. * }
  152. * @phpstan-type AssociationFieldMapping array{
  153. * type?: string,
  154. * fieldName: string,
  155. * name: string,
  156. * isCascadeRemove: bool,
  157. * isCascadePersist: bool,
  158. * isCascadeRefresh: bool,
  159. * isCascadeMerge: bool,
  160. * isCascadeDetach: bool,
  161. * isOwningSide: bool,
  162. * isInverseSide: bool,
  163. * targetDocument: class-string|null,
  164. * association: int,
  165. * strategy?: string,
  166. * id?: bool,
  167. * collectionClass?: class-string,
  168. * cascade?: list<string>|string,
  169. * embedded?: bool,
  170. * orphanRemoval?: bool,
  171. * options?: array<string, mixed>,
  172. * nullable?: bool,
  173. * reference?: bool,
  174. * storeAs?: string,
  175. * mappedBy?: string|null,
  176. * inversedBy?: string|null,
  177. * discriminatorField?: string,
  178. * defaultDiscriminatorValue?: string,
  179. * discriminatorMap?: array<string, class-string>,
  180. * repositoryMethod?: string|null,
  181. * sort?: array<string, string|int>,
  182. * limit?: int|null,
  183. * skip?: int|null,
  184. * version?: bool,
  185. * lock?: bool,
  186. * notSaved?: bool,
  187. * inherited?: string,
  188. * declared?: class-string,
  189. * prime?: list<string>,
  190. * sparse?: bool,
  191. * unique?: bool,
  192. * index?: bool,
  193. * criteria?: array<string, mixed>,
  194. * alsoLoadFields?: list<string>,
  195. * storeEmptyArray?: bool,
  196. * }
  197. * @phpstan-type IndexKeys array<string, mixed>
  198. * @phpstan-type IndexOptions array{
  199. * background?: bool,
  200. * bits?: int,
  201. * default_language?: string,
  202. * expireAfterSeconds?: int,
  203. * language_override?: string,
  204. * min?: float,
  205. * max?: float,
  206. * name?: string,
  207. * partialFilterExpression?: mixed[],
  208. * sparse?: bool,
  209. * storageEngine?: mixed[],
  210. * textIndexVersion?: int,
  211. * unique?: bool,
  212. * weights?: array{string, int},
  213. * }
  214. * @phpstan-type IndexMapping array{
  215. * keys: IndexKeys,
  216. * options: IndexOptions
  217. * }
  218. * @phpstan-type SearchIndexStoredSourceInclude array{include: list<string>}
  219. * @phpstan-type SearchIndexStoredSourceExclude array{exclude: list<string>}
  220. * @phpstan-type SearchIndexStoredSource bool|SearchIndexStoredSourceInclude|SearchIndexStoredSourceExclude
  221. * @phpstan-type SearchIndexSynonym array{
  222. * analyzer: string,
  223. * name: string,
  224. * source: array{
  225. * collection: string,
  226. * },
  227. * }
  228. * @phpstan-type SearchIndexDefinition array{
  229. * mappings: array{
  230. * dynamic?: bool,
  231. * fields?: array,
  232. * },
  233. * analyzer?: string,
  234. * searchAnalyzer?: string,
  235. * analyzers?: array,
  236. * storedSource?: SearchIndexStoredSource,
  237. * synonyms?: list<SearchIndexSynonym>,
  238. * }
  239. * @phpstan-type SearchIndexMapping array{
  240. * name: string,
  241. * definition: SearchIndexDefinition
  242. * }
  243. * @phpstan-type ShardKeys array<string, mixed>
  244. * @phpstan-type ShardOptions array<string, mixed>
  245. * @phpstan-type ShardKey array{
  246. * keys?: ShardKeys,
  247. * options?: ShardOptions
  248. * }
  249. * @final
  250. * @template-covariant T of object
  251. * @template-implements BaseClassMetadata<T>
  252. */
  253. /* final */ class ClassMetadata implements BaseClassMetadata
  254. {
  255. /* The Id generator types. */
  256. /**
  257. * AUTO means Doctrine will automatically create a new \MongoDB\BSON\ObjectId instance for us.
  258. */
  259. public const GENERATOR_TYPE_AUTO = 1;
  260. /**
  261. * INCREMENT means a separate collection is used for maintaining and incrementing id generation.
  262. * Offers full portability.
  263. */
  264. public const GENERATOR_TYPE_INCREMENT = 2;
  265. /**
  266. * UUID means Doctrine will generate a uuid for us.
  267. */
  268. public const GENERATOR_TYPE_UUID = 3;
  269. /**
  270. * ALNUM means Doctrine will generate Alpha-numeric string identifiers, using the INCREMENT
  271. * generator to ensure identifier uniqueness
  272. */
  273. public const GENERATOR_TYPE_ALNUM = 4;
  274. /**
  275. * CUSTOM means Doctrine expect a class parameter. It will then try to initiate that class
  276. * and pass other options to the generator. It will throw an Exception if the class
  277. * does not exist or if an option was passed for that there is not setter in the new
  278. * generator class.
  279. *
  280. * The class will have to implement IdGenerator.
  281. */
  282. public const GENERATOR_TYPE_CUSTOM = 5;
  283. /**
  284. * NONE means Doctrine will not generate any id for us and you are responsible for manually
  285. * assigning an id.
  286. */
  287. public const GENERATOR_TYPE_NONE = 6;
  288. /**
  289. * Default discriminator field name.
  290. *
  291. * This is used for associations value for associations where a that do not define a "targetDocument" or
  292. * "discriminatorField" option in their mapping.
  293. */
  294. public const DEFAULT_DISCRIMINATOR_FIELD = '_doctrine_class_name';
  295. /**
  296. * Association types
  297. */
  298. public const REFERENCE_ONE = 1;
  299. public const REFERENCE_MANY = 2;
  300. public const EMBED_ONE = 3;
  301. public const EMBED_MANY = 4;
  302. /**
  303. * Mapping types
  304. */
  305. public const MANY = 'many';
  306. public const ONE = 'one';
  307. /**
  308. * The types of storeAs references
  309. */
  310. public const REFERENCE_STORE_AS_ID = 'id';
  311. public const REFERENCE_STORE_AS_DB_REF = 'dbRef';
  312. public const REFERENCE_STORE_AS_DB_REF_WITH_DB = 'dbRefWithDb';
  313. public const REFERENCE_STORE_AS_REF = 'ref';
  314. /**
  315. * Rejects any insert or update that violates the validation criteria.
  316. *
  317. * Value for collection schema validationAction.
  318. *
  319. * @see https://www.mongodb.com/docs/manual/core/schema-validation/handle-invalid-documents/#option-1--reject-invalid-documents
  320. */
  321. public const SCHEMA_VALIDATION_ACTION_ERROR = 'error';
  322. /**
  323. * MongoDB allows the operation to proceed, but records the violation in the MongoDB log.
  324. *
  325. * Value for collection schema validationAction.
  326. *
  327. * @see https://www.mongodb.com/docs/manual/core/schema-validation/handle-invalid-documents/#option-2--allow-invalid-documents--but-record-them-in-the-log
  328. */
  329. public const SCHEMA_VALIDATION_ACTION_WARN = 'warn';
  330. /**
  331. * Disable schema validation for the collection.
  332. *
  333. * Value of validationLevel.
  334. *
  335. * @see https://www.mongodb.com/docs/manual/core/schema-validation/specify-validation-level/
  336. */
  337. public const SCHEMA_VALIDATION_LEVEL_OFF = 'off';
  338. /**
  339. * MongoDB applies the same validation rules to all document inserts and updates.
  340. *
  341. * Value of validationLevel.
  342. *
  343. * @see https://www.mongodb.com/docs/manual/core/schema-validation/specify-validation-level/#steps--use-strict-validation
  344. */
  345. public const SCHEMA_VALIDATION_LEVEL_STRICT = 'strict';
  346. /**
  347. * MongoDB applies the same validation rules to document inserts and updates
  348. * to existing valid documents that match the validation rules. Updates to
  349. * existing documents in the collection that don't match the validation rules
  350. * aren't checked for validity.
  351. *
  352. * Value of validationLevel.
  353. *
  354. * @see https://www.mongodb.com/docs/manual/core/schema-validation/specify-validation-level/#steps--use-moderate-validation
  355. */
  356. public const SCHEMA_VALIDATION_LEVEL_MODERATE = 'moderate';
  357. /* The inheritance mapping types */
  358. /**
  359. * NONE means the class does not participate in an inheritance hierarchy
  360. * and therefore does not need an inheritance mapping type.
  361. */
  362. public const INHERITANCE_TYPE_NONE = 1;
  363. /**
  364. * SINGLE_COLLECTION means the class will be persisted according to the rules of
  365. * <tt>Single Collection Inheritance</tt>.
  366. */
  367. public const INHERITANCE_TYPE_SINGLE_COLLECTION = 2;
  368. /**
  369. * COLLECTION_PER_CLASS means the class will be persisted according to the rules
  370. * of <tt>Concrete Collection Inheritance</tt>.
  371. */
  372. public const INHERITANCE_TYPE_COLLECTION_PER_CLASS = 3;
  373. /**
  374. * DEFERRED_IMPLICIT means that changes of entities are calculated at commit-time
  375. * by doing a property-by-property comparison with the original data. This will
  376. * be done for all entities that are in MANAGED state at commit-time.
  377. *
  378. * This is the default change tracking policy.
  379. */
  380. public const CHANGETRACKING_DEFERRED_IMPLICIT = 1;
  381. /**
  382. * DEFERRED_EXPLICIT means that changes of entities are calculated at commit-time
  383. * by doing a property-by-property comparison with the original data. This will
  384. * be done only for entities that were explicitly saved (through persist() or a cascade).
  385. */
  386. public const CHANGETRACKING_DEFERRED_EXPLICIT = 2;
  387. /**
  388. * NOTIFY means that Doctrine relies on the entities sending out notifications
  389. * when their properties change. Such entity classes must implement
  390. * the <tt>NotifyPropertyChanged</tt> interface.
  391. *
  392. * @deprecated
  393. */
  394. public const CHANGETRACKING_NOTIFY = 3;
  395. /**
  396. * SET means that fields will be written to the database using a $set operator
  397. */
  398. public const STORAGE_STRATEGY_SET = 'set';
  399. /**
  400. * INCREMENT means that fields will be written to the database by calculating
  401. * the difference and using the $inc operator
  402. */
  403. public const STORAGE_STRATEGY_INCREMENT = 'increment';
  404. public const STORAGE_STRATEGY_PUSH_ALL = 'pushAll';
  405. public const STORAGE_STRATEGY_ADD_TO_SET = 'addToSet';
  406. public const STORAGE_STRATEGY_ATOMIC_SET = 'atomicSet';
  407. public const STORAGE_STRATEGY_ATOMIC_SET_ARRAY = 'atomicSetArray';
  408. public const STORAGE_STRATEGY_SET_ARRAY = 'setArray';
  409. /**
  410. * Default search index name.
  411. *
  412. * @see https://www.mongodb.com/docs/manual/reference/command/createSearchIndexes/
  413. */
  414. public const DEFAULT_SEARCH_INDEX_NAME = 'default';
  415. private const ALLOWED_GRIDFS_FIELDS = ['_id', 'chunkSize', 'filename', 'length', 'metadata', 'uploadDate'];
  416. /**
  417. * READ-ONLY: The name of the mongo database the document is mapped to.
  418. *
  419. * @var string|null
  420. */
  421. public $db;
  422. /**
  423. * READ-ONLY: The name of the mongo collection the document is mapped to.
  424. *
  425. * @var string
  426. */
  427. public $collection;
  428. /**
  429. * READ-ONLY: The name of the GridFS bucket the document is mapped to.
  430. *
  431. * @var string
  432. */
  433. public $bucketName = 'fs';
  434. /**
  435. * READ-ONLY: If the collection should be a fixed size.
  436. *
  437. * @var bool
  438. */
  439. public $collectionCapped = false;
  440. /**
  441. * READ-ONLY: If the collection is fixed size, its size in bytes.
  442. *
  443. * @var int|null
  444. */
  445. public $collectionSize;
  446. /**
  447. * READ-ONLY: If the collection is fixed size, the maximum number of elements to store in the collection.
  448. *
  449. * @var int|null
  450. */
  451. public $collectionMax;
  452. /**
  453. * READ-ONLY Describes how MongoDB clients route read operations to the members of a replica set.
  454. *
  455. * @var string|null
  456. */
  457. public $readPreference;
  458. /**
  459. * READ-ONLY Associated with readPreference Allows to specify criteria so that your application can target read
  460. * operations to specific members, based on custom parameters.
  461. *
  462. * @var array<array<string, string>>
  463. */
  464. public $readPreferenceTags = [];
  465. /**
  466. * READ-ONLY: Describes the level of acknowledgement requested from MongoDB for write operations.
  467. *
  468. * @var string|int|null
  469. */
  470. public $writeConcern;
  471. /**
  472. * READ-ONLY: The field name of the document identifier.
  473. *
  474. * @var string|null
  475. */
  476. public $identifier;
  477. /**
  478. * READ-ONLY: The array of indexes for the document collection.
  479. *
  480. * @var array<array<string, mixed>>
  481. * @phpstan-var array<IndexMapping>
  482. */
  483. public $indexes = [];
  484. /**
  485. * READ-ONLY: The array of search indexes for the document collection.
  486. *
  487. * @var list<SearchIndexMapping>
  488. */
  489. public $searchIndexes = [];
  490. /**
  491. * READ-ONLY: Keys and options describing shard key. Only for sharded collections.
  492. *
  493. * @var array<string, array>
  494. * @phpstan-var ShardKey
  495. */
  496. public $shardKey = [];
  497. /**
  498. * Allows users to specify a validation schema for the collection.
  499. *
  500. * @phpstan-var array<string, mixed>|object|null
  501. */
  502. private array|object|null $validator = null;
  503. /**
  504. * Determines whether to error on invalid documents or just warn about the violations but allow invalid documents to be inserted.
  505. */
  506. private string $validationAction = self::SCHEMA_VALIDATION_ACTION_ERROR;
  507. /**
  508. * Determines how strictly MongoDB applies the validation rules to existing documents during an update.
  509. */
  510. private string $validationLevel = self::SCHEMA_VALIDATION_LEVEL_STRICT;
  511. /**
  512. * READ-ONLY: The name of the document class.
  513. *
  514. * @var class-string<T>
  515. */
  516. public $name;
  517. /**
  518. * READ-ONLY: The name of the document class that is at the root of the mapped document inheritance
  519. * hierarchy. If the document is not part of a mapped inheritance hierarchy this is the same
  520. * as {@link $documentName}.
  521. *
  522. * @var class-string
  523. */
  524. public $rootDocumentName;
  525. /**
  526. * The name of the custom repository class used for the document class.
  527. * (Optional).
  528. *
  529. * @var class-string|null
  530. */
  531. public $customRepositoryClassName;
  532. /**
  533. * READ-ONLY: The names of the parent classes (ancestors).
  534. *
  535. * @var list<class-string>
  536. */
  537. public $parentClasses = [];
  538. /**
  539. * READ-ONLY: The names of all subclasses (descendants).
  540. *
  541. * @var list<class-string>
  542. */
  543. public $subClasses = [];
  544. /**
  545. * The ReflectionProperty instances of the mapped class.
  546. *
  547. * @var ReflectionProperty[]
  548. */
  549. public $reflFields = [];
  550. /**
  551. * READ-ONLY: The inheritance mapping type used by the class.
  552. *
  553. * @var int
  554. */
  555. public $inheritanceType = self::INHERITANCE_TYPE_NONE;
  556. /**
  557. * READ-ONLY: The Id generator type used by the class.
  558. *
  559. * @var int
  560. */
  561. public $generatorType = self::GENERATOR_TYPE_AUTO;
  562. /**
  563. * READ-ONLY: The Id generator options.
  564. *
  565. * @var array<string, mixed>
  566. */
  567. public $generatorOptions = [];
  568. /**
  569. * READ-ONLY: The ID generator used for generating IDs for this class.
  570. *
  571. * @var IdGenerator|null
  572. */
  573. public $idGenerator;
  574. /**
  575. * READ-ONLY: The field mappings of the class.
  576. * Keys are field names and values are mapping definitions.
  577. *
  578. * The mapping definition array has the following values:
  579. *
  580. * - <b>fieldName</b> (string)
  581. * The name of the field in the Document.
  582. *
  583. * - <b>id</b> (boolean, optional)
  584. * Marks the field as the primary key of the document. Multiple fields of an
  585. * document can have the id attribute, forming a composite key.
  586. *
  587. * @var array<string, mixed>
  588. * @phpstan-var array<string, FieldMapping>
  589. */
  590. public $fieldMappings = [];
  591. /**
  592. * READ-ONLY: The association mappings of the class.
  593. * Keys are field names and values are mapping definitions.
  594. *
  595. * @var array<string, mixed>
  596. * @phpstan-var array<string, AssociationFieldMapping>
  597. */
  598. public $associationMappings = [];
  599. /**
  600. * READ-ONLY: Array of fields to also load with a given method.
  601. *
  602. * @var array<string, mixed[]>
  603. */
  604. public $alsoLoadMethods = [];
  605. /**
  606. * READ-ONLY: The registered lifecycle callbacks for documents of this class.
  607. *
  608. * @var array<string, list<string>>
  609. */
  610. public $lifecycleCallbacks = [];
  611. /**
  612. * READ-ONLY: The discriminator value of this class.
  613. *
  614. * <b>This does only apply to the JOINED and SINGLE_COLLECTION inheritance mapping strategies
  615. * where a discriminator field is used.</b>
  616. *
  617. * @see discriminatorField
  618. *
  619. * @var class-string|null
  620. */
  621. public $discriminatorValue;
  622. /**
  623. * READ-ONLY: The discriminator map of all mapped classes in the hierarchy.
  624. *
  625. * <b>This does only apply to the SINGLE_COLLECTION inheritance mapping strategy
  626. * where a discriminator field is used.</b>
  627. *
  628. * @see discriminatorField
  629. *
  630. * @var array<string, class-string>
  631. */
  632. public $discriminatorMap = [];
  633. /**
  634. * READ-ONLY: The definition of the discriminator field used in SINGLE_COLLECTION
  635. * inheritance mapping.
  636. *
  637. * @var string|null
  638. */
  639. public $discriminatorField;
  640. /**
  641. * READ-ONLY: The default value for discriminatorField in case it's not set in the document
  642. *
  643. * @see discriminatorField
  644. *
  645. * @var string|null
  646. */
  647. public $defaultDiscriminatorValue;
  648. /**
  649. * READ-ONLY: Whether this class describes the mapping of a mapped superclass.
  650. *
  651. * @var bool
  652. */
  653. public $isMappedSuperclass = false;
  654. /**
  655. * READ-ONLY: Whether this class describes the mapping of a embedded document.
  656. *
  657. * @var bool
  658. */
  659. public $isEmbeddedDocument = false;
  660. /**
  661. * READ-ONLY: Whether this class describes the mapping of an aggregation result document.
  662. *
  663. * @var bool
  664. */
  665. public $isQueryResultDocument = false;
  666. /**
  667. * READ-ONLY: Whether this class describes the mapping of a database view.
  668. */
  669. private bool $isView = false;
  670. /**
  671. * READ-ONLY: Whether this class describes the mapping of a gridFS file
  672. *
  673. * @var bool
  674. */
  675. public $isFile = false;
  676. /**
  677. * READ-ONLY: The default chunk size in bytes for the file
  678. *
  679. * @var int|null
  680. */
  681. public $chunkSizeBytes;
  682. /**
  683. * READ-ONLY: The policy used for change-tracking on entities of this class.
  684. *
  685. * @var int
  686. */
  687. public $changeTrackingPolicy = self::CHANGETRACKING_DEFERRED_IMPLICIT;
  688. /**
  689. * READ-ONLY: A flag for whether or not instances of this class are to be versioned
  690. * with optimistic locking.
  691. *
  692. * @var bool $isVersioned
  693. */
  694. public $isVersioned = false;
  695. /**
  696. * READ-ONLY: The name of the field which is used for versioning in optimistic locking (if any).
  697. *
  698. * @var string|null $versionField
  699. */
  700. public $versionField;
  701. /**
  702. * READ-ONLY: A flag for whether or not instances of this class are to allow pessimistic
  703. * locking.
  704. *
  705. * @var bool $isLockable
  706. */
  707. public $isLockable = false;
  708. /**
  709. * READ-ONLY: The name of the field which is used for locking a document.
  710. *
  711. * @var mixed $lockField
  712. */
  713. public $lockField;
  714. /**
  715. * The ReflectionClass instance of the mapped class.
  716. *
  717. * @var ReflectionClass<T>
  718. */
  719. public $reflClass;
  720. /**
  721. * READ_ONLY: A flag for whether or not this document is read-only.
  722. *
  723. * @var bool
  724. */
  725. public $isReadOnly;
  726. /** READ ONLY: stores metadata about the time series collection */
  727. public ?TimeSeries $timeSeriesOptions = null;
  728. private InstantiatorInterface $instantiator;
  729. private ReflectionService $reflectionService;
  730. /** @var class-string|null */
  731. private ?string $rootClass;
  732. /**
  733. * Initializes a new ClassMetadata instance that will hold the object-document mapping
  734. * metadata of the class with the given name.
  735. *
  736. * @param class-string<T> $documentName
  737. */
  738. public function __construct(string $documentName)
  739. {
  740. $this->name = $documentName;
  741. $this->rootDocumentName = $documentName;
  742. $this->reflectionService = new RuntimeReflectionService();
  743. $this->reflClass = new ReflectionClass($documentName);
  744. $this->setCollection($this->reflClass->getShortName());
  745. $this->instantiator = new Instantiator();
  746. }
  747. /**
  748. * Helper method to get reference id of ref* type references
  749. *
  750. * @internal
  751. *
  752. * @param mixed $reference
  753. *
  754. * @return mixed
  755. */
  756. public static function getReferenceId($reference, string $storeAs)
  757. {
  758. return $storeAs === self::REFERENCE_STORE_AS_ID ? $reference : $reference[self::getReferencePrefix($storeAs) . 'id'];
  759. }
  760. /**
  761. * Returns the reference prefix used for a reference
  762. */
  763. private static function getReferencePrefix(string $storeAs): string
  764. {
  765. if (! in_array($storeAs, [self::REFERENCE_STORE_AS_REF, self::REFERENCE_STORE_AS_DB_REF, self::REFERENCE_STORE_AS_DB_REF_WITH_DB])) {
  766. throw new LogicException('Can only get a reference prefix for DBRef and reference arrays');
  767. }
  768. return $storeAs === self::REFERENCE_STORE_AS_REF ? '' : '$';
  769. }
  770. /**
  771. * Returns a fully qualified field name for a given reference
  772. *
  773. * @internal
  774. *
  775. * @param string $pathPrefix The field path prefix
  776. */
  777. public static function getReferenceFieldName(string $storeAs, string $pathPrefix = ''): string
  778. {
  779. if ($storeAs === self::REFERENCE_STORE_AS_ID) {
  780. return $pathPrefix;
  781. }
  782. return ($pathPrefix ? $pathPrefix . '.' : '') . static::getReferencePrefix($storeAs) . 'id';
  783. }
  784. public function getReflectionClass(): ReflectionClass
  785. {
  786. return $this->reflClass;
  787. }
  788. /** @param string $fieldName */
  789. public function isIdentifier($fieldName): bool
  790. {
  791. return $this->identifier === $fieldName;
  792. }
  793. /**
  794. * Sets the mapped identifier field of this class.
  795. *
  796. * @internal
  797. */
  798. public function setIdentifier(?string $identifier): void
  799. {
  800. $this->identifier = $identifier;
  801. }
  802. /**
  803. * Since MongoDB only allows exactly one identifier field
  804. * this will always return an array with only one value
  805. *
  806. * @return array<string|null>
  807. */
  808. public function getIdentifier(): array
  809. {
  810. return [$this->identifier];
  811. }
  812. /**
  813. * Since MongoDB only allows exactly one identifier field
  814. * this will always return an array with only one value
  815. *
  816. * @return array<string|null>
  817. */
  818. public function getIdentifierFieldNames(): array
  819. {
  820. return [$this->identifier];
  821. }
  822. /** @param string $fieldName */
  823. public function hasField($fieldName): bool
  824. {
  825. return isset($this->fieldMappings[$fieldName]);
  826. }
  827. /**
  828. * Sets the inheritance type used by the class and it's subclasses.
  829. */
  830. public function setInheritanceType(int $type): void
  831. {
  832. $this->inheritanceType = $type;
  833. }
  834. /**
  835. * Checks whether a mapped field is inherited from an entity superclass.
  836. */
  837. public function isInheritedField(string $fieldName): bool
  838. {
  839. return isset($this->fieldMappings[$fieldName]['inherited']);
  840. }
  841. /**
  842. * Registers a custom repository class for the document class.
  843. *
  844. * @param class-string|null $repositoryClassName
  845. */
  846. public function setCustomRepositoryClass(?string $repositoryClassName): void
  847. {
  848. if ($this->isEmbeddedDocument || $this->isQueryResultDocument) {
  849. return;
  850. }
  851. $this->customRepositoryClassName = $repositoryClassName;
  852. }
  853. /**
  854. * Dispatches the lifecycle event of the given document by invoking all
  855. * registered callbacks.
  856. *
  857. * @param mixed[]|null $arguments
  858. *
  859. * @throws InvalidArgumentException If document class is not this class or
  860. * a Proxy of this class.
  861. */
  862. public function invokeLifecycleCallbacks(string $event, object $document, ?array $arguments = null): void
  863. {
  864. if ($this->isView()) {
  865. return;
  866. }
  867. if (! $document instanceof $this->name) {
  868. throw new InvalidArgumentException(sprintf('Expected document class "%s"; found: "%s"', $this->name, $document::class));
  869. }
  870. if (empty($this->lifecycleCallbacks[$event])) {
  871. return;
  872. }
  873. foreach ($this->lifecycleCallbacks[$event] as $callback) {
  874. if ($arguments !== null) {
  875. $document->$callback(...$arguments);
  876. } else {
  877. $document->$callback();
  878. }
  879. }
  880. }
  881. /**
  882. * Checks whether the class has callbacks registered for a lifecycle event.
  883. */
  884. public function hasLifecycleCallbacks(string $event): bool
  885. {
  886. return ! empty($this->lifecycleCallbacks[$event]);
  887. }
  888. /**
  889. * Gets the registered lifecycle callbacks for an event.
  890. *
  891. * @return list<string>
  892. */
  893. public function getLifecycleCallbacks(string $event): array
  894. {
  895. return $this->lifecycleCallbacks[$event] ?? [];
  896. }
  897. /**
  898. * Adds a lifecycle callback for documents of this class.
  899. *
  900. * If the callback is already registered, this is a NOOP.
  901. */
  902. public function addLifecycleCallback(string $callback, string $event): void
  903. {
  904. if (isset($this->lifecycleCallbacks[$event]) && in_array($callback, $this->lifecycleCallbacks[$event])) {
  905. return;
  906. }
  907. $this->lifecycleCallbacks[$event][] = $callback;
  908. }
  909. /**
  910. * Sets the lifecycle callbacks for documents of this class.
  911. *
  912. * Any previously registered callbacks are overwritten.
  913. *
  914. * @param array<string, list<string>> $callbacks
  915. */
  916. public function setLifecycleCallbacks(array $callbacks): void
  917. {
  918. $this->lifecycleCallbacks = $callbacks;
  919. }
  920. /**
  921. * Registers a method for loading document data before field hydration.
  922. *
  923. * Note: A method may be registered multiple times for different fields.
  924. * it will be invoked only once for the first field found.
  925. *
  926. * @param array<string, mixed>|string $fields Database field name(s)
  927. */
  928. public function registerAlsoLoadMethod(string $method, $fields): void
  929. {
  930. $this->alsoLoadMethods[$method] = is_array($fields) ? $fields : [$fields];
  931. }
  932. /**
  933. * Sets the AlsoLoad methods for documents of this class.
  934. *
  935. * Any previously registered methods are overwritten.
  936. *
  937. * @param array<string, mixed[]> $methods
  938. */
  939. public function setAlsoLoadMethods(array $methods): void
  940. {
  941. $this->alsoLoadMethods = $methods;
  942. }
  943. /**
  944. * Sets the discriminator field.
  945. *
  946. * The field name is the the unmapped database field. Discriminator values
  947. * are only used to discern the hydration class and are not mapped to class
  948. * properties.
  949. *
  950. * @param array{name?: string, fieldName?: string}|string|null $discriminatorField
  951. *
  952. * @throws MappingException If the discriminator field conflicts with the
  953. * "name" attribute of a mapped field.
  954. */
  955. public function setDiscriminatorField($discriminatorField): void
  956. {
  957. if ($this->isFile) {
  958. throw MappingException::discriminatorNotAllowedForGridFS($this->name);
  959. }
  960. if ($discriminatorField === null) {
  961. $this->discriminatorField = null;
  962. return;
  963. }
  964. // @todo: deprecate, document and remove this:
  965. // Handle array argument with name/fieldName keys for BC
  966. if (is_array($discriminatorField)) {
  967. if (isset($discriminatorField['name'])) {
  968. $discriminatorField = $discriminatorField['name'];
  969. } elseif (isset($discriminatorField['fieldName'])) {
  970. $discriminatorField = $discriminatorField['fieldName'];
  971. }
  972. }
  973. foreach ($this->fieldMappings as $fieldMapping) {
  974. if ($discriminatorField === $fieldMapping['name']) {
  975. throw MappingException::discriminatorFieldConflict($this->name, $discriminatorField);
  976. }
  977. }
  978. $this->discriminatorField = $discriminatorField;
  979. }
  980. /**
  981. * Sets the discriminator values used by this class.
  982. * Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
  983. *
  984. * @param array<string, class-string> $map
  985. *
  986. * @throws MappingException
  987. */
  988. public function setDiscriminatorMap(array $map): void
  989. {
  990. if ($this->isFile) {
  991. throw MappingException::discriminatorNotAllowedForGridFS($this->name);
  992. }
  993. $this->subClasses = [];
  994. $this->discriminatorMap = [];
  995. $this->discriminatorValue = null;
  996. foreach ($map as $value => $className) {
  997. $this->discriminatorMap[$value] = $className;
  998. if ($this->name === $className) {
  999. $this->discriminatorValue = $value;
  1000. } else {
  1001. if (! class_exists($className)) {
  1002. throw MappingException::invalidClassInDiscriminatorMap($className, $this->name);
  1003. }
  1004. if (is_subclass_of($className, $this->name)) {
  1005. $this->subClasses[] = $className;
  1006. }
  1007. }
  1008. }
  1009. }
  1010. /**
  1011. * Sets the default discriminator value to be used for this class
  1012. * Used for SINGLE_TABLE inheritance mapping strategies if the document has no discriminator value
  1013. *
  1014. * @throws MappingException
  1015. */
  1016. public function setDefaultDiscriminatorValue(?string $defaultDiscriminatorValue): void
  1017. {
  1018. if ($this->isFile) {
  1019. throw MappingException::discriminatorNotAllowedForGridFS($this->name);
  1020. }
  1021. if ($defaultDiscriminatorValue === null) {
  1022. $this->defaultDiscriminatorValue = null;
  1023. return;
  1024. }
  1025. if (! array_key_exists($defaultDiscriminatorValue, $this->discriminatorMap)) {
  1026. throw MappingException::invalidDiscriminatorValue($defaultDiscriminatorValue, $this->name);
  1027. }
  1028. $this->defaultDiscriminatorValue = $defaultDiscriminatorValue;
  1029. }
  1030. /**
  1031. * Sets the discriminator value for this class.
  1032. * Used for JOINED/SINGLE_TABLE inheritance and multiple document types in a single
  1033. * collection.
  1034. *
  1035. * @throws MappingException
  1036. */
  1037. public function setDiscriminatorValue(string $value): void
  1038. {
  1039. if ($this->isFile) {
  1040. throw MappingException::discriminatorNotAllowedForGridFS($this->name);
  1041. }
  1042. $this->discriminatorMap[$value] = $this->name;
  1043. $this->discriminatorValue = $value;
  1044. }
  1045. /**
  1046. * Add a index for this Document.
  1047. *
  1048. * @param array<string, int|string> $keys
  1049. * @phpstan-param IndexKeys $keys
  1050. * @phpstan-param IndexOptions $options
  1051. */
  1052. public function addIndex(array $keys, array $options = []): void
  1053. {
  1054. $this->indexes[] = [
  1055. 'keys' => array_map(static function ($value) {
  1056. if ($value === 1 || $value === -1) {
  1057. return $value;
  1058. }
  1059. if (is_string($value)) {
  1060. $lower = strtolower($value);
  1061. if ($lower === 'asc') {
  1062. return 1;
  1063. }
  1064. if ($lower === 'desc') {
  1065. return -1;
  1066. }
  1067. }
  1068. return $value;
  1069. }, $keys),
  1070. 'options' => $options,
  1071. ];
  1072. }
  1073. /**
  1074. * Returns the array of indexes for this Document.
  1075. *
  1076. * @phpstan-return array<IndexMapping>
  1077. */
  1078. public function getIndexes(): array
  1079. {
  1080. return $this->indexes;
  1081. }
  1082. /**
  1083. * Checks whether this document has indexes or not.
  1084. */
  1085. public function hasIndexes(): bool
  1086. {
  1087. return $this->indexes !== [];
  1088. }
  1089. /**
  1090. * Add a search index for this Document.
  1091. *
  1092. * @phpstan-param SearchIndexDefinition $definition
  1093. */
  1094. public function addSearchIndex(array $definition, ?string $name = null): void
  1095. {
  1096. $name ??= self::DEFAULT_SEARCH_INDEX_NAME;
  1097. if (empty($definition['mappings']['dynamic']) && empty($definition['mappings']['fields'])) {
  1098. throw MappingException::emptySearchIndexDefinition($this->name, $name);
  1099. }
  1100. $this->searchIndexes[] = [
  1101. 'definition' => $definition,
  1102. 'name' => $name,
  1103. ];
  1104. }
  1105. /**
  1106. * Returns the array of search indexes for this Document.
  1107. *
  1108. * @phpstan-return list<SearchIndexMapping>
  1109. */
  1110. public function getSearchIndexes(): array
  1111. {
  1112. return $this->searchIndexes;
  1113. }
  1114. /**
  1115. * Checks whether this document has search indexes or not.
  1116. */
  1117. public function hasSearchIndexes(): bool
  1118. {
  1119. return $this->searchIndexes !== [];
  1120. }
  1121. /**
  1122. * Set shard key for this Document.
  1123. *
  1124. * @param array<string, string|int> $keys
  1125. * @param array<string, mixed> $options
  1126. * @phpstan-param ShardKeys $keys
  1127. * @phpstan-param ShardOptions $options
  1128. *
  1129. * @throws MappingException
  1130. */
  1131. public function setShardKey(array $keys, array $options = []): void
  1132. {
  1133. if ($this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION && $this->shardKey !== []) {
  1134. throw MappingException::shardKeyInSingleCollInheritanceSubclass($this->getName());
  1135. }
  1136. if ($this->isEmbeddedDocument) {
  1137. throw MappingException::embeddedDocumentCantHaveShardKey($this->getName());
  1138. }
  1139. foreach (array_keys($keys) as $field) {
  1140. if (! isset($this->fieldMappings[$field])) {
  1141. continue;
  1142. }
  1143. if (in_array($this->fieldMappings[$field]['type'], [self::MANY, Type::COLLECTION])) {
  1144. throw MappingException::noMultiKeyShardKeys($this->getName(), $field);
  1145. }
  1146. if ($this->fieldMappings[$field]['strategy'] !== self::STORAGE_STRATEGY_SET) {
  1147. throw MappingException::onlySetStrategyAllowedInShardKey($this->getName(), $field);
  1148. }
  1149. }
  1150. $this->shardKey = [
  1151. 'keys' => array_map(static function ($value) {
  1152. if ($value === 1 || $value === -1) {
  1153. return $value;
  1154. }
  1155. if (is_string($value)) {
  1156. $lower = strtolower($value);
  1157. if ($lower === 'asc') {
  1158. return 1;
  1159. }
  1160. if ($lower === 'desc') {
  1161. return -1;
  1162. }
  1163. }
  1164. return $value;
  1165. }, $keys),
  1166. 'options' => $options,
  1167. ];
  1168. }
  1169. /** @phpstan-return ShardKey */
  1170. public function getShardKey(): array
  1171. {
  1172. return $this->shardKey;
  1173. }
  1174. /**
  1175. * Checks whether this document has shard key or not.
  1176. */
  1177. public function isSharded(): bool
  1178. {
  1179. return $this->shardKey !== [];
  1180. }
  1181. /** @return array<string, mixed>|object|null */
  1182. public function getValidator()
  1183. {
  1184. return $this->validator;
  1185. }
  1186. /** @param array<string, mixed>|object|null $validator */
  1187. public function setValidator($validator): void
  1188. {
  1189. $this->validator = $validator;
  1190. }
  1191. public function getValidationAction(): string
  1192. {
  1193. return $this->validationAction;
  1194. }
  1195. public function setValidationAction(string $validationAction): void
  1196. {
  1197. $this->validationAction = $validationAction;
  1198. }
  1199. public function getValidationLevel(): string
  1200. {
  1201. return $this->validationLevel;
  1202. }
  1203. public function setValidationLevel(string $validationLevel): void
  1204. {
  1205. $this->validationLevel = $validationLevel;
  1206. }
  1207. /**
  1208. * Sets the read preference used by this class.
  1209. *
  1210. * @param array<array<string, string>> $tags
  1211. */
  1212. public function setReadPreference(?string $readPreference, array $tags): void
  1213. {
  1214. $this->readPreference = $readPreference;
  1215. $this->readPreferenceTags = $tags;
  1216. }
  1217. /**
  1218. * Sets the write concern used by this class.
  1219. *
  1220. * @param string|int|null $writeConcern
  1221. */
  1222. public function setWriteConcern($writeConcern): void
  1223. {
  1224. $this->writeConcern = $writeConcern;
  1225. }
  1226. /** @return int|string|null */
  1227. public function getWriteConcern()
  1228. {
  1229. return $this->writeConcern;
  1230. }
  1231. /**
  1232. * Whether there is a write concern configured for this class.
  1233. */
  1234. public function hasWriteConcern(): bool
  1235. {
  1236. return $this->writeConcern !== null;
  1237. }
  1238. /**
  1239. * Sets the change tracking policy used by this class.
  1240. */
  1241. public function setChangeTrackingPolicy(int $policy): void
  1242. {
  1243. $this->changeTrackingPolicy = $policy;
  1244. }
  1245. /**
  1246. * Whether the change tracking policy of this class is "deferred explicit".
  1247. */
  1248. public function isChangeTrackingDeferredExplicit(): bool
  1249. {
  1250. return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_EXPLICIT;
  1251. }
  1252. /**
  1253. * Whether the change tracking policy of this class is "deferred implicit".
  1254. */
  1255. public function isChangeTrackingDeferredImplicit(): bool
  1256. {
  1257. return $this->changeTrackingPolicy === self::CHANGETRACKING_DEFERRED_IMPLICIT;
  1258. }
  1259. /**
  1260. * Whether the change tracking policy of this class is "notify".
  1261. *
  1262. * @deprecated This method was deprecated in doctrine/mongodb-odm 2.4. Please use DEFERRED_EXPLICIT tracking
  1263. * policy and isChangeTrackingDeferredImplicit method to detect it.
  1264. */
  1265. public function isChangeTrackingNotify(): bool
  1266. {
  1267. return $this->changeTrackingPolicy === self::CHANGETRACKING_NOTIFY;
  1268. }
  1269. /**
  1270. * Gets the ReflectionProperties of the mapped class.
  1271. *
  1272. * @return ReflectionProperty[]
  1273. */
  1274. public function getReflectionProperties(): array
  1275. {
  1276. return $this->reflFields;
  1277. }
  1278. /**
  1279. * Gets a ReflectionProperty for a specific field of the mapped class.
  1280. */
  1281. public function getReflectionProperty(string $name): ReflectionProperty
  1282. {
  1283. return $this->reflFields[$name];
  1284. }
  1285. /** @return class-string<T> */
  1286. public function getName(): string
  1287. {
  1288. return $this->name;
  1289. }
  1290. /**
  1291. * Returns the database this Document is mapped to.
  1292. */
  1293. public function getDatabase(): ?string
  1294. {
  1295. return $this->db;
  1296. }
  1297. /**
  1298. * Set the database this Document is mapped to.
  1299. */
  1300. public function setDatabase(?string $db): void
  1301. {
  1302. $this->db = $db;
  1303. }
  1304. /**
  1305. * Get the collection this Document is mapped to.
  1306. */
  1307. public function getCollection(): string
  1308. {
  1309. return $this->collection;
  1310. }
  1311. /**
  1312. * Sets the collection this Document is mapped to.
  1313. *
  1314. * @param array{name: string, capped?: bool, size?: int, max?: int}|string $name
  1315. *
  1316. * @throws InvalidArgumentException
  1317. */
  1318. public function setCollection($name): void
  1319. {
  1320. if (is_array($name)) {
  1321. if (! isset($name['name'])) {
  1322. throw new InvalidArgumentException('A name key is required when passing an array to setCollection()');
  1323. }
  1324. $this->collectionCapped = $name['capped'] ?? false;
  1325. $this->collectionSize = $name['size'] ?? 0;
  1326. $this->collectionMax = $name['max'] ?? 0;
  1327. $this->collection = $name['name'];
  1328. } else {
  1329. $this->collection = $name;
  1330. }
  1331. }
  1332. public function getBucketName(): ?string
  1333. {
  1334. return $this->bucketName;
  1335. }
  1336. public function setBucketName(string $bucketName): void
  1337. {
  1338. $this->bucketName = $bucketName;
  1339. $this->setCollection($bucketName . '.files');
  1340. }
  1341. public function getChunkSizeBytes(): ?int
  1342. {
  1343. return $this->chunkSizeBytes;
  1344. }
  1345. public function setChunkSizeBytes(int $chunkSizeBytes): void
  1346. {
  1347. $this->chunkSizeBytes = $chunkSizeBytes;
  1348. }
  1349. /**
  1350. * Get whether or not the documents collection is capped.
  1351. */
  1352. public function getCollectionCapped(): bool
  1353. {
  1354. return $this->collectionCapped;
  1355. }
  1356. /**
  1357. * Set whether or not the documents collection is capped.
  1358. */
  1359. public function setCollectionCapped(bool $bool): void
  1360. {
  1361. $this->collectionCapped = $bool;
  1362. }
  1363. /**
  1364. * Get the collection size
  1365. */
  1366. public function getCollectionSize(): ?int
  1367. {
  1368. return $this->collectionSize;
  1369. }
  1370. /**
  1371. * Set the collection size.
  1372. */
  1373. public function setCollectionSize(int $size): void
  1374. {
  1375. $this->collectionSize = $size;
  1376. }
  1377. /**
  1378. * Get the collection max.
  1379. */
  1380. public function getCollectionMax(): ?int
  1381. {
  1382. return $this->collectionMax;
  1383. }
  1384. /**
  1385. * Set the collection max.
  1386. */
  1387. public function setCollectionMax(int $max): void
  1388. {
  1389. $this->collectionMax = $max;
  1390. }
  1391. /**
  1392. * Returns TRUE if this Document is mapped to a collection FALSE otherwise.
  1393. */
  1394. public function isMappedToCollection(): bool
  1395. {
  1396. return $this->collection !== '' && $this->collection !== null;
  1397. }
  1398. /**
  1399. * Validates the storage strategy of a mapping for consistency
  1400. *
  1401. * @phpstan-param FieldMappingConfig $mapping
  1402. *
  1403. * @throws MappingException
  1404. */
  1405. private function applyStorageStrategy(array &$mapping): void
  1406. {
  1407. if (! isset($mapping['type']) || isset($mapping['id'])) {
  1408. return;
  1409. }
  1410. switch (true) {
  1411. case $mapping['type'] === self::MANY:
  1412. $defaultStrategy = CollectionHelper::DEFAULT_STRATEGY;
  1413. $allowedStrategies = [
  1414. self::STORAGE_STRATEGY_PUSH_ALL,
  1415. self::STORAGE_STRATEGY_ADD_TO_SET,
  1416. self::STORAGE_STRATEGY_SET,
  1417. self::STORAGE_STRATEGY_SET_ARRAY,
  1418. self::STORAGE_STRATEGY_ATOMIC_SET,
  1419. self::STORAGE_STRATEGY_ATOMIC_SET_ARRAY,
  1420. ];
  1421. break;
  1422. case $mapping['type'] === self::ONE:
  1423. $defaultStrategy = self::STORAGE_STRATEGY_SET;
  1424. $allowedStrategies = [self::STORAGE_STRATEGY_SET];
  1425. break;
  1426. default:
  1427. $defaultStrategy = self::STORAGE_STRATEGY_SET;
  1428. $allowedStrategies = [self::STORAGE_STRATEGY_SET];
  1429. $type = Type::getType($mapping['type']);
  1430. if ($type instanceof Incrementable) {
  1431. $allowedStrategies[] = self::STORAGE_STRATEGY_INCREMENT;
  1432. }
  1433. }
  1434. if (! isset($mapping['strategy'])) {
  1435. $mapping['strategy'] = $defaultStrategy;
  1436. }
  1437. if (! in_array($mapping['strategy'], $allowedStrategies)) {
  1438. throw MappingException::invalidStorageStrategy($this->name, $mapping['fieldName'], $mapping['type'], $mapping['strategy']);
  1439. }
  1440. if (
  1441. isset($mapping['reference']) && $mapping['type'] === self::MANY && $mapping['isOwningSide']
  1442. && ! empty($mapping['sort']) && ! CollectionHelper::usesSet($mapping['strategy'])
  1443. ) {
  1444. throw MappingException::referenceManySortMustNotBeUsedWithNonSetCollectionStrategy($this->name, $mapping['fieldName'], $mapping['strategy']);
  1445. }
  1446. }
  1447. /**
  1448. * Map a single embedded document.
  1449. *
  1450. * @phpstan-param FieldMappingConfig $mapping
  1451. */
  1452. public function mapOneEmbedded(array $mapping): void
  1453. {
  1454. $mapping['embedded'] = true;
  1455. $mapping['type'] = self::ONE;
  1456. $this->mapField($mapping);
  1457. }
  1458. /**
  1459. * Map a collection of embedded documents.
  1460. *
  1461. * @phpstan-param FieldMappingConfig $mapping
  1462. */
  1463. public function mapManyEmbedded(array $mapping): void
  1464. {
  1465. $mapping['embedded'] = true;
  1466. $mapping['type'] = self::MANY;
  1467. $this->mapField($mapping);
  1468. }
  1469. /**
  1470. * Map a single document reference.
  1471. *
  1472. * @phpstan-param FieldMappingConfig $mapping
  1473. */
  1474. public function mapOneReference(array $mapping): void
  1475. {
  1476. $mapping['reference'] = true;
  1477. $mapping['type'] = self::ONE;
  1478. $this->mapField($mapping);
  1479. }
  1480. /**
  1481. * Map a collection of document references.
  1482. *
  1483. * @phpstan-param FieldMappingConfig $mapping
  1484. */
  1485. public function mapManyReference(array $mapping): void
  1486. {
  1487. $mapping['reference'] = true;
  1488. $mapping['type'] = self::MANY;
  1489. $this->mapField($mapping);
  1490. }
  1491. /**
  1492. * Adds a field mapping without completing/validating it.
  1493. * This is mainly used to add inherited field mappings to derived classes.
  1494. *
  1495. * @internal
  1496. *
  1497. * @phpstan-param FieldMapping $fieldMapping
  1498. */
  1499. public function addInheritedFieldMapping(array $fieldMapping): void
  1500. {
  1501. $this->fieldMappings[$fieldMapping['fieldName']] = $fieldMapping;
  1502. if (! isset($fieldMapping['association'])) {
  1503. return;
  1504. }
  1505. $this->associationMappings[$fieldMapping['fieldName']] = $fieldMapping;
  1506. }
  1507. /**
  1508. * Adds an association mapping without completing/validating it.
  1509. * This is mainly used to add inherited association mappings to derived classes.
  1510. *
  1511. * @internal
  1512. *
  1513. * @phpstan-param AssociationFieldMapping $mapping
  1514. *
  1515. * @throws MappingException
  1516. */
  1517. public function addInheritedAssociationMapping(array $mapping): void
  1518. {
  1519. $this->associationMappings[$mapping['fieldName']] = $mapping;
  1520. }
  1521. /**
  1522. * Checks whether the class has a mapped association with the given field name.
  1523. */
  1524. public function hasReference(string $fieldName): bool
  1525. {
  1526. return isset($this->fieldMappings[$fieldName]['reference']);
  1527. }
  1528. /**
  1529. * Checks whether the class has a mapped embed with the given field name.
  1530. */
  1531. public function hasEmbed(string $fieldName): bool
  1532. {
  1533. return isset($this->fieldMappings[$fieldName]['embedded']);
  1534. }
  1535. /**
  1536. * Checks whether the class has a mapped association (embed or reference) with the given field name.
  1537. *
  1538. * @param string $fieldName
  1539. */
  1540. public function hasAssociation($fieldName): bool
  1541. {
  1542. return $this->hasReference($fieldName) || $this->hasEmbed($fieldName);
  1543. }
  1544. /**
  1545. * Checks whether the class has a mapped reference or embed for the specified field and
  1546. * is a single valued association.
  1547. *
  1548. * @param string $fieldName
  1549. */
  1550. public function isSingleValuedAssociation($fieldName): bool
  1551. {
  1552. return $this->isSingleValuedReference($fieldName) || $this->isSingleValuedEmbed($fieldName);
  1553. }
  1554. /**
  1555. * Checks whether the class has a mapped reference or embed for the specified field and
  1556. * is a collection valued association.
  1557. *
  1558. * @param string $fieldName
  1559. */
  1560. public function isCollectionValuedAssociation($fieldName): bool
  1561. {
  1562. return $this->isCollectionValuedReference($fieldName) || $this->isCollectionValuedEmbed($fieldName);
  1563. }
  1564. /**
  1565. * Checks whether the class has a mapped association for the specified field
  1566. * and if yes, checks whether it is a single-valued association (to-one).
  1567. */
  1568. public function isSingleValuedReference(string $fieldName): bool
  1569. {
  1570. return isset($this->fieldMappings[$fieldName]['association']) &&
  1571. $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_ONE;
  1572. }
  1573. /**
  1574. * Checks whether the class has a mapped association for the specified field
  1575. * and if yes, checks whether it is a collection-valued association (to-many).
  1576. */
  1577. public function isCollectionValuedReference(string $fieldName): bool
  1578. {
  1579. return isset($this->fieldMappings[$fieldName]['association']) &&
  1580. $this->fieldMappings[$fieldName]['association'] === self::REFERENCE_MANY;
  1581. }
  1582. /**
  1583. * Checks whether the class has a mapped embedded document for the specified field
  1584. * and if yes, checks whether it is a single-valued association (to-one).
  1585. */
  1586. public function isSingleValuedEmbed(string $fieldName): bool
  1587. {
  1588. return isset($this->fieldMappings[$fieldName]['association']) &&
  1589. $this->fieldMappings[$fieldName]['association'] === self::EMBED_ONE;
  1590. }
  1591. /**
  1592. * Checks whether the class has a mapped embedded document for the specified field
  1593. * and if yes, checks whether it is a collection-valued association (to-many).
  1594. */
  1595. public function isCollectionValuedEmbed(string $fieldName): bool
  1596. {
  1597. return isset($this->fieldMappings[$fieldName]['association']) &&
  1598. $this->fieldMappings[$fieldName]['association'] === self::EMBED_MANY;
  1599. }
  1600. /**
  1601. * Sets the ID generator used to generate IDs for instances of this class.
  1602. */
  1603. public function setIdGenerator(IdGenerator $generator): void
  1604. {
  1605. $this->idGenerator = $generator;
  1606. }
  1607. /**
  1608. * Casts the identifier to its portable PHP type.
  1609. *
  1610. * @param mixed $id
  1611. *
  1612. * @return mixed $id
  1613. */
  1614. public function getPHPIdentifierValue($id)
  1615. {
  1616. $idType = $this->fieldMappings[$this->identifier]['type'];
  1617. return Type::getType($idType)->convertToPHPValue($id);
  1618. }
  1619. /**
  1620. * Casts the identifier to its database type.
  1621. *
  1622. * @param mixed $id
  1623. *
  1624. * @return mixed $id
  1625. */
  1626. public function getDatabaseIdentifierValue($id)
  1627. {
  1628. $idType = $this->fieldMappings[$this->identifier]['type'];
  1629. return Type::getType($idType)->convertToDatabaseValue($id);
  1630. }
  1631. /**
  1632. * Sets the document identifier of a document.
  1633. *
  1634. * The value will be converted to a PHP type before being set.
  1635. *
  1636. * @param mixed $id
  1637. */
  1638. public function setIdentifierValue(object $document, $id): void
  1639. {
  1640. $id = $this->getPHPIdentifierValue($id);
  1641. $this->reflFields[$this->identifier]->setValue($document, $id);
  1642. }
  1643. /**
  1644. * Gets the document identifier as a PHP type.
  1645. *
  1646. * @return mixed $id
  1647. */
  1648. public function getIdentifierValue(object $document)
  1649. {
  1650. return $this->reflFields[$this->identifier]->getValue($document);
  1651. }
  1652. /**
  1653. * Since MongoDB only allows exactly one identifier field this is a proxy
  1654. * to {@see getIdentifierValue()} and returns an array with the identifier
  1655. * field as a key.
  1656. *
  1657. * @param object $object
  1658. */
  1659. public function getIdentifierValues($object): array
  1660. {
  1661. return [$this->identifier => $this->getIdentifierValue($object)];
  1662. }
  1663. /**
  1664. * Get the document identifier object as a database type.
  1665. *
  1666. * @return mixed $id
  1667. */
  1668. public function getIdentifierObject(object $document)
  1669. {
  1670. return $this->getDatabaseIdentifierValue($this->getIdentifierValue($document));
  1671. }
  1672. /**
  1673. * Sets the specified field to the specified value on the given document.
  1674. *
  1675. * @param mixed $value
  1676. */
  1677. public function setFieldValue(object $document, string $field, $value): void
  1678. {
  1679. if ($document instanceof InternalProxy && ! $document->__isInitialized()) {
  1680. //property changes to an uninitialized proxy will not be tracked or persisted,
  1681. //so the proxy needs to be loaded first.
  1682. $document->__load();
  1683. } elseif ($document instanceof GhostObjectInterface && ! $document->isProxyInitialized()) {
  1684. $document->initializeProxy();
  1685. }
  1686. $this->reflFields[$field]->setValue($document, $value);
  1687. }
  1688. /**
  1689. * Gets the specified field's value off the given document.
  1690. *
  1691. * @return mixed
  1692. */
  1693. public function getFieldValue(object $document, string $field)
  1694. {
  1695. if ($document instanceof InternalProxy && $field !== $this->identifier && ! $document->__isInitialized()) {
  1696. $document->__load();
  1697. } elseif ($document instanceof GhostObjectInterface && $field !== $this->identifier && ! $document->isProxyInitialized()) {
  1698. $document->initializeProxy();
  1699. }
  1700. return $this->reflFields[$field]->getValue($document);
  1701. }
  1702. /**
  1703. * Gets the mapping of a field.
  1704. *
  1705. * @phpstan-return FieldMapping
  1706. *
  1707. * @throws MappingException If the $fieldName is not found in the fieldMappings array.
  1708. */
  1709. public function getFieldMapping(string $fieldName): array
  1710. {
  1711. if (! isset($this->fieldMappings[$fieldName])) {
  1712. throw MappingException::mappingNotFound($this->name, $fieldName);
  1713. }
  1714. return $this->fieldMappings[$fieldName];
  1715. }
  1716. /**
  1717. * Gets mappings of fields holding embedded document(s).
  1718. *
  1719. * @return array<string, AssociationFieldMapping>
  1720. */
  1721. public function getEmbeddedFieldsMappings(): array
  1722. {
  1723. return array_filter(
  1724. $this->associationMappings,
  1725. static fn ($assoc) => ! empty($assoc['embedded']),
  1726. );
  1727. }
  1728. /**
  1729. * Gets the field mapping by its DB name.
  1730. * E.g. it returns identifier's mapping when called with _id.
  1731. *
  1732. * @phpstan-return FieldMapping
  1733. *
  1734. * @throws MappingException
  1735. */
  1736. public function getFieldMappingByDbFieldName(string $dbFieldName): array
  1737. {
  1738. foreach ($this->fieldMappings as $mapping) {
  1739. if ($mapping['name'] === $dbFieldName) {
  1740. return $mapping;
  1741. }
  1742. }
  1743. throw MappingException::mappingNotFoundByDbName($this->name, $dbFieldName);
  1744. }
  1745. /**
  1746. * Check if the field is not null.
  1747. */
  1748. public function isNullable(string $fieldName): bool
  1749. {
  1750. $mapping = $this->getFieldMapping($fieldName);
  1751. return isset($mapping['nullable']) && $mapping['nullable'] === true;
  1752. }
  1753. /**
  1754. * Checks whether the document has a discriminator field and value configured.
  1755. */
  1756. public function hasDiscriminator(): bool
  1757. {
  1758. return isset($this->discriminatorField, $this->discriminatorValue);
  1759. }
  1760. /**
  1761. * Sets the type of Id generator to use for the mapped class.
  1762. */
  1763. public function setIdGeneratorType(int $generatorType): void
  1764. {
  1765. $this->generatorType = $generatorType;
  1766. }
  1767. /**
  1768. * Sets the Id generator options.
  1769. *
  1770. * @param array<string, mixed> $generatorOptions
  1771. */
  1772. public function setIdGeneratorOptions(array $generatorOptions): void
  1773. {
  1774. $this->generatorOptions = $generatorOptions;
  1775. }
  1776. public function isInheritanceTypeNone(): bool
  1777. {
  1778. return $this->inheritanceType === self::INHERITANCE_TYPE_NONE;
  1779. }
  1780. /**
  1781. * Checks whether the mapped class uses the SINGLE_COLLECTION inheritance mapping strategy.
  1782. */
  1783. public function isInheritanceTypeSingleCollection(): bool
  1784. {
  1785. return $this->inheritanceType === self::INHERITANCE_TYPE_SINGLE_COLLECTION;
  1786. }
  1787. /**
  1788. * Checks whether the mapped class uses the COLLECTION_PER_CLASS inheritance mapping strategy.
  1789. */
  1790. public function isInheritanceTypeCollectionPerClass(): bool
  1791. {
  1792. return $this->inheritanceType === self::INHERITANCE_TYPE_COLLECTION_PER_CLASS;
  1793. }
  1794. /**
  1795. * Sets the mapped subclasses of this class.
  1796. *
  1797. * @param class-string[] $subclasses The names of all mapped subclasses.
  1798. */
  1799. public function setSubclasses(array $subclasses): void
  1800. {
  1801. foreach ($subclasses as $subclass) {
  1802. $this->subClasses[] = $subclass;
  1803. }
  1804. }
  1805. /**
  1806. * Sets the parent class names.
  1807. * Assumes that the class names in the passed array are in the order:
  1808. * directParent -> directParentParent -> directParentParentParent ... -> root.
  1809. *
  1810. * @param list<class-string> $classNames
  1811. */
  1812. public function setParentClasses(array $classNames): void
  1813. {
  1814. $this->parentClasses = $classNames;
  1815. if (count($classNames) <= 0) {
  1816. return;
  1817. }
  1818. $this->rootDocumentName = (string) array_pop($classNames);
  1819. }
  1820. /**
  1821. * Checks whether the class will generate a new \MongoDB\BSON\ObjectId instance for us.
  1822. */
  1823. public function isIdGeneratorAuto(): bool
  1824. {
  1825. return $this->generatorType === self::GENERATOR_TYPE_AUTO;
  1826. }
  1827. /**
  1828. * Checks whether the class will use a collection to generate incremented identifiers.
  1829. */
  1830. public function isIdGeneratorIncrement(): bool
  1831. {
  1832. return $this->generatorType === self::GENERATOR_TYPE_INCREMENT;
  1833. }
  1834. /**
  1835. * Checks whether the class will generate a uuid id.
  1836. */
  1837. public function isIdGeneratorUuid(): bool
  1838. {
  1839. return $this->generatorType === self::GENERATOR_TYPE_UUID;
  1840. }
  1841. /**
  1842. * Checks whether the class uses no id generator.
  1843. */
  1844. public function isIdGeneratorNone(): bool
  1845. {
  1846. return $this->generatorType === self::GENERATOR_TYPE_NONE;
  1847. }
  1848. /**
  1849. * Sets the version field mapping used for versioning. Sets the default
  1850. * value to use depending on the column type.
  1851. *
  1852. * @phpstan-param FieldMapping $mapping
  1853. *
  1854. * @throws LockException
  1855. */
  1856. public function setVersionMapping(array &$mapping): void
  1857. {
  1858. if (! Type::getType($mapping['type']) instanceof Versionable) {
  1859. throw LockException::invalidVersionFieldType($mapping['type']);
  1860. }
  1861. $this->isVersioned = true;
  1862. $this->versionField = $mapping['fieldName'];
  1863. }
  1864. /**
  1865. * Sets whether this class is to be versioned for optimistic locking.
  1866. */
  1867. public function setVersioned(bool $bool): void
  1868. {
  1869. $this->isVersioned = $bool;
  1870. }
  1871. /**
  1872. * Sets the name of the field that is to be used for versioning if this class is
  1873. * versioned for optimistic locking.
  1874. */
  1875. public function setVersionField(?string $versionField): void
  1876. {
  1877. $this->versionField = $versionField;
  1878. }
  1879. /**
  1880. * Sets the version field mapping used for versioning. Sets the default
  1881. * value to use depending on the column type.
  1882. *
  1883. * @phpstan-param FieldMapping $mapping
  1884. *
  1885. * @throws LockException
  1886. */
  1887. public function setLockMapping(array &$mapping): void
  1888. {
  1889. if ($mapping['type'] !== 'int') {
  1890. throw LockException::invalidLockFieldType($mapping['type']);
  1891. }
  1892. $this->isLockable = true;
  1893. $this->lockField = $mapping['fieldName'];
  1894. }
  1895. /**
  1896. * Sets whether this class is to allow pessimistic locking.
  1897. */
  1898. public function setLockable(bool $bool): void
  1899. {
  1900. $this->isLockable = $bool;
  1901. }
  1902. /**
  1903. * Sets the name of the field that is to be used for storing whether a document
  1904. * is currently locked or not.
  1905. */
  1906. public function setLockField(string $lockField): void
  1907. {
  1908. $this->lockField = $lockField;
  1909. }
  1910. /**
  1911. * Marks this class as read only, no change tracking is applied to it.
  1912. */
  1913. public function markReadOnly(): void
  1914. {
  1915. $this->isReadOnly = true;
  1916. }
  1917. public function getRootClass(): ?string
  1918. {
  1919. return $this->rootClass;
  1920. }
  1921. public function isView(): bool
  1922. {
  1923. return $this->isView;
  1924. }
  1925. /** @param class-string $rootClass */
  1926. public function markViewOf(string $rootClass): void
  1927. {
  1928. $this->isView = true;
  1929. $this->rootClass = $rootClass;
  1930. }
  1931. public function markAsTimeSeries(TimeSeries $options): void
  1932. {
  1933. $this->validateTimeSeriesOptions($options);
  1934. $this->timeSeriesOptions = $options;
  1935. }
  1936. public function getFieldNames(): array
  1937. {
  1938. return array_keys($this->fieldMappings);
  1939. }
  1940. public function getAssociationNames(): array
  1941. {
  1942. return array_keys($this->associationMappings);
  1943. }
  1944. /** @param string $fieldName */
  1945. public function getTypeOfField($fieldName): ?string
  1946. {
  1947. return isset($this->fieldMappings[$fieldName]) ?
  1948. $this->fieldMappings[$fieldName]['type'] : null;
  1949. }
  1950. /**
  1951. * @param string $assocName
  1952. *
  1953. * @return class-string|null
  1954. */
  1955. public function getAssociationTargetClass($assocName): ?string
  1956. {
  1957. if (! isset($this->associationMappings[$assocName])) {
  1958. throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
  1959. }
  1960. return $this->associationMappings[$assocName]['targetDocument'] ?? null;
  1961. }
  1962. /**
  1963. * Retrieve the collectionClass associated with an association
  1964. *
  1965. * @return class-string
  1966. */
  1967. public function getAssociationCollectionClass(string $assocName): string
  1968. {
  1969. if (! isset($this->associationMappings[$assocName])) {
  1970. throw new InvalidArgumentException("Association name expected, '" . $assocName . "' is not an association.");
  1971. }
  1972. if (! array_key_exists('collectionClass', $this->associationMappings[$assocName])) {
  1973. throw new InvalidArgumentException("collectionClass can only be applied to 'embedMany' and 'referenceMany' associations.");
  1974. }
  1975. return $this->associationMappings[$assocName]['collectionClass'];
  1976. }
  1977. /** @param string $assocName */
  1978. public function isAssociationInverseSide($assocName): bool
  1979. {
  1980. throw new BadMethodCallException(__METHOD__ . '() is not implemented yet.');
  1981. }
  1982. /** @param string $assocName */
  1983. public function getAssociationMappedByTargetField($assocName): string
  1984. {
  1985. throw new BadMethodCallException(__METHOD__ . '() is not implemented yet.');
  1986. }
  1987. /**
  1988. * Map a field.
  1989. *
  1990. * @phpstan-param FieldMappingConfig $mapping
  1991. *
  1992. * @phpstan-return FieldMapping
  1993. *
  1994. * @throws MappingException
  1995. */
  1996. public function mapField(array $mapping): array
  1997. {
  1998. if (! isset($mapping['fieldName']) && isset($mapping['name'])) {
  1999. $mapping['fieldName'] = $mapping['name'];
  2000. }
  2001. if ($this->isTypedProperty($mapping['fieldName'])) {
  2002. $mapping = $this->validateAndCompleteTypedFieldMapping($mapping);
  2003. if (isset($mapping['type']) && $mapping['type'] === self::MANY) {
  2004. $mapping = $this->validateAndCompleteTypedManyAssociationMapping($mapping);
  2005. }
  2006. }
  2007. if (! isset($mapping['fieldName']) || ! is_string($mapping['fieldName'])) {
  2008. throw MappingException::missingFieldName($this->name);
  2009. }
  2010. if (! isset($mapping['name'])) {
  2011. $mapping['name'] = $mapping['fieldName'];
  2012. }
  2013. if ($this->identifier === $mapping['name'] && empty($mapping['id'])) {
  2014. throw MappingException::mustNotChangeIdentifierFieldsType($this->name, (string) $mapping['name']);
  2015. }
  2016. if ($this->discriminatorField !== null && $this->discriminatorField === $mapping['name']) {
  2017. throw MappingException::discriminatorFieldConflict($this->name, $this->discriminatorField);
  2018. }
  2019. if (isset($mapping['collectionClass'])) {
  2020. $mapping['collectionClass'] = ltrim($mapping['collectionClass'], '\\');
  2021. }
  2022. if (! empty($mapping['collectionClass'])) {
  2023. $rColl = new ReflectionClass($mapping['collectionClass']);
  2024. if (! $rColl->implementsInterface(Collection::class)) {
  2025. throw MappingException::collectionClassDoesNotImplementCommonInterface($this->name, $mapping['fieldName'], $mapping['collectionClass']);
  2026. }
  2027. }
  2028. if (isset($mapping['cascade']) && isset($mapping['embedded'])) {
  2029. throw MappingException::cascadeOnEmbeddedNotAllowed($this->name, $mapping['fieldName']);
  2030. }
  2031. $cascades = isset($mapping['cascade']) ? array_map('strtolower', (array) $mapping['cascade']) : [];
  2032. if (in_array('all', $cascades) || isset($mapping['embedded'])) {
  2033. $cascades = ['remove', 'persist', 'refresh', 'merge', 'detach'];
  2034. }
  2035. if (isset($mapping['embedded'])) {
  2036. unset($mapping['cascade']);
  2037. } elseif (isset($mapping['cascade'])) {
  2038. $mapping['cascade'] = $cascades;
  2039. }
  2040. $mapping['isCascadeRemove'] = in_array('remove', $cascades);
  2041. $mapping['isCascadePersist'] = in_array('persist', $cascades);
  2042. $mapping['isCascadeRefresh'] = in_array('refresh', $cascades);
  2043. $mapping['isCascadeMerge'] = in_array('merge', $cascades);
  2044. $mapping['isCascadeDetach'] = in_array('detach', $cascades);
  2045. if (isset($mapping['id']) && $mapping['id'] === true) {
  2046. $mapping['name'] = '_id';
  2047. $this->identifier = $mapping['fieldName'];
  2048. if (isset($mapping['strategy'])) {
  2049. $this->generatorType = constant(self::class . '::GENERATOR_TYPE_' . strtoupper($mapping['strategy']));
  2050. }
  2051. $this->generatorOptions = $mapping['options'] ?? [];
  2052. switch ($this->generatorType) {
  2053. case self::GENERATOR_TYPE_AUTO:
  2054. $mapping['type'] = 'id';
  2055. break;
  2056. default:
  2057. if (! empty($this->generatorOptions['type'])) {
  2058. $mapping['type'] = (string) $this->generatorOptions['type'];
  2059. } elseif (empty($mapping['type'])) {
  2060. $mapping['type'] = $this->generatorType === self::GENERATOR_TYPE_INCREMENT ? Type::INT : Type::CUSTOMID;
  2061. }
  2062. }
  2063. unset($this->generatorOptions['type']);
  2064. }
  2065. if (! isset($mapping['type'])) {
  2066. // Default to string
  2067. $mapping['type'] = Type::STRING;
  2068. }
  2069. if (! isset($mapping['nullable'])) {
  2070. $mapping['nullable'] = false;
  2071. }
  2072. if (
  2073. isset($mapping['reference'])
  2074. && isset($mapping['storeAs'])
  2075. && $mapping['storeAs'] === self::REFERENCE_STORE_AS_ID
  2076. && ! isset($mapping['targetDocument'])
  2077. ) {
  2078. throw MappingException::simpleReferenceRequiresTargetDocument($this->name, $mapping['fieldName']);
  2079. }
  2080. if (
  2081. isset($mapping['reference']) && empty($mapping['targetDocument']) && empty($mapping['discriminatorMap']) &&
  2082. (isset($mapping['mappedBy']) || isset($mapping['inversedBy']))
  2083. ) {
  2084. throw MappingException::owningAndInverseReferencesRequireTargetDocument($this->name, $mapping['fieldName']);
  2085. }
  2086. if ($this->isEmbeddedDocument && $mapping['type'] === self::MANY && isset($mapping['strategy']) && CollectionHelper::isAtomic($mapping['strategy'])) {
  2087. throw MappingException::atomicCollectionStrategyNotAllowed($mapping['strategy'], $this->name, $mapping['fieldName']);
  2088. }
  2089. if (isset($mapping['repositoryMethod']) && ! (empty($mapping['skip']) && empty($mapping['limit']) && empty($mapping['sort']))) {
  2090. throw MappingException::repositoryMethodCanNotBeCombinedWithSkipLimitAndSort($this->name, $mapping['fieldName']);
  2091. }
  2092. if (isset($mapping['targetDocument']) && isset($mapping['discriminatorMap'])) {
  2093. trigger_deprecation(
  2094. 'doctrine/mongodb-odm',
  2095. '2.2',
  2096. 'Mapping both "targetDocument" and "discriminatorMap" on field "%s" in class "%s" is deprecated. Only one of them can be used at a time',
  2097. $mapping['fieldName'],
  2098. $this->name,
  2099. );
  2100. }
  2101. if (isset($mapping['reference']) && $mapping['type'] === self::ONE) {
  2102. $mapping['association'] = self::REFERENCE_ONE;
  2103. }
  2104. if (isset($mapping['reference']) && $mapping['type'] === self::MANY) {
  2105. $mapping['association'] = self::REFERENCE_MANY;
  2106. }
  2107. if (isset($mapping['embedded']) && $mapping['type'] === self::ONE) {
  2108. $mapping['association'] = self::EMBED_ONE;
  2109. }
  2110. if (isset($mapping['embedded']) && $mapping['type'] === self::MANY) {
  2111. $mapping['association'] = self::EMBED_MANY;
  2112. }
  2113. if (isset($mapping['association']) && ! isset($mapping['targetDocument']) && ! isset($mapping['discriminatorField'])) {
  2114. $mapping['discriminatorField'] = self::DEFAULT_DISCRIMINATOR_FIELD;
  2115. }
  2116. if (isset($mapping['targetDocument']) && ! class_exists($mapping['targetDocument']) && ! interface_exists($mapping['targetDocument'])) {
  2117. throw MappingException::invalidTargetDocument(
  2118. $mapping['targetDocument'],
  2119. $this->name,
  2120. $mapping['fieldName'],
  2121. );
  2122. }
  2123. if (isset($mapping['discriminatorMap'])) {
  2124. foreach ($mapping['discriminatorMap'] as $value => $class) {
  2125. if (! class_exists($class) && ! interface_exists($class)) {
  2126. throw MappingException::invalidClassInReferenceDiscriminatorMap($class, $this->name, $mapping['fieldName']);
  2127. }
  2128. }
  2129. }
  2130. if (isset($mapping['version'])) {
  2131. $mapping['notSaved'] = true;
  2132. $this->setVersionMapping($mapping);
  2133. }
  2134. if (isset($mapping['lock'])) {
  2135. $mapping['notSaved'] = true;
  2136. $this->setLockMapping($mapping);
  2137. }
  2138. $mapping['isOwningSide'] = true;
  2139. $mapping['isInverseSide'] = false;
  2140. if (isset($mapping['reference'])) {
  2141. if (isset($mapping['inversedBy']) && $mapping['inversedBy']) {
  2142. $mapping['isOwningSide'] = true;
  2143. $mapping['isInverseSide'] = false;
  2144. }
  2145. if (isset($mapping['mappedBy']) && $mapping['mappedBy']) {
  2146. $mapping['isInverseSide'] = true;
  2147. $mapping['isOwningSide'] = false;
  2148. }
  2149. if (isset($mapping['repositoryMethod'])) {
  2150. $mapping['isInverseSide'] = true;
  2151. $mapping['isOwningSide'] = false;
  2152. }
  2153. if (! isset($mapping['orphanRemoval'])) {
  2154. $mapping['orphanRemoval'] = false;
  2155. }
  2156. }
  2157. if (! empty($mapping['prime']) && ($mapping['association'] !== self::REFERENCE_MANY || ! $mapping['isInverseSide'])) {
  2158. throw MappingException::referencePrimersOnlySupportedForInverseReferenceMany($this->name, $mapping['fieldName']);
  2159. }
  2160. if ($this->isFile && ! $this->isAllowedGridFSField($mapping['name'])) {
  2161. throw MappingException::fieldNotAllowedForGridFS($this->name, $mapping['fieldName']);
  2162. }
  2163. $this->applyStorageStrategy($mapping);
  2164. $this->checkDuplicateMapping($mapping);
  2165. $this->typeRequirementsAreMet($mapping);
  2166. $deprecatedTypes = [
  2167. Type::BOOLEAN => Type::BOOL,
  2168. Type::INTEGER => Type::INT,
  2169. Type::INTID => Type::INT,
  2170. ];
  2171. if (isset($deprecatedTypes[$mapping['type']])) {
  2172. trigger_deprecation(
  2173. 'doctrine/mongodb-odm',
  2174. '2.1',
  2175. 'The "%s" mapping type is deprecated. Use "%s" instead.',
  2176. $mapping['type'],
  2177. $deprecatedTypes[$mapping['type']],
  2178. );
  2179. }
  2180. $this->fieldMappings[$mapping['fieldName']] = $mapping;
  2181. if (isset($mapping['association'])) {
  2182. $this->associationMappings[$mapping['fieldName']] = $mapping;
  2183. }
  2184. $reflProp = $this->reflectionService->getAccessibleProperty($this->name, $mapping['fieldName']);
  2185. assert($reflProp instanceof ReflectionProperty);
  2186. if (isset($mapping['enumType'])) {
  2187. if (! enum_exists($mapping['enumType'])) {
  2188. throw MappingException::nonEnumTypeMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
  2189. }
  2190. $reflectionEnum = new ReflectionEnum($mapping['enumType']);
  2191. if (! $reflectionEnum->isBacked()) {
  2192. throw MappingException::nonBackedEnumMapped($this->name, $mapping['fieldName'], $mapping['enumType']);
  2193. }
  2194. $reflProp = new EnumReflectionProperty($reflProp, $mapping['enumType']);
  2195. }
  2196. $this->reflFields[$mapping['fieldName']] = $reflProp;
  2197. return $mapping;
  2198. }
  2199. /**
  2200. * Determines which fields get serialized.
  2201. *
  2202. * It is only serialized what is necessary for best unserialization performance.
  2203. * That means any metadata properties that are not set or empty or simply have
  2204. * their default value are NOT serialized.
  2205. *
  2206. * Parts that are also NOT serialized because they can not be properly unserialized:
  2207. * - reflClass (ReflectionClass)
  2208. * - reflFields (ReflectionProperty array)
  2209. *
  2210. * @return array The names of all the fields that should be serialized.
  2211. */
  2212. public function __sleep()
  2213. {
  2214. // This metadata is always serialized/cached.
  2215. $serialized = [
  2216. 'fieldMappings',
  2217. 'associationMappings',
  2218. 'identifier',
  2219. 'name',
  2220. 'db',
  2221. 'collection',
  2222. 'readPreference',
  2223. 'readPreferenceTags',
  2224. 'writeConcern',
  2225. 'rootDocumentName',
  2226. 'generatorType',
  2227. 'generatorOptions',
  2228. 'idGenerator',
  2229. 'indexes',
  2230. 'shardKey',
  2231. 'timeSeriesOptions',
  2232. ];
  2233. // The rest of the metadata is only serialized if necessary.
  2234. if ($this->changeTrackingPolicy !== self::CHANGETRACKING_DEFERRED_IMPLICIT) {
  2235. $serialized[] = 'changeTrackingPolicy';
  2236. }
  2237. if ($this->customRepositoryClassName) {
  2238. $serialized[] = 'customRepositoryClassName';
  2239. }
  2240. if ($this->inheritanceType !== self::INHERITANCE_TYPE_NONE || $this->discriminatorField !== null) {
  2241. $serialized[] = 'inheritanceType';
  2242. $serialized[] = 'discriminatorField';
  2243. $serialized[] = 'discriminatorValue';
  2244. $serialized[] = 'discriminatorMap';
  2245. $serialized[] = 'defaultDiscriminatorValue';
  2246. $serialized[] = 'parentClasses';
  2247. $serialized[] = 'subClasses';
  2248. }
  2249. if ($this->isMappedSuperclass) {
  2250. $serialized[] = 'isMappedSuperclass';
  2251. }
  2252. if ($this->isEmbeddedDocument) {
  2253. $serialized[] = 'isEmbeddedDocument';
  2254. }
  2255. if ($this->isQueryResultDocument) {
  2256. $serialized[] = 'isQueryResultDocument';
  2257. }
  2258. if ($this->isView()) {
  2259. $serialized[] = 'isView';
  2260. $serialized[] = 'rootClass';
  2261. }
  2262. if ($this->isFile) {
  2263. $serialized[] = 'isFile';
  2264. $serialized[] = 'bucketName';
  2265. $serialized[] = 'chunkSizeBytes';
  2266. }
  2267. if ($this->isVersioned) {
  2268. $serialized[] = 'isVersioned';
  2269. $serialized[] = 'versionField';
  2270. }
  2271. if ($this->isLockable) {
  2272. $serialized[] = 'isLockable';
  2273. $serialized[] = 'lockField';
  2274. }
  2275. if ($this->lifecycleCallbacks) {
  2276. $serialized[] = 'lifecycleCallbacks';
  2277. }
  2278. if ($this->collectionCapped) {
  2279. $serialized[] = 'collectionCapped';
  2280. $serialized[] = 'collectionSize';
  2281. $serialized[] = 'collectionMax';
  2282. }
  2283. if ($this->isReadOnly) {
  2284. $serialized[] = 'isReadOnly';
  2285. }
  2286. if ($this->validator !== null) {
  2287. $serialized[] = 'validator';
  2288. $serialized[] = 'validationAction';
  2289. $serialized[] = 'validationLevel';
  2290. }
  2291. return $serialized;
  2292. }
  2293. /**
  2294. * Restores some state that can not be serialized/unserialized.
  2295. */
  2296. public function __wakeup()
  2297. {
  2298. // Restore ReflectionClass and properties
  2299. $this->reflectionService = new RuntimeReflectionService();
  2300. $this->reflClass = new ReflectionClass($this->name);
  2301. $this->instantiator = new Instantiator();
  2302. foreach ($this->fieldMappings as $field => $mapping) {
  2303. $prop = $this->reflectionService->getAccessibleProperty($mapping['declared'] ?? $this->name, $field);
  2304. assert($prop instanceof ReflectionProperty);
  2305. if (isset($mapping['enumType'])) {
  2306. $prop = new EnumReflectionProperty($prop, $mapping['enumType']);
  2307. }
  2308. $this->reflFields[$field] = $prop;
  2309. }
  2310. }
  2311. /**
  2312. * Creates a new instance of the mapped class, without invoking the constructor.
  2313. *
  2314. * @phpstan-return T
  2315. */
  2316. public function newInstance(): object
  2317. {
  2318. /** @phpstan-var T */
  2319. return $this->instantiator->instantiate($this->name);
  2320. }
  2321. private function isAllowedGridFSField(string $name): bool
  2322. {
  2323. return in_array($name, self::ALLOWED_GRIDFS_FIELDS, true);
  2324. }
  2325. /** @phpstan-param FieldMapping $mapping */
  2326. private function typeRequirementsAreMet(array $mapping): void
  2327. {
  2328. if ($mapping['type'] === Type::DECIMAL128 && ! extension_loaded('bcmath')) {
  2329. throw MappingException::typeRequirementsNotFulfilled($this->name, $mapping['fieldName'], Type::DECIMAL128, 'ext-bcmath is missing');
  2330. }
  2331. }
  2332. /** @phpstan-param FieldMapping $mapping */
  2333. private function checkDuplicateMapping(array $mapping): void
  2334. {
  2335. if ($mapping['notSaved'] ?? false) {
  2336. return;
  2337. }
  2338. foreach ($this->fieldMappings as $fieldName => $otherMapping) {
  2339. // Ignore fields with the same name - we can safely override their mapping
  2340. if ($mapping['fieldName'] === $fieldName) {
  2341. continue;
  2342. }
  2343. // Ignore fields with a different name in the database
  2344. if ($mapping['name'] !== $otherMapping['name']) {
  2345. continue;
  2346. }
  2347. // If the other field is not saved, ignore it as well
  2348. if ($otherMapping['notSaved'] ?? false) {
  2349. continue;
  2350. }
  2351. throw MappingException::duplicateDatabaseFieldName($this->getName(), $mapping['fieldName'], $mapping['name'], $fieldName);
  2352. }
  2353. }
  2354. private function isTypedProperty(string $name): bool
  2355. {
  2356. return $this->reflClass->hasProperty($name)
  2357. && $this->reflClass->getProperty($name)->hasType();
  2358. }
  2359. /**
  2360. * Validates & completes the given field mapping based on typed property.
  2361. *
  2362. * @phpstan-param FieldMappingConfig $mapping
  2363. *
  2364. * @return FieldMappingConfig
  2365. */
  2366. private function validateAndCompleteTypedFieldMapping(array $mapping): array
  2367. {
  2368. $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
  2369. if (! $type instanceof ReflectionNamedType || isset($mapping['type'])) {
  2370. return $mapping;
  2371. }
  2372. if (! $type->isBuiltin() && enum_exists($type->getName())) {
  2373. $reflection = new ReflectionEnum($type->getName());
  2374. if (! $reflection->isBacked()) {
  2375. throw MappingException::nonBackedEnumMapped($this->name, $mapping['fieldName'], $reflection->getName());
  2376. }
  2377. $type = $reflection->getBackingType();
  2378. assert($type instanceof ReflectionNamedType);
  2379. $mapping['enumType'] = $reflection->getName();
  2380. }
  2381. switch ($type->getName()) {
  2382. case DateTime::class:
  2383. $mapping['type'] = Type::DATE;
  2384. break;
  2385. case DateTimeImmutable::class:
  2386. $mapping['type'] = Type::DATE_IMMUTABLE;
  2387. break;
  2388. case 'array':
  2389. $mapping['type'] = Type::HASH;
  2390. break;
  2391. case 'bool':
  2392. $mapping['type'] = Type::BOOL;
  2393. break;
  2394. case 'float':
  2395. $mapping['type'] = Type::FLOAT;
  2396. break;
  2397. case 'int':
  2398. $mapping['type'] = Type::INT;
  2399. break;
  2400. case 'string':
  2401. $mapping['type'] = Type::STRING;
  2402. break;
  2403. }
  2404. return $mapping;
  2405. }
  2406. /**
  2407. * Validates & completes the basic mapping information based on typed property.
  2408. *
  2409. * @phpstan-param FieldMappingConfig $mapping
  2410. *
  2411. * @return FieldMappingConfig
  2412. */
  2413. private function validateAndCompleteTypedManyAssociationMapping(array $mapping): array
  2414. {
  2415. $type = $this->reflClass->getProperty($mapping['fieldName'])->getType();
  2416. if (! $type instanceof ReflectionNamedType) {
  2417. return $mapping;
  2418. }
  2419. if (! isset($mapping['collectionClass']) && class_exists($type->getName())) {
  2420. $mapping['collectionClass'] = $type->getName();
  2421. }
  2422. return $mapping;
  2423. }
  2424. private function validateTimeSeriesOptions(TimeSeries $options): void
  2425. {
  2426. if (! $this->hasField($options->timeField)) {
  2427. throw MappingException::timeSeriesFieldNotFound($this->name, $options->timeField, 'time');
  2428. }
  2429. if ($options->metaField !== null && ! $this->hasField($options->metaField)) {
  2430. throw MappingException::timeSeriesFieldNotFound($this->name, $options->metaField, 'metadata');
  2431. }
  2432. }
  2433. }