Update: Pretty certain this a bug, made an issue on Jira: http://bit.ly/gpstW9
Update (5th May 2011): Upon jwage's recommendation I have switch to Referenced relationships between Categories and Post (as opposed to Embdedded).
I'm using the latest version of Doctrine ODM (fresh from Git).
I have three levels of Documents (Two embedded); Category -> EmbedsMany: Post -> EmbedsMany PostVersion.
PostVersion is automatically handled by Post. When I make a new post, it actually makes a new PostVersion under the hood as well.
My issue is that Doctrine get confused with PostVersions, if I retrieve an existing Category and add a new Post to it, the new Post's PostVersions get add to the first Post in the Category's $posts collection.
Step-by-step:
- Make a new Post (Post1) and Category
- Add Post1 to Category
- Persist Category, Flush, Clear
- Retrieve Category
- Make a new Post (Post2)
- Add Post2 to Category
- Flush
At this stage in the database, there should be one Category, two Posts and each Post has one PostVersion. However, what actually happens is there is one Category, two Posts, the first Post has two PostVersions and the second Post has zero PostVersions.
The Documents themselves during the request are correct, it's just want is persisted to the database that is wrong. What am I missing?
Expected Result:
{
"_id": ObjectId("4da66baa6dd08df1f6000001"),
"name": "The Category",
"posts": [
{
"_id": ObjectId("4da66baa6dd08df1f6000002"),
"activeVersionIndex": 0,
"versions": [
{
"_id": ObjectId("4da66baa6dd08df1f6000003"),
"name": "One Post",
"content": "One Content",
"metaDescription": null,
"isAutosave": false,
"createdAt": "Thu, 14 Apr 2011 13:36:10 +1000",
"createdBy": "Cobby"
}
]
},
{
"_id": ObjectId("4da66baa6dd08df1f6000004"),
"activeVersionIndex": 0
"versions": [
{
"_id": ObjectId("4da66baa6dd08df1f6000005"),
"name": "Two Post",
"content": "Two Content",
"metaDescription": null,
"isAutosave": false,
"createdAt": "Thu, 14 Apr 2011 13:36:10 +1000",
"createdBy": "Cobby"
}
]
}
]
}
Actual Result:
{
"_id": ObjectId("4da66baa6dd08df1f6000001"),
"name": "The Category",
"posts": [
{
"_id": ObjectId("4da66baa6dd08df1f6000002"),
"activeVersionIndex": 0,
"versions": [
{
"_id": ObjectId("4da66baa6dd08df1f6000003"),
"name": "One Post",
"content": "One Content",
"metaDescription": null,
"isAutosave": false,
"createdAt": "Thu, 14 Apr 2011 13:36:10 +1000",
"createdBy": "Cobby"
},
{
"_id": ObjectId("4da66baa6dd08df1f6000005"),
"name": "Two Post",
"content": "Two Content",
"metaDescription": null,
"isAutosave": false,
"createdAt": "Thu, 14 Apr 2011 13:36:10 +1000",
"createdBy": "Cobby"
}
]
},
{
"_id": ObjectId("4da66baa6dd08df1f6000004"),
"activeVersionIndex": 0
}
]
}
Here are my Documents
Category.php
<?php
namespace Documents\Blog;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @Document(collection="blog")
* @HasLifecycleCallbacks
*/
class Category
{
/**
* @Id
*/
private $id;
/**
* @String
*/
private $name;
/**
* @EmbedMany(targetDocument="Documents\Blog\Post")
*/
private $posts;
public function __construct($name = null)
{
$this->posts = new ArrayCollection();
$this->setName($name);
}
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getPosts()
{
return $this->posts->toArray();
}
public function addPost(Post $post)
{
$this->posts->add($post);
}
public function getPost($id)
{
return $this->posts->filter(function($post) use($id){
return $post->getId() === $id;
})->first();
}
}
Post.php
<?php
namespace Documents\Blog;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @EmbeddedDocument
* @HasLifecycleCallbacks
*/
class Post
{
/**
* @Id
*/
private $id;
private $firstVersion;
private $activeVersion;
/**
* @Int
*/
private $activeVersionIndex;
/**
* @EmbedMany(targetDocument="Documents\Blog\PostVersion")
*/
private $versions;
static private $currentUser;
private $isDirty = false;
public function __construct($name = "", $content = "")
{
if(!self::$currentUser){
throw new \BlogException("Cannot create a post without the current user being set");
}
$this->versions = new ArrayCollection();
$this->activeVersion = $this->firstVersion = new PostVersion($name, $content, self::$currentUser);
$this->versions->add($this->firstVersion);
$this->isDirty = true;
}
public function getId()
{
return $this->id;
}
public function getFirstVersion()
{
return $this->firstVersion;
}
public function getActiveVersion()
{
return $this->activeVersion;
}
public function setName($name)
{
$this->_setVersionValue('name', $name);
}
public function getName()
{
return $this->getActiveVersion()->getName();
}
public function setContent($content)
{
$this->_setVersionValue('content', $content);
}
public function getContent()
{
return $this->getActiveVersion()->getContent();
}
public function setMetaDescription($metaDescription)
{
$this->_setVersionValue('metaDescription', $metaDescription);
}
public function getMetaDescription()
{
return $this->getActiveVersion()->getMetaDescription();
}
public function getVersions()
{
return $this->versions->toArray();
}
private function _setVersionValue($property, $value)
{
$version = $this->activeVersion;
if(!$this->isDirty){
// not dirty, make a new version
$version = new PostVersion($version->getName(), $version->getContent(), self::getCurrentUser());
}
$refl = new \ReflectionProperty(get_class($version), $property);
$refl->setAccessible(true);
// updated current user
$refl->setValue($version, $value);
// unset ID
$refl = new \ReflectionProperty(get_class($version), 'id');
$refl->setAccessible(true);
$refl->setValue($version, null);
// updated self
if(!$this->isDirty){
$this->activeVersion = $version;
$this->versions->add($version);
$this->isDirty = true;
}
// no first version, this must be the first
if($this->versions->count() === 1){
$this->firstVersion = $version;
}
}
static public function setCurrentUser($user)
{
self::$currentUser = $user;
}
static public function getCurrentUser()
{
return self::$currentUser;
}
/**
* @PostLoad
*/
public function findFirstVersion()
{
$firstVersion = null;
foreach($this->versions as $version){
if(null === $firstVersion){
// first iteration, start with any version
$firstVersion = $version;
continue;
}
if($version->getCreatedAt() < $firstVersion->getCreatedAt()){
// current version is newer than existing version
$firstVersion = $version;
}
}
if(null === $firstVersion){
throw new \DomainException("No first version found.");
}
$this->firstVersion = $firstVersion;
}
/**
* @PostLoad
*/
public function findActiveVersion()
{
$this->activeVersion = $this->versions->get($this->activeVersionIndex);
}
/**
* @PrePersist
* @PreUpdate
*/
public function doActiveVersionIndex()
{
$this->activeVersionIndex = $this->versions->indexOf($this->activeVersion);
$this->isDirty = false;
}
/**
* @PostPersist
* @PostUpdate
*/
public function makeClean()
{
$this->isDirty = false;
}
public function getCreatedBy()
{
return $this->getFirstVersion()->getCreatedBy();
}
public function getCreatedAt()
{
return $this->getFirstVersion()->getCreatedAt();
}
}
PostVersion.php
<?php
namespace Documents\Blog;
/**
* @EmbeddedDocument
*/
class PostVersion
{
/**
* @Id
*/
private $id;
/**
* @String
*/
private $name;
/**
* @String
*/
private $content;
/**
* @String(nullable="true")
*/
private $metaDescription;
/**
* @Boolean
*/
private $isAutosave = false;
/**
* @Date
*/
private $createdAt;
/**
* @String
*/
private $createdBy;
public function __construct($name, $content, $author)
{
$this->setName($name);
$this->setContent($content);
$this->setCreatedBy($author);
$this->touch();
}
public function __clone()
{
if($this->id){
$this->id = null;
$this->touch();
}
}
private function touch()
{
$this->createdAt = new \DateTime();
}
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getContent()
{
return $this->content;
}
public function setContent($content)
{
$this->content = $content;
}
public function getIsAutosave()
{
return $this->isAutosave;
}
public function setIsAutosave($isAutosave)
{
$this->isAutosave = $isAutosave;
}
public function getCreatedAt()
{
return $this->createdAt;
}
public function setCreatedAt(\DateTime $createdAt)
{
$this->createdAt = $createdAt;
}
public function getCreatedBy()
{
return $this->createdBy;
}
public function setCreatedBy($createdBy)
{
$this->createdBy = $createdBy;
}
public function setMetaDescription($metaDescription)
{
$this->metaDescription = $metaDescription;
}
public function getMetaDescription()
{
return $this->metaDescription;
}
}
...time to get dirty with xdebug I think.