How to retrieve entities by conditioning all their ManyToMany relations? - php

The context
I'm working on an online shop. Every Product on the website can have several ProductVariations (e. g. size, color, ...) which values are a list of ProductVariationValue (e. g. XL, L, Blue, Red, ...).
Each ProductVariationValue is bound to a ProductVariation (e. g. you can't choose Red for Size).
I'm trying to create a stock management system which tracks the amount of in-stock (Product, ProductVariations, ProductVariationValues) combination.
Product <--> ProductVariation <--> ProductVariationValue
I've managed to do it by creating a Stock entity holding a Product and a collection of ProductVariationValues:
/** #ORM\Entity */
public class Stock
{
// ...
/** #ORM\ManyToOne(targetEntity="AcmeDemoBundle:Product") */
protected $product;
/** #ORM\ManyToMany(targetEntity="AcmeDemoBundle:ProductVariationValues") */
protected $productVariationValues;
/** #ORM\Column(type="integer") */
protected $number = 0;
// ...
}
When the user adds a Product to his cart, I create an instance of OrderItem which holds the ProductVariationValues they selected:
/** #ORM\Entity */
public class OrderItem
{
// ...
/** #ORM\ManyToOne(targetEntity="AcmeDemoBundle:Product") */
protected $product;
/** #ORM\ManyToMany(targetEntity="AcmeDemoBundle:ProductVariationValues") */
protected $productVariationValues;
// ...
}
The problem
I understand the Stock entity is similar to the OrderItem entity. However, when the user adds a Product to his cart, and I'm building the OrderItem entity, I cannot find how to retrieve the Stock instance to check whether the Product with its ProductVariationValues are in stock, because I can't find which relation to add in the Stock entity.
Indeed, the Stock entity cannot be identified by the $product or $productVariationValues relations by themselves: they need to be put together.
What I've tried to do
In the controller, given the $product and an array of $productVariationValues, the user selected, I've tried to set up a query to retrieve the Stock instance.
class StockRepository
{
public function retrieveStock($product, $productVariationValues)
{
$this->getQueryBuilder('s')
->leftJoin('s.product', 'p')
->leftJoin('s.productVariationValues', 'pvv')
->where('s.product = ?')
->andWhere('s.productVariationValues = ?')
->setParameter(1, $product)
->setParameter(2, $productVariationValues);
// ...
}
}
This code does not work as the setParameter() method can not accept array or ArrayCollection parameters.
Either way, this solution would not be optimal as this code must be run from the controller and I wouldn't be able to run it from the OrderItem entity.
Is my entity model wrong? Is there a simple way to add a Stock relation in my OrderItem class? Or at least to retrieve a Stock item, given its Product and ProductVariationValues?

You need to use an IN clause, and iterate over all the values; something like:
class StockRepository
{
public function retrieveStock($product, $productVariationValues)
{
$qb=$this->getQueryBuilder('s');
$qb->leftJoin('s.product', 'p')
->leftJoin('s.productVariationValues', 'pvv')
->where('s.product = ?')
->setParameter(1, $product)
;
$stockVariationValues=$qb->getQuery()-
>getSingleResult()->getProductVariationValues();
$qb=$this->getQueryBuilder('s');
$qb->leftJoin('s.product', 'p')
->leftJoin('s.productVariationValues', 'pvv')
->where('s.product = ?')
->setParameter(1, $product)
;
$stockVariationValues->map(function ($element) use ($qb){
$qb->andWhere($qb->expr()
->in('s.productVariationValues'
, "'".implode(","
,$productVariationValues->toArray())
."'")
);
});
// ...
}
}

Related

Allow ManyToMany Duplications on Symfony 5 (Doctrine)

I have a problem, it's for a school project and I need to allow duplications of the same relation between two entities on my app using Symfony 5 & Doctrine & postgresql .
I have a basicly a ManyToMany relation between Order and Products, I don't want to add fields for quantity, so I'm looking to count the number of occurences of the a same relation id_order & id_product on my order_product table, but I can't persist more than one same relation between order & product.
I searched and mainly saw people tryng to avoid duplications of the same relation, i'm looking for the exact contrary.
Thx
When using relation with Many on at least one side of the relation, you get Collection on the opposite side. On the collection you can call count() method.
So if you need to calculate quantity of Products in your Order, your Order entity can look like this:
/** #Entity */
class Order
{
...
/**
* #ManyToMany(targetEntity="Product", inversedBy="orders")
* #JoinTable(name="order_product")
*/
private $products;
public function __construct()
{
$this->products = new ArrayCollection();
}
public function countProducts(): int
{
return $this->products->count();
}
public function countProductsById(int $productId): int
{
return $this->products->filter(static function(Product $product) use ($productId) {
return $product->getId() === $productId;
})->count();
}
...
}
PS: Also be aware that word Order is a reserved word in PostgreSQL. You need to either name your Order entity differently or escape the naming correctly.

Doctrine ManyToOne relationship - auto-remove on "set"

I'm working with relationships in Doctrine (using Symfony 5).
What I have is these 2 relations:
User
Availability
User has an ID and has Many Availabilities.
So Entity User has
/**
* #ORM\OneToMany(targetEntity="UserAvailability", mappedBy="user")
*/
private $availability;
and the reverse on Entity Availability.
Availability is a relation with:
id, user_id, day_name, start_time and end_time, that simple.
What I already achieved with ManyToMany and I want to achieve in this case too is:
I need to receive the entire set of availabilities for a User from the client and use it to update the availabilities of my User, so I defined a setAvailability method which receives a Collection of Availability entities and simply does
$this->availabilities = $availabilities.
This works when I add new availabilities but the ones that are on the DB and not in the collection are not dropped when I persist the entity.
The same method works flawlessly with ManyToMany Relationship.
What am I missing?
*** UPDATE **
public function setAvailability($availability): self
{
$this->availability = $availability;
return $this;
}
this same code works when removing relations in ManyToMany relationship but not in ManyToOne, the attribute "availability" is correctly set, but when using persist/flush the availability which was removed is not removed on the DB.
Thanks
Try to set the attributes as in the example from the doctrine documentation below :
<?php
use Doctrine\Common\Collections\ArrayCollection;
/** #Entity */
class User
{
// ...
/**
* One user has many availabilities. This is the inverse side.
* #OneToMany(targetEntity="Availability", mappedBy="user")
*/
private $availabilities;
// ...
public function __construct() {
$this->availabilities = new ArrayCollection();
}
}
/** #Entity */
class Availability
{
// ...
/**
* Many availabilities have one user. This is the owning side.
* #ManyToOne(targetEntity="User", inversedBy="availabilities")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
private $user;
// ...
}
the attribute mappedBy and inversedBy are necessary for relations

Symfony 2 OneToMany performance optimisation

I found a performance problem on my website.
I have an entity "Cart" with a oneToMany relation. When I call the getter method in a view that gives about 2000 queries. Then the performance of page decreases very strongly.
My entity Cart with OneTMany assoc :
class Cart {
/**
* #ORM\OneToMany(targetEntity="Comiti\UserBundle\Entity\Subscription", mappedBy="cart")
*/
protected $subscriptions;
}
My entity Subscription with ManyToOne assoc :
class Subscription {
/**
* #ORM\ManyToOne(targetEntity="Comiti\UserBundle\Entity\Cart",inversedBy="subscriptions")
* #ORM\JoinColumn(name="cart_id", referencedColumnName="id")
* #JMS\Exclude()
*/
protected $cart;
}
My twig view with call of getSubscriptions() producing a lot of database requests :
{% for subscription in cart.subscriptions %}
What can I do to get better performances on it?
The problem you're experiencing is called the N+1 problem. You're fetching an entity which has an association that you then traverse and query again. In your concrete example, this occurs in this loop, assuming that your subscriptions have a cost:
{% for subscription in cart.subscriptions %}
{{ subscription.cost }}
Given that you've queried for a cart, you haven't loaded all its subscriptions and its attributes, and that's happening at loop time. To get around this, you should do a fetch join on your cart with subscriptions:
// in CartRepository
public function findCartWithSubscriptions($cartId)
{
$qb = $this->createQueryBuilder('c');
$qb->leftJoin('c.subscriptions', 's')
->where("c = :cart")
->setParameter("cart", $cartId);
return $qb->getQuery()->getResult();
}
This will hydrate for you a Cart object with its subscriptions loaded in memory.

How to automatically remove nested objects in Symfony?

I'm a big begginer in Symfony, coming from Ruby on Rails world and trying to get same behaviours.
I have a simple application with 2 entities : Product and Category, with the relation Product belongs to Category and Category has many Products.
class Category {
[ ... ]
/**
* #ORM\OneToMany(targetEntity="Product", mappedBy="category", cascade={"all"})
*/
protected $products;
}
class Product {
[ ... ]
/**
* #ORM\ManyToOne(targetEntity="Category", inversedBy="products", cascade={"all"})
*/
protected $category;
}
What I'm trying to do is to delete every nested products when I'm deleting a Category.
My current action looks like
public function deleteAction($id, Request $request)
{
$repository = $this->getDoctrine()->getRepository('AppBundle:Category');
$category = $repository->find($id);
$em = $this->getDoctrine()->getManager();
$em->remove($category);
$em->flush();
return $this->redirect('/categories/');
}
A simple method could be remove all the products in the controller, but it's not very maintainable and not very object oriented. I'm looking about a practice to remove all the products of the deleted category directly in the model. A method, in RoR, is the callbacks (named after_destroy), automatically called when the object is destroyed. Is there any looking-like method in Symfony ?

Is it possible to pass an id instead of the entity when creating an association between two entities?

I have an Item entity and a Category entity. An Item has one Category. My mapping code looks like this:
// Item.php
/**
* #ORM\ManyToOne(targetEntity = "Category")
* #ORM\JoinColumn(name = "category_id", referencedColumnName = "id")
*/
protected $category;
To create the association, I use this method:
// Item.php
public function setCategory(Category $category) {
$this->category = $category;
}
This works fine as long as I first fetch the Category entity from the DB. But I'm wondering if it's possible to pass an id instead of the Category entity. I'd like to manually set the JoinColumn category_id with a scalar value. But sine category_id isn't an actual member of Item, I'm not sure how I can do this.
Use getReference:
$item->setCategory($em->getReference('Category', $id));

Categories