Attempt to update entity without persisting on it DOCTRINE - php

I have a Product entity with a ManyToOne relationship with Category.
use Doctrine\ORM\Mapping as ORM;
class Product
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Category")
* #ORM\JoinColumn(nullable=true)
*/
private $category;
}
class Category
{
use BlameableTrait;
...
}
The Category entity implements a trait, with properties to record when a record is created, updated, etc.
use App\Entity\User\User;
use Gedmo\Mapping\Annotation as Gedmo;
trait BlameableTrait
{
/**
* #Gedmo\Blameable(on="create")
* #ORM\ManyToOne(targetEntity="App\Entity\User\User")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
* #var User
*/
private $createdBy;
/**
* #var User|null
*
* #Gedmo\Blameable(on="update")
* #ORM\ManyToOne(targetEntity="App\Entity\User\User")
* #ORM\JoinColumn(nullable=true, onDelete="SET NULL")
*/
private $updatedBy;
public function getCreatedBy(): ?User
{
return $this->createdBy;
}
public function setCreatedBy(?User $createdBy): self
{
$this->createdBy = $createdBy;
return $this;
}
public function setUpdatedBy(?User $updatedBy): self
{
$this->updatedBy = $updatedBy;
return $this;
}
public function getUpdatedBy(): ?User
{
return $this->updatedBy;
}
}
When I try to update the product entity I have an error indicating that an error has occurred while trying to update the Category entity, but I have not indicated that it should be updated.
This only happens to me in the production environment and randomly, one time it works, another time it doesn't.
Locally, debugging the symfony profiler, for the same curl, only one update is done on the Product entity, which is fine.
I don't understand where Symfony or Doctrine try to update the Category entity.
Both the production and local environments run on the same Docker image.

Related

Doctrine lifecycle callbacks not working with EasyAdminBundle

I am using Symfony 4 with EasyAdminBundle to create a simple administrative interface. I'm having a few problems trying to automatically set the value for createdAt and updatedAt columns. When creating/updating an entity within EasyAdmin, the configured Doctrine lifecycle callbacks are not called. For example, here's a simple entity that is managed with EasyAdmin, note the lifecycle callback hooks:
<?php
namespace App\Entity;
/**
* #ORM\Entity(repositoryClass="App\Repository\ProductRepository")
* #ORM\HasLifecycleCallbacks
*/
class Product
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
// Additional column configuration removed for brevity
/**
* #ORM\Column(type="datetime")
*/
private $createdAt;
/**
* #ORM\Column(type="datetime", nullable=true)
*/
private $updatedAt;
// Additional getters/setters removed for brevity
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
public function setCreatedAt(?\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getUpdatedAt(): ?\DateTimeInterface
{
return $this->updatedAt;
}
public function setUpdatedAt(?\DateTimeInterface $updatedAt): self
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* #ORM\PrePersist
*/
public function onPrePersist(): void
{
$this->createdAt = new \DateTime();
}
/**
* #ORM\PreUpdate
*/
public function onPreUpdate(): void
{
$this->updatedAt = new \DateTime();
}
}
When I create a new product within EasyAdmin, onPrePersist() is not called, and when I edit an existing product with EasyAdmin, onPreUpdate() is not called.
If I create a new product the "traditional" way, the lifecycle callbacks work exactly as expected. For example:
<?php
$product = new Product();
$product->setTitle('Test Product');
$product->setDescription('Test description');
// Doctrine lifecycle callbacks work as expected
$this->getDoctrine()->getManager()->persist($product);
Is EasyAdminBundle bypassing Doctrine lifecycle callbacks? If so, why? How do I utilize Doctrine lifecycle callbacks within Doctrine entities managed by EasyAdminBundle?
I understand there is documentation to do something similar with an AdminController:
https://symfony.com/doc/master/bundles/EasyAdminBundle/book/complex-dynamic-backends.html#update-some-properties-for-all-entities
But again, why do I need to do this when we already have Doctrine lifecycle callbacks. My other problem with using an AdminController and extending various methods, persistEntity() within AdminController is never called either.
What am I missing?
Any help would be greatly appreciated! Cheers!

Symfony Doctrine2 manyToMany relationship not removed - Specific to SQLite

