How to be warned a many-to-many relation gonna be update? - php

I want to do a specific treatment when a specific field is updated.
The obvious way is to do it with event preUpdate, and see what fields are updated. It works fine ... except for a many-to-many field. It triggers the event, but the ChangeSet is empty.
/**
* #ORM\PreUpdate
*/
public function updateDate(PreUpdateEventArgs $event){
$changeSet = $event->getEntityChangeSet();
$res = "";
foreach($changeSet as $key => $change){
$line = $key." : ".$event->getOldValue($key)." || ".$event->getNewValue($key);
$res .= $line;
}
}
In $res all my fields are modified except for the many-to-many field.
Also, I'm trying to do it in a listener, but I can't find how to extract the fields which are updated from the entityManager.
Thank you.
More informations :
Relation from the update entity :
/**
* #var Status
*
* #ORM\ManyToMany(targetEntity="User", inversedBy="projectsSupervisor", cascade={"persist"})
* #ORM\JoinTable(name="projects_supervisors")
*/
protected $supervisors;
From the other side :
/**
* #var Project
*
* #ORM\ManyToMany(targetEntity="Task", mappedBy="users")
*/
protected *tasks
Symfony version : 3.1.10

It is not possible to track changes made to an many-to-many-association. See here :
Changes made only to the inverse side of an association are ignored.
Make sure to update both sides of a bidirectional association (or at
least the owning side, from Doctrine’s point of view)
Additionally, ::getEntityChangeSet() is only useful for regular fields, not associations. For One-To-Many-Associations, you can use $unitOfWork->getScheduledCollectionUpdates() :
foreach ($uow->getScheduledCollectionUpdates() as $collectionUpdate) {
/** #var $collectionUpdate \Doctrine\ORM\PersistentCollection */
if ($collectionUpdate->getOwner() === $entity) {
// This entity has an association mapping which contains updates.
$collectionMapping = $collectionUpdate->getMapping();
print_r($collectionMapping); // Investigate this further
}
}
A practical example is viewable in my github repository "DoctrineWatcher" which does exactly the same (line 196+).

Related

Doctrine: Custom repository to determine UniqueEntity does not work

In order to solve a problem I asked about earlier, I am trying to create a custom repository function that will determine whether an instance of Repair is unique, based on the device, name, and colors constraints.
Here's my Doctrine Annotation for class Repair. Mind that the device property is Many To One (many Repairs for one Device), and that colors is Many to Many.
/**
* #ORM\Table(name="repair")
* #ORM\Entity(repositoryClass="AppBundle\Repository\RepairRepository")
* #UniqueEntity(fields={"name", "device", "colors"}, repositoryMethod="getSimilarRepairs", message="Repair {{ value }} already exists for this name, device and colour combination.")
*/
This is my RepairRepository.php, in which $criteria['colors'] is an array.
public function getSimilarRepairs(array $criteria) {
$builder = $this->createQueryBuilder('r')
->where('r.device = :device')
->andWhere('r.colors = :colors')
->andWhere('r.name = :name')
->setParameters(['deviceid'=>$criteria['device'],'colors'=>$criteria['colors'],'name'=>$criteria['name']]);
return $builder->getQuery();
}
I have three problems that can probably be brought back to one:
editing: with every change, causing a duplicate or not, I get the message that a duplicate entity exists.
editing: despite the error message, name changes are performed anyway!
adding: I can create as many duplicates as I like, there never is an error message.
Your problem is that the colors relation is a ManyToMany.
In SQL you can not query '=' on this relation.
It is very complicated, that's why Doctrine (and we probably) can't make it alone .
A partial solution to build a query :
public function getSimilarRepairs(array $criteria) {
$builder = $this->createQueryBuilder('r')
->where('r.device = :device')
->andWhere('r.name = :name')->setParameter('name',$criteria['name'])
->andWhere('r.colors = :colors')->setParameter('deviceid',$criteria['device']);
// r matches only if each of your colors exists are related to r :
$i=0;
foreach($criteria['colors'] as $color){
$i++;
$builder->join('r.colors','c'.$i)->andWhere('c = :color'.$i)->setParameter('color'.$i,$color);
}
// Then you had also to check than there is no other color related to r :
// I don't know how
return $builder->getQuery();
}
But let me propose another solution :
In your repair entity, your can store a duplicate of your related colours :
/**
* #var string
*
* #ORM\Column(name="name_canonical", type="string")
*/
private $serializedColors;
set it with doctrine lifecycle events :
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateColors()
{
$serializedColors = '';
foreach($this->colors as $color){
$serializedColors .= $color->getId().'#';
}
$this->serializedColors = $serializedColors;
}
Don't forget to add #HasLifecycleCallbacks
Then change your UniqueEntityConstraint to fields={"name", "device", "serializedColors"}, forget the custom query, and it will work.

