Symfony2 - Many to Many relationship and LIMIT - symfony

Im use many-to-many relationship;
User entity;
/**
* #ORM\ManyToMany(targetEntity="Conversation", inversedBy="users")
*/
protected $conversations;
Conversation entity;
/**
* #ORM\ManyToMany(targetEntity="User", mappedBy="conversations")
* #ORM\JoinTable(name="user_conversation")
*/
protected $users;
When, I work this function;
$user->getConversations();
Symfony work this sql code in background;
SELECT
t0.id AS id1,
t0.conversationid AS conversationid2
FROM
Conversation t0
INNER JOIN user_conversation ON t0.id = user_conversation.conversation_id
WHERE
user_conversation.user_id = ?
And select all conversation. This will be performance problem. So, I work with repository class. But, I can't work many-to-many and limit function with together. What should I do? What I write to repository class?

If you want to optimize access to large collections in doctrine just use Criteria (That only works on OneToMany associations.)
Example:
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Criteria;
/**
* #ORM\Entity
* #ORM\Table
*/
class User
{
....
public function getLatestConversation()
{
$criteria = Criteria::create()
->setMaxResults(10);
return $this->conversations->matching($criteria);
}
}
For ManyToMany I think you must create a custom query:
public function getLatestConversations($user)
{
$qb = $this->createQueryBuilder("c");
$qb
->leftjoin("c.users", "u")
->where("u = :user")
->setParameter("user", $user)
->setMaxResults(2);
return $qb->getQuery()->getResult();
}

You can convert it to Arraycollection Object:
$input = $repo->getConversations(); //manytomany relation
$arr = new ArrayCollection();
foreach ($input as $e){
$arr->add($e);
}
And then use your Criteria on $arr

Try
$repository = $em->getRepository('YourBundle:Conversations');
$query = $repository->createQueryBuilder('C')
->join('C.Users', 'U')
->where('U.id = :uid')
->setMaxResults(20) //set your amount for limit
->setParameter('uid', $user_id)
->getQuery();
$result = $query->getResults();

Related

Symfony Doctrine Hydrator - With custom hydrator I lose my ManyToOne relation

