I'm coding an API in Symfony with API Platform and I have an issue when I persist a relation of my object.
I have few entities. Entity Lead can have few LeadJob and for each LeadJob I create a Project. I use a Subscriber to trigger those creations.
Project relation of entity LeadJob :
/**
* #ORM\OneToOne(targetEntity=Project::class, inversedBy="leadJob", cascade={"persist", "remove"})
*/
private $project;
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): self
{
$this->project = $project;
return $this;
}
LeadJob relation of entity Project :
/**
* #ORM\OneToOne(targetEntity=LeadJob::class, mappedBy="project", cascade={"persist", "remove"})
*/
private $leadJob;
public function getLeadJob(): ?LeadJob
{
return $this->leadJob;
}
public function setLeadJob(?LeadJob $leadJob): self
{
$this->leadJob = $leadJob;
// set (or unset) the owning side of the relation if necessary
$newProject = null === $leadJob ? null : $this;
if ($leadJob->getProject() !== $newProject) {
$leadJob->setProject($newProject);
}
return $this;
}
And the Subscriber that create the Project :
final class LeadCreateProjectSubscriber implements EventSubscriber
{
protected EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
public function getSubscribedEvents(): array
{
return [
Events::postPersist,
Events::postUpdate,
];
}
public function postPersist(LifecycleEventArgs $event): void
{
$entity = $event->getObject();
if (!$entity instanceof Lead) {
return;
}
$this->createProject($entity);
}
public function postUpdate(LifecycleEventArgs $event): void
{
$entity = $event->getObject();
if (!$entity instanceof Lead) {
return;
}
$this->createProject($entity);
}
private function createProject(Lead $lead): void
{
if (LeadStatusEnum::FINISHED !== $lead->getStatus()) {
return;
}
foreach ($lead->getLeadJobs() as $leadJob) {
$project = (new Project())
->setOwner($leadJob->getUser())
->addUser($lead->getOwner())
->setOrganization($lead->getOrganization())
->setContact($lead->getContact())
->setName('Lead '.$lead->getSource()->getName())
->setJob($leadJob->getJob())
->setLeadJob($leadJob) //this line that causes the error
->setComment($lead->getDescription());
$this->entityManager->persist($project);
}
$this->entityManager->flush();
}
}
So, when I trigger the creation of an Project with everything I need, I have this error message thrown from my Subscriber. There is some properties that I didn't notice, this is the raw error message :
"An exception occurred while executing 'INSERT INTO lead_job (id, deleted_at, created_at,
updated_at, job_id, user_id, lead_id, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)'
with params [\"eafb3b13-bc14-4eb8-92e8-cf3acc55719e\", \"2021-07-22
16:54:45\"]:\n\nSQLSTATE[08P01]: <<Unknown error>>: 7 ERROR: bind message supplies 2 parameters, but prepared statement \"\" requires 8"
The only way that work is to persist the project, flush, set the relation and persist it in the Subscriber. And delete the setLeadJob on the Project object :
$this->entityManager->persist($project);
$this->entityManager->flush();
$leadJob->setProject($project);
$this->entityManager->persist($leadJob);
$this->entityManager->flush();
Why the cascade persist is not doing the job? And why I have this error?
From the Doctrine documentation:
postUpdate, postRemove, postPersist
The three post events are called
inside EntityManager#flush(). Changes in here are not relevant to the
persistence in the database, but you can use these events to alter
non-persistable items, like non-mapped fields, logging or even
associated classes that are not directly mapped by Doctrine.
I think this is not very clear, so in other words: don't persist (other) entities inside a postPersist listener. Because postPersist happens during the flush event, new persisted entities (like your Project) aren't flushed. And flushing during a flush event leads to unexpected behaviour, like the error in your question.
You should use a onFlush event instead:
class FlushExampleListener
{
public function onFlush(OnFlushEventArgs $eventArgs)
{
$em = $eventArgs->getEntityManager();
$uow = $em->getUnitOfWork();
foreach ($uow->getScheduledEntityInsertions() as $entity) {
if ($entity instanceof Lead) {
$this->createProject($entity);
}
}
}
private function createProject(Lead $lead): void
{
// your logic here
}
}
Related
I'm working on a Symfony 6 project and I'm using sqlite as db.
I have a ManyToOne relation between two entities: Neighborhood and PropertyForSale.
When I delete a Neighborhood I want the $Neighborhood field of PropertyForSale to be set to null so I added:
#[ORM\JoinColumn(onDelete: 'SET NULL')] to the property:
#[ORM\ManyToOne(inversedBy: 'propertiesForSale')]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Neighborhood $neighborhood = null;
Everything seems to work properly if I change the database to MySql but with Sqlite this attribute seems to be ignored. I know has something to do with the default foreign key behavior in sqlite and
PRAGMA foreign_keys = ON; should be executed but I canĀ“t find I way to make it work with Symfony and Doctrine; Any ideas?
I share a bigger portion of my code:
// PropertyForSale.php
#[ORM\Entity(repositoryClass: PropertyForSaleRepository::class)]
class PropertyForSale
{
// ...
#[ORM\ManyToOne(inversedBy: 'propertiesForSale')]
#[ORM\JoinColumn(onDelete: 'SET NULL')]
private ?Neighborhood $neighborhood = null;
// ...
public function getNeighborhood(): ?Neighborhood
{
return $this->neighborhood;
}
public function setNeighborhood(?Neighborhood $neighborhood): self
{
$this->neighborhood = $neighborhood;
return $this;
}
}
// Neighborhood.php
#[ORM\Entity(repositoryClass: NeighborhoodRepository::class)]
class Neighborhood
{
// ...
#[ORM\OneToMany(mappedBy: 'neighborhood', targetEntity: PropertyForSale::class)]
private Collection $propertiesForSale;
// ...
public function getPropertiesForSale(): Collection
{
return $this->propertiesForSale;
}
public function addPropertiesForSale(PropertyForSale $propertiesForSale): self
{
if (!$this->propertiesForSale->contains($propertiesForSale)) {
$this->propertiesForSale->add($propertiesForSale);
$propertiesForSale->setNeighborhood($this);
}
return $this;
}
public function removePropertiesForSale(PropertyForSale $propertiesForSale): self
{
if ($this->propertiesForSale->removeElement($propertiesForSale)) {
// set the owning side to null (unless already changed)
if ($propertiesForSale->getNeighborhood() === $this) {
$propertiesForSale->setNeighborhood(null);
}
}
return $this;
}
}
The only workaround I found was to add an event listener on the entity preRemove event and manually set to null the relation:
// NeighborhoodListener
namespace App\EventListener;
use Doctrine\ORM\EntityManagerInterface;
class NeighborhoodListener
{
public function __construct(private EntityManagerInterface $entityManager) {}
public function preRemove($args) {
$properties = $args->getObject()->getPropertiesForSale();
foreach ($properties as $property) {
$property->setNeighborhood(null);
$this->entityManager->persist($property);
}
$this->entityManager->flush();
}
}
I am still new to Symfony and can't figure out this error.
Basically I try to remove the byUser from my database on the table wish. When the flush() function is executed I get the error # unable to fetch the response from the backend: unexpected EOF
My controller:
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/wish/delete/{id}', name: 'delete_wish')]
public function deleteWish(Wish $wish): Response
{
$user = $this->getUser();
$user->removeWish($wish);
$this->entityManager->persist($wish);
$this->entityManager->flush();
$this->addFlash("success", "Successfully removed the wish!");
return $this->redirectToRoute('wishlist');
}
From user.php:
public function removeWish(Wish $wish): self
{
if ($this->wishes->removeElement($wish)) {
// set the owning side to null (unless already changed)
if ($wish->getByUser() === $this) {
$wish->setByUser(null);
}
}
return $this;
}
Dev server logs:
[2021-05-07T15:16:45.680666+00:00] doctrine.DEBUG: "START TRANSACTION" [] []
[2021-05-07T15:16:45.684106+00:00] doctrine.DEBUG: UPDATE wish SET by_user_id = ? WHERE id = ? [null,3] []
My code worked before and I did not change anything on purpose. So I am quite lost where to start looking. Would anyone have some idea?
Try edit your user entity atribute wish like this: The main is onDelete="SET NULL"
class User
{
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Wish")
* #ORM\JoinColumn(name="wish_id", referencedColumnName="id", onDelete="SET NULL")
*/
protected $wish;
}
Then you can delete
$this->entityManager->remove($wish);
$this->entityManager->flush();
I dont know how looks removeWish($wish); but you can try:
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/wish/delete/{id}', name: 'delete_wish')]
public function deleteWish(Wish $wish): Response
{
$user = $this->getUser();
$this->entityManager->remove($wish);
$this->entityManager->flush();
$this->addFlash("success", "Successfully removed the wish!");
return $this->redirectToRoute('wishlist');
}
I am trying to monitor what happens in my app. For this I created this class:
class ChangeLogListener implements EventSubscriber
{
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
'onDelete',
);
}
public function postPersist(LifecycleEventArgs $args)
{
# Avoid to log the logging process
if (!$args->getEntity() instanceof ChangeLog)
$this->createLog($args, 'creation');
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->createLog($args, 'update');
}
public function preRemove(LifecycleEventArgs $args)
{
# Handle the log creation
$this->createLog($args, 'remove');
}
public function createLog(LifecycleEventArgs $args, $action)
{
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $args->getEntity();
$user = $this->tokenStorage->getToken()->getUser();
$changes[] = $uow->getEntityChangeSet($entity);
$cl = new ChangeLog();
$cl->setDate(new \DateTime());
$cl->setUser($user);
$cl->setEntityName(get_class($entity));
$cl->setEntityId($entity->getId());
$cl->setAction($action);
$cl->setChangeset($changes);//<---change this
$cl->setDescription('');
$em->persist($cl);
$em->flush();
}
}
But I have some problems with changes, I don't know how I can correctly write them into the DB and make them readable. Maybe there are some methods to do this correctly?
When I try to flush changes, I always get error:
String data, right truncated: 1406 Data too long for column
'change_set' at row 1"
My ChangeLog.orm.yml:
#...
fields:
changeSet:
type: array
#...
Do a var_dump($changes);die(); after your getEntityChangeSet() line and figure out what you actually want from that data set. It is currently too long for the field you are trying to save it in. It will need to be reduced.
I have this EventSubscriber:
class ChangeLogListener implements EventSubscriber
{
private $tokenStorage;
private $str,$str1;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function getSubscribedEvents()
{
return array(
'postPersist',
'postUpdate',
'onDelete',
);
}
public function postPersist(LifecycleEventArgs $args)
{
if (!$args->getEntity() instanceof ChangeLog)
$this->createLog($args, 'creation');
}
public function postUpdate(LifecycleEventArgs $args)
{
$this->createLog($args, 'update');
}
public function preRemove(LifecycleEventArgs $args)
{
$this->createLog($args, 'remove');
}
public function createLog(LifecycleEventArgs $args, $action)
{
# Entity manager
$em = $args->getEntityManager();
$uow = $em->getUnitOfWork();
$entity = $args->getEntity();
# Get user
$user = $this->tokenStorage->getToken()->getUser();
#Get changes
$changes = $uow->getEntityChangeSet($entity);
$cl = new ChangeLog();
$cl->setDate(new \DateTime());
$cl->setUser($user);
$cl->setEntityName(get_class($entity));
$cl->setEntityId($entity->getId());
$cl->setAction($action);
$cl->setDescription($log);
$cl->setChangeset($changes);
$em->persist($cl);
$em->flush();
}
}
And when I want to POST item, some data must be recorded to db. After all actions I receive this in change_set in my db:
a:3:{s:5:"value";a:2:{i:0;N;i:1;s:3:"120";}s:4:"item";a:2:{i:0;N;i:1;O:21:"AppBundle\Entity\Item":6:{s:25:"AppBundle\Entity\Itemid";i:127;s:27:"AppBundle\Entity\Itemname";s:7:"newitem";s:13:"*categories";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:2:{i:0;O:25:"AppBundle\Entity\Category":7:{s:29:"AppBundle\Entity\Categoryid";i:2;s:31:"AppBundle\Entity\Categoryname";s:10:"child
to
1";s:33:"AppBundle\Entity\Categoryparent";O:40:"Proxies__CG__\AppBundle\Entity\Category":8:{s:17:"isInitialized";b:0;s:29:"AppBundle\Entity\Categoryid";i:1;s:31:"AppBundle\Entity\Categoryname";N;s:33:"AppBundle\Entity\Categoryparent";N;s:35:"AppBundle\Entity\Categorychildren";N;s:8:"*items";N;s:36:"AppBundle\Entity\CategorycreatedAt";N;s:36:"AppBundle\Entity\CategoryupdatedAt";N;}s:35:"AppBundle\Entity\Categorychildren";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:8:"*items";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:36:"AppBundle\Entity\CategorycreatedAt";N;s:36:"AppBundle\Entity\CategoryupdatedAt";N;}i:1;O:25:"AppBundle\Entity\Category":7:{s:29:"AppBundle\Entity\Categoryid";i:4;s:31:"AppBundle\Entity\Categoryname";s:8:"child1.1";s:33:"AppBundle\Entity\Categoryparent";r:13;s:35:"AppBundle\Entity\Categorychildren";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:8:"*items";O:33:"Doctrine\ORM\PersistentCollection":2:{s:13:"*collection";O:43:"Doctrine\Common\Collections\ArrayCollection":1:{s:53:"Doctrine\Common\Collections\ArrayCollectionelements";a:0:{}}s:14:"*initialized";b:0;}s:36:"AppBundle\Entity\CategorycreatedAt";N;s:36:"AppBundle\Entity\CategoryupdatedAt";N;}}}s:14:"*initialized";b:1;}s:13:"*attributes";N;s:32:"AppBundle\Entity\ItemcreatedAt";O:8:"DateTime":3:{s:4:"date";s:26:"2018-03-19
10:22:47.000000";s:13:"timezone_type";i:3;s:8:"timezone";s:3:"UTC";}s:32:"AppBundle\Entity\ItemupdatedAt";N;}}s:9:"attribute";a:2:{i:0;N;i:1;O:26:"AppBundle\Entity\Attribute":3:{s:30:"AppBundle\Entity\Attributeid";i:96;s:33:"AppBundle\Entity\Attributealias";s:5:"price";s:32:"AppBundle\Entity\Attributename";s:5:"price";}}}
But I think this data is not readable.I think I need to parse received data before writing it into db, but I don't understand how to parse this into readable format, something like this:
name: Old Value: 12 => New Value: 121, updatedAt: Old Value:
2018-03-20 05:51:44 => New Value: 2018-03-20 08:36:12 and other
Any idea how to parse this?
You are directly inserting all work done on entities with whole object, that's why you are saving all the meta-data into db. Better to doctrine customized extension to handle this (doctrine-extensions and see Loggable behavioral extension for Doctrine2) or if you want to create self customized ChangeLogListner then use methods to compute or get exact change-Set using doctrine methods. to methods see here.
you can change your EventListner code something like this:
$em = $this->getDoctrine()->getManager();
$entity = $em->find('My\Entity', 1);
$entity->setTitle('Changed Title!');
$uow = $em->getUnitOfWork();
$uow->computeChangeSets(); // do not compute changes if inside a listener
$changeset = $uow->getEntityChangeSet($entity);
or check Is there a built-in way to get all of the changed/updated fields in a Doctrine 2 entity
if you are trying inside EventListner then try inside particular events like:
public function preUpdate(Event\LifecycleEventArgs $eventArgs)
{
$changeArray = $eventArgs->getEntityChangeSet();
//do stuff with the change array
}
I have a onPreSubmit in my Symfony2 FormType.
public function onPreSubmit(FormEvent $event)
{
$data = $event->getData();
// Avoid sending empty RegisterProduct
foreach ($data['registerProducts'] as $key => $registerProduct) {
if (empty($registerProduct['quantity'])) {
unset($data['registerProducts'][ $key ]);
}
}
$event->setData($data);
}
The purpose is to remove an array value if a certain fiels in this one is empty (which works)
But when I set the data I have this error:
An exception occurred while executing 'INSERT INTO register_product .....
Integrity constraint violation: 1048 Column 'register_id' cannot be null
I don't understand why.
EDIT: I tried with onPostSubmit, but still have the same error
public function onPostSubmit(FormEvent $event)
{
/** #var Register $register */
$register = $event->getForm()->getData();
foreach ($register->getRegisterProducts() as $registerProduct) {
if ($registerProduct->getQuantity() < 1) {
$register->removeRegisterProduct($registerProduct);
}
}
}
As say in comments, you should do it in postSubmit instead of preSubmit.
Example :
public function onPostSubmit(FormEvent $event) {
$data = $event->getData();
foreach ($data['registerProducts'] as $key => $registerProduct) {
if (empty($registerProduct['quantity'])) {
unset($data['registerProducts'][$key]);
}
}
});
If the SQL error persists, try to replace $event->getData() by $event->getForm()->getData() , the first returns data from the client.
EDIT
Unfortunately, the FormEvent doesn't work.
Also, I'll propose to use the prePersist and preUpdate hooks in your Entity.
It may not be an alternative for you if your question is only about the form context.
Usage :
/**
* #ORM\HasLifecycleCallbacks
* #ORM\Entity
*/
class YourEntity
{
// ...
/**
*
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function manageProducts()
{
foreach ($this->getRegisterProducts() as $registerProduct) {
if ($registerProduct->getQuantity() < 1) {
$this->removeRegisterProduct($registerProduct);
}
}
}
}
See the Events part of doctrine documentation.
I hope you can use this solution and that it works.