I'm trying to create a ManyToMany relation beetwin services of a company.
Each service had N parents services and N children services.
I looked at the doctrine documentation here : Many-To-Many, Self-referencing and I implemented it as followed :
Here is my service entity :
<?
namespace AppBundle\Entity;
class Service
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
private $parents;
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", inversedBy="parents")
* #ORM\JoinTable(name="app_services_hierarchy",
* joinColumns={#ORM\JoinColumn(name="parent_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="enfant_id", referencedColumnName="id")}
* )
*/
private $enfants;
public function __construct()
{
$this->enfants = new ArrayCollection();
$this->parents = new ArrayCollection();
}
public function getId(){
return $this->id;
}
//--------------------------------------------------Enfants
public function getEnfants(){
return $this->enfants;
}
public function setEnfants($enfant){
$this->enfants = $enfant;
}
public function addEnfant(Service $s){
$this->enfants[] = $s;
return $this;
}
public function removeEnfant(Service $s){
$this->enfants->removeElement($s);
}
//--------------------------------------------------Parents
public function getParents(){
return $this->parents;
}
public function setParents($parents){
$this->parents = $parents;
}
public function addParent(Service $s){
$this->parents[] = $s;
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
}
}
And here is my edit function( Controller.php) :
public function editAction(Request $request, $id)
{
$service = $this->getDoctrine()->getRepository(Service::class)->find($id);
$form = $this->createForm(ServiceType::class, $service);
$form ->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->getDoctrine()->getManager();
$entityManager ->persist($service);
dump($service);
$entityManager ->flush();
}
return $this->render('AppBundle:Service:edit.html.twig', array(
'form' => $form->createView(),
));
}
And the generated form looks like :
PROBLEM :
My problem is that the childrens are updated but not the parents. I can see the parents in the $service variable when I dump() it in my controler but the only ones updated in my database table (app_services_hierarchie) are the children.
The difference between $parents and $enfants in your code is that the service you are looking at is the Owning side in case of your $enfants mapping, but not in the case of your $parents mapping.
Doctrine will not store the $parents unless you tell it to do so via cascade={"persist"}.
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Service", mappedBy="enfants", cascade={"persist"})
*/
This is basically the same anwer given in the post linked by #GregoireDucharme.
Edit: after some research, apparently this problem cannot be solved using cascade. According to the Doctrine documentation:
Doctrine will only check the owning side of an association for changes.
So what you have to do is tell your $parents to also update the $children property.
public function addParent(Service $s){
$this->parents[] = $s;
$s->addEnfant($this);
return $this;
}
public function removeParent(Service $s){
$this->parents->removeElement($s);
$s->removeEnfant($this);
}
In your form, make sure to specify the following:
->add('parents', 'collection', array(
'by_reference' => false,
//...
))
(I haven't spellchecked any of the code above, so tread carefully.)
If 'by_reference' is set to true, addParent and removeParent will not be called.
Credit goes to this blog post by Anny Filina.
It also states that you can remove the cascade option from your $parents property, but you probably should add cascade={"persist","remove"} to your $enfants property.
Related
My page
User entity :
<?php
s
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use App\Validator\Constraints as AssertPerso;
/**
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #ORM\Table(name="app_user")
*/
class User implements UserInterface
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\ManyToOne (targetEntity="Formation")
*/
private $customerName;
public function getId(): ?int
{
return $this->id;
}
/**
* #return CustomerName
*/
public function getCustomerName()
{
return $this->customerName;
}
public function setCustomerName($customerName): self
{
$this->customerName = $customerName;
return $this;
}
}
Formation entity :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="App\Repository\FormationRepository")
*/
class Formation
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=180, unique=true)
*/
private $customerName;
public function getId(): ?int
{
return $this->id;
}
/**
* #return CustomerName
*/
public function getCustomerName()
{
return $this->customerName;
}
public function setCustomerName($customerName): self
{
$this->customerName = $customerName;
return $this;
}
public function __toString()
{
return $this->customerName;
}
}
and my form UserType :
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('customerName', null, array(
'label' => 'Client ',
'required' => true)
)
->add('submit', SubmitType::class, ['label_format' => "Register", 'attr'=>['class'=>'btn-primary btn-block']]);
$builder->addEventListener(
FormEvents::PRE_SET_DATA,
array($this, 'preSetData')
);
}
public function preSetData(FormEvent $event)
{
$form = $event->getForm();
$user = $event->getData();
// $form->remove('customerName');
$user->setEnabled(true);
}
In my database "formation", I have customer_name "xxx".
I want, when the user register, set the customer_name_id in my table "user" with the domain of the email if this email is like "xxxx" and if the domain of this email does not correspond with the customer_name, display error
Can you help me please ? Thank you
edit with Rebru response :
I succeeded to make the custom validator to check if the domain is an existing domain in "formation"
public function validate($value, Constraint $constraint)
{
$mailParts = explode('#', $value);
$mailParts = explode('.', $mailParts[1]);
$domain = $mailParts[0];
if(!$this->entityManager->getRepository(Formation::class)->findBy(array('customerName' => $domain))){
$this->context->buildViolation($constraint->messageWrongDomain)
->setParameter('{{ value }}', $this->formatValue($value))
->setCode(Email::INVALID_FORMAT_ERROR)
->addViolation();
return;
}
}
But now I do not know how to set the customer_name_id automatically when the user registers ?
Thank you
Edit 2 :
I just tried that for set the customer_name_id in user table.
In my UserController :
$mailParts = explode('#', $user->getEmail());
$mailParts = explode('.', $mailParts[1]);
$domain = $mailParts[0];
$customerNameId = $entityManager->getRepository(Formation::class)->findBy(array('customerName' => $domain));
$customerNameId = $customerNameId[0]->getId();
I get the following error:
Expected value of type "App\Entity\Formation" for association field "App\Entity\User#$customerName", got "integer" instead.
Ok, then your design is wrong - btw. i would suggest never use of manytomany over strings.
The right path to achieve this is to use custom validators ... like described here: https://symfony.com/doc/current/validation/custom_constraint.html
Short:
table with all customers ( id, name, formation_id)
table with all formations ( id, name, domain )
In the custom validator, you can split the customers email ( explode("#", $email) ) and check this against the domains in the formation table. If it match, the validator should return true otherwise false. If the validator is true, you can save the customer.
With this approach the form will never be valid, until it matches the custom validator.
I got error while persisting object saying I need to configure cascade persist option in ManyToMany relation, but it is configured.
A new entity was found through the relationship 'AppBundle\Entity\ShopProducts#shopProductImages' that was not configured to cascade persist operations for entity: AppBundle\Entity\ShopProductImages#000000007d4db89e00000000344e8db2. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example #ManyToOne(..,cascade={"persist"}). If you cannot find out which entity causes the problem implement 'AppBundle\Entity\ShopProductImages#__toString()' to get a clue.
Controller/ShopController.php
$product = new ShopProducts();
$form = $this->createForm(ProductTypeNew::class, $product);
if ($form->isSubmitted() && $form->isValid())
{
$image = new ShopProductImages();
...
$product->addShopProductImages($image);
...
$em->persist($product);
$em->flush();
Entity/ShopProducts.php
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(
* targetEntity="AppBundle\Entity\ShopProductImages",
* mappedBy="shopProducts",
* cascade={"persist"}
* )
*/
private $shopProductImages;
/**
* #return ArrayCollection|ShopProductImages[]
*/
public function getShopProductImages()
{
return $this->shopProductImages;
}
public function addShopProductImages(ShopProductImages $image)
{
if ($this->shopProductImages->contains($image)) {
return;
}
$this->shopProductImages[] = $image;
$image->addImagesProduct($this);
}
public function removeShopProductImages(ShopProductImages $image)
{
if (!$this->shopProductImages->contains($image)) {
return;
}
$this->shopProductImages->removeElement($image);
$image->removeImagesProduct($this);
}
Entity/ShopProductImages.php
/**
* #var \Doctrine\Common\Collections\Collection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\ShopProducts",
* inversedBy="shopProductImages",
* cascade={"persist"}
* )
* #ORM\JoinTable(name="shop_product_images_has_shop_products"),
* joinColumns={
* #ORM\JoinColumn(name="shop_product_images_id", referencedColumnName="id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="shop_products_id", referencedColumnName="id")
* }
*/
private $shopProducts;
public function addImagesProduct(ShopProducts $product)
{
if ($this->shopProducts->contains($product)) {
return;
}
$this->shopProducts[] = $product;
$product->addShopProductImages($this);
}
public function removeImagesProduct(ShopProducts $product)
{
if (!$this->shopProducts->contains($product)) {
return;
}
$this->shopProducts->removeElement($product);
$product->removeShopProductImages($this);
}
Form/Type/ProductTypeNew.php
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('price')
->add('description', TextareaType::class)
->add('quantity')
->add('file', FileType::class, array('label' => 'Zdjęcie'))
In your controller, try adding $em->persist($image); before you flush.
Your issue seems to be that he can't create the values of the shop_product_images_has_shop_products table because the images don't have an Id yet, so you have to persist the $image and the $product entities before you flush.
Also, your cascade={"persist"} should only be in the image entity entity annotations, not on the product ones.
On manyToMany, you don't need cascade={"persist"}
You have your add..() and remove...() It replace your cascade to persist data into the middle table
In your form builder you need : ->add('ShopProductImages', 'collection', array( 'by_reference' => false, ))
And remove cascades from your annotations
Imagine these 2 entities
Intervention
- items # OneToMany (no cascade)
addItem()
removeItem()
Item
- intervention # ManyToOne
When I'm doing an Intervention I want to select the Items concerned.
I use an Intervention form in which I can attach/unattach items
->add('items', EntityIdType::class, array(
'class' => Item::class,
'multiple' => true,
))
When the form is submitted, I see Doctrine calls my Intervention's addItem(), removeItem()
But when I empty any previously attached items (thus sending null as items), Doctrine tells me:
Neither the property "items" nor one of the methods "addItem()"/"removeItem()", "setItems()", "items()", "__set()" or "__call()" exist and have public access in class "AppBundle\Entity\Intervention".
The first question is: Why Doctrine is not finding my accessors when I send a null item list ?
My workaround for now is to implement a setItems() doing the adds/removes:
/**
* Set items
*
* #param $items
*
* #return Intervention
*/
public function setItems($items)
{
foreach ($this->items as $item) {
$this->removeItem($item);
}
if ($items) {
foreach ($items as $item) {
$this->addItem($item);
}
}
return $this;
}
I think you need to use ArrayCollection in your other side of the ManyToOne relationship.
Your AppBundle\Entity\Item entity class needs to have:
use AppBundle\Entity\Intervention;
//...
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Intervention", inverseBy="items")
*/
private $intervention;
/**
* #param Intervention $intervention
*/
public function setIntervention(Intervention $intervention){
$this->intervention = $intervention;
}
/**
* #return Intervention
*/
public function getIntervention(){
return $this->intervention;
}
Then in the AppBundle\Entity\Intervention entity class:
use Doctrine\Common\Collections\ArrayCollection;
//...
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Item", mappedBy="intervention")
*/
private $items;
public function __construct(){
$this->items = new ArrayCollection();
}
public function getItems(){
return $this->items;
}
public function setItems($items){
$this->items = $items;
}
Here is the situation, I have a form in which I need an entity field type. Inside the BenefitGroup entity I have a BenefitGroupCategory selection.
My buildform is:
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('BenefitGroupCategories', 'entity', array(
'class' => 'AppBundle:BenefitGroupCategory',
'property' => 'name',
'label' => false,
'query_builder' => function(EntityRepository $er) {
return $er->createQueryBuilder('c')
->orderBy('c.name', 'ASC');
},))
->add('benefitsubitems', 'collection', array('type' => new BenefitSubItemFormType(), 'allow_add' => true, 'label' => false,));
}
It's almost a typical product-category relationship. A BenefitGroup can have only one category and a category can belong to many BenefitGroups (the only complication, not implemented yet, but that's the reason I need the query builder, is that all will depend on another parameter (project) so that some categories will be the default ones (always available), others will be available only for specific projects (see below the reference to project in the BenefitGroupCategory entity)).
You'll notice another field, benefitsubitems, which is not relevant for the question at hand.
As far I understand it, from the Doctrine perspective, I have to set up a One-To-Many, Unidirectional with Join Table.
The two entities are:
<?php
// src/AppBundle/Entity/BenefitGroup.php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity(repositoryClass="AppBundle\Entity\BenefitGroupRepository")
* #ORM\Table(name="benefit_groups")
*/
class BenefitGroup
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="BenefitItem", cascade={"persist"}, inversedBy="BenefitGroups")
*/
protected $benefitItem;
/**
* #ORM\oneToMany(targetEntity="BenefitSubItem", mappedBy="benefitGroup")
*/
protected $BenefitSubItems;
/**
* #ORM\ManyToMany(targetEntity="BenefitGroupCategory")
* #ORM\JoinTable(name="BenefitGroup_BenefitGroupCategory", joinColumns={#ORM\JoinColumn(name="BenefitGroup_id", referencedColumnName="id")}, inverseJoinColumns={#ORM\JoinColumn(name="BenefitGroupCategory_id", referencedColumnName="id", unique=true)})
*/
protected $BenefitGroupCategories;
// HERE I HAVE SOME IRRELEVANT GETTERS AND SETTERS
/**
* Constructor
*/
public function __construct()
{
$this->BenefitSubItems = new ArrayCollection();
$this->BenefitGroupCategories = new ArrayCollection();
}
/**
* Add BenefitGroupCategories
*
* #param \AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories
* #return BenefitGroup
*/
public function addBenefitGroupCategory(\AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories)
{
$this->BenefitGroupCategories[] = $benefitGroupCategories;
return $this;
}
/**
* Remove BenefitGroupCategories
*
* #param \AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories
*/
public function removeBenefitGroupCategory(\AppBundle\Entity\BenefitGroupCategory $benefitGroupCategories)
{
$this->BenefitGroupCategories->removeElement($benefitGroupCategories);
}
/**
* Get BenefitGroupCategories
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getBenefitGroupCategories()
{
return $this->BenefitGroupCategories;
}
}
You'll also notice another entity, BenefitItem, which is the "father" of BenefitGroup.
And
<?php
// src/AppBundle/Entity/BenefitGroupCategory.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* #ORM\Entity()
* #ORM\Table(name="benefit_group_category")
* #UniqueEntity(fields={"name", "project"}, ignoreNull=false, message="Duplicated group category for this project")
*/
class BenefitGroupCategory
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", length=50)
*/
protected $name;
/**
* #ORM\ManyToOne(targetEntity="Project")
*/
protected $project;
// HERE I HAVE SOME IRRELEVANT GETTERS AND SETTERS
}
In the controller (you'll see several embedded collections, which work ok) I have:
/**
* #Route("/benefit/show/{projectID}", name="benefit_show")
*/
public function showAction(Request $request, $projectID)
{
$id=4; //the Id of the CVC to look for
$storedCVC = $this->getDoctrine()
->getRepository('AppBundle:CVC')
->find($id);
$form = $this->createForm(new CVCFormType(), clone $storedCVC);
$form->handleRequest($request);
if ($form->isValid())
{
$em = $this->getDoctrine()->getManager();
//$benefitGroupCategoryRepository = $this->getDoctrine()->getRepository('AppBundle:BenefitGroupCategory');
$formCVC = $form->getData();
$em->persist($formCVC);
foreach ($formCVC->getBenefitItems() as $formBI)
{
$newBI = new BenefitItem();
$newBI->setCVC($formCVC);
$newBI->setComment($formBI->getComment());
$em->persist($newBI);
foreach ($formBI->getBenefitGroups() as $formBG)
{
$newBG = new BenefitGroup();
$newBG->setBenefitItem($newBI);
$newBG->setBenefitGroupCategories($formBG->getBenefitGroupCategories());
$em->persist($newBG);
foreach ($formBG->getBenefitSubItems() as $formSI)
{
$newSI = new BenefitSubItem();
$newSI->setBenefitGroup($newBG);
$newSI->setComment($formSI->getComment());
$em->persist($newSI);
}
}
}
$em->flush();
}
return $this->render('benefit/show.html.twig', array(
'form' => $form->createView(),
));
}
The problem is: in visualization it visualizes correctly the form (even though it does not retrieve correctly the category. I have a choice of categories, which is ok, but it does not retrieve the right one. Do I have to set the default value in the form?
The problem gets way worse when I sumbit the form it's supposed to create a new entity (notice the clone) with all the nested ones. The problem is that it crashes saying:
Neither the property "BenefitGroupCategories" nor one of the methods
"addBenefitGroupCategory()"/"removeBenefitGroupCategory()",
"setBenefitGroupCategories()", "benefitGroupCategories()", "__set()" or
"__call()" exist and have public access in class
"AppBundle\Entity\BenefitGroup".
The "beauty" is that even if I comment completeley the (nasty) part inside the "isValid" it behaves exactly the same.
I'm lost :(
About the cloning you have to unset the id of the cloned entity, look here: https://stackoverflow.com/a/14158815/4723525
EDIT:
Yes, but PHP just do shallow copy, you have to clone other objects. Look at Example #1 Cloning an object in http://php.net/manual/en/language.oop5.cloning.php. You have to clone your objects by defining __clone method (for Doctrine lower than 2.0.2 you have to do this by calling own method after cloning because proxy defines it's own __clone method). So for example:
function __clone() {
$oldCollection = $this->collection;
$this->collection = new ArrayCollection();
foreach ($oldCollection as $oldElement) {
$newElement = clone $oldElement;
// additional actions for example setting this object to owning side
$newElement->setParent($this);
$this->collection->add($newElement);
}
}
I have an entity Subject:
/**
*
* #ORM\Table()
* #ORM\HasLifecycleCallbacks()
*/
class Subject
{
//... Some fields
/**
* #ORM\OneToMany(targetEntity="Subject", mappedBy="mark", cascade={"persist", "remove"})
*/
private $subjects;
private function calculateMarks()
{
//... Do something
// return array with (abilities => marks);
}
/**
* #ORM\PrePersist()
*/
public function prePersist(){
$now = new \DateTime();
$this->setCreatedAt( $now );
$this->setModifiedAt( $now );
}
/**
* #ORM\PreUpdate()
*/
public function preUpdate(){
$this->setModifiedAt( new \DateTime() );
$this->setUpdated(true);
}
/**
* #ORM\PreFlush()
*/
public function preFlush(){
$marks = calculateMarks();
foreach($marks as $ability => $score){
$mark = new Mark();
$mark->setSubject( $this );
$this->addMark( $score );
$mark->setAbility( $ability );
}
}
}
and the class Mark:
class Mark{
// Many fields
/**
* #ORM\ManyToOne(targetEntity="Subject", inversedBy="subjects")
* #ORM\JoinColumn(name="subject_id", referencedColumnName="id")
*/
private $subject;
}
My problem is that I calculate and I create the Marks in the preFlush event (this is done this because in the official documentation is said this about preUpdate event: "Changes to associations of the updated entity are never allowed in this event, since Doctrine cannot guarantee to correctly handle referential integrity at this point of the flush operation"). When I save one subject, all work fine, but when I save many Subjects at the same time in a webservice, some marks are stored in the database many times.
The webservice action below:
public function setSubjects(Request $request)
{
//... Do something
$subjects = $request["Subjects"];
foreach($subjects as $s){
$em = $this->getDoctrine()->getManager();
//... Do something
$em->persist($s);
$em->flush();
}
return new JsonResponse($response);
}
Has anybody an idea of how could I avoid this behavior in the preFlush event?
Thanks in advance.
I always try to avoid LifecycleCallbacks unless it's simple and i'm only changing properties in the same entity.
to solve your issue i would create a function calculateMarks() inside the entity and tweak my loop to be something like
$em = $this->getDoctrine()->getManager();
foreach($subjects as $s){
//... Do something
$s->calculateMarks();
$em->persist($s);
}
$em->flush();
NOTICE
avoid $em = $this->getDoctrine()->getManager(); & $em->flush(); inside the loop