Adding a Having Clause to Doctrine Statement - php

I am new to Doctrine and I am trying to figure out how to add a having clause on my statement. Basically I want to be able to filter down on items returned based on how many attributes the user selects. The code is as follows:
// create query builder
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('p')
->from($this->_entityName, 'p')
->leftJoin('p.options', 'o')
->where('p.active = :active')
->setParameter('active', 1);
// add filters
$qb->leftJoin('o.attributes', 'a');
$ands = array();
foreach ($value as $id => $values)
{ echo count($values);
$ands[] = $qb->expr()->andX(
$qb->expr()->eq('a.attribute_id', intval($id)),
$qb->expr()->in('a.attribute_value_id', array_map('intval', $values))
$qb->having('COUNT(*)=3) // THIS DOESN'T WORK
//$qb->expr()->having('COUNT(*)=3) // THIS DOESN'T WORK EITHER
);
}
$where = $qb->expr()->andX();
foreach ($ands as $and)
{
$where->add($and);
}
$qb->andWhere($where);
$result = $qb->getQuery()->getResult();
return $result;
When I try to execute the statement with the having() clause I get this error:
Expression of type 'Doctrine\ORM\QueryBuilder' not allowed in this context.
Without the having() clause everything works perfectly.
I have no idea how to solve this.

HAVING clause requires a GROUP BY. In doctrine it would be something like that:
$qb->groupBy('p.id'); // or use an appropriate field
$qb->having('COUNT(*) = :some_count');
$qb->setParameter('some_count', 3);
Assuming you're using mysql, here is a having clause tutorial: http://www.mysqltutorial.org/mysql-having.aspx

Perhaps you should bind number 3 to a parameter:
$qb->having('COUNT(*)=:some_count')
$qb->setParameter('some_count',3)

Goal: filter down The one side where we have some known summable conditions we want to filter by (e.g., the count of Original Parts in a Bundle) on the many side of a O2M relationship wehere want to limit the One side along with some other criteria to select on.
We are then adding in a few conditions for the LEFT_JOIN operation:
Condition #1 - the bundle.id == the original_part.bundle ID.
Condition #2 - The original_part's partStatusType == 3 (some hard-coded value).
Then we filter down to the COUNT(op) >= 1 which gives our more limited response that works just fine with pagination.
$qb->leftJoin(OriginalPart::class, 'op', Expr\Join::WITH,
$qb->expr()->andX(
$qb->expr()->eq('op.bundle', 'row.id'),
$qb->expr()->eq('op.partStatusType', 3)));
$qb->groupBy('row.id');
$qb->having($qb->expr()->gte('COUNT(op)', 1));
row is the alias for the One (bundle entity).

Related

Order pagination result in Symfony 2

I have a question about the order in the pagination in Symfony 2.7.
Before we used pagination we used the PHP function usort to sort some things. But my question now is how could we implement the usort in the doctrine query with the same order like the usort. Which needs to be working with the Paginator. Since when we use now the query (here under) we don't get the proper results.
usort function:
usort($result, function ($a, $b) {
$aBegin = $a->getStartDate() ?: $a->getCreatedDate();
$bBegin = $b->getStartDate() ?: $b->getCreatedDate();
if ($aBegin < $bBegin) {
return -1;
}
if ($aBegin == $bBegin) {
return 0;
}
if ($aBegin > $bBegin) {
return 1;
}
return 0;
});
How could we implemented the usort in the following query:
$build = $this->createQueryBuilder('building');
$build
->addSelect('users', 'furniture')
->join('building.users', 'users')
->leftJoin('building.furniture', 'furniture')
->where('building.id = :id')
->setParameter('id', $id)
->orderBy('building.getStartDate', 'ASC')
->addOrderBy('building.getCreatedDate', 'DESC');
$paginator = new Paginator($build->getQuery(), $fetchJoinCollection = true);
$result = $paginator->getQuery()
->setFirstResult($offset)
->setMaxResults($limit)
->getResult();
Thanks!
Doctrine orm: 2.2.3,
Symfony version: 2.7
To add such a condition, you can use a CASE expression in your select clause. You can write something like CASE WHEN b.startDate IS NULL THEN b.createdDate ELSE b.startDate END to have the behaviour described in your usort function.
That being said, you can't simply add this to your order by clause. You will need to select this value, give it an alias and then add an order by based on the newly selected value. Since you probably don't want to get a mixed result (where your entities would be mixed with scalar values), you can use the HIDDEN keyword to remove the computed field from the result set.
All put together, it could look like this:
// $qb your query builder with all your other parameters
$qb->addSelect('CASE
WHEN building.startDate IS NULL
THEN building.createdDate
ELSE building.startDate
END
AS HIDDEN beginDate');
$qb->orderBy('beginDate', 'DESC');
Note that while this works, you might encounter performance issues if you have a lot of entries in your table as the whole table is very likely to be scanned entirely for this query to be executed.

How to use one query for different selects in laravel?

I thought it would be a good idea to define a query and use it for several selects or counts later, but it does not work. The second select has both wheres in the sql statement:
$query = Pic::where('pics.user_id',$user->id);
if($cat) $query->where('cat',$cat);
if($year) $query->where('jahrprod',$year);
$zb = $query->select('pics.id','pics.title','pics.created_at')
->where('pics.id', '>', $pic->id)
->orderBy('pics.id')
->take(2)
->get()->reverse();
$za = $query->select('pics.id','pics.title','pics.created_at')
->where('pics.id', '<', $pic->id)
->orderBy('pics.id')
->take(13)
->get();
Query:
SELECT `pics`.`id`, `pics`.`title`, `pics`.`created_at`
FROM `pics`
WHERE `pics`.`user_id` = '3'
AND `pics`.`id` > '2180'
AND `pics`.`id` < '2180'
ORDER BY `pics`.`id` ASC, `pics`.`id` ASC
LIMIT 13
I tried to "pass it as reference" i.e. &$query->select... but "only variables can be passed as reference".
How can I use the query , or save it, and use it for both actions. Is it possible?
You are updating object state with the statements when you do $query->where(), so yeah, when you're doing a second select, all conditions from the first one are still there. Thats the reason why these lines work without any assignments:
if($cat) $query->where('cat',$cat);
if($year) $query->where('jahrprod',$year);
To achieve described behaviour you would need to create an query object copy:
$query = Pic::where('pics.user_id',$user->id);
if($cat) $query->where('cat',$cat);
if($year) $query->where('jahrprod',$year);
$queryClone = clone $query;
$zb = $query->select('pics.id','pics.title','pics.created_at')
->where('pics.id', '>', $pic->id)
->orderBy('pics.id')
->take(2)
->get()->reverse();
$za = $queryClone->select('pics.id','pics.title','pics.created_at')
->where('pics.id', '<', $pic->id)
->orderBy('pics.id')
->take(13)
->get();
Notice that mere assignment would not work here:
$queryClone = $query;
Because that would pass object reference and would result in the same behaviour as in your case. Clone creates a full object copy.
http://php.net/manual/en/language.oop5.cloning.php

Assign a query to variable to then perform two separate queries?

Is it possible to start an eloquent query, assign it to a variable then continue using the variable for two separate queries without them conflicting with one another. A simple example:
$students = $this->student
// more query stuff
->where('is_active', 1);
$bachelorStudents = $students
->where('course_id', 3)
->get();
$masterStudents = $students
->where('course_id', 4)
->get();
or would I need to do:
$bachelorStudents = $this->student
->where('course_id', 3)
->get();
$masterStudents = $this->student
->where('course_id', 4)
->get();
I always thought I could do the former, but some of my results appear to show I can't but I am open to believe that if you can do it then perhaps I'm doing something wrong.
When you're calling
$students = $this->student->where('is_active', 1);
you're creating a query builder object. Calling where*() on this object updates the object by adding given criteria. Therefore it's not possible to achieve what you want in your first code snippet, because when you call
$masterStudents = $students
->where('course_id', 4)
->get();
the query builder already contains where('course_id', 3) constraint added when you bachelorStudents.
Once you do that:
$students = $this->student->where('is_active', 1);
$stundents will contain a query builder with your where clause
If you do:
$bachelorStudents = $students->where('course_id', 3)->get();
You'll add another where clasuse to the $students builder, and this should work as you expect
But, when you do:
$masterStudents = $students->where('course_id', 4)->get();
You are adding another where clasuse to the same $students builder, thus resulting the query builder to be something like this:
$students->where('is_active', 1)
->where('course_id', 3)
->where('course_id', 4)
->get();
That probably isn't what you expect, because you have 2 where clauses with different course_id values
Think of $student as an object you modify everytime you add a clause, so you can use it for progressive query building, but remember that once you've added a clause to the query builder, the object is modified and the clause will be keept in the builder, so when you re-use the builder it will contain all the clasuses you previously added
Also, Rembember that when you need to apply some pre-defined filters to your query, in Laravel you should use query scopes
While everyone is explaining query builder and how it works, here's your answer.
1) Start off your query builder
$studentsQuery = $this->student
//Start a new query builder (optional)
->newQuery()
->where('is_active', 1);
2) Clone the initial query builder to our separate queries
$bachelorStudentsQuery = clone $studentsQuery;
$masterStudentsQuery = clone $studentsQuery;
3) Assign your where conditions and get the results
$bachelorStudentsResult = $bachelorStudentsQuery->where('course_id', 3)
->get();
$masterStudentsResult = $masterStudentsQuery->where('course_id',4)
->get();
Your use case is too simple for cloning.
It might help you DRY your code when lots of method chaining has been performed, especially when applying filters to queries.

