Updating Entity OneToMany relationship by passing arraycollection - php

Im trying to change an old arraycolection of $linhas for a new one by using the method
setLinhas(Arraycollection $linhas)
but what happens when it does the changes is that internally he creates a new object with the new lines and dont update the old object with the new lines. It creates a new instance with the same values as the old object. It was suppose to update the same object and not create a new one!
Entity's Property :
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\LinhasPrecos", mappedBy="preco",orphanRemoval=true,cascade={"persist","merge"})
*/
protected $linhas;
/**
* #param $linhas
*/
public function setLinhas($linhas)
{
$this->linhas = new ArrayCollection($linhas);
}
In the service:
$oldObject->setLinhas($newObectWithNewLinhas->getLinhas());
$this->em->persist($oldObject);
but if I do the change manually it will work:
$oldLinhas = $oldObject->getLinhas()->getValues();
foreach($oldLinhas as $oldLinha)
{
$oldObject->removeLinha($oldLinha);
}
$linhaToCopy = $newObectWithNewLinhas->getLinhas()->getValues();
foreach($linhasCopyNew as $linhaCopyNew)
{
$oldObject->addLinha($linhaCopyNew);
}
thanks in advance!

You are doing it wrong!
use this constructor and setter instead:
Preco
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
*/
class Preco
{
//...
/**
* #var Collection
*
* #ORM\OneToMany(targetEntity="AppBundle\Entity\LinhasPrecos", mappedBy="preco", orphanRemoval=true, cascade={"persist","merge"})
*/
protected $linhas;
//...
public function __construct()
{
$this->linhas = new ArrayCollection();
}
public function setLinhas($linhas)
{
$this->linhas = $linhas;
}
}
Notice
You should pass a doctrine collection into setLinhas.
This way you are totally replacing an old collection, with the new collection (and not adding an element to the old collection).

Related

Why is Id AutoGenerated anew on UPDATE with Relation using Symfony Doctrine?

Symfony 5.3
Doctrine bundle ^2.4 ORM ^2.9
MariaDB 10.6.4
This has been rather difficult to diagnose especially as I was dealing with some complicated layered code. If I was sure, I would have filed a bug with Doctrine, but I want to first make sure I'm not making some glaring mistake or such, in implementation.
I have painstakingly tried to reduce the code to a simplified working example. On my test database tables, there are additional columns that are not referred to in the demo code.
// src/Entity/Record.php
declare(strict_types = 1);
namespace App\Entity;
use App\Repository\RecordRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RecordRepository::class)
* #ORM\Table(name="Records")
*/
class Record {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
public function getId(): ?int {
return $this->id;
}
/**
* #ORM\OneToOne(targetEntity="App\Entity\RecordStatus", mappedBy="record", cascade={"persist"})
* #ORM\JoinColumn(name="Id")
*/
private ?RecordStatus $recordStatus = NULL;
public function getRecordStatus(): ?RecordStatus {
return $this->recordStatus;
}
public function setRecordStatus(RecordStatus $value): void {
$value->setRecord($this);
$this->recordStatus = $value;
}
}
// src/Entity/RecordStatus.php
declare(strict_types = 1);
namespace App\Entity;
use App\Repository\RecordStatusRepository;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass=RecordStatusRepository::class)
* #ORM\Table(name="Record_Statuses")
*/
class RecordStatus {
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
protected $id;
public function getId(): ?int {
return $this->id;
}
/**
* #ORM\OneToOne(targetEntity="App\Entity\Record", inversedBy="record")
* #ORM\JoinColumn(name="RecordId")
*/
private Record $record;
public function getRecord(): Record {
return $this->record;
}
public function setRecord(Record $value): void {
$this->record = $value;
}
}
// src/Controller/DefaultController.php
declare(strict_types = 1);
namespace App\Controller;
use App\Entity\Record;
use App\Entity\RecordStatus;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DefaultController extends AbstractController {
/**
* #Route("/")
*/
public function home(): Response {
$doctrine = $this->getDoctrine();
$entityManager = $doctrine->getManager();
$recordRepository = $doctrine->getRepository(Record::class);
$recordStatus = new RecordStatus;
$item = $recordRepository->find(1);
$item->setRecordStatus($recordStatus);
$entityManager->flush();
return new JsonResponse(['id' => $item->getId()]);
}
}
When this route ("/") is triggered, the record is updated and the old Id is displayed. But after the update, checking the database shows that the record now has a newly Auto-Generated Id, that comes after the last previous record in the table. Note that this is on UPDATE and not INSERT.
My current partial workaround is to load the currently mapped relation if one exists and update it instead of using new RecordStatus, but it should also be possible to use a new Related instance if required (especially if there was no Relation assigned on first insert).

