Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Revived #265: [WIP] Mapping support for Embeddables (VOs). #547

Closed
wants to merge 9 commits into from

10 participants

@Burgov

I decided to merge #265 onto master in order to revive the branch. We're going to be in desperate need for something similar to this soon.

However, I'm getting a lot of errors by PHPUnit, so I clearly missed something. Can anyone tell me what needs to happen to get this stable again?

@beberlei

i vote for "isEmbedded". This is the same we use in the ODMs and compared to isEmbeddable does not suggest that you can also use it standalone.

Owner

In MongoDB ODM we use isEmbeddedDocument :( I suppose we could change it to be consistent.

@stof

you should add an empty line after this set of constant (and probably a comment to describe the next set of constants)

@stof

why using a locale variable instead of setting it directly in the property ?

@stof

you changed the logic here during the reformatting. Is it intended ?

Yes... it was a code that was already done. Basically I addressed the BasicEntityPersister by changing its logic.
It was just included in the commit when I pushed everything.

guilhermeblanco and others added some commits
@guilhermeblanco guilhermeblanco More work around Value Objects. cc960ba
@Burgov Burgov Merge remote-tracking branch 'upstream/DDC-93'
Conflicts:
	doctrine-mapping.xsd
	lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
	lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
	lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
	lib/Doctrine/ORM/Mapping/MappingException.php
	lib/Doctrine/ORM/Persisters/BasicEntityPersister.php
	lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
	lib/Doctrine/ORM/Tools/SchemaTool.php
	tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php
	tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php
	tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml
	tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml
24fdf9f
@doctrinebot
Collaborator

Hello,

thank you for positing this Pull Request. I have automatically opened an issue on our Jira Bug Tracker for you with the details of this Pull-Request. See the Link:

http://doctrine-project.org/jira/browse/DDC-2232

lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -554,6 +578,6 @@ protected function getDriver()
*/
protected function isEntity(ClassMetadataInterface $class)
{
- return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
+ return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false && isset($class->isEmbeddable) && $class->isEmbeddable;
@stof
stof added a note

This is wrong. Uou are sayign that a class is an entity only if it is embeddable. It does not make sense

@Burgov
Burgov added a note

Nice catch! That solved a lot of the errors already

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Mapping/Embeddable.php
((1 lines not shown))
+<?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 LGPL. For more information, see
@stof
stof added a note

This should be updated. We switched to MIT

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Mapping/MappingException.php
@@ -384,6 +394,16 @@ public static function duplicateAssociationMapping($entity, $fieldName)
/**
* @param string $entity
+ * @param string $fieldName
+ *
+ * @return MappingException
+ */
+ public static function duplicateEmbeddedMapping($entity, $fieldName) {
@stof
stof added a note

the curly brace should be on its own line

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Ocramius
Owner

@Burgov was this rebased?

lib/Doctrine/ORM/Tools/SchemaTool.php
@@ -497,7 +501,38 @@ private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$black
}
/**
- * Gets the class metadata that is responsible for the definition of the referenced column name.
+ * Gathers the SQL for properly setting up the embeddeds of the given class.
+ *
+ * @param ClassMetadata $class
+ * @param \Doctrine\DBAL\Schema\Table $table
+ * @param \Doctrine\DBAL\Schema\Schema $schema
+ * @return void
+ */
+ private function gatherEmbeddedsSql($class, $table, $schema)
+ {
+ foreach ($class->embeddedMappings as $embeddedFieldMapping) {
+ \Doctrine\Common\Util\Debug::dump($embeddedFieldMapping, 6);
@stof
stof added a note

this statement must be removed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Tools/SchemaTool.php
((16 lines not shown))
+
+ if (isset($embeddedFieldMapping['inherited'])) {
+ continue;
+ }
+
+ $embeddedClass = $this->em->getClassMetadata($embeddedFieldMapping['class']);
+
+ // Map each individual field in an optimized way
+ foreach ($embeddedClass->fieldMappings as $mapping) {
+ // Override fieldName for prefix generation
+ $mapping['fieldName'] = $embeddedFieldMapping['prefix'] . '_' . $mapping['fieldName'];
+
+ $this->gatherColumn($embeddedClass, $mapping, $table);
+ }
+
+ \Doctrine\Common\Util\Debug::dump($table, 6);
@stof
stof added a note

same here

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Tools/SchemaTool.php
((21 lines not shown))
+ $embeddedClass = $this->em->getClassMetadata($embeddedFieldMapping['class']);
+
+ // Map each individual field in an optimized way
+ foreach ($embeddedClass->fieldMappings as $mapping) {
+ // Override fieldName for prefix generation
+ $mapping['fieldName'] = $embeddedFieldMapping['prefix'] . '_' . $mapping['fieldName'];
+
+ $this->gatherColumn($embeddedClass, $mapping, $table);
+ }
+
+ \Doctrine\Common\Util\Debug::dump($table, 6);
+ }
+ }
+
+ /**
+ * Get the class metadata that is responsible for the definition of the referenced column name.
@stof
stof added a note

why removing the s in Gets ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@Burgov

@Ocramius i merged the old branch onto master, instead of rebasing. I hope that's not too big of a problem? Otherwise is there a way to use the merge commit to rebase it?

@Ocramius
Owner

@Burgov well, you can rebase now then... That will already be quite a nightmare after so much time. You could probably squash the commits before doing that

@Burgov

@Ocramius i'm not sure I'm following you? This PR is up to date with master, but that's thanks to the merge commit rather than a rebase action. I'm not sure how I can rebase this without redoing what I've been doing all day :)

@Ocramius
Owner

@Burgov sorry, I guess I just need to regain some focus :) my bad

@Burgov

Just spent some more time trying to find out what is going wrong, but I just can't get the hang of it. My guess is that everything will be fixed when the test below will be fixed. However, after debugging and looking at the old PR's code, I cannot find out exactly where and how the embedded mapping is supposed to be added to the $fieldMapping property

$ phpunit --filter SchemaTool
PHPUnit 3.7.9 by Sebastian Bergmann.

Configuration read from /home/bart/workspace/doctrine2/phpunit.xml.dist

..SSSSSSSSSSSSSE..E

Time: 2 seconds, Memory: 25.25Mb

There were 2 errors:

1) Doctrine\Tests\ORM\Tools\SchemaToolTest::testAddUniqueIndexForUniqueFieldAnnocation
Undefined index: parents_father

/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php:40
/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Tools/SchemaTool.php:371
/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Tools/SchemaTool.php:525
/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Tools/SchemaTool.php:229
/home/bart/workspace/doctrine2/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php:29

2) Doctrine\Tests\ORM\Tools\SchemaToolTest::testPostGenerateEvents
Undefined index: parents_father

/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Mapping/DefaultQuoteStrategy.php:40
/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Tools/SchemaTool.php:371
/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Tools/SchemaTool.php:525
/home/bart/workspace/doctrine2/lib/Doctrine/ORM/Tools/SchemaTool.php:229
/home/bart/workspace/doctrine2/tests/Doctrine/Tests/ORM/Tools/SchemaToolTest.php:99

FAILURES!
Tests: 19, Assertions: 14, Errors: 2, Skipped: 13.
``
@mvrhov

@Burgov are by any chance failing a postgresql tests?

@Burgov

@mvrhov

$ phpunit | grep -i postgresql
$

it appears not to be the case

@mvrhov

@Burgov I might be barking up the wrong tree, but the above looks quite like doctrine/dbal#221, that's why I asked if the fail only on postgresql

@beberlei
Owner

@Burgov i have this feature on my todo list very high and work on it, probably for 2.5. I am not sure opening this pull request here makes sense just to have this open. We are aware of this feature request, but its the most complicated feature addition of all time.

@Burgov

@beberlei it's good to know you're working on it (I had no way of knowing that though). Is there some kind of roadmap or an publicly visible branch for this feature?

@Burgov Burgov closed this
@jankramer

Very interested in this feature as well; +1 for publicly visible branch as soon there is something to see/test :)

@marcospassos

Killer feature. Looking forward for updates!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 20, 2012
  1. @guilhermeblanco
Commits on Jan 23, 2012
  1. @guilhermeblanco

    First round of refactorings after initial commit about VOs implementa…

    guilhermeblanco authored
    …tion after some valuable comments.
Commits on Jun 18, 2012
  1. @guilhermeblanco
Commits on Jan 10, 2013
  1. @Burgov

    Merge remote-tracking branch 'upstream/DDC-93'

    Burgov authored
    Conflicts:
    	doctrine-mapping.xsd
    	lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
    	lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
    	lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
    	lib/Doctrine/ORM/Mapping/MappingException.php
    	lib/Doctrine/ORM/Persisters/BasicEntityPersister.php
    	lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
    	lib/Doctrine/ORM/Tools/SchemaTool.php
    	tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php
    	tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php
    	tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml
    	tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml
  2. @Burgov

    fixed isEntity method

    Burgov authored
  3. @Burgov

    switched to correct license

    Burgov authored
  4. @Burgov

    fixed code formatting

    Burgov authored
  5. @Burgov

    removed debug code

    Burgov authored
  6. @Burgov
This page is out of date. Refresh to see the latest.
Showing with 602 additions and 75 deletions.
  1. +58 −34 doctrine-mapping.xsd
  2. +29 −5 lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
  3. +235 −21 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
  4. +18 −4 lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
  5. +2 −0  lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php
  6. +20 −0 lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
  7. +19 −0 lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
  8. +28 −0 lib/Doctrine/ORM/Mapping/Embeddable.php
  9. +33 −0 lib/Doctrine/ORM/Mapping/Embedded.php
  10. +27 −1 lib/Doctrine/ORM/Mapping/MappingException.php
  11. +23 −9 lib/Doctrine/ORM/Persisters/BasicEntityPersister.php
  12. +8 −0 lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
  13. +31 −0 lib/Doctrine/ORM/Tools/SchemaTool.php
  14. +34 −0 tests/Doctrine/Tests/Models/CMS/CmsParents.php
  15. +4 −0 tests/Doctrine/Tests/Models/CMS/CmsUser.php
  16. +24 −0 tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php
  17. +4 −0 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php
  18. +1 −0  tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml
  19. +4 −1 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml
View
92 doctrine-mapping.xsd
@@ -5,9 +5,9 @@
xmlns:orm="http://doctrine-project.org/schemas/orm/doctrine-mapping"
elementFormDefault="qualified">
- <xs:annotation>
- <xs:documentation><![CDATA[
- This is the XML Schema for the object/relational
+ <xs:annotation>
+ <xs:documentation><![CDATA[
+ This is the XML Schema for the object/relational
mapping file used by the Doctrine ORM.
]]></xs:documentation>
</xs:annotation>
@@ -17,31 +17,32 @@
<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"/>
</xs:complexType>
</xs:element>
-
+
<xs:complexType name="emptyType">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="cascade-type">
<xs:sequence>
- <xs:element name="cascade-all" type="orm:emptyType" minOccurs="0"/>
- <xs:element name="cascade-persist" type="orm:emptyType" minOccurs="0"/>
- <xs:element name="cascade-merge" type="orm:emptyType" minOccurs="0"/>
- <xs:element name="cascade-remove" type="orm:emptyType" minOccurs="0"/>
- <xs:element name="cascade-refresh" type="orm:emptyType" minOccurs="0"/>
+ <xs:element name="cascade-all" type="orm:emptyType" minOccurs="0"/>
+ <xs:element name="cascade-persist" type="orm:emptyType" minOccurs="0"/>
+ <xs:element name="cascade-merge" type="orm:emptyType" minOccurs="0"/>
+ <xs:element name="cascade-remove" type="orm:emptyType" minOccurs="0"/>
+ <xs:element name="cascade-refresh" type="orm:emptyType" minOccurs="0"/>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
</xs:sequence>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:simpleType name="lifecycle-callback-type">
<xs:restriction base="xs:token">
<xs:enumeration value="prePersist"/>
@@ -53,7 +54,7 @@
<xs:enumeration value="postLoad"/>
</xs:restriction>
</xs:simpleType>
-
+
<xs:complexType name="lifecycle-callback">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
@@ -62,7 +63,7 @@
<xs:attribute name="method" type="xs:NMTOKEN" use="required" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="lifecycle-callbacks">
<xs:sequence>
<xs:element name="lifecycle-callback" type="orm:lifecycle-callback" minOccurs="1" maxOccurs="unbounded" />
@@ -141,6 +142,7 @@
<xs:element name="named-native-queries" type="orm:named-native-queries" minOccurs="0" maxOccurs="1" />
<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" />
@@ -158,6 +160,7 @@
<xs:attribute name="read-only" type="xs:boolean" default="false" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
+
<xs:complexType name="option" mixed="true">
<xs:sequence minOccurs="0" maxOccurs="unbounded">
@@ -187,6 +190,17 @@
</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:anyAttribute namespace="##other"/>
+ </xs:extension>
+ </xs:complexContent>
+ </xs:complexType>
+
<xs:simpleType name="change-tracking-policy">
<xs:restriction base="xs:token">
<xs:enumeration value="DEFERRED_IMPLICIT"/>
@@ -194,7 +208,7 @@
<xs:enumeration value="NOTIFY"/>
</xs:restriction>
</xs:simpleType>
-
+
<xs:simpleType name="inheritance-type">
<xs:restriction base="xs:token">
<xs:enumeration value="SINGLE_TABLE"/>
@@ -202,7 +216,7 @@
<xs:enumeration value="TABLE_PER_CLASS"/>
</xs:restriction>
</xs:simpleType>
-
+
<xs:simpleType name="generator-strategy">
<xs:restriction base="xs:token">
<xs:enumeration value="TABLE"/>
@@ -213,18 +227,18 @@
<xs:enumeration value="CUSTOM" />
</xs:restriction>
</xs:simpleType>
-
- <xs:simpleType name="fk-action">
- <xs:restriction base="xs:token">
- <xs:enumeration value="CASCADE"/>
- <xs:enumeration value="RESTRICT"/>
+
+ <xs:simpleType name="fk-action">
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="CASCADE"/>
+ <xs:enumeration value="RESTRICT"/>
<xs:enumeration value="SET NULL"/>
- </xs:restriction>
+ </xs:restriction>
</xs:simpleType>
-
- <xs:simpleType name="fetch-type">
- <xs:restriction base="xs:token">
- <xs:enumeration value="EAGER"/>
+
+ <xs:simpleType name="fetch-type">
+ <xs:restriction base="xs:token">
+ <xs:enumeration value="EAGER"/>
<xs:enumeration value="LAZY"/>
<xs:enumeration value="EXTRA_LAZY"/>
</xs:restriction>
@@ -247,7 +261,17 @@
<xs:attribute name="scale" type="xs:integer" use="optional" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
+ <xs:complexType name="embedded">
+ <xs:sequence>
+ <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
+ </xs:sequence>
+ <xs:attribute name="name" type="xs:NMTOKEN" use="required" />
+ <xs:attribute name="class" type="xs:NMTOKEN" use="required" />
+ <xs:attribute name="prefix" type="xs:NMTOKEN" />
+ <xs:anyAttribute namespace="##other"/>
+ </xs:complexType>
+
<xs:complexType name="discriminator-column">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
@@ -259,7 +283,7 @@
<xs:attribute name="column-definition" type="xs:string" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="unique-constraint">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
@@ -268,7 +292,7 @@
<xs:attribute name="columns" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="unique-constraints">
<xs:sequence>
<xs:element name="unique-constraint" type="orm:unique-constraint" minOccurs="1" maxOccurs="unbounded"/>
@@ -276,7 +300,7 @@
</xs:sequence>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="index">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
@@ -285,7 +309,7 @@
<xs:attribute name="columns" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="indexes">
<xs:sequence>
<xs:element name="index" type="orm:index" minOccurs="1" maxOccurs="unbounded"/>
@@ -293,7 +317,7 @@
</xs:sequence>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="discriminator-mapping">
<xs:sequence>
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
@@ -302,7 +326,7 @@
<xs:attribute name="class" type="xs:string" use="required"/>
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="discriminator-map">
<xs:sequence>
<xs:element name="discriminator-mapping" type="orm:discriminator-mapping" minOccurs="1" maxOccurs="unbounded"/>
@@ -447,7 +471,7 @@
<xs:attribute name="fetch" type="orm:fetch-type" default="LAZY" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="many-to-one">
<xs:sequence>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
@@ -465,7 +489,7 @@
<xs:attribute name="inversed-by" type="xs:NMTOKEN" />
<xs:anyAttribute namespace="##other"/>
</xs:complexType>
-
+
<xs:complexType name="one-to-one">
<xs:sequence>
<xs:element name="cascade" type="orm:cascade-type" minOccurs="0" />
View
34 lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -94,6 +94,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
$class->setDiscriminatorColumn($parent->discriminatorColumn);
$class->setIdGeneratorType($parent->generatorType);
$this->addInheritedFields($class, $parent);
+ $this->addInheritedEmbeddeds($class, $parent);
$this->addInheritedRelations($class, $parent);
$class->setIdentifier($parent->identifier);
$class->setVersioned($parent->isVersioned);
@@ -188,11 +189,12 @@ protected function validateRuntimeMetadata($class, $parent)
}
$class->validateIdentifier();
+ $class->validateEmdeddeds();
$class->validateAssocations();
$class->validateLifecycleCallbacks($this->getReflectionService());
// verify inheritance
- if ( ! $class->isMappedSuperclass && !$class->isInheritanceTypeNone()) {
+ if ( ! $class->isMappedSuperclass && ! $class->isEmbeddable && !$class->isInheritanceTypeNone()) {
if ( ! $parent) {
if (count($class->discriminatorMap) == 0) {
throw MappingException::missingDiscriminatorMap($class->name);
@@ -286,7 +288,7 @@ private function getShortName($className)
private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $parentClass)
{
foreach ($parentClass->fieldMappings as $mapping) {
- if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
+ if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass && ! $parentClass->isEmbeddable) {
$mapping['inherited'] = $parentClass->name;
}
if ( ! isset($mapping['declared'])) {
@@ -299,6 +301,28 @@ private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $pare
}
}
+
+ /**
+ * Adds inherited fields to the subclass mapping.
+ *
+ * @param \Doctrine\ORM\Mapping\ClassMetadata $subClass
+ * @param \Doctrine\ORM\Mapping\ClassMetadata $parentClass
+ */
+ private function addInheritedEmbeddeds(ClassMetadata $subClass, ClassMetadata $parentClass)
+ {
+ foreach ($parentClass->embeddedMappings as $fieldName => $mapping) {
+ if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass && ! $parentClass->isEmbeddable) {
+ $mapping['inherited'] = $parentClass->name;
+ }
+
+ if ( ! isset($mapping['declared'])) {
+ $mapping['declared'] = $parentClass->name;
+ }
+
+ $subClass->addInheritedEmbeddedMapping($mapping);
+ }
+ }
+
/**
* Adds inherited association mappings to the subclass mapping.
*
@@ -312,7 +336,7 @@ private function addInheritedFields(ClassMetadata $subClass, ClassMetadata $pare
private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $parentClass)
{
foreach ($parentClass->associationMappings as $field => $mapping) {
- if ($parentClass->isMappedSuperclass) {
+ if ($parentClass->isMappedSuperclass || $parentClass->isEmbeddable) {
if ($mapping['type'] & ClassMetadata::TO_MANY && !$mapping['isOwningSide']) {
throw MappingException::illegalToManyAssocationOnMappedSuperclass($parentClass->name, $field);
}
@@ -320,7 +344,7 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
}
//$subclassMapping = $mapping;
- if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass) {
+ if ( ! isset($mapping['inherited']) && ! $parentClass->isMappedSuperclass && ! $parentClass->isEmbeddable) {
$mapping['inherited'] = $parentClass->name;
}
if ( ! isset($mapping['declared'])) {
@@ -554,6 +578,6 @@ protected function getDriver()
*/
protected function isEntity(ClassMetadataInterface $class)
{
- return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false;
+ return isset($class->isMappedSuperclass) && $class->isMappedSuperclass === false && isset($class->isEmbeddable) && $class->isEmbeddable === false;
}
}
View
256 lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -140,6 +140,20 @@ class ClassMetadataInfo implements ClassMetadata
*/
const CHANGETRACKING_NOTIFY = 3;
+ /* The Entity types */
+ /**
+ * Specifies that current mapping points to an entity class.
+ */
+ const TYPE_ENTITY = 1;
+ /**
+ * Specifies that current mapping points to a mapped superclass.
+ */
+ const TYPE_MAPPEDSUPERCLASS = 2;
+ /**
+ * Specifies that current mapping points to an embeddable class.
+ */
+ const TYPE_EMBEDDABLE = 3;
+
/**
* Specifies that an association is to be fetched when it is first accessed.
*/
@@ -196,6 +210,12 @@ class ClassMetadataInfo implements ClassMetadata
public $name;
/**
+ * READ-ONLY: The type of the class. Can be any of ClassMetadata::TYPE_* constants.
+ * @var type
+ */
+ public $type = self::TYPE_ENTITY;
+
+ /**
* READ-ONLY: The namespace the entity class is contained in.
*
* @var string
@@ -246,6 +266,13 @@ class ClassMetadataInfo implements ClassMetadata
public $isMappedSuperclass = false;
/**
+ * READ-ONLY: Whether this class describes the mapping of a mapped embeddable.
+ *
+ * @var boolean
+ */
+ public $isEmbeddable = false;
+
+ /**
* READ-ONLY: The names of the parent classes (ancestors).
*
* @var array
@@ -494,6 +521,23 @@ class ClassMetadataInfo implements ClassMetadata
public $associationMappings = array();
/**
+ * READ-ONLY: The embedded mappings of this class.
+ * Keys are field names and value are mapping definitions.
+ * The mapping definition array supports the following keys:
+ *
+ * - <b>fieldName</b> (string)
+ * The name of the field in the entity the embeddable is mapped to.
+ *
+ * = <b>class</b> (string)
+ * The class name of the embeddable class. If it is fully-qualified it is used as is.
+ * If it is a simple, unqualified class name the namespace is assumed to be the same
+ * as the namespace of the source entity.
+ *
+ * @var array
+ */
+ public $embeddedMappings = array();
+
+ /**
* READ-ONLY: Flag indicating whether the identifier/primary key of the class is composite.
*
* @var boolean
@@ -767,6 +811,7 @@ public function __sleep()
{
// This metadata is always serialized/cached.
$serialized = array(
+ 'embeddedMappings',
'associationMappings',
'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName']
'fieldMappings',
@@ -881,6 +926,12 @@ public function wakeupReflection($reflService)
? $reflService->getAccessibleProperty($mapping['declared'], $field)
: $reflService->getAccessibleProperty($this->name, $field);
}
+
+ foreach ($this->embeddedMappings as $field => $mapping) {
+ $this->reflFields[$field] = isset($mapping['declared'])
+ ? $reflService->getAccessibleProperty($mapping['declared'], $field)
+ : $reflService->getAccessibleProperty($this->name, $field);
+ }
}
/**
@@ -913,7 +964,7 @@ public function initializeReflection($reflService)
public function validateIdentifier()
{
// Verify & complete identifier mapping
- if ( ! $this->identifier && ! $this->isMappedSuperclass) {
+ if ( ! $this->identifier && ! $this->isMappedSuperclass && ! $this->isEmbeddable) {
throw MappingException::identifierRequired($this->name);
}
@@ -923,6 +974,19 @@ public function validateIdentifier()
}
/**
+ * Validate that emdeddables actually exist.
+ *
+ * @return void
+ */
+ public function validateEmdeddeds()
+ {
+ foreach ($this->embeddedMappings as $field => $mapping) {
+ if ( ! \Doctrine\Common\ClassLoader::classExists($mapping['class']) ) {
+ throw MappingException::invalidEmbeddedClass($mapping['class'], $this->name, $mapping['fieldName']);
+ }
+ }
+ }
+ /**
* Validates association targets actually exist.
*
* @return void
@@ -1090,6 +1154,32 @@ public function getFieldMapping($fieldName)
}
/**
+ * Gets the mapping of an embeddable.
+ *
+ * @see ClassMetadataInfo::$embeddedMappings
+ * @param string $fieldName The field name that represents the embeddable in
+ * the object model.
+ * @return array The mapping.
+ */
+ public function getEmbeddedMapping($fieldName)
+ {
+ if ( ! isset($this->embeddedMappings[$fieldName])) {
+ throw MappingException::mappingNotFound($this->name, $fieldName);
+ }
+ return $this->embeddedMappings[$fieldName];
+ }
+
+ /**
+ * Gets all embedded mappings of the class.
+ *
+ * @return array
+ */
+ public function getEmbeddedMappings()
+ {
+ return $this->embeddedMappings;
+ }
+
+ /**
* Gets the mapping of an association.
*
* @see ClassMetadataInfo::$associationMappings
@@ -1237,6 +1327,7 @@ protected function _validateAndCompleteFieldMapping(array &$mapping)
if ( ! isset($mapping['fieldName']) || strlen($mapping['fieldName']) == 0) {
throw MappingException::missingFieldName($this->name);
}
+
if ( ! isset($mapping['type'])) {
// Default to string
$mapping['type'] = 'string';
@@ -1253,6 +1344,7 @@ protected function _validateAndCompleteFieldMapping(array &$mapping)
}
$this->columnNames[$mapping['fieldName']] = $mapping['columnName'];
+
if (isset($this->fieldNames[$mapping['columnName']]) || ($this->discriminatorColumn != null && $this->discriminatorColumn['name'] == $mapping['columnName'])) {
throw MappingException::duplicateColumnName($this->name, $mapping['columnName']);
}
@@ -1281,6 +1373,38 @@ protected function _validateAndCompleteFieldMapping(array &$mapping)
$mapping['requireSQLConversion'] = true;
}
+
+ return $mapping;
+ }
+
+ /**
+ * Validates & completes the given embedded mapping.
+ *
+ * @param array $mapping The embedded mapping to validated & complete.
+ * @return array The validated and completed embedded mapping.
+ */
+ protected function _validateAndCompleteEmbeddedMapping(array $mapping)
+ {
+ // Check mandatory fields
+ if ( ! isset($mapping['fieldName']) || strlen($mapping['fieldName']) == 0) {
+ throw MappingException::missingFieldName($this->name);
+ }
+
+ if ( ! isset($mapping['class'])) {
+ throw MappingException::missingEmbeddedClass($mapping['fieldName']);
+ }
+
+ if (strlen($this->namespace) > 0 && strpos($mapping['class'], '\\') === false) {
+ $mapping['class'] = $this->namespace . '\\' . $mapping['class'];
+ }
+
+ $mapping['class'] = ltrim($mapping['class'], '\\');
+
+ if ( ! isset($mapping['prefix'])) {
+ $mapping['prefix'] = $this->namingStrategy->propertyToColumnName($mapping['fieldName']);
+ }
+
+ return $mapping;
}
/**
@@ -1298,9 +1422,11 @@ protected function _validateAndCompleteAssociationMapping(array $mapping)
if ( ! isset($mapping['mappedBy'])) {
$mapping['mappedBy'] = null;
}
+
if ( ! isset($mapping['inversedBy'])) {
$mapping['inversedBy'] = null;
}
+
$mapping['isOwningSide'] = true; // assume owning side until we hit mappedBy
// unset optional indexBy attribute if its empty
@@ -1343,6 +1469,7 @@ protected function _validateAndCompleteAssociationMapping(array $mapping)
$this->identifier[] = $mapping['fieldName'];
$this->containsForeignIdentifier = true;
}
+
// Check for composite key
if ( ! $this->isIdentifierComposite && count($this->identifier) > 1) {
$this->isIdentifierComposite = true;
@@ -1354,6 +1481,7 @@ protected function _validateAndCompleteAssociationMapping(array $mapping)
if ( ! isset($mapping['fieldName']) || strlen($mapping['fieldName']) == 0) {
throw MappingException::missingFieldName($this->name);
}
+
if ( ! isset($mapping['targetEntity'])) {
throw MappingException::missingTargetEntity($mapping['fieldName']);
}
@@ -1857,8 +1985,9 @@ public function isIdentifierUuid()
*/
public function getTypeOfField($fieldName)
{
- return isset($this->fieldMappings[$fieldName]) ?
- $this->fieldMappings[$fieldName]['type'] : null;
+ return isset($this->fieldMappings[$fieldName])
+ ? $this->fieldMappings[$fieldName]['type']
+ : null;
}
/**
@@ -2061,6 +2190,17 @@ public function isRootEntity()
}
/**
+ * Checks whether a mapped embedded field is inherited from a superclass.
+ *
+ * @param string $fieldName
+ * @return boolean TRUE if the field is inherited, FALSE otherwise.
+ */
+ public function isInheritedEmbedded($fieldName)
+ {
+ return isset($this->embeddedMappings[$fieldName]['inherited']);
+ }
+
+ /**
* Checks whether a mapped association field is inherited from a superclass.
*
* @param string $fieldName
@@ -2140,7 +2280,9 @@ private function _isInheritanceType($type)
}
/**
- * Adds a mapped field to the class.
+ * INTERNAL:
+ * Adds an association mapping without completing/validating it.
+ * This is mainly used to add inherited association mappings to derived classes.
*
* @param array $mapping The field mapping.
*
@@ -2148,19 +2290,18 @@ private function _isInheritanceType($type)
*
* @throws MappingException
*/
- public function mapField(array $mapping)
+ public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/)
{
- $this->_validateAndCompleteFieldMapping($mapping);
- if (isset($this->fieldMappings[$mapping['fieldName']]) || isset($this->associationMappings[$mapping['fieldName']])) {
- throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']);
+ if (isset($this->associationMappings[$mapping['fieldName']])) {
+ throw MappingException::duplicateAssociationMapping($this->name, $mapping['fieldName']);
}
- $this->fieldMappings[$mapping['fieldName']] = $mapping;
+ $this->associationMappings[$mapping['fieldName']] = $mapping;
}
/**
* INTERNAL:
- * Adds an association mapping without completing/validating it.
- * This is mainly used to add inherited association mappings to derived classes.
+ * Adds an embedded mapping without completing/validating it.
+ * This is mainly used to add inherited enbedded mappings to derived classes.
*
* @param array $mapping
*
@@ -2168,12 +2309,12 @@ public function mapField(array $mapping)
*
* @throws MappingException
*/
- public function addInheritedAssociationMapping(array $mapping/*, $owningClassName = null*/)
+ public function addInheritedEmbeddedMapping(array $mapping/*, $owningClassName = null*/)
{
- if (isset($this->associationMappings[$mapping['fieldName']])) {
- throw MappingException::duplicateAssociationMapping($this->name, $mapping['fieldName']);
+ if (isset($this->embeddedMappings[$mapping['fieldName']])) {
+ throw MappingException::duplicateEmbeddedMapping($this->name, $mapping['fieldName']);
}
- $this->associationMappings[$mapping['fieldName']] = $mapping;
+ $this->embeddedMappings[$mapping['fieldName']] = $mapping;
}
/**
@@ -2334,6 +2475,44 @@ public function addSqlResultSetMapping(array $resultMapping)
}
/**
+ * Adds a mapped field to the class.
+ *
+ * @param array $mapping The field mapping.
+ */
+ public function mapField(array $mapping)
+ {
+ $mapping = $this->_validateAndCompleteFieldMapping($mapping);
+ $sourceFieldName = $mapping['fieldName'];
+
+ if (isset($this->fieldMappings[$sourceFieldName]) ||
+ isset($this->associationMappings[$sourceFieldName]) ||
+ isset($this->embeddedMappings[$sourceFieldName])) {
+ throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName);
+ }
+
+ $this->fieldMappings[$sourceFieldName] = $mapping;
+ }
+
+ /**
+ * Adds a mapped embedded to the class.
+ *
+ * @param array $mapping The embedded mapping.
+ */
+ public function mapEmbedded(array $mapping)
+ {
+ $mapping = $this->_validateAndCompleteEmbeddedMapping($mapping);
+ $sourceFieldName = $mapping['fieldName'];
+
+ if (isset($this->fieldMappings[$sourceFieldName]) ||
+ isset($this->associationMappings[$sourceFieldName]) ||
+ isset($this->embeddedMappings[$sourceFieldName])) {
+ throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName);
+ }
+
+ $this->embeddedMappings[$sourceFieldName] = $mapping;
+ }
+
+ /**
* Adds a one-to-one mapping.
*
* @param array $mapping The mapping.
@@ -2343,7 +2522,9 @@ public function addSqlResultSetMapping(array $resultMapping)
public function mapOneToOne(array $mapping)
{
$mapping['type'] = self::ONE_TO_ONE;
+
$mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
+
$this->_storeAssociationMapping($mapping);
}
@@ -2357,7 +2538,9 @@ public function mapOneToOne(array $mapping)
public function mapOneToMany(array $mapping)
{
$mapping['type'] = self::ONE_TO_MANY;
+
$mapping = $this->_validateAndCompleteOneToManyMapping($mapping);
+
$this->_storeAssociationMapping($mapping);
}
@@ -2371,8 +2554,10 @@ public function mapOneToMany(array $mapping)
public function mapManyToOne(array $mapping)
{
$mapping['type'] = self::MANY_TO_ONE;
+
// A many-to-one mapping is essentially a one-one backreference
$mapping = $this->_validateAndCompleteOneToOneMapping($mapping);
+
$this->_storeAssociationMapping($mapping);
}
@@ -2386,7 +2571,9 @@ public function mapManyToOne(array $mapping)
public function mapManyToMany(array $mapping)
{
$mapping['type'] = self::MANY_TO_MANY;
+
$mapping = $this->_validateAndCompleteManyToManyMapping($mapping);
+
$this->_storeAssociationMapping($mapping);
}
@@ -2399,15 +2586,17 @@ public function mapManyToMany(array $mapping)
*
* @throws MappingException
*/
- protected function _storeAssociationMapping(array $assocMapping)
+ protected function _storeAssociationMapping(array $mapping)
{
- $sourceFieldName = $assocMapping['fieldName'];
+ $sourceFieldName = $mapping['fieldName'];
- if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) {
+ if (isset($this->fieldMappings[$sourceFieldName]) ||
+ isset($this->associationMappings[$sourceFieldName]) ||
+ isset($this->embeddedMappings[$sourceFieldName])) {
throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName);
}
- $this->associationMappings[$sourceFieldName] = $assocMapping;
+ $this->associationMappings[$sourceFieldName] = $mapping;
}
/**
@@ -2419,10 +2608,12 @@ protected function _storeAssociationMapping(array $assocMapping)
*/
public function setCustomRepositoryClass($repositoryClassName)
{
- if ($repositoryClassName !== null && strpos($repositoryClassName, '\\') === false
- && strlen($this->namespace) > 0) {
+ if ($repositoryClassName !== null &&
+ strpos($repositoryClassName, '\\') === false &&
+ strlen($this->namespace) > 0) {
$repositoryClassName = $this->namespace . '\\' . $repositoryClassName;
}
+
$this->customRepositoryClassName = $repositoryClassName;
}
@@ -2601,6 +2792,17 @@ public function hasNamedNativeQuery($queryName)
}
/**
+ * Checks whether the class has a mapped embeddedable with the given field name.
+ *
+ * @param string $fieldName
+ * @return boolean
+ */
+ public function hasEmbedded($fieldName)
+ {
+ return isset($this->embeddedMappings[$fieldName]);
+ }
+
+ /**
* Checks whether the class has a named native query with the given query name.
*
* @param string $name
@@ -2837,6 +3039,18 @@ public function getFieldNames()
}
/**
+ * A numerically indexed list of embedded names of this persistent class.
+ *
+ * This array includes identifier embeddedables if present on this class.
+ *
+ * @return array
+ */
+ public function getEmbeddedNames()
+ {
+ return array_keys($this->embeddedMappings);
+ }
+
+ /**
* {@inheritDoc}
*/
public function getAssociationNames()
View
22 lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -83,6 +83,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
$mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
$metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass);
$metadata->isMappedSuperclass = true;
+ } else if (isset($classAnnotations['Doctrine\ORM\Mapping\Embeddable'])) {
+ $metadata->markReadOnly();
+ $metadata->isEmbeddable = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
@@ -238,7 +241,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
||
$metadata->isInheritedField($property->name)
||
- $metadata->isInheritedAssociation($property->name)) {
+ $metadata->isInheritedAssociation($property->name)
+ ||
+ $metadata->isInheritedEmbedded($property->name)) {
continue;
}
@@ -257,7 +262,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
}
// Field can only be annotated with one of:
- // @Column, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany
+ // @Column, @Embedded, @OneToOne, @OneToMany, @ManyToOne, @ManyToMany
if ($columnAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Column')) {
if ($columnAnnot->type == null) {
throw MappingException::propertyTypeIsRequired($className, $property->getName());
@@ -282,9 +287,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
// Check for SequenceGenerator/TableGenerator definition
if ($seqGeneratorAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\SequenceGenerator')) {
$metadata->setSequenceGeneratorDefinition(array(
- 'sequenceName' => $seqGeneratorAnnot->sequenceName,
+ 'sequenceName' => $seqGeneratorAnnot->sequenceName,
'allocationSize' => $seqGeneratorAnnot->allocationSize,
- 'initialValue' => $seqGeneratorAnnot->initialValue
+ 'initialValue' => $seqGeneratorAnnot->initialValue
));
} else if ($this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\TableGenerator')) {
throw MappingException::tableIdGeneratorNotImplemented($className);
@@ -293,6 +298,14 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
'class' => $customGeneratorAnnot->class
));
}
+ } else if ($embeddedAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Embedded')) {
+ $mapping['class'] = $embeddedAnnot->class;
+
+ if (isset($embeddedAnnot->prefix)) {
+ $mapping['prefix'] = $embeddedAnnot->prefix;
+ }
+
+ $metadata->mapEmbedded($mapping);
} else if ($oneToOneAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\OneToOne')) {
if ($idAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Id')) {
$mapping['id'] = true;
@@ -329,6 +342,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
$mapping['inversedBy'] = $manyToOneAnnot->inversedBy;
$mapping['targetEntity'] = $manyToOneAnnot->targetEntity;
$mapping['fetch'] = $this->getFetchMode($className, $manyToOneAnnot->fetch);
+
$metadata->mapManyToOne($mapping);
} else if ($manyToManyAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\ManyToMany')) {
$joinTable = array();
View
2  lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php
@@ -20,6 +20,7 @@
require_once __DIR__.'/../Annotation.php';
require_once __DIR__.'/../Entity.php';
require_once __DIR__.'/../MappedSuperclass.php';
+require_once __DIR__.'/../Embeddable.php';
require_once __DIR__.'/../InheritanceType.php';
require_once __DIR__.'/../DiscriminatorColumn.php';
require_once __DIR__.'/../DiscriminatorMap.php';
@@ -29,6 +30,7 @@
require_once __DIR__.'/../JoinColumn.php';
require_once __DIR__.'/../JoinColumns.php';
require_once __DIR__.'/../Column.php';
+require_once __DIR__.'/../Embedded.php';
require_once __DIR__.'/../OneToOne.php';
require_once __DIR__.'/../OneToMany.php';
require_once __DIR__.'/../ManyToOne.php';
View
20 lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
@@ -68,6 +68,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null
);
$metadata->isMappedSuperclass = true;
+ } else if ($xmlRoot->getName() == 'embeddable') {
+ $metadata->markReadOnly();
+ $metadata->isEmbeddable = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
@@ -249,6 +252,7 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
// Evaluate <id ...> mappings
$associationIds = array();
+
foreach ($xmlRoot->id as $idElement) {
if (isset($idElement['association-key']) && $this->evaluateBoolean($idElement['association-key'])) {
$associationIds[(string)$idElement['name']] = true;
@@ -303,6 +307,22 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
}
}
+ // Evaluate <embedded ...> mappings
+ if (isset($xmlRoot->{'embedded'})) {
+ foreach ($xmlRoot->{'embedded'} as $embeddedMapping) {
+ $mapping = array(
+ 'fieldName' => (string) $embeddedMapping['name'],
+ 'class' => (string) $embeddedMapping['class'],
+ );
+
+ if (isset($embeddedMapping['prefix'])) {
+ $mapping['prefix'] = (string) $embeddedMapping['prefix'];
+ }
+
+ $metadata->mapEmbedded($mapping);
+ }
+ }
+
// Evaluate <one-to-one ...> mappings
if (isset($xmlRoot->{'one-to-one'})) {
foreach ($xmlRoot->{'one-to-one'} as $oneToOneElement) {
View
19 lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
@@ -65,6 +65,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
isset($element['repositoryClass']) ? $element['repositoryClass'] : null
);
$metadata->isMappedSuperclass = true;
+ } else if ($element['type'] == 'embeddable') {
+ $metadata->markReadOnly();
+ $metadata->isEmbeddable = true;
} else {
throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
}
@@ -305,6 +308,22 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
}
}
+ // Evaluate embedded fields
+ if (isset($element['embedded'])) {
+ foreach ($element['embedded'] as $name => $embeddedMapping) {
+ $mapping = array(
+ 'fieldName' => $name,
+ 'class' => $embeddedMapping['class'],
+ );
+
+ if (isset($embeddedMapping['prefix'])) {
+ $mapping['prefix'] = $embeddedMapping['prefix'];
+ }
+
+ $metadata->mapEmbedded($mapping);
+ }
+ }
+
// Evaluate oneToOne relationships
if (isset($element['oneToOne'])) {
foreach ($element['oneToOne'] as $name => $oneToOneElement) {
View
28 lib/Doctrine/ORM/Mapping/Embeddable.php
@@ -0,0 +1,28 @@
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the MIT license. For more information, see
+ * <http://www.doctrine-project.org>.
+ */
+
+namespace Doctrine\ORM\Mapping;
+
+/**
+ * @Annotation
+ * @Target("CLASS")
+ */
+final class Embeddable implements Annotation
+{
+}
View
33 lib/Doctrine/ORM/Mapping/Embedded.php
@@ -0,0 +1,33 @@
+<?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
+{
+ /** @var string */
+ public $class;
+
+ /** @var string */
+ public $prefix;
+}
View
28 lib/Doctrine/ORM/Mapping/MappingException.php
@@ -90,6 +90,16 @@ public static function missingFieldName($entity)
*
* @return MappingException
*/
+ public static function missingEmbeddedClass($fieldName)
+ {
+ return new self("The embedded mapping '$fieldName' misses the 'class' attribute.");
+ }
+
+ /**
+ * @param string $fieldName
+ *
+ * @return MappingException
+ */
public static function missingTargetEntity($fieldName)
{
return new self("The association mapping '$fieldName' misses the 'targetEntity' attribute.");
@@ -384,6 +394,17 @@ public static function duplicateAssociationMapping($entity, $fieldName)
/**
* @param string $entity
+ * @param string $fieldName
+ *
+ * @return MappingException
+ */
+ public static function duplicateEmbeddedMapping($entity, $fieldName)
+ {
+ return new self('Property "'.$fieldName.'" in "'.$entity.'" was already declared, but it must be declared only once');
+ }
+
+ /**
+ * @param string $entity
* @param string $queryName
*
* @return MappingException
@@ -714,7 +735,12 @@ public static function compositeKeyAssignedIdGeneratorRequired($className)
*/
public static function invalidTargetEntityClass($targetEntity, $sourceEntity, $associationName)
{
- return new self("The target-entity " . $targetEntity . " cannot be found in '" . $sourceEntity."#".$associationName."'.");
+ return new self("The target-entity " . $targetEntity . " cannot be found in '" . $sourceEntity . "#" . $associationName . "'.");
+ }
+
+ public static function invalidEmbeddedClass($embeddedClass, $sourceEntity, $fieldName)
+ {
+ return new self("The embedded " . $embeddedClass . " cannot be found in '" . $sourceEntity . "#" . $fieldName . "'.");
}
/**
View
32 lib/Doctrine/ORM/Persisters/BasicEntityPersister.php
@@ -1395,27 +1395,41 @@ protected function getInsertSQL()
*/
protected function getInsertColumnList()
{
- $columns = array();
+ return $this->getClassMetadataInsertColumnList($this->class, array());
+ }
+
+ private function getClassMetadataInsertColumnList($class, $columns)
+ {
- foreach ($this->class->reflFields as $name => $field) {
- if ($this->class->isVersioned && $this->class->versionField == $name) {
+ foreach ($class->reflFields as $name => $field) {
+ if ($class->isVersioned && $this->class->versionField == $name) {
continue;
}
- if (isset($this->class->associationMappings[$name])) {
- $assoc = $this->class->associationMappings[$name];
+ if (isset($class->associationMappings[$name])) {
+ $assoc = $class->associationMappings[$name];
if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
foreach ($assoc['joinColumns'] as $joinColumn) {
- $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
+ $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
}
}
continue;
}
+
+ if (isset($class->embeddedMappings[$name])) {
+ $embed = $class->embeddedMappings[$name];
+ $embeddable = $this->em->getClassMetadata($embed['class']);
+ $embeddedColumns = $this->getClassMetadataInsertColumnList($embeddable, array());
+
+ foreach(array_keys($embeddedColumns) as $embeddedColumnName) {
+ $columns[] = $embed['prefix'] . '_' . $embeddedColumnName;
+ }
+ }
- if ($this->class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->class->identifier[0] != $name) {
- $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
- $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
+ if ($class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $class->identifier[0] != $name) {
+ $columns[] = $this->quoteStrategy->getColumnName($name, $class, $this->platform);
+ $this->columnTypes[$name] = $class->fieldMappings[$name]['type'];
}
}
View
8 lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php
@@ -520,6 +520,7 @@ protected function getInsertColumnList()
if (isset($this->class->fieldMappings[$name]['inherited'])
&& ! isset($this->class->fieldMappings[$name]['id'])
|| isset($this->class->associationMappings[$name]['inherited'])
+ || isset($this->class->embeddedMappings[$name]['inherited'])
|| ($this->class->isVersioned && $this->class->versionField == $name)) {
continue;
}
@@ -531,6 +532,13 @@ protected function getInsertColumnList()
$columns[] = $sourceCol;
}
}
+ } else if (isset($this->class->embeddedMappings[$name])) {
+ $embedded = $this->class->embeddedMappings[$name];
+ $embeddedClass = $this->em->getClassMetadata($embedded['class']);
+
+ foreach ($embeddedClass->fieldMappings as $embeddedFieldName => $embeddedFieldMapping) {
+ $columns[] = $embeddedClass->getQuotedColumnName($embeddedFieldName, $this->platform);
+ }
} else if ($this->class->name != $this->class->rootEntityName ||
! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] != $name) {
$columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
View
31 lib/Doctrine/ORM/Tools/SchemaTool.php
@@ -163,6 +163,7 @@ public function getSchemaFromMetadata(array $classes)
if ($class->isInheritanceTypeSingleTable()) {
$columns = $this->gatherColumns($class, $table);
$this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
+ $this->gatherEmbeddedsSql($class, $table, $schema);
// Add the discriminator column
$this->addDiscriminatorColumnDefinition($class, $table);
@@ -194,6 +195,7 @@ public function getSchemaFromMetadata(array $classes)
}
$this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
+ $this->gatherEmbeddedsSql($class, $table, $schema);
// Add the discriminator column only to the root table
if ($class->name == $class->rootEntityName) {
@@ -221,8 +223,10 @@ public function getSchemaFromMetadata(array $classes)
} elseif ($class->isInheritanceTypeTablePerClass()) {
throw ORMException::notSupported();
} else {
+
$this->gatherColumns($class, $table);
$this->gatherRelationsSql($class, $table, $schema, $addedFks, $blacklistedFks);
+ $this->gatherEmbeddedsSql($class, $table, $schema);
}
$pkColumns = array();
@@ -497,6 +501,33 @@ private function gatherRelationsSql($class, $table, $schema, &$addedFks, &$black
}
/**
+ * Gathers the SQL for properly setting up the embeddeds of the given class.
+ *
+ * @param ClassMetadata $class
+ * @param \Doctrine\DBAL\Schema\Table $table
+ * @param \Doctrine\DBAL\Schema\Schema $schema
+ * @return void
+ */
+ private function gatherEmbeddedsSql($class, $table, $schema)
+ {
+ foreach ($class->embeddedMappings as $embeddedFieldMapping) {
+ if (isset($embeddedFieldMapping['inherited'])) {
+ continue;
+ }
+
+ $embeddedClass = $this->em->getClassMetadata($embeddedFieldMapping['class']);
+
+ // Map each individual field in an optimized way
+ foreach ($embeddedClass->fieldMappings as $mapping) {
+ // Override fieldName for prefix generation
+ $mapping['fieldName'] = $embeddedFieldMapping['prefix'] . '_' . $mapping['fieldName'];
+
+ $this->gatherColumn($embeddedClass, $mapping, $table);
+ }
+ }
+ }
+
+ /**
* Gets the class metadata that is responsible for the definition of the referenced column name.
*
* Previously this was a simple task, but with DDC-117 this problem is actually recursive. If its
View
34 tests/Doctrine/Tests/Models/CMS/CmsParents.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Doctrine\Tests\Models\CMS;
+
+/**
+ * @Embeddable
+ */
+class CmsParents
+{
+ /**
+ * @Column(length=250)
+ */
+ public $father;
+ /**
+ * @Column(length=250)
+ */
+ public $mother;
+
+ public function setFather($father) {
+ $this->father = $father;
+ }
+
+ public function getFather() {
+ return $this->father;
+ }
+
+ public function setMother($mother) {
+ $this->mother = $mother;
+ }
+
+ public function getMother() {
+ return $this->mother;
+ }
+}
View
4 tests/Doctrine/Tests/Models/CMS/CmsUser.php
@@ -138,6 +138,10 @@ class CmsUser
*/
public $name;
/**
+ * @Embedded(class="CmsParents", prefix="parents")
+ */
+ public $parents;
+ /**
* @OneToMany(targetEntity="CmsPhonenumber", mappedBy="user", cascade={"persist", "merge"}, orphanRemoval=true)
*/
public $phonenumbers;
View
24 tests/Doctrine/Tests/ORM/Mapping/AbstractMappingDriverTest.php
@@ -205,6 +205,20 @@ public function testIdentifier($class)
* @depends testIdentifier
* @param ClassMetadata $class
*/
+ public function testEmbeddeds($class)
+ {
+ $this->assertEquals(1, count($class->embeddedMappings));
+
+ $this->assertTrue(isset($class->embeddedMappings['parents']));
+ $this->assertEquals('Doctrine\Tests\ORM\Mapping\Parents', $class->embeddedMappings['parents']['class']);
+
+ return $class;
+ }
+
+ /**
+ * @depends testEmbeddeds
+ * @param ClassMetadata $class
+ */
public function testAssocations($class)
{
$this->assertEquals(3, count($class->associationMappings));
@@ -784,6 +798,11 @@ class User
public $email;
/**
+ * @Embedded(class="Parents")
+ */
+ public $parents;
+
+ /**
* @OneToOne(targetEntity="Address", cascade={"remove"}, inversedBy="user")
* @JoinColumn(onDelete="CASCADE")
*/
@@ -867,6 +886,10 @@ public static function loadMetadata(ClassMetadataInfo $metadata)
$mapping = array('fieldName' => 'version', 'type' => 'integer');
$metadata->setVersionMapping($mapping);
$metadata->mapField($mapping);
+ $metadata->mapEmbedded(array(
+ 'fieldName' => 'parents',
+ 'class' => 'Parents',
+ ));
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
$metadata->mapOneToOne(array(
'fieldName' => 'address',
@@ -1093,6 +1116,7 @@ public static function loadMetadata(ClassMetadataInfo $metadata)
class DDC807SubClasse1 {}
class DDC807SubClasse2 {}
+class Parents {}
class Address {}
class Phonenumber {}
class Group {}
View
4 tests/Doctrine/Tests/ORM/Mapping/php/Doctrine.Tests.ORM.Mapping.User.php
@@ -38,6 +38,10 @@
$mapping = array('fieldName' => 'version', 'type' => 'integer');
$metadata->setVersionMapping($mapping);
$metadata->mapField($mapping);
+$metadata->mapEmbedded(array(
+ 'fieldName' => 'parents',
+ 'class' => 'Parents',
+));
$metadata->setIdGeneratorType(ClassMetadataInfo::GENERATOR_TYPE_AUTO);
$metadata->mapOneToOne(array(
'fieldName' => 'address',
View
1  tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.ORM.Mapping.User.dcm.xml
@@ -48,6 +48,7 @@
<field name="email" column="user_email" type="string" column-definition="CHAR(32) NOT NULL" />
<field name="version" type="integer" version="true" />
+ <embedded name="parents" class="Parents" />
<one-to-one field="address" target-entity="Address" inversed-by="user">
<cascade><cascade-remove /></cascade>
View
5 tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.ORM.Mapping.User.dcm.yml
@@ -33,6 +33,9 @@ Doctrine\Tests\ORM\Mapping\User:
version:
type: integer
version: true
+ embedded:
+ parents:
+ class: Parents
oneToOne:
address:
targetEntity: Address
@@ -67,7 +70,7 @@ Doctrine\Tests\ORM\Mapping\User:
cascade:
- all
lifecycleCallbacks:
- prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersistToo ]
+ prePersist: [ doStuffOnPrePersist, doOtherStuffOnPrePersistToo ]
postPersist: [ doStuffOnPostPersist ]
uniqueConstraints:
search_idx:
Something went wrong with that request. Please try again.