1
0
mirror of synced 2025-01-18 06:21:40 +03:00

Merge pull request #835 from schmittjoh/ValueObjects

Value objects (Based on #634)
This commit is contained in:
Benjamin Eberlei 2014-02-08 16:24:47 +01:00
commit 8a0901c92b
25 changed files with 759 additions and 10 deletions

View File

@ -1,5 +1,10 @@
# Upgrade to 2.5
## BC BREAK: NamingStrategy has a new method ``embeddedFieldToColumnName($propertyName, $embeddedColumnName)``
This method generates the column name for fields of embedded objects. If you implement your custom NamingStrategy, you
now also need to implement this new method.
## Updates on entities scheduled for deletion are no longer processed
In Doctrine 2.4, if you modified properties of an entity scheduled for deletion, UnitOfWork would

View File

@ -90,6 +90,7 @@ Tutorials
* :doc:`Ordered associations <tutorials/ordered-associations>`
* :doc:`Pagination <tutorials/pagination>`
* :doc:`Override Field/Association Mappings In Subclasses <tutorials/override-field-association-mappings-in-subclasses>`
* :doc:`Embeddables <tutorials/embeddables>`
Cookbook
--------

View File

@ -16,6 +16,7 @@ Tutorials
tutorials/ordered-associations
tutorials/override-field-association-mappings-in-subclasses
tutorials/pagination.rst
tutorials/embeddables.rst
Reference Guide
---------------

View File

@ -0,0 +1,83 @@
Separating Concerns using Embeddables
-------------------------------------
Embeddables are classes which are not entities themself, but are embedded
in entities and can also be queried in DQL. You'll mostly want to use them
to reduce duplication or separating concerns.
For the purposes of this tutorial, we will assume that you have a ``User``
class in your application and you would like to store an address in
the ``User`` class. We will model the ``Address`` class as an embeddable
instead of simply adding the respective columns to the ``User`` class.
.. configuration-block::
.. code-block:: php
<?php
/** @Entity */
class User
{
/** @Embedded(class = "Address") */
private $address;
}
/** @Embeddable */
class Address
{
/** @Column(type = "string") */
private $street;
/** @Column(type = "string") */
private $postalCode;
/** @Column(type = "string") */
private $city;
/** @Column(type = "string") */
private $country;
}
.. code-block:: xml
<doctrine-mapping>
<entity name="User">
<embedded name="address" class="Address" />
</entity>
<embeddable name="Address">
<field name="street" type="string" />
<field name="postalCode" type="string" />
<field name="city" type="string" />
<field name="country" type="string" />
</embeddable>
</doctrine-mapping>
.. code-block:: yaml
User:
type: entity
embedded:
address:
class: Address
Address:
type: embeddable
fields:
street: { type: string }
postalCode: { type: string }
city: { type: string }
country: { type: string }
In terms of your database schema, Doctrine will automatically inline all
columns from the ``Address`` class into the table of the ``User`` class,
just as if you had declared them directly there.
You can also use mapped fields of embedded classes in DQL queries, just
as if they were declared in the ``User`` class:
.. code-block:: sql
SELECT u FROM User u WHERE u.address.city = :myCity

View File

@ -17,6 +17,7 @@
<xs:sequence>
<xs:element name="mapped-superclass" type="orm:mapped-superclass" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="entity" type="orm:entity" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="embeddable" type="orm:embeddable" minOccurs="0" maxOccurs="unbounded" />
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
<xs:anyAttribute namespace="##other"/>
@ -180,6 +181,7 @@
<xs:element name="sql-result-set-mappings" type="orm:sql-result-set-mappings" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="id" type="orm:id" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="field" type="orm:field" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="embedded" type="orm:embedded" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="one-to-one" type="orm:one-to-one" minOccurs="0" maxOccurs="unbounded"/>
<xs:element name="one-to-many" type="orm:one-to-many" minOccurs="0" maxOccurs="unbounded" />
<xs:element name="many-to-one" type="orm:many-to-one" minOccurs="0" maxOccurs="unbounded" />
@ -226,6 +228,16 @@
</xs:complexContent>
</xs:complexType>
<xs:complexType name="embeddable">
<xs:complexContent>
<xs:extension base="orm:entity">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
</xs:extension>
</xs:complexContent>
</xs:complexType>
<xs:simpleType name="change-tracking-policy">
<xs:restriction base="xs:token">
<xs:enumeration value="DEFERRED_IMPLICIT"/>
@ -288,6 +300,12 @@
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
<xs:complexType name="embedded">
<xs:attribute name="name" type="xs:string" use="required" />
<xs:attribute name="class" type="xs:string" use="required" />
<xs:attribute name="column-prefix" type="xs:string" use="optional" />
</xs:complexType>
<xs:complexType name="discriminator-column">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>

View File

@ -96,6 +96,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
$this->addInheritedRelations($class, $parent);
$this->addInheritedEmbeddedClasses($class, $parent);
$class->setIdentifier($parent->identifier);
$class->setVersioned($parent->isVersioned);
$class->setVersionField($parent->versionField);
@ -140,6 +141,15 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
$this->completeIdGeneratorMapping($class);
}
foreach ($class->embeddedClasses as $property => $embeddableClass) {
if (isset($embeddableClass['inherited'])) {
continue;
}
$embeddableMetadata = $this->getMetadataFor($embeddableClass['class']);
$class->inlineEmbeddable($property, $embeddableMetadata);
}
if ($parent && $parent->isInheritanceTypeSingleTable()) {
$class->setPrimaryTable($parent->table);
}
@ -342,6 +352,20 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
}
}
private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass)
{
foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
if ( ! isset($embeddedClass['inherited']) && ! $parentClass->isMappedSuperclass) {
$embeddedClass['inherited'] = $parentClass->name;
}
if ( ! isset($embeddedClass['declared'])) {
$embeddedClass['declared'] = $parentClass->name;
}
$subClass->embeddedClasses[$field] = $embeddedClass;
}
}
/**
* Adds inherited named queries to the subclass mapping.
*

View File

@ -260,6 +260,13 @@ class ClassMetadataInfo implements ClassMetadata
*/
public $isMappedSuperclass = false;
/**
* READ-ONLY: Whether this class describes the mapping of an embeddable class.
*
* @var boolean
*/
public $isEmbeddedClass = false;
/**
* READ-ONLY: The names of the parent classes (ancestors).
*
@ -274,6 +281,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.
*
@ -799,6 +813,7 @@ class ClassMetadataInfo implements ClassMetadata
'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName']
'fieldMappings',
'fieldNames',
'embeddedClasses',
'identifier',
'isIdentifierComposite', // TODO: REMOVE
'name',
@ -907,6 +922,18 @@ class ClassMetadataInfo implements ClassMetadata
$this->reflClass = $reflService->getClass($this->name);
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']
);
continue;
}
$this->reflFields[$field] = isset($mapping['declared'])
? $reflService->getAccessibleProperty($mapping['declared'], $field)
: $reflService->getAccessibleProperty($this->name, $field);
@ -948,8 +975,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);
}
@ -2150,6 +2181,11 @@ class ClassMetadataInfo implements ClassMetadata
return isset($this->associationMappings[$fieldName]['inherited']);
}
public function isInheritedEmbeddedClass($fieldName)
{
return isset($this->embeddedClasses[$fieldName]['inherited']);
}
/**
* Sets the name of the primary table the class is mapped to.
*
@ -2229,9 +2265,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;
}
@ -2479,9 +2514,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;
}
@ -3120,4 +3153,55 @@ 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']] = array(
'class' => $this->fullyQualifiedClassName($mapping['class']),
'columnPrefix' => $mapping['columnPrefix'],
);
}
/**
* 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'] = ! empty($this->embeddedClasses[$property]['columnPrefix'])
? $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName']
: $this->namingStrategy->embeddedFieldToColumnName($property, $fieldMapping['columnName'], $this->reflClass->name, $embeddable->reflClass->name);
$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);
}
}
}

View File

@ -50,6 +50,14 @@ class DefaultNamingStrategy implements NamingStrategy
return $propertyName;
}
/**
* {@inheritdoc}
*/
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
{
return $propertyName.'_'.$embeddedColumnName;
}
/**
* {@inheritdoc}
*/

