Proper way to check if many-to-many relation exists - Symfony2/Doctrine - symfony

Let's suppose I have two entities User and Product related by a Many-to-Many relationship with Doctrine.
I would like to know the best way to handle a $user->hasProduct($product) method for my User entity that returns true is relation exists or false if not.
I'm currently doing this :
public function hasProduct($id)
{
foreach($this->getProducts() as $product) {
if($product->getId() == $id) {
return true;
}
}
return false;
}
But i'm not sure it's the best way, especially if there is many relations in the loop.
If someone has something better, let me know :)

Your function getProducts gives you an ArrayCollection.
Just do
if($user->getProducts()->contains($product)) //the real product object not the id
//your stuff
Edit :
For twig template :
{% if product in user.products %}
//your stuff
{% endif %}

I was struggling with the same problem, and came across this blog entry which solved this problem by making use of Doctrine Filters.
If I understand your problem correctly and you have three tables (user, user_product and product) you should be able to rewrite your hasProduct($id) function like this:
use Doctrine\Common\Collections\Criteria;
public function hasProduct(int $productId): bool {
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('id', $productId));
if(count($this->products->matching($criteria)) > 0) {
return true;
}
return false;
}
When running this code, Doctrine does not load all the Products linked to the User. It in fact only queries the cross reference table (user_product).

Related

Bug in AssociationField with autocomplete() for Many to Many field in EasyAdmin

I have an entity called Question which has a Many to Many relationship with another entity User. So I have following tables:
question(id, field1, users)
user(id, name)
question_user(question_id, user_id)
I use EasyAdmin for creating QuestionCrud form where I use AssociationField for assigning multiple Users to a Question. If I use AssociationField without autocomplete() method it works like a charm and stores the data. But I have really big amount of data in User table and that's why need to use autocomplete in order to load only small amount of data.
Using autocomplete() with AssociationField giving following error and giving validation error on a form submit:
The choices 123, 2323 do not exist in the choice list.
123, 2323 are IDs of the selected users.
Has anybody faced such a problem?
A month ago another developer faced the same problem - https://github.com/EasyCorp/EasyAdminBundle/issues/5467. He proposed to edit vendor file src/Form/EventListener/CrudAutocompleteSubscriber.php and thanks to his investigation on what was wrong with autocomplete() method on many-to-many relationship we came out with another solution.
Instead of editing a vendor file we decided to override findBy method of the Entity repository.
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
{
if (isset($criteria['id']) && is_array($criteria['id'])) {
$ids = [];
foreach ($criteria['id'] as $id) {
if (Uuid::isValid($id)) {
$ids[] = Uuid::fromString($id)->toBinary();
} else {
$ids[] = $id;
}
}
$criteria['id'] = $ids;
}
return parent::findBy($criteria, $orderBy, $limit, $offset);
}
This solution is good as far as you have only one Entity which is used as many-to-many relationship, but if there are several entities, then the findBy method should be overridden in all of them.
Hope that supporters of EasyAdmin will fix this bug soon and we will not need to use such 'ugly' solutions.

add a variable session in entity?

i create an entity "Products".
in this entity, i get the price like
public function getPrice()
{
return $this->price;
}
Indeed, i would like to add in this method a session variable for convert currency like this :
public function getPrix()
{
$devise = $this->session->get('CurencyToConvert');
$json = json_decode(file_get_contents('http://api.fixer.io/latest?symbols='.$devise));
$rates = $json->rates->CHF;
return $this->prix * $rates;
}
but i think this is the wrong solution.
i don't know how to do to get an automatic conversion when i get the price!!
do I create a service for my checkout and a twig filter for my views?
thank you
The Products class is a POPO (Playing Old PHP Object) it's really required to keep it simple which means this class must have only attributes like price and getters and setters for those attributes,
You can create a service to handle Currency conversion, and you can also inject it into a twig filter so you gonna have one piece of code implementing this functionality