Reading annotations with Symfony4

I'm trying to read annotations with Symfony4 but looks like something is not working!
The class I'm trying to read from:
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
/**
* #ORM\Entity(repositoryClass="App\Repository\OAuthClientRepository")
*/
class OAuthClient {
{
/**
* #Assert\NotBlank()
* #ORM\Column(type="string")
*/
protected $name;
}
The code I'm using to read the annotations:
<?php
namespace App\Test;
use Doctrine\Common\Annotations\SimpleAnnotationReader as DocReader;
/**
* Helper AnnotationReader
*/
class AnnotationReader
{
/**
* #param $class
* #return null|\StdClass
*/
public static function getClass($class)
{
$reader = new DocReader;
$reflector = new \ReflectionClass($class);
return $reader->getClassAnnotations($reflector);
}
/**
* #param $class
* #param $property
* #return array
*/
public static function getProperty($class, $property)
{
$reader = new DocReader;
$reflector = new \ReflectionProperty($class, $property);
return $reader->getPropertyAnnotations($reflector);
}
/**
* #param $class
* #param $method
* #return array
*/
public static function getMethod($class, $method)
{
$reader = new DocReader;
$reflector = new \ReflectionMethod($class, $method);
return $reader->getMethodAnnotations($reflector);
}
}
I get empty arrays when I call:
App\Test\AnnotationReader::getClass(App\Entity\OAuthClient::class);
App\Test\AnnotationReader::getProperty(App\Entity\OAuthClient::class, 'name');
What am I doing wrong?
What is the best way to read annotation?
I'm looking to read the validations used on a class property.
Thank you for your help!
change
use Doctrine\Common\Annotations\SimpleAnnotationReader as DocReader;
to
use Doctrine\Common\Annotations\AnnotationReader as DocReader;
and it works.
You may have to call the addNamespace() method on the SimpleAnnotationReader instance.
For instance, for ORM annotations:
$reader->addNamespace('Doctrine\ORM\Mapping');
And for validation annotations:
$reader->addNamespace('Symfony\Component\Validator\Constraints');
See:
SimpleAnnotationReader API: https://www.doctrine-project.org/api/annotations/latest/Doctrine/Annotations/SimpleAnnotationReader.html
SimpleAnnotationReader examples: https://github.com/doctrine/doctrine2/blob/462173ad71ae63cd9877e1e642f7968ed1f9971b/lib/Doctrine/ORM/Configuration.php#L140-L141

Symfony 3 easyadmin __toString() must not throw an exception

I'm using EasyAdmin Bundle. When I'm trying to add a new element in Entity named "Company" which have 'ManyToMany' relation with "Service" entity I'm getting an error:
Error: Method AppBundle\Entity\Service::__toString() must not throw an exception
But when I'm going to add a new element in "Service" entity, everything works fine and the field with "Company" entities is displaying correctly.
I was trying to catch the exception implementing this workaround, but It doesn't take effect.
The Service class:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Company
*
* #ORM\Table(name="company")
* #ORM\Entity(repositoryClass="AppBundle\Repository\CompanyRepository")
*/
class Company
{
/**
* #var
*
* Many Companys have Many Services.
* #ORM\ManyToMany(targetEntity="Service", inversedBy="companys")
* #ORM\JoinTable(name="companys_services")
*/
private $services;
public function __construct() {
$this->services = new ArrayCollection();
}
public function __toString()
{
return $this->name;
}
}
And the Service class:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Service
*
* #ORM\Table(name="service")
* #ORM\Entity(repositoryClass="AppBundle\Repository\ServiceRepository")
*/
class Service
{
/**
* Many Services have Many Companys.
* #ORM\ManyToMany(targetEntity="Company", mappedBy="services")
*/
private $companys;
public function __construct()
{
$this->companys = new ArrayCollection();
}
public function __toString()
{
return (string) $this->name;
}
}
What is wrong?
The error message is so specific (Service::__toString() must not throw an exception) that the problem must be in the $this->name property of Service. Is it defined? Is a "normal" property or some advanced object which fails when casting it into a string?

Symfony 3 Doctrine 2: Circular reference on relations

I'm trying to get working 4 entities in Symfony 3 with Doctrine 2 but I'm stuck on a circular reference exception when I want to serialize an Account entity for example:
A circular reference has been detected (configured limit: 1).
I chose bi-directional relations in my entities and schema is like this:
- Account [1] ---- [0..*] AccountSheet
- AccountSheet [1] ---- [0..*] Operation
- Operation [0..*] ---- [1] Category
Here are entities (with some cleanings for clarity):
src\AppBundle\Entity\Account.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="accounts",
* uniqueConstraints={#ORM\UniqueConstraint(name="accounts_name_unique",columns={"name"})})
*/
class Account extends AbstractGenericEntity{
/**
* #ORM\OneToMany(targetEntity="AccountSheet", mappedBy="account")
* #var AccountSheet[]
*/
protected $accountSheets;
public function __construct($name = null, $description = null){
$this->accountSheets = new ArrayCollection();
$this->name = $name;
$this->description = $description;
}
}
src\AppBundle\Entity\AccountSheet.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="accounts_sheets",
* uniqueConstraints={#ORM\UniqueConstraint(name="accountsheet_account_unique", columns={"name", "account_id"})})
* #ORM\HasLifecycleCallbacks
*/
class AccountSheet extends AbstractGenericEntity{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Account", inversedBy="accountSheets")
* #var Account
*/
protected $account;
/**
* #ORM\OneToMany(targetEntity="Operation", mappedBy="accountSheet")
* #var Operation[]
*/
protected $operations;
public function __construct($name = null){
$this->operations = new ArrayCollection();
$this->name = $name;
}
}
src\AppBundle\Entity\Operation.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="operations")
*/
class Operation extends AbstractGenericEntity{
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\AccountSheet", inversedBy="operations")
* #ORM\JoinColumn(nullable=false)
* #var AccountSheet
*/
protected $accountSheet;
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Category", inversedBy="operations")
* #var Category
*/
protected $category;
public function __construct($type = null, $label = null, $montant = null, $comment = null){
$this->label = $label;
$this->type = $type;
$this->comment = $comment;
$this->montant = $montant;
}
}
src\AppBundle\Entity\Category.php
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="categories")
*/
class Category extends AbstractGenericEntity{
/**
* #ORM\Column(type="string")
*/
protected $label;
/**
* #ORM\Column(type="string")
*/
protected $description;
/**
* #ORM\OneToMany(targetEntity="Operation", mappedBy="category")
* #var Operation[]
*/
protected $operations;
public function __construct($name = null){
$this->operations = new ArrayCollection();
$this->name = $name;
}
}
I guess it's on the Operation entity, where AccountSheet is referenced again. The bi-directional on operation is not really needed.
How could I rearrange this?
Thanks!
From the official documentation :
Circular references are common when dealing with entity relations
To avoid infinite loops, GetSetMethodNormalizer throws a CircularReferenceException when such a case is encountered:
$member = new Member();
$member->setName('Kévin');
$org = new Organization();
$org->setName('Les-Tilleuls.coop');
$org->setMembers(array($member));
$member->setOrganization($org);
echo $serializer->serialize($org, 'json'); // Throws a CircularReferenceException
So, from this point, you have 3 solutions to get rid of this issue :
Set a circular reference handler :
Instead of throwing an exception, circular references can also be handled by custom callables. This is especially useful when serializing entities having unique identifiers:
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getName();
});
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($org, 'json'));
// {"name":"Les-Tilleuls.coop","members":[{"name":"K\u00e9vin", organization: "Les-Tilleuls.coop"}]}
Set ignored attributes (not my preferred solution) :
in your case :
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
normalizer->setIgnoredAttributes(array("account", "accountSheet", "category", "operation"));
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($org, 'json'));
Use group attributes (my preferred solution) :
This method is similar to setting ignored attributes because you will chose which attribute you want to serialize by adding the group annotation on it and the rest will be ignored for recursivity during normalization process.
Using Serialization Groups Annotations
Attributes Groups
In your case with the Account entity for example do this on the account side :
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use AppBundle\Entity\AbstractGenericEntity;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity()
* #ORM\Table(name="accounts",
* uniqueConstraints={#ORM\UniqueConstraint(name="accounts_name_unique",columns={"name"})})
*/
class Account extends AbstractGenericEntity{
/**
* #ORM\OneToMany(targetEntity="AccountSheet", mappedBy="account")
* #var AccountSheet[]
* #Groups({"account"})
*/
protected $accountSheets;
public function __construct($name = null, $description = null){
$this->accountSheets = new ArrayCollection();
$this->name = $name;
$this->description = $description;
}
}
Then do not put this group annotation on the $account field in the AccountSheet entity to get rid of the circular reference issue.
Finally you serialize your Account :
$encoder = new JsonEncoder();
$normalizer = new ObjectNormalizer();
$serializer = new Serializer(array($normalizer), array($encoder));
var_dump($serializer->serialize($account, 'json', array('groups' => array('account')) ));
$jsonContent = $serializer->serialize($yourObject, 'json', [
'circular_reference_handler' => function ($object) {
return $object->getId();
}
]);
Above code works for me to fix circular reference exception. (Symfony >=4.2)

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