Symfony 2: persist entity with one to many relationship - php

I have an entity Project that is related with some files via one-to-many relationship like this:
class projects{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="files", mappedBy="project", cascade= {"persist","merge","remove"},orphanRemoval=true)
*/
protected $project_files;
}
And File entity:
class files{
private $files;
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="projects", inversedBy="project_files")
*/
protected $project;
}
So when I edit a project, I want to be able to add some new files to it or delete some olds. I found a workaround to implement this but I can't figure out the way Symfony and Doctrine really handle this kind of operations. More precisely, my code (that works fine) that handles the deletion of some old files on edit is:
private function persistProject(Form $form) {
$project = $form->getData();
$raw_files = $project->getProjectFiles()->toArray();
$ready_files = self::setUpFiles($raw_files, $project);
$project->setProjectFiles($ready_files);
$em->merge($project);
$em->flush();
}
private function setUpFiles($project_files, $project) {
$ready_files = new ArrayCollection();
foreach ($project_files as $project_file){
if ((!empty($project_file->getId())) && ($project_file->getDescription() != '--del')){
$ready_files->add($project_file);
}
else if ((!empty($project_file->getId())) && ($project_file->getDescription() == '--del')){
unlink($this->uploadDir . $project_file->getPath());
// --- controversial line of code --- //
$project->removeProjectFile($project_file);
}
}
return $ready_files;
}
So in brief the situation is about this: if I remove line "$project->removeProjectFile($project_file);", the files are not deleted, even if my ArrayCollection $ready_files, that is ultimately stored to the object in line "$project->setProjectFiles($ready_files);", is exactly as it should be and does not contain the deleted files.
So my question is dual:
Shouldn't symfony persist the exact ArrayCollection (and only this) that I set and delete all the others files stored? Why does not the code work properly without the line "$project->removeProjectFile($project_file);"?
How does the line "$project->removeProjectFile($project_file);" affects the $project object in function persistProject() since the object is passed as a simple argument and not by reference? Isn't it obvious to expect that everything that happens to $project object in function setUpFiles() will do nothing to the original $project in function persistProject() as it is in different scope?
Can anyone help me to clear this up?

Related

Dont work "remove" in Doctrine2

Why I can't delete entry in database using Doctrine 2 Entity manager?
I have next controller and entity with whom I have a problem.
I get in controller object form entity manager and i can't delete this object. Why?
// /Controller/Controller.php
/**
* Handler delete checkbox
* #Route("/administrator/services/delete/{id}", requirements={"id" = "\d+"}, defaults={"id" = 0}, name="service_delete")
* #Template()
*/
public function serviceDeleteAction(Request $request, $id){
$em = $this->getDoctrine()->getEntityManager();
$repoServices = $em->getRepository(CoworkingService::class);
$services = $repoServices->findOneBy(['id' => $id]);
$em->remove($services);
$em->persist($services);
$em->flush();
return [];//$this->redirectToRoute('administrator');
}
// /Entity/CoworkingService.php
class CoworkingService
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string", length=50)
*/
private $name;
/**
* #ORM\ManyToOne(targetEntity="SentviBundle\Entity\Language")
* #ORM\JoinColumn(name="language_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $language;
/**
* #ORM\Column(name="common_identifier", type="text")
*/
private $commonIdentifier;
Thanks!
#Matteo’s comment has already solved the issue, but let me explain what has happened.
You’re executing 3 entity manager operations:
$em->remove($services);
$em->persist($services);
$em->flush();
You must know that, before you call $em->flush(), all operations are registered in a service called the “unit of work” (UOW).
The UOW keeps track of all modifications in your entities (including adding/deleting entities), and only applies them to the database when you call flush().
When calling $em->remove($services), you told the UOW that you want to delete the entity. However, when calling $em->persist($services) directly afterwards, you told the UOW that you want to create (or, effectively: keep) the entity. (Note that, in Doctrine, “persist” doesn’t mean that a connection is made to the database, but instead, you pass an entity to the EM/UOW to calculate the modifications.)
So, in conclusion, the persist operation cancelled the remove out, and, at that point, flush had nothing to do.
For more details on the entity lifecycle and EM states see http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html