I would like to extends ObjectHydrator to benefit of the hydration of my ManyToOne relation and add extra field to the Entity.
Here is my hydrator: StatisticsDataHydrator.php
namespace AppBundle\Hydrator\ProjectAssignment;
use AppBundle\Entity\ProjectAssignment;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
class StatisticsDataHydrator extends ObjectHydrator
{
/**
* {#inheritdoc}
*/
protected function hydrateRowData(array $data, array &$result)
{
$hydrated_result = array();
parent::hydrateRowData($data, $hydrated_result);
/** #var ProjectAssignment $project_assignment */
$project_assignment = $hydrated_result[0][0];
$result[] = $project_assignment;
}
}
Here is my config: config.yml
doctrine:
orm:
hydrators:
project_assignment_statisticsdata_hydrator: AppBundle\Hydrator\ProjectAssignment\StatisticsDataHydrator
Where I don't use the hydrator I have no problem:
/**
* #param ProjectStage $stage
* #return array
*/
public function findByStageWithStatisticsData(ProjectStage $stage){
$qb = $this->createQueryBuilder('pa');
$qb
//->addSelect('44')
->where($qb->expr()->eq('pa.project_stage', ':stage'))
->setParameter('stage', $stage);
return $qb->getQuery()->getResult();
}
But when I use my hydrator:
/**
* #param ProjectStage $stage
* #return array
*/
public function findByStageWithStatisticsData(ProjectStage $stage){
$qb = $this->createQueryBuilder('pa');
$qb
->addSelect('1234') // referencial value
->where($qb->expr()->eq('pa.project_stage', ':stage'))
->setParameter('stage', $stage);
return $qb->getQuery()->getResult('project_assignment_statisticsdata_hydrator');
}
The strangest behavior is that the same occure with this config: config.yml
doctrine:
orm:
hydrators:
project_assignment_statisticsdata_hydrator: Doctrine\ORM\Internal\Hydration\ObjectHydrator
I have tried all kind of fetch on relation with no success:
#ORM\ManyToOne(... , fetch="EAGER")
#ORM\ManyToOne(... , fetch="LAZY")
...
Maybe I have to use a Proxy on my Entity, I really don't know :(
Thank you for any help!
Great! I found the problem, it was with my query builder. I had to manually add the joins and the select of related objects.
/**
* #param ProjectStage $stage
* #return array
*/
public function findByStageWithStatisticsData(ProjectStage $stage){
$qb = $this->createQueryBuilder('pa');
$qb
->addSelect('e') // added
->addSelect('r') // added
->addSelect('1234')
->leftJoin('pa.employee', 'e') // added
->leftJoin('pa.role', 'r') // added
->where($qb->expr()->eq('pa.project_stage', ':stage'))
->setParameter('stage', $stage);
return $qb->getQuery()->getResult('project_assignment_statisticsdata_hydrator');
}
Bonus, here is my Hydrator (it can help someone):
namespace AppBundle\Hydrator\ProjectAssignment;
use AppBundle\Entity\Hydrator\ProjectAssignment\StatisticsData;
use AppBundle\Entity\ProjectAssignment;
use Doctrine\ORM\Internal\Hydration\ObjectHydrator;
class StatisticsDataHydrator extends ObjectHydrator
{
/**
* {#inheritdoc}
*/
protected function hydrateRowData(array $data, array &$result)
{
$hydrated_result = array();
parent::hydrateRowData($data, $hydrated_result);
/** #var ProjectAssignment $project_assignment */
$project_assignment = $hydrated_result[0][0];
$keys = array_keys($hydrated_result); $key = end($keys);
$statistics_data = new StatisticsData($project_assignment);
$statistics_data->setTotalWorkedTime((int)$hydrated_result[$key][1]);
$project_assignment->setStatisticsData($statistics_data);
$result[] = $project_assignment;
}
}
In my Entity I have the folowing attribute/getter/setter
/********** NON SYNCED FIELDS **********/
/** #var StatisticsData $statistics_data */
private $statistics_data;
/**
* #return StatisticsData
*/
public function getStatisticsData()
{
return $this->statistics_data;
}
/**
* #param StatisticsData $statistics_data
*/
public function setStatisticsData($statistics_data)
{
$this->statistics_data = $statistics_data;
}
/***************************************/
The problem is that the Doctrine SqlWalker will not load meta columns, which include subclasses and associations, if the query Hydration Mode is not HYDRATE_OBJECT or if the query hint HINT_INCLUDE_META_COLUMNS is not set to true:
$addMetaColumns = ! $this->query->getHint(Query::HINT_FORCE_PARTIAL_LOAD) &&
$this->query->getHydrationMode() == Query::HYDRATE_OBJECT
||
$this->query->getHydrationMode() != Query::HYDRATE_OBJECT &&
$this->query->getHint(Query::HINT_INCLUDE_META_COLUMNS);
The problem has already been reported in this issue.
As mentioned by the author of the issue, you can either implement the suggested fix, or set the query hint HINT_INCLUDE_META_COLUMNS to true:
$query = $queryBuilder->getQuery();
$query->setHint(Query::HINT_INCLUDE_META_COLUMNS, true);
$result = $query->getResult('CustomHydrator');

Symfony doctrine ManyToMany unidirectional mapping

