How to OrderBy on OneToMany/ManyToOne - symfony

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.

Related

Sort a doctrine's #OneToMany ArrayCollection by field

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.

Symfony Relations

In my app I have 3 entities; User, Booking and Room.
Booking entity:
namespace App\Entity;
/**
* #ORM\Table(name="booking")
* #ORM\Entity(repositoryClass="App\Repository\BookingRepository")
*/
class Booking
{
/**
* #ORM\Column(type="boolean")
* #Assert\NotBlank()
*/
private $isActive;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Room", inversedBy="bookings")
*/
private $room;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\User", inversedBy="bookings")
*/
private $user;
Room entity:
/**
* #ORM\Table(name="room")
* #ORM\Entity(repositoryClass="App\Repository\RoomRepository")
*/
class Room
{
/**
* #ORM\OneToMany(targetEntity="App\Entity\Booking", mappedBy="room")
* #Expose
*/
private $bookings;
User entity:
/**
* #ORM\Table(name="app_user")
* #ORM\Entity(repositoryClass="App\Repository\UserRepository")
* #UniqueEntity(fields="email", message="This email address is already in use")
*/
class User implements AdvancedUserInterface
{
/**
* #ORM\Column(type="string", length=255, unique=true)
* #Assert\NotBlank()
* #Assert\Email()
*/
private $email;
/**
* #ORM\OneToMany(targetEntity="App\Entity\Booking", mappedBy="user")
* #Expose
*/
private $bookings;
Given a user's email I can get a room id like this:
$user = $this->getEntityManager()
->getRepository(User::class)
->findOneBy([
'email' => 'johndoe#domain.com'
]);
$booking = $this->getEntityManager()
->getRepository(Booking::class)
->findOneBy([
'user' => $user,
'isActive' => true,
]);
$roomId = $booking->getRoom()->getId();
However this seems like a long way to do it. Is it possible to optimise this and query for a room without having to make 2 databases calls?
Yes you can get it directly from the $user variable. I don't see any getters or setters listed here but I'm assuming you have created them. If so then you can do the following:
$bookings = $user->getBookings();
Bookings is an array so you will need to select which booking you want to get the room for. Let's just select the first:
$roomId = $bookings->first()->getRoom()->getId()
To make it cleaner you can add a getCurrentBooking method or something similar to your User class that will return the exact booking you want.
Then you would end up with something like this:
$roomId = $user->getCurrentBooking->getRoom()->getId()
You could use a single join query to get the room for a spcific user
$this->getEntityManager()
->createQueryBuilder()
->select('r')
->from(Room::class, 'r')
->join('r.bookings', 'b')
->join('b.user', 'u')
->where('u.email = :email')
->andWhere('b.isActive = :isActive')
->setParameter('isActive', true)
->setParameter('email', 'johndoe#domain.com')
->getQuery()
->getResult();

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.

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.

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