Merge remote-tracking branch 'origin/ValueObjects'
Conflicts: lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
This commit is contained in:
commit
38b041d909
@ -140,6 +140,11 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
$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);
|
||||
}
|
||||
|
@ -246,6 +246,13 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
*/
|
||||
public $isMappedSuperclass = false;
|
||||
|
||||
/**
|
||||
* READ-ONLY: Wheather this class describes the mapping of an embeddable class.
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public $isEmbeddedClass = false;
|
||||
|
||||
/**
|
||||
* READ-ONLY: The names of the parent classes (ancestors).
|
||||
*
|
||||
@ -260,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.
|
||||
*
|
||||
@ -884,6 +898,15 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$this->reflClass = $reflService->getClass($this->name);
|
||||
|
||||
foreach ($this->fieldMappings as $field => $mapping) {
|
||||
if (isset($mapping['declaredField'])) {
|
||||
$this->reflFields[$field] = new ReflectionEmbeddedProperty(
|
||||
$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);
|
||||
@ -925,8 +948,12 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
*/
|
||||
public function validateIdentifier()
|
||||
{
|
||||
if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify & complete identifier mapping
|
||||
if ( ! $this->identifier && ! $this->isMappedSuperclass) {
|
||||
if ( ! $this->identifier) {
|
||||
throw MappingException::identifierRequired($this->name);
|
||||
}
|
||||
|
||||
@ -2162,9 +2189,8 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
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;
|
||||
}
|
||||
|
||||
@ -2412,9 +2438,7 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
{
|
||||
$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;
|
||||
}
|
||||
@ -3044,4 +3068,49 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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']; // TODO: Change DQL parser to accept this dot notation
|
||||
$fieldMapping['columnName'] = $property . "_" . $fieldMapping['columnName']; // TODO: Use naming strategy
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,6 +85,8 @@ class AnnotationDriver extends AbstractAnnotationDriver
|
||||
$mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
|
||||
$metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass);
|
||||
$metadata->isMappedSuperclass = true;
|
||||
} else if (isset($classAnnotations['Doctrine\ORM\Mapping\Embeddable'])) {
|
||||
$metadata->isEmbeddedClass = true;
|
||||
} else {
|
||||
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
|
||||
}
|
||||
@ -364,6 +366,9 @@ class AnnotationDriver extends AbstractAnnotationDriver
|
||||
}
|
||||
|
||||
$metadata->mapManyToMany($mapping);
|
||||
} else if ($embeddedAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Embedded')) {
|
||||
$mapping['class'] = $embeddedAnnot->class;
|
||||
$metadata->mapEmbedded($mapping);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,8 @@
|
||||
|
||||
require_once __DIR__.'/../Annotation.php';
|
||||
require_once __DIR__.'/../Entity.php';
|
||||
require_once __DIR__.'/../Embeddable.php';
|
||||
require_once __DIR__.'/../Embedded.php';
|
||||
require_once __DIR__.'/../MappedSuperclass.php';
|
||||
require_once __DIR__.'/../InheritanceType.php';
|
||||
require_once __DIR__.'/../DiscriminatorColumn.php';
|
||||
@ -64,4 +66,4 @@ require_once __DIR__.'/../AssociationOverride.php';
|
||||
require_once __DIR__.'/../AssociationOverrides.php';
|
||||
require_once __DIR__.'/../AttributeOverride.php';
|
||||
require_once __DIR__.'/../AttributeOverrides.php';
|
||||
require_once __DIR__.'/../EntityListeners.php';
|
||||
require_once __DIR__.'/../EntityListeners.php';
|
||||
|
28
lib/Doctrine/ORM/Mapping/Embeddable.php
Normal file
28
lib/Doctrine/ORM/Mapping/Embeddable.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the MIT license. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("PROPERTY")
|
||||
*/
|
||||
final class Embeddable implements Annotation
|
||||
{
|
||||
}
|
32
lib/Doctrine/ORM/Mapping/Embedded.php
Normal file
32
lib/Doctrine/ORM/Mapping/Embedded.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the MIT license. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
/**
|
||||
* @Annotation
|
||||
* @Target("CLASS")
|
||||
*/
|
||||
final class Embedded implements Annotation
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $class;
|
||||
}
|
66
lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php
Normal file
66
lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php
Normal file
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* This software consists of voluntary contributions made by many individuals
|
||||
* and is licensed under the MIT license. For more information, see
|
||||
* <http://www.doctrine-project.org>.
|
||||
*/
|
||||
|
||||
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.
|
||||
*
|
||||
* TODO: Move this class into Common\Reflection
|
||||
*/
|
||||
class ReflectionEmbeddedProperty
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
@ -126,6 +126,7 @@ class SchemaTool
|
||||
return (
|
||||
isset($processedClasses[$class->name]) ||
|
||||
$class->isMappedSuperclass ||
|
||||
$class->isEmbeddedClass ||
|
||||
($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
|
||||
);
|
||||
}
|
||||
|
143
tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
Normal file
143
tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
Normal file
@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
/**
|
||||
* @group DDC-93
|
||||
*/
|
||||
class ValueObjectsTest extends \Doctrine\Tests\OrmFunctionalTestCase
|
||||
{
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
try {
|
||||
$this->_schemaTool->createSchema(array(
|
||||
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Person'),
|
||||
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Address'),
|
||||
));
|
||||
} catch(\Exception $e) {
|
||||
}
|
||||
}
|
||||
|
||||
public function testCRUD()
|
||||
{
|
||||
$person = new DDC93Person();
|
||||
$person->name = "Tara";
|
||||
$person->address = new DDC93Address();
|
||||
$person->address->street = "United States of Tara Street";
|
||||
$person->address->zip = "12345";
|
||||
$person->address->city = "funkytown";
|
||||
|
||||
// 1. check saving value objects works
|
||||
$this->_em->persist($person);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
// 2. check loading value objects works
|
||||
$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);
|
||||
|
||||
// 3. check changing value objects works
|
||||
$person->address->street = "Street";
|
||||
$person->address->zip = "54321";
|
||||
$person->address->city = "another town";
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->clear();
|
||||
|
||||
$person = $this->_em->find(DDC93Person::CLASSNAME, $person->id);
|
||||
|
||||
$this->assertEquals('Street', $person->address->street);
|
||||
$this->assertEquals('54321', $person->address->zip);
|
||||
$this->assertEquals('another town', $person->address->city);
|
||||
|
||||
// 4. check deleting works
|
||||
$personId = $person->id;;
|
||||
$this->_em->remove($person);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->assertNull($this->_em->find(DDC93Person::CLASSNAME, $personId));
|
||||
}
|
||||
|
||||
public function testLoadDql()
|
||||
{
|
||||
for ($i = 0; $i < 3; $i++) {
|
||||
$person = new DDC93Person();
|
||||
$person->name = "Donkey Kong$i";
|
||||
$person->address = new DDC93Address();
|
||||
$person->address->street = "Tree";
|
||||
$person->address->zip = "12345";
|
||||
$person->address->city = "funkytown";
|
||||
|
||||
$this->_em->persist($person);
|
||||
}
|
||||
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p";
|
||||
$persons = $this->_em->createQuery($dql)->getResult();
|
||||
|
||||
$this->assertCount(3, $persons);
|
||||
foreach ($persons as $person) {
|
||||
$this->assertInstanceOf(DDC93Address::CLASSNAME, $person->address);
|
||||
$this->assertEquals('Tree', $person->address->street);
|
||||
$this->assertEquals('12345', $person->address->zip);
|
||||
$this->assertEquals('funkytown', $person->address->city);
|
||||
}
|
||||
|
||||
$dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p";
|
||||
$persons = $this->_em->createQuery($dql)->getArrayResult();
|
||||
|
||||
foreach ($persons as $person) {
|
||||
$this->assertEquals('Tree', $person['address.street']);
|
||||
$this->assertEquals('12345', $person['address.zip']);
|
||||
$this->assertEquals('funkytown', $person['address.city']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class DDC93Person
|
||||
{
|
||||
const CLASSNAME = __CLASS__;
|
||||
|
||||
/** @Id @GeneratedValue @Column(type="integer") */
|
||||
public $id;
|
||||
|
||||
/** @Column(type="string") */
|
||||
public $name;
|
||||
|
||||
/** @Embedded(class="DDC93Address") */
|
||||
public $address;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Embeddable
|
||||
*/
|
||||
class DDC93Address
|
||||
{
|
||||
const CLASSNAME = __CLASS__;
|
||||
|
||||
/**
|
||||
* @Column(type="string")
|
||||
*/
|
||||
public $street;
|
||||
/**
|
||||
* @Column(type="string")
|
||||
*/
|
||||
public $zip;
|
||||
/**
|
||||
* @Column(type="string")
|
||||
*/
|
||||
public $city;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user