I am making a query with Doctrine which calculates a custom field using a CASE WHEN like this:
public function findLatestPaginator($page = 1, $itemsPerPage)
{
$qb = $this->createQueryBuilder('n');
$qb = $qb
->select(['n AS news', 'CASE WHEN lu.id IS NOT NULL THEN 1 ELSE 0 END AS n.liked'])
->leftJoin('n.likingUsers', 'lu')
;
$qb = $qb
->orderBy('n.date', 'DESC')
->setFirstResult($itemsPerPage * ($page - 1))
->setMaxResults($itemsPerPage)
->getQuery()
;
return $qb->getResult();
}
In my entity I have a field called $liked which is not mapped. Is it possible to make the query (or the hydrator?) automatically set the field on the resulting entity?
Right now I am making a foreach loop and manually setting the property:
/**
* #return News[]
*/
private function convertNews(array $records)
{
$newsList = [];
foreach ($records as $record) {
if (isset($record['liked'], $record['news'])) {
/** #var News */
$news = $record['news'];
$news->liked = boolval($record['liked']);
$newsList[] = $news;
}
}
return $newsList;
}
Maybe a DTO could be useful here : doctrine documentation
Basically, you define a PHP class which is not mapped to your model.
You can then select what you need and trigger the instantiation of an object of this PHP class from the DQL query.
Hope this helps !
Default hydrator does not include unmapped properties (wow, right?).
You want to override/extend doctrine/orm/lib/Docrtrine/ORM/Internal/Hydration/AbstractHydrator
line 386 where it only checks for $classMetadata fieldMappings
Or you can hack around by using DTO and query in transformer, if performance is not needed
Related
In a Doctrine Criteria instance, I have some static parameters, that I wrote directly in the query as below :
/**
* #return \Doctrine\Common\Collections\Criteria
*/
public function lastContratCriteria()
{
$criteria = Criteria::create();
$baseCondition = $criteria->expr()->andX(
$criteria->expr()->eq('BaseType', 1), // STATIC VALUE
$criteria->expr()->orX(
$criteria->expr()->gt('dateFinContractuellePrevue','CURRENT_DATE()'), // STATIC VALUE
$criteria->expr()->isNull('dateFinContractuellePrevue')
)
);
$criteria
->andWhere($baseCondition)
->orderBy(['dateDebut' => 'DESC'])
->setMaxResults(1);
return $criteria;
}
But this throws:
QueryException : Too few parameters: the query defines 2 parameters but you bound 0.
This is the DQL output, where you can see bound params based on property name (which I did not defined) :
WHERE contrat.BaseType = :BaseType AND (contrat.dateFinContractuellePrevue > :dateFinContractuellePrevue OR contrat.dateFinContractuellePrevue IS NULL)
I could not find in the docs why my static params were not used. To debug , I changed the static values to bound parameters :
$baseCondition = $criteria->expr()->andX(
$criteria->expr()->eq('BaseType', ':bType'),
$criteria->expr()->orX(
$criteria->expr()->gt('dateFinContractuellePrevue',':dateFinCP'),
$criteria->expr()->isNull('dateFinContractuellePrevue')
)
);
But No matter how I named the parameters, to make it work I had to call each one in setParameter() with its correspondent property name instead of the custom name I choose :
->setParameter('BaseType', 1) // Should be bType as above, not BaseType ?
->setParameter('dateFinContractuellePrevue', 'CURRENT_DATE()') // Should be :dateFinCP ?
Here's the complete query :
/**
* #return \Doctrine\Common\Collections\Criteria
*/
public function lastContratCriteria()
{
$criteria = Criteria::create();
$baseCondition = $criteria->expr()->andX(
$criteria->expr()->eq('BaseType', ':bType'),
$criteria->expr()->orX(
$criteria->expr()->gt('dateFinContractuellePrevue',':dateFinCP'),
$criteria->expr()->isNull('dateFinContractuellePrevue')
)
);
}
/**
* #return \Doctrine\ORM\QueryBuilder
* #throws \Doctrine\ORM\Query\QueryException
*/
public function getCurrentContratQB()
{
return $this->createQueryBuilder('contrat')
->addCriteria($this->lastContratCriteria());
}
/**
* #return \Doctrine\Common\Collections\Collection
* #throws \Doctrine\ORM\Query\QueryException
*/
public function getSeminaire()
{
$qb = $this->createQueryBuilder('salarie')
->andWhere('salarie.archive = FALSE')
->leftJoin('salarie.contrats', 'c')
->addSelect('c')
->andWhere("c.id IN (". $this->contratRepository->getCurrentContratQB() .")")
// ONLY ORIGINAL FIELD NAME IS RECOGNIZED HERE, not the bound parameter name
->setParameter('BaseType', 1) // Should be bType, not BaseType
->setParameter('dateFinContractuellePrevue', 'CURRENT_DATE()') // Should be :dateFinCP
//dump($qb->getDQL());
return $qb->getQuery()
->getResult();
}
Why can't I pass static values in $criteria->expr()->eq() ? Why is Doctrine demanding me to set parameters with the exact name of the queried property instead of taking care of the parameters I named ?
You need the DQL of the subquery to add it inside another querybuilder and expose the original parameter names.
Try replacing
->andWhere("c.id IN (". $this->contratRepository->getCurrentContratQB() .")")
with
->andWhere("c.id IN (". $this->contratRepository->getCurrentContratQB()->getDQL() .")")
or
; $qb->andWhere($qb->expr()->in("c.id", $this->contratRepository->getCurrentContratQB()->getDQL())
With easy admin you have the possibility to sort on one field for a list.
Symfony - Easy Admin v2: Sorting Entity Listings
But is there any way to sort on more than one field for my list ?
You can do it overriding createListQueryBuilder or createSearchQueryBuilder as mentioned here.
Example:
protected function createListQueryBuilder($entityClass, $sortDirection, $sortField = null, $dqlFilter = null)
{
/* #var EntityManager */
$em = $this->getDoctrine()->getManagerForClass($this->entity['class']);
/* #var QueryBuilder */
$queryBuilder = $em->createQueryBuilder()
->select('entity')
->from($this->entity['class'], 'entity')
;
if (!empty($dqlFilter)) {
$queryBuilder->andWhere($dqlFilter);
}
$queryBuilder->addOrderBy('entity.status', 'ASC');
$queryBuilder->addOrderBy('entity.createdAt', 'DESC');
return $queryBuilder;
}
In my Symfony code I've used Doctrine. In an Entity ( AppBundle\Entity\Core\User ) I defined a column foodTypes, which is associated with another Entity (AppBundle\Entity\FoodRecording\FoodType). I've defined an Many-to-Many relationship between User and FoodType, with a linking table foodrecording_user, joining User.username and FoodType.foodtype_code. The code is shown below.
// Entity\Core\User
namespace AppBundle\Entity\Core;
......
class User implements AdvancedUserInterface, \Serializable {
......
/**
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\FoodRecording\FoodType")
* #ORM\JoinTable(name="foodrecording_user",
* joinColumns={#ORM\JoinColumn(name="username", referencedColumnName="username", onDelete="CASCADE")},
* inverseJoinColumns={#ORM\JoinColumn(name="foodtype_code", referencedColumnName="code", onDelete="CASCADE")}
* )
*/
private $foodTypes;
public function getFoodTypes()
{
$this->foodTypes = new \Doctrine\Common\Collections\ArrayCollection();
return $this->foodTypes;
}
However, as I wanted to get directly all the food types of a certain user, using
$userFoodTypes = $this->get('security.token_storage')->getToken()->getUser()->getFoodTypes();
then I got
$userFoodTypes =====> array[]
I expected that as I've created the M-M relationship, Doctrine would automatically fetch the data I need, but it is not the case!
Therefore, I have to write my own code to retrieve the data from the DB / table like following:
public function fetchUserFoodTypes()
{
global $kernel;
$container = $kernel->getContainer();
$em = $container->get('doctrine.orm.entity_manager');
$conn = $em->getConnection();
$sql = 'SELECT * FROM foodrecording_user where username = :username';
$stmt = $conn->prepare($sql);
$stmt->execute([
'username' => $this->getUsername(),
]);
$data = $stmt->fetchAll();
$res = [];
foreach ($data as $item) {
$foodtype = $em->getRepository('AppBundle\Entity\FoodRecording\FoodType')->findByCode($item['foodtype_code']);
$res[] = $foodtype;
}
return $res;
}
public function getFoodTypes()
{
$this->foodTypes = $this->fetchUserFoodTypes();
//$this->foodTypes = new \Doctrine\Common\Collections\ArrayCollection();
return $this->foodTypes;
}
Only in this way I am able to get the food types associated with a user.
Could anyone explain to me, why I can't simply use the M-M definition and let doctrine do all the thing automatically for me? Why should I explicitly write my own function to retrieve data from DB? Is Doctrine not smart enough?
This part:
$this->foodTypes = new \Doctrine\Common\Collections\ArrayCollection();
Belongs to the __construct method, not getter.. You see, like this, every time you call your getter, you reset the property foodTypes to an empty instance of an ArrayCollection
I've been reading Doctrine's documentation, but I haven't been able to find a way to sort findAll() Results.
I'm using symfony2 + doctrine, this is the statement that I'm using inside my Controller:
$this->getDoctrine()->getRepository('MyBundle:MyTable')->findAll();
but I want the results to be ordered by ascending usernames.
I've been trying to pass an array as an argument this way:
findAll( array('username' => 'ASC') );
but it doesn't work (it doesn't complain either).
Is there any way to do this without building a DQL query?
As #Lighthart as shown, yes it's possible, although it adds significant fat to the controller and isn't DRY.
You should really define your own query in the entity repository, it's simple and best practice.
use Doctrine\ORM\EntityRepository;
class UserRepository extends EntityRepository
{
public function findAll()
{
return $this->findBy(array(), array('username' => 'ASC'));
}
}
Then you must tell your entity to look for queries in the repository:
/**
* #ORM\Table(name="User")
* #ORM\Entity(repositoryClass="Acme\UserBundle\Entity\Repository\UserRepository")
*/
class User
{
...
}
Finally, in your controller:
$this->getDoctrine()->getRepository('AcmeBundle:User')->findAll();
$this->getDoctrine()->getRepository('MyBundle:MyTable')->findBy([], ['username' => 'ASC']);
Simple:
$this->getDoctrine()->getRepository('AcmeBundle:User')->findBy(
array(),
array('username' => 'ASC')
);
It's useful to look at source code sometimes.
For example findAll implementation is very simple (vendor/doctrine/orm/lib/Doctrine/ORM/EntityRepository.php):
public function findAll()
{
return $this->findBy(array());
}
So we look at findBy and find what we need (orderBy)
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
This works for me:
$entities = $em->getRepository('MyBundle:MyTable')->findBy(array(),array('name' => 'ASC'));
Keeping the first array empty fetches back all data, it worked in my case.
Look at the Doctrine API source-code :
class EntityRepository{
...
public function findAll(){
return $this->findBy(array());
}
...
}
You need to use a criteria, for example:
<?php
namespace Bundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\Common\Collections\Criteria;
/**
* Thing controller
*/
class ThingController extends Controller
{
public function thingsAction(Request $request, $id)
{
$ids=explode(',',$id);
$criteria = new Criteria(null, <<DQL ordering expression>>, null, null );
$rep = $this->getDoctrine()->getManager()->getRepository('Bundle:Thing');
$things = $rep->matching($criteria);
return $this->render('Bundle:Thing:things.html.twig', [
'entities' => $things,
]);
}
}
findBy method in Symfony excepts two parameters. First is array of fields you want to search on and second array is the the sort field and its order
public function findSorted()
{
return $this->findBy(['name'=>'Jhon'], ['date'=>'DESC']);
}
You can sort an existing ArrayCollection using an array iterator.
assuming $collection is your ArrayCollection returned by findAll()
$iterator = $collection->getIterator();
$iterator->uasort(function ($a, $b) {
return ($a->getPropery() < $b->getProperty()) ? -1 : 1;
});
$collection = new ArrayCollection(iterator_to_array($iterator));
This can easily be turned into a function you can put into your repository in order to create findAllOrderBy() method.
Try this:
$em = $this->getDoctrine()->getManager();
$entities = $em->getRepository('MyBundle:MyTable')->findBy(array(), array('username' => 'ASC'));
I use an alternative to the solution that wrote nifr.
$resultRows = $repository->fetchAll();
uasort($resultRows, function($a, $b){
if ($a->getProperty() == $b->getProperty()) {
return 0;
}
return ($a->getProperty()< $b->getProperty()) ? -1 : 1;
});
It's quicker than the ORDER BY clause, and without the overhead of the Iterator.
Modify the default findAll function in EntityRepository like this:
public function findAll( array $orderBy = null )
{
return $this->findBy([], $orderBy);
}
That way you can use the ''findAll'' on any query for any data table with an option to sort the query
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();
}