Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Value objects (Based on #634) #835

Merged
merged 33 commits into from 2 months ago
Johannes

This is PR #634 with the following additional changes:

  • Merged into master (fixed some conflict in Metadata classes)
  • Support for DQL queries on fields of embedded objects
Miha Vrhovnik

Why don't we raise the requirements and also drive up the php 5.5 adaption?

@mvrhov bumping the requirement to 5.5 would be a BC break (what about people which cannot use 5.5 yet ?).
And doing this bump only to make the testsuite a bit more convenient would be weird IMO

If this is required just for the test, then I don't care. However if I need to provide this additionally in each of my classes, then it's a shame.
I've also listed my reasoning behind why it should be a good idea.

This is just a convenience for tests to avoid writing the class name as a string when doing a $em->find call, making it easier to move fixture classes. See line 34.

doctrinebot
Collaborator

Hello,

thank you for creating this pull request. I have automatically opened an issue
on our Jira Bug Tracker for you. See the issue link:

http://www.doctrine-project.org/jira/browse/DDC-2773

We use Jira to track the state of pull requests and the versions they got
included in.

lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
((15 lines not shown))
  3082
+        $this->embeddedClasses[$mapping['fieldName']] = $this->fullyQualifiedClassName($mapping['class']);
  3083
+    }
  3084
+
  3085
+    /**
  3086
+     * Inline the embeddable class
  3087
+     *
  3088
+     * @param string $property
  3089
+     * @param ClassMetadataInfo $embeddable
  3090
+     */
  3091
+    public function inlineEmbeddable($property, ClassMetadataInfo $embeddable)
  3092
