diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index a1da3ffcfd1..d661eb7a319 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -136,6 +136,11 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS $this->completeIdGeneratorMapping($class); } + foreach ($class->embeddedClasses as $property => $embeddableClass) { + $embeddableMetadata = $this->getMetadataFor($embeddableClass); + $class->inlineEmbeddable($property, $embeddableMetadata); + } + if ($parent && $parent->isInheritanceTypeSingleTable()) { $class->setPrimaryTable($parent->table); } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 63c324a624e..9ab80a72c15 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -267,6 +267,13 @@ class ClassMetadataInfo implements ClassMetadata */ public $subClasses = array(); + /** + * READ-ONLY: The names of all embedded classes based on properties. + * + * @var array + */ + public $embeddedClasses = array(); + /** * READ-ONLY: The named queries allowed to be called directly from Repository. * @@ -887,6 +894,15 @@ public function wakeupReflection($reflService) $this->reflClass = $reflService->getClass($this->name); foreach ($this->fieldMappings as $field => $mapping) { + if (isset($mapping['declaredField'])) { + $this->reflFields[$field] = new ReflectionProxy( + $reflService->getAccessibleProperty($this->name, $mapping['declaredField']), + $reflService->getAccessibleProperty($this->embeddedClasses[$mapping['declaredField']], $mapping['originalField']), + $this->embeddedClasses[$mapping['declaredField']] + ); + continue; + } + $this->reflFields[$field] = isset($mapping['declared']) ? $reflService->getAccessibleProperty($mapping['declared'], $field) : $reflService->getAccessibleProperty($this->name, $field); @@ -2166,9 +2182,8 @@ private function _isInheritanceType($type) public function mapField(array $mapping) { $this->_validateAndCompleteFieldMapping($mapping); - if (isset($this->fieldMappings[$mapping['fieldName']]) || isset($this->associationMappings[$mapping['fieldName']])) { - throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']); - } + $this->assertFieldNotMapped($mapping['fieldName']); + $this->fieldMappings[$mapping['fieldName']] = $mapping; } @@ -2416,9 +2431,7 @@ protected function _storeAssociationMapping(array $assocMapping) { $sourceFieldName = $assocMapping['fieldName']; - if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) { - throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName); - } + $this->assertFieldNotMapped($sourceFieldName); $this->associationMappings[$sourceFieldName] = $assocMapping; } @@ -3030,4 +3043,49 @@ public function fullyQualifiedClassName($className) return $className; } + + /** + * Map Embedded Class + * + * @array $mapping + * @return void + */ + public function mapEmbedded(array $mapping) + { + $this->assertFieldNotMapped($mapping['fieldName']); + + $this->embeddedClasses[$mapping['fieldName']] = $this->fullyQualifiedClassName($mapping['class']); + } + + /** + * Inline the embeddable class + * + * @param string $property + * @param ClassMetadataInfo $embeddable + */ + public function inlineEmbeddable($property, ClassMetadataInfo $embeddable) + { + foreach ($embeddable->fieldMappings as $fieldMapping) { + $fieldMapping['declaredField'] = $property; + $fieldMapping['originalField'] = $fieldMapping['fieldName']; + $fieldMapping['fieldName'] = $property . $fieldMapping['fieldName']; + $fieldMapping['columnName'] = $property . "_" . $fieldMapping['columnName']; + + $this->mapField($fieldMapping); + } + } + + /** + * @param string $fieldName + * @throws MappingException + */ + private function assertFieldNotMapped($fieldName) + { + if (isset($this->fieldMappings[$fieldName]) || + isset($this->associationMappings[$fieldName]) || + isset($this->embeddedClasses[$fieldName])) { + + throw MappingException::duplicateFieldMapping($this->name, $fieldName); + } + } } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index 56b693e678e..2529edeedf5 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -366,6 +366,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata) } $metadata->mapManyToMany($mapping); + } else if ($embeddedAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Embedded')) { + $mapping['class'] = $embeddedAnnot->class; + $metadata->mapEmbedded($mapping); } } diff --git a/lib/Doctrine/ORM/Mapping/ReflectionProxy.php b/lib/Doctrine/ORM/Mapping/ReflectionProxy.php new file mode 100644 index 00000000000..9d391d9a873 --- /dev/null +++ b/lib/Doctrine/ORM/Mapping/ReflectionProxy.php @@ -0,0 +1,64 @@ +. + */ + +namespace Doctrine\ORM\Mapping; + +/** + * Acts as a proxy to a nested Property structure, making it look like + * just a single scalar property. + * + * This way value objects "just work" without UnitOfWork, Persisters or Hydrators + * needing any changes. + */ +class ReflectionProxy +{ + private $parentProperty; + private $childProperty; + private $class; + + public function __construct($parentProperty, $childProperty, $class) + { + $this->parentProperty = $parentProperty; + $this->childProperty = $childProperty; + $this->class = $class; + } + + public function getValue($object) + { + $embeddedObject = $this->parentProperty->getValue($object); + + if ($embeddedObject === null) { + return null; + } + + return $this->childProperty->getValue($embeddedObject); + } + + public function setValue($object, $value) + { + $embeddedObject = $this->parentProperty->getValue($object); + + if ($embeddedObject === null) { + $embeddedObject = new $this->class; // TODO + $this->parentProperty->setValue($object, $embeddedObject); + } + + $this->childProperty->setValue($embeddedObject, $value); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php b/tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php index ac0f332a1d3..26f129c7158 100644 --- a/tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php @@ -32,7 +32,11 @@ public function testMetadata() $this->_em->clear(); $person = $this->_em->find(DDC93Person::CLASSNAME, $person->id); + $this->assertInstanceOf(DDC93Address::CLASSNAME, $person->address); + $this->assertEquals('United States of Tara Street', $person->address->street); + $this->assertEquals('12345', $person->address->zip); + $this->assertEquals('funkytown', $person->address->city); } }