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
|
# 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
|
## 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
|
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:`Ordered associations <tutorials/ordered-associations>`
|
||||||
* :doc:`Pagination <tutorials/pagination>`
|
* :doc:`Pagination <tutorials/pagination>`
|
||||||
* :doc:`Override Field/Association Mappings In Subclasses <tutorials/override-field-association-mappings-in-subclasses>`
|
* :doc:`Override Field/Association Mappings In Subclasses <tutorials/override-field-association-mappings-in-subclasses>`
|
||||||
|
* :doc:`Embeddables <tutorials/embeddables>`
|
||||||
|
|
||||||
Cookbook
|
Cookbook
|
||||||
--------
|
--------
|
||||||
|
@ -16,6 +16,7 @@ Tutorials
|
|||||||
tutorials/ordered-associations
|
tutorials/ordered-associations
|
||||||
tutorials/override-field-association-mappings-in-subclasses
|
tutorials/override-field-association-mappings-in-subclasses
|
||||||
tutorials/pagination.rst
|
tutorials/pagination.rst
|
||||||
|
tutorials/embeddables.rst
|
||||||
|
|
||||||
Reference Guide
|
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:sequence>
|
||||||
<xs:element name="mapped-superclass" type="orm:mapped-superclass" minOccurs="0" maxOccurs="unbounded" />
|
<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="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:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||||
</xs:sequence>
|
</xs:sequence>
|
||||||
<xs:anyAttribute namespace="##other"/>
|
<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="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="id" type="orm:id" minOccurs="0" maxOccurs="unbounded" />
|
||||||
<xs:element name="field" type="orm:field" 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-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="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" />
|
<xs:element name="many-to-one" type="orm:many-to-one" minOccurs="0" maxOccurs="unbounded" />
|
||||||
@ -226,6 +228,16 @@
|
|||||||
</xs:complexContent>
|
</xs:complexContent>
|
||||||
</xs:complexType>
|
</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:simpleType name="change-tracking-policy">
|
||||||
<xs:restriction base="xs:token">
|
<xs:restriction base="xs:token">
|
||||||
<xs:enumeration value="DEFERRED_IMPLICIT"/>
|
<xs:enumeration value="DEFERRED_IMPLICIT"/>
|
||||||
@ -288,6 +300,12 @@
|
|||||||
<xs:anyAttribute namespace="##other"/>
|
<xs:anyAttribute namespace="##other"/>
|
||||||
</xs:complexType>
|
</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:complexType name="discriminator-column">
|
||||||
<xs:sequence>
|
<xs:sequence>
|
||||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||||
|
@ -96,6 +96,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
|||||||
$class->setIdGeneratorType($parent->generatorType);
|
$class->setIdGeneratorType($parent->generatorType);
|
||||||
$this->addInheritedFields($class, $parent);
|
$this->addInheritedFields($class, $parent);
|
||||||
$this->addInheritedRelations($class, $parent);
|
$this->addInheritedRelations($class, $parent);
|
||||||
|
$this->addInheritedEmbeddedClasses($class, $parent);
|
||||||
$class->setIdentifier($parent->identifier);
|
$class->setIdentifier($parent->identifier);
|
||||||
$class->setVersioned($parent->isVersioned);
|
$class->setVersioned($parent->isVersioned);
|
||||||
$class->setVersionField($parent->versionField);
|
$class->setVersionField($parent->versionField);
|
||||||
@ -140,6 +141,15 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
|||||||
$this->completeIdGeneratorMapping($class);
|
$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()) {
|
if ($parent && $parent->isInheritanceTypeSingleTable()) {
|
||||||
$class->setPrimaryTable($parent->table);
|
$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.
|
* Adds inherited named queries to the subclass mapping.
|
||||||
*
|
*
|
||||||
|
@ -260,6 +260,13 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
*/
|
*/
|
||||||
public $isMappedSuperclass = false;
|
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).
|
* READ-ONLY: The names of the parent classes (ancestors).
|
||||||
*
|
*
|
||||||
@ -274,6 +281,13 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
*/
|
*/
|
||||||
public $subClasses = array();
|
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.
|
* 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']
|
'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName']
|
||||||
'fieldMappings',
|
'fieldMappings',
|
||||||
'fieldNames',
|
'fieldNames',
|
||||||
|
'embeddedClasses',
|
||||||
'identifier',
|
'identifier',
|
||||||
'isIdentifierComposite', // TODO: REMOVE
|
'isIdentifierComposite', // TODO: REMOVE
|
||||||
'name',
|
'name',
|
||||||
@ -907,6 +922,18 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
$this->reflClass = $reflService->getClass($this->name);
|
$this->reflClass = $reflService->getClass($this->name);
|
||||||
|
|
||||||
foreach ($this->fieldMappings as $field => $mapping) {
|
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'])
|
$this->reflFields[$field] = isset($mapping['declared'])
|
||||||
? $reflService->getAccessibleProperty($mapping['declared'], $field)
|
? $reflService->getAccessibleProperty($mapping['declared'], $field)
|
||||||
: $reflService->getAccessibleProperty($this->name, $field);
|
: $reflService->getAccessibleProperty($this->name, $field);
|
||||||
@ -948,8 +975,12 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
*/
|
*/
|
||||||
public function validateIdentifier()
|
public function validateIdentifier()
|
||||||
{
|
{
|
||||||
|
if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Verify & complete identifier mapping
|
// Verify & complete identifier mapping
|
||||||
if ( ! $this->identifier && ! $this->isMappedSuperclass) {
|
if ( ! $this->identifier) {
|
||||||
throw MappingException::identifierRequired($this->name);
|
throw MappingException::identifierRequired($this->name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2150,6 +2181,11 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
return isset($this->associationMappings[$fieldName]['inherited']);
|
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.
|
* 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)
|
public function mapField(array $mapping)
|
||||||
{
|
{
|
||||||
$this->_validateAndCompleteFieldMapping($mapping);
|
$this->_validateAndCompleteFieldMapping($mapping);
|
||||||
if (isset($this->fieldMappings[$mapping['fieldName']]) || isset($this->associationMappings[$mapping['fieldName']])) {
|
$this->assertFieldNotMapped($mapping['fieldName']);
|
||||||
throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']);
|
|
||||||
}
|
|
||||||
$this->fieldMappings[$mapping['fieldName']] = $mapping;
|
$this->fieldMappings[$mapping['fieldName']] = $mapping;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2479,9 +2514,7 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
{
|
{
|
||||||
$sourceFieldName = $assocMapping['fieldName'];
|
$sourceFieldName = $assocMapping['fieldName'];
|
||||||
|
|
||||||
if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) {
|
$this->assertFieldNotMapped($sourceFieldName);
|
||||||
throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->associationMappings[$sourceFieldName] = $assocMapping;
|
$this->associationMappings[$sourceFieldName] = $assocMapping;
|
||||||
}
|
}
|
||||||
@ -3120,4 +3153,55 @@ class ClassMetadataInfo implements ClassMetadata
|
|||||||
|
|
||||||
return null;
|
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;
|
return $propertyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
|
||||||
|
{
|
||||||
|
return $propertyName.'_'.$embeddedColumnName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -85,6 +85,8 @@ class AnnotationDriver extends AbstractAnnotationDriver
|
|||||||
$mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
|
$mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
|
||||||
$metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass);
|
$metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass);
|
||||||
$metadata->isMappedSuperclass = true;
|
$metadata->isMappedSuperclass = true;
|
||||||
|
} else if (isset($classAnnotations['Doctrine\ORM\Mapping\Embeddable'])) {
|
||||||
|
$metadata->isEmbeddedClass = true;
|
||||||
} else {
|
} else {
|
||||||
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
|
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
|
||||||
}
|
}
|
||||||
@ -251,7 +253,9 @@ class AnnotationDriver extends AbstractAnnotationDriver
|
|||||||
||
|
||
|
||||||
$metadata->isInheritedField($property->name)
|
$metadata->isInheritedField($property->name)
|
||||||
||
|
||
|
||||||
$metadata->isInheritedAssociation($property->name)) {
|
$metadata->isInheritedAssociation($property->name)
|
||||||
|
||
|
||||||
|
$metadata->isInheritedEmbeddedClass($property->name)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,6 +379,10 @@ class AnnotationDriver extends AbstractAnnotationDriver
|
|||||||
}
|
}
|
||||||
|
|
||||||
$metadata->mapManyToMany($mapping);
|
$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
|
// Evaluate @Cache annotation
|
||||||
|
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
require_once __DIR__.'/../Annotation.php';
|
require_once __DIR__.'/../Annotation.php';
|
||||||
require_once __DIR__.'/../Entity.php';
|
require_once __DIR__.'/../Entity.php';
|
||||||
|
require_once __DIR__.'/../Embeddable.php';
|
||||||
|
require_once __DIR__.'/../Embedded.php';
|
||||||
require_once __DIR__.'/../MappedSuperclass.php';
|
require_once __DIR__.'/../MappedSuperclass.php';
|
||||||
require_once __DIR__.'/../InheritanceType.php';
|
require_once __DIR__.'/../InheritanceType.php';
|
||||||
require_once __DIR__.'/../DiscriminatorColumn.php';
|
require_once __DIR__.'/../DiscriminatorColumn.php';
|
||||||
|
@ -69,6 +69,8 @@ class XmlDriver extends FileDriver
|
|||||||
isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null
|
isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null
|
||||||
);
|
);
|
||||||
$metadata->isMappedSuperclass = true;
|
$metadata->isMappedSuperclass = true;
|
||||||
|
} else if ($xmlRoot->getName() == 'embeddable') {
|
||||||
|
$metadata->isEmbeddedClass = true;
|
||||||
} else {
|
} else {
|
||||||
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
|
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) {
|
foreach ($mappings as $mapping) {
|
||||||
if (isset($mapping['version'])) {
|
if (isset($mapping['version'])) {
|
||||||
$metadata->setVersionMapping($mapping);
|
$metadata->setVersionMapping($mapping);
|
||||||
@ -796,6 +809,11 @@ class XmlDriver extends FileDriver
|
|||||||
$className = (string)$mappedSuperClass['name'];
|
$className = (string)$mappedSuperClass['name'];
|
||||||
$result[$className] = $mappedSuperClass;
|
$result[$className] = $mappedSuperClass;
|
||||||
}
|
}
|
||||||
|
} else if (isset($xmlElement->embeddable)) {
|
||||||
|
foreach ($xmlElement->embeddable as $embeddableElement) {
|
||||||
|
$embeddableName = (string) $embeddableElement['name'];
|
||||||
|
$result[$embeddableName] = $embeddableElement;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
|
@ -66,6 +66,8 @@ class YamlDriver extends FileDriver
|
|||||||
isset($element['repositoryClass']) ? $element['repositoryClass'] : null
|
isset($element['repositoryClass']) ? $element['repositoryClass'] : null
|
||||||
);
|
);
|
||||||
$metadata->isMappedSuperclass = true;
|
$metadata->isMappedSuperclass = true;
|
||||||
|
} else if ($element['type'] == 'embeddable') {
|
||||||
|
$metadata->isEmbeddedClass = true;
|
||||||
} else {
|
} else {
|
||||||
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
|
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
|
// Evaluate oneToOne relationships
|
||||||
if (isset($element['oneToOne'])) {
|
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);
|
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.
|
* 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);
|
return $this->underscore($propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
|
||||||
|
{
|
||||||
|
return $this->underscore($propertyName).'_'.$embeddedColumnName;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritdoc}
|
* {@inheritdoc}
|
||||||
*/
|
*/
|
||||||
|
@ -1049,7 +1049,7 @@ class Parser
|
|||||||
* Parses an arbitrary path expression and defers semantical validation
|
* Parses an arbitrary path expression and defers semantical validation
|
||||||
* based on expected types.
|
* based on expected types.
|
||||||
*
|
*
|
||||||
* PathExpression ::= IdentificationVariable "." identifier
|
* PathExpression ::= IdentificationVariable "." identifier [ ("." identifier)* ]
|
||||||
*
|
*
|
||||||
* @param integer $expectedTypes
|
* @param integer $expectedTypes
|
||||||
*
|
*
|
||||||
@ -1065,6 +1065,12 @@ class Parser
|
|||||||
$this->match(Lexer::T_IDENTIFIER);
|
$this->match(Lexer::T_IDENTIFIER);
|
||||||
|
|
||||||
$field = $this->lexer->token['value'];
|
$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
|
// Creating AST node
|
||||||
|
@ -126,6 +126,7 @@ class SchemaTool
|
|||||||
return (
|
return (
|
||||||
isset($processedClasses[$class->name]) ||
|
isset($processedClasses[$class->name]) ||
|
||||||
$class->isMappedSuperclass ||
|
$class->isMappedSuperclass ||
|
||||||
|
$class->isEmbeddedClass ||
|
||||||
($class->isInheritanceTypeSingleTable() && $class->name != $class->rootEntityName)
|
($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;
|
namespace Doctrine\Tests\ORM\Mapping;
|
||||||
|
|
||||||
use Doctrine\ORM\Mapping\ClassMetadata,
|
use Doctrine\ORM\Mapping\ClassMetadata,
|
||||||
|
Doctrine\ORM\Mapping\ClassMetadataFactory,
|
||||||
Doctrine\ORM\Mapping\Driver\XmlDriver,
|
Doctrine\ORM\Mapping\Driver\XmlDriver,
|
||||||
Doctrine\ORM\Mapping\Driver\YamlDriver;
|
Doctrine\ORM\Mapping\Driver\YamlDriver;
|
||||||
|
|
||||||
@ -38,7 +39,7 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest
|
|||||||
{
|
{
|
||||||
$driver = $this->_loadDriver();
|
$driver = $this->_loadDriver();
|
||||||
$em = $this->_getTestEntityManager();
|
$em = $this->_getTestEntityManager();
|
||||||
$factory = new \Doctrine\ORM\Mapping\ClassMetadataFactory();
|
$factory = new ClassMetadataFactory();
|
||||||
|
|
||||||
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
$em->getConfiguration()->setMetadataDriverImpl($driver);
|
||||||
$factory->setEntityManager($em);
|
$factory->setEntityManager($em);
|
||||||
@ -52,6 +53,28 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest
|
|||||||
$this->assertTrue($class->associationMappings['article']['id']);
|
$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
|
* @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