Well, back again, i'll try to simplify my question as much as i can.
First of all, i have 2 Entities
Post
PostRating
I've created unidirectional ManyToMany relation between them, because I only need ratings to be added to each Post, if I try to map Post to PostRating too, I get Circular Reference error.
Post Entity, it creates 3rd table post_has_rating, no mapping inside PostRating Entity, It workes like expected, rating collection is added to each post, but if i want to find one rating, and edit it if needed, then it comes to be bigger headache than expected.
/**
* Post have many PostRating
* #ORM\ManyToMany(targetEntity="PostRating")
* #ORM\JoinTable(name="post_has_rating",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="postrating_id", referencedColumnName="id", unique=true)}
* )
*/
protected $ratings;
PostController thumbAction, simple word "ratingAction"
/**
* Search related videos from youtube
* #Route("/post/thumb", name="post_thumb")
* #param Request $request
* #return string
*/
public function thumbAction (Request $request) {
$content = json_decode($request->getContent());
$serializer = $this->get('serializer');
$em = $this->getDoctrine()->getManager();
$postRatingRepo = $this->getDoctrine()->getRepository(PostRating::class);
$postRepo = $this->getDoctrine()->getRepository(Post::class);
$me = $this->getUser()->getId();
/** #var PostRating $rating */
$rating = $postRatingRepo->findOneBy(['userId' => $me]);
/** #var Post $post */
$post = $postRepo->find($content->id);
if ($post->getRatings()->contains($rating)) {
$post->removeRating($rating);
$em->remove($rating);
}
$rating = new PostRating();
$rating->setUserId($me);
switch ($content->action) {
//NVM about those hardcoded words, they are about to be removed
case 'up':
$rating->setRating(1);
break;
case 'down':
$rating->setRating(0);
break;
}
$post->addRating($rating);
$em->persist($rating);
$em->persist($post);
$em->flush();
return new JsonResponse( $serializer->normalize( ['success' => 'Post thumbs up created'] ) );
}
Problems: $rating = $postRatingRepo->findOneBy(['userId' => $me]); this row needs to have postId too for $post->getRatings()->contains($rating), right now im getting all the raitings, that I have ever created, but it Throws error if i add it, Unknown column
Should i create custom repository, so i can create something like "findRating" with DQL?
OR
Can i make Post and PostRating Entities mapped to each other more simple way, i don't really want many-to-many relation, because I don't see point of using it
Considering you want to keep OneToMany unidirectional here is my suggestion
create a custom repository for your Post Entity
namespace AppBundle\Repository;
use Doctrine\ORM\EntityRepository;
class PostRepository extends EntityRepository
{
public function findOneRatingByUser($post, $user)
{
$query = $this->createQueryBuilder('p')
->select('r')
->innerJoin('p.ratings', 'r')
->where('p.id = :post')
->andWhere('r.user = :user')
->setParameter('post', $post)
->setParameter('user', $user)
->getQuery()
;
return $query->getOneOrNullResult();
}
}
Then in your controller:
public function thumbAction (Request $request)
{
$content = json_decode($request->getContent());
$serializer = $this->get('serializer');
$em = $this->getDoctrine()->getManager();
$postRepo = $this->getDoctrine()->getRepository(Post::class);
$me = $this->getUser()->getId();
/** #var Post $post */
$post = $postRepo->find($content->id);
$rating = $postRepo->findOneRatingByUser($post->getId(), $me);
if (null === $rating) {
$rating = new PostRating();
$rating->setUserId($me);
}
switch ($content->action) {
//NVM about those hardcoded words, they are about to be removed
case 'up':
$rating->setRating(1);
break;
case 'down':
$rating->setRating(0);
break;
}
$post->addRating($rating);
$em->persist($rating);
$em->persist($post);
$em->flush();
return new JsonResponse( $serializer->normalize( ['success' => 'Post thumbs up created'] ) );
}
If you want your custom repository to work dont forget to declare it in your entity
/**
* #ORM\Entity(repositoryClass="AppBundle\Repository\PostRepository")
*/
class Post

Doctrine querybuilder where element is child of a doctrine nested set tree parent

I want to build a querybuilder that return elements which have at a certain point a given element in their parents.
Doubling on the parent or using root is not fine because there might be multiple levels.
So far I have this code :
return = $this
->createQueryBuilder('o')
->leftJoin('o.organizationCategories', 'c')
->leftJoin('c.parent', 'parent')
->where('parent = ?2')
->orWhere('parent.parent = ?2')
->setParameter(2, $parent)
->getQuery()
->getResult();
Of course if the parent is not the root parent nor the grandparent not the parent this does not work anymore.
How can I do ?
I ended up doing the following
/**
* ProductRepository
*
* This class was generated by the Doctrine ORM. Add your own custom
* repository methods below.
*/
abstract class HasCategoryRepository extends EntityRepository
{
/**
* #return QueryBuilder
*/
abstract function getExplorerQueryBuilder();
/**
* Filters products on ingredients, which belong to the category 'consommable'
*
* #param ObjectCategory $objectCategory
* #throws \Exception
* #return \Doctrine\ORM\QueryBuilder
*/
public function findByTopCategoryQueryBuilder($objectCategory)
{
$objectCategoryRepository = $this->_em->getRepository('AppBundle:Core\ObjectCategory');
if (!$objectCategory instanceof ObjectCategory) $objectCategory=$objectCategoryRepository->findOneBy(array('slug' => $objectCategory));
if (!$objectCategory) throw new \Exception("La catégorie n'a pas été trouvée.");
$queryBuilder = $this->getExplorerQueryBuilder();
$categories = array_merge(array($objectCategory), $objectCategoryRepository->children($objectCategory));
$categoriesIds = array_map(function(BaseCategoryClass $category){return $category->getid();}, $categories);
$queryBuilder = $queryBuilder
->andWhere($this->createQueryBuilder('object')->expr()->in('c.id', $categoriesIds))
;
return $queryBuilder;
}
}