Symfony Doctrine One to Many does not insert foreign key

I am having annoying problems with persisting an entity with one or more OneToMany-Childs.
I have a "Buchung" entity which can have multiple "Einsatztage" (could be translated to an event with many days)
In the "Buchung entity I have
/**
* #param \Doctrine\Common\Collections\Collection $property
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung", cascade={"all"})
*/
private $einsatztage;
$einsatztage is set to an ArrayCollection() in the __constructor().
Then there is the "Einsatztag" Entity which has a $Buchung_id variable to reference the "Buchung"
/**
* #ORM\ManyToOne(targetEntity="Buchung", inversedBy="einsatztage", cascade={"all"})
* #ORM\JoinColumn(name="buchung_id", referencedColumnName="id")
*/
private $Buchung_id;
Now If I try to persist an object to the database the foreign key of the "Einsatztag" Table is always left empty.
$buchung = new Buchung();
$buchung->setEvent( $r->request->get("event_basis"));
$buchung->setStartDate(new \DateTime($r->request->get("date_from")));
$buchung->setEndDate(new \DateTime($r->request->get("date_to")));
$von = $r->request->get("einsatz_von");
$bis = $r->request->get("einsatz_bis");
$i = 0;
foreach($von as $tag){
$einsatztag = new Einsatztag();
$einsatztag->setNum($i);
$einsatztag->setVon($von[$i]);
$einsatztag->setBis($bis[$i]);
$buchung->addEinsatztage($einsatztag);
$i++;
}
$em = $this->getDoctrine()->getManager();
$em->persist($buchung);
foreach($buchung->getEinsatztage() as $e){
$em->persist($e);
}
$em->flush();
Firstly, you have to understand that Doctrine and Symfony does not work with id's within your entities.In Einsatztag entity, your property should not be called $Buchung_id since it's an instance of buchung and not an id you will find out there.
Moreover, in your loop, you add the Einsatztag to Buchung. But do you process the reverse set ?
I do it this way to always reverse the set/add of entities.
Einsatztag
public function setBuchung(Buchung $pBuchung, $recurs = true){
$this->buchung = $pBuchung;
if($recurs){
$buchung->addEinsatztag($this, false);
}
}
Buchung
public function addEinsatztag(Einsatztag $pEinsatztag, $recurs = true){
$this->einsatztages[] = $pEinsatztag;
if($recurs){
$pEinsatztag->setBuchung($this, false);
}
}
Then, when you will call
$buchung->addEinsatztag($einsatztag);
Or
$einsatztag->set($buchung);
The relation will be set on both side making your FK to be set. Take care of this, you'll have some behavior like double entries if you do not use them properly.
SImplier , you can use default getter/setters and call them on both sides of your relation, using what you already have, like following:
$einsatztag->set($buchung);
$buchung->addEinsatztag($einsatztag);
Hope it helped ;)
First of all, don't use _id properties in your code. Let it be $buchung. If you want it in the database, do it in the annotation. And this also the reason, why it's not working. Your are mapping to buchung, but your property is $Buchung_id
<?php
/** #ORM\Entity **/
class Buchung
{
// ...
/**
* #ORM\OneToMany(targetEntity="Einsatztag", mappedBy="buchung")
**/
private $einsatztage;
// ...
}
/** #ORM\Entity **/
class Einsatztag
{
// ...
/**
* #ORM\ManyToOne(targetEntity="Product", inversedBy="einsatztage")
* #JoinColumn(name="buchung_id", referencedColumnName="id")
**/
private $buchung;
// ...
}
You don't have to write the #JoinColumn, because <propertyname>_id would the default column name.
I'm going to ignore the naming issue and add a fix to the actual problem.
You need to have in the adder method a call to set the owner.
//Buchung entity
public function addEinsatztage($einsatztag)
{
$this->einsatztags->add($einsatztag);
$ein->setBuchung($this);
}
And to have this adder called when the form is submitted you need to add to the form collection field the by_reference property set to false.
Here is the documentation:
Similarly, if you're using the CollectionType field where your underlying collection data is an object (like with Doctrine's ArrayCollection), then by_reference must be set to false if you need the adder and remover (e.g. addAuthor() and removeAuthor()) to be called.
http://symfony.com/doc/current/reference/forms/types/collection.html#by-reference

