PageRenderTime 45ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 0ms

/Classes/TYPO3/FLOW3/Persistence/Doctrine/Mapping/Driver/Flow3AnnotationDriver.php

https://github.com/christianjul/FLOW3-Composer
PHP | 863 lines | 699 code | 52 blank | 112 comment | 64 complexity | bd8569600fc3741f77fd3109b51816b4 MD5 | raw file
Possible License(s): BSD-3-Clause, LGPL-3.0
  1. <?php
  2. namespace TYPO3\FLOW3\Persistence\Doctrine\Mapping\Driver;
  3. /* *
  4. * This script belongs to the FLOW3 framework. *
  5. * *
  6. * It is free software; you can redistribute it and/or modify it under *
  7. * the terms of the GNU Lesser General Public License, either version 3 *
  8. * of the License, or (at your option) any later version. *
  9. * *
  10. * The TYPO3 project - inspiring people to share! *
  11. * */
  12. use TYPO3\FLOW3\Annotations as FLOW3;
  13. /**
  14. * This driver reads the mapping metadata from docblock annotations.
  15. * It gives precedence to Doctrine annotations but fills gaps from other info
  16. * if possible:
  17. * Entity.repositoryClass is set to the repository found in the class schema
  18. * Table.name is set to a sane value
  19. * Column.type is set to @var type
  20. * *.targetEntity is set to @var type
  21. *
  22. * If a property is not marked as an association the mapping type is set to
  23. * "object" for objects.
  24. *
  25. * @FLOW3\Scope("singleton")
  26. */
  27. class Flow3AnnotationDriver implements \Doctrine\ORM\Mapping\Driver\Driver, \TYPO3\FLOW3\Aop\Pointcut\PointcutFilterInterface {
  28. const MAPPING_REGULAR = 0;
  29. const MAPPING_MM_REGULAR = 1;
  30. /**
  31. * @var \TYPO3\FLOW3\Reflection\ReflectionService
  32. */
  33. protected $reflectionService;
  34. /**
  35. * @var \Doctrine\Common\Annotations\AnnotationReader
  36. */
  37. protected $reader;
  38. /**
  39. * @var \Doctrine\Common\Persistence\ObjectManager
  40. */
  41. protected $entityManager;
  42. /**
  43. * @var array
  44. */
  45. protected $classNames;
  46. /**
  47. * @var integer
  48. */
  49. protected $tableNameLengthLimit = NULL;
  50. /**
  51. * Initializes a new AnnotationDriver that uses the given AnnotationReader for reading
  52. * docblock annotations.
  53. */
  54. public function __construct() {
  55. $this->reader = new \Doctrine\Common\Annotations\IndexedReader(new \Doctrine\Common\Annotations\AnnotationReader());
  56. }
  57. /**
  58. * @param \TYPO3\FLOW3\Reflection\ReflectionService $reflectionService
  59. * @return void
  60. */
  61. public function injectReflectionService(\TYPO3\FLOW3\Reflection\ReflectionService $reflectionService) {
  62. $this->reflectionService = $reflectionService;
  63. }
  64. /**
  65. * @param \Doctrine\Common\Persistence\ObjectManager $entityManager
  66. * @return void
  67. */
  68. public function setEntityManager(\Doctrine\Common\Persistence\ObjectManager $entityManager) {
  69. $this->entityManager = $entityManager;
  70. }
  71. /**
  72. * Fetch a class schema for the given class, if possible.
  73. *
  74. * @param string $className
  75. * @return \TYPO3\FLOW3\Reflection\ClassSchema
  76. * @throws \TYPO3\FLOW3\Persistence\Doctrine\Mapping\Exception\ClassSchemaNotFoundException
  77. */
  78. protected function getClassSchema($className) {
  79. $className = preg_replace('/' . \TYPO3\FLOW3\Object\Proxy\Compiler::ORIGINAL_CLASSNAME_SUFFIX . '$/', '', $className);
  80. $classSchema = $this->reflectionService->getClassSchema($className);
  81. if (!$classSchema) {
  82. throw new \TYPO3\FLOW3\Persistence\Doctrine\Mapping\Exception\ClassSchemaNotFoundException('No class schema found for "' . $className . '".', 1295973082);
  83. }
  84. return $classSchema;
  85. }
  86. /**
  87. * Check for $className being an aggregate root.
  88. *
  89. * @param string $className
  90. * @param string $propertySourceHint
  91. * @return boolean
  92. * @throws \TYPO3\FLOW3\Persistence\Doctrine\Mapping\Exception\ClassSchemaNotFoundException
  93. */
  94. protected function isAggregateRoot($className, $propertySourceHint) {
  95. $className = preg_replace('/' . \TYPO3\FLOW3\Object\Proxy\Compiler::ORIGINAL_CLASSNAME_SUFFIX . '$/', '', $className);
  96. try {
  97. $classSchema = $this->getClassSchema($className);
  98. return $classSchema->isAggregateRoot();
  99. } catch (\TYPO3\FLOW3\Persistence\Doctrine\Mapping\Exception\ClassSchemaNotFoundException $exception) {
  100. throw new \TYPO3\FLOW3\Persistence\Doctrine\Mapping\Exception\ClassSchemaNotFoundException('No class schema found for "' . $className . '". The class should probably marked as entity or value object! This happened while examining "' . $propertySourceHint . '"', 1340185197);
  101. }
  102. }
  103. /**
  104. * Loads the metadata for the specified class into the provided container.
  105. *
  106. * @param string $className
  107. * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
  108. * @return void
  109. * @throws \Doctrine\ORM\Mapping\MappingException
  110. * @throws \UnexpectedValueException
  111. * @todo adjust when Doctrine 2 supports value objects, see http://www.doctrine-project.org/jira/browse/DDC-93
  112. */
  113. public function loadMetadataForClass($className, \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata) {
  114. $class = $metadata->getReflectionClass();
  115. $classSchema = $this->getClassSchema($class->getName());
  116. $classAnnotations = $this->reader->getClassAnnotations($class);
  117. // Evaluate Entity annotation
  118. if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) {
  119. $mappedSuperclassAnnotation = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
  120. if ($mappedSuperclassAnnotation->repositoryClass !== NULL) {
  121. $metadata->setCustomRepositoryClass($mappedSuperclassAnnotation->repositoryClass);
  122. }
  123. $metadata->isMappedSuperclass = TRUE;
  124. } elseif (isset($classAnnotations['TYPO3\FLOW3\Annotations\Entity']) || isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) {
  125. $entityAnnotation = isset($classAnnotations['TYPO3\FLOW3\Annotations\Entity']) ? $classAnnotations['TYPO3\FLOW3\Annotations\Entity'] : $classAnnotations['Doctrine\ORM\Mapping\Entity'];
  126. if ($entityAnnotation->repositoryClass !== NULL) {
  127. $metadata->setCustomRepositoryClass($entityAnnotation->repositoryClass);
  128. } elseif ($classSchema->getRepositoryClassName() !== NULL) {
  129. if ($this->reflectionService->isClassImplementationOf($classSchema->getRepositoryClassName(), 'Doctrine\ORM\EntityRepository')) {
  130. $metadata->setCustomRepositoryClass($classSchema->getRepositoryClassName());
  131. }
  132. }
  133. if ($entityAnnotation->readOnly) {
  134. $metadata->markReadOnly();
  135. }
  136. } elseif ($classSchema->getModelType() === \TYPO3\FLOW3\Reflection\ClassSchema::MODELTYPE_VALUEOBJECT) {
  137. // also ok... but we make it read-only
  138. $metadata->markReadOnly();
  139. } else {
  140. throw \Doctrine\ORM\Mapping\MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
  141. }
  142. // Evaluate Table annotation
  143. $primaryTable = array();
  144. if (isset($classAnnotations['Doctrine\ORM\Mapping\Table'])) {
  145. $tableAnnotation = $classAnnotations['Doctrine\ORM\Mapping\Table'];
  146. $primaryTable['name'] = $tableAnnotation->name;
  147. $primaryTable['schema'] = $tableAnnotation->schema;
  148. if ($tableAnnotation->indexes !== NULL) {
  149. foreach ($tableAnnotation->indexes as $indexAnnotation) {
  150. $index = array('columns' => $indexAnnotation->columns);
  151. if (!empty($indexAnnotation->name)) {
  152. $primaryTable['indexes'][$indexAnnotation->name] = $index;
  153. } else {
  154. $primaryTable['indexes'][] = $index;
  155. }
  156. }
  157. }
  158. if ($tableAnnotation->uniqueConstraints !== NULL) {
  159. foreach ($tableAnnotation->uniqueConstraints as $uniqueConstraint) {
  160. $uniqueConstraint = array('columns' => $uniqueConstraint->columns);
  161. if (!empty($uniqueConstraint->name)) {
  162. $primaryTable['uniqueConstraints'][$uniqueConstraint->name] = $uniqueConstraint;
  163. } else {
  164. $primaryTable['uniqueConstraints'][] = $uniqueConstraint;
  165. }
  166. }
  167. }
  168. }
  169. if (!isset($primaryTable['name'])) {
  170. $className = $classSchema->getClassName();
  171. $primaryTable['name'] = $this->inferTableNameFromClassName($className);
  172. }
  173. // Evaluate NamedQueries annotation
  174. if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedQueries'])) {
  175. $namedQueriesAnnotation = $classAnnotations['Doctrine\ORM\Mapping\NamedQueries'];
  176. if (!is_array($namedQueriesAnnotation->value)) {
  177. throw new \UnexpectedValueException("@NamedQueries should contain an array of @NamedQuery annotations.");
  178. }
  179. foreach ($namedQueriesAnnotation->value as $namedQuery) {
  180. if (!($namedQuery instanceof \Doctrine\ORM\Mapping\NamedQuery)) {
  181. throw new \UnexpectedValueException("@NamedQueries should contain an array of @NamedQuery annotations.");
  182. }
  183. $metadata->addNamedQuery(array(
  184. 'name' => $namedQuery->name,
  185. 'query' => $namedQuery->query
  186. ));
  187. }
  188. }
  189. // Evaluate InheritanceType annotation
  190. if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) {
  191. $inheritanceTypeAnnotation = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType'];
  192. $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . strtoupper($inheritanceTypeAnnotation->value)));
  193. if ($metadata->inheritanceType !== \Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_NONE) {
  194. // Evaluate DiscriminatorColumn annotation
  195. if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn'])) {
  196. $discriminatorColumnAnnotation = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn'];
  197. $metadata->setDiscriminatorColumn(array(
  198. 'name' => $discriminatorColumnAnnotation->name,
  199. 'type' => $discriminatorColumnAnnotation->type,
  200. 'length' => $discriminatorColumnAnnotation->length
  201. ));
  202. } else {
  203. $metadata->setDiscriminatorColumn(array('name' => 'dtype', 'type' => 'string', 'length' => 255));
  204. }
  205. // Evaluate DiscriminatorMap annotation
  206. if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap'])) {
  207. $discriminatorMapAnnotation = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap'];
  208. $metadata->setDiscriminatorMap($discriminatorMapAnnotation->value);
  209. } else {
  210. $discriminatorMap = array();
  211. $subclassNames = $this->reflectionService->getAllSubClassNamesForClass($className);
  212. if (!$this->reflectionService->isClassAbstract($className)) {
  213. $mappedClassName = strtolower(str_replace('Domain_Model_', '', str_replace('\\', '_', $className)));
  214. $discriminatorMap[$mappedClassName] = $className;
  215. }
  216. foreach ($subclassNames as $subclassName) {
  217. $mappedSubclassName = strtolower(str_replace('Domain_Model_', '', str_replace('\\', '_', $subclassName)));
  218. $discriminatorMap[$mappedSubclassName] = $subclassName;
  219. }
  220. $metadata->setDiscriminatorMap($discriminatorMap);
  221. }
  222. }
  223. }
  224. // Evaluate DoctrineChangeTrackingPolicy annotation
  225. if (isset($classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy'])) {
  226. $changeTrackingAnnotation = $classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy'];
  227. $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . strtoupper($changeTrackingAnnotation->value)));
  228. } else {
  229. $metadata->setChangeTrackingPolicy(\Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_DEFERRED_EXPLICIT);
  230. }
  231. // Evaluate annotations on properties/fields
  232. $this->evaluatePropertyAnnotations($metadata);
  233. // build unique index for table
  234. if (!isset($primaryTable['uniqueConstraints'])) {
  235. $idProperties = array_keys($classSchema->getIdentityProperties());
  236. if (array_diff($idProperties, $metadata->getIdentifierFieldNames()) !== array()) {
  237. $uniqueIndexName = $this->truncateIdentifier('flow3_identity_' . $primaryTable['name']);
  238. foreach ($idProperties as $idProperty) {
  239. $primaryTable['uniqueConstraints'][$uniqueIndexName]['columns'][] = isset($metadata->columnNames[$idProperty]) ? $metadata->columnNames[$idProperty] : strtolower($idProperty);
  240. }
  241. }
  242. }
  243. $metadata->setPrimaryTable($primaryTable);
  244. // Evaluate @HasLifecycleCallbacks annotation
  245. $this->evaluateLifeCycleAnnotations($class, $metadata);
  246. }
  247. /**
  248. * Given a class name a table name is returned. That name should be reasonably unique.
  249. *
  250. * @param string $className
  251. * @param integer $lengthLimit
  252. * @return string
  253. */
  254. public function inferTableNameFromClassName($className, $lengthLimit = NULL) {
  255. return $this->truncateIdentifier(strtolower(str_replace('\\', '_', $className)), $lengthLimit, $className);
  256. }
  257. /**
  258. * Truncate an identifier if needed and append a hash to ensure uniqueness.
  259. *
  260. * @param string $identifier
  261. * @param integer $lengthLimit
  262. * @param string $hashSource
  263. * @return string
  264. */
  265. protected function truncateIdentifier($identifier, $lengthLimit = NULL, $hashSource = NULL) {
  266. if ($lengthLimit === NULL) {
  267. $lengthLimit = $this->getMaxIdentifierLength();
  268. }
  269. if (strlen($identifier) > $lengthLimit) {
  270. $identifier = substr($identifier, 0, $lengthLimit - 6) . '_' . substr(sha1($hashSource !== NULL ? $hashSource : $identifier), 0, 5);
  271. }
  272. return $identifier;
  273. }
  274. /**
  275. * Given a class and property name a table name is returned. That name should be reasonably unique.
  276. *
  277. * @param string $className
  278. * @param string $propertyName
  279. * @return string
  280. */
  281. protected function inferJoinTableNameFromClassAndPropertyName($className, $propertyName) {
  282. $prefix = $this->inferTableNameFromClassName($className);
  283. $suffix = '_' . strtolower($propertyName . '_join');
  284. if (strlen($prefix . $suffix) > $this->getMaxIdentifierLength()) {
  285. $prefix = $this->inferTableNameFromClassName($className, $this->getMaxIdentifierLength() - strlen($suffix));
  286. }
  287. return $prefix . $suffix;
  288. }
  289. /**
  290. * Build a name for a column in a jointable.
  291. *
  292. * @param string $className
  293. * @return string
  294. */
  295. protected function buildJoinTableColumnName($className) {
  296. if (preg_match('/^(?P<PackageNamespace>\w+(?:\\\\\w+)*)\\\\Domain\\\\Model\\\\(?P<ModelNamePrefix>(\w+\\\\)?)(?P<ModelName>\w+)$/', $className, $matches)) {
  297. $packageNamespaceParts = explode('\\', $matches['PackageNamespace']);
  298. $tableName = strtolower(strtr($packageNamespaceParts[count($packageNamespaceParts) - 1], '\\', '_') . ($matches['ModelNamePrefix'] !== '' ? '_' . strtr(rtrim($matches['ModelNamePrefix'], '\\'), '\\', '_') : '') . '_' . $matches['ModelName']);
  299. } else {
  300. $classNameParts = explode('\\', $className);
  301. $tableName = strtolower($classNameParts[1] . '_' . implode('_', array_slice($classNameParts, -2, 2)));
  302. }
  303. return $this->truncateIdentifier($tableName);
  304. }
  305. /**
  306. * Check if the referenced column name is set (and valid) and if not make sure
  307. * it is initialized properly.
  308. *
  309. * @param array $joinColumns
  310. * @param array $mapping
  311. * @param \ReflectionProperty $property
  312. * @param integer $direction regular or inverse mapping (use is to be coded)
  313. * @return array
  314. */
  315. protected function buildJoinColumnsIfNeeded(array $joinColumns, array $mapping, \ReflectionProperty $property, $direction = self::MAPPING_REGULAR) {
  316. if ($joinColumns === array()) {
  317. $joinColumns[] = array(
  318. 'name' => strtolower($property->getName()),
  319. 'referencedColumnName' => NULL,
  320. );
  321. }
  322. foreach ($joinColumns as &$joinColumn) {
  323. if ($joinColumn['referencedColumnName'] === NULL || $joinColumn['referencedColumnName'] === 'id') {
  324. if ($direction === self::MAPPING_REGULAR) {
  325. $idProperties = $this->reflectionService->getPropertyNamesByTag($mapping['targetEntity'], 'id');
  326. $joinColumnName = $this->buildJoinTableColumnName($mapping['targetEntity']);
  327. } else {
  328. $className = preg_replace('/' . \TYPO3\FLOW3\Object\Proxy\Compiler::ORIGINAL_CLASSNAME_SUFFIX . '$/', '', $property->getDeclaringClass()->getName());
  329. $idProperties = $this->reflectionService->getPropertyNamesByTag($className, 'id');
  330. $joinColumnName = $this->buildJoinTableColumnName($className);
  331. }
  332. if (count($idProperties) === 0) {
  333. $joinColumn['name'] = $joinColumn['name'] === NULL ? $joinColumnName : $joinColumn['name'];
  334. $joinColumn['referencedColumnName'] = strtolower('FLOW3_Persistence_Identifier');
  335. } elseif (count($idProperties) === 1) {
  336. $joinColumn['name'] = $joinColumn['name'] === NULL ? $joinColumnName : $joinColumn['name'];
  337. $joinColumn['referencedColumnName'] = strtolower(current($idProperties));
  338. }
  339. }
  340. }
  341. return $joinColumns;
  342. }
  343. /**
  344. * Evaluate the property annotations and amend the metadata accordingly.
  345. *
  346. * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
  347. * @return void
  348. * @throws \Doctrine\ORM\Mapping\MappingException
  349. */
  350. protected function evaluatePropertyAnnotations(\Doctrine\ORM\Mapping\ClassMetadataInfo $metadata) {
  351. $className = $metadata->name;
  352. $class = $metadata->getReflectionClass();
  353. $classSchema = $this->getClassSchema($className);
  354. foreach ($class->getProperties() as $property) {
  355. if (!$classSchema->hasProperty($property->getName())
  356. || $metadata->isMappedSuperclass && !$property->isPrivate()
  357. || $metadata->isInheritedField($property->getName())
  358. || $metadata->isInheritedAssociation($property->getName())) {
  359. continue;
  360. }
  361. $propertyMetaData = $classSchema->getProperty($property->getName());
  362. $mapping = array();
  363. $mapping['fieldName'] = $property->getName();
  364. $mapping['columnName'] = strtolower($property->getName());
  365. $mapping['targetEntity'] = $propertyMetaData['type'];
  366. $joinColumns = $this->evaluateJoinColumnAnnotations($property);
  367. // Field can only be annotated with one of:
  368. // @OneToOne, @OneToMany, @ManyToOne, @ManyToMany, @Column (optional)
  369. if ($oneToOneAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) {
  370. if ($oneToOneAnnotation->targetEntity) {
  371. $mapping['targetEntity'] = $oneToOneAnnotation->targetEntity;
  372. }
  373. $mapping['joinColumns'] = $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property);
  374. $mapping['mappedBy'] = $oneToOneAnnotation->mappedBy;
  375. $mapping['inversedBy'] = $oneToOneAnnotation->inversedBy;
  376. if ($oneToOneAnnotation->cascade) {
  377. $mapping['cascade'] = $oneToOneAnnotation->cascade;
  378. } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === FALSE) {
  379. $mapping['cascade'] = array('all');
  380. }
  381. if ($oneToOneAnnotation->orphanRemoval) {
  382. $mapping['orphanRemoval'] = $oneToOneAnnotation->orphanRemoval;
  383. } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === FALSE) {
  384. $mapping['orphanRemoval'] = TRUE;
  385. }
  386. $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAnnotation->fetch);
  387. $metadata->mapOneToOne($mapping);
  388. } elseif ($oneToManyAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToMany')) {
  389. $mapping['mappedBy'] = $oneToManyAnnotation->mappedBy;
  390. if ($oneToManyAnnotation->targetEntity) {
  391. $mapping['targetEntity'] = $oneToManyAnnotation->targetEntity;
  392. } elseif (isset($propertyMetaData['elementType'])) {
  393. $mapping['targetEntity'] = $propertyMetaData['elementType'];
  394. }
  395. if ($oneToManyAnnotation->cascade) {
  396. $mapping['cascade'] = $oneToManyAnnotation->cascade;
  397. } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === FALSE) {
  398. $mapping['cascade'] = array('all');
  399. }
  400. if ($oneToManyAnnotation->orphanRemoval) {
  401. $mapping['orphanRemoval'] = $oneToManyAnnotation->orphanRemoval;
  402. } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === FALSE) {
  403. $mapping['orphanRemoval'] = TRUE;
  404. }
  405. $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAnnotation->fetch);
  406. if ($orderByAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) {
  407. $mapping['orderBy'] = $orderByAnnotation->value;
  408. }
  409. $metadata->mapOneToMany($mapping);
  410. } elseif ($manyToOneAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToOne')) {
  411. if ($manyToOneAnnotation->targetEntity) {
  412. $mapping['targetEntity'] = $manyToOneAnnotation->targetEntity;
  413. }
  414. $mapping['joinColumns'] = $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property);
  415. if ($manyToOneAnnotation->cascade) {
  416. $mapping['cascade'] = $manyToOneAnnotation->cascade;
  417. } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === FALSE) {
  418. $mapping['cascade'] = array('all');
  419. }
  420. $mapping['inversedBy'] = $manyToOneAnnotation->inversedBy;
  421. $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAnnotation->fetch);
  422. $metadata->mapManyToOne($mapping);
  423. } elseif ($manyToManyAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) {
  424. if ($manyToManyAnnotation->targetEntity) {
  425. $mapping['targetEntity'] = $manyToManyAnnotation->targetEntity;
  426. } elseif (isset($propertyMetaData['elementType'])) {
  427. $mapping['targetEntity'] = $propertyMetaData['elementType'];
  428. }
  429. if ($joinTableAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinTable')) {
  430. $joinTable = $this->evaluateJoinTableAnnotation($joinTableAnnotation, $property, $className, $mapping);
  431. } else {
  432. $joinColumns = array(
  433. array(
  434. 'name' => NULL,
  435. 'referencedColumnName' => NULL,
  436. )
  437. );
  438. $joinTable = array(
  439. 'name' => $this->inferJoinTableNameFromClassAndPropertyName($className, $property->getName()),
  440. 'joinColumns' => $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property, self::MAPPING_MM_REGULAR),
  441. 'inverseJoinColumns' => $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property)
  442. );
  443. }
  444. $mapping['joinTable'] = $joinTable;
  445. $mapping['mappedBy'] = $manyToManyAnnotation->mappedBy;
  446. $mapping['inversedBy'] = $manyToManyAnnotation->inversedBy;
  447. if ($manyToManyAnnotation->cascade) {
  448. $mapping['cascade'] = $manyToManyAnnotation->cascade;
  449. } elseif ($this->isAggregateRoot($mapping['targetEntity'], $className) === FALSE) {
  450. $mapping['cascade'] = array('all');
  451. }
  452. $mapping['orphanRemoval'] = $manyToManyAnnotation->orphanRemoval;
  453. $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAnnotation->fetch);
  454. if ($orderByAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) {
  455. $mapping['orderBy'] = $orderByAnnotation->value;
  456. }
  457. $metadata->mapManyToMany($mapping);
  458. } else {
  459. $mapping['nullable'] = FALSE;
  460. if ($columnAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Column')) {
  461. $mapping['type'] = ($columnAnnotation->type === 'string') ? NULL : $columnAnnotation->type;
  462. $mapping['length'] = $columnAnnotation->length;
  463. $mapping['precision'] = $columnAnnotation->precision;
  464. $mapping['scale'] = $columnAnnotation->scale;
  465. $mapping['nullable'] = $columnAnnotation->nullable;
  466. $mapping['unique'] = $columnAnnotation->unique;
  467. if ($columnAnnotation->options) {
  468. $mapping['options'] = $columnAnnotation->options;
  469. }
  470. if (isset($columnAnnotation->name)) {
  471. $mapping['columnName'] = $columnAnnotation->name;
  472. }
  473. if (isset($columnAnnotation->columnDefinition)) {
  474. $mapping['columnDefinition'] = $columnAnnotation->columnDefinition;
  475. }
  476. }
  477. if (!isset($mapping['type'])) {
  478. switch ($propertyMetaData['type']) {
  479. case 'DateTime':
  480. $mapping['type'] = 'datetime';
  481. break;
  482. case 'string':
  483. case 'integer':
  484. case 'boolean':
  485. case 'float':
  486. case 'array':
  487. $mapping['type'] = $propertyMetaData['type'];
  488. break;
  489. default:
  490. if (strpos($propertyMetaData['type'], '\\') !== FALSE) {
  491. if ($this->reflectionService->isClassAnnotatedWith($propertyMetaData['type'], 'TYPO3\FLOW3\Annotations\ValueObject')) {
  492. $mapping['type'] = 'object';
  493. } elseif (class_exists($propertyMetaData['type'])) {
  494. throw \Doctrine\ORM\Mapping\MappingException::missingRequiredOption($property->getName(), 'OneToOne', sprintf('The property "%s" in class "%s" has a non standard data type and doesn\'t define the type of the relation. You have to use one of these annotations: @OneToOne, @OneToMany, @ManyToOne, @ManyToMany', $property->getName(), $className));
  495. }
  496. } else {
  497. throw \Doctrine\ORM\Mapping\MappingException::propertyTypeIsRequired($className, $property->getName());
  498. }
  499. }
  500. }
  501. if ($this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id') !== NULL) {
  502. $mapping['id'] = TRUE;
  503. }
  504. if ($generatedValueAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\GeneratedValue')) {
  505. $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . strtoupper($generatedValueAnnotation->strategy)));
  506. }
  507. if ($this->reflectionService->isPropertyAnnotatedWith($className, $property->getName(), 'Doctrine\ORM\Mapping\Version')) {
  508. $metadata->setVersionMapping($mapping);
  509. }
  510. $metadata->mapField($mapping);
  511. // Check for SequenceGenerator/TableGenerator definition
  512. if ($seqGeneratorAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\SequenceGenerator')) {
  513. $metadata->setSequenceGeneratorDefinition(array(
  514. 'sequenceName' => $seqGeneratorAnnotation->sequenceName,
  515. 'allocationSize' => $seqGeneratorAnnotation->allocationSize,
  516. 'initialValue' => $seqGeneratorAnnotation->initialValue
  517. ));
  518. } elseif ($this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\TableGenerator') !== NULL) {
  519. throw \Doctrine\ORM\Mapping\MappingException::tableIdGeneratorNotImplemented($className);
  520. }
  521. }
  522. }
  523. }
  524. /**
  525. * Evaluate JoinTable annotations and fill missing bits as needed.
  526. *
  527. * @param \Doctrine\ORM\Mapping\JoinTable $joinTableAnnotation
  528. * @param \ReflectionProperty $property
  529. * @param string $className
  530. * @param array $mapping
  531. * @return array
  532. */
  533. protected function evaluateJoinTableAnnotation(\Doctrine\ORM\Mapping\JoinTable $joinTableAnnotation, \ReflectionProperty $property, $className, array $mapping) {
  534. $joinTable = array(
  535. 'name' => $joinTableAnnotation->name,
  536. 'schema' => $joinTableAnnotation->schema
  537. );
  538. if ($joinTable['name'] === NULL) {
  539. $joinTable['name'] = $this->inferJoinTableNameFromClassAndPropertyName($className, $property->getName());
  540. }
  541. foreach ($joinTableAnnotation->joinColumns as $joinColumn) {
  542. $joinTable['joinColumns'][] = array(
  543. 'name' => $joinColumn->name,
  544. 'referencedColumnName' => $joinColumn->referencedColumnName,
  545. 'unique' => $joinColumn->unique,
  546. 'nullable' => $joinColumn->nullable,
  547. 'onDelete' => $joinColumn->onDelete,
  548. 'columnDefinition' => $joinColumn->columnDefinition,
  549. );
  550. }
  551. if (array_key_exists('joinColumns', $joinTable)) {
  552. $joinTable['joinColumns'] = $this->buildJoinColumnsIfNeeded($joinTable['joinColumns'], $mapping, $property, self::MAPPING_MM_REGULAR);
  553. } else {
  554. $joinColumns = array(
  555. array(
  556. 'name' => NULL,
  557. 'referencedColumnName' => NULL,
  558. )
  559. );
  560. $joinTable['joinColumns'] = $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property, self::MAPPING_MM_REGULAR);
  561. }
  562. foreach ($joinTableAnnotation->inverseJoinColumns as $joinColumn) {
  563. $joinTable['inverseJoinColumns'][] = array(
  564. 'name' => $joinColumn->name,
  565. 'referencedColumnName' => $joinColumn->referencedColumnName,
  566. 'unique' => $joinColumn->unique,
  567. 'nullable' => $joinColumn->nullable,
  568. 'onDelete' => $joinColumn->onDelete,
  569. 'columnDefinition' => $joinColumn->columnDefinition,
  570. );
  571. }
  572. if (array_key_exists('inverseJoinColumns', $joinTable)) {
  573. $joinTable['inverseJoinColumns'] = $this->buildJoinColumnsIfNeeded($joinTable['inverseJoinColumns'], $mapping, $property);
  574. } else {
  575. $joinColumns = array(
  576. array(
  577. 'name' => NULL,
  578. 'referencedColumnName' => NULL,
  579. )
  580. );
  581. $joinTable['inverseJoinColumns'] = $this->buildJoinColumnsIfNeeded($joinColumns, $mapping, $property);
  582. }
  583. return $joinTable;
  584. }
  585. /**
  586. * Check for and build JoinColummn/JoinColumns annotations.
  587. *
  588. * If no annotations are found, a default is returned.
  589. *
  590. * @param \ReflectionProperty $property
  591. * @return array
  592. */
  593. protected function evaluateJoinColumnAnnotations(\ReflectionProperty $property) {
  594. $joinColumns = array();
  595. if ($joinColumnAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) {
  596. $joinColumns[] = array(
  597. 'name' => $joinColumnAnnotation->name === NULL ? strtolower($property->getName()) : $joinColumnAnnotation->name,
  598. 'referencedColumnName' => $joinColumnAnnotation->referencedColumnName,
  599. 'unique' => $joinColumnAnnotation->unique,
  600. 'nullable' => $joinColumnAnnotation->nullable,
  601. 'onDelete' => $joinColumnAnnotation->onDelete,
  602. 'columnDefinition' => $joinColumnAnnotation->columnDefinition,
  603. );
  604. } else if ($joinColumnsAnnotation = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumns')) {
  605. foreach ($joinColumnsAnnotation->value as $joinColumnAnnotation) {
  606. $joinColumns[] = array(
  607. 'name' => $joinColumnAnnotation->name === NULL ? strtolower($property->getName()) : $joinColumnAnnotation->name,
  608. 'referencedColumnName' => $joinColumnAnnotation->referencedColumnName,
  609. 'unique' => $joinColumnAnnotation->unique,
  610. 'nullable' => $joinColumnAnnotation->nullable,
  611. 'onDelete' => $joinColumnAnnotation->onDelete,
  612. 'columnDefinition' => $joinColumnAnnotation->columnDefinition,
  613. );
  614. }
  615. }
  616. return $joinColumns;
  617. }
  618. /**
  619. * Evaluate the lifecycle annotations and amend the metadata accordingly.
  620. *
  621. * @param \ReflectionClass $class
  622. * @param \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata
  623. * @return void
  624. */
  625. protected function evaluateLifeCycleAnnotations(\ReflectionClass $class, \Doctrine\ORM\Mapping\ClassMetadataInfo $metadata) {
  626. foreach ($class->getMethods() as $method) {
  627. if ($method->isPublic()) {
  628. $annotations = $this->reader->getMethodAnnotations($method);
  629. if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) {
  630. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist);
  631. }
  632. if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) {
  633. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist);
  634. }
  635. if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) {
  636. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate);
  637. }
  638. if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) {
  639. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate);
  640. }
  641. if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) {
  642. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove);
  643. }
  644. if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) {
  645. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove);
  646. }
  647. if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) {
  648. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad);
  649. }
  650. if (isset($annotations['Doctrine\ORM\Mapping\PreFlush'])) {
  651. $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preFlush);
  652. }
  653. }
  654. }
  655. // FIXME this can be removed again once Doctrine is fixed (see fixMethodsAndAdvicesArrayForDoctrineProxiesCode())
  656. $metadata->addLifecycleCallback('FLOW3_Aop_Proxy_fixMethodsAndAdvicesArrayForDoctrineProxies', \Doctrine\ORM\Events::postLoad);
  657. // FIXME this can be removed again once Doctrine is fixed (see fixInjectedPropertiesForDoctrineProxiesCode())
  658. $metadata->addLifecycleCallback('FLOW3_Aop_Proxy_fixInjectedPropertiesForDoctrineProxies', \Doctrine\ORM\Events::postLoad);
  659. }
  660. /**
  661. * Derive maximum identifier length from doctrine DBAL
  662. *
  663. * @return integer
  664. */
  665. protected function getMaxIdentifierLength() {
  666. if ($this->tableNameLengthLimit === NULL) {
  667. $this->tableNameLengthLimit = $this->entityManager->getConnection()->getDatabasePlatform()->getMaxIdentifierLength();
  668. }
  669. return $this->tableNameLengthLimit;
  670. }
  671. /**
  672. * Whether the class with the specified name should have its metadata loaded.
  673. * This is only the case if it is either mapped as an Entity or a
  674. * MappedSuperclass (i.e. is not transient).
  675. *
  676. * @param string $className
  677. * @return boolean
  678. */
  679. public function isTransient($className) {
  680. return strpos($className, \TYPO3\FLOW3\Object\Proxy\Compiler::ORIGINAL_CLASSNAME_SUFFIX) !== FALSE ||
  681. (
  682. !$this->reflectionService->isClassAnnotatedWith($className, 'TYPO3\FLOW3\Annotations\Entity') &&
  683. !$this->reflectionService->isClassAnnotatedWith($className, 'TYPO3\FLOW3\Annotations\ValueObject') &&
  684. !$this->reflectionService->isClassAnnotatedWith($className, 'Doctrine\ORM\Mapping\Entity') &&
  685. !$this->reflectionService->isClassAnnotatedWith($className, 'Doctrine\ORM\Mapping\MappedSuperclass')
  686. );
  687. }
  688. /**
  689. * Returns the names of all mapped (non-transient) classes known to this driver.
  690. *
  691. * @return array
  692. */
  693. public function getAllClassNames() {
  694. if (is_array($this->classNames)) {
  695. return $this->classNames;
  696. }
  697. $this->classNames = array_merge(
  698. $this->reflectionService->getClassNamesByAnnotation('TYPO3\FLOW3\Annotations\ValueObject'),
  699. $this->reflectionService->getClassNamesByAnnotation('TYPO3\FLOW3\Annotations\Entity'),
  700. $this->reflectionService->getClassNamesByAnnotation('Doctrine\ORM\Mapping\Entity'),
  701. $this->reflectionService->getClassNamesByAnnotation('Doctrine\ORM\Mapping\MappedSuperclass')
  702. );
  703. $this->classNames = array_filter($this->classNames,
  704. function ($className) {
  705. return !interface_exists($className, FALSE)
  706. && strpos($className, \TYPO3\FLOW3\Object\Proxy\Compiler::ORIGINAL_CLASSNAME_SUFFIX) === FALSE;
  707. }
  708. );
  709. return $this->classNames;
  710. }
  711. /**
  712. * Attempts to resolve the fetch mode.
  713. *
  714. * @param string $className The class name
  715. * @param string $fetchMode The fetch mode
  716. * @return integer The fetch mode as defined in ClassMetadata
  717. * @throws \Doctrine\ORM\Mapping\MappingException If the fetch mode is not valid
  718. */
  719. private function getFetchMode($className, $fetchMode) {
  720. $fetchMode = strtoupper($fetchMode);
  721. if (!defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) {
  722. throw \Doctrine\ORM\Mapping\MappingException::invalidFetchMode($className, $fetchMode);
  723. }
  724. return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode);
  725. }
  726. /**
  727. * Checks if the specified class has a property annotated with Id
  728. *
  729. * @param string $className Name of the class to check against
  730. * @param string $methodName Name of the method to check against
  731. * @param string $methodDeclaringClassName Name of the class the method was originally declared in
  732. * @param mixed $pointcutQueryIdentifier Some identifier for this query - must at least differ from a previous identifier. Used for circular reference detection.
  733. * @return boolean TRUE if the class has *no* Id properties
  734. */
  735. public function matches($className, $methodName, $methodDeclaringClassName, $pointcutQueryIdentifier) {
  736. $class = new \ReflectionClass($className);
  737. foreach ($class->getProperties() as $property) {
  738. if ($this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id') !== NULL) {
  739. return FALSE;
  740. }
  741. }
  742. return TRUE;
  743. }
  744. /**
  745. * Returns TRUE if this filter holds runtime evaluations for a previously matched pointcut
  746. *
  747. * @return boolean TRUE if this filter has runtime evaluations
  748. */
  749. public function hasRuntimeEvaluationsDefinition() {
  750. return FALSE;
  751. }
  752. /**
  753. * Returns runtime evaluations for a previously matched pointcut
  754. *
  755. * @return array Runtime evaluations
  756. */
  757. public function getRuntimeEvaluationsDefinition() {
  758. return array();
  759. }
  760. /**
  761. * This method is used to optimize the matching process.
  762. *
  763. * @param \TYPO3\FLOW3\Aop\Builder\ClassNameIndex $classNameIndex
  764. * @return \TYPO3\FLOW3\Aop\Builder\ClassNameIndex
  765. */
  766. public function reduceTargetClassNames(\TYPO3\FLOW3\Aop\Builder\ClassNameIndex $classNameIndex) {
  767. return $classNameIndex;
  768. }
  769. }
  770. ?>