I have several classes using a Taggable trait to set up a tag system common to several doctrine entities (Project, Note, ...).
The relationship between these entities and these tags is a ManyToMany relationship that I can not make multi-directional.
My problem: When I delete a Project entity, it is removed from the project table, but the relationships in the project_tag table between this project and the tags are not deleted. Then, if I create a new Project entity, an exception is thrown.
An exception exists while executing 'INSERT INTO project_tag (project_id, tag_id) VALUES (?,?)' With params [2, 4]:
SQLSTATE [23000]: Integrity constraint violation: 19 UNIQUE constraint failed: project_tag.project_id, project_tag.tag_id
Entities :
Tag
/**
* Tag
*
* #ORM\Table(name="tag")
* #ORM\Entity(repositoryClass="AppBundle\Repository\TagRepository")
*/
class Tag
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, unique=true)
*/
private $name;
/**
* #ORM\Column(name="last_use_at", type="datetime", nullable=false)
* #var \DateTime
*/
private $lastUseAt;
public function __construct()
{
$this->lastUseAt = new \DateTime();
}
public function __toString()
{
return $this->name;
}
/**
* Get id
*
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
*
* #return Tag
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName(): string
{
return $this->name;
}
/**
* #return \DateTime
*/
public function getLastUseAt(): \DateTime
{
return $this->lastUseAt;
}
/**
* #param \DateTime $lastUseAt
*/
public function setLastUseAt(\DateTime $lastUseAt)
{
$this->lastUseAt = $lastUseAt;
}
}
Taggable
trait Taggable
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Tag", cascade={"persist"})
*/
protected $tags;
/**
* Add tag
*
* #param Tag $tag
*
* #return $this
*/
public function addTag(Tag $tag)
{
$tag->setLastUseAt(new \DateTime());
$this->tags[] = $tag;
return $this;
}
/**
* Remove tag
*
* #param Tag $tag
*/
public function removeTag(Tag $tag)
{
$this->tags->removeElement($tag);
}
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
Project
/**
* Project
*
* #ORM\Table(name="project")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ProjectRepository")
*/
class Project
{
use Taggable;
}
Note
class Note
{
use Taggable;
}
Is this the only solution or is my annotation incomplete / incorrect?
I tried with JoinColumns, JoinTable and onDelete = "cascade" but nothing works.
In the meantime, I dodged the problem with this instruction placed before the suppresion.
$project->getTags()->clear();
Full code of the action in the controller :
/**
* #Route("/project/{id}/delete", name="project_delete")
*/
public function deleteAction($id) {
$em = $this->getDoctrine()->getManager();
$project = $em->getRepository('AppBundle:Project')->find($id);
if(!$project) {
return $this->redirectToRoute('index');
}
$project->getTags()->clear();
$em->remove($project);
$em->flush();
return $this->redirectToRoute('index');
}
I think I found a better solution: you can set the PRAGMA within Doctrine configuration. Like:
doctrine:
dbal:
# configure these for your database server
driver: 'pdo_sqlite'
#server_version: '5.7'
#charset: utf8mb4
#default_table_options:
#charset: utf8mb4
#collate: utf8mb4_unicode_ci
url: '%env(resolve:DATABASE_URL)%'
options:
'PRAGMA foreign_keys': 'ON'
I just tried it on my Symfony 4 application, re-created the database and tested using DB Browser for SQLite and it works as I expected.
Hope this helps
I managed to fix the problem. Here's my solution working for SQLite conections.
Create an eventListener listening on the kernel.request event :
namespace AppBundle\EventListener;
use Doctrine\Bundle\DoctrineBundle\Registry;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
class RequestListener
{
/**
* #var Registry
*/
private $doctrine;
public function __construct(Registry $doctrine)
{
$this->doctrine = $doctrine;
}
public function onKernelRequest(GetResponseEvent $event)
{
$this->doctrine->getConnection()->exec('PRAGMA foreign_keys = ON');
}
}
Service declaration
app.event_listener.request_listener:
class: AppBundle\EventListener\RequestListener
arguments:
- '#doctrine'
tags:
- { name: kernel.event_listener, event: kernel.request }
I think the problem is that you have your trait Taggable set as the owning side of the ManyToMany relationship but your are deleting the inverse side and expecting something to happen as a result. Doctrine will only check the owning side of the relationship in order to persist any changes. See here for docs on this.
You can solve by making the Taggable the inverse side of each of your relationships, or by manually telling doctrine to delete the owning side.
The first solution will probably not work for you since you won't (easily) specify multiple inverse sides. (Are you sure a trait is the right way to go for this??)
The second solution is easy. In your entities like Project for your deleteTag($tag) function, call a delete function on the owning side (e.g., deleteProject($project). You will have to create if one does not exist.
class Project
{
use Taggable;
public function deleteTag($tag)
{
$this->tags->removeElement($tag);
// persist on the owning side
$tag->deleteProject($this);
}
}
EDIT:
After seeing full code, it looks like you are deleting correctly. Now you need to tell doctrine to carry that through. See this post for full details, but basically you can change your trait to this:
trait Taggable
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\Tag",
* cascade={"persist"},
* onDelete="CASCADE"
* )
*/
protected $tags;
// ...
}

