3 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15 * This software consists of voluntary contributions made by many individuals
16 * and is licensed under the MIT license. For more information, see
17 * <http://www.doctrine-project.org>.
20 namespace Doctrine\ORM\Mapping\Driver;
22 use Doctrine\Common\Annotations\AnnotationReader,
23 Doctrine\ORM\Mapping\MappingException,
24 Doctrine\ORM\Mapping\JoinColumn,
25 Doctrine\ORM\Mapping\Column,
26 Doctrine\Common\Persistence\Mapping\ClassMetadata,
27 Doctrine\Common\Persistence\Mapping\Driver\AnnotationDriver as AbstractAnnotationDriver;
30 * The AnnotationDriver reads the mapping metadata from docblock annotations.
33 * @author Benjamin Eberlei <kontakt@beberlei.de>
34 * @author Guilherme Blanco <guilhermeblanco@hotmail.com>
35 * @author Jonathan H. Wage <jonwage@gmail.com>
36 * @author Roman Borschel <roman@code-factory.org>
38 class AnnotationDriver extends AbstractAnnotationDriver
43 protected $entityAnnotationClasses = array(
44 'Doctrine\ORM\Mapping\Entity' => 1,
45 'Doctrine\ORM\Mapping\MappedSuperclass' => 2,
51 public function loadMetadataForClass($className, ClassMetadata $metadata)
53 /* @var $metadata \Doctrine\ORM\Mapping\ClassMetadataInfo */
54 $class = $metadata->getReflectionClass();
56 // this happens when running annotation driver in combination with
57 // static reflection services. This is not the nicest fix
58 $class = new \ReflectionClass($metadata->name);
61 $classAnnotations = $this->reader->getClassAnnotations($class);
63 if ($classAnnotations) {
64 foreach ($classAnnotations as $key => $annot) {
65 if ( ! is_numeric($key)) {
69 $classAnnotations[get_class($annot)] = $annot;
73 // Evaluate Entity annotation
74 if (isset($classAnnotations['Doctrine\ORM\Mapping\Entity'])) {
75 $entityAnnot = $classAnnotations['Doctrine\ORM\Mapping\Entity'];
76 if ($entityAnnot->repositoryClass !== null) {
77 $metadata->setCustomRepositoryClass($entityAnnot->repositoryClass);
79 if ($entityAnnot->readOnly) {
80 $metadata->markReadOnly();
82 } else if (isset($classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'])) {
83 $mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
84 $metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass);
85 $metadata->isMappedSuperclass = true;
87 throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
90 // Evaluate Table annotation
91 if (isset($classAnnotations['Doctrine\ORM\Mapping\Table'])) {
92 $tableAnnot = $classAnnotations['Doctrine\ORM\Mapping\Table'];
93 $primaryTable = array(
94 'name' => $tableAnnot->name,
95 'schema' => $tableAnnot->schema
98 if ($tableAnnot->indexes !== null) {
99 foreach ($tableAnnot->indexes as $indexAnnot) {
100 $index = array('columns' => $indexAnnot->columns);
102 if ( ! empty($indexAnnot->name)) {
103 $primaryTable['indexes'][$indexAnnot->name] = $index;
105 $primaryTable['indexes'][] = $index;
110 if ($tableAnnot->uniqueConstraints !== null) {
111 foreach ($tableAnnot->uniqueConstraints as $uniqueConstraintAnnot) {
112 $uniqueConstraint = array('columns' => $uniqueConstraintAnnot->columns);
114 if ( ! empty($uniqueConstraintAnnot->name)) {
115 $primaryTable['uniqueConstraints'][$uniqueConstraintAnnot->name] = $uniqueConstraint;
117 $primaryTable['uniqueConstraints'][] = $uniqueConstraint;
122 if ($tableAnnot->options !== null) {
123 $primaryTable['options'] = $tableAnnot->options;
126 $metadata->setPrimaryTable($primaryTable);
129 // Evaluate NamedNativeQueries annotation
130 if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'])) {
131 $namedNativeQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedNativeQueries'];
133 foreach ($namedNativeQueriesAnnot->value as $namedNativeQuery) {
134 $metadata->addNamedNativeQuery(array(
135 'name' => $namedNativeQuery->name,
136 'query' => $namedNativeQuery->query,
137 'resultClass' => $namedNativeQuery->resultClass,
138 'resultSetMapping' => $namedNativeQuery->resultSetMapping,
143 // Evaluate SqlResultSetMappings annotation
144 if (isset($classAnnotations['Doctrine\ORM\Mapping\SqlResultSetMappings'])) {
145 $sqlResultSetMappingsAnnot = $classAnnotations['Doctrine\ORM\Mapping\SqlResultSetMappings'];
147 foreach ($sqlResultSetMappingsAnnot->value as $resultSetMapping) {
150 foreach ($resultSetMapping->entities as $entityResultAnnot) {
151 $entityResult = array(
153 'entityClass' => $entityResultAnnot->entityClass,
154 'discriminatorColumn' => $entityResultAnnot->discriminatorColumn,
157 foreach ($entityResultAnnot->fields as $fieldResultAnnot) {
158 $entityResult['fields'][] = array(
159 'name' => $fieldResultAnnot->name,
160 'column' => $fieldResultAnnot->column
164 $entities[] = $entityResult;
167 foreach ($resultSetMapping->columns as $columnResultAnnot) {
169 'name' => $columnResultAnnot->name,
173 $metadata->addSqlResultSetMapping(array(
174 'name' => $resultSetMapping->name,
175 'entities' => $entities,
176 'columns' => $columns
181 // Evaluate NamedQueries annotation
182 if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedQueries'])) {
183 $namedQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedQueries'];
185 if ( ! is_array($namedQueriesAnnot->value)) {
186 throw new \UnexpectedValueException("@NamedQueries should contain an array of @NamedQuery annotations.");
189 foreach ($namedQueriesAnnot->value as $namedQuery) {
190 if ( ! ($namedQuery instanceof \Doctrine\ORM\Mapping\NamedQuery)) {
191 throw new \UnexpectedValueException("@NamedQueries should contain an array of @NamedQuery annotations.");
193 $metadata->addNamedQuery(array(
194 'name' => $namedQuery->name,
195 'query' => $namedQuery->query
200 // Evaluate InheritanceType annotation
201 if (isset($classAnnotations['Doctrine\ORM\Mapping\InheritanceType'])) {
202 $inheritanceTypeAnnot = $classAnnotations['Doctrine\ORM\Mapping\InheritanceType'];
203 $metadata->setInheritanceType(constant('Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_' . $inheritanceTypeAnnot->value));
205 if ($metadata->inheritanceType != \Doctrine\ORM\Mapping\ClassMetadata::INHERITANCE_TYPE_NONE) {
206 // Evaluate DiscriminatorColumn annotation
207 if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn'])) {
208 $discrColumnAnnot = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorColumn'];
209 $metadata->setDiscriminatorColumn(array(
210 'name' => $discrColumnAnnot->name,
211 'type' => $discrColumnAnnot->type,
212 'length' => $discrColumnAnnot->length,
213 'columnDefinition' => $discrColumnAnnot->columnDefinition
216 $metadata->setDiscriminatorColumn(array('name' => 'dtype', 'type' => 'string', 'length' => 255));
219 // Evaluate DiscriminatorMap annotation
220 if (isset($classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap'])) {
221 $discrMapAnnot = $classAnnotations['Doctrine\ORM\Mapping\DiscriminatorMap'];
222 $metadata->setDiscriminatorMap($discrMapAnnot->value);
228 // Evaluate DoctrineChangeTrackingPolicy annotation
229 if (isset($classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy'])) {
230 $changeTrackingAnnot = $classAnnotations['Doctrine\ORM\Mapping\ChangeTrackingPolicy'];
231 $metadata->setChangeTrackingPolicy(constant('Doctrine\ORM\Mapping\ClassMetadata::CHANGETRACKING_' . $changeTrackingAnnot->value));
234 // Evaluate annotations on properties/fields
235 /* @var $property \ReflectionProperty */
236 foreach ($class->getProperties() as $property) {
237 if ($metadata->isMappedSuperclass && ! $property->isPrivate()
239 $metadata->isInheritedField($property->name)
241 $metadata->isInheritedAssociation($property->name)) {
246 $mapping['fieldName'] = $property->getName();
248 // Check for JoinColummn/JoinColumns annotations
249 $joinColumns = array();
251 if ($joinColumnAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumn')) {
252 $joinColumns[] = $this->joinColumnToArray($joinColumnAnnot);
253 } else if ($joinColumnsAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinColumns')) {
254 foreach ($joinColumnsAnnot->value as $joinColumn) {
255 $joinColumns[] = $this->joinColumnToArray($joinColumn);
259 // Field can only be annotated with one of:
260 // @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany
261 if ($columnAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Column')) {
262 if ($columnAnnot->type == null) {
263 throw MappingException::propertyTypeIsRequired($className, $property->getName());
266 $mapping = $this->columnToArray($property->getName(), $columnAnnot);
268 if ($idAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) {
269 $mapping['id'] = true;
272 if ($generatedValueAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\GeneratedValue')) {
273 $metadata->setIdGeneratorType(constant('Doctrine\ORM\Mapping\ClassMetadata::GENERATOR_TYPE_' . $generatedValueAnnot->strategy));
276 if ($this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Version')) {
277 $metadata->setVersionMapping($mapping);
280 $metadata->mapField($mapping);
282 // Check for SequenceGenerator/TableGenerator definition
283 if ($seqGeneratorAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\SequenceGenerator')) {
284 $metadata->setSequenceGeneratorDefinition(array(
285 'sequenceName' => $seqGeneratorAnnot->sequenceName,
286 'allocationSize' => $seqGeneratorAnnot->allocationSize,
287 'initialValue' => $seqGeneratorAnnot->initialValue
289 } else if ($this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\TableGenerator')) {
290 throw MappingException::tableIdGeneratorNotImplemented($className);
291 } else if ($customGeneratorAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\CustomIdGenerator')) {
292 $metadata->setCustomGeneratorDefinition(array(
293 'class' => $customGeneratorAnnot->class
296 } else if ($oneToOneAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) {
297 if ($idAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) {
298 $mapping['id'] = true;
301 $mapping['targetEntity'] = $oneToOneAnnot->targetEntity;
302 $mapping['joinColumns'] = $joinColumns;
303 $mapping['mappedBy'] = $oneToOneAnnot->mappedBy;
304 $mapping['inversedBy'] = $oneToOneAnnot->inversedBy;
305 $mapping['cascade'] = $oneToOneAnnot->cascade;
306 $mapping['orphanRemoval'] = $oneToOneAnnot->orphanRemoval;
307 $mapping['fetch'] = $this->getFetchMode($className, $oneToOneAnnot->fetch);
308 $metadata->mapOneToOne($mapping);
309 } else if ($oneToManyAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToMany')) {
310 $mapping['mappedBy'] = $oneToManyAnnot->mappedBy;
311 $mapping['targetEntity'] = $oneToManyAnnot->targetEntity;
312 $mapping['cascade'] = $oneToManyAnnot->cascade;
313 $mapping['indexBy'] = $oneToManyAnnot->indexBy;
314 $mapping['orphanRemoval'] = $oneToManyAnnot->orphanRemoval;
315 $mapping['fetch'] = $this->getFetchMode($className, $oneToManyAnnot->fetch);
317 if ($orderByAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) {
318 $mapping['orderBy'] = $orderByAnnot->value;
321 $metadata->mapOneToMany($mapping);
322 } else if ($manyToOneAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToOne')) {
323 if ($idAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) {
324 $mapping['id'] = true;
327 $mapping['joinColumns'] = $joinColumns;
328 $mapping['cascade'] = $manyToOneAnnot->cascade;
329 $mapping['inversedBy'] = $manyToOneAnnot->inversedBy;
330 $mapping['targetEntity'] = $manyToOneAnnot->targetEntity;
331 $mapping['fetch'] = $this->getFetchMode($className, $manyToOneAnnot->fetch);
332 $metadata->mapManyToOne($mapping);
333 } else if ($manyToManyAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) {
334 $joinTable = array();
336 if ($joinTableAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\JoinTable')) {
338 'name' => $joinTableAnnot->name,
339 'schema' => $joinTableAnnot->schema
342 foreach ($joinTableAnnot->joinColumns as $joinColumn) {
343 $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
346 foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) {
347 $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
351 $mapping['joinTable'] = $joinTable;
352 $mapping['targetEntity'] = $manyToManyAnnot->targetEntity;
353 $mapping['mappedBy'] = $manyToManyAnnot->mappedBy;
354 $mapping['inversedBy'] = $manyToManyAnnot->inversedBy;
355 $mapping['cascade'] = $manyToManyAnnot->cascade;
356 $mapping['indexBy'] = $manyToManyAnnot->indexBy;
357 $mapping['orphanRemoval'] = $manyToManyAnnot->orphanRemoval;
358 $mapping['fetch'] = $this->getFetchMode($className, $manyToManyAnnot->fetch);
360 if ($orderByAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OrderBy')) {
361 $mapping['orderBy'] = $orderByAnnot->value;
364 $metadata->mapManyToMany($mapping);
368 // Evaluate AssociationOverrides annotation
369 if (isset($classAnnotations['Doctrine\ORM\Mapping\AssociationOverrides'])) {
370 $associationOverridesAnnot = $classAnnotations['Doctrine\ORM\Mapping\AssociationOverrides'];
372 foreach ($associationOverridesAnnot->value as $associationOverride) {
374 $fieldName = $associationOverride->name;
376 // Check for JoinColummn/JoinColumns annotations
377 if ($associationOverride->joinColumns) {
378 $joinColumns = array();
379 foreach ($associationOverride->joinColumns as $joinColumn) {
380 $joinColumns[] = $this->joinColumnToArray($joinColumn);
382 $override['joinColumns'] = $joinColumns;
385 // Check for JoinTable annotations
386 if ($associationOverride->joinTable) {
388 $joinTableAnnot = $associationOverride->joinTable;
390 'name' => $joinTableAnnot->name,
391 'schema' => $joinTableAnnot->schema
394 foreach ($joinTableAnnot->joinColumns as $joinColumn) {
395 $joinTable['joinColumns'][] = $this->joinColumnToArray($joinColumn);
398 foreach ($joinTableAnnot->inverseJoinColumns as $joinColumn) {
399 $joinTable['inverseJoinColumns'][] = $this->joinColumnToArray($joinColumn);
402 $override['joinTable'] = $joinTable;
405 $metadata->setAssociationOverride($fieldName, $override);
409 // Evaluate AttributeOverrides annotation
410 if (isset($classAnnotations['Doctrine\ORM\Mapping\AttributeOverrides'])) {
411 $attributeOverridesAnnot = $classAnnotations['Doctrine\ORM\Mapping\AttributeOverrides'];
412 foreach ($attributeOverridesAnnot->value as $attributeOverrideAnnot) {
413 $attributeOverride = $this->columnToArray($attributeOverrideAnnot->name, $attributeOverrideAnnot->column);
414 $metadata->setAttributeOverride($attributeOverrideAnnot->name, $attributeOverride);
418 // Evaluate @HasLifecycleCallbacks annotation
419 if (isset($classAnnotations['Doctrine\ORM\Mapping\HasLifecycleCallbacks'])) {
420 /* @var $method \ReflectionMethod */
421 foreach ($class->getMethods() as $method) {
422 // filter for the declaring class only, callbacks from parents will already be registered.
423 if ($method->isPublic() && $method->getDeclaringClass()->getName() == $class->name) {
424 $annotations = $this->reader->getMethodAnnotations($method);
427 foreach ($annotations as $key => $annot) {
428 if ( ! is_numeric($key)) {
431 $annotations[get_class($annot)] = $annot;
435 if (isset($annotations['Doctrine\ORM\Mapping\PrePersist'])) {
436 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::prePersist);
439 if (isset($annotations['Doctrine\ORM\Mapping\PostPersist'])) {
440 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postPersist);
443 if (isset($annotations['Doctrine\ORM\Mapping\PreUpdate'])) {
444 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preUpdate);
447 if (isset($annotations['Doctrine\ORM\Mapping\PostUpdate'])) {
448 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postUpdate);
451 if (isset($annotations['Doctrine\ORM\Mapping\PreRemove'])) {
452 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preRemove);
455 if (isset($annotations['Doctrine\ORM\Mapping\PostRemove'])) {
456 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postRemove);
459 if (isset($annotations['Doctrine\ORM\Mapping\PostLoad'])) {
460 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::postLoad);
463 if (isset($annotations['Doctrine\ORM\Mapping\PreFlush'])) {
464 $metadata->addLifecycleCallback($method->getName(), \Doctrine\ORM\Events::preFlush);
472 * Attempts to resolve the fetch mode.
474 * @param string $className The class name
475 * @param string $fetchMode The fetch mode
476 * @return integer The fetch mode as defined in ClassMetadata
477 * @throws MappingException If the fetch mode is not valid
479 private function getFetchMode($className, $fetchMode)
481 if( ! defined('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode)) {
482 throw MappingException::invalidFetchMode($className, $fetchMode);
485 return constant('Doctrine\ORM\Mapping\ClassMetadata::FETCH_' . $fetchMode);
489 * Parse the given JoinColumn as array
491 * @param JoinColumn $joinColumn
494 private function joinColumnToArray(JoinColumn $joinColumn)
497 'name' => $joinColumn->name,
498 'unique' => $joinColumn->unique,
499 'nullable' => $joinColumn->nullable,
500 'onDelete' => $joinColumn->onDelete,
501 'columnDefinition' => $joinColumn->columnDefinition,
502 'referencedColumnName' => $joinColumn->referencedColumnName,
507 * Parse the given Column as array
509 * @param string $fieldName
510 * @param Column $column
513 private function columnToArray($fieldName, Column $column)
516 'fieldName' => $fieldName,
517 'type' => $column->type,
518 'scale' => $column->scale,
519 'length' => $column->length,
520 'unique' => $column->unique,
521 'nullable' => $column->nullable,
522 'precision' => $column->precision
525 if ($column->options) {
526 $mapping['options'] = $column->options;
529 if (isset($column->name)) {
530 $mapping['columnName'] = $column->name;
533 if (isset($column->columnDefinition)) {
534 $mapping['columnDefinition'] = $column->columnDefinition;
541 * Factory method for the Annotation Driver
543 * @param array|string $paths
544 * @param AnnotationReader $reader
545 * @return AnnotationDriver
547 static public function create($paths = array(), AnnotationReader $reader = null)
549 if ($reader == null) {
550 $reader = new AnnotationReader();
553 return new self($reader, $paths);