Symfony3 Fetching data from entities with ManyToMany association - symfony

I have a problem with ManyToMany association. I cant't figure out how to fetch data from database.
I have 2 entities - Teacher and Subject.
Class Subject
/**
*
* #ORM\ManyToMany(targetEntity="Teacher", mappedBy="subjects")
*/
private $teachers;
public function __construct() {
$this->teachers = new ArrayCollection();
}
/**
* Add teacher
*
* #param \AppBundle\Entity\Teacher $teacher
*
* #return Subject
*/
public function addTeacher(\AppBundle\Entity\Teacher $teacher)
{
$this->teachers[] = $teacher;
return $this;
}
/**
* Remove teacher
*
* #param \AppBundle\Entity\Teacher $teacher
*/
public function removeTeacher(\AppBundle\Entity\Teacher $teacher)
{
$this->teachers->removeElement($teacher);
}
/**
* Get teachers
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTeachers()
{
return $this->teachers;
}
And entity Teacher
Class Teacher
/**
*
* #ORM\ManyToMany(targetEntity="Subject", inversedBy="teachers")
*/
private $subjects;
public function __construct()
{
$this->subjects = new ArrayCollection();
}
/**
* Add subject
*
* #param \AppBundle\Entity\Subject $subject
*
* #return Teacher
*/
public function addSubject(\AppBundle\Entity\Subject $subject)
{
$this->subjects[] = $subject;
return $this;
}
/**
* Remove subject
*
* #param \AppBundle\Entity\Subject $subject
*/
public function removeSubject(\AppBundle\Entity\Subject $subject)
{
$this->subjects->removeElement($subject);
}
/**
* Get subjects
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getSubjects()
{
return $this->subjects;
}
All i want to do is fetch all information from Teacher + Subjects associated with them.
How should looks like my controller to do this?
I try do this by
$teacher = $this->getDoctrine()->getRepository(Teacher::class)->findAll();
return $this->render('admin/adminDashboard.html.twig', [
'teachers' => $teacher]);
But i still got errors

First, my advice is to change your $teacher variable to $teachers as
findAll() method returns an array so you have to iterate through it. In your controller:
foreach($teachers as $teacher) { // Here you have $teacher and you can do $teacher->getSubjects() }
In your twig template:
{% for teacher in teachers %}
{{ dump(teacher) }}
{% endfor %}
If that doesnt work, let us know what kind of errors do you get.
This is all assuming that you have entries inside your database, or from wherever you're fetching your entities.

if {% for t in teachers %}{{dump(t.subjects.count)}}{% endfor %} worked, it means the data is there.
If you mean {{teacher.subjects}} throws error: An exception has been thrown during the rendering of a template ("Catchable Fatal Error: Object of class Doctrine\ORM\PersistentCollection could not be converted to string. Well in case you have never seen this error before, take a close read:
Object of class Doctrine\ORM\PersistentCollection refers to subjects, which as a collection that it is, cannot be just thrown into an html tag like a string.
Depends on how you want to present your data but the very basics would be:
{% for t in teachers %}
<tr>
{% for s in t.subjects %}
<td>s.getName</td>
{% endfor %}
</tr>
{% endfor %}
assuming subject entity has a property name and a getter for it. Hope you get the gist.
let me know

Related

Can't figure out how to make the correct repository method I need

I am trying to make a QueryBuilder with a join between my entity Product and my entity Store. I'd like to display all products that are related to OneStore. They have a many-to-many relation. I can't figure out how to get the ID's. Welp please. Thanks in advance!
here's a screenshot of my product_store table :
https://imgur.com/a/wUZMEEn
So if I'm clear enough and my english legit, you understood that I'd like to display only the product that are related to one store id.
Let's make an example. If I'm on the /store/detail/{1} page, I'd like to display product id 1,3,6,8,10,13 since they are related to store.id = 1.
So I updated as I have been advised, but it's not working, and I don't get what is wrong. Where am I mistaking? (controller updated)
Here's the error I get :
https://imgur.com/a/q3Uun3E
----------------------my repository function -----------------------------
public function getProductsForStore($sid)
{
return $this->createQueryBuilder('s')
->join('s.product', 'p')
->addSelect('p')
->andWhere('s.id = :sid')
->orderBy('p.name', 'ASC')
->setParameter('sid', $sid)
->getQuery()
->getResult();
}
$produt in store entity :
/**
*
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="App\Entity\Product", mappedBy="store")
* * #ORM\JoinTable(name="product_store",
* joinColumns={#ORM\JoinColumn(name="product_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="store_id", referencedColumnName="id")}
* )
*/
private $product;
$store in product entity :
/**
* * #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="App\Entity\Store", inversedBy="product")
* * #ORM\JoinTable(name="product_store",
* joinColumns={#ORM\JoinColumn(name="store_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="product_id", referencedColumnName="id")}
* )
*/
private $store;
my controller function :
/**
* #Route("stores/detail/{id}", name="detail_store"))
*/
public function getOneStore(StoreRepository $repository, Store $store): Response
{
$store = $repository->findOneBy(array(
'id' => $store->getId(),
));
$products_store = $repository->getProductsForStore(':id');
return $this->render('store.html.twig', array(
'store' => $store,
'product' => $products_store,
));
}
you can better call your properties $stores and $products instead of the singular version to make clear that we talk about an array or collection with possible multiple stores or products. You dont really need the 2nd - 4th rule of your annotations. Just
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Product", mappedBy="store")
*/
and
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Store", inversedBy="product")
*/
will do. It tells us that you use a bidirectional ManyToMany relation. Both Store and Product entities hold a list of the opposite enitity. Therefor you can query Stores with a collection of Products or Products with a collection of Stores.
and what is the correct way to use the $store->getProducts() in my controller?
So exchange store for product and vica versa in the first answer:
public function getStore($sid)
{
return $this->createQueryBuilder('s')
->join('s.product', 'p')
->addSelect('p')
->andWhere('s.id = :sid')
->orderBy('p.name', 'ASC')
->setParameter('sid', $sid);
->getQuery()
->getResult()
;
}
Controller:
/**
* #Route("/{id}", name="store_show", methods={"GET"})
*/
public function show($id): Response
{
$em = $this->getDoctrine()->getManager();
$em->getRepository('App:Store')->getStore($id);
if(null === $store) {
throw $this->createNotFoundException('Store not found.');
}
return $this->render('store/show.html.twig', [
'store' => $store,
]);
}
Twig:
{% extends 'base.html.twig' %}
{% block title %}Store{% endblock %}
{% block body %}
<h1>{{ store.name }}</h1>
<table class="table">
<thead>
<tr>
<th>ProductId</th>
<th>ProductName</th>
</tr>
</thead>
<tbody>
{% for product in store.products %} {# loop through all products in store #}
<tr>
<td>{{ product.id }}</td>
<td>{{ product.name }}</td>
</tr>
{% else %}
<tr>
<td colspan="4">no records found</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
I do wonder why you make a custom query if you want to get all products from one store.
When you made your ManyToMany relation (with maker bundle hopefully), you should also have added in Store entity a getter ($store->getProducts()) to get all products of said store.
Anyway, here is your query...
public function getAllProductsFromOneStore(Store $store) {
$qb=$this->createQueryBuilder('store');
$qb->addSelect('product')
->join('store.product', 'product')
->andWhere('store.id = :storeId')
->setParameters(array(
'storeId'=>$store->getId();
));
return $qb->getQuery->getResult();
}
The above query should return all products from one store.
Also, never use where(), always andWhere(). Read more about it here.
[EDIT]
In response to your edit. As Frank B said, you can do better.
First, as I said, you don't need a custom query.
Instead, make a getter in Store and Product entity.
src/Entity/Store.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
class Store {
/**
* #ORM\ManyToMany(targetEntity="App\Entity\Product", inversedBy="stores")
*/
private $products;
public function __construct() {
$this->products=new ArrayCollection();
}
/**
* GETTER
*
* #return Collection|Product[]
*/
public function getProducts(): Collection {
return $this->products;
}
/**
* ADD A PRODUCT TO A STORE
*/
public function addProduct(Product $product): self {
if(!$this->products->contains($product)) {
$this->products[]=$product;
}
return $this;
}
/**
* REMOVE A PRODUCT FROM A STORE
*/
public function removeProduct(Product $product): self {
if($this->products->contains($product)) {
$this->products->removeElement($product);
}
return $this;
}
}
src/Entity/Product.php
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
class Product {
private $stores;
public function __construct() {
$this->stores=new ArrayCollection();
}
/**
* GETTER
*
* #return Collection|Store[]
*/
public function getStores(): Collection {
return $this->stores;
}
/**
* ADD A STORE TO A PRODUCT
*/
public function addStore(Store $store): self {
if(!$this->stores->contains($store)) {
$this->stores[]=$store;
$store->addProduct($this);
}
return $this;
}
/**
* REMOVE A STORE FROM A PRODUCT
*/
public function removeStore(Store $store): self {
if($this->stores->contains($store)) {
$this->stores->removeElement($store);
$store->removeProduct($this);
}
return $this;
}
}
Now in your controller, all you have to do is to call your getter.
Doctrine will handle the query for you :
/**
* #Route("stores/store-{id}", name="index_store"))
*/
public function index(Store $store): Response {
$products=$store->getProducts();
return $this->render('store.html.twig', array(
'store'=>$store,
'products'=>$products,
));
}
And then in your twig view :
{% for product in store.products %}
// Your code here
{% endfor %}
And that is it. Once you have your getter in your entities, you don't need a custom query.
Take note that you can do the same with Product ($product->getStores()) to know in which store you can find your product.

Twig_Error_Runtime an exception has been thrown during the rendering of a template

Object of class Proxies__CG__\AppBundle\Entity\Formation could not be converted to float
in vendor\easycorp\easyadmin-bundle\src\Resources\views\default\field_integer.html.twig (line 4)
{% if field_options.format %}
{{ field_options.format|format(value) }}
{% else %}
{{ value|number_format }}
{% endif %}
The error is fixed by commenting out my getters and setters for my foreign key in my entity i'm trying to set them up because they show inaccessible in my easy-admin Bundle
Entity code :
/**
* Questions
*
* #ORM\Table(name="questions", indexes={#ORM\Index(name="question_form_id", columns={"formid"})})
* #ORM\Entity
*/
/**
* #var \AppBundle\Entity\Formation
*
* #ORM\ManyToOne(targetEntity="AppBundle\Entity\Formation")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="formid", referencedColumnName="Form_id")
* })
*/
private $formid;
getters and setters :
/**
* #return Formation
*/
public function getFormid()
{
return $this->formid;
}
/**
* #param Formation $formid
*/
public function setFormid($formid)
{
$this->formid = $formid;
}
Fixed it, the #param was using the wrong column, sorry.

Symfony / Twig - Recursion decrease db queries

I have my category tree in twig template:
{% for category in categories %}
<li>
<div class="li">{{ category.name|trans({}, 'categories')|raw }}</div>
{% if category.children is not empty %}
<ul>
{% include "default/_menu_links.html.twig" with {'categories':category.children} only %}
</ul>
{% endif %}
</li>
{% endfor %}
It creates +- 53 database queries if I have 6 categories and 7 subcategories in each single category.
Is there a way to decrease that number?
I'm using useResultCache(true) in doctrine, but looks like it's not loading from the cache(at least not in dev mode).
How are you handling category trees?
UPDATE:
Entity:
...
/**
* One Category has Many Subcategories.
* #ORM\OneToMany(targetEntity="Category", mappedBy="parent", cascade={"persist"}))
*/
private $children;
/**
* Many Subcategories have One Category.
* #ORM\ManyToOne(targetEntity="Category", inversedBy="children", cascade={"persist"})
* #ORM\JoinColumn(name="parent_id", referencedColumnName="id")
*/
private $parent;
...
/**
* Add child
*
* #param \App\Entity\Product\Category $child
*
* #return Category
*/
public function addChild(\App\Entity\Product\Category $child): Category
{
$this->children[] = $child;
return $this;
}
/**
* Remove child
*
* #param \App\Entity\Product\Category $child
*/
public function removeChild(\App\Entity\Product\Category $child)
{
$this->children->removeElement($child);
}
/**
* Get children
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getChildren(): Collection
{
return $this->children;
}
/**
* Set parent
*
* #param \App\Entity\Product\Category $parent
*
* #return Category
*/
public function setParent(\App\Entity\Product\Category $parent = null): Category
{
$this->parent = $parent;
return $this;
}
/**
* Get parent
*
* #return \App\Entity\Product\Category
*/
public function getParent()
{
return $this->parent;
}
...
According to my comment: you have to JOINyour subcategories. I'm assuming you are currently doing something like this to retrieve your categories:
public function getCategories()
{
return $this->getEntityManager()->createQueryBuilder()
->select("category")
->from("App:Category", "category")
->where("category.parent IS NULL")
->getQuery()->getResult();
}
So if you are now iterating over this array of categories and trying to access the children property, a sub query will be fired for each children which causes this high amount of database queries.
Instead you should JOIN them like this:
public function getCategories()
{
return $this->getEntityManager()->createQueryBuilder()
->select("category", "subcat")
->from("App:Category", "category")
->leftJoin("category.children", "subcat")
->where("category.parent IS NULL")
->getQuery()->getResult();
}
If you now iterate over your categories and access the children property, no extra query will be fired!
Using the query above and this snippet in twig will result only in one database query:
{% for category in categories %}
<p>cat={{ category.id }}</p>
{% for subcat in category.children %}
<p>subcat={{ subcat.id }}</p>
{% endfor %}
<hr>
{% endfor %}

Symfony Doctrine entity friend hasFriend followers

I need to set follower, following (myFriends) hasFriend logic to my project. column "odobera" means "following" to example nick(id user) odobera (following to this user). User (25) is following user(37).
Requests table:
User entity:
/**
* #ORM\OneToMany(targetEntity="TB\RequestsBundle\Entity\Requests", mappedBy="odobera")
*/
protected $followers;
/**
* #ORM\OneToMany(targetEntity="TB\RequestsBundle\Entity\Requests", mappedBy="nick")
*/
protected $myFriends;
public function __construct()
{
parent::__construct();
$this->followers = new \Doctrine\Common\Collections\ArrayCollection();
$this->myFriends = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get myFriends
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getMyFriends()
{
return $this->myFriends;
}
/**
*
* #param \TB\UserBundle\Entity\User $user
* #return bool
*/
public function hasFriend(User $user)
{
return $this->myFriends->contains($user);
}
class Requests
{
/**
* #ORM\ManyToOne(targetEntity="TB\UserBundle\Entity\User", inversedBy="myFriends")
* #ORM\JoinColumn(name="nick", referencedColumnName="id")
*/
protected $nick;
/**
* #ORM\ManyToOne(targetEntity="TB\UserBundle\Entity\User",inversedBy="followers")
* #ORM\JoinColumn(name="odobera", referencedColumnName="id")
*/
protected $odobera;
In controller:
$myFollowers=$user->getMyFriends();
returns:
What is good: returns 1 record. I actually follow just one person as you can see here the id of record is 24395
DB requests table:
I don't know if is good that getMyFriends function returns response in that "format". Please look at it carefully.
Then I have select followers from query and in loop:
{% for follower in followers %}
and i print data like this (works greate) {{ follower.nick }}
or if i want some fields from user entity {{ follower.nick.rank }}
{% endfor %}
But the problem is here:
{% if (app.user.hasFriend(follower.nick)) %}
That returns false, why? I follow this user as I checked in controller with dump :P Few lines over.
The problem seems to be that you are comparing two different type variable.
When you do this: {% if (app.user.hasFriend(follower.nick)) %} the following function is called:
/**
*
* #param \TB\UserBundle\Entity\User $user
* #return bool
*/
public function hasFriend(User $user)
{
return $this->myFriends->contains($user);
}
This function is called, taking a User type $user variable and you then use the contains() function on $this->myFriends.
$this->myFriends is an ArrayCollection of Requests (so different type than User) and from the doctrine documentation about contains():
The comparison of two elements is strict, that means not only the
value but also the type must match.
http://www.doctrine-project.org/api/common/2.1/class-Doctrine.Common.Collections.ArrayCollection.html

New entities added using Symfony2 many-to-one embedded forms not being saved

This is related to my earlier question about embedded forms. As advised, I switched to a twig template and now everything is displaying as expected and the link to add a new empty form is working correctly. The problem is that when I try to save a new record, it doesn't work (although edits to existing entities are saved).
There are several points where I may have gone wrong, so I'm going to ask questions as I go along.
Here's some background:
A study can have many participants (i.e. a Study entity has a OneToMany relationship with the entity Participant). In the database, each Participant record has the foreign key link from the column "study" to the "study_id" column of a record in the Study table, making it the owning side of the relation. The annotation in the classes should reflect this relationship.
Study class:
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CRUK\MyBundle\Entity\Study
*
* #ORM\Table(name="study")
* #ORM\Entity
*/
class Study
{
/**
* #var integer $id
*
* #ORM\Column(name="study_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string $studyName
*
* #ORM\Column(name="study_name", type="string", length=50, nullable=false)
*/
private $studyName;
/*
* #ORM\OneToMany(targetEntity="Participant", mappedBy="study", cascade={"persist"})
*
* #var ArrayCollection $participants
*/
protected $participants;
public function __construct()
{
$this->participants = new ArrayCollection();
}
public function setParticipants(ArrayCollection $participants)
{
foreach($participants as $participant) {
$participant->setStudy($this);
}
$this->participants = $participants;
}
/**
* #return ArrayCollection A Doctrine ArrayCollection
*/
public function getParticipants()
{
return $this->participants;
}
}
My Participant class:
namespace MyBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* CRUK\SampleTrackingBundle\Entity\Participant
*
* #ORM\Table(name="participant")
* #ORM\Entity
*/
class Participant
{
/**
* #var integer $id
*
* #ORM\Column(name="participant_id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
...
/**
* #var study
*
* #ORM\ManyToOne(targetEntity="Study", inversedBy="participants")
* #ORM\JoinColumn(name="study", referencedColumnName="study_id")
*
*/
private $study;
//setters and getters...
}
First of all, are these annotations correct? (I'm pretty sure I got the whole owning/inverse many-to-one/one-to many relationship straight in my head, but I could be mistaken)
My controller:
Class StudyController extends Controller
{
...
public function addParticipantsAction($id)
{
$em = $this->getDoctrine()->getEntityManager();
$entity = $em->getRepository('MyBundle:Study')->find($id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find Study id='.$id);
}
$participantArray = $em->getRepository('MyBundle:Participant')->findByStudy($id);
//this is supposed to return a Doctrine ArrayCollection, but for some reason, returns an array
// This needs to be converted to an ArrayCollection
$participants = new ArrayCollection();
foreach ($participantArray as $participant) {
$participants->add($participant);
}
$entity->setParticipants($participants);
$form = $this->createForm(new StudyType(), $entity);
$request = $this->getRequest();
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
}
}
return $this->render('MyBundle:Study:addParticipants.html.twig', array(
'form' => $form->createView(),
'entity' => $entity
));
}
...
}
At this point I have to ask why it is neccessary to explicitly fetch the collection of participants and use it to set the collection on the study entity? Before I added that code, $entity->getParticipants() would return null (even when I know there were several participants with the foreign key set for the study). I have two other tables in a many-to-many relationship where the collections seem to come up automatically just by having the correct annotations in the entity classes. Is this a difference between a many-to-many mapping vs. a many-to-one, or have I messed up the annotation somehow?
I'm not sure if the rest of the code will help, but here's some more:
My study form class:
class StudyType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('studyName', null, array('label'=> 'Study Name:'))
->add('participants', 'collection', array(
'type'=> new ParticipantType(),
'allow_add'=>true,
'by_reference'=>false
));
}
public function getName()
{
return 'study';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'MyBundle\Entity\Study',
);
}
}
My embedded form class:
class ParticipantType extends AbstractType
{
public function buildForm(FormBuilder $builder, array $options)
{
$builder
->add('participantId','text', array('label'=>'Participant ID'))
));
}
public function getName()
{
return 'participant';
}
public function getDefaultOptions(array $options)
{
return array(
'data_class' => 'MyBundle\Entity\Participant',
);
}
}
My template:
{% extends 'MyBundle::base.html.twig' %}
{% block body%}
<form action="{{ path('study_addparticipants', { 'id': entity.id }) }}" method="POST" {{ form_enctype(form) }}>
<!-- renders global errors -->
{{ form_errors(form) }}
<h2>Study</h2>
{{ form_label(form.studyName) }}
{{ form_errors(form.studyName) }}
{{ form_widget(form.studyName) }}
<h3>Participants in this study</h3>
<ul class="participants" data-prototype="{{ form_widget(form.participants.get('prototype')) | e }}">
{% for participant in form.participants %}
<li>{{form_row(participant) }}</li>
{% endfor %}
</ul>
{{ form_rest(form) }}
<button type="submit">Save Changes</button>
</form>
{% endblock%}
{% block javascripts %}
{# parent block includes jQuery #}
{{ parent() }}
<script type='text/javascript'>
jQuery(document).ready(function() {
// keep track of how many participant fields have been rendered
var collectionHolder = $('ul.participants');
var $addLink = $('Add new Participant');
var $newLinkLi = $('<li></li>'). append($addLink);
collectionHolder.append($newLinkLi);
$addLink.on('click', function(e) {
e.preventDefault();
addParticipantForm(collectionHolder, $newLinkLi);
});
});
function addParticipantForm(collectionHolder, $newLinkLi) {
// Get the data-prototype we explained earlier
var prototype = collectionHolder.attr('data-prototype');
// Replace '$$name$$' in the prototype's HTML to
// instead be a number based on the current collection's length.
var newForm = prototype.replace(/\$\$name\$\$/g, collectionHolder.children().length);
// Display the form in the page in an li, before the "Add a tag" link li
var $newFormLi = $('<li></li>').append(newForm);
$newLinkLi.before($newFormLi);
}
</script>
{% endblock %}
So, the form displays correctly and when I click the "Add new participant" link, an empty participant form is appended. Changes to the Study and exisitng participant records are saved. There are no errors, but any new participants are not saved.
I have read many similar questions to this one and as far as I know, incorporated everything that should make this work. I've obviously missed something, so would appreciate any suggestions on how to put this right.
Many Thanks.
Thanks to #Luke for the advice. I have solved the problem by looping through the pariticpants collection of my study object and saving each one individually in my controller. The new contoller code:
...
if ('POST' === $request->getMethod()) {
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
foreach($entity->getParticipants() as $participant) {
$em->persist($participant);
}
// flush once to commit all entities
$em->flush();
}
}
...

Resources