Symfony, Many to many with attributes self referencing - symfony

I have an entity Room that has a ManyToMany relationship with itself, and this relationship (RoomLinkRoom) bears an attribute "weight" :
a Room can be linked to several Rooms, with an ordering(weight) value (and potentially other attributes).
The code i have sofar handles the linkage of one or more RoomTo to a RoomFrom.
Let's say we have :
RoomA : 1
RoomB : 2
In the Room Form, we can add RoomB to RoomA
we have this entry in RoomLinkRoom :
RoomFromId : 1
RoomToId : 2
But this is only half of what i need : i also need the reverse part management.
When RoomFromId,1 is linked to RoomToId,2
i also want to add in this intermediate entity : RoomFromId,2, RoomToId,1
And then i also have to manage the delete part : If i remove Room A, the entry (roomFrom, roomTo) : (1, 2) will be deleted but the reverse (2, 1) must also be deleted.
How can i achieve this ? What is the best (cleanest ?) way to handle this whole problem ? Is there a "standard pattern" ("automatic" or not) for handling this case ?
I'm not sure how to procede, but maybe it involves events, like postFlush ? But will it be able to take care also of the delete for the "reverse" (To) side ?
The (relevant part of the) Entities are :
<?php
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use MyBundle\Entity\RoomLinkRoom;
/**
* Room
*
* #ORM\Table(name="room")
* #ORM\Entity(repositoryClass="MyBundle\Repository\RoomRepository")
* #ORM\HasLifecycleCallbacks()
*/
class Room
{
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #ORM\Column(name="name", type="string", length=255, nullable=false)
*/
private $name;
/**
* #ORM\OneToMany(
* targetEntity="MyBundle\Entity\RoomLinkRoom", mappedBy="roomFrom",
* cascade={"persist", "remove"}, orphanRemoval=TRUE)
*/
private $roomFromLinks;
/**
* #ORM\OneToMany(
* targetEntity="MyBundle\Entity\RoomLinkRoom", mappedBy="roomTo",
* cascade={"persist", "remove"}, orphanRemoval=TRUE)
*/
private $roomToLinks;
public function __construct()
{
$this->roomFromLinks = new ArrayCollection();
$this->roomToLinks = new ArrayCollection();
}
public function addRoomFromLink(RoomLinkRoom $roomFromLink)
{
$this->roomFromLinks[] = $roomFromLink;
$roomFromLink->setRoomFrom($this);
return $this;
}
public function removeRoomFromLink(RoomLinkRoom $roomFromLink)
{
$this->roomFromLinks->removeElement($roomFromLink);
}
public function getRoomFromLinks()
{
return $this->roomFromLinks;
}
public function addRoomToLink(RoomLinkRoom $roomToLink)
{
$this->roomToLinks[] = $roomToLink;
$roomToLink->setRoomTo($this);
return $this;
}
public function removeRoomToLink(RoomLinkRoom $roomToLink)
{
$this->roomToLinks->removeElement($roomToLink);
}
public function getRoomToLinks()
{
return $this->roomToLinks;
}
}
And
<?php
use Doctrine\ORM\Mapping as ORM;
use MyBundle\Entity\Room;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
/**
* RoomLinkRoom.
*
* #ORM\Table(name="roomlinkroom", indexes={
* #ORM\Index(name="FK_RoomLinkRoom_roomfromId", columns={"roomfromId"}),
* #ORM\Index(name="FK_RoomLinkRoom_roomtoId", columns={"roomtoId"}),
* })
* #ORM\Entity(repositoryClass="MyBundle\Repository\RoomLinkRoomRepository")
* #UniqueEntity(
* fields={"roomFrom", "roomTo"},
* errorPath="weight",
* message="Ces salles sont déjà liées."
* )
*/
class RoomLinkRoom
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\Room", inversedBy="roomFromLinks")
* #ORM\JoinColumn(name="roomFromId", referencedColumnName="id", nullable=false)
*/
private $roomFrom;
/**
* #ORM\ManyToOne(targetEntity="MyBundle\Entity\Room", inversedBy="roomToLinks")
* #ORM\JoinColumn(name="roomToId", referencedColumnName="id", nullable=false)
*/
private $roomTo;
public function getId()
{
return $this->id;
}
public function setRoomFrom(Room $roomFrom)
{
$this->roomFrom = $roomFrom;
return $this;
}
public function getRoomFrom()
{
return $this->roomFrom;
}
public function setRoomTo(Room $roomTo)
{
$this->roomTo = $roomTo;
return $this;
}
public function getRoomTo()
{
return $this->roomTo;
}
}
I've tried in the controller to add after the flush :
if ($form->isSubmitted() && $form->isValid()) {
//...
$em->persist($room);
$em->flush();
//look for existing relationship between To and From
foreach ($room->getRoomFromLinks() as $rfl){
$res = $em->getRepository('MyBundle:RoomLinkRoom')
->findBy(['roomFrom' => $rfl->getRoomTo(), 'roomTo' => $room]);
//add the reverse side
if (count($res) === 0){
$rlr = new RoomLinkRoom();
$rlr->setRoomFrom($rfl->getRoomTo());
$rlr->setRoomTo($room);
$em->persist($rlr);
$em->flush();
}
}
//redirect
}
This adds the reverse/complementary entry, but i still don't know if this is clean/bugproof. But i'm sure this doesn't take care of the delete.
So does anyone have used this kind of relationship ?
I've seen other discussions here (for instance, this one) but, they don't seem to care for this "reverse" management.
Thank you in advance.

