1
0
mirror of synced 2025-02-20 22:23:14 +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) {
foreach ($class->embeddedClasses as $property => $embeddableClass) {
if (isset($embeddableClass['inherited'])) {
continue;
}
if ($embeddableClass['class'] === $class->name) {
throw MappingException::infiniteEmbeddableNesting($class->name, $property);
}
$embeddableMetadata = $this->getMetadataFor($embeddableClass['class']);
if ($embeddableMetadata->isEmbeddedClass) {
$this->addNestedEmbeddedClasses($embeddableMetadata, $class, $property);
}
$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.
*

View File

@ -929,15 +929,31 @@ class ClassMetadataInfo implements ClassMetadata
// Restore ReflectionClass and properties
$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) {
if (isset($mapping['declaredField'])) {
$declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
? $this->embeddedClasses[$mapping['declaredField']]['declared'] : $this->name;
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
$reflService->getAccessibleProperty($declaringClass, $mapping['declaredField']),
$reflService->getAccessibleProperty($this->embeddedClasses[$mapping['declaredField']]['class'], $mapping['originalField']),
$this->embeddedClasses[$mapping['declaredField']]['class']
$parentReflFields[$mapping['declaredField']],
$reflService->getAccessibleProperty($mapping['originalClass'], $mapping['originalField']),
$mapping['originalClass']
);
continue;
}
@ -3171,15 +3187,13 @@ class ClassMetadataInfo implements ClassMetadata
*/
public function mapEmbedded(array $mapping)
{
if ($this->isEmbeddedClass) {
throw MappingException::noEmbeddablesInEmbeddable($this->name);
}
$this->assertFieldNotMapped($mapping['fieldName']);
$this->embeddedClasses[$mapping['fieldName']] = array(
'class' => $this->fullyQualifiedClassName($mapping['class']),
'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)
{
foreach ($embeddable->fieldMappings as $fieldMapping) {
$fieldMapping['declaredField'] = $property;
$fieldMapping['originalField'] = $fieldMapping['fieldName'];
$fieldMapping['originalClass'] = isset($fieldMapping['originalClass'])
? $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'];
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(
"You embedded one or more embeddables in embeddable '%s', but this behavior is currently unsupported.",
$className
));
return new self(
sprintf(
'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->zip = "12345";
$person->address->city = "funkytown";
$person->address->country = new DDC93Country('Germany');
// 1. check saving value objects works
$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('12345', $person->address->zip);
$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
$person->address->street = "Street";
$person->address->zip = "54321";
$person->address->city = "another town";
$person->address->country->name = "United States of America";
$this->_em->flush();
$this->_em->clear();
@ -60,6 +64,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('Street', $person->address->street);
$this->assertEquals('54321', $person->address->zip);
$this->assertEquals('another town', $person->address->city);
$this->assertEquals('United States of America', $person->address->country->name);
// 4. check deleting works
$personId = $person->id;;
@ -78,6 +83,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$person->address->street = "Tree";
$person->address->zip = "12345";
$person->address->city = "funkytown";
$person->address->country = new DDC93Country('United States of America');
$this->_em->persist($person);
}
@ -94,6 +100,8 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('Tree', $person->address->street);
$this->assertEquals('12345', $person->address->zip);
$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";
@ -103,6 +111,7 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$this->assertEquals('Tree', $person['address.street']);
$this->assertEquals('12345', $person['address.zip']);
$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.');
}
$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->flush($person);
// 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)
->setParameter('city', 'Karlsruhe')
->setParameter('country', 'Germany')
->getSingleResult();
$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
$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)
->setParameter('street', 'Boo')
->setParameter('country', 'DE')
->setParameter('city', 'Karlsruhe')
->execute();
$this->_em->refresh($person);
$this->assertEquals('Boo', $person->address->street);
$this->assertEquals('DE', $person->address->country->name);
// 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('country', 'DE')
->execute();
$this->_em->clear();
@ -165,43 +183,24 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$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()
{
$expectedColumnName = 'foobar_id';
$metadata = $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonWithPrefix');
$actualColumnName = $this->_em
->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonWithPrefix')
->getColumnName('id.id');
$this->assertEquals($expectedColumnName, $actualColumnName);
$this->assertEquals('foobar_id', $metadata->getColumnName('id.id'));
$this->assertEquals('bloo_foo_id', $metadata->getColumnName('nested.nestedWithPrefix.id'));
$this->assertEquals('bloo_nestedWithEmptyPrefix_id', $metadata->getColumnName('nested.nestedWithEmptyPrefix.id'));
$this->assertEquals('bloo_id', $metadata->getColumnName('nested.nestedWithPrefixFalse.id'));
}
public function testInlineEmbeddableEmptyPrefix()
{
$expectedColumnName = 'id_id';
$metadata = $this->_em->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonEmptyPrefix');
$actualColumnName = $this->_em
->getClassMetadata(__NAMESPACE__ . '\DDC3028PersonEmptyPrefix')
->getColumnName('id.id');
$this->assertEquals($expectedColumnName, $actualColumnName);
$this->assertEquals('id_id', $metadata->getColumnName('id.id'));
$this->assertEquals('nested_foo_id', $metadata->getColumnName('nested.nestedWithPrefix.id'));
$this->assertEquals('nested_nestedWithEmptyPrefix_id', $metadata->getColumnName('nested.nestedWithEmptyPrefix.id'));
$this->assertEquals('nested_id', $metadata->getColumnName('nested.nestedWithPrefixFalse.id'));
}
public function testInlineEmbeddablePrefixFalse()
@ -223,6 +222,22 @@ class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
$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
*/
@ -316,12 +349,15 @@ class DDC93Address
* @Column(type="string")
*/
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->zip = $zip;
$this->city = $city;
$this->country = $country;
}
}
@ -338,8 +374,14 @@ class DDC93Customer
/** @Embeddable */
class DDC93ContactInfo
{
const CLASSNAME = __CLASS__;
/**
* @Column(type="string")
*/
public $email;
/** @Embedded(class = "DDC93Address") */
private $address;
public $address;
}
/**
@ -352,9 +394,13 @@ class DDC3028PersonWithPrefix
/** @Embedded(class="DDC3028Id", columnPrefix = "foobar_") */
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->nested = $nested;
}
}
@ -368,9 +414,13 @@ class DDC3028PersonEmptyPrefix
/** @Embedded(class="DDC3028Id", columnPrefix = "") */
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->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
*/
@ -426,3 +503,12 @@ abstract class DDC3027Animal
class DDC3027Dog extends DDC3027Animal
{
}
/**
* @Embeddable
*/
class DDCInfiniteNestingEmbeddable
{
/** @Embedded(class="DDCInfiniteNestingEmbeddable") */
public $nested;
}

View File

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