How to handle Primary/Secondary/Normal statuses for entity in Symfony2

I am developing an application and I came across the following: Lets say I have an entity called Contact, that Contact belongs to a Company and the Company has a Primary Contact and a Secondary Contact and also has the remaining Contacts which I've named Normal.
My question is, what is the best approach for this when talking about entities properties and also form handling. I've though about two things:
Having 2 fields on the Company entity called PrimaryContact and SecondaryContact and also have a one-to-many relationship to a property called contacts.
What I don't like (or I'm not 100% how to do) about this option is that on the Contact entity I would need an inversedBy field for each of the 2 one-to-one properties and also 1 for the one-to-many relationship and my personal thought is that this is kind of messy for the purpose.
Having a property on the Contact entity called Type which would hold if it's primary, secondary or normal and in the Company methods that has to do with Contacts I would modify it and add the getPrimaryContact, getSecondaryContact, etc.
What I don't like about this option is that I would need to have 2 unmapped properties for the Company and I would need to do a lot on the form types in order to get this to work smoothly.
My question is what is the best approach for this structure and how to deal with forms and these dependencies. Let me know if this is not clear enough and I will take time and preparate an example with code and images.
I'm not yet a Symfony expert but i'm currently learning entites manipulation and relations !
And there is not simple way to do relations with attributes.
You have to create an entity that represent your relation.
Let's suppose you have an entity Company and and entity Contact
Then you will have an entity named CompanyContact whick will represent the relation between your objects. (you can have as many attributes as you wish in your relation entity). (Not sure for the Many-to-One for your case but the idea is the same)
<?php
namespace My\Namespace\Entity
use Doctrine\ORM\Mapping as ORM
/**
* #ORM\Entity(repositoryClass="My\Namespace\Entity\CompanyContactRepository")
*/
class CompanyContact
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(name="contact_type", type="string", length=255)
*/
private $contactType;
/**
* #ORM\ManyToOne(targetEntity="My\Namespace\Entity\Company")
* #ORM\JoinColumn(nullable=false)
*/
private $company;
/**
* #ORM\ManyToOne(targetEntity="My\Namespace\Entity\Contact")
* #ORM\JoinColumn(nullable=false)
*/
private $contact;
}
And in your controller you can do this:
$em = $this->getDoctrine()->getManager();
$company = $em->getRepository('YourBundle:Company')->find($yourCompanyId);
$yourType = "primary";
$companyContacts = $em->getRepository('YourBundle:CompanyContact')
->findBy(array('company' => $company, 'type' => $yourType));
What do you think about this approach ?
If i learn more soon i will get you posted ;)
Thanks to #Cerad this is the following approach I took:
I have a OneToMany property on the Company to hold all the contacts.
Implemented the getPrimaryContact/setPrimaryContact methods and looped through all the contacts and retrieving the one of the type I want. Did the same for the secondary.
On the Form type of the company my issue was that I had the 'mapped' => 'false' option, I removed this since I implemented the getters and setters SF2 knows it has to go to these methods.
`
<?php
namespace XYZ\Entity;
/**
* #ORM\Entity
* #ORM\HasLifecycleCallbacks()
*/
class Company
{
...
/**
* #ORM\OneToMany(targetEntity="\XYZ\Entity\Contact", mappedBy="company", cascade={"persist", "remove"})
*/
private $contacts;
public function getPrimaryContact() { ... }
public function setPrimaryContact(Contact $contact) { //Set the type of $contact and add it $this->addContact($contact) }
public function getSecondaryContact() { ... }
public function setSecondaryContact(Contact $contact) { //Set the type of $contact and add it $this->addContact($contact) }
}`
And for the Form Type I have:
`
class CompanyType extends AbstractType
{
/**
* #param FormBuilderInterface $builder
* #param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
...
->add('primaryContact', new ContactType())
->add('secondaryContact', new ContactType())
}
...
}`
With this set everything runs smoothly and I can CRUD without much struggle.

Unable to persist complex entity graph in Symfony2/Doctrine2

