1
0
mirror of synced 2025-02-22 15:13:13 +03:00

add support for nesting embeddables

This commit is contained in:
Steve Müller 2014-08-11 16:53:18 +02:00
parent 723529ffff
commit bca9d31531
5 changed files with 212 additions and 57 deletions

View File

@ -142,14 +142,22 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
} }
if (!$class->isMappedSuperclass) { if (!$class->isMappedSuperclass) {
foreach ($class->embeddedClasses as $property => $embeddableClass) { foreach ($class->embeddedClasses as $property => $embeddableClass) {
if (isset($embeddableClass['inherited'])) { if (isset($embeddableClass['inherited'])) {
continue; continue;
} }
if ($embeddableClass['class'] === $class->name) {
throw MappingException::infiniteEmbeddableNesting($class->name, $property);
}
$embeddableMetadata = $this->getMetadataFor($embeddableClass['class']); $embeddableMetadata = $this->getMetadataFor($embeddableClass['class']);
if ($embeddableMetadata->isEmbeddedClass) {
$this->addNestedEmbeddedClasses($embeddableMetadata, $class, $property);
}
$class->inlineEmbeddable($property, $embeddableMetadata); $class->inlineEmbeddable($property, $embeddableMetadata);
} }
} }
@ -370,6 +378,34 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
} }
} }
/**
* Adds nested embedded classes metadata to a parent class.
*
* @param ClassMetadata $subClass Sub embedded class metadata to add nested embedded classes metadata from.
* @param ClassMetadata $parentClass Parent class to add nested embedded classes metadata to.
* @param string $prefix Embedded classes' prefix to use for nested embedded classes field names.
*/
private function addNestedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass, $prefix)
{
foreach ($subClass->embeddedClasses as $property => $embeddableClass) {
if (isset($embeddableClass['inherited'])) {
continue;
}
$embeddableMetadata = $this->getMetadataFor($embeddableClass['class']);
$parentClass->mapEmbedded(array(
'fieldName' => $prefix . '.' . $property,
'class' => $embeddableMetadata->name,
'columnPrefix' => $embeddableClass['columnPrefix'],
'declaredField' => $embeddableClass['declaredField']
? $prefix . '.' . $embeddableClass['declaredField']
: $prefix,
'originalField' => $embeddableClass['originalField'] ?: $property,
));
}
}
/** /**
* Adds inherited named queries to the subclass mapping. * Adds inherited named queries to the subclass mapping.
* *

View File

@ -929,15 +929,31 @@ class ClassMetadataInfo implements ClassMetadata
// Restore ReflectionClass and properties // Restore ReflectionClass and properties
$this->reflClass = $reflService->getClass($this->name); $this->reflClass = $reflService->getClass($this->name);
$parentReflFields = array();
foreach ($this->embeddedClasses as $property => $embeddedClass) {
if (isset($embeddedClass['declaredField'])) {
$parentReflFields[$property] = new ReflectionEmbeddedProperty(
$parentReflFields[$embeddedClass['declaredField']],
$reflService->getAccessibleProperty(
$this->embeddedClasses[$embeddedClass['declaredField']]['class'],
$embeddedClass['originalField']
),
$embeddedClass['class']
);
continue;
}
$parentReflFields[$property] = $reflService->getAccessibleProperty($this->name, $property);
}
foreach ($this->fieldMappings as $field => $mapping) { foreach ($this->fieldMappings as $field => $mapping) {
if (isset($mapping['declaredField'])) { if (isset($mapping['declaredField'])) {
$declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
? $this->embeddedClasses[$mapping['declaredField']]['declared'] : $this->name;
$this->reflFields[$field] = new ReflectionEmbeddedProperty( $this->reflFields[$field] = new ReflectionEmbeddedProperty(
$reflService->getAccessibleProperty($declaringClass, $mapping['declaredField']), $parentReflFields[$mapping['declaredField']],
$reflService->getAccessibleProperty($this->embeddedClasses[$mapping['declaredField']]['class'], $mapping['originalField']), $reflService->getAccessibleProperty($mapping['originalClass'], $mapping['originalField']),
$this->embeddedClasses[$mapping['declaredField']]['class'] $mapping['originalClass']
); );
continue; continue;
} }
@ -3171,15 +3187,13 @@ class ClassMetadataInfo implements ClassMetadata
*/ */
public function mapEmbedded(array $mapping) public function mapEmbedded(array $mapping)
{ {
if ($this->isEmbeddedClass) {
throw MappingException::noEmbeddablesInEmbeddable($this->name);
}
$this->assertFieldNotMapped($mapping['fieldName']); $this->assertFieldNotMapped($mapping['fieldName']);
$this->embeddedClasses[$mapping['fieldName']] = array( $this->embeddedClasses[$mapping['fieldName']] = array(
'class' => $this->fullyQualifiedClassName($mapping['class']), 'class' => $this->fullyQualifiedClassName($mapping['class']),
'columnPrefix' => $mapping['columnPrefix'], 'columnPrefix' => $mapping['columnPrefix'],
'declaredField' => isset($mapping['declaredField']) ? $mapping['declaredField'] : null,
'originalField' => isset($mapping['originalField']) ? $mapping['originalField'] : null,
); );
} }
@ -3192,8 +3206,15 @@ class ClassMetadataInfo implements ClassMetadata
public function inlineEmbeddable($property, ClassMetadataInfo $embeddable) public function inlineEmbeddable($property, ClassMetadataInfo $embeddable)
{ {
foreach ($embeddable->fieldMappings as $fieldMapping) { foreach ($embeddable->fieldMappings as $fieldMapping) {
$fieldMapping['declaredField'] = $property; $fieldMapping['originalClass'] = isset($fieldMapping['originalClass'])
$fieldMapping['originalField'] = $fieldMapping['fieldName']; ? $fieldMapping['originalClass']
: $embeddable->name;
$fieldMapping['declaredField'] = isset($fieldMapping['declaredField'])
? $property . '.' . $fieldMapping['declaredField']
: $property;
$fieldMapping['originalField'] = isset($fieldMapping['originalField'])
? $fieldMapping['originalField']
: $fieldMapping['fieldName'];
$fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName']; $fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName'];
if (! empty($this->embeddedClasses[$property]['columnPrefix'])) { if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {

View File

@ -782,11 +782,21 @@ class MappingException extends \Doctrine\ORM\ORMException
); );
} }
public static function noEmbeddablesInEmbeddable($className) /**
* @param string $className
* @param string $propertyName
*
* @return MappingException
*/
public static function infiniteEmbeddableNesting($className, $propertyName)
{ {
return new self(sprintf( return new self(
"You embedded one or more embeddables in embeddable '%s', but this behavior is currently unsupported.", sprintf(
$className 'Infinite nesting detected for embedded property %s::%s. ' .
)); 'You cannot embed an embeddable from the same type inside an embeddable.',
$className,
$propertyName
)
);
} }
} }