View File

@ -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);
}
@ -251,7 +253,9 @@ class AnnotationDriver extends AbstractAnnotationDriver
||
$metadata->isInheritedField($property->name)
||
$metadata->isInheritedAssociation($property->name)) {
$metadata->isInheritedAssociation($property->name)
||
$metadata->isInheritedEmbeddedClass($property->name)) {
continue;
}
@ -375,6 +379,10 @@ class AnnotationDriver extends AbstractAnnotationDriver
}
$metadata->mapManyToMany($mapping);
} else if ($embeddedAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Embedded')) {
$mapping['class'] = $embeddedAnnot->class;
$mapping['columnPrefix'] = $embeddedAnnot->columnPrefix;
$metadata->mapEmbedded($mapping);
}
// Evaluate @Cache annotation

View File

@ -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';

View File

@ -69,6 +69,8 @@ class XmlDriver extends FileDriver
isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null
);
$metadata->isMappedSuperclass = true;
} else if ($xmlRoot->getName() == 'embeddable') {
$metadata->isEmbeddedClass = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
@ -246,6 +248,17 @@ class XmlDriver extends FileDriver
}
}
if (isset($xmlRoot->embedded)) {
foreach ($xmlRoot->embedded as $embeddedMapping) {
$mapping = array(
'fieldName' => (string) $embeddedMapping['name'],
'class' => (string) $embeddedMapping['class'],
'columnPrefix' => isset($embeddedMapping['column-prefix']) ? (string) $embeddedMapping['column-prefix'] : null,
);
$metadata->mapEmbedded($mapping);
}
}
foreach ($mappings as $mapping) {
if (isset($mapping['version'])) {
$metadata->setVersionMapping($mapping);
@ -796,6 +809,11 @@ class XmlDriver extends FileDriver
$className = (string)$mappedSuperClass['name'];
$result[$className] = $mappedSuperClass;
}
} else if (isset($xmlElement->embeddable)) {
foreach ($xmlElement->embeddable as $embeddableElement) {
$embeddableName = (string) $embeddableElement['name'];
$result[$embeddableName] = $embeddableElement;
}
}
return $result;

