I tried to implement a versionable ORM using Doctrine2.
Everything works well. When I update an existing entry the old entry gets inserted into the *_version table.
But when I update the entry in another request (and therefor a new instance of EntityManager) the old entry won't be written anymore to the *_version table although the entry in the basic table gets updated without any problems (even the version no. gets incremented by 1).
I like to show you my very simple versionable ORM:
UPDATE: The example code below works now!
Also check my Gist with the logEntityVersion helper method.
ProductBase.php
trait ProductBase
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* #ORM\Column(type="string")
*/
protected $name;
// getters and setters here
}
Product.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Product
*
* #ORM\Entity
* #ORM\Table(name="product")
* #ORM\HasLifeCycleCallbacks
*/
class Product
{
use ProductBase;
/**
* #ORM\OneToMany(targetEntity="ProductVersion", mappedBy="product", cascade={"persist"})
*/
private $auditLog;
/**
* #ORM\Column(type="integer")
* #ORM\Version
*/
private $version = 1;
public function __construct()
{
$this->auditLog = new ArrayCollection();
}
public function logVersion()
{
echo sprintf("Creating a new version for ID %s, version %s\n", $this->getId(), $this->getVersion());
$this->auditLog[] = new ProductVersion($this);
}
// Version getter and setter
}
ProductVersion.php
use Doctrine\ORM\Mapping as ORM;
/**
* ProductVersion
*
* #ORM\Entity
* #ORM\Table(name="product_version")
*/
class ProductVersion
{
use ProductBase;
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="auditLog")
*/
private $product;
/**
* #ORM\Column(type="integer")
*/
private $version;
public function __construct(Product $product)
{
$this->product = $product;
$this->name = $product->getName();
$this->version = $product->getVersion();
var_dump($product->getVersion());
}
// Version getter and setter
}
And this is the code for inserting a new entry and update it to create a new version:
// Insert new product
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$product = new Product();
$product->setName('Product V1');
$this->entityManager->persist($product);
$this->entityManager->flush();
$this->entityManager->commit();
$productId = $product->getId();
echo "Created Product with ID " . $product->getId() . "\n";
/** #var Product $foo */
$foo = $this->entityManager->getRepository('orm\Product')->find($productId);
// Log version (Product V1)
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$foo->logVersion();
$this->entityManager->flush();
$this->entityManager->commit();
// Update 1
$foo->setName('Product V2');
$this->entityManager->flush();
// Log version (Product V2)
$this->entityManager->beginTransaction();
$this->entityManager->flush();
$foo->logVersion();
$this->entityManager->flush();
$this->entityManager->commit();
// Update 2
$foo->setName('Product V3');
$this->entityManager->flush();
Schema generation
$tools = new SchemaTool($this->entityManager);
var_dump($tools->getCreateSchemaSql(array(
$this->entityManager->getClassMetadata('orm\Product'),
$this->entityManager->getClassMetadata('orm\ProductVersion')
)));
I see a couple of issues with your code, and unfortunately the concept as well:
Array vs Collection
You've defined Product::$auditLog as an array, but it needs to be a Collection. Use this in stead:
class Product
{
private $auditLog;
public function __construct()
{
$this->auditLog = new \Doctrine\Common\Collections\ArrayCollection();
}
Association mapping
You've defined Product::$auditLog as being mapped by ProductVersion::$product, but haven't defined ProductVersion::$product as being inversed by Product::$auditLog. Fix it like this:
class ProductVersion
{
/** #ORM\ManyToOne(targetEntity="Product", inversedBy="auditLog") */
private $product;
Also, please validate your mappings and database schema. Every error might lead to unexpected results.
$ doctrine orm:validate-schema // plain Doctrine2
$ app/console doctrine:schema:validate // Symfony2
Versioning
You're using the #Version annotation in the trait, meaning both Product and ProductVersion will be versioned by Doctrine. As #Version is used for optimistic locking, I doubt this is what you're really after.
Records in an audit-log should never be updated (or deleted), only added. So locking records doesn't really make sense here.
Please remove ProductBase::$version and in stead add:
Product::$version with #Version annotation.
ProductVersion::$version without #Version annotation.
Now only Product will be versioned by Doctrine, ProductVersion will simply contain the version-number of the product that is logged.
PreUpdate event
You're using #PrePersist and #PreUpdate lifecycle callbacks to populate the audit-log. #PreUpdate is going to be a problem, as the docs state:
Changes to associations of the updated entity are never allowed in this event
Yet you are changing the Product::$auditLog association by adding a record to the collection.
You'll have to move this logic, and there are many options as to where to:
One option could be to start a transaction manually, flush (and keep track of versioned entities), create log entries, flush again, commit the transaction. Or you could replace the second flush with direct inserts using the DBAL connection.
Related
Is there a way to tell Doctrine the name of a number of entities and it creates their related tables (incl. foreign keys etc.)?
My scenario:
I want to have annotations at my Doctrine entities as the only source for my database schema. Which means, that for instance for tests, i don't want to maintain a copy of these information in a SQL file or something.
To be clear, i mean annotations in entity classes like the following:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields={"email"}, message="There is already an account with this email")
*
* #ORM\Table(
* uniqueConstraints={
* #ORM\UniqueConstraint(name="email", columns={"email"})
* }
* )
*/
class User
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, nullable=false)
*/
private $email;
// ...
}
What i would like to do:
In my tests i would like to create the table for, lets say User, like:
<?php
namespace App\Test;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class SomeTestCase extends KernelTestCase
{
public function setUp()
{
// ...
$this->entityManager = $kernel->getContainer()
->get('doctrine')
->getManager();
}
public function test1()
{
// Is there a function available which has this functionality?
$this->entityManager->createTableForEntity('App\Entity\User'); // <---------
// ...
}
}
Is that possible? If not, even creating all tables at once is fine for me.
Is there another way to achieve it?
I use the following to create all the tables in my tests:
use Doctrine\ORM\Tools\SchemaTool;
$metadatas = $this->entityManager->getMetadataFactory()->getAllMetadata();
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($metadatas);
There is a method getMetadataFactory() on the MetadataFactory class so I guess the following should work as well if you want to create just one table.
$metadata = $this->entityManager->getMetadataFactory()->getMetadataFor('App\Entity\User');
$schemaTool = new SchemaTool($this->entityManager);
$schemaTool->updateSchema($metadata);
I've an entity Order, with a property events which should contain a list of all changes made to this entity.
The Order class:
<?php
/**
* #ORM\Entity
*/
class Order
{
// more stuff...
/**
* #ORM\OneToMany(
* targetEntity="OrderEvent",
* mappedBy="order",
* cascade={"persist", "merge"}
* )
*/
protected $events;
// more stuff...
}
The OrderEvent class:
/**
* #ORM\Entity
*/
class OrderEvent
{
// more stuff...
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="events")
* #ORM\JoinColumn(nullable=false)
*/
protected $order;
// more stuff...
}
class OrderLifecycle
{
public function preUpdate(Order $order, PreUpdateEventArgs $args)
{
$changes = $args->getEntityChangeSet();
if (!empty($changes)) {
$event = new OrderEvent();
$event->setOrder($order)
->setChanges($changes);
$order->addEvent($event);
return $event;
}
}
}
But according to the doctrine documentation, the preUpdate method should not be used to change associations.
What is the recommended way to do things like this one?
I am using Zend Framework 2, but I think that's not relevant.
I think in this case you could use PostUpdate event. In that case you are sure that the update action was successful and you can do what you want; add the new OrderEvent instance to your $events collection.
EDIT
You are not the first one implementing such thing. Maybe you should check existing examples and see how they deal with this (or even consider using it). For example the Gedmo Loggable solution.
With this extension you can mark entities as loggable with a simple #annotiation:
/**
* #Entity
* #Gedmo\Loggable
*/
class Order
{
// Your class definition
}
I'm trying to do bi-directional one-to-one with doctrine.
User model
<?php
/**
* #Entity
* #Table(name="User")
*/
class User {
/**
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #OneToOne(targetEntity="Cart",mappedBy="user",cascade={"persist"})
*/
public $cart;
}
Cart model
<?php
/**
* #Entity
* #Table(name="Cart")
*/
class Cart {
/**
* #Id
* #Column(type="integer")
* #GeneratedValue
*/
protected $id;
/**
* #OneToOne(targetEntity="User",inversedBy="cart")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
public $user;
}
And I want to set the cart like that
<?php
$user->cart = new Cart;
$entityManager->flush();
The cart has been created, but the user_id is not set. (NULL)
What's wrong ?
(I've not created getters/setters for test)
So I did some extended testing to see what could be done.
Your relationship code appears correct. I'd personally drop the join column annotation, as it defaults to that anyway.
You DO need getters/setters inside each of the entities such as:
// In User entity
public function setCart(Cart $cart) {
$this->cart = $cart;
}
// In Cart entity
public function setUser(User $user) {
$this->user = $user;
}
You also need to do something like:
$user = new User();
$em->persist($user);
$cart = new Cart();
$cart->setUser($user);
$em->persist($cart);
$user->setCart($cart);
$em->flush();
Using this snippet I was able to save two relationships to the DB and load them again. The documentation specifically states that new owners with cascade do not cascade.
"Doctrine 2 does not cascade the persist operation to all nested
entities that are new as well."
In my example, I've specifically set and persisted both the new items. Existing parents with new cascading children don't have this problem.
Hopefully you can use this information to figure out exactly what needs to change.
To simplify:
You DO need getters/setters inside each of the entities such as:
// In User entity
public function setCart(Cart $cart) {
$this->cart = $cart;
}
// In Cart entity
public function setUser(User $user) {
$user->setCart($this);
$this->user = $user;
}
You also need to do something like:
$user = new User();
$em->persist($user);
$cart = new Cart();
$cart->setUser($user);
$em->persist($cart);
$em->flush();
This process "$user->setCart($cart);" is deported to setter of User in cart;
It's a pretty old question but I faced something similar and found another alternative to kinghfb answer. You can also do the following on your inverse side's entity (User in case of this question).
/**
* User's Constructor
*/
public function __construct()
{
$this->cart= new Cart();
$this->cart->setUser($this);
}
Note that you will need getters and setters as suggested in accepted answer but after doing above, you do not need to create and persist the Cart entity every time you are saving User.
Caution: This solution is for the case where a user will always have a cart.
Hope this helps someone facing similar issues. Thanks!
Background:
In my application I have an entity that has a self referencing ManyToOne association (many children can point to a single parent). And I have a feature that does mass updates on many entities at one time using the Doctrine ORM. To keep performance from dropping dramatically due to many entities being loaded I detach entities once they've been updated.
Problem:
When I detach an entity that has children and later try to update any of those children Doctrine complains that it doesn't know the parent anymore. Even if I merge the parent entity before trying to update the child.
Question:
What am I doing wrong when I detach the parent entity? I've tried doing cascade="merge" and/or "detach" on the parent column and Doctrine still complains about the parent being an unknown entity when I try to persist.
I've mocked up a simple example that reproduces this. See below.
Test Code:
Entity\Thing.php
/**
* #ORM\Entity()
* #ORM\Table(name="things")
*/
class Thing
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Thing", inversedBy="children", cascade={"detach","merge"})
* #ORM\JoinColumn(name="parentId", referencedColumnName="id", onDelete="SET NULL")
*/
protected $parent;
/**
* #ORM\OneToMany(targetEntity="Thing", mappedBy="parent")
*/
protected $children;
/**
* #ORM\Column(type="string", length=64)
*/
protected $name;
public function __construct($name = null)
{
$this->children = new ArrayCollection();
$this->name = $name;
}
// .. SNIP ...
}
Test Action:
public function testThingAction($_route)
{
$em = $this->getDoctrine()->getEntityManager();
$repo = $em->getRepository('AcmeThingBundle:Thing');
// simple setup of a couple things in the DB
$t1 = $repo->findByName('Thing1');
if (!$t1) {
$t1 = new Thing('Thing1');
$t2 = new Thing('Thing2');
$t2->setParent($t1);
$em->persist($t1);
$em->persist($t2);
$em->flush();
return $this->redirect($this->generateUrl($_route));
}
list($t1, $t2) = $repo->findAll();
// detach and re-merge Thing1
// This should cause Thing1 to be removed and then re-added
// to the doctrine's known entities; but it doesn't!?
$em->detach($t1);
$em->merge($t1);
// try to update T2
$t2->setName('Thing2 - ' . time());
$em->persist($t2);
// will fail with:
// A new entity was found through the relationship Thing#parent
$em->flush();
return array();
}
The issue is that the child has a relationship to a specific parent object that is no longer managed by Doctrine. When you call $entityManager->merge($entity) you get a new managed entity back from that function.
When you get that back, you need to manually call setParent() on each of your children with the newly managed entity.
Firstly, this question is similar to How to re-save the entity as another row in Doctrine 2
The difference is that I'm trying to save the data within an entity that has a OneToMany relationship. I'd like to re-save the entity as a new row in the parent entity (on the "one" side) and then as new rows in each subsequent child (on the "many" side).
I've used a pretty simple example of a Classroom having many Pupils to keep it simple.
So me might have ClassroomA with id=1 and it has 5 pupils (ids 1 through 5). I'd like to know how I could, within Doctrine2, take that Entity and re-save it to the database (after potential data changes) all with new IDs throughout and the original rows being untouched during the persist/flush.
Lets first define our Doctrine Entities.
The Classroom Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="classroom")
*/
class Classroom
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $miscVars;
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom")
*/
protected $pupils;
public function __construct()
{
$this->pupils = new ArrayCollection();
}
// ========== GENERATED GETTER/SETTER FUNCTIONS BELOW ============
}
The Pupil Entity:
namespace Acme\TestBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="pupil")
*/
class Pupil
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $moreVars;
/**
* #ORM\ManyToOne(targetEntity="Classroom", inversedBy="pupils")
* #ORM\JoinColumn(name="classroom_id", referencedColumnName="id")
*/
protected $classroom;
// ========== GENERATED FUNCTIONS BELOW ============
}
And our generic Action function:
public function someAction(Request $request, $id)
{
$em = $this->getDoctrine()->getEntityManager();
$classroom = $em->find('AcmeTestBundle:Classroom', $id);
$form = $this->createForm(new ClassroomType(), $classroom);
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
// Normally you would do the following:
$em->persist($classroom);
$em->flush();
// But how do I create a new row with a new ID
// Including new rows for the Many side of the relationship
// ... other code goes here.
}
}
return $this->render('AcmeTestBundle:Default:index.html.twig');
}
I've tried using clone but that only saved the parent relationship (Classroom in our example) with a fresh ID, while the children data (Pupils) was updated against the original IDs.
Thanks in advance to any assistance.
The thing with clone is...
When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. Any properties that are references to other variables, will remain references.
If you are using Doctrine >= 2.0.2, you can implement your own custom __clone() method:
public function __clone() {
// Get current collection
$pupils = $this->getPupils();
$this->pupils = new ArrayCollection();
foreach ($pupils as $pupil) {
$clonePupil = clone $pupil;
$this->pupils->add($clonePupil);
$clonePupil->setClassroom($this);
}
}
NOTE: before Doctrine 2.0.2 you cannot implement a __clone() method in your entity as the generated proxy class implements its own __clone() which does not check for or call parent::__clone(). So you'll have to make a separate method for that like clonePupils() (in Classroom) instead and call that after you clone the entity. Either way, you can use the same code inside your __clone() or clonePupils() methods.
When you clone your parent class, this function will create a new collection full of child object clones as well.
$cloneClassroom = clone $classroom;
$cloneClassroom->clonePupils();
$em->persist($cloneClassroom);
$em->flush();
You'll probably want to cascade persist on your $pupils collection to make persisting easier, eg
/**
* #ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom", cascade={"persist"})
*/
protected $pupils;
I did it like this and it works fine.
Inside cloned Entity we have magic __clone(). There we also don't forget our one-to-many.
/**
* Clone element with values
*/
public function __clone(){
// we gonna clone existing element
if($this->id){
// get values (one-to-many)
/** #var \Doctrine\Common\Collections\Collection $values */
$values = $this->getElementValues();
// reset id
$this->id = null;
// reset values
$this->elementValues = new \Doctrine\Common\Collections\ArrayCollection();
// if we had values
if(!$values->isEmpty()){
foreach ($values as $value) {
// clone it
$clonedValue = clone $value;
// add to collection
$this->addElementValues($clonedValue);
}
}
}
}
/**
* addElementValues
*
* #param \YourBundle\Entity\ElementValue $elementValue
* #return Element
*/
public function addElementValues(\YourBundle\Entity\ElementValue $elementValue)
{
if (!$this->getElementValues()->contains($elementValue))
{
$this->elementValues[] = $elementValue;
$elementValue->setElement($this);
}
return $this;
}
Somewhere just clone it:
// Returns \YourBundle\Entity\Element which we wants to clone
$clonedEntity = clone $this->getElement();
// Do this to say doctrine that we have new object
$this->em->persist($clonedEntity);
// flush it to base
$this->em->flush();
I do this:
if ($form->isValid()) {
foreach($classroom->getPupils() as $pupil) {
$pupil->setClassroom($classroom);
}
$em->persist($classroom);
$em->flush();
}