Doctrine 2 OneToOne, load owning side only when neccessary

I have two entities - User and UserSettings. In User entity, I want to have UserSettings as an attribute. That would be OK, I would add a OneToOne relation but there's a problem - because UserSettings is an owning side of the relation, every time I load User entity, Doctrine has to load the UserSettings entity too.
Is there a way how to load User but not UserSettings?
I made maybe a weird solution - there's no relation between these entities and the settings are loaded by method of Facade. For example:
/**
* #ORM\Entity
*/
class User
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(type="string")
*/
private $name;
/** #var UserSettings */
private $settings;
public function __construct()
{
$this->settings = new UserSettings();
}
public function setSettings(UserSettings $settings)
{
$this->settings = $settings;
}
}
/**
* #ORM\Entity
*/
class UserSettings
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue
*/
private $id;
/**
* #ORM\Column(name="user_id", type="integer")
*/
private $userId;
}
class UserFacade
{
/**
* #var EntityManager
*/
private $em; // is injected automatically by DI
public function loadSettings(User $user)
{
$settings = $this->em->getRepository("UserSettings")->findOneBy(array("userId" => $user->id));
$user->setSettings($settings);
}
}
$user = $em->find("User", 1);
// if I want user's settings
$userFacade->loadSettings($user); // now I can use $user->getSettings()->something;
Side note: UserFacade is a service class that manipulates with users' data like adding new user, editing, deleting etc. In my MVC application, controller classes communicate with Facades, not with EntityManager directly.
That's OK - settings are loaded only when I want to. However, there are two possible problems:
a) I don't think this is a clear way
b) When I want a list of users, I cannot JOIN a table where settings are, because entities are not associated, so I have to make an extra SQL for each user.
My question is - how to solve the problem with OneToOne relation? I don't have much experience with Doctrine, so it may be a stupid question - sorry for that.
Thanks!

Symfony2 lazy loading doesn't work

I have a problem with lazy loading in symfony2/doctrine2.
I have a normal object (for example: type item) and this object has an id. If I look at the object at runtime I see that the id is set. Every other parameters like icon and amount are empty. I know, this is how lazy loading works but when I call the getters (getIcon) nothing happens. The icon attribute is still empty. I also tried to call the __load method but it doesn't help.
Sorry, forgot the code
class Character {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
/*********************************************************************
* Custom methods
*/
public function getItem() {
return $this->item;
}
}
And this is the object where the lazy loading not works.
class Item {
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="integer")
*/
protected $amount;
/**
* #ORM\Column(type="string")
*/
protected $icon;
}
EDIT2:
Constructor of character class
public function __construct()
{
$this->item = new \Doctrine\Common\Collections\ArrayCollection();
}
So what the previos comments to your initial post are pointing at, is, that you need to implemend a ManyToOne relation in your Item entity to get all your stuff working.
In yout Character Entity you have this lines of code
/**
* #ORM\OneToMany(targetEntity="Entity\Item", mappedBy="character")
*/
protected $item;
This says you have a relation to an Entity Item which mappes the relation in the attribute "character". In this attribute the relation is stored. If you look into the database, you won't find any stored relations, because you class Item does not have the described mapping attribute character. Like gp_sflover pointed out, a OneToMany relations needs to be Bidirectional an required a ManyToOne relation in the "owning" side. So what you have to do is, add the following code to your Item Entity
/**
* #ORM\ManyToOne(targetEntity="Entity\Character", inversedBy="item")
*/
protected $character;
The inversedBy attribute creates a bidirectional relation. Without this statement, you wouldn't be able to load getItems from your Character entity.
If you have changed your code you have to update your database and to restore the elements. After this, everything will work fine.

Doctrine: Object of class User could not be converted to string