Related

API Platform Sonata Media Bundle Gallery - Circular Reference

New in SF3, I use API Platform and Sonata Media Bundle.
I'm blocked while getting Gallery entity of Sonata using API Platform GET request.
"A circular reference has been detected when serializing the object of class \"Application\\Sonata\\MediaBundle\\Entity\\Gallery\" (configured limit: 1)"
The admin of the entity works great, I can add a gallery to the entity.
When the entity have a gallery it cause this error, when it does not it's ok.
Entity Technic
GET /technics in API Platform
[
{
"id": 0,
"type": "string",
"comment": "string",
"links": [
"string"
],
"gallery": "string"
}
]
Entity Class
<?php
// src/AppBundle/Entity/Technic.php
namespace AppBundle\Entity;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* #ORM\Entity
* #ApiResource
*/
class Technic
{
/**
* #var int The id of this evaluation.
*
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* #var string $type TechnicType of the evaluation
*
* #ORM\OneToOne(targetEntity="TechnicType")
* #Assert\NotBlank
*/
public $type;
/**
* #var string $note Note of the evaluation
*
* #ORM\Column(type="string", length=255, nullable=true)
*/
public $comment;
/**
* #var Link[] Link Links of this technic.
*
* #ORM\ManyToMany(targetEntity="Link", cascade={"persist"})
*/
private $links;
/**
* #ORM\OneToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Gallery",cascade={"persist"})
* #ORM\JoinColumn(name="gallery", referencedColumnName="id", nullable=true)
*/
private $gallery;
/**
* Constructor
*/
public function __construct()
{
$this->links = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set type
*
* #param \AppBundle\Entity\TechnicType $type
*
* #return Technic
*/
public function setType(\AppBundle\Entity\TechnicType $type = null)
{
$this->type = $type;
return $this;
}
/**
* Get type
*
* #return \AppBundle\Entity\TechnicType
*/
public function getType()
{
return $this->type;
}
/**
* Add link
*
* #param \AppBundle\Entity\Link $link
*
* #return Technic
*/
public function addLink(\AppBundle\Entity\Link $link)
{
$this->links[] = $link;
return $this;
}
/**
* Remove link
*
* #param \AppBundle\Entity\Link $link
*/
public function removeLink(\AppBundle\Entity\Link $link)
{
$this->links->removeElement($link);
}
/**
* Get links
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getLinks()
{
return $this->links;
}
/**
* Set comment
*
* #param string $comment
*
* #return Technic
*/
public function setComment($comment)
{
$this->comment = $comment;
return $this;
}
/**
* Get comment
*
* #return string
*/
public function getComment()
{
return $this->comment;
}
/**
* Set gallery
*
* #param \Application\Sonata\MediaBundle\Entity\Gallery $gallery
*
* #return Technic
*/
public function setGallery(\Application\Sonata\MediaBundle\Entity\Gallery $gallery = null)
{
$this->gallery = $gallery;
return $this;
}
/**
* Get gallery
*
* #return \Application\Sonata\MediaBundle\Entity\Gallery
*/
public function getGallery()
{
return $this->gallery;
}
}
Thank a lot guys, I'm desesperate I try a lot of things in StackQ/A, annotations, seraliazer config...
You need to configure serialization correctly. Either setup serialization groups, so that on GETting some entity serializer would only pick (for example) IDs of related entities, or set up circualr reference handler in normalizer and inject this normalizer into serializer.
$normalizer = new GetSetMethodNormalizer();
$normalizer->setCircularReferenceHandler(function ($object) {
return $object->getId();
});
There might be more specific answer for api-platform, which I don't know, because serialization of related entities is popular issue.

Doctrine 2 - ManyToMany relationship - empty collection

I have many to many relationship between user and groups, but when I want to access all groups for user I get empty collection.
namespace LoginBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="User")
*/
class User
{
/**
* #ORM\Column(type="integer", name="id")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $_iId;
/**
* #ORM\Column(type="string", name="login", length=45)
*/
private $_sLogin;
/**
* #ORM\ManyToMany(targetEntity="GroupBundle\Entity\Group", inversedBy="_aUser")
* #ORM\JoinTable(name="Group_x_User",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="group_id", referencedColumnName="id")}
* )
*/
private $_aGroup;
public function __construct() {
$this->_aGroup = new ArrayCollection();
}
/**
* Get iId
*
* #return integer
*/
public function getId()
{
return $this->_iId;
}
/**
* Set sLogin
*
* #param string $sLogin
*
* #return User
*/
public function setLogin($sLogin)
{
$this->_sLogin = $sLogin;
return $this;
}
/**
* Get sLogin
*
* #return string
*/
public function getLogin()
{
return $this->_sLogin;
}
public function getGroups()
{
return $this->_aGroup;
}
User and Group use Group_x_User table to store their relationships.
namespace GroupBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="Group")
*/
class Group
{
/**
* #ORM\Column(type="integer", name="id")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $_iId;
/**
* #ORM\Column(type="string", name="name", length=45)
*/
private $_sName;
/**
* #ORM\ManyToMany(targetEntity="LoginBundle\Entity\User", mappedBy="_aGroup")
*/
private $_aUser;
public function __construct() {
$this->_aUser = new ArrayCollection();
}
/**
* Get iId
*
* #return integer
*/
public function getId()
{
return $this->_iId;
}
/**
* Set sName
*
* #param string $sName
*
* #return Group
*/
public function setName($sName)
{
$this->_sName = $sName;
return $this;
}
/**
* Get sName
*
* #return string
*/
public function getName()
{
return $this->_sName;
}
public function getUsers()
{
return $this->_aUser;
}
}
For restoring data from database I use code:
$oUser = $this->getDoctrine()
->getRepository('LoginBundle:User')
->find(2);
$aGroup = $oUser->getGroups();
Unfortunatelly $aGroup collection contains array of 0 elements while in database are records which are matching. What am I missing in my mapping?
There is something missing from your User class. You only declare _aGroup with ArrayCollection. This is not enough. You have to store the data into ArrayCollection.
Add this into User class.
public class addGroups(\GroupBundle\Entity\Group $group)
{
$group->addUsers($this);
$this->_aGroup->add($group);
}
This is for Group class.
public class addUsers(\LoginBundle\Entity\User $user)
{
if (!$this->_aUser->contains($user)) {
$this->_aUser->add($user);
}
}
For more information, you can visit this link.
#ORM\JoinTable() annotation syntax on User::$_aGroup definition is intended to be used for self-referencing relations Doctrine documentation.
Try using simplified syntax :
#JoinTable(name="Group_x_User")
If you use common conventions ('id' as column name for identifiers, and so on) Doctrine and Symfony will do the rest for you.