+    {
  3093
+        foreach ($embeddable->fieldMappings as $fieldMapping) {
  3094
+            $fieldMapping['declaredField'] = $property;
  3095
+            $fieldMapping['originalField'] = $fieldMapping['fieldName'];
  3096
+            $fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName'];
  3097
+            $fieldMapping['columnName'] = $property . "_" . $fieldMapping['columnName'];
4
Benjamin Eberlei Owner

This probably needs to be delegated to the strategy handling automatic column generation instead of doing it inline here.

You mean as an additional method?

Benjamin Eberlei Owner

No Doctrine\ORM\Mapping\NamingStrategy handles automatic naming of columns based on class and field names. However changing the interface is obviously a BC break. I think its small enough and easy enough to handle to make it, we need a mention in UPGRADE file though.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Benjamin Eberlei beberlei commented on the diff November 01, 2013
lib/Doctrine/ORM/Query/Parser.php
@@ -1065,6 +1065,12 @@ public function PathExpression($expectedTypes)
1065 1065
             $this->match(Lexer::T_IDENTIFIER);
1066 1066
 
1067 1067
             $field = $this->lexer->token['value'];
  1068
+
  1069
+            while ($this->lexer->isNextToken(Lexer::T_DOT)) {
8
Benjamin Eberlei Owner

Does this loop need validation of sorts? Or are there sane failures for it?

If the field does not exist, there is a QueryException at some later point. I don't think that we need to add more validation here, or did you have anything specific in mind?

Benjamin Eberlei Owner

query exception is perfect, i was afraid it might notice out or something ugly.

Guilherme Blanco Owner

This seems incomplete. We're relying on a breakage somewhere as a Query Exception else instead of properly throwing a Semantical or Parser exception properly.
I'd say that here we should properly add fields to PathExpression and then evaluating them on processDeferredPathExpressions.

Benjamin Eberlei Owner

Since this fields are saved as "foo.bar" in fieldMappings this is passed to deferred path expressions etc.

The beauty of this implementation really is that almost all parts except the metadata drivers can be left completely unaware of embeddables. We now just allow field names to contain dots.

Guilherme Blanco Owner

@beberlei but it's not properly validated. Is it "foo" or "bar" that is wrong? I describe some of my concerns below.

@schmittjoh I see a problem with this. There's no way to differ a field from an embedded (and this is a huge problem IMHO).
Also, "user.location.geo.latitude" doesn't seem to be supported.
Finally, Embeddeable ClassMetadata is created purely for 3rd-party consumers, but never consumed internally on DQL, Hydrator, Persister, etc.
It seems to me we're relying on an unintentional support to build a big feature which may bring a lot of headaches in the future.

I can address some of these concerns at least:

  1. You can distinguish an embedded field vs a normal field by the fact that it contains dots. We can make the exception message more precise for these cases if deemed necessary.
  2. Embedded fields can work over multiple levels

I cannot address the other things. I really don't know whether this feature will cause headaches in the future. At this point, I don't see why, but I'm not a fortune teller :) however, the internal implementation could be changed for a Doctrine 3 release without breaking BC with the end-user facing part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
((129 lines not shown))
  129
+
  130
+        $this->_em->refresh($person);
  131
+        $this->assertEquals('Boo', $person->address->street);
  132
+
  133
+        // DELETE
  134
+        $this->_em->createQuery("DELETE " . __NAMESPACE__ . "\\DDC93Person p WHERE p.address.city = :city")
  135
+            ->setParameter('city', 'Karlsruhe')
  136
+            ->execute();
  137
+
  138
+        $this->_em->clear();
  139
+        $this->assertNull($this->_em->find(__NAMESPACE__.'\\DDC93Person', $person->id));
  140
+    }
  141
+
  142
+    /**
  143
+     * @expectedException Doctrine\ORM\Query\QueryException
  144
+     * @expectedExceptionMessage no field or association named address.asdfasdf
2
Benjamin Eberlei Owner

We use $this->setExpectedException in the code by convention, can you change it to using the method?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Mapping/NamingStrategy.php
@@ -50,6 +50,16 @@ function classToTableName($className);
50 50
     function propertyToColumnName($propertyName, $className = null);
51 51
 
52 52
     /**
  53
+     * Returns a column name for an embedded property.
  54
+     *
  55
+     * @param string $propertyName
  56
+     * @param string $embeddedColumnName
  57
+     *
  58
+     * @return string
  59
+     */
  60
+    function embeddedFieldToColumnName($propertyName, $embeddedColumnName);
1
Fabio B. Silva Owner

Would be nice to have $entityClass and $embeddedClass

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php
@@ -53,6 +53,14 @@ public function propertyToColumnName($propertyName, $className = null)
53 53
     /**
54 54
      * {@inheritdoc}
55 55
      */
  56
+    public function embeddedFieldToColumnName($propertyName, $embeddedColumnName)
  57
+    {
  58
+        return $propertyName.ucfirst($embeddedColumnName);
1
Fabio B. Silva Owner

IMO should be $propertyName . '_' . $embeddedColumnName;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php
((45 lines not shown))
  45
+    {
  46
+        $embeddedObject = $this->parentProperty->getValue($object);
  47
+
  48
+        if ($embeddedObject === null) {
  49
+            return null;
  50
+        }
  51
+
  52
+        return $this->childProperty->getValue($embeddedObject);
  53
+    }
  54
+
  55
+    public function setValue($object, $value)
  56
+    {
  57
+        $embeddedObject = $this->parentProperty->getValue($object);
  58
+
  59
+        if ($embeddedObject === null) {
  60
+            $embeddedObject = new $this->class; // TODO
1

How about changing this to use the unserialize trick, or did you have something else in mind?

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

@guilhermeblanco Feedback please, this is mergable from my POV.

Jurian Sluiman

Is there any docs already how this would work in userland code? I am really eager to see what this creates for new opportunities :)

Christophe Coevoet

This is a very good point from @juriansluiman. The doc needs to be updated

Steve Müller deeky666 commented on the diff November 12, 2013
lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -338,6 +348,20 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
338 348
         }
339 349
     }
340 350
 
  351
+    private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass)
  352
+    {
  353
+        foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
  354
+            if ( ! isset($embeddedClass['inherited']) && ! $parentClass->isMappedSuperclass) {
  355
+                $embeddedClass['inherited'] = $parentClass->name;
  356
+            }
  357
+            if ( ! isset($embeddedClass['declared'])) {
3
Steve Müller Collaborator

Missing line break between IFs

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

Steve Müller Collaborator

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Marco Pivetta
Collaborator

@schmittjoh support for the other metadata drivers is still missing - should be added before merge and after this is actually approved as good (looking good though!)

@beberlei what about DQL support?

Benjamin Eberlei
Owner

@Ocramius DQL is supported, it was a three liner that johannes added.

Marco Pivetta
Collaborator

Yeah, saw afterwards that there were tests for it, nice!

Christophe Coevoet

@schmittjoh The first commit in the list (adding a new output format for the RunDqlCommand) looks weird. Is it an issue with a diverging branch after a rebase was done ?

lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -884,6 +898,18 @@ public function wakeupReflection($reflService)
884 898
         $this->reflClass = $reflService->getClass($this->name);
885 899
 
886 900
         foreach ($this->fieldMappings as $field => $mapping) {
  901
+            if (isset($mapping['declaredField'])) {
  902
+                $declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
  903
+                                    ? $this->embeddedClasses[$mapping['declaredField']]['declared'] : $this->name;
1
Christophe Coevoet
stof added a note November 12, 2013

wrong indentation (should be 4 spaces per level)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet stof commented on the diff November 12, 2013
lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
((21 lines not shown))
  3096
+    /**
  3097
+     * Inline the embeddable class
  3098
+     *
  3099
+     * @param string $property
  3100
+     * @param ClassMetadataInfo $embeddable
  3101
+     */
  3102
+    public function inlineEmbeddable($property, ClassMetadataInfo $embeddable)
  3103
+    {
  3104
+        foreach ($embeddable->fieldMappings as $fieldMapping) {
  3105
+            $fieldMapping['declaredField'] = $property;
  3106
+            $fieldMapping['originalField'] = $fieldMapping['fieldName'];
  3107
+            $fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName'];
  3108
+
  3109
+            $fieldMapping['columnName'] = ! empty($this->embeddedClasses[$property]['columnPrefix'])
  3110
+                    ? $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName']
  3111
+                        : $this->namingStrategy->embeddedFieldToColumnName($property, $fieldMapping['columnName'], $this->reflClass->name, $embeddable->reflClass->name);
2
Christophe Coevoet
stof added a note November 12, 2013

wrong indentation

boite
boite added a note February 08, 2014

$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);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Christophe Coevoet stof commented on the diff November 12, 2013
lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
((20 lines not shown))
  3095
+
  3096
+    /**
  3097
+     * Inline the embeddable class
  3098
+     *
  3099
+     * @param string $property
  3100
+     * @param ClassMetadataInfo $embeddable
  3101
+     */
  3102
+    public function inlineEmbeddable($property, ClassMetadataInfo $embeddable)
  3103
+    {
  3104
+        foreach ($embeddable->fieldMappings as $fieldMapping) {
  3105
+            $fieldMapping['declaredField'] = $property;
  3106
+            $fieldMapping['originalField'] = $fieldMapping['fieldName'];
  3107
+            $fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName'];
  3108
+
  3109
+            $fieldMapping['columnName'] = ! empty($this->embeddedClasses[$property]['columnPrefix'])
  3110
+                    ? $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName']
2
Christophe Coevoet
stof added a note November 12, 2013

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

columnPrefix is always defined by the user.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -247,6 +247,13 @@ class ClassMetadataInfo implements ClassMetadata
247 247
     public $isMappedSuperclass = false;
248 248
 
249 249
     /**
  250
+     * READ-ONLY: Wheather this class describes the mapping of an embeddable class.
1
Christophe Coevoet
stof added a note November 12, 2013

typo

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

I've now added some docs, the missing driver support and made some cs fixes.

Fabio B. Silva

@schmittjoh, instead of columnPrefix
I'd like to suggest the java/hibernate approach to allow multiple embeddables and override mappings : Embedded + AttributeOverride

As we already support @AttributeOverride to override parent atributes would be nice to keep the same approach here..

<?php
/** 
* @Embedded(class="Address") 
* @AttributeOverrides({
*   @AttributeOverride(name="street", column=@Column("street_billing")),
*   @AttributeOverride(name="zip", column=@Column("zip_billing"))
* })
*/
 public $billingAddress;

/** 
* @Embedded(class="Address") 
* @AttributeOverrides({
*   @AttributeOverride(name="street", column=@Column("street_delivery")),
*   @AttributeOverride(name="zip", column=@Column("zip_delivery"))
* })
*/
 public $deliveryAddress;
Michael Moravec

@FabioBatSilva: That was already suggested a month ago in #634, but noone cared.

Johannes

I've considered this before adding support for columnPrefix.

I've come to the conclusion though that both approaches @AttributeOverride and columnPrefix are complementary, not mutually exclusive. If you would like to change a specific column name, then you can use @AttributeOverride (this is already supported, see below); however if you'd like to change all columns (which I think is a common operation), then columnPrefix makes this very easy.

The syntax for @AttributeOverride is as follows:

/**
 * @AttributeOverrides({
 *      @AttributeOverride(name = "address.street", column = @Column("....")),
 * })
 */
class User
{
    /** @Embedded */
    private $address;
}
Dorian Villet

http://docs.doctrine-project.org/en/latest/reference/limitations-and-known-issues.html#value-objects This page could then be updated as well. (chapter 26.1.3. Value Objects)

Christophe Coevoet

@gnutix surely not this one as you are linking to the 2.0.x version of the doc :)
but the limitation should indeed be removed from the doc of the new version

lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
@@ -241,6 +243,17 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
241 243
             }
242 244
         }
243 245
 
  246
+        if (isset($xmlRoot->embedded)) {
  247
+            foreach ($xmlRoot->embedded as $embeddedMapping) {
  248
+                $mapping = array(
  249
+                    'fieldName' => (string) $embeddedMapping['name'],
  250
+                    'class' => (string) $embeddedMapping['class'],
  251
+                    'columnPrefix' => isset($embeddedMapping['class']) ? (string) $embeddedMapping['class'] : null,
1
Fabio B. Silva Owner

$embeddedMapping['columnPrefix']

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Fabio B. Silva FabioBatSilva commented on the diff November 13, 2013
lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -884,6 +898,18 @@ public function wakeupReflection($reflService)
884 898
         $this->reflClass = $reflService->getClass($this->name);
885 899
 
886 900
         foreach ($this->fieldMappings as $field => $mapping) {
  901
+            if (isset($mapping['declaredField'])) {
  902
+                $declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
1
Fabio B. Silva Owner
<?php
$declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
    ? $this->embeddedClasses[$mapping['declaredField']]['declared'] 
    : $this->name;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Dorian Villet

@stof Indeed ! I've been tricked by the "Project Versions" selector on the left which was showing me "latest"... Link updated in my previous comment.

Marco Pivetta
Collaborator

@schmittjoh maybe a silly question, but does this also work with VOs as identifiers (obviously implementing __toString in the VO)?

Not a requirement, but just curious.

Johannes
Marco Pivetta
Collaborator

Right, __toString is probably not needed thanks to the reflection hack

Johannes
Christophe Coevoet

I still see a drawback to this implementation: these "value objects" are tracked for changes property by property, while object (no other way here, and probably not so commonly used) and date/datetime fields are tracked by reference.
Setting a new datetime with the same value currently triggers an update in the DB (which is common when using Symfony forms as it returns a new DateTime instance after transforming the user input to avoid mutating the existing one), and mutating a datetime would not be saved. Both of these behaviors is inconsistent with the value object implementation (the case of mutating a DateTime is less an issue IMO as most code tries to avoid it to avoid weird side effects, hence the new immutable classes in PHP 5.5).

Marco Pivetta
Collaborator

@stof I don't think that can be fixed in this implementation - there's not really a reliable way of understanding whether the internal state of a wrapped object (DateTime or generic object) has changed except for passing it through DBAL types at every comparison... I wouldn't try to fit it in here...

Scratch that - we just don't have a reliable way of comparing objects

Johannes
Christophe Coevoet

@schmittjoh It is not affected by this PR, but there was no inconsistency before the PR: all objects were treated by reference.

The issue with equal datetime is already annoying and reported by several users, but at least we could answer them that we are comparing by reference all the time.

The current implementation of value objects is probably the best way to go. It works fine for mutable value objects and for immutable ones, covering all use cases.
Forcing users to treat their value objects as immutable would forbid them to use them with the Symfony Form component easily for instance (there is some way to support it, but it is more difficult). And comparing value objects by reference (possible only if they are immutable, or at least treated as immutable by the code changing them) would introduce the same issues than for DateTime instances.

What I would like to see (as a separate PR but ideally in the same release to avoid this inconsistency) is a better comparison for DateTime instances in the ORM (comparing them by value instead of reference, to avoid useless updates). Unfortunately, the previous attempts at doing it were rejected because they were impacting performances.

@Ocramius for the object type, I think keeping the comparison by reference is OK for now, given that this type is quite a hack itself. In my opinion, no sane DB would contain a serialized PHP object anyway.
The annoying case if for DateTime. And the reason why it is still compared by ref is indeed the performance of the change tracking
On a side note, the same extra updates can happen if you put a numeric string in a field mapped as integer, or the opposite (it can happen regularly for the decimal type as it maps to a PHP string). So it is definetely a separate issue.

sherifsabry20000

Will this implementation support mapping collections of embeddable objects?
In Hibernate we can do something like this:

@Entity
public class User {
   [...]
   public String getLastname() { ...}

   @ElementCollection
   @CollectionTable(name="Addresses", joinColumns=@JoinColumn(name="user_id"))
   @AttributeOverrides({
      @AttributeOverride(name="street1", column=@Column(name="fld_street"))
   })
   public Set<Address> getAddresses() { ... } 
}

@Embeddable
public class Address {
   public String getStreet1() {...}
   [...]
}

Basically a collection of value objects is mapped to a new table. Can we expect Doctrine to act similarly? I sure hope so ( ^ ^,)

Benjamin Eberlei
Owner

@sherifsabry20000 no that will not be possible with this patch. What you ask for is a @OneToMany as owning side of an association (without @ManyToOne). While I really would like to have this, it is not currently planned. You can approximate this with a ManyToMany table inbetween.

sherifsabry20000

@beberlei I'm sorry to hear that such behavior isn't supported, but is it feasible with the current state of Doctrine? or will we have to wait for Doctrine 3 perhaps?

About your other comment, Do you mean that I'll have to treat Address as an entity instead of a value object, right?

Benjamin Eberlei
Owner

@sherifsabry20000 I havent evaluated this.

Yes, address would be an entity.

Miha Vrhovnik

What is holding back this PR?

Jan Kramer

This implementation of VO's currently breaks in ClassMetadataInfo::wakeupReflection when embeddables are used over multiple levels (an embeddable in an embeddable).

jankramer@7fd585a Adds tests for this issue and fixes it, but this requires injecting the CMF in the classmetadata which is obviously a hack. Perhaps someone else has a better idea to fix embeddables over multiple levels?

Marco Pivetta
Collaborator

@jankramer can you simply make a branch with the failing test case + a PR, so we can fix that in a non-hacky way if possible?

Jan Kramer

@Ocramius https://github.com/jankramer/doctrine2/compare/ValueObjects. Can you update doctrine/doctrine2/ValueObjects or should I base the PR on schmittjoh/doctrine2/ValueObjects?

Marco Pivetta
Collaborator

@jankramer base the PR on it, I'd say...

Benjamin Eberlei
Owner

@schmittjoh can you rebase this one? Then we can merge it as well.

Benjamin Eberlei
Owner

I would throw an error for now on embeddeable ins embeddables to avoid the problem @jankramer describes.

Benjamin Eberlei beberlei referenced this pull request January 02, 2014
Closed

[WIP] Value objects #634

Vadim Golodko

@beberlei what news about this PR?

Miha Vrhovnik

hm is it just me or this really looks like a bad rebase? @schmittjoh

Johannes

What @beberlei was looking for was not actually a rebase, but merging the master branch.

Unfortunately, the second level cache PR which was merged earlier, now causes some tests to fail. Maybe @FabioBatSilva could help with this?

Marco Pivetta
Collaborator

Uhmm, not really - rebase is rebase - was it too messy to apply here? :|

Johannes
Miha Vrhovnik

@FabioBatSilva Could you help with failed tests. I'd really like to see this one merged into the 2.5

Marcos Passos

This is one of most wanted feature nowadays, IMHO.

Fabio B. Silva

The postgres failure is related to the default sort strategy, not related to SLC ... The build passes after restart..

Fabio B. Silva FabioBatSilva commented on the diff February 05, 2014
tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
((93 lines not shown))
  93
+            $this->assertEquals('12345', $person->address->zip);
  94
+            $this->assertEquals('funkytown', $person->address->city);
  95
+        }
  96
+
  97
+        $dql = "SELECT p FROM " . __NAMESPACE__ . "\DDC93Person p";
  98
+        $persons = $this->_em->createQuery($dql)->getArrayResult();
  99
+
  100
+        foreach ($persons as $person) {
  101
+            $this->assertEquals('Tree', $person['address.street']);
  102
+            $this->assertEquals('12345', $person['address.zip']);
  103
+            $this->assertEquals('funkytown', $person['address.city']);
  104
+        }
  105
+    }
  106
+
  107
+    /**
  108
+     * @group dql
1
Fabio B. Silva Owner

add @non-cacheable instead of markTestSkipped

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Jonathan Cardoso Machado

Just a little question, only this PR is blocking the 2.5 beta?

Guilherme Blanco

@JCMais we also have custom persisters in the pipeline IIRC.

Benjamin Eberlei beberlei merged commit 8a0901c into from February 08, 2014
Benjamin Eberlei beberlei closed this February 08, 2014
Benjamin Eberlei
Owner

Done Done :) Thanks for picking up the torch @schmittjoh

alsar

:clap: Thank you for this.

Jan Kramer

Great news, thanks guys!

Miha Vrhovnik

Big big thanks for everyone working on this.

Vadim Golodko

Thank you!

Marco Pivetta
Collaborator

Awesome.

Gábor Egyed

Great. Thank you!

Alan Gabriel Bem
Thomas Ploch

+100000000000

Johannes

You're welcome :)

Johannes schmittjoh deleted the branch February 08, 2014
alsar

I played around with the new functionality and encountered a problem with embeddables inside embeddables.
Is this behaviour not supported yet?

Raul Rodriguez

Thanks a lot guys.

sherifsabry20000

Finally the feature was added :) Thank you all who contributed.

Maybe we can now look forward to adding support for mapping collections of embeddable objects
http://www.doctrine-project.org/jira/browse/DDC-2826

Baptiste Clavié Taluu commented on the diff February 08, 2014
lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php
((22 lines not shown))
  22
+/**
  23
+ * Acts as a proxy to a nested Property structure, making it look like
  24
+ * just a single scalar property.
  25
+ *
  26
+ * This way value objects "just work" without UnitOfWork, Persisters or Hydrators
  27
+ * needing any changes.
  28
+ *
  29
+ * TODO: Move this class into Common\Reflection
  30
+ */
  31
+class ReflectionEmbeddedProperty
  32
+{
  33
+    private $parentProperty;
  34
+    private $childProperty;
  35
+    private $class;
  36
+
  37
+    public function __construct($parentProperty, $childProperty, $class)
2
Baptiste Clavié
Taluu added a note February 08, 2014

Aren't these parameters missing some typehint ? As it is used as (possibly ?) ReflectionProperty afterwards...

Marco Pivetta Collaborator

@Taluu see standing TODO in the class header`.

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

@alsar Ah yes, i have to disable doing that for now. Its not yet supported.

Benjamin Eberlei
Owner

@sherifsabry20000 a collection of embeddable objects is just a @OneToMany/@ManyToOne with {"cascade":"all"}

Marcos Passos

Thanks a lot guys.

Mark Badolato

Is it possible to use, for example, Address as both an entity and an embeddable? A contrived example, that has a basis in our application.

Let's say I have an entity Client that can have multiple Addresses (physical, mailing, etc). In that case Address needs to be a collection, and a OneToMany needs to be established.

In addition, each Client has a OneToMany for Facility. A Facility has one embedded Address.

Would we have to define two things with the basically the same definition (i.e., an Address entity and Address as embeddable), or can one definition serve both purposes?

Does that make sense? Or, am I thinking about the problem in entirely the wrong way?

Marco Pivetta
Collaborator

@mbadolato an entity has an ID, an embeddable doesn't, so I don't think it makes sense here. I'd also suggest moving these discussions to the mailing list

Mark Badolato

@Ocramius Re: having an id vs not having an id: Yep, hence my question. Address cannot be used in both contexts?

Miha Vrhovnik

@mbadolato I thought that was possible. The use case would be an invoice and a customer associated with it. When you create an invoice almost full customer information should be persisted together with it.

Johannes
Mark Badolato

That's what I was confirming. Thanks @schmittjoh.

@mvrhov Yes that is a good example of a use case for it.

Florent Paterno

@beberlei How can you use manyToOne relation between entities and embeddable element ?
I have used manyToOne but nothing happened when i've created the database.

Marco Pivetta
Collaborator

@fpaterno please open a new issue at http://www.doctrine-project.org/jira/browse/DDC or ask on the mailing list.

Matěj Koubík

@mvrhov then you should have @Embeddable CustomerData embedded both in Customer and in Invoice.

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

Showing 33 unique commits by 3 authors.

Mar 03, 2013
Johannes adds a new output format b4b9709
Mar 26, 2013
Benjamin Eberlei [DDC-93] Started ValueObjectsTest 02d34bb
Benjamin Eberlei [DDC-93] Parse @Embedded and @Embeddable during SchemaTool processing…
… to make parsing work.
32988b3
Mar 27, 2013
Benjamin Eberlei [DDC-93] Implement first working version of value objects using a Ref…
…lectionProxy object, bypassing changes to UnitOfWork, Persisters and Hydrators.
0204a8b
Benjamin Eberlei [DDC-93] Add some TODOs in code. 011776f
Benjamin Eberlei [DDC-93] Show CRUD with value objects with current change tracking as…
…sumptions.
879ab6e
Benjamin Eberlei [DDC-93] Rename ReflectionProxy to ReflectionEmbeddedProperty, Add DQ…
…L test with Object and Array Hydration.
9613f1d
Nov 01, 2013
Johannes Merge remote-tracking branch 'origin/ValueObjects'
Conflicts:
	lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
38b041d
Johannes adds support for selecting based on embedded fields c67ac8a
Johannes adds tests for update/delete DQL queries 30897c3
Johannes adds test for non-existent field 41c937b
Johannes removes outdated todos fd8b5bd
Johannes make use of NamingStrategy for columns of embedded fields 20fb827
Johannes fixes coding style 4f6c150
Johannes fixes annotation context f86abd8
Johannes some consistency fixes 97836ef
Nov 02, 2013
Johannes Merge remote-tracking branch 'schmittjoh/ValueObjects' d4e6618
Johannes adds support & tests for embeddables in inheritance schemes ece62d6
Johannes removes restrictions on constructors of embedded objects 5586ddd
Johannes fixes a bad merge 0cd6061
Johannes fixes declaring class 2b2f489
Johannes makes column prefix configurable 17e0a7b
Nov 12, 2013
Johannes adds docs 9ad376c
Nov 13, 2013
Johannes adds support for XML/Yaml drivers fb3a06b
Johannes some cs fixes 2a73a6f
Johannes small fix 0ee7b68
Nov 28, 2013
Johannes adds embedded classes to cache e5cab1d
Dec 07, 2013
Jan Kramer Update XML schema to reflect addition of embeddables 928c32d
Jan Kramer Fix XmlDriver to accept embeddables fbb7b5a
Johannes Merge pull request #1 from jankramer/ValueObjects
Update xml mapping driver and schema to work with embeddables
f7f7c46
Jan 04, 2014
Johannes Merge branch 'master' of github.com:doctrine/doctrine2 into ValueObjects
Conflicts:
	UPGRADE.md
	lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php
4f585a3
Johannes fixes bad merge 9464194
Johannes skips DQL UPDATE/DELETE tests with SLC enabled 7020f41
This page is out of date. Refresh to see the latest.

Showing 25 changed files with 759 additions and 10 deletions. Show diff stats Hide diff stats

  1. 5  UPGRADE.md
  2. 1  docs/en/index.rst
  3. 1  docs/en/toc.rst
  4. 83  docs/en/tutorials/embeddables.rst
  5. 18  doctrine-mapping.xsd
  6. 24  lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
  7. 98  lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
  8. 8  lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php
  9. 10  lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
  10. 2  lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php
  11. 18  lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
  12. 12  lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
  13. 28  lib/Doctrine/ORM/Mapping/Embeddable.php
  14. 38  lib/Doctrine/ORM/Mapping/Embedded.php
  15. 10  lib/Doctrine/ORM/Mapping/NamingStrategy.php
  16. 66  lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php
  17. 8  lib/Doctrine/ORM/Mapping/UnderscoreNamingStrategy.php
  18. 8  lib/Doctrine/ORM/Query/Parser.php
  19. 1  lib/Doctrine/ORM/Tools/SchemaTool.php
  20. 9  tests/Doctrine/Tests/Models/ValueObjects/Name.php
  21. 9  tests/Doctrine/Tests/Models/ValueObjects/Person.php
  22. 265  tests/Doctrine/Tests/ORM/Functional/ValueObjectsTest.php
  23. 25  tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php
  24. 10  tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.ValueObjects.Name.dcm.xml
  25. 12  tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.ValueObjects.Person.dcm.xml
5  UPGRADE.md
Source Rendered
... ...
@@ -1,5 +1,10 @@
1 1
 # Upgrade to 2.5
2 2
 
  3
+## BC BREAK: NamingStrategy has a new method ``embeddedFieldToColumnName($propertyName, $embeddedColumnName)``
  4
+
  5
+This method generates the column name for fields of embedded objects. If you implement your custom NamingStrategy, you
  6
+now also need to implement this new method.
  7
+
3 8
 ## Updates on entities scheduled for deletion are no longer processed
4 9
 
5 10
 In Doctrine 2.4, if you modified properties of an entity scheduled for deletion, UnitOfWork would
1  docs/en/index.rst
Source Rendered
@@ -90,6 +90,7 @@ Tutorials
90 90
 * :doc:`Ordered associations <tutorials/ordered-associations>`
91 91
 * :doc:`Pagination <tutorials/pagination>`
92 92
 * :doc:`Override Field/Association Mappings In Subclasses <tutorials/override-field-association-mappings-in-subclasses>`
  93
+* :doc:`Embeddables <tutorials/embeddables>`
93 94
 
94 95
 Cookbook
95 96
 --------
1  docs/en/toc.rst
Source Rendered
@@ -16,6 +16,7 @@ Tutorials
16 16
    tutorials/ordered-associations
17 17
    tutorials/override-field-association-mappings-in-subclasses
18 18
    tutorials/pagination.rst
  19
+   tutorials/embeddables.rst
19 20
 
20 21
 Reference Guide
21 22
 ---------------
83  docs/en/tutorials/embeddables.rst
Source Rendered
... ...
@@ -0,0 +1,83 @@
  1
+Separating Concerns using Embeddables
  2
+-------------------------------------
  3
+
  4
+Embeddables are classes which are not entities themself, but are embedded
  5
+in entities and can also be queried in DQL. You'll mostly want to use them
  6
+to reduce duplication or separating concerns.
  7
+
  8
+For the purposes of this tutorial, we will assume that you have a ``User``
  9
+class in your application and you would like to store an address in
  10
+the ``User`` class. We will model the ``Address`` class as an embeddable
  11
+instead of simply adding the respective columns to the ``User`` class.
  12
+
  13
+.. configuration-block::
  14
+
  15
+    .. code-block:: php
  16
+
  17
+        <?php
  18
+
  19
+        /** @Entity */
  20
+        class User
  21
+        {
  22
+            /** @Embedded(class = "Address") */
  23
+            private $address;
  24
+        }
  25
+
  26
+        /** @Embeddable */
  27
+        class Address
  28
+        {
  29
+            /** @Column(type = "string") */
  30
+            private $street;
  31
+
  32
+            /** @Column(type = "string") */
  33
+            private $postalCode;
  34
+
  35
+            /** @Column(type = "string") */
  36
+            private $city;
  37
+
  38
+            /** @Column(type = "string") */
  39
+            private $country;
  40
+        }
  41
+
  42
+    .. code-block:: xml
  43
+
  44
+        <doctrine-mapping>
  45
+            <entity name="User">
  46
+                <embedded name="address" class="Address" />
  47
+            </entity>
  48
+
  49
+            <embeddable name="Address">
  50
+                <field name="street" type="string" />
  51
+                <field name="postalCode" type="string" />
  52
+                <field name="city" type="string" />
  53
+                <field name="country" type="string" />
  54
+            </embeddable>
  55
+        </doctrine-mapping>
  56
+
  57
+    .. code-block:: yaml
  58
+
  59
+        User:
  60
+          type: entity
  61
+          embedded:
  62
+            address:
  63
+              class: Address
  64
+
  65
+        Address:
  66
+          type: embeddable
  67
+          fields:
  68
+            street: { type: string }
  69
+            postalCode: { type: string }
  70
+            city: { type: string }
  71
+            country: { type: string }
  72
+
  73
+In terms of your database schema, Doctrine will automatically inline all
  74
+columns from the ``Address`` class into the table of the ``User`` class,
  75
+just as if you had declared them directly there.
  76
+
  77
+You can also use mapped fields of embedded classes in DQL queries, just
  78
+as if they were declared in the ``User`` class:
  79
+
  80
+.. code-block:: sql
  81
+
  82
+    SELECT u FROM User u WHERE u.address.city = :myCity
  83
+
18  doctrine-mapping.xsd
@@ -17,6 +17,7 @@
17 17
       <xs:sequence>
18 18
         <xs:element name="mapped-superclass" type="orm:mapped-superclass" minOccurs="0" maxOccurs="unbounded" />
19 19
         <xs:element name="entity" type="orm:entity" minOccurs="0" maxOccurs="unbounded" />
  20
+        <xs:element name="embeddable" type="orm:embeddable" minOccurs="0" maxOccurs="unbounded" />
20 21
         <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
21 22
       </xs:sequence>
22 23
       <xs:anyAttribute namespace="##other"/>
@@ -180,6 +181,7 @@
180 181
       <xs:element name="sql-result-set-mappings" type="orm:sql-result-set-mappings" minOccurs="0" maxOccurs="unbounded" />
181 182
       <xs:element name="id" type="orm:id" minOccurs="0" maxOccurs="unbounded" />
182 183
       <xs:element name="field" type="orm:field" minOccurs="0" maxOccurs="unbounded"/>
  184
+      <xs:element name="embedded" type="orm:embedded" minOccurs="0" maxOccurs="unbounded"/>
183 185
       <xs:element name="one-to-one" type="orm:one-to-one" minOccurs="0" maxOccurs="unbounded"/>
184 186
       <xs:element name="one-to-many" type="orm:one-to-many" minOccurs="0" maxOccurs="unbounded" />
185 187
       <xs:element name="many-to-one" type="orm:many-to-one" minOccurs="0" maxOccurs="unbounded" />
@@ -226,6 +228,16 @@
226 228
     </xs:complexContent>
227 229
   </xs:complexType>
228 230
 
  231
+  <xs:complexType name="embeddable">
  232
+    <xs:complexContent>
  233
+      <xs:extension base="orm:entity">
  234
+        <xs:sequence>
  235
+          <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
  236
+        </xs:sequence>
  237
+      </xs:extension>
  238
+    </xs:complexContent>
  239
+  </xs:complexType>
  240
+
229 241
   <xs:simpleType name="change-tracking-policy">
230 242
     <xs:restriction base="xs:token">
231 243
       <xs:enumeration value="DEFERRED_IMPLICIT"/>
@@ -288,6 +300,12 @@
288 300
     <xs:anyAttribute namespace="##other"/>
289 301
   </xs:complexType>
290 302
   
  303
+  <xs:complexType name="embedded">
  304
+    <xs:attribute name="name" type="xs:string" use="required" />
  305
+    <xs:attribute name="class" type="xs:string" use="required" />
  306
+    <xs:attribute name="column-prefix" type="xs:string" use="optional" />
  307
+  </xs:complexType>
  308
+  
291 309
   <xs:complexType name="discriminator-column">
292 310
     <xs:sequence>
293 311
       <xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
24  lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php
@@ -96,6 +96,7 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
96 96
             $class->setIdGeneratorType($parent->generatorType);
97 97
             $this->addInheritedFields($class, $parent);
98 98
             $this->addInheritedRelations($class, $parent);
  99
+            $this->addInheritedEmbeddedClasses($class, $parent);
99 100
             $class->setIdentifier($parent->identifier);
100 101
             $class->setVersioned($parent->isVersioned);
101 102
             $class->setVersionField($parent->versionField);
@@ -140,6 +141,15 @@ protected function doLoadMetadata($class, $parent, $rootEntityFound, array $nonS
140 141
             $this->completeIdGeneratorMapping($class);
141 142
         }
142 143
 
  144
+        foreach ($class->embeddedClasses as $property => $embeddableClass) {
  145
+            if (isset($embeddableClass['inherited'])) {
  146
+                continue;
  147
+            }
  148
+
  149
+            $embeddableMetadata = $this->getMetadataFor($embeddableClass['class']);
  150
+            $class->inlineEmbeddable($property, $embeddableMetadata);
  151
+        }
  152
+
143 153
         if ($parent && $parent->isInheritanceTypeSingleTable()) {
144 154
             $class->setPrimaryTable($parent->table);
145 155
         }
@@ -342,6 +352,20 @@ private function addInheritedRelations(ClassMetadata $subClass, ClassMetadata $p
342 352
         }
343 353
     }
344 354
 
  355
+    private function addInheritedEmbeddedClasses(ClassMetadata $subClass, ClassMetadata $parentClass)
  356
+    {
  357
+        foreach ($parentClass->embeddedClasses as $field => $embeddedClass) {
  358
+            if ( ! isset($embeddedClass['inherited']) && ! $parentClass->isMappedSuperclass) {
  359
+                $embeddedClass['inherited'] = $parentClass->name;
  360
+            }
  361
+            if ( ! isset($embeddedClass['declared'])) {
  362
+                $embeddedClass['declared'] = $parentClass->name;
  363
+            }
  364
+
  365
+            $subClass->embeddedClasses[$field] = $embeddedClass;
  366
+        }
  367
+    }
  368
+
345 369
     /**
346 370
      * Adds inherited named queries to the subclass mapping.
347 371
      *
98  lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php
@@ -261,6 +261,13 @@ class ClassMetadataInfo implements ClassMetadata
261 261
     public $isMappedSuperclass = false;
262 262
 
263 263
     /**
  264
+     * READ-ONLY: Whether this class describes the mapping of an embeddable class.
  265
+     *
  266
+     * @var boolean
  267
+     */
  268
+    public $isEmbeddedClass = false;
  269
+
  270
+    /**
264 271
      * READ-ONLY: The names of the parent classes (ancestors).
265 272
      *
266 273
      * @var array
@@ -275,6 +282,13 @@ class ClassMetadataInfo implements ClassMetadata
275 282
     public $subClasses = array();
276 283
 
277 284
     /**
  285
+     * READ-ONLY: The names of all embedded classes based on properties.
  286
+     *
  287
+     * @var array
  288
+     */
  289
+    public $embeddedClasses = array();
  290
+
  291
+    /**
278 292
      * READ-ONLY: The named queries allowed to be called directly from Repository.
279 293
      *
280 294
      * @var array
@@ -799,6 +813,7 @@ public function __sleep()
799 813
             'columnNames', //TODO: Not really needed. Can use fieldMappings[$fieldName]['columnName']
800 814
             'fieldMappings',
801 815
             'fieldNames',
  816
+            'embeddedClasses',
802 817
             'identifier',
803 818
             'isIdentifierComposite', // TODO: REMOVE
804 819
             'name',
@@ -907,6 +922,18 @@ public function wakeupReflection($reflService)
907 922
         $this->reflClass = $reflService->getClass($this->name);
908 923
 
909 924
         foreach ($this->fieldMappings as $field => $mapping) {
  925
+            if (isset($mapping['declaredField'])) {
  926
+                $declaringClass = isset($this->embeddedClasses[$mapping['declaredField']]['declared'])
  927
+                                      ? $this->embeddedClasses[$mapping['declaredField']]['declared'] : $this->name;
  928
+
  929
+                $this->reflFields[$field] = new ReflectionEmbeddedProperty(
  930
+                    $reflService->getAccessibleProperty($declaringClass, $mapping['declaredField']),
  931
+                    $reflService->getAccessibleProperty($this->embeddedClasses[$mapping['declaredField']]['class'], $mapping['originalField']),
  932
+                    $this->embeddedClasses[$mapping['declaredField']]['class']
  933
+                );
  934
+                continue;
  935
+            }
  936
+
910 937
             $this->reflFields[$field] = isset($mapping['declared'])
911 938
                 ? $reflService->getAccessibleProperty($mapping['declared'], $field)
912 939
                 : $reflService->getAccessibleProperty($this->name, $field);
@@ -948,8 +975,12 @@ public function initializeReflection($reflService)
948 975
      */
949 976
     public function validateIdentifier()
950 977
     {
  978
+        if ($this->isMappedSuperclass || $this->isEmbeddedClass) {
  979
+            return;
  980
+        }
  981
+
951 982
         // Verify & complete identifier mapping
952  
-        if ( ! $this->identifier && ! $this->isMappedSuperclass) {
  983
+        if ( ! $this->identifier) {
953 984
             throw MappingException::identifierRequired($this->name);
954 985
         }
955 986
 
@@ -2150,6 +2181,11 @@ public function isInheritedAssociation($fieldName)
2150 2181
         return isset($this->associationMappings[$fieldName]['inherited']);
2151 2182
     }
2152 2183
 
  2184
+    public function isInheritedEmbeddedClass($fieldName)
  2185
+    {
  2186
+        return isset($this->embeddedClasses[$fieldName]['inherited']);
  2187
+    }
  2188
+
2153 2189
     /**
2154 2190
      * Sets the name of the primary table the class is mapped to.
2155 2191
      *
@@ -2229,9 +2265,8 @@ private function _isInheritanceType($type)
2229 2265
     public function mapField(array $mapping)
2230 2266
     {
2231 2267
         $this->_validateAndCompleteFieldMapping($mapping);
2232  
-        if (isset($this->fieldMappings[$mapping['fieldName']]) || isset($this->associationMappings[$mapping['fieldName']])) {
2233  
-            throw MappingException::duplicateFieldMapping($this->name, $mapping['fieldName']);
2234  
-        }
  2268
+        $this->assertFieldNotMapped($mapping['fieldName']);
  2269
+
2235 2270
         $this->fieldMappings[$mapping['fieldName']] = $mapping;
2236 2271
     }
2237 2272
 
@@ -2479,9 +2514,7 @@ protected function _storeAssociationMapping(array $assocMapping)
2479 2514
     {
2480 2515
         $sourceFieldName = $assocMapping['fieldName'];
2481 2516
 
2482  
-        if (isset($this->fieldMappings[$sourceFieldName]) || isset($this->associationMappings[$sourceFieldName])) {
2483  
-            throw MappingException::duplicateFieldMapping($this->name, $sourceFieldName);
2484  
-        }
  2517
+        $this->assertFieldNotMapped($sourceFieldName);
2485 2518
 
2486 2519
         $this->associationMappings[$sourceFieldName] = $assocMapping;
2487 2520
     }
@@ -3116,4 +3149,55 @@ public function getMetadataValue($name) {
3116 3149
 
3117 3150
         return null;
3118 3151
     }
  3152
+
  3153
+    /**
  3154
+     * Map Embedded Class
  3155
+     *
  3156
+     * @array $mapping
  3157
+     * @return void
  3158
+     */
  3159
+    public function mapEmbedded(array $mapping)
  3160
+    {
  3161
+        $this->assertFieldNotMapped($mapping['fieldName']);
  3162
+
  3163
+        $this->embeddedClasses[$mapping['fieldName']] = array(
  3164
+            'class' => $this->fullyQualifiedClassName($mapping['class']),
  3165
+            'columnPrefix' => $mapping['columnPrefix'],
  3166
+        );
  3167
+    }
  3168
+
  3169
+    /**
  3170
+     * Inline the embeddable class
  3171
+     *
  3172
+     * @param string $property
  3173
+     * @param ClassMetadataInfo $embeddable
  3174
+     */
  3175
+    public function inlineEmbeddable($property, ClassMetadataInfo $embeddable)
  3176
+    {
  3177
+        foreach ($embeddable->fieldMappings as $fieldMapping) {
  3178
+            $fieldMapping['declaredField'] = $property;
  3179
+            $fieldMapping['originalField'] = $fieldMapping['fieldName'];
  3180
+            $fieldMapping['fieldName'] = $property . "." . $fieldMapping['fieldName'];
  3181
+
  3182
+            $fieldMapping['columnName'] = ! empty($this->embeddedClasses[$property]['columnPrefix'])
  3183
+                    ? $this->embeddedClasses[$property]['columnPrefix'] . $fieldMapping['columnName']
  3184
+                        : $this->namingStrategy->embeddedFieldToColumnName($property, $fieldMapping['columnName'], $this->reflClass->name, $embeddable->reflClass->name);
  3185
+
  3186
+            $this->mapField($fieldMapping);
  3187
+        }
  3188
+    }
  3189
+
  3190
+    /**
  3191
+     * @param string $fieldName
  3192
+     * @throws MappingException
  3193
+     */
  3194
+    private function assertFieldNotMapped($fieldName)
  3195
+    {
  3196
+        if (isset($this->fieldMappings[$fieldName]) ||
  3197
+            isset($this->associationMappings[$fieldName]) ||
  3198
+            isset($this->embeddedClasses[$fieldName])) {
  3199
+
  3200
+            throw MappingException::duplicateFieldMapping($this->name, $fieldName);
  3201
+        }
  3202
+    }
3119 3203
 }
8  lib/Doctrine/ORM/Mapping/DefaultNamingStrategy.php
@@ -53,6 +53,14 @@ public function propertyToColumnName($propertyName, $className = null)
53 53
     /**
54 54
      * {@inheritdoc}
55 55
      */
  56
+    public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
  57
+    {
  58
+        return $propertyName.'_'.$embeddedColumnName;
  59
+    }
  60
+
  61
+    /**
  62
+     * {@inheritdoc}
  63
+     */
56 64
     public function referenceColumnName()
57 65
     {
58 66
         return 'id';
10  lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php
@@ -85,6 +85,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
85 85
             $mappedSuperclassAnnot = $classAnnotations['Doctrine\ORM\Mapping\MappedSuperclass'];
86 86
             $metadata->setCustomRepositoryClass($mappedSuperclassAnnot->repositoryClass);
87 87
             $metadata->isMappedSuperclass = true;
  88
+        } else if (isset($classAnnotations['Doctrine\ORM\Mapping\Embeddable'])) {
  89
+            $metadata->isEmbeddedClass = true;
88 90
         } else {
89 91
             throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
90 92
         }
@@ -251,7 +253,9 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
251 253
                 ||
252 254
                 $metadata->isInheritedField($property->name)
253 255
                 ||
254  
-                $metadata->isInheritedAssociation($property->name)) {
  256
+                $metadata->isInheritedAssociation($property->name)
  257
+                ||
  258
+                $metadata->isInheritedEmbeddedClass($property->name)) {
255 259
                 continue;
256 260
             }
257 261
 
@@ -375,6 +379,10 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
375 379
                 }
376 380
 
377 381
                 $metadata->mapManyToMany($mapping);
  382
+            } else if ($embeddedAnnot = $this->reader->getPropertyAnnotation($property, 'Doctrine\ORM\Mapping\Embedded')) {
  383
+                $mapping['class'] = $embeddedAnnot->class;
  384
+                $mapping['columnPrefix'] = $embeddedAnnot->columnPrefix;
  385
+                $metadata->mapEmbedded($mapping);
378 386
             }
379 387
 
380 388
             // Evaluate @Cache annotation
2  lib/Doctrine/ORM/Mapping/Driver/DoctrineAnnotations.php
@@ -19,6 +19,8 @@
19 19
 
20 20
 require_once __DIR__.'/../Annotation.php';
21 21
 require_once __DIR__.'/../Entity.php';
  22
+require_once __DIR__.'/../Embeddable.php';
  23
+require_once __DIR__.'/../Embedded.php';
22 24
 require_once __DIR__.'/../MappedSuperclass.php';
23 25
 require_once __DIR__.'/../InheritanceType.php';
24 26
 require_once __DIR__.'/../DiscriminatorColumn.php';
18  lib/Doctrine/ORM/Mapping/Driver/XmlDriver.php
@@ -69,6 +69,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
69 69
                 isset($xmlRoot['repository-class']) ? (string)$xmlRoot['repository-class'] : null
70 70
             );
71 71
             $metadata->isMappedSuperclass = true;
  72
+        } else if ($xmlRoot->getName() == 'embeddable') {
  73
+            $metadata->isEmbeddedClass = true;
72 74
         } else {
73 75
             throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
74 76
         }
@@ -246,6 +248,17 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
246 248
             }
247 249
         }
248 250
 
  251
+        if (isset($xmlRoot->embedded)) {
  252
+            foreach ($xmlRoot->embedded as $embeddedMapping) {
  253
+                $mapping = array(
  254
+                    'fieldName' => (string) $embeddedMapping['name'],
  255
+                    'class' => (string) $embeddedMapping['class'],
  256
+                    'columnPrefix' => isset($embeddedMapping['column-prefix']) ? (string) $embeddedMapping['column-prefix'] : null,
  257
+                );
  258
+                $metadata->mapEmbedded($mapping);
  259
+            }
  260
+        }
  261
+
249 262
         foreach ($mappings as $mapping) {
250 263
             if (isset($mapping['version'])) {
251 264
                 $metadata->setVersionMapping($mapping);
@@ -796,6 +809,11 @@ protected function loadMappingFile($file)
796 809
                 $className = (string)$mappedSuperClass['name'];
797 810
                 $result[$className] = $mappedSuperClass;
798 811
             }
  812
+        } else if (isset($xmlElement->embeddable)) {
  813
+            foreach ($xmlElement->embeddable as $embeddableElement) {
  814
+                $embeddableName = (string) $embeddableElement['name'];
  815
+                $result[$embeddableName] = $embeddableElement;
  816
+            }
799 817
         }
800 818
 
801 819
         return $result;
12  lib/Doctrine/ORM/Mapping/Driver/YamlDriver.php
@@ -66,6 +66,8 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
66 66
                 isset($element['repositoryClass']) ? $element['repositoryClass'] : null
67 67
             );
68 68
             $metadata->isMappedSuperclass = true;
  69
+        } else if ($element['type'] == 'embeddable') {
  70
+            $metadata->isEmbeddedClass = true;
69 71
         } else {
70 72
             throw MappingException::classIsNotAValidEntityOrMappedSuperClass($className);
71 73
         }
@@ -318,6 +320,16 @@ public function loadMetadataForClass($className, ClassMetadata $metadata)
318 320
             }
319 321
         }
320 322
 
  323
+        if (isset($element['embedded'])) {
  324
+            foreach ($element['embedded'] as $name => $embeddedMapping) {
  325
+                $mapping = array(
  326
+                    'fieldName' => $name,
  327
+                    'class' => $embeddedMapping['class'],
  328
+                    'columnPrefix' => isset($embeddedMapping['columnPrefix']) ? $embeddedMapping['columnPrefix'] : null,
  329
+                );
  330
+                $metadata->mapEmbedded($mapping);
  331
+            }
  332
+        }
321 333
 
322 334
         // Evaluate oneToOne relationships
323 335
         if (isset($element['oneToOne'])) {
28  lib/Doctrine/ORM/Mapping/Embeddable.php
... ...
@@ -0,0 +1,28 @@
  1
+<?php
  2
+/*
  3
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14
+ *
  15
+ * This software consists of voluntary contributions made by many individuals
  16
+ * and is licensed under the MIT license. For more information, see
  17
+ * <http://www.doctrine-project.org>.
  18
+ */
  19
+
  20
+namespace Doctrine\ORM\Mapping;
  21
+
  22
+/**
  23
+ * @Annotation
  24
+ * @Target("CLASS")
  25
+ */
  26
+final class Embeddable implements Annotation
  27
+{
  28
+}
38  lib/Doctrine/ORM/Mapping/Embedded.php
... ...
@@ -0,0 +1,38 @@
  1
+<?php
  2
+/*
  3
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14
+ *
  15
+ * This software consists of voluntary contributions made by many individuals
  16
+ * and is licensed under the MIT license. For more information, see
  17
+ * <http://www.doctrine-project.org>.
  18
+ */
  19
+
  20
+namespace Doctrine\ORM\Mapping;
  21
+
  22
+/**
  23
+ * @Annotation
  24
+ * @Target("PROPERTY")
  25
+ */
  26
+final class Embedded implements Annotation
  27
+{
  28
+    /**
  29
+     * @Required
  30
+     * @var string
  31
+     */
  32
+    public $class;
  33
+
  34
+    /**
  35
+     * @var string
  36
+     */
  37
+    public $columnPrefix;
  38
+}
10  lib/Doctrine/ORM/Mapping/NamingStrategy.php
@@ -50,6 +50,16 @@ function classToTableName($className);
50 50
     function propertyToColumnName($propertyName, $className = null);
51 51
 
52 52
     /**
  53
+     * Returns a column name for an embedded property.
  54
+     *
  55
+     * @param string $propertyName
  56
+     * @param string $embeddedColumnName
  57
+     *
  58
+     * @return string
  59
+     */
  60
+    function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null);
  61
+
  62
+    /**
53 63
      * Returns the default reference column name.
54 64
      *
55 65
      * @return string A column name.
66  lib/Doctrine/ORM/Mapping/ReflectionEmbeddedProperty.php
... ...
@@ -0,0 +1,66 @@
  1
+<?php
  2
+/*
  3
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,