Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value objects (Based on #634) #835

Merged
merged 33 commits into from
Feb 8, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b4b9709
adds a new output format
schmittjoh Mar 3, 2013
02d34bb
[DDC-93] Started ValueObjectsTest
beberlei Feb 19, 2013
32988b3
[DDC-93] Parse @Embedded and @Embeddable during SchemaTool processing…
beberlei Mar 26, 2013
0204a8b
[DDC-93] Implement first working version of value objects using a Ref…
beberlei Mar 26, 2013
011776f
[DDC-93] Add some TODOs in code.
beberlei Mar 26, 2013
879ab6e
[DDC-93] Show CRUD with value objects with current change tracking as…
beberlei Mar 27, 2013
9613f1d
[DDC-93] Rename ReflectionProxy to ReflectionEmbeddedProperty, Add DQ…
beberlei Mar 27, 2013
38b041d
Merge remote-tracking branch 'origin/ValueObjects'
schmittjoh Nov 1, 2013
c67ac8a
adds support for selecting based on embedded fields
schmittjoh Nov 1, 2013
30897c3
adds tests for update/delete DQL queries
schmittjoh Nov 1, 2013
41c937b
adds test for non-existent field
schmittjoh Nov 1, 2013
fd8b5bd
removes outdated todos
schmittjoh Nov 1, 2013
20fb827
make use of NamingStrategy for columns of embedded fields
schmittjoh Nov 1, 2013
4f6c150
fixes coding style
schmittjoh Nov 1, 2013
f86abd8
fixes annotation context
schmittjoh Nov 1, 2013
97836ef
some consistency fixes
schmittjoh Nov 1, 2013
d4e6618
Merge remote-tracking branch 'schmittjoh/ValueObjects'
schmittjoh Nov 2, 2013
ece62d6
adds support & tests for embeddables in inheritance schemes
schmittjoh Nov 2, 2013
5586ddd
removes restrictions on constructors of embedded objects
schmittjoh Nov 2, 2013
0cd6061
fixes a bad merge
schmittjoh Nov 2, 2013
2b2f489
fixes declaring class
schmittjoh Nov 2, 2013
17e0a7b
makes column prefix configurable
schmittjoh Nov 2, 2013
9ad376c
adds docs
schmittjoh Nov 12, 2013
fb3a06b
adds support for XML/Yaml drivers
schmittjoh Nov 12, 2013
2a73a6f
some cs fixes
schmittjoh Nov 12, 2013
0ee7b68
small fix
schmittjoh Nov 12, 2013
e5cab1d
adds embedded classes to cache
schmittjoh Nov 28, 2013
928c32d
Update XML schema to reflect addition of embeddables
Dec 7, 2013
fbb7b5a
Fix XmlDriver to accept embeddables
Dec 7, 2013
f7f7c46
Merge pull request #1 from jankramer/ValueObjects
schmittjoh Dec 7, 2013
4f585a3
Merge branch 'master' of github.com:doctrine/doctrine2 into ValueObjects
schmittjoh Jan 4, 2014
9464194
fixes bad merge
schmittjoh Jan 4, 2014
7020f41
skips DQL UPDATE/DELETE tests with SLC enabled
schmittjoh Jan 4, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'])) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing line break between IFs

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, this looks the same like the code 10 lines above (you need to open the entire file).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but AFAIK this is wrong from a CS perspective. See doLoadMetadata(). But there are still no concrete CS specifications ;)

$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'])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

<?php
$declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
    ? $this->embeddedClasses[$mapping['declaredField']]['declared'] 
    : $this->name;

? $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 @@ -3116,4 +3149,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']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

souldn't this case be handled by the naming strategy too ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

columnPrefix is always defined by the user.

: $this->namingStrategy->embeddedFieldToColumnName($property, $fieldMapping['columnName'], $this->reflClass->name, $embeddable->reflClass->name);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrong indentation

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$this->reflClass->name, $embeddable->reflClass->name cause PHP Notice: Trying to get property of non-object if either of the reflClass properties are null. Suggest something like:

if (! empty($this->embeddedClasses[$property]['columnPrefix'])) {
    $fieldMapping['columnName'] = $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName'];
} else {
    $className = $this->reflClass ? $this->reflClass->name : null;
    $embeddedClassName = $embeddable->reflClass ? $embeddable->reflClass->name : null;
    $fieldMapping['columnName'] = $this->namingStrategy->embeddedFieldToColumnName($property, $fieldMapping['columnName'], $className, $embeddedClassName);
}


$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