How to search ManyToOne side by OneToMany

I have Four Entities:
Batch
BachGroup
BachGroupApplication
Application
Entity 1(Batch) has OneToMany Bidirectional relationship with Entity 2(BatchGroup).
Entity 2(BatchGroup) has OneToMany Bidirectional relationship with Entity 3(BachGroupApplication).
Entity 3(BachGroupApplication) has ManyToOne Bidirectional relationship with Entity 4(Application).
DETAIL ON ABOVE ENTITIES:
Each batch has it's groups which i'm storing in bachGroup entity with batch reference column in it, now each batchGroup can have multiple applications so i have created BatchGroupApplication entity which holds reference to batchGroup and Application.
TODO:
I want to be able to search applications by individual batch group which i'm not able to do i searched the web and stackoverflow but failed to get it working! i don't know if my mind has stopped somewhere! my entities are listed below please help:
Batch Entity
<?php
namespace ModelBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Batch
*
* #ORM\Table(name="batch")
* #ORM\Entity(repositoryClass="ModelBundle\Repository\BatchRepository")
*/
class Batch
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// ..............
/**
* If a Batch is removed all it's associated Groups will be deleted as well..
*
* #ORM\OneToMany(targetEntity="BatchGroup", mappedBy="batch", cascade={"remove"})
**/
private $batchGroups;
public function __construct() {
$this->batchGroups = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
// ...............
/**
* Add batchGroups
*
* #param \ModelBundle\Entity\BatchGroup $batchGroups
* #return Batch
*/
public function addBatchGroup(\ModelBundle\Entity\BatchGroup $batchGroups)
{
$this->batchGroups[] = $batchGroups;
return $this;
}
/**
* Remove batchGroups
*
* #param \ModelBundle\Entity\BatchGroup $batchGroups
*/
public function removeBatchGroup(\ModelBundle\Entity\BatchGroup $batchGroups)
{
$this->batchGroups->removeElement($batchGroups);
}
/**
* Get batchGroups
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getBatchGroups()
{
return $this->batchGroups;
}
}
BatchGroup Entity
<?php
namespace ModelBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
/**
* BatchGroup
*
* #ORM\Table(name="batch_group")
* #ORM\Entity()
*/
class BatchGroup
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="title", type="string", length=128)
*/
private $title;
/**
* #ORM\ManyToOne(targetEntity="Batch", inversedBy="batchGroups")
* #ORM\JoinColumn(name="batch_id", referencedColumnName="id")
**/
private $batch;
/**
* #ORM\OneToMany(targetEntity="BatchGroupApplication", mappedBy="batchGroup", cascade={"remove"})
**/
private $batchGroupApplications;
public function __construct() {
$this->batchGroupApplications = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
// ............
/**
* Set batch
*
* #param \ModelBundle\Entity\Batch $batch
* #return BatchGroup
*/
public function setBatch(\ModelBundle\Entity\Batch $batch = null)
{
$this->batch = $batch;
return $this;
}
/**
* Get batch
*
* #return \ModelBundle\Entity\Batch
*/
public function getBatch()
{
return $this->batch;
}
/**
* Add batchGroupApplications
*
* #param \ModelBundle\Entity\BatchGroupApplication $batchGroupApplications
* #return BatchGroup
*/
public function addBatchGroupApplication(\ModelBundle\Entity\BatchGroupApplication $batchGroupApplications)
{
$this->batchGroupApplications[] = $batchGroupApplications;
return $this;
}
/**
* Remove batchGroupApplications
*
* #param \ModelBundle\Entity\BatchGroupApplication $batchGroupApplications
*/
public function removeBatchGroupApplication(\ModelBundle\Entity\BatchGroupApplication $batchGroupApplications)
{
$this->batchGroupApplications->removeElement($batchGroupApplications);
}
/**
* Get batchGroupApplications
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getBatchGroupApplications()
{
return $this->batchGroupApplications;
}
}
BatchGroupApplication Entity
<?php
namespace ModelBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* BatchGroupApplication
*
* Table generated through this entity class will hold all application and can be filtered through BatchGroup
*
* #ORM\Table(name="batch_group_application")
* #ORM\Entity()
*/
class BatchGroupApplication
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="BatchGroup", inversedBy="batchGroupApplications")
* #ORM\JoinColumn(name="batch_group_id", referencedColumnName="id")
**/
private $batchGroup;
/**
* #ORM\ManyToOne(targetEntity="Application", inversedBy="batchGroupApplications")
* #ORM\JoinColumn(name="application_id", referencedColumnName="id")
**/
private $application;
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set batchGroup
*
* #param \ModelBundle\Entity\BatchGroup $batchGroup
* #return BatchGroupApplication
*/
public function setBatchGroup(\ModelBundle\Entity\BatchGroup $batchGroup = null)
{
$this->batchGroup = $batchGroup;
return $this;
}
/**
* Get batchGroup
*
* #return \ModelBundle\Entity\BatchGroup
*/
public function getBatchGroup()
{
return $this->batchGroup;
}
/**
* Set application
*
* #param \ModelBundle\Entity\Application $application
* #return BatchGroupApplication
*/
public function setApplication(\ModelBundle\Entity\Application $application = null)
{
$this->application = $application;
return $this;
}
/**
* Get application
*
* #return \ModelBundle\Entity\Application
*/
public function getApplication()
{
return $this->application;
}
}
Application Entity
<?php
namespace ModelBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Doctrine\Common\Collections\ArrayCollection;
/**
* Application
*
* #ORM\Table(name="application")
* #ORM\Entity(repositoryClass="ModelBundle\Repository\ApplicationRepository")
*/
class Application
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
// ............
/**
* #ORM\OneToMany(targetEntity="BatchGroupApplication", mappedBy="application", cascade={"remove"})
**/
private $batchGroupApplications;
public function __construct()
{
$this->batchGroupApplications = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
// ............
/**
* Add batchGroupApplications
*
* #param \ModelBundle\Entity\BatchGroupApplication $batchGroupApplications
* #return Application
*/
public function addBatchGroupApplication(\ModelBundle\Entity\BatchGroupApplication $batchGroupApplications)
{
$this->batchGroupApplications[] = $batchGroupApplications;
return $this;
}
/**
* Remove batchGroupApplications
*
* #param \ModelBundle\Entity\BatchGroupApplication $batchGroupApplications
*/
public function removeBatchGroupApplication(\ModelBundle\Entity\BatchGroupApplication $batchGroupApplications)
{
$this->batchGroupApplications->removeElement($batchGroupApplications);
}
/**
* Get batchGroupApplications
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getBatchGroupApplications()
{
return $this->batchGroupApplications;
}
}
ApplicationRepository
Search function in this repository is being called from controller and from data is being sent to it via parameter that is in key value pair format, because form may have more properties so i'm checking if user is searching through batchGroup as well if yes it should combine the result from there as well.
namespace ModelBundle\Repository;
use Doctrine\ORM\EntityRepository;
/**
* ApplicationRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
class ApplicationRepository extends EntityRepository
{
/**
* Search Applications
*
* data: search form data in key => value fashion
*
*
*/
public function search($data)
{
$em = $this->getEntityManager();
$applicationRepository = $em->getRepository('ModelBundle:Application');
$qb = $applicationRepository->createQueryBuilder('a');
foreach ($data as $field => $value) {
if($field=="batchGroupApplications"){
$qb->andWhere(":value_$field MEMBER OF a.batchGroupApplications")->setParameter("value_$field", $value);
}else{
$qb->andWhere("a.$field = :value_$field")->setParameter("value_$field", $value);
}
}
$query = $qb->getQuery();
$result = $query->getResult();
return $result;
}
}
Controller
Here i'm creating a search form with batchGroupApplications dropdown, where user will choose any batch group and submit the form, once submitted user should be able to see all applications which are in that group but i'm getting nothing!
namespace SISBundle\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use ModelBundle\Entity\Batch;
use SISBundle\Form\BatchType;
use ModelBundle\Entity\Application;
use DefaultBundle\Form\ApplicationType;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use SISBundle\Form\SearchApplicationType;
use ModelBundle\Entity\Group;
use ModelBundle\Entity\BatchGroup;
/**
* Admission controller.
*
* #Route("/admission")
*/
class AdmissionController extends Controller
{
// ..............
/**
* List Applications for selected Batch
*
* #Route("/{batch}/applications")
*
* #Method({"GET", "POST"})
* #Template()
*/
public function applicationsAction(Request $request, Batch $batch)
{
$em = $this->getDoctrine()->getManager();
if( $batch != null ){
$applications = $batch->getApplications();
/*
* Search Applications
*
*/
$searchForm = $this->createForm(SearchApplicationType::class, null, array(
'action' => $this->generateUrl('sis_admission_applications', array('batch'=>$batch->getId())),
'method' => 'POST',
'attr' => array(
'novalidate' => 'novalidate',
'id' => 'sis_application_search_form',
),
));
// Manually Populating groups dropdown based on their availability for current loaded batch
$searchForm->add('batchGroupApplications', ChoiceType::class, array(
'choices' => $batch->getBatchGroups(),
'choices_as_values' => true,
'multiple'=>false,
'choice_label'=>'title',
'placeholder'=>' - Select Group..',
)
);
$searchForm->handleRequest($request);
if ($searchForm->isSubmitted()) {
// data is an array with "firstname", "middlename", and "lastname" keys etc
$data = $searchForm->getData();
$applications = $em->getRepository('ModelBundle:Application')->search($data);
}
/*********************************** end: Search Applications ***************************************/
return array(
'batch' => $batch,
'applications' => $applications,
'searchForm' => $searchForm->createView(),
);
}
}
}
Again, please suggest how can i search applications through batchGroup in andWhere fashion as search form may have some more fields which are available in application entity, for other fields it's working fine but this search by BatchGroup is somewhat tricky may be because of complicated associations etc.
Looking forward to solution..

