Symfony\Doctrine createQueryBuilder() select 'not in' from a OneToMany relation - symfony

I have three entities : Trophy | Competition | Season
One Competition is created for one trophy for one season (you can't have two competitions with same combination "season + trophy").
Competition as a ManyToOne relation with Trophy, and a ManyToOne relation with Season.
Trophy and Season have no direct relation.
I want to display two dropdowns on a page with the content of the second one being dependent from the value of the first :
First dropdown allow to select a trophy type (which is a property of Trophy entity), second dropdown must list seasons that are "still available" for trophy type selected (meaning by that "list all seasons for which there are no competition for this trophy type")
I've got almost all working (listener in the Formtype, ajax etc) I've created a specific function allWithoutThisCompetitionType() in SeasonRepository. Function is correctly called every-time user select a new value in dropdown BUT... I don't know anything about SQL nor dql, so I'm struggling to find the correct formulation for my query. I've tried with notin(), with "sub" or "nested" queries... I definitely don't know what I'm doing...
How can I do something like ? :
$qb = $em->getRepository(\App\Entity\Seasonmanager\Season::class)->createQueryBuilder('s')
->where('s.competitions.trophy != :trophy')
->setParameter('trophy', $trophy);
= Here are all the seasons for which no competition has been already created with this trophy
Thank you for your help.
Trophy entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\Seasonmanager\TrophyRepository")
*/
class Trophy
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $uniqueid;
// other properties here...
//////////////////////////////////////////////////////////////////////////////////
//// LIAISONS VERS D'AUTRES ENTITY ////
/**
* #ORM\OneToMany(targetEntity="App\Entity\Seasonmanager\Competition", mappedBy="trophy", orphanRemoval=true)
*/
private $competitions;
Competition entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\Seasonmanager\CompetitionRepository")
* #UniqueEntity(
* fields={"trophy","season"},
* errorPath="trophy",
* message="Une compétition existe déjà pour ce trophée et cette saison"
* )
*/
class Competition
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
// other properties here...
//////////////////////////////////////////////////////////////////////////////////
//// LIAISONS VERS D'AUTRES ENTITY ////
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Seasonmanager\Trophy", inversedBy="competitions")
* #ORM\JoinColumn(nullable=false)
*/
private $trophy;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Seasonmanager\Season", inversedBy="competitions")
* #ORM\JoinColumn(nullable=false)
*/
private $season;
Season entity :
/**
* #ORM\Entity(repositoryClass="App\Repository\Seasonmanager\SeasonRepository")
* #UniqueEntity("yearin")
*/
class Season
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="integer", length=4)
*/
private $yearout;
/**
* #ORM\Column(type="string", length=8)
*/
private $uniqueid;
// other properties here...
//////////////////////////////////////////////////////////////////////////////////
//// LIAISONS VERS D'AUTRES ENTITY ////
/**
* #ORM\OneToMany(targetEntity="App\Entity\Seasonmanager\Competition", mappedBy="season", orphanRemoval=true)
*/
private $competitions;
The SeasonRepository where I try to add my query :
namespace App\Repository\Seasonmanager;
use App\Entity\Seasonmanager\Season;
use App\Entity\Seasonmanager\Trophy;
use App\Entity\Seasonmanager\Competition;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Common\Persistence\ManagerRegistry;
/**
* #method Season|null find($id, $lockMode = null, $lockVersion = null)
* #method Season|null findOneBy(array $criteria, array $orderBy = null)
* #method Season[] findAll()
* #method Season[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class SeasonRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Season::class);
}
public function allWithoutThisCompetitionType($type): array
{
$em = $this->getEntityManager();
$trophys = $em
->getRepository(Trophy::class)
->findBy(['uniqueid' => $type],['id'=>'DESC'])
;
$trophy = reset($trophys);
$qb = $em->getRepository(\App\Entity\Seasonmanager\Season::class)->createQueryBuilder('s')
->where('s.competitions.trophy', $trophy);
$query = $qb->getQuery();
$result = $query->getResult();
$donnees = $result;
return $donnees;
}

Here is the query, though, I'm not 100% sure it will match your need.
Let me know in comment if something is wrong, I will edit my answer.
public function allWithoutThisCompetitionType($trophy) {
// Split init from the rest of the query in case you need to use `$qb->expr()`
$qb=$this->createQueryBuilder("season");
$qb->leftJoin("season.competition", "competition") // Join competition
->join("competition.trophy", "trophy") // Join Trophy
->andWhere($qb->expr()->orX( // Or (either one of the following satements)
$qb->expr()->isNull("competition.id"),
$qb->expr()->notIn("trophy.uniqueid", ":trophy")))
->setParameter("trophy", $trophy);
return $qb->getQuery()->getResult();
}

Related

Symfony 2.8 Doctrine. Using composite primary key and ManyToMany associations

I have 2 entities - Platform and Product. In the Products table I have composite primary key by [Product ID + Platform ID]. One product can be present at many platforms so and one platform can contain many products, so the association is ManyToMany.
The Platform entity:
/**
* Platform
*
* #ORM\Table(name="Platforms")
*/
class Platform
{
/**
* #var int
*
* #ORM\Column(name="Platform_Id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="Platform_Name", type="string", length=64)
*/
private $name;
/**
* #ORM\ManyToMany(targetEntity="Product", mappedBy="platforms", cascade={"ALL"}, indexBy="numpp")
*/
protected $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function addProducts($numpp)
{
$this->products[$numpp] = new Product($numpp, $this);
}
The Product entity:
/**
* Product
*
* #ORM\Table(name="Products")
*/
class Product
{
/**
* #var string
*
* #ORM\Column(name="Numpp", type="string", length=6)
* #ORM\Id
*/
private $numpp;
/**
* #var int
*
* #ORM\ManyToMany(targetEntity="Platform", inversedBy="products")
* #ORM\JoinColumn(name="Platform_Id", referencedColumnName="Platform_Id")
* #ORM\Id
*/
private $platforms;
public function __construct($numpp, Platform $platform)
{
$this->numpp = $numpp;
$this->platforms = new ArrayCollection();
$this->platforms[] = $platform;
}
In my controller when trying to create new Product entity...
$em = $this->getDoctrine()->getManager();
$platform = $em->getRepository("AGAAnalyticsBundle:Platform")->find(1);
$product = new Product('05062', $platform);
$em->persist($product);
$em->flush();
I get an error - Cannot insert the value NULL into column 'Platform_Id', table 'dbo.Products'
And other way using addProduct method...
$em = $this->getDoctrine()->getManager();
$platform = $em->getRepository("AGAAnalyticsBundle:Platform")->find(1);
$platform->addProduct('05062');
$em->flush();
I get an error - The column id must be mapped to a field in class AGA\AnalyticsBundle\Entity\Platform since it is referenced by a join column of another class.
Please help to understand where I am wrong and how I should build this relation between my entities correctly.

doctrine composite key and one to many

I use Symfony 2.8. I have two table and in both the primary key is composed by 3 columns:
id, tipo_corso, comune
02, it, devi
01, en, capi
09, es, file
Obviously the two table have other different columns. I can't change the primary key by use only one or two columns. For one record in StranieriCRS table there are many record in EsoneroLingua table (OneToMany):
First entity:
class StranieriCRS
{
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
private $id;
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
private $tipo_corso;
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
private $comune;
public function __construct($id, $tipo_corso, $comune)
{
$this->id = $id;
$this->tipo_corso = $tipo_corso;
$this->comune = $comune;
$this->esonerolingua = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="EsoneroLingua", mappedBy="stranieriCRS", fetch="EAGER")
*/
private $esonerolingua;
/**
* Get esonerolingua
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getEsonerolingua()
{
return $this->esonerolingua;
}
Second entity:
class EsoneroLingua
{
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
private $id;
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
private $tipo_corso;
/**
* #ORM\Column(type="string")
* #ORM\Id
*/
private $comune;
public function __construct($id, $tipo_corso, $comune)
{
$this->id = $id;
$this->tipo_corso = $tipo_corso;
$this->comune = $comune;
}
/**
* #ORM\ManyToOne(targetEntity="StranieriCRS", inversedBy="esonerolingua")
* #ORM\JoinColumns(
* #ORM\JoinColumn(name="id", referencedColumnName="id"),
* #ORM\JoinColumn(name="tipo_corso", referencedColumnName="tipo_corso"),
* #ORM\JoinColumn(name="comune", referencedColumnName="comune"),
* )
*/
private $stranieriCRS;
The problem occur when I want get the StranieriCRS object because he give me as result only one result...seems like a OneToOne relation.
My Controller:
$sql = $entityManager->createQuery("
SELECT c
FROM AppBundle:EsoneroLingua c
WHERE c.id = '1546871' and c.tipo_corso = 'C' and c.comune = '7868'
");
$test = $sql->getResult();
In $test I was expect N record of EsoneroLingua with the same record StranieriCRS but I get only one EsoneroLingua with the correct StranieriCRS object. Seems work like OneToOne relation...why? Plus if I made dump($sql->getSql()); I obtain the raw sql...I try to use it directly in my db and he give me the right result. Is it a Doctrine bug?
To make a bidirectionnal One-To-Many, specify the JoinColumns only in the Many-To-One side.
So, in StranieriCRS, remove the following lines :
* #ORM\JoinColumns(
* #ORM\JoinColumn(name="id", referencedColumnName="id"),
* #ORM\JoinColumn(name="tipo_corso", referencedColumnName="tipo_corso"),
* #ORM\JoinColumn(name="comune", referencedColumnName="comune"),
* )
And just let Doctrine guess the columns with the inversedBy and mappedBy attributes.
For more information on the mappings, see this page.

Self referencing children not attached

I have a family tree like that:
class Family
{
/**
* #var integer
*
* #ORM\Column(type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var Family
*
* #ORM\ManyToOne(targetEntity="Family", inversedBy="children")
*/
private $parent;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
*/
private $name;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Family", mappedBy="parent")
*/
private $children;
// [...]
}
I'm trying to findAll() and get the parent and children attached
$familyRepo = $this->em->getRepository(Family::class);
$families = $familyRepo->findAll();
foreach ($families as $family) {
dump($family->getParent()->getName());
}
I can see the parents name dumped and only one query executed, so they are well attached.
However if I try to show the children:
dump($family->getChildren()->count());
I'm seeing as much queries as there are families.
How can I get the children attached as the parents are ? (without more queries)
What am I forgetting ?
On the one-to-many relation for $children you can specify to fetch objects eagerly as follows:
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Family", mappedBy="parent", fetch="EAGER")
*/
private $children;
See also the docs for other params.
Following #dlondero's suggestion, I forced the deep fetch into the repository.
Here is how I did:
public function getRootNodes($eagerLevels = 5)
{
$qb = $this->createQueryBuilder('entity0')
->select('partial entity0.{id, name, parent}')
->where('entity0.parent IS NULL')
;
for ($i = 0; $i < $eagerLevels; $i++) {
$qb
->leftJoin('entity'.$i.'.children', 'entity'.($i+1))
->addSelect('partial entity'.($i+1).'.{id, name, parent}')
;
}
return $qb->getQuery()->getResult();
}
This partially fetches just what I need so no lazy loading happens.
I also made the level of deepness configurable.

Error in doctrine query : Binding an entity with a composite primary key to a query

Hello i have an entity with two primary keys .
class UsefulnessEvaluation
{
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="App\EvaluationBundle\Entity\Evaluation", cascade={"persist","remove"})
*/
private $evaluation;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity="App\UserBundle\Entity\User", cascade={"persist","remove"})
*/
private $user;
/**
*
* #ORM\Column(type="string", nullable=false)
*/
private $type;
/**
* #ORM\Column(type="datetime", nullable=false)
*/
private $createdAt;
//etc
}
I want , in repository , count the number of an evaluation:
class UsefulnessEvaluationRepository extends EntityRepository
{
public function countEvaluationLikes($evaluation_id)
{
$query = $this->getEntityManager()->createQuery(
'SELECT count(p.evaluation) as nbre
FROM AppEvaluationBundle:UsefulnessEvaluation p
WHERE p.evaluation = :id
)->setParameter('id', $evaluation_id);
return $query->getSingleScalarResult();
}
}
this is the error :
Binding an entity with a composite primary key to a query is not
supported. You should split the parameter into the explicit fields and
bind them separately.
I think the issue is that you're selecting count(p.evaluation) but since you're already specifying the id of p.evaluation it seems unnecessary because you're guaranteed to get non-null values for p.evaluation.
Try this
$query = $this->getEntityManager()->createQuery(
'SELECT count(p) as nbre
FROM AppEvaluationBundle:UsefulnessEvaluation p
WHERE IDENTITY(p.evaluation) = :id'
)->setParameter('id', $evaluation_id);

Doctrine - ManyToMany Self Referencing Association + nested toArray() on child elements

I'm trying to perform a ManyToMany self referencing association in my Symfony 2.1 project by following the Doctrine docs: http://docs.doctrine-project.org/en/latest/reference/association-mapping.html#many-to-many-self-referencing
My use-case is that I'm working on a CMS and I'm adding the ability to have related items of content. For example: I could have a sidebar on a website which would say that this piece of content X is related to Y and Z. Similarly on pages where content Y appears it says that it is related to content item X.
In my tests using this to add a new relation between content items fails because it reaches PHP's maximum nesting level of 100 because it is running toArray() on the current content item and then again on the related content item and so on and so on.
I've seen many similar questions on SO about Many-to-Many Self referential Doctrine associations but none with enough complete code to be able to see how others have managed this. Can anybody help?
My Content entity:
/**
* #ORM\MappedSuperclass
* #ORM\Table(name="content")
* #ORM\Entity(repositoryClass="CMS\Bundle\Common\ContentBundle\Entity\ContentRepository")
* #ORM\InheritanceType("JOINED")
*/
abstract class content implements ContentInterface
{
/**
* #var int $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string $title
*
* #ORM\Column(name="title", type="string", length=255)
* #Assert\NotBlank()
*/
private $title;
// Other class properties
/**
* #var array
*
* #ORM\ManyToMany(targetEntity="Content", cascade={"persist"})
* #ORM\JoinTable(name="content_relation",
* joinColumns={#ORM\JoinColumn(name="relation_id", referencedColumnName="id")},
* inverseJoinColumns={
* #ORM\JoinColumn(name="related_content_id", referencedColumnName="id")
* })
**/
private $related;
public function __construct()
{
$this->related = new ArrayCollection();
}
// Other getters & setters for class properties
/**
* #return array
*/
public function getRelated()
{
return $this->related;
}
/**
* #param Content $relation
*/
public function addRelation(Content $relation)
{
$this->related->add($relation);
$this->related->add($this);
}
/**
* #return array
*/
public function toArray()
{
$related = array();
foreach($this->getRelated() as $relatedItem) {
$related[] = $relatedItem->toArray();
}
return array(
'type' => static::getType(),
'id' => $this->id,
'title' => $this->title,
....
'related' => $related
);
}
In my RelationsController for managing the related content data I use it like this:
/**
* Creates a new relation to a content item
*
* #Route("{_locale}/content/{id}/related", name="relation_add")
* #Method("POST")
*/
public function addAction(Request $request, $id)
{
// Validation and error checking
// $entity is loaded by the repository manager doing a find on the passed $id
$entity->addRelation($relation);
$em = $this->getEntityManager();
$em->persist($entity);
$em->persist($relation);
$em->flush();
$response = $relation->toArray();
return new JsonResponse($response, 201);
}
The fix for this was to use the JMSSerializerBundle to encode the entity to JSON instead of using a toArray method and change the addRelation function to:
/**
* #param Content $relation
*/
public function addRelation(Content $relation)
{
$this->related[] = $relation;
if (! $relation->getRelated()->contains($this)) {
$relation->addRelation($this);
}
}

Resources