Skip to content

Commit

Permalink
Merge pull request #835 from schmittjoh/ValueObjects
Browse files Browse the repository at this point in the history
Value objects (Based on #634)
  • Loading branch information
beberlei committed Feb 8, 2014
2 parents 8e3f456 + 7020f41 commit 8a0901c
Show file tree
Hide file tree
Showing 25 changed files with 759 additions and 10 deletions.
5 changes: 5 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/en/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------
Expand Down
1 change: 1 addition & 0 deletions docs/en/toc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Tutorials
tutorials/ordered-associations
tutorials/override-field-association-mappings-in-subclasses
tutorials/pagination.rst
tutorials/embeddables.rst

Reference Guide
---------------
Expand Down
83 changes: 83 additions & 0 deletions docs/en/tutorials/embeddables.rst
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions doctrine-mapping.xsd
Original file line number Diff line number Diff line change
Expand Up @@ -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"/>
Expand Down Expand Up @@ -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" />
Expand Down Expand Up @@ -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"/>
Expand Down Expand Up @@ -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"/>
Expand Down
24 changes: 24 additions & 0 deletions lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
$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);
Expand Down Expand Up @@ -140,6 +141,15 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
$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);
}
Expand Down Expand Up @@ -342,6 +352,20 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
}
}

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.
*
Expand Down
98 changes: 91 additions & 7 deletions lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
*
Expand All @@ -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.
*
Expand Down Expand Up @@ -799,6 +813,7 @@ public function __sleep()
'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName']
'fieldMappings',
'fieldNames',
'embeddedClasses',
'identifier',
'isIdentifierComposite', // TODO: REMOVE
'name',
Expand Down Expand Up @@ -907,6 +922,18 @@ public function wakeupReflection($reflService)
$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);
Expand Down Expand Up @@ -948,8 +975,12 @@ public function initializeReflection($reflService)
*/
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);
}

Expand Down Expand Up @@ -2150,6 +2181,11 @@ public function isInheritedAssociation($fieldName)
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.
*
Expand Down Expand Up @@ -2229,9 +2265,8 @@ private function _isInheritanceType($type)
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;
}

Expand Down Expand Up @@ -2479,9 +2514,7 @@ protected function _storeAssociationMapping(array $assocMapping)
{
$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;
}
Expand Down Expand Up @@ -3120,4 +3153,55 @@ public function getMetadataValue($name) {

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);
}
}
}
8 changes: 8 additions & 0 deletions lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ public function propertyToColumnName($propertyName, $className = null)
return $propertyName;
}

/**
* {@inheritdoc}
*/
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
{
return $propertyName.'_'.$embeddedColumnName;
}

/**
* {@inheritdoc}
*/
Expand Down
Loading

0 comments on commit 8a0901c

Please sign in to comment.