How to retrieve translation of a "sub entity" using Symfony a2lix knp doctrine behaviors translatable

I'm new in symfo but I need to translate content of my site.
I'm using a2lix (last version) and KNP doctrine behaviors (Translatable).
Let's say that I have 2 entities (e.g. Articles and Categories).
As in the doc (https://github.com/KnpLabs/DoctrineBehaviors) for translations, I'm using 2 classes for Categories (Category and CategoryTranslation).
To retrieve the translations, of my category, I'm using a query with the locale. I get the locale with Request $request ($locale = $request->getLocale();). Here is an example of my controller and the query in my repository.
Controller
public function indexAction(Request $request)
{
$em = $this->getDoctrine()->getManager();
$locale = $request->getLocale();
$entities = $em->getRepository('AcmeBundle:Category')->findAllByLocale($locale);
return $this->render('CTCArtworkBundle:Backend/Artwork:index.html.twig', array(
'entities' => $entities,
));
}
Repository
I'm trying to retrieve informations for the locale.
public function findAllByLocale($locale){
return $this->createQueryBuilder('a')
->join('a.translations', 'aTrans')
->where('aTrans.locale = :locale')
->setParameter("locale", $locale)
->addSelect('aTrans')
->getQuery()
->getResult()
;
}
I don't know if it's a good practice but it works for me. I retrieve fr/en categories in my Twig template like so when I change the url :
<tr>
<th>Category</th>
<td>{{ category.translations|First.name }}</td>
</tr>
My problem
For the translation of my article, I do the same. I have 3 properties
- title
- description
- category (I'm using a2lix_translatedEntity (http://a2lix.fr/bundles/translation-form/#bundle-additional))
When I try to render a record of Article, I never retrieve the translation for my category Name but well for title and description.
I also read that (https://github.com/KnpLabs/DoctrineBehaviors#guess-the-current-locale) but I don't really understand. Is that a way to always pass locale ?
What am I doing wrong ?
I'm blocked and don't find any documentation to resolve my problem. Sorry for my english ;-)
Any help would be very appreciate. Many Thanks
KNP has its own way to guess the current locale, simply by accessing current request scope. The whole "passing locale" thing is useful if you want to pull records for specific locale.
Now, for your category translation. Since you did not include your entities, I will try to show you some examples to access your translations.
In your Category entity, lets say you have a property name that would return your category name. Then you can define a simple helper method that would return that name, by current locale:
public function getName() {
if( $name == $this->translate()->getName() ) {
return $name;
}
return '';
}
So, what have we done here?
$this->translate()->getName() - this line looks for your translation entity (in this case that would be CategoryTranslation) and invokes method getName() . Then, we either return translated category name, or an empty string if no translation has been added.
And lastly, this is how you can access your category name in your twig template:
Since we defined our helper method, there is no longer any need to access .translations in your template. You can simply call:
{{ category.name }}
Hope you got the idea.
And you can also use this
{{ category.translate.name }}
With DoctrineBehaviors v2, you can add this to your Category class:
public function __call($name, $arguments)
{
return $this->proxyCurrentLocaleTranslation($name, $arguments);
}
Here's what it does. So, in your Category entity, lets say you have a property description that would hold your category description. The code above will generate a corresponding property getter: getDescription(). Which ultimately will allow you to use this property in your Twig template:
{{ category.description }}

Querying a collection within an entity

I currently have a simple one to many relationship between products and multiple deals (a table of 1 million deals in total) associated with the products.
What I'm trying to do is loop through the top 10 products and select the top deals relating to the product.
What would be the best way to achieve this in Doctrine 2? I was contemplating adding a method such as getTopDeals within the product entity, and then calling it within twig as I looped through each product like so:
{% for product in popular_products %}
{% set deal = product.getTopDeal() %}
{{ product.title }} - {{ deal.title }}, {{deal.price }}
{% endfor %}
However, I've read that generally it is frowned upon adding methods such as this into models, so I'm at an end as to what the best way to do this is.
Make a method in your Deals repository to accept a parameter and return the topdeal. In your controller, array_map() your products to produce an array of deals keyed by product. Then pass the deals array along with your products array to your template.
edit: sample requested:
Repository:
public function getTopDealProduct($productid)
{
$em=$this->getEntityManager();
$qb = $em->getRepository('Bundle:Deal')->createQueryBuilder('d');
$qb->join('d.product', 'p');
$qb->setMaxResults(1);
$qb->addOrderBy('d.price');
$query = $qb->getQuery();
$results = $query->getResult();
return $results;
}
Controller:
public function s2JUsorAction(Request $request, $id)
{
$dealrep = $this->em->getRepository('Bundle:Deal');
$prodrep = $this->em->getRepository('Bundle:Product');
$products= $prodrep->getProducts(); // Not shown here, write this
$deals= array_map(function($element) use ($dealrep){
return $dealrep->getTopDealProduct($element->getId());
}
,$products);
return $this->render('Bundle:Product:Deal.html.twig', array(
'products' => $products
,'deals' => $deals
));
}
The best practice is, "fat models, thin controllers". The logic for selecting the top deals for a product definitely has a place on the model, if the model itself is capable of doing this filtering, eg. it only needs the deal objects, which it has a relation with. For this, you could use the Criteria API, something like:
use Doctrine\Common\Collections\Criteria;
class Product {
private $deals; // many-to-many to Products
public function getTopDeals() {
$criteria = Criteria::create()->orderBy(array('price', 'DESC'))->setMaxResults(10);
return $this->deals->matching($criteria);
}
}
If the selection logic is more complicated, and needs to reach into the entity manager, then it is better suited for placing on an EntityRepository.

Doctrine2 subquery in template - equivalent of symfony1 lazy query getter

I have two tables:
comment - id, application_id, comment, user_id, created_at, deleted_at
comment_likes - comment_id, user_id
I can retrieve the comments for an application using the standard DQLSELECT u FROM Comment WHERE :application = application
When lopping through the comments, I want to see if the logged in user has already liked a comment.
In symfony1, I would have used a simple lazy query $comment->hasUserLiked()
At the moment, in symfony2, I have to do a query of all the user likes for an application comments and a query of all the application comments.
When looping through application comments I do a sub-loop in each comment to check whether that a user likes record exists in the user likes comments collection. This is not clean.
Hope this makes sense.
Is there a better way?
EDIT: I could use a sub-controller to render whether a user likes the comment or not....but that seems rather over the top just for a couple of lines of html. Although, cleaner than the current implementation.
You need to set up a bidirectional one-to-many relationship between the Comment and Comment\Like entities. This way, a Comment entity would know about all the likes it has. Then you could implement a method in it like $comment->hasBeenLikedBy($user) which would loop through all the likes it has and see if any of them was done by the user you passed.
The Comment entity:
<?php
namespace Model;
class Comment
{
/**
* #OneToMany(targetEntity="Model\Comment\Like", mappedBy="comment")
*/
private $likes;
public function hasBeenLikedBy(User $user)
{
foreach ($this->likes as $like) {
if ($like->getUser() == $user) {
return true;
}
return false;
}
}
}
The Comment\Like entity:
<?php
namespace Model\Comment;
class Like
{
/**
* #ManyToOne(targetEntity="Model\Comment")
*/
private $comment;
/**
* #ManyToOne(targetEntity="Model\User")
*/
private $user
public function getUser()
{
return $this->user;
}
}
This code is not complete and may contain mistakes, but I hope it's enough to show you the overall approach.
You can write your own hasUserLiked() function to query the database when called
You could join the comments and likes table and get the likes in the same call.
You can use the following query if your doctrine schema is setup correctly:
SELECT c FROM Comment c
LEFT JOIN c.CommentLikes cl
WHERE c.application = :application

Resources