Order pagination result in Symfony 2 - php

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.

Related

How can I filter a negative value from a query made with eloquent and then paginate it?

I was trying to get the remaining time for movie delivery in a rental system made with laravel.
I built the following query using eloquent and it returned the result I wanted:
$resting_time = DB::table('alquiler')
->join('socio','alquiler.soc_id','=','socio.id')
->join('pelicula','alquiler.pel_id','=','pelicula.id')
->select('socio.soc_nombre','pelicula.pel_nombre','alquiler.created_at', DB::raw("DATEDIFF(alq_fecha_hasta,NOW()) AS Days"))
->orderBy('Days','asc')
->paginate(6);
but there is a problem when these rentals go over the delivery deadline it returns negative values, so I would like the query to return only the rentals that have the remaining days greater than zero and then paginate those results.
I create this statement and using map() filter only the positives that are returned in a collection but the problem is that I can't paginate them.
$resting_time = DB::table('alquiler')
->join('socio','alquiler.soc_id','=','socio.id')
->join('pelicula','alquiler.pel_id','=','pelicula.id')
->select('socio.soc_nombre','pelicula.pel_nombre','alquiler.created_at', DB::raw("DATEDIFF(alq_fecha_hasta,NOW()) AS Days"))
->get()->map(function($alquiler){
return ($alquiler->Days >= 0) ? $alquiler : null;
});
$resting_time = $resting_time->filter()->sortBy('Days');
This is the returning collection:
But this type of collection cannot be paginated.
Any idea how to fix it, or maybe an easier way to do it? Sorry if something doesn't make sense, I'm just starting in laravel.
In second case its not working,because you work with:
\Illuminate\Support\Collection::class
in first case, you work with :
\Illuminate\Database\Eloquent\Collection::class
To make it work , you can try to do next thing:
take a
\Illuminate\Support\Collection::class
and return it paginated via
Illuminate\Pagination\Paginator::class
so the end result will look like this:
$resting_time = DB::table('alquiler')
->join('socio','alquiler.soc_id','=','socio.id')
->join('pelicula','alquiler.pel_id','=','pelicula.id')
->select('socio.soc_nombre','pelicula.pel_nombre','alquiler.created_at', DB::raw("DATEDIFF(alq_fecha_hasta,NOW()) AS Days"))
->get()->map(function($alquiler){
return ($alquiler->Days >= 0) ? $alquiler : null;
});
$resting_time = $resting_time->filter()->sortBy('Days');
return new Illuminate\Pagination\Paginator($resting_time, 6);
However, i would recommend to prepare data from SQL side, neither doing all of the manipulations from collection perspective.
Most of the answers already provided will work, but will return a collection instead of a paginated resource. The trick is to use the tap helper method before map'ping, to return the same object you modified.
return tap(Alquiler::select(['socio.soc_nombre','pelicula.pel_nombre','alquiler.created_at', DB::raw("DATEDIFF(alq_fecha_hasta,NOW()) AS Days")])
->with('socio', 'pelicula')
->paginate(20))
->map(function ($model) {
return ($model->Days >= 0) ? $model : null;
});
or you can do this way too:
return Alquiler::select(['socio.soc_nombre','pelicula.pel_nombre','alquiler.created_at', DB::raw("DATEDIFF(alq_fecha_hasta,NOW()) AS Days")])
->with('socio', 'pelicula')
->paginate(20))
->map(function ($model) {
if($alquiler->Days >= 0) {
return $model;
}
});
I tried both methods and it didn't work at least the way I wanted, so I did a little more research and put this together:
public function getRestingTime(){
$resting_time = Alquiler::select(['socio.soc_nombre','pelicula.pel_nombre','alquiler.created_at', DB::raw("DATEDIFF(alq_fecha_hasta,NOW()) AS Days")])
->whereRaw('DATEDIFF(alq_fecha_hasta,NOW()) >= ?', [0])
->join('socio','alquiler.soc_id','=','socio.id')
->join('pelicula','alquiler.pel_id','=','pelicula.id')
->orderBy('Days','asc')->paginate(6);
return $resting_time;
}
I hope it helps someone, thanks likewise to the people who responded cleared my mind a bit and gave me new things to try.

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.

Laravel eloquent, how to order custom sortby

I have the following code that works fine:
$products = Product::like($search)->whereIn('id', $request->input('product_ids'))->skip($offset)->take($limit)->get(array('products.*'))->sortBy(function($product) use ($sort_order) {
$number = (isset($sort_order[$product->id])) ? $sort_order[$product->id] : 0;
return $number;
});
This returns the items in ascending order, how do I specify whether I want sortby to return the products in ascending or descending order?
//$order contains either 'asc' or 'desc'
$products = Product::like($search)->whereIn('id', $request->input('product_ids'))->skip($offset)->take($limit)->get(array('products.*'))->sortBy(function($product) use ($sort_order, $direction) {
$number = (isset($sort_order[$product->id])) ? $sort_order[$product->id] : 0;
return ($direction == 'asc') ? $number : -$number;
});
I really don’t understand the query. There’s a lot going on there that really shouldn’t be. For example:
You’re not selecting data from any other table, so why are you specifying all rows from the products table in the get() method (->get(array('products.*')))?
Why are you applying the ordering function to the returned collection instead of just applying the order clause to the query?
With the above, you query could be simplified to something like:
$productIds = [1, 2, 3, 4];
$direction = 'asc'; // or desc
$products = Product::like($search)
->whereIn('id', $productIds)
->skip($offset)
->take($limit)
->orderBy('id', $direction)
->get();
Also, you don’t need to manually specify the offset and limit if you use the paginate() helper method.
Just use sortBy for ASC (how you used it now) and sortByDesc for DESC.

Eloquent ORM: count() remove the select(...)

I am using Eloquent ORM outside of Laravel-4 and I am building a custom Paginator.
First, I build a query using Fluent Query Builder. I want to get the number of result the query could return using count() and then I do a custom pagination using take(x) and skip(y). I need to do the count() before the take()->skip()->get() so I dont fall outside of the page range. The problem is that when I use the count() method on the query, it seems to remove any select I added previously.
I isolated the problem to this simple example:
$query = DB::table('companies')
->join('countries','companies.country_id','=','countries.id')
->select(
'companies.name as company_name',
'countries.name as country_name'
);
$nbPages = $query->count();
$results = $query->get();
//$results contains all fields of both tables 'companies' and 'countries'
If i invert the order of the count and get, it works fine:
$results = $query->get();
$nbPages = $query->count();
//$results contains only 'company_name' and 'country_name'
Question: is there a more elegant way the using something like this:
$tmp = clone $query;
$nbPages = $tmp->count();
$results = $query->get();
There is not, unfortunately. Open issue on github about the problem: https://github.com/laravel/framework/pull/3416

Adding a Having Clause to Doctrine Statement

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).

Categories