Sort a doctrine's #OneToMany ArrayCollection by field - symfony

Close question was enter link description here but I need to more deep sorting:
/**
* #var ArrayCollection[SubjectTag]
*
* #ORM\OneToMany(targetEntity="SubjectTag", mappedBy="subject")
* #ORM\OrderBy({"position" = "ASC"})
* #Assert\Valid()
*/
protected $subjectTags;
In subjectTag I have:
/**
* #var ArrayCollection[tag]
*
* #ORM\OneToMany(targetEntity="Tag", mappedBy="subject")
* #ORM\OrderBy({"name" = "ASC"})
* #Assert\Valid()
*/
protected $tags;
Now I want to sort by SubjectTag.tags. How can I do that?
EDIT:
Entity1.php:
/**
* #ORM\ManyToOne(targetEntity="Entity2", referencedColumnName="id", nullable=false)
* #Assert\Valid()
*/
protected $entity2;
Entity2.php:
/**
* #ORM\ManyToOne(targetEntity="Entity3", referencedColumnName="id", nullable=false)
* #Assert\Valid()
*/
protected $entity3;
Entity3.php:
/**
* #ORM\Column(type="integer", nullable=true)
*/
protected $position;
And now.. I want have in Entity1 Entity2 sorted by position. How can I do that by default?