I'm a bit limited in the details I can provide due to a NDA, so please bear with me.
I have a complex entity graph. It consists of:
A 1-to-1 relationship between a Parent and Child.
The Child contains an ArrayCollection of FooChild entities. Cascade all.
FooChild represents a many-to-many join table between Foo and Child, but also contains some metadata that Child needs to track. Cascade persist on each side (Foo and Child)
Parents aren't required to have a Child.
To be 100% clear regarding FooChild, the relationship is many-to-many, but because of the metadata, it contains many-to-one relationship definitions:
/**
* #ORM\Entity
* #ORM\Table(name="foo_children", indexes={
* #ORM\Index(name="fooid_idx", columns={"foo_id"}),
* #ORM\Index(name="childid_idx", columns={"child_id"}),
* })
*/
class FooChild
{
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Foo", cascade={"persist"})
* #ORM\JoinColumn(name="foo_id", referencedColumnName="id", nullable=false)
*/
protected $foo;
/**
* #ORM\Id()
* #ORM\ManyToOne(targetEntity="Child", inversedBy="fooChildren", cascade={"persist"})
* #ORM\JoinColumn(name="child_id", referencedColumnName="id", nullable=false)
*/
protected $child;
/**
* #ORM\Column(type="smallint")
*/
private $count;
// methods
}
Okay, so with that structure, on the Parent edit page, I created the option for someone to add a Child to it and populate it with FooChilds with the Symfony prototype mechanism seen here. When I attempt to submit the rather large form, I get the following exception:
Entity of type MyBundle\Entity\FooChild has identity through a foreign entity MyBundle\Entity\Child, however this entity has no identity itself. You have to call EntityManager#persist() on the related entity and make sure that an identifier was generated before trying to persist 'MyBundle\Entity\FooChild'. In case of Post Insert ID Generation (such as MySQL Auto-Increment or PostgreSQL SERIAL) this means you have to call EntityManager#flush() between both persist operations.
The thing is, I've attempted to persist the various parts of this graph in different orders, and the exception still remains. My current attempt is:
$form = $this->createForm(new ParentType(), $parent);
if ($request->getMethod() == 'POST') {
$form->handleRequest($request);
if ($form->has('child')) {
$data = $form->getData();
$child = $data->getChild();
$fooChildren = $child->getFooChildren();
foreach ($fooChildren as $fc) {
$em->persist($fc);
$em->flush();
}
$em->persist($child);
$em->flush();
}
$em->persist($parent);
$em->flush();
}
The exception is thrown at the first attempt to persist, in the foreach. Like I said before, I've swapped the order of what gets persisted when several times, but it hasn't made a difference. I'm not sure what else to try.
I had an initial solution of removing the Child's form type and having the Parent's form type handle unmapped (very important) FooChild entries. Then, in the controller, I had:
$em->persist($parent);
$em->flush();
if ($form->has('fooChildren')) {
$child = new Child();
$child->setParent($parent);
$em->persist($child);
$em->flush();
// run through the FooChild entites and add them to the child
}
It worked, but I ran into some other non-related issues, so I'm currently reorganizing my schema.

Problem saving from inversed side of many-to-many relationship

I have two entities: AudioPlaylist and AudioTrack.
AudioPlaylist.php:
/**
* #ORM\ManyToMany(targetEntity = "AudioTrack", inversedBy = "audioPlaylists")
* #ORM\JoinTable(name = "audioplaylist_audiotrack")
*
* #var ArrayCollection
*/
protected $audioTracks;
AudioTrack.php:
/**
* #ORM\ManyToMany(targetEntity = "AudioPlaylist", mappedBy = "audioTracks")
*
* #var ArrayCollection
*/
protected $audioPlaylists;
My problem is that when I call $audioTrack->addAudioPlaylist($audioPlaylist), the audioplaylist_audiotrack table doesn't get updated. I'm expecting a new row to be added to the table signifying the relationship between the two entities. Everything works fine for the inverse though $audioPlaylist->addAudioTrack($audioTrack) adds a new row.
I'm making sure to persist $audioTrack and flush the entity manager, but no luck, so I assume there must be something wrong with my annotations (I'm using this example from the Doctrine docs). Any ideas?
This is probably because you have not set the cascade property for your inverse side. You must define cascading explicitly for Doctrine2 to persist any related entities.
/**
* #ORM\ManyToMany(targetEntity = "AudioPlaylist",
* mappedBy = "audioTracks",
* cascade = {"persist", "remove"})
*
* #var ArrayCollection
*/
protected $audioPlaylists;
Make sure you also add your AudioTrack to AudioPlaylist as well, when calling AudioTrack::addAudioPlaylist():
public function addAudioPlaylist(AudioPlaylist $playlist)
{
$this->getAudioPlaylists()->add($playlist);
$playlist->getAudioTracks()->add($this);
}

Categories