View File

@ -66,6 +66,8 @@ class YamlDriver extends FileDriver
isset($element['repositoryClass']) ? $element['repositoryClass'] : null
);
$metadata->isMappedSuperclass = true;
} else if ($element['type'] == 'embeddable') {
$metadata->isEmbeddedClass = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
@ -318,6 +320,16 @@ class YamlDriver extends FileDriver
}
}
if (isset($element['embedded'])) {
foreach ($element['embedded'] as $name => $embeddedMapping) {
$mapping = array(
'fieldName' => $name,
'class' => $embeddedMapping['class'],
'columnPrefix' => isset($embeddedMapping['columnPrefix']) ? $embeddedMapping['columnPrefix'] : null,
);
$metadata->mapEmbedded($mapping);
}
}
// Evaluate oneToOne relationships
if (isset($element['oneToOne'])) {

View 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("CLASS")
*/
final class Embeddable implements Annotation
{
}

View File

@ -0,0 +1,38 @@
<?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 Embedded implements Annotation
{
/**
* @Required
* @var string
*/
public $class;
/**
* @var string
*/
public $columnPrefix;
}

View File

@ -49,6 +49,16 @@ interface NamingStrategy
*/
function propertyToColumnName($propertyName, $className = null);
/**
* Returns a column name for an embedded property.
*
* @param string $propertyName
* @param string $embeddedColumnName
*
* @return string
*/
function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null);
/**
* Returns the default reference column name.
*

View 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 = unserialize(sprintf('O:%d:"%s":0:{}', strlen($this->class), $this->class));
$this->parentProperty->setValue($object, $embeddedObject);
}
$this->childProperty->setValue($embeddedObject, $value);
}
}

View File

@ -87,6 +87,14 @@ class UnderscoreNamingStrategy implements NamingStrategy
return $this->underscore($propertyName);
}
/**
* {@inheritdoc}
*/
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
{
return $this->underscore($propertyName).'_'.$embeddedColumnName;
}
/**
* {@inheritdoc}
*/