View File

@ -32,6 +32,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$person->address->street = "United States of Tara Street"; $person->address->street = "United States of Tara Street";
$person->address->zip = "12345"; $person->address->zip = "12345";
$person->address->city = "funkytown"; $person->address->city = "funkytown";
$person->address->country = new DDC93Country('Germany');
// 1. check saving value objects works // 1. check saving value objects works
$this->_em->persist($person); $this->_em->persist($person);
@ -46,11 +47,14 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('United States of Tara Street', $person->address->street); $this->assertEquals('United States of Tara Street', $person->address->street);
$this->assertEquals('12345', $person->address->zip); $this->assertEquals('12345', $person->address->zip);
$this->assertEquals('funkytown', $person->address->city); $this->assertEquals('funkytown', $person->address->city);
$this->assertInstanceOf(DDC93Country::CLASSNAME, $person->address->country);
$this->assertEquals('Germany', $person->address->country->name);
// 3. check changing value objects works // 3. check changing value objects works
$person->address->street = "Street"; $person->address->street = "Street";
$person->address->zip = "54321"; $person->address->zip = "54321";
$person->address->city = "another town"; $person->address->city = "another town";
$person->address->country->name = "United States of America";
$this->_em->flush(); $this->_em->flush();
$this->_em->clear(); $this->_em->clear();
@ -60,6 +64,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('Street', $person->address->street); $this->assertEquals('Street', $person->address->street);
$this->assertEquals('54321', $person->address->zip); $this->assertEquals('54321', $person->address->zip);
$this->assertEquals('another town', $person->address->city); $this->assertEquals('another town', $person->address->city);
$this->assertEquals('United States of America', $person->address->country->name);
// 4. check deleting works // 4. check deleting works
$personId = $person->id;; $personId = $person->id;;
@ -78,6 +83,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$person->address->street = "Tree"; $person->address->street = "Tree";
$person->address->zip = "12345"; $person->address->zip = "12345";
$person->address->city = "funkytown"; $person->address->city = "funkytown";
$person->address->country = new DDC93Country('United States of America');
$this->_em->persist($person); $this->_em->persist($person);
} }
@ -94,6 +100,8 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('Tree', $person->address->street); $this->assertEquals('Tree', $person->address->street);
$this->assertEquals('12345', $person->address->zip); $this->assertEquals('12345', $person->address->zip);
$this->assertEquals('funkytown', $person->address->city); $this->assertEquals('funkytown', $person->address->city);
$this->assertInstanceOf(DDC93Country::CLASSNAME, $person->address->country);
$this->assertEquals('United States of America', $person->address->country->name);
} }
$dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p"; $dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p";
@ -103,6 +111,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('Tree', $person['address.street']); $this->assertEquals('Tree', $person['address.street']);
$this->assertEquals('12345', $person['address.zip']); $this->assertEquals('12345', $person['address.zip']);
$this->assertEquals('funkytown', $person['address.city']); $this->assertEquals('funkytown', $person['address.city']);
$this->assertEquals('United States of America', $person['address.country.name']);
} }
} }
@ -115,32 +124,41 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->markTestSkipped('SLC does not work with UPDATE/DELETE queries through EM.'); $this->markTestSkipped('SLC does not work with UPDATE/DELETE queries through EM.');
} }
$person = new DDC93Person('Johannes', new DDC93Address('Moo', '12345', 'Karlsruhe')); $person = new DDC93Person('Johannes', new DDC93Address('Moo', '12345', 'Karlsruhe', new DDC93Country('Germany')));
$this->_em->persist($person); $this->_em->persist($person);
$this->_em->flush($person); $this->_em->flush($person);
// SELECT // SELECT
$selectDql = "SELECT p FROM " . __NAMESPACE__ ."\\DDC93Person p WHERE p.address.city = :city"; $selectDql = "SELECT p FROM " . __NAMESPACE__ ."\\DDC93Person p WHERE p.address.city = :city AND p.address.country.name = :country";
$loadedPerson = $this->_em->createQuery($selectDql) $loadedPerson = $this->_em->createQuery($selectDql)
->setParameter('city', 'Karlsruhe') ->setParameter('city', 'Karlsruhe')
->setParameter('country', 'Germany')
->getSingleResult(); ->getSingleResult();
$this->assertEquals($person, $loadedPerson); $this->assertEquals($person, $loadedPerson);
$this->assertNull($this->_em->createQuery($selectDql)->setParameter('city', 'asdf')->getOneOrNullResult()); $this->assertNull(
$this->_em->createQuery($selectDql)
->setParameter('city', 'asdf')
->setParameter('country', 'Germany')
->getOneOrNullResult()
);
// UPDATE // UPDATE
$updateDql = "UPDATE " . __NAMESPACE__ . "\\DDC93Person p SET p.address.street = :street WHERE p.address.city = :city"; $updateDql = "UPDATE " . __NAMESPACE__ . "\\DDC93Person p SET p.address.street = :street, p.address.country.name = :country WHERE p.address.city = :city";
$this->_em->createQuery($updateDql) $this->_em->createQuery($updateDql)
->setParameter('street', 'Boo') ->setParameter('street', 'Boo')
->setParameter('country', 'DE')
->setParameter('city', 'Karlsruhe') ->setParameter('city', 'Karlsruhe')
->execute(); ->execute();
$this->_em->refresh($person); $this->_em->refresh($person);
$this->assertEquals('Boo', $person->address->street); $this->assertEquals('Boo', $person->address->street);
$this->assertEquals('DE', $person->address->country->name);
// DELETE // DELETE
$this->_em->createQuery("DELETE " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.city = :city") $this->_em->createQuery("DELETE " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.city = :city AND p.address.country.name = :country")
->setParameter('city', 'Karlsruhe') ->setParameter('city', 'Karlsruhe')
->setParameter('country', 'DE')
->execute(); ->execute();
$this->_em->clear(); $this->_em->clear();
@ -165,43 +183,24 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals($car, $reloadedCar); $this->assertEquals($car, $reloadedCar);
} }
public function testEmbeddableWithinEmbeddable()
{
$this->setExpectedException(
'Doctrine\ORM\Mapping\MappingException',
sprintf(
"You embedded one or more embeddables in embeddable '%s', but this behavior is currently unsupported.",
__NAMESPACE__ . '\DDC93ContactInfo'
)
);
$this->_schemaTool->createSchema(array(
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Customer'),
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93ContactInfo'),
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93PhoneNumber')
));
}
public function testInlineEmbeddableWithPrefix() public function testInlineEmbeddableWithPrefix()
{ {
$expectedColumnName = 'foobar_id'; $metadata = $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonWithPrefix');
$actualColumnName = $this->_em $this->assertEquals('foobar_id', $metadata->getColumnName('id.id'));
->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonWithPrefix') $this->assertEquals('bloo_foo_id', $metadata->getColumnName('nested.nestedWithPrefix.id'));
->getColumnName('id.id'); $this->assertEquals('bloo_nestedWithEmptyPrefix_id', $metadata->getColumnName('nested.nestedWithEmptyPrefix.id'));
$this->assertEquals('bloo_id', $metadata->getColumnName('nested.nestedWithPrefixFalse.id'));
$this->assertEquals($expectedColumnName, $actualColumnName);
} }
public function testInlineEmbeddableEmptyPrefix() public function testInlineEmbeddableEmptyPrefix()
{ {
$expectedColumnName = 'id_id'; $metadata = $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonEmptyPrefix');
$actualColumnName = $this->_em $this->assertEquals('id_id', $metadata->getColumnName('id.id'));
->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonEmptyPrefix') $this->assertEquals('nested_foo_id', $metadata->getColumnName('nested.nestedWithPrefix.id'));
->getColumnName('id.id'); $this->assertEquals('nested_nestedWithEmptyPrefix_id', $metadata->getColumnName('nested.nestedWithEmptyPrefix.id'));
$this->assertEquals('nested_id', $metadata->getColumnName('nested.nestedWithPrefixFalse.id'));
$this->assertEquals($expectedColumnName, $actualColumnName);
} }
public function testInlineEmbeddablePrefixFalse() public function testInlineEmbeddablePrefixFalse()
@ -223,6 +222,22 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertTrue($isFieldMapped); $this->assertTrue($isFieldMapped);
} }
public function testThrowsExceptionOnInfiniteEmbeddableNesting()
{
$this->setExpectedException(
'Doctrine\ORM\Mapping\MappingException',
sprintf(
'Infinite nesting detected for embedded property %s::nested. ' .
'You cannot embed an embeddable from the same type inside an embeddable.',
__NAMESPACE__ . '\DDCInfiniteNestingEmbeddable'
)
);
$this->_schemaTool->createSchema(array(
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDCInfiniteNestingEmbeddable'),
));
}
} }
@ -297,6 +312,24 @@ class DDC93Car extends DDC93Vehicle
{ {
} }
/**
* @Embeddable
*/
class DDC93Country
{
const CLASSNAME = __CLASS__;
/**
* @Column(type="string", nullable=true)
*/
public $name;
public function __construct($name = null)
{
$this->name = $name;
}
}
/** /**
* @Embeddable * @Embeddable
*/ */
@ -316,12 +349,15 @@ class DDC93Address
* @Column(type="string") * @Column(type="string")
*/ */
public $city; public $city;
/** @Embedded(class = "DDC93Country") */
public $country;
public function __construct($street = null, $zip = null, $city = null) public function __construct($street = null, $zip = null, $city = null, DDC93Country $country = null)
{ {
$this->street = $street; $this->street = $street;
$this->zip = $zip; $this->zip = $zip;
$this->city = $city; $this->city = $city;
$this->country = $country;
} }
} }
@ -338,8 +374,14 @@ class DDC93Customer
/** @Embeddable */ /** @Embeddable */
class DDC93ContactInfo class DDC93ContactInfo
{ {
const CLASSNAME = __CLASS__;
/**
* @Column(type="string")
*/
public $email;
/** @Embedded(class = "DDC93Address") */ /** @Embedded(class = "DDC93Address") */
private $address; public $address;
} }
/** /**
@ -352,9 +394,13 @@ class DDC3028PersonWithPrefix
/** @Embedded(class="DDC3028Id", columnPrefix = "foobar_") */ /** @Embedded(class="DDC3028Id", columnPrefix = "foobar_") */
public $id; public $id;
public function __construct(DDC3028Id $id = null) /** @Embedded(class="DDC3028NestedEmbeddable", columnPrefix = "bloo_") */
public $nested;
public function __construct(DDC3028Id $id = null, DDC3028NestedEmbeddable $nested = null)
{ {
$this->id = $id; $this->id = $id;
$this->nested = $nested;
} }
} }
@ -368,9 +414,13 @@ class DDC3028PersonEmptyPrefix
/** @Embedded(class="DDC3028Id", columnPrefix = "") */ /** @Embedded(class="DDC3028Id", columnPrefix = "") */
public $id; public $id;
public function __construct(DDC3028Id $id = null) /** @Embedded(class="DDC3028NestedEmbeddable", columnPrefix = "") */
public $nested;
public function __construct(DDC3028Id $id = null, DDC3028NestedEmbeddable $nested = null)
{ {
$this->id = $id; $this->id = $id;
$this->nested = $nested;
} }
} }
@ -408,6 +458,33 @@ class DDC3028Id
} }
} }
/**
* @Embeddable
*/
class DDC3028NestedEmbeddable
{
const CLASSNAME = __CLASS__;
/** @Embedded(class="DDC3028Id", columnPrefix = "foo_") */
public $nestedWithPrefix;
/** @Embedded(class="DDC3028Id", columnPrefix = "") */
public $nestedWithEmptyPrefix;
/** @Embedded(class="DDC3028Id", columnPrefix = false) */
public $nestedWithPrefixFalse;
public function __construct(
DDC3028Id $nestedWithPrefix = null,
DDC3028Id $nestedWithEmptyPrefix = null,
DDC3028Id $nestedWithPrefixFalse = null
) {
$this->nestedWithPrefix = $nestedWithPrefix;
$this->nestedWithEmptyPrefix = $nestedWithEmptyPrefix;
$this->nestedWithPrefixFalse = $nestedWithPrefixFalse;
}
}
/** /**
* @MappedSuperclass * @MappedSuperclass
*/ */
@ -426,3 +503,12 @@ abstract class DDC3027Animal
class DDC3027Dog extends DDC3027Animal class DDC3027Dog extends DDC3027Animal
{ {
} }
/**
* @Embeddable
*/
class DDCInfiniteNestingEmbeddable
{
/** @Embedded(class="DDCInfiniteNestingEmbeddable") */
public $nested;
}

View File

@ -65,7 +65,9 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest
array( array(
'name' => array( 'name' => array(
'class' => 'Doctrine\Tests\Models\ValueObjects\Name', 'class' => 'Doctrine\Tests\Models\ValueObjects\Name',
'columnPrefix' => 'nm_' 'columnPrefix' => 'nm_',
'declaredField' => null,
'originalField' => null,
) )
), ),
$class->embeddedClasses $class->embeddedClasses