Maintaining OneToMany links with a form - symfony

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;
}

Related

Add PersistentCollection to an object in fixture

I'm trying to create a new fixture for creating a user.
This is the fixture :
class UserFixtures extends Fixture implements DependentFixtureInterface
{
private ManagerRegistry $_managerRegistry;
public function __construct(ManagerRegistry $managerRegistry)
{
$this->_managerRegistry = $managerRegistry;
}
public function load(ObjectManager $manager)
{
$groups = $manager->getRepository(Group::class)->findAll(); // This return an array of object. I want a PersistentCollection
$company = $manager->getRepository(Company::class)->findOneBy(['company_name' => 'HANFF - Global Health Solution']);
$user = new User();
$user->setLogin("TEST_TEST")
->setName("TEST_Name")
->setFirstName("TEST_Firstname")
->setPassword("test")
->setEmail("TEST#hanff.lu");
$user->setCompany($company);
$user->setGroups($groups); // This don't work as it is just an array
$manager->persist($user);
$manager->flush();
}
/**
* #inheritDoc
*/
public function getDependencies()
{
return array(
CompanyFixture::class,
GroupFixture::class
);
}
}
So I have already created the company and group which persist into my database.
And now I want to set to my new user the company and group which have been previously persisted by doctrine.
This work for my company as this is a single object.
But this is not working for my groups as it is typed as a PersistenCollection object and the getRepository(Group::class)->findAll() return an array of object Group.
Here the data contains in the $groups variable :
array:2 [
0 => App\Entity\Group {#1039
-code: "NAT"
-label: "National"
}
1 => App\Entity\Group {#1044
-code: "VET"
-label: "Vétérinaire"
}
]
Here this is how I defined the groups into my User entity :
Class User{
// ...
/**
* #var PersistentCollection
* Many user has many groups
* #ORM\ManyToMany(targetEntity="Group")
* #ORM\JoinTable(name="user_group",
* joinColumns={#ORm\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_code", referencedColumnName="`code`")}
* )
*/
private PersistentCollection $groups;
public function getGroups(): PersistentCollection
{
return $this->groups;
}
public function setGroups(PersistentCollection $groups): self
{
$this->groups = $groups;
return $this;
}
public function addGroup($group): self
{
$this->getGroups()->add($group);
return $this;
}
// ...
}
I have read somewhere (can't remember where) that when you persist an object using doctrine, it can be accessed as a PersistentCollection but I can't figure how to do that (Except by creating a new PersistentCollection() which is certainly not the best manner to do it)?
I have tried setting ArrayCollection instead of PersistentCollection, but if I do that, doctrine yells at me when I try to persist my user object because it can't convert ArrayCollection to PersistentCollection (i guess).
You have to change the types of your properties, arguments and return values to the Doctrine\Common\Collections\Collection interface. That's the interface ArrayCollection and PersistentCollection share. Don't forget to initialize your groups property to an ArrayCollection in the constructor. Otherwise calls to addGroup will fail on new user entities.
class User
{
// ...
/**
* #ORM\ManyToMany(targetEntity="Group")
* #ORM\JoinTable(name="user_group",
* joinColumns={#ORm\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_code", referencedColumnName="`code`")}
* )
*/
private Collection $groups;
public function __construct()
{
// other initialization
$this->groups = new ArrayCollection();
}
public function getGroups(): Collection
{
return $this->groups;
}
public function setGroups(Collection $groups): self
{
$this->groups = $groups;
return $this;
}
public function addGroup($group): self
{
$this->getGroups()->add($group);
return $this;
}
// ...
}

Doctrine ManyToMany self referencing bidirectionnal - Parent not updated

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.

Doctrine one to many - persisting multiple files from owning side not working

I have 2 entities Submission and Documents. 1 Submission can have Multiple documents.
Submission Entity:
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*/
protected $document;
/**
* #return mixed
*/
public function getDocument()
{
return $this->document->toArray();
}
public function setDocument(Document $document)
{
$this->document[] = $document;
return $this;
}
Document Entity:
/**
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Submission", inversedBy="document")
* #ORM\JoinColumn(name="submission_id", referencedColumnName="id",onDelete="cascade", nullable=true)
*/
protected $submission;
public function getSubmission()
{
return $this->submission;
}
/**
* #param mixed $submission
*/
public function setSubmission($submission)
{
$this->submission = $submission;
}
After receiving files dropzonejs - I'm saving them into Document object, and then, i'm try to save this object into Submission, and persist.
$document = new Document();
$em = $this->getDoctrine()->getManager();
$media = $request->files->get('file');
foreach($media as $req){
$document->setFile($req);
$document->setPath($req->getPathName());
$document->setName($req->getClientOriginalName());
$em->persist($document);
}
$submission->setSubmissionStatus(true);
foreach($document as $item){
$submission->setDocument($item);
}
$submission->setUser($user);
$em = $this->getDoctrine()->getManager();
$em->persist($submission);
$em->flush();
Problem is that all the time, i'm receiving error that submission_title is not set, but that's not true, because i have set this field before. I haven't got idea, what is wrong.
I think you'll get some mileage out of following the tutorial over at http://symfony.com/doc/current/doctrine/associations.html, if you haven't already.
I can see that your getters / setters aren't optimal for associating more than one Document with your Submission.
As they write in the Symfony docs, where they want to associate one category with many products, they have the following code:
// src/AppBundle/Entity/Category.php
// ...
use Doctrine\Common\Collections\ArrayCollection;
class Category
{
// ...
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
}
From the docs:
The code in the constructor is important. Rather than being
instantiated as a traditional array, the $products property must be of
a type that implements Doctrine's Collection interface. In this case,
an ArrayCollection object is used. This object looks and acts almost
exactly like an array, but has some added flexibility. If this makes
you uncomfortable, don't worry. Just imagine that it's an array and
you'll be in good shape.
So, you'll want to be sure the constructor for your Document entity has something like $this->submissions = new ArrayCollection();. I've changed the property to a plural name, because I think it's more semantically correct. But you can keep your $submission property name, if you like.
Next is to add a addSubmission, removeSubmission, and a getSubmissions method.
Then, your class might end up looking like this:
<?php
// src/AppBundle/Entity/Submission.php
namespace AppBundle\Entity
use Doctrine\Common\Collections\ArrayCollection;
class Submission
{
/**
* #ORM\OneToMany(targetEntity="AppBundle\Entity\Document", mappedBy="submission",cascade={"persist", "remove" })
* #ORM\JoinColumn(name="id", referencedColumnName="submission_id")
*
* #var ArrayCollection()
*/
protected $documents;
...
/**
* Instantiates the Submission Entity
*
* #return void
*/
public function __construct()
{
$this->documents = new ArrayCollection();
}
/**
* Returns all documents on the Submission
*
* #return mixed
*/
public function getDocuments()
{
return $this->documents;
}
/**
* Add document to this Submission
*
* #param Document $document The object to add to the $documents collection.
*
* #return Submission
*/
public function setDocument(Document $document)
{
$this->documents[] = $document;
return $this;
}
/**
* Remove a document from this Submission
*
* #param Document $document The object to remove from the $documents collection.
*
* #return Submission
*/
public function removeDocument(Document $document)
{
$this->documents->removeElement($document);
return $this;
}
}

Symfony - FOSRestBundle - show selected fields

I'm trying to show only selected fields in my REST action in controller.
I've found one solution - I can set groups in Entities/Models and select this group in annotation above action in my Controller.
But actually i don't want use groups, i want determine which fields i wanna expose.
I see one solution - I can create one group for every field in my Entities/Model. Like this:
class User
{
/**
* #var integer
*
* #Groups({"entity_user_id"})
*/
protected $id;
/**
* #var string
*
* #Groups({"entity_user_firstName"})
*/
protected $firstName;
/**
* #var string
*
* #Groups({"entity_user_lastName"})
*/
protected $lastName;
}
And then i can list fields above controller action.
My questions are:
Can I use better solution for this?
Can I list all groups? Like I can list all routes or all services.
This is mainly about serialization not about fosrestbundle itself.
The right way would be to create your own fieldserialization strategy.
This article got it down really nicely:
http://jolicode.com/blog/how-to-implement-your-own-fields-inclusion-rules-with-jms-serializer
It build a custom exclusion strategy as describeted here:
How do I create a custom exclusion strategy for JMS Serializer that allows me to make run-time decisions about whether to include a particular field?
Example code from first link for reference:
custom FieldExclusion strategy:
namespace Acme\Bundle\ApiBundle\Serializer\Exclusion;
use JMS\Serializer\Exclusion\ExclusionStrategyInterface;
use JMS\Serializer\Metadata\ClassMetadata;
use JMS\Serializer\Metadata\PropertyMetadata;
use JMS\Serializer\Context;
class FieldsListExclusionStrategy implements ExclusionStrategyInterface
{
private $fields = array();
public function __construct(array $fields)
{
$this->fields = $fields;
}
/**
* {#inheritDoc}
*/
public function shouldSkipClass(ClassMetadata $metadata, Context $navigatorContext)
{
return false;
}
/**
* {#inheritDoc}
*/
public function shouldSkipProperty(PropertyMetadata $property, Context $navigatorContext)
{
if (empty($this->fields)) {
return false;
}
$name = $property->serializedName ?: $property->name;
return !in_array($name, $this->fields);
}
}
Interface
interface ExclusionStrategyInterface
{
public function shouldSkipClass(ClassMetadata $metadata, Context $context);
public function shouldSkipProperty(PropertyMetadata $property, Context $context);
}
usage
in controller or where you need it:
$context = new SerializationContext();
$fieldList = ['id', 'title']; // fields to return
$context->addExclusionStrategy(
new FieldsListExclusionStrategy($fieldList)
);
// serialization
$serializer->serialize(new Pony(), 'json', $context);
You should be also able to mix and match with groups eg. you can also set $content->setGroups(['myGroup']) together with the fieldExclusio

Get entities from a unidirectional many to many relation with Doctrine2 and Symfony2

I'm currently working on a language assessment project which enables you to take an exam in the language you want and evaluate your level. I use Symfony2 framework and work with Doctrine2 as well. My issue is the following one:
I have two entities Exam and Question linked by a Many-To-Many relation (Exam being the owner). Each exam can be related to several questions, and each question can be related to several exams.
Here is my code:
Exam entity
/**
* Exam
*
* #ORM\Table(name="cids_exam")
* #ORM\Entity(repositoryClass="LA\AdminBundle\Entity\ExamRepository")
*/
class Exam
{
...
/**
* #ORM\ManyToMany(targetEntity="LA\AdminBundle\Entity\Question", cascade={"persist"})
* #ORM\JoinTable(name="cids_exam_question")
*/
private $questions;
...
/**
* Constructor
*/
public function __construct()
{
$this->questions = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Add questions
*
* #param \LA\AdminBundle\Entity\Question $questions
* #return Exam
*/
public function addQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions[] = $questions;
return $this;
}
/**
* Remove questions
*
* #param \LA\AdminBundle\Entity\Question $questions
*/
public function removeQuestion(\LA\AdminBundle\Entity\Question $questions)
{
$this->questions->removeElement($questions);
}
/**
* Get questions
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuestions()
{
return $this->questions;
}
}
As long as it is a unidirectional relation, there is no 'exams' attribute in my Question class.
Now, what I want to do is getting all the questions related to a specific exam, calling the getQuestions() method, like this:
$questions = $exam->getQuestions();
But this method returns an empty array, even if I have data in my database. If I var_dump the $exam variable, I can see the questions array is empty:
object(LA\AdminBundle\Entity\Exam)[47]
private 'id' => int 5
...
private 'questions' =>
object(Doctrine\ORM\PersistentCollection)[248]
private 'snapshot' =>
array (size=0)
empty
private 'owner' => null
private 'association' => null
private 'em' => null
private 'backRefFieldName' => null
private 'typeClass' => null
private 'isDirty' => boolean false
private 'initialized' => boolean false
private 'coll' =>
object(Doctrine\Common\Collections\ArrayCollection)[249]
private '_elements' =>
array (size=0)
...
I think I could maybe write a findByExam() function in my QuestionRepository, but I don't really know how to implement the joins in this case.
Any help would be great!
To have a findByExam() method in your QuestionRepository do the following:
public function findByExam($exam)
{
$q = $this->createQueryBuilder('q')
->where('q.exam = :exam')
->setParameter('exam', $exam)
->getQuery();
return $q->getResult();
}
You could also create a bi-directional relationship not uni-directional !
Each exam can be related to several questions, and each question can
be related to several exams.
Create a bi-directional relationship by adding this to your Question entity:
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
use Vendor\YourExamBundle\Entity\ExamInterface;
class Question
{
protected $exams;
public function __construct()
{
$this->exams = new ArrayCollection();
}
public function getExams()
{
return $this->exams;
}
public function addExam(ExamInterface $exam)
{
if !($this->exams->contains($exam)) {
$this->exams->add($exam);
}
return $this;
}
public function setExams(Collection $exams)
{
$this->exams = $exams;
return $this;
}
// ...
Afterwards you can use...
$question->getExams()
... in your controller.
To automatically join your related entities doctrine's fetch option can be used with:
LAZY ( loads the relations when accessed )
EAGER ( auto-joins the relations )
EXTRA_LAZY ( manual fetching )
example:
/**
* #ManyToMany(targetEntity="Question",inversedBy="exams", cascade={"all"}, fetch="EAGER")
*/
Though eager loading has a downside in terms of performance it might be an option for you.
Doctrine Fetch with EAGER
Whenever you query for an entity that has persistent associations and
these associations are mapped as EAGER, they will automatically be
loaded together with the entity being queried and is thus immediately
available to your application.
Read more about it in the Doctrine Documentation.
Another option you should check when working with relations is the cascade option.
See the Doctrine - Working with Associations chapter of the documentation.
Tip:
You should create interfaces for exams and questions and use them instead of the original entity in your set and add methods to allow easier extending.
Bi-Directional Relations using Doctrine2 ORM with association table exam_questions
exam_id question_id
<?php
class Exams
....OTHER PROPERTIES...
/**
* Owning Side
*
* #ManyToMany(targetEntity="Questions", inversedBy="exams")
* #JoinTable(name="exam_questions",
* joinColumns={#JoinColumn(name="exam_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="question_id", referencedColumnName="id")}
* )
*/
private $questions;
..OTHER CODES..
}
class Questions{
..OTHER CODES..
/**
* Inverse Side
*
* #ManyToMany(targetEntity="Exams", mappedBy="questions")
*/
private $exams;
..OTHER CODES..
}
http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/annotations-reference.html#annref-manytomany

Resources