Doctrine returns many-to-many collection in incorrect order

I'm starting to struggle with finding a solution for this online/offline so maybe someone has an answer or suggestion of best approach.
The problem is that a Question entity has a many-to-many relation tags which should return Tag entities in alphabetical order. The generated SQL is correct, but the collection which is returned doesn't follow the order.
I'm starting to lean towards using:
a) lifecycle events to call a method which will sort the tags collection.
b) on getTags() method call a method which will check if collection was sorted (for performance to avoid multiple sorts) and then sort the collection if it's the first time.
Does anyone know why collection is not sorted or which solution will be best?
Some info of code:
Generated SQL
SELECT
q0_.id AS id0,
q0_.question AS question1,
t1_.id AS id2,
t1_.name AS name3
FROM question q0_
LEFT JOIN questions_tags q2_ ON q0_.id = q2_.question_id
LEFT JOIN tag t1_ ON t1_.id = q2_.tag_id
ORDER BY
q0_.question ASC,
t1_.name ASC
Available data
INSERT INTO `question` (`id`,`question`) VALUES (1,'Who are you?');
INSERT INTO `questions_tags` (`question_id`,`tag_id`) VALUES (1,1);
INSERT INTO `questions_tags` (`question_id`,`tag_id`) VALUES (1,2);
INSERT INTO `questions_tags` (`question_id`,`tag_id`) VALUES (1,3);
INSERT INTO `tag` (`id`,`name`) VALUES (1,'personal');
INSERT INTO `tag` (`id`,`name`) VALUES (2,'jobs');
INSERT INTO `tag` (`id`,`name`) VALUES (3,'achievements');
Partial dump of getQuestions() method
Doctrine\ORM\PersistentCollection {#3131
-owner: AppBundle\Entity\Question {#3118
-id: 1
-question: "Who are you?"
-tags: Doctrine\ORM\PersistentCollection {#3131}
}
-coll: Doctrine\Common\Collections\ArrayCollection {#3127
-_elements: array:3 [
0 => AppBundle\Entity\Tag {#3121
-id: 1
-name: "personal"
}
1 => AppBundle\Entity\Tag {#3123
-id: 2
-name: "jobs"
}
2 => AppBundle\Entity\Tag {#3074
-id: 3
-name: "achievements"
}
]
}
}
Question entity
<?php
namespace AppBundle\Entity;
use AppBundle\Entity\Tag;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Question
*
* #ORM\Table(name="question")
* #ORM\Entity(repositoryClass="AppBundle\Entity\QuestionRepository")
*/
class Question
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="question", type="text")
*/
private $question;
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="questions", cascade={"persist"})
* #ORM\JoinTable(name="questions_tags")
* #ORM\OrderBy({"name"="ASC"})
*/
private $tags;
public function __construct()
{
$this->tags = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set question
*
* #param string $question
* #return Question
*/
public function setQuestion($question)
{
$this->question = $question;
return $this;
}
/**
* Get question
*
* #return string
*/
public function getQuestion()
{
return $this->question;
}
/**
* Add tags
*
* #param \AppBundle\Entity\Tag $tags
* #return Question
*/
public function addTag(Tag $tags)
{
$this->tags[] = $tags;
return $this;
}
/**
* Remove tags
*
* #param \AppBundle\Entity\Tag $tags
*/
public function removeTag(Tag $tags)
{
$this->tags->removeElement($tags);
}
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
}
Tag entity
<?php
namespace AppBundle\Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;
/**
* Tag
*
* #ORM\Table(name="tag")
* #ORM\Entity(repositoryClass="AppBundle\Entity\TagRepository")
*/
class Tag
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=128)
*/
private $name;
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Question", mappedBy="tags")
*/
private $questions;
public function __construct()
{
$this->questions = new ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* #param string $name
* #return Tag
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* #return string
*/
public function getName()
{
return $this->name;
}
/**
* Add questions
*
* #param \AppBundle\Entity\Question $questions
* #return Tag
*/
public function addQuestion(\AppBundle\Entity\Question $questions)
{
$this->questions[] = $questions;
return $this;
}
/**
* Remove questions
*
* #param \AppBundle\Entity\Question $questions
*/
public function removeQuestion(\AppBundle\Entity\Question $questions)
{
$this->questions->removeElement($questions);
}
/**
* Get questions
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getQuestions()
{
return $this->questions;
}
public function __toString()
{
return $this->name;
}
}
QuestionRepository
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\EntityRepository;
class QuestionRepository extends EntityRepository
{
/** #return Question[] */
public function getQuestions()
{
return $this->createQueryBuilder('q')
->select(['q', 't'])
->leftJoin('q.tags', 't')
->orderBy('q.question', 'ASC')
->getQuery()
->execute()
;
}
}
EDIT 2015-03-27:
For the time being I chose to extend the question entity with sort method:
<?php
// ...
class Question
{
// ...
/** #var boolean a flag to check if tags collection was sorted */
private $isTagsSorted = false;
// ...
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
$this->sortTagsAlphabetically();
return $this->tags;
}
/** #return void */
private function sortTagsAlphabetically()
{
if ($this->isTagsSorted) {
return;
}
$tags = $this->tags->toArray();
usort($tags, function ($a, $b) {
return $a->getName() > $b->getName();
});
$this->tags = new ArrayCollection($tags);
}
}
You need a subquery to achieve this, because the order by affect the main query so obviously it will try to order first by question it does it and then try to order the questions by tag (not the tags). So something like this should do it :
SELECT
q0_.id AS id0,
q0_.question AS question1,
t1_.id AS id2,
t1_.name AS name3
FROM question q0_
LEFT JOIN questions_tags q2_ ON q0_.id = q2_.question_id
LEFT JOIN (select * from tag t1_ ORDER BY t1_.name ) t1_
ON t1_.id = q2_.tag_id
ORDER BY
q0_.question ASC,
t1_.name ASC
For the DQL i am sorry but i think you will have to do it the old fashion way NativeSql since doctrine doesnt support joining subqueries with dql :
https://groups.google.com/forum/#!msg/doctrine-user/0rNbXlD0E_8/xMNiQgp9c3QJ
But nativesql would be fine :) also :
http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/native-sql.html
Just use the query i gave you, change it a bit to be readable then use :
$sql = "your query"
$connection = $this->getEntityManager()->getConnection();
$query = $connection->prepare($sql);
$query->execute();
$results = $query->fetchAll();
return $results;
The annotation of ordering is used only if the content is not already fetched, as example if you have fetch from the dm only the .. if you call the get.. method the result is sorted.
In your case, you get all the data in one shot, so the annotation doesn't work as expected.
In your case you can add a mehod in the Tag entity for sort the data on the fly, as example:
public function getSortedTags()
{
$criteria = Criteria::create()
->orderBy(array("name" => Criteria::ASC));
return $this->tags->matching($criteria);
}
Hope this help