As explained in my previous comment, you should do a custom query in your repository class corresponding to your base Entity (You didn't give the name of it).
So in your App\Repository\"YourBaseENtityName"Repository class, you do something like this.
public function findOrderByTags()
{
return $this
->createQueryBuilder('baseEntityAlias')
->addSelect('st')
->addSelect('t')
->leftJoin('baseEntityAlias.subjectTags', 'st')
->leftJoin('st.tags', 't')
->orderBy('st.position', 'ASC')
->addOrderBy('t.name', 'ASC')
->getQuery()
->getResult();
}
Moreover, I'm not sure about what kind of order you want to perform based on your question. Here the baseEntity->subjectTags will be ordered by their positions and then the baseEntity->subjectTags->tags will be ordered by name.
Now you can call this method from your base entity repository class
Hope it will be helpful for you.
EDIT:
Here is a way to introduce a default behavior for your queryBuilder and reuse it.
/**
* In your EntityRepository add a method to init your query builder
*/
public function createDefaultQueryBuilder(string $alias = 'a')
{
return $this
->createQueryBuilder($alias)
->addSelect('st')
->addSelect('t')
->leftJoin('baseEntityAlias.subjectTags', 'st')
->leftJoin('st.tags', 't')
->orderBy('st.position', 'ASC')
->addOrderBy('t.name', 'ASC');
}
/**
* In this example, I override the default find method. I don't recommend it thought
*/
public function find($id, $lockMode = null, $lockVersion = null)
{
return $this
->createDefaultQueryBuilder()
->where('a.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
As you can see, I reuse the createDefaultQueryBuilder method in order to get a default behavior with subjectTags and tags init in the relation and ordered in the right way.

Related

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

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

Symfony Doctrine SortBy ToMany relation objects

I have entity post and points, the are connected by oneToMany relation. I want make method that will return objects with most count of related comments.
Is it possible?
Please help, i don't have any idea.
https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/tutorials/ordered-associations.html - should i use this?
entities:
post:
/**
* #ORM\Entity(repositoryClass="App\Repository\PostRepository")
*/
class Post
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var Points
* #ORM\OneToMany(targetEntity="Points", mappedBy="post", fetch="EAGER")
*/
private $points;
/**
* #return Collection|Points[]
*/
public function getPoints(): Collection {
return $this->points;
}
...
points
/**
* #ORM\Entity(repositoryClass="App\Repository\PointsRepository")
*/
class Points
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
/**
* #var Post
* #ORM\ManyToOne(targetEntity="Post", inversedBy="points", fetch="EAGER")
*/
private $post;
public function getPost(): Post {
return $this->post;
}
public function setPost(Post $post ){
$this->post = $post;
}
...
On the assumption that you are already able to return a post with its points you might try something like this:
in App\Repository\PostRepository:
public function postsByPoints() {
return $this->getEntityManager()->createQueryBuilder()
->select('p.post, count(pt.points) N)
->from('App:Points', 'pt')
->join('pt.post', 'p')
->where('some where clause') <- delete this if you're not selecting a subset
->groupBy('p.post')
->orderBy('N')
->getQuery()->getResult();
}
In some controller:
$em = $this->getDoctrine()->getManager();
$postsByPoints = $em->getRepository('App:Post')->postsByPoints();
NB: not tested
This is an working (for me) code
return $this->createQueryBuilder('p')
->innerJoin('p.user', 'c')
->innerJoin('p.points', 'pp')
->andWhere("p.date > '".$now->format("Y-m-d H:i:s")."'")
->setMaxResults($max)
->groupBy('pp.post')
->orderBy('pp.post','DESC')
->getQuery()
->getResult();

symfony2 - add value to protected object

How can I set the protected object user? After filling the form i have to add user object with current user data (for example like saving comments). I tried something like that:
if ($form->isValid()) {
$comment = $form->getData();
$comment->user = $this->contextSecurity->getToken()->getUser();
$this->model->save($comment);
}
And i've got this error
FatalErrorException: Error: Cannot access protected property AppBundle\Entity\Comment::$user in /home/AppBundle/Controller/CommentsController.php line 184
Here is my Comment entity:
class Comment
{
/**
* Id.
*
* #ORM\Id
* #ORM\Column(
* type="integer",
* nullable=false,
* options={
* "unsigned" = true
* }
* )
* #ORM\GeneratedValue(strategy="IDENTITY")
*
* #var integer $id
*/
private $id;
/**
* Content.
*
* #ORM\Column(
* name="content",
* type="string",
* length=250,
* nullable=false
* )
* #Assert\NotBlank(groups={"c-default"})
* #Assert\Length(min=3, max=250, groups={"c-default"})
*
* #var string $content
*/
private $content;
/**
* #ORM\ManyToOne(targetEntity="User", inversedBy="comments")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id", nullable=false)
*/
protected $user;
I'm using Symfony2.3. Any help will be appreciated.
You can't modify protected properties from outside of the object. You need a public property or a setter for that.
class Comment
{
// ...
public function setUser(User $user)
{
$this->user = $user;
}
}
And in a controller you can write:
$comment->setUser($this->getUser());
This question is not related to Symfony2, at first you should read about php types, especially about objects. read here and then here
You should understand how Visibility works. After that you will understand that access to protected/private properties of the object is only available from the object itself, so you need to create public method
setUser($user) {
$this->user = $user;
}
I always use protected, If i want edit variable or take the value, I use the getter and setter:
public function setUser($user) {
$this->user = $user;
}
public function getUser(){
return $this->user;
}

Symfony2 CRUD Many-to-Many - Sort Listbox with Multiple items [duplicate]

I have a Product class that has many fields on it for ManyToMany, such as ingredients, sizes, species, etc.. A total of about 14 different fields
Not all of the fields are are relevant to each product.
I have mapping set up like this
Class product {
/**
* #var Species[]
* #ORM\ManyToMany(targetEntity="Species")
* #ORM\JoinTable(name="product_species",
* joinColumns={#ORM\JoinColumn(name="productId", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="speciesId", referencedColumnName="id")}
* )
* #ORM\OrderBy({"name" = "asc"})
*/
private $species;
This works great for a manytomany/manyto one.
The problem is in my product_ingredients table I needed to add an additional field, meaning need to switch from ManyToMany to a OneToMany/ManyToOne
So like this
/**
* #var ProductIngredient[]
*
* #ORM\OneToMany(targetEntity="ProductIngredient", mappedBy="product")
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
*/
private $ingredients;
Now my ProductIngredient Entity Looks like this
/**
* #var IngredientType
* #ORM\ManyToOne(targetEntity="IngredientType", fetch="EAGER")
* #ORM\JoinColumn(name="ingredientTypeId", referencedColumnName="id")
*/
private $ingredientType;
/**
* #var Ingredient
*
* #ORM\ManyToOne(targetEntity="Ingredient", inversedBy="products", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="ingredientId", referencedColumnName="id")
* })
*/
private $ingredient;
/**
* #var Product
*
* #ORM\ManyToOne(targetEntity="Product", inversedBy="ingredients")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
* })
*/
private $product;
So in my product class for species I use the #ORM\OrderBy so that species is already ordered.. Is there a way I can somehow also do this for my ingredients field?
Or am I doing my logic wrong and these shouldn't even be fields on the product class and should just be looking up by the repository instead?
I was wanting it to be easy so I could loop through my objects like $product->getIngredients()
instead of doing
$ingredients = $this->getDoctrine()->getRepository('ProductIngredient')->findByProduct($product->getId());
in the Product entity just also aadd the orderBy to the ingredients relation
/**
* ...
* #ORM\OrderBy({"some_attribute" = "ASC", "another_attribute" = "DESC"})
*/
private $ingredients;
Well I came up with a hackish way.. Since I really only care about the sort on output, I have made a basic twig extension
use Doctrine\Common\Collections\Collection;
public function sort(Collection $objects, $name, $property = null)
{
$values = $objects->getValues();
usort($values, function ($a, $b) use ($name, $property) {
$name = 'get' . $name;
if ($property) {
$property = 'get' . $property;
return strcasecmp($a->$name()->$property(), $b->$name()->$property());
} else {
return strcasecmp($a->$name(), $b->$name());
}
});
return $values;
}
I would like to avoid this hack though and still would like to know a real solution
You should use 'query_builder' option in your Form: http://symfony.com/doc/master/reference/forms/types/entity.html#query-builder
The value of the option can be something like this:
function(EntityRepository $er) {
return $er->createQueryBuilder('i')->orderBy('i.name');
}
Don't forget to add the "use" statement for EntityRepository
If you use xml mapping, you could use
<order-by>
<order-by-field name="some_field" direction="ASC" />
</order-by>
inside your <one-to-many> tag.

How to OrderBy on OneToMany/ManyToOne

I have a Product class that has many fields on it for ManyToMany, such as ingredients, sizes, species, etc.. A total of about 14 different fields
Not all of the fields are are relevant to each product.
I have mapping set up like this
Class product {
/**
* #var Species[]
* #ORM\ManyToMany(targetEntity="Species")
* #ORM\JoinTable(name="product_species",
* joinColumns={#ORM\JoinColumn(name="productId", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="speciesId", referencedColumnName="id")}
* )
* #ORM\OrderBy({"name" = "asc"})
*/
private $species;
This works great for a manytomany/manyto one.
The problem is in my product_ingredients table I needed to add an additional field, meaning need to switch from ManyToMany to a OneToMany/ManyToOne
So like this
/**
* #var ProductIngredient[]
*
* #ORM\OneToMany(targetEntity="ProductIngredient", mappedBy="product")
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
*/
private $ingredients;
Now my ProductIngredient Entity Looks like this
/**
* #var IngredientType
* #ORM\ManyToOne(targetEntity="IngredientType", fetch="EAGER")
* #ORM\JoinColumn(name="ingredientTypeId", referencedColumnName="id")
*/
private $ingredientType;
/**
* #var Ingredient
*
* #ORM\ManyToOne(targetEntity="Ingredient", inversedBy="products", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="ingredientId", referencedColumnName="id")
* })
*/
private $ingredient;
/**
* #var Product
*
* #ORM\ManyToOne(targetEntity="Product", inversedBy="ingredients")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
* })
*/
private $product;
So in my product class for species I use the #ORM\OrderBy so that species is already ordered.. Is there a way I can somehow also do this for my ingredients field?
Or am I doing my logic wrong and these shouldn't even be fields on the product class and should just be looking up by the repository instead?
I was wanting it to be easy so I could loop through my objects like $product->getIngredients()
instead of doing
$ingredients = $this->getDoctrine()->getRepository('ProductIngredient')->findByProduct($product->getId());
in the Product entity just also aadd the orderBy to the ingredients relation
/**
* ...
* #ORM\OrderBy({"some_attribute" = "ASC", "another_attribute" = "DESC"})
*/
private $ingredients;
Well I came up with a hackish way.. Since I really only care about the sort on output, I have made a basic twig extension
use Doctrine\Common\Collections\Collection;
public function sort(Collection $objects, $name, $property = null)
{
$values = $objects->getValues();
usort($values, function ($a, $b) use ($name, $property) {
$name = 'get' . $name;
if ($property) {
$property = 'get' . $property;
return strcasecmp($a->$name()->$property(), $b->$name()->$property());
} else {
return strcasecmp($a->$name(), $b->$name());
}
});
return $values;
}
I would like to avoid this hack though and still would like to know a real solution
You should use 'query_builder' option in your Form: http://symfony.com/doc/master/reference/forms/types/entity.html#query-builder
The value of the option can be something like this:
function(EntityRepository $er) {
return $er->createQueryBuilder('i')->orderBy('i.name');
}
Don't forget to add the "use" statement for EntityRepository
If you use xml mapping, you could use
<order-by>
<order-by-field name="some_field" direction="ASC" />
</order-by>
inside your <one-to-many> tag.

Resources