Doctrine ORM, ManyToMany - duplicates on lazy fetch

Symfony 2.8. When using default fetch mode duplicates are returned (why?), using fetch="EAGER" - everything is ok.
I have following objects:
/**
* #ORM\Entity()
*/
class User implements AdvancedUserInterface, \Serializable
{
(...)
/**
* #ORM\ManyToMany(targetEntity="Role", inversedBy="users")
* #ORM\JoinTable(name="user_role",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="role_id", referencedColumnName="id")}
* )
*/
private $role;
public function addRole(\WerbeoBundle\Entity\Role $role)
{
$this->role[] = $role;
return $this;
}
public function removeRole(\WerbeoBundle\Entity\Role $role)
{
$this->role->removeElement($role);
}
public function getRole()
{
return $this->role;
}
Role:
/**
* #ORM\Entity()
*/
class Role
{
(...)
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="role")
*/
private $users;
(... and getters/setters ...)
Now I have following table user_role:
user_id | role_id
1 | ADMIN
1 | EDITOR
When I call $user->getRole() result is
ADMIN
EDITOR
EDITOR
ADMIN
EDITOR
This happens in twig/controller only when using default fetch mode (lazy). When fetch="EAGER" everything is ok.
Any ideas what am I doing wrong?
Thanks
You have to check if an entry already exists before adding
public function __construct()
{
$this-role = new \Doctrine\Common\Collections\ArrayCollection()
}
public function addRole(\WerbeoBundle\Entity\Role $role)
{
if(!$this->role->contains($role)){
$this->role->add($role)
}
return $this;
}

Add brands through company, it's possible? How?

I have this two tables (see pics below) mapped as follow:
class Brand
{
...
/**
* #var Company
*
* #ORM\ManyToOne(targetEntity="Company")
* #ORM\JoinColumn(name="companies_id", referencedColumnName="id")
*/
protected $company;
}
class Company
{
...
}
I need to add support for add a new Brand from Company but I have not idea in how to achieve this. This are handled through SonataAdminBundle but I think I need to add something else to entities in order to create brands from company but I am not sure what this would be, can I get some help? I am stucked
1st attempt
After get an answer this is how I modify Company entity:
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
class Company
{
...
/**
* #var Brand
* #ORM\OneToMany(targetEntity="Brand", mappedBy="company", cascade={"persist"})
**/
protected $brands;
public function __construct()
{
$this->brands = new ArrayCollection();
}
...
public function getBrands()
{
return $this->brands;
}
/**
* Add brands
*
* #param Brand $brand
* #return Brands
*/
public function addBrand( Brand $brand)
{
$this->brands[] = $brand;
return $this;
}
/**
* Remove brands
*
* #param Brand $brand
*/
public function removeBrand( Brand $brand)
{
$this->brands->removeElement($brand);
}
}
But I am getting this error:
No entity manager defined for class
Doctrine\Common\Collections\ArrayCollection
Why is that?
You could try setting up your entities like this:
class Brand
{
/**
* #var Company
*
* #ORM\ManyToOne(targetEntity="Company", inversedBy="brands")
* #ORM\JoinColumn(name="companies_id", referencedColumnName="id")
*/
protected $company;
}
class Company
{
/**
* #var ArrayCollection
*
* #OneToMany(targetEntity="Brand", mappedBy="company", cascade={"persist"})
**/
protected $brands;
}
What we're defining here is that new Brands can be created from the Company entity with cascade={"persist"}.
It's recommended you implement addBrand and removeBrand in Company for direct interaction with the ArrayCollection.
A simple example of the final functionality:
$company = $service->getCompany(1); // our company entity
$brand = new Brand();
$brand->set...
...
$company->addBrand($brand);
$entityManager->persist($company);
EDIT
This is just an example, you may choose not to add with keys or even implement a remove function, but this is a starting point:
public function addBrand(Brand $brand)
{
// key needs to be something that can uniquely identify the brand
// e.g. name
$this->getBrands()->set(*key*, $brand);
return $this;
}
public function removeBrand($key)
{
$this->getBrands()->remove($key);
return $this;
}

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);
}
//...

Categories