Symfony 2 - findall() too many DB queries

why Symfony2 performs 40 DB queries if I use following code:
$em = $this->getDoctrine()->getManager();
$records = $em->getRepository('MyWebBundle:Highlight')->findAll();
I thought that findAll() method returns only all items from Highlight entity and associations to other entities replaces Proxy objects. But now findAll() method gettings all associations entities.
Do you know where is the problem ?
indexAction
public function indexAction() {
$em = $this->getDoctrine()->getManager();
$records = $em->getRepository('MyWebBundle:Highlight')->findAll();
$csrf = $this->get('security.csrf.token_manager');
$token = $csrf->refreshToken(self::FORM_TOKEN_ID);
$params = array(
"data" => array(
"all" => $records,
),
"token" => $token->getValue(),
"static" => array(
"add" => $this->generateUrl("admin_highlight_add"),
"edit" => $this->generateUrl("admin_highlight_edit"),
"del" => $this->generateUrl("admin_highlight_del"),
),
);
$ser = $this->get('jms_serializer');
$jsonContent = $ser->serialize($params, 'json');
return array('jsonContent' => $jsonContent);
}
Highlight entity
namespace My\WebBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* Highlight
*
* #JMS\ExclusionPolicy("none")
* #ORM\Table()
* #ORM\Entity(repositoryClass="My\WebBundle\Entity\HighlightRepository")
*/
class Highlight {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="abbreviation", type="string", length=8, unique=true)
*/
private $abbreviation;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=80, nullable=true)
*/
private $description;
/**
* #var string
*
* #ORM\Column(name="color", type="string", length=7)
*/
private $color;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Goods", mappedBy="highlight")
*/
private $goods;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Calibration", mappedBy="highlight")
*/
private $calibrations;
/**
* Constructor
*/
public function __construct() {
$this->goods = new \Doctrine\Common\Collections\ArrayCollection();
$this->calibrations = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set abbreviation
*
* #param string $abbreviation
* #return Highlight
*/
public function setAbbreviation($abbreviation) {
$this->abbreviation = $abbreviation;
return $this;
}
/**
* Get abbreviation
*
* #return string
*/
public function getAbbreviation() {
return $this->abbreviation;
}
/**
* Set description
*
* #param string $description
* #return Highlight
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription() {
return $this->description;
}
/**
* Set color
*
* #param string $color
* #return Highlight
*/
public function setColor($color) {
$this->color = $color;
return $this;
}
/**
* Get color
*
* #return string
*/
public function getColor() {
return $this->color;
}
/**
* Add goods
*
* #param \My\WebBundle\Entity\Goods $goods
* #return Highlight
*/
public function addGood(\My\WebBundle\Entity\Goods $goods) {
$this->goods[] = $goods;
return $this;
}
/**
* Remove goods
*
* #param \My\WebBundle\Entity\Goods $goods
*/
public function removeGood(\My\WebBundle\Entity\Goods $goods) {
$this->goods->removeElement($goods);
}
/**
* Get goods
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getGoods() {
return $this->goods;
}
/**
* Add calibrations
*
* #param \My\WebBundle\Entity\Calibration $calibrations
* #return Highlight
*/
public function addCalibration(\My\WebBundle\Entity\Calibration $calibrations) {
$this->calibrations[] = $calibrations;
return $this;
}
/**
* Remove calibrations
*
* #param \My\WebBundle\Entity\Calibration $calibrations
*/
public function removeCalibration(\My\WebBundle\Entity\Calibration $calibrations) {
$this->calibrations->removeElement($calibrations);
}
/**
* Get calibrations
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCalibrations() {
return $this->calibrations;
}
}
Highlight repository is empty
I think the problem comes from the serializer. Since you serializer highliths, each of them has their properties serialized as well which means that lazy query will be performed to retieved Goods which will be also serialized.
You should then prevent this behaviour by adding annotations to highlight's goods property as this
use ...
use JMS\SerializerBundle\Annotation\ExclusionPolicy;
use JMS\SerializerBundle\Annotation\Exclude;
/**
* ...
* #ExclusionPolicy("none")
*/
class Highlight
{
/**
* ...
* #Exclude
*/
private $goods;
}
You can have further details about exclusion stratigies from JMSSerializer doc
findAll itself does not perform many queries. Queries are executed when you access related entity via getters. As relation are not fetched eagerly, they first time are fetched when you are acessing them.
I think serializer access all children to send your object.
See Doctrine documentation
Whenever you have a managed entity instance at hand, you can traverse
and use any associations of that entity that are configured LAZY as if
they were in-memory already. Doctrine will automatically load the
associated objects on demand through the concept of lazy-loading.
To prevent this either disable children serialization or use fetch EAGER or build a DQL query, which prefetch all the children alongside with parents, like (just sample, not valid DQL)
SELECT Highlight, Good, Calibration
FROM Highlights Highlight
LEFT JOIN Highlight.googs Good
LEFT JOIN Goog.calibrations Calibration
WHERE ...

Resources