Doctrine WHERE IN Problems

Before I begin, I believe I have tried everything from this previous post: How to use WHERE IN with Doctrine 2
So I have a Silex application connected to a MySQL DB using Doctrine via composer (doctrine/dbal 2.2.*)
The query builder I am trying to run is this:
$qb = $this->db->createQueryBuilder();
$stmt = $qb->select('DAY(datefield1) x, COUNT(*) value')
->from('table1', 's')
->join('s', 'table2', 't', 't.key=s.key')
->where('MONTH(datefield1) = :billMonth')
->andWhere('YEAR(datefield1) = :billYear')
->andWhere('t.key IN (:keylist)')
->groupBy('x')
->orderBy('x', 'asc')
->setParameter(':billMonth', $month)
->setParameter(':billYear', $year)
->setParameter(':keylist', implode(",", $keylist))
->execute();
return $stmt->fetchAll(\PDO::FETCH_ASSOC);
The parameters are (month=8)(year=2014)(keylist=array(1,2,3,4))
The query does not fail but it curiously doesn't contain all the data that it should.
I have tried ->setParameter(':keylist', $keylist) to use the raw array, and this didn't work.
I have tried this kind of syntax too:
$qb->add('where', $qb->expr()->in('r.winner', array('?1')));
However that threw up an error because the in method wasn't available in expression builder class.
Please will someone cast an eye over this and save me from having to hardcode my SQL?
OK seeing as this old thread has seen some action since I last looked I wanted to confirm that the issue is long resolved - a third parameter in setParameter allows you to inform Doctrine how to handle the array:
$qb = $this->db->createQueryBuilder();
$stmt = $qb
->select('*')
->from(self::DB_TABLE, 'x')
->where('x.service IN (:services)')
->orderBy('x.session_end', 'DESC')
->addOrderBy('x.direction', 'DESC')
->setParameter(':services', $services, \Doctrine\DBAL\Connection::PARAM_STR_ARRAY)
->setFirstResult($offset)
->setMaxResults($count)
->execute();
$result = $stmt->fetchAll(\PDO::FETCH_ASSOC);
DB placeholders/parameters are for single values. You're passing in a monolithic string 1,2,3,4 due to calling implode() on the array. Given:
WHERE t.key IN (:keylist)
then this query will be executed as the equivalent of
WHERE t.key IN ('1,2,3,4')
^-------^---note the quotes
Since it's a string, and only one single string in the IN clause, it's the functional equivalent of
WHERE t.key = '1,2,3,4'
and not the
WHERE (t.key = 1 OR t.key = 2 OR ....)
you want it to be. Either set up multiple parameters, one for each value in your array, or embed your string in the query directly
->andWhere('t.key IN (' . implode(',', $keylist) . ')')
which of course opens you up to sql injection attack vulnerabilities.
If you build the sql query yourself you can use DBAL's PARAM_INT_ARRAY type:
use Doctrine\DBAL\Connection as DB;
$db->executeQuery('SELECT DAY(datefield1) x, COUNT(*) value
FROM table1 s
JOIN table2 t ON t.key=s.key
WHERE MONTH(datefield1) = :billMonth
AND YEAR(datefield1) = :billYear
AND t.key IN (:keylist)
GROUP BY x
ORDER BY x ASC',
array(':billMonth' => $month, ':billYear' => $year, ':keylist' => $keylist),
array(':billMonth' => \PDO::PARAM_INT, ':billYear' => \PDO::PARAM_INT, ':keylist' => DB::PARAM_INT_ARRAY
)->fetchAll(\PDO::FETCH_ASSOC);
Correct way to handle such IN clause is as simple as it can be:
$qb = $this->db->createQueryBuilder();
$stmt = $qb->(...)
->andWhere('t.key IN (:keylist)')
->setParameter(':keylist', $keylist)
->getQuery()
->getArrayResult();
I have used this dozen times and it works - doctrine is smart enough to handle your $keylist being array.
Other thing is that I don't know why you're using fetchAll method which is reduntant here - execute() is just enough. Maybe this is a root of your problem.
My suggestion: try to fire action in dev mode (app_dev.php) and check your app/logs/dev.log - you will find all sql queries performed. Verify that database returns data which you are expecting from Doctrine.
->add('where', $qb->expr()->andX(
$qb->expr()->in('r.winner', ':winner')
))
->setParameters(array(
"winner" => $winners,
"billMonth", $month,
// add all params here
));
You should use the in expression via the querybuilder like below, and change how you set the parameter:
->andWhere($qb->expr()->in('t.key', ':keylist'))
The complete code:
$qb = $this->db->createQueryBuilder();
$stmt = $qb->select('DAY(datefield1) x, COUNT(*) value')
->from('table1', 's')
->join('s', 'table2', 't', 't.key=s.key')
->where('MONTH(datefield1) = :billMonth')
->andWhere('YEAR(datefield1) = :billYear')
->andWhere('t.key')
->andWhere($qb->expr()->in('t.key', ':keylist'))
->groupBy('x')
->orderBy('x', 'asc')
->setParameter(':billMonth', $month)
->setParameter(':billYear', $year)
->setParameter(':keylist', $keylist)
->execute();
return $stmt->fetchAll(\PDO::FETCH_ASSOC);

Symfony 2 Doctrine COUNT

I have in my table "Artiste" one column "valideAdmin" who takes value 1 or 0.
I try to make a simple count to return the number of entries in my table where "valideAdmin" is to 1:
$repo = $this ->getDoctrine()
->getManager()
->getRepository('ProjectMainBundle:Artiste');
$qb = $repo->createQueryBuilder('valideAdmin');
$qb->select('COUNT(valideAdmin)');
$qb->where('valideAdmin=1');
$count = $qb->getQuery()->getSingleScalarResult();
return array(
'count' => $count
);
But it always "1" who's return...
Without where clause, I have the total count of the entries of the table, but valideAdmin can be 0 or 1. I only want the count number where valideAdmin=1
Thanks for help
createQueryBuilder()'s first parameter is the alias that you want your entity to take (ie.: a short name to be used to refer to your entity in the query).
What you need to do is set a proper alias for your entity (for example a for Artiste) and then COUNT() the instances of your entity where the property (not the column) valideAdmin is set to one:
$repo = $this ->getDoctrine()
->getManager()
->getRepository('ProjectMainBundle:Artiste');
$qb = $repo->createQueryBuilder('a');
$qb->select('COUNT(a)');
$qb->where('a.valideAdmin = :valideAdmin');
$qb->setParameter('valideAdmin', 1);
$count = $qb->getQuery()->getSingleScalarResult();
Remember that DQL runs queries on entities. The DQL your write is then translated into SQL to query the underlying data source after.
Also you can fetch all date then use of COUNT function in PHP
This method has an advantage.You do not have to use a complex query.
You have all the information with count columns
$repositoryArtiste = $this->getDoctrine()
->getRepository('ProjectMainBundle:Artiste');
$queryArtiste= $repositoryArtiste->createQueryBuilder('a')
->Where('a.valideAdmin = :valideAdmin')
->setParameter('valideAdmin',1)
->getQuery();
$Artiste = $queryArtiste->getResult();
var_dump(count($Artiste));

Categories