Doctrine DQL doesn't set ON JOIN automatically

I thought if I don't want to override relation's on condition I just use WITH to add an additional condition. I'm not sure if my mapping is wrong, but DQL without join condition in WITH makes a CROSS Join
Here My Entities Relations (Symfony):
Movie:
/**
* #ORM\OneToMany(targetEntity="MovieInterest", mappedBy="movie")
*/
private $movieInterests;
MovieInterest:
/**
* #ORM\ManyToOne(targetEntity="Movie", inversedBy="movieInterests")
*/
private $movie;
Here my Query in MovieInterestRepository:
public function deleteByMovieId($id, $user)
{
$em = $this->getEntityManager();
$result = $em->createQuery(
"SELECT mi FROM CineupsWebAppBundle:MovieInterest mi
JOIN CineupsWebAppBundle:Movie m WITH m.id=mi.movie
WHERE m.id=".$id." AND mi.user=".$user->getId())
->getResult();
foreach ($result as $entity) {
$em->remove($entity);
}
$em->flush();
}
If I remove WITH m.id=mi.movie the Query gets too many results
Try to do it this way:
$result = $em->getRepository('CineupsWebAppBundle:MovieInterest')
->createQuery('mi')
->innerJoin('mi.Movie', 'm')
->andWhere('m.id = :id')
->andWhere('mi.user = :user')
->getQuery()
->setParameter(':id', $id)
->setParameter(':user', $user->getId())
->getResult();
This will also solve sqj-injection vulnerability at your original query
You need to add mapping information to your entity:
/**
* #ORM\ManyToOne(targetEntity="Movie", inversedBy="movieInterests")
* #ORM\JoinColumn(name="id", referencedColumnName="movie")
*/
private $movie;
I found the problem: I joined with table and not with relation. Here the correct code:
"SELECT mi FROM CineupsWebAppBundle:MovieInterest mi
JOIN mi.movie m
WHERE m.id=".$id." AND mi.user=".$user->getId())

FOS bundle - How to select users with a specific role?

I am using the FOS bundle and I want to retrieve all users with a given ROLE from the database.
What is the best way to do this?
Just add this in your UserRepository or replace $this->_entityName by YourUserBundle:User:
/**
* #param string $role
*
* #return array
*/
public function findByRole($role)
{
$qb = $this->_em->createQueryBuilder();
$qb->select('u')
->from($this->_entityName, 'u')
->where('u.roles LIKE :roles')
->setParameter('roles', '%"'.$role.'"%');
return $qb->getQuery()->getResult();
}
If you are using FOSUser Groups you should use:
/**
* #param string $role
*
* #return array
*/
public function findByRole($role)
{
$qb = $this->_em->createQueryBuilder();
$qb->select('u')
->from($this->_entityName, 'u')
->leftJoin('u.groups', 'g')
->where($qb->expr()->orX(
$qb->expr()->like('u.roles', ':roles'),
$qb->expr()->like('g.roles', ':roles')
))
->setParameter('roles', '%"'.$role.'"%');
return $qb->getQuery()->getResult();
}
Well, if there is no better solution, I think I will go to a DQL query:
$query = $this->getDoctrine()->getEntityManager()
->createQuery(
'SELECT u FROM MyBundle:User u WHERE u.roles LIKE :role'
)->setParameter('role', '%"ROLE_MY_ADMIN"%');
$users = $query->getResult();
If you have this requirement and your user list will be extensive, you will have problems with performance. I think you should not store the roles in a field as a serialized array. You should create an entity roles and many to many relationship with the users table.
As #Tirithen states, the problem is that you will not get the users that have an implicit role due to role hierarchy. But there is a way to work around that!
The Symfony security component provides a service that gives us all child roles for a specific parent roles. We can create a service that does almost the same thing, only it gives us all parent roles for a given child role.
Create a new service:
namespace Foo\BarBundle\Role;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\Security\Core\Role\Role;
/**
* ReversedRoleHierarchy defines a reversed role hierarchy.
*/
class ReversedRoleHierarchy extends RoleHierarchy
{
/**
* Constructor.
*
* #param array $hierarchy An array defining the hierarchy
*/
public function __construct(array $hierarchy)
{
// Reverse the role hierarchy.
$reversed = [];
foreach ($hierarchy as $main => $roles) {
foreach ($roles as $role) {
$reversed[$role][] = $main;
}
}
// Use the original algorithm to build the role map.
parent::__construct($reversed);
}
/**
* Helper function to get an array of strings
*
* #param array $roleNames An array of string role names
*
* #return array An array of string role names
*/
public function getParentRoles(array $roleNames)
{
$roles = [];
foreach ($roleNames as $roleName) {
$roles[] = new Role($roleName);
}
$results = [];
foreach ($this->getReachableRoles($roles) as $parent) {
$results[] = $parent->getRole();
}
return $results;
}
}
Define your service for instance in yaml and inject the role hierarchy into it:
# Provide a service that gives you all parent roles for a given role.
foo.bar.reversed_role_hierarchy:
class: Foo\BarBundle\Role\ReversedRoleHierarchy
arguments: ["%security.role_hierarchy.roles%"]
Now you are ready to use the class in your own service. By calling $injectedService->getParentRoles(['ROLE_YOUR_ROLE']); you will get an array containing all parent roles that will lead to the 'ROLE_YOUR_ROLE' permission. Query for users that have one or more of those roles... profit!
For instance, when you use MongoDB you can add a method to your user document repository:
/**
* Find all users with a specific role.
*/
public function fetchByRoles($roles = [])
{
return $this->createQueryBuilder('u')
->field('roles')->in($roles)
->sort('email', 'asc');
}
I'm not into Doctrine ORM but I'm sure it won't be so different.
You can use just this on your DQL:
SELECT u FROM YourFavouriteBundle:User u WHERE u.roles [NOT] LIKE '%ROLE_YOUR_ROLE%'
Of course with QueryBuilder it's more elegant:
// $role = 'ROLE_YOUR_ROLE';
$qb->where('u.roles [NOT] LIKE :role')
->setParameter('role', "%$role%");
Finally i solved it, following is an exact solution:
public function searchUsers($formData)
{
$em = $this->getEntityManager();
$usersRepository = $em->getRepository('ModelBundle:User');
$qb = $usersRepository->createQueryBuilder('r');
foreach ($formData as $field => $value) {
if($field == "roles"){
$qb->andWhere(":value_$field MEMBER OF r.roles")->setParameter("value_$field", $value);
}else{
$qb->andWhere("r.$field = :value_$field")->setParameter("value_$field", $value);
}
}
return $qb->getQuery()->getResult();
}
Cheers!
In case you need to filter users by role using a DQL filter in a YAML file (In EasyAdminBundle for instance)
entities:
Admin:
class: App\Entity\User
list:
dql_filter: "entity.roles LIKE '%%ROLE_ADMIN%%'"
Here I give an alternative solution :
I find users of roles for a given array
In controller I call the function like that
$users = $userRepository->findUsersOfRoles(['ROLE_ADMIN', 'ROLE_SUPER_USER']);
Then in my repository I make a loop to generate condition and set the parameters :
public function findUsersOfRoles($roles)
{
$condition = 'u.roles LIKE :roles0';
foreach ($roles as $key => $role){
if ($key !== 0){
$condition .= " OR u.roles LIKE :roles".$key;
}
}
$query = $this->createQueryBuilder('u')
->where($condition);
foreach ($roles as $key => $role){
$query ->setParameter('roles'.$key, '%"'.$role.'"%');
}
return $query->getQuery() ->getResult();
}

Resources