Merge pull request #835 from schmittjoh/ValueObjects
Value objects (Based on #634)
This commit is contained in:
commit
8a0901c92b
@ -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
|
||||
|
@ -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
|
||||
--------
|
||||
|
@ -16,6 +16,7 @@ Tutorials
|
||||
tutorials/ordered-associations
|
||||
tutorials/override-field-association-mappings-in-subclasses
|
||||
tutorials/pagination.rst
|
||||
tutorials/embeddables.rst
|
||||
|
||||
Reference Guide
|
||||
---------------
|
||||
|
83
docs/en/tutorials/embeddables.rst
Normal file
83
docs/en/tutorials/embeddables.rst
Normal 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
|
||||
|
@ -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"/>
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +50,14 @@ class DefaultNamingStrategy implements NamingStrategy
|
||||
return $propertyName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
|
||||
{
|
||||
return $propertyName.'_'.$embeddedColumnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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'])) {
|
||||
|
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("CLASS")
|
||||
*/
|
||||
final class Embeddable implements Annotation
|
||||
{
|
||||
}
|
38
lib/Doctrine/ORM/Mapping/Embedded.php
Normal file
38
lib/Doctrine/ORM/Mapping/Embedded.php
Normal 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;
|
||||
}
|
@ -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.
|
||||
*
|
||||
|
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 = unserialize(sprintf('O:%d:"%s":0:{}', strlen($this->class), $this->class));
|
||||
$this->parentProperty->setValue($object, $embeddedObject);
|
||||
}
|
||||
|
||||
$this->childProperty->setValue($embeddedObject, $value);
|
||||
}
|
||||
}
|
@ -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}
|
||||
*/
|
||||
|
@ -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
|
||||
|
@ -126,6 +126,7 @@ class SchemaTool
|
||||
return (
|
||||
isset($processedClasses[$class->name]) ||
|
||||
$class->isMappedSuperclass ||
|
||||
$class->isEmbeddedClass ||
|
||||
($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
|
||||
);
|
||||
}
|
||||
|
9
tests/Doctrine/Tests/Models/ValueObjects/Name.php
Normal file
9
tests/Doctrine/Tests/Models/ValueObjects/Name.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\Models\ValueObjects;
|
||||
|
||||
class Name
|
||||
{
|
||||
private $firstName;
|
||||
private $lastName;
|
||||
}
|
9
tests/Doctrine/Tests/Models/ValueObjects/Person.php
Normal file
9
tests/Doctrine/Tests/Models/ValueObjects/Person.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Doctrine\Tests\Models\ValueObjects;
|
||||
|
||||
class Person
|
||||
{
|
||||
private $id;
|
||||
private $name;
|
||||
}
|
265
tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
Normal file
265
tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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>
|
@ -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>
|
Loading…
x
Reference in New Issue
Block a user