View File

@ -1049,7 +1049,7 @@ class Parser
* Parses an arbitrary path expression and defers semantical validation
* based on expected types.
*
* PathExpression ::= IdentificationVariable "." identifier
* PathExpression ::= IdentificationVariable "." identifier [ ("." identifier)* ]
*
* @param integer $expectedTypes
*
@ -1065,6 +1065,12 @@ class Parser
$this->match(Lexer::T_IDENTIFIER);
$field = $this->lexer->token['value'];
while ($this->lexer->isNextToken(Lexer::T_DOT)) {
$this->match(Lexer::T_DOT);
$this->match(Lexer::T_IDENTIFIER);
$field .= '.'.$this->lexer->token['value'];
}
}
// Creating AST node

View File

@ -126,6 +126,7 @@ class SchemaTool
return (
isset($processedClasses[$class->name]) ||
$class->isMappedSuperclass ||
$class->isEmbeddedClass ||
($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
);
}

View File

@ -0,0 +1,9 @@
<?php
namespace Doctrine\Tests\Models\ValueObjects;
class Name
{
private $firstName;
private $lastName;
}

View File

@ -0,0 +1,9 @@
<?php
namespace Doctrine\Tests\Models\ValueObjects;
class Person
{
private $id;
private $name;
}

View File