I keep getting this error with Doctrine:
PHP Catchable fatal error: Object of class User could not be converted to string in vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php on line 1337
In my system users can have many permissions in a One to Many relationship. I have set up a User and Permission entity. They look like this (I removed some annotations, getters and setters to reduce clutter):
class User {
/**
* #ORM\Column(name="user_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
public function getId()
{
return $this->id;
}
/**
* #ORM\OneToMany(targetEntity="Permission", mappedBy="user", cascade={"persist"})
*/
protected $permissions;
public function getPermissions()
{
return $this->permissions;
}
}
class Permission {
/**
* #ORM\Column(name="user_id", type="integer")
* #ORM\ManyToOne(targetEntity="User", inversedBy="permissions")
*/
protected $user;
public function getUser()
{
return $this->user;
}
public function setUser( $user )
{
$this->user = $user;
return $this;
}
}
The problem occurs when I add a new Permission to a User:
$permission = new Permission();
$user->getPermissions()->add( $permission );
$em->persist( $user );
$em->flush();
This is the last bit of my stack trace:
PHP 11. Doctrine\ORM\UnitOfWork->persist() vendor/doctrine/orm/lib/Doctrine/ORM/EntityManager.php:565
PHP 12. Doctrine\ORM\UnitOfWork->doPersist() vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1555
PHP 13. Doctrine\ORM\UnitOfWork->cascadePersist() vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1615
PHP 14. Doctrine\ORM\UnitOfWork->doPersist() vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:2169
PHP 15. Doctrine\ORM\UnitOfWork->persistNew() vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1597
PHP 16. Doctrine\ORM\UnitOfWork->scheduleForInsert() doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:836
PHP 17. Doctrine\ORM\UnitOfWork->addToIdentityMap() vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1157
PHP 18. implode() vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php:1337
Any insight would be greatly appreciated.
OK. I've got it working.
I haven't fully worked out the reason yet but when I add the following to my User entity it works:
class User {
public function __toString()
{
return strval( $this->getId() );
}
}
If I find out more I will post here.
Your solution gave me a clue of what is happening.
Even though you have the entities and the anotations, Doctrine is not being able to understand the relation between entities. When doctrine understands the relation between entities, it knows what methods to call (ie User::getId()) but otherwise, it tries to transform whatever you are sending to a scalar value that it can use to query the database. Thats why it is calling the __toString function of the User, and thats why if you return the id in toString, everything works from here.
This is ok, but its a patch, and probably you dont want to keep it if we can find a better solution, since it could be harder to maintain as your application grows.
What i can see, is that in Permissions you have:
/**
* #ORM\Column(name="user_id", type="integer")
* #ORM\ManyToOne(targetEntity="User", inversedBy="permissions")
*/
protected $user;
You should remove the #ORM\Column(type="integer")
About the join columns, it is not mandatory, but you have to be sure that the defauts, are what you want. As we can read here
Before we introduce all the association mappings in detail, you should
note that the #JoinColumn and #JoinTable definitions are usually
optional and have sensible default values. The defaults for a join
column in a one-to-one/many-to-one association is as follows:
name: "<fieldname>_id"
referencedColumnName: "id"
so, they will be the same as an explicit:
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="permissions", cascade={"persist"})
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
* })
*/
protected $user;
So it is supposed to look for a column user_id in the Permissions table, and join it with the id column of the User table. We suppose that this is ok.
If this is true, then in your User, the id shouldnt be user_id, but id:
/**
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
Or if the column name is actually user_id, then the User class is ok, but you have to change the join column to #ORM\JoinColumn(name="user_id", referencedColumnName="user_id")
That much i can say. I cannot try it know, but i will be glad if you can give it a second.
I think there's a problem with the mapping of user property in permission entity. Try this one:
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="permissions")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
are you initializing the collection in your OneToMany side?
and also, the methods to add and remove from the collection?
class User {
/**
* #ORM\OneToMany(targetEntity="Permission", mappedBy="user", cascade={"persist"})
*/
protected $permissions;
public function getPermissions()
{
return $this->permissions;
}
public function __construct()
{
$this->permissions = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addPermissions (Permission $permissions)
{
$this->permissions[] = $permissions;
return $this;
}
public function removePermissions(Permission $permissions)
{
$this->permissions->removeElement($permissions);
}
//...

Symfony2/Doctrine: How to re-save an entity with a OneToMany as a cascading new row

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();
}

Categories