@ -0,0 +1,265 @@
<?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'),
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Vehicle'),
$this->_em->getClassMetadata(__NAMESPACE__ . '\DDC93Car'),
));
} 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']);
}
}
/**
* @group dql
*/
public function testDqlOnEmbeddedObjectsField()
{
if ($this->isSecondLevelCacheEnabled) {
$this->markTestSkipped('SLC does not work with UPDATE/DELETE queries through EM.');
}
$person = new DDC93Person('Johannes', new DDC93Address('Moo', '12345', 'Karlsruhe'));
$this->_em->persist($person);
$this->_em->flush($person);
// SELECT
$selectDql = "SELECT p FROM " . __NAMESPACE__ ."\\DDC93Person p WHERE p.address.city = :city";
$loadedPerson = $this->_em->createQuery($selectDql)
->setParameter('city', 'Karlsruhe')
->getSingleResult();
$this->assertEquals($person, $loadedPerson);
$this->assertNull($this->_em->createQuery($selectDql)->setParameter('city', 'asdf')->getOneOrNullResult());
// UPDATE
$updateDql = "UPDATE " . __NAMESPACE__ . "\\DDC93Person p SET p.address.street = :street WHERE p.address.city = :city";
$this->_em->createQuery($updateDql)
->setParameter('street', 'Boo')
->setParameter('city', 'Karlsruhe')
->execute();
$this->_em->refresh($person);
$this->assertEquals('Boo', $person->address->street);
// DELETE
$this->_em->createQuery("DELETE " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.city = :city")
->setParameter('city', 'Karlsruhe')
->execute();
$this->_em->clear();
$this->assertNull($this->_em->find(__NAMESPACE__.'\\DDC93Person', $person->id));
}
public function testDqlWithNonExistentEmbeddableField()
{
$this->setExpectedException('Doctrine\ORM\Query\QueryException', 'no field or association named address.asdfasdf');
$this->_em->createQuery("SELECT p FROM " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.asdfasdf IS NULL")
->execute();
}
public function testEmbeddableWithInheritance()
{
$car = new DDC93Car(new DDC93Address('Foo', '12345', 'Asdf'));
$this->_em->persist($car);
$this->_em->flush($car);
$reloadedCar = $this->_em->find(__NAMESPACE__.'\\DDC93Car', $car->id);
$this->assertEquals($car, $reloadedCar);
}
}
/**
* @Entity
*/
class DDC93Person
{
const CLASSNAME = __CLASS__;
/** @Id @GeneratedValue @Column(type="integer") */
public $id;
/** @Column(type="string") */
public $name;
/** @Embedded(class="DDC93Address") */
public $address;
/** @Embedded(class = "DDC93Timestamps") */
public $timestamps;
public function __construct($name = null, DDC93Address $address = null)
{
$this->name = $name;
$this->address = $address;
$this->timestamps = new DDC93Timestamps(new \DateTime);
}
}
/**
* @Embeddable
*/
class DDC93Timestamps
{
/** @Column(type = "datetime") */
public $createdAt;
public function __construct(\DateTime $createdAt)
{
$this->createdAt = $createdAt;
}
}
/**
* @Entity
*
* @InheritanceType("SINGLE_TABLE")
* @DiscriminatorColumn(name = "t", type = "string", length = 10)
* @DiscriminatorMap({
* "v" = "Doctrine\Tests\ORM\Functional\DDC93Car",
* })
*/
abstract class DDC93Vehicle
{
/** @Id @GeneratedValue(strategy = "AUTO") @Column(type = "integer") */
public $id;
/** @Embedded(class = "DDC93Address") */
public $address;
public function __construct(DDC93Address $address)
{
$this->address = $address;
}
}
/**
* @Entity
*/
class DDC93Car extends DDC93Vehicle
{
}
/**
* @Embeddable
*/
class DDC93Address
{
const CLASSNAME = __CLASS__;
/**
* @Column(type="string")
*/
public $street;
/**
* @Column(type="string")
*/
public $zip;
/**
* @Column(type="string")
*/
public $city;
public function __construct($street = null, $zip = null, $city = null)
{
$this->street = $street;
$this->zip = $zip;
$this->city = $city;
}
}

View File

@ -3,6 +3,7 @@
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\ORM\Mapping\ClassMetadata,
Doctrine\ORM\Mapping\ClassMetadataFactory,
Doctrine\ORM\Mapping\Driver\XmlDriver,
Doctrine\ORM\Mapping\Driver\YamlDriver;
@ -38,7 +39,7 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest
{
$driver = $this->_loadDriver();
$em = $this->_getTestEntityManager();
$factory = new \Doctrine\ORM\Mapping\ClassMetadataFactory();
$factory = new ClassMetadataFactory();
$em->getConfiguration()->setMetadataDriverImpl($driver);
$factory->setEntityManager($em);
@ -52,6 +53,28 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest
$this->assertTrue($class->associationMappings['article']['id']);
}
public function testEmbeddableMapping()
{
$class = $this->createClassMetadata('Doctrine\Tests\Models\ValueObjects\Name');
$this->assertEquals(true, $class->isEmbeddedClass);
}
public function testEmbeddedMapping()
{
$class = $this->createClassMetadata('Doctrine\Tests\Models\ValueObjects\Person');
$this->assertEquals(
array(
'name' => array(
'class' => 'Doctrine\Tests\Models\ValueObjects\Name',
'columnPrefix' => 'nm_'
)
),
$class->embeddedClasses
);
}
/**
* @group DDC-1468
*

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<embeddable name="Doctrine\Tests\Models\ValueObjects\Name">
<field name="firstName"/>
<field name="lastName"/>
</embeddable>
</doctrine-mapping>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
http://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
<entity name="Doctrine\Tests\Models\ValueObjects\Person">
<id name="id" type="integer" column="id">
<generator strategy="AUTO"/>
</id>
<embedded name="name" class="Doctrine\Tests\Models\ValueObjects\Name" column-prefix="nm_"/>
</entity>
</doctrine-mapping>