Laravel 8 Paginate Collection (sortBy) - laravel

I try to paginate a sorted collection in Laravel 8, maybee any one have an idea?
That's my code:
$where = Business::where('name', 'LIKE', '%' . $what . '%');
$businesses = $where->get()->sortByDesc(function($business) {
return $business->totalReviews;
})->paginate(10); // <--- not working.. Collection::paginate not exists

Paginate can only be called on a builder instance (it makes no sense to call it on a collection as you already have all the data). But you are doing some logic based on the review count that requires a model method which must can only be called after fetching the data.
So you must refactor the ordering so that it gets called on the builder instance so that the ordering happens on SQL before the pagination logic happens.
withCount('relation') is perfect for this as it will append on a count of a specific relation onto your query which you can then sort by on SQL.
For example you can try this where reviews is a relation on the Business model that you have many of (likely either belongsToMany or hasMany):
Business::withCount('reviews')
->where('name', 'LIKE', '%' . $what . '%')
->orderBy('reviews_count', 'desc')
->paginate(10);
Where inside your Business model you have:
public function reviews()
{
return $this->hasMany(Review::class);
}

Remove the get:
$businesses = $where->sortByDesc(function($business) {
return $business->totalReviews;
})->paginate(10);

I fixed it on this way
$businesses = $where->get()->sortByDesc(function($business) {
return $business->getTotalReviews();
});
$businesses = ViewHelper::paginate($businesses, 10);
ViewHelper.class
<?php
namespace App\Classes;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Pagination\Paginator;
use Illuminate\Database\Eloquent\Collection;
class ViewHelper
{
/**
* Gera a paginação dos itens de um array ou collection.
*
* #param array|Collection $items
* #param int $perPage
* #param int $page
* #param array $options
*
* #return LengthAwarePaginator
*/
public static function paginate($items, $perPage = 15, $page = null, $options = [])
{
$page = $page ?: (Paginator::resolveCurrentPage() ?: 1);
$items = $items instanceof Collection ? $items : Collection::make($items);
return new LengthAwarePaginator($items->forPage($page, $perPage), $items->count(), $perPage, $page, $options);
}
}

Related

Laravel: pass an argument to the relationship function (not the query callback function) when using whereHas

I would like to know how to pass an argument to a model relationship function. Just to be clear, I'm NOT talking about the query callback.
Consider a model like so:
class UserRelationships extends Model
{
// other stuff
// dynamic scope:
/**
* Scope a query to only include users of a given type.
*
* #param \Illuminate\Database\Eloquent\Builder $query
* #param mixed $type
* #return \Illuminate\Database\Eloquent\Builder
*/
// $relationships = UserRelationships::at( Carbon::parse('2022-10-10') )->get();
public function scopeAt($query, Carbon $date)
{
return $query->where('superseded_at', '>', $date )
->where('created_at', '<=', $date );
}
}
And a related model featuring the following relationships:
class User extends Authenticatable
{
public function progenial_relation(Carbon $date=null) // returns this user record in the userRelationships table, which can be used to retrieve this users parent (directly lookup the sponsor_id)
// when eager loading, this is useful for getting all users whose parent is x, hence the name
{
return $this->hasOne(UserRelationships::class, 'user_id', 'id')
->at( #$date ?: Carbon::now() ) // local dynamic scope in userRelationships
->orderByDesc('created_at')
->limit(1);
}
public function parental_relation(Carbon $date=null) // returns records from the userRelationships table, of all the users which refer to this user as their sponsor
// when eager loading, this is useful for getting the user whose child is x, hence the name
{
return $this->hasMany(UserRelationships::class, 'sponsor_id', 'id')
->at( #$date ?: Carbon::now() ); // local dynamic scope in userRelationships
}
}
As you can see my relationships accept an argument (the date).
Now, if you wanted to use those relationships straightforwardly like so, there's no issues:
$date = Carbon\Carbon::parse('2022-06-01');
$relations_at_date = User::find(1)->parental_relation( $date )->get();
But what happens if you need to use eager-loading methods such as has(), whereHas(), doesntHave(), whereDoesntHave()?
How do you pass an argument to the relationship? For example, I wanted to add other relationships to my User model.
public function children(Carbon $date=null)
{
$date = #$date ?: Carbon::now();
return self::whereHas('progenial_relation', function($q) {
$q->where('sponsor_id', $this->id);
}, $date); // not working
}
I tried with these syntax, but it doesn't work:
whereHas( 'relationship_name', $callback, $argument )
whereHas( 'relationship_name', $argument, $callback )
whereHas( 'relationship_name', [$argument], $callback )
whereHas( 'relationship_name', $callback, [$argument] )
Is it somehow possible?
Are there any alternatives?
For completeness I'm going to add what happens if I use a normal closure:
public function children(Carbon $date=null)
{
$date = #$date ?: Carbon::now();
return self::whereHas('progenial_relation', function($q) use ($date) {
$q->at($date)->where('sponsor_id', $this->id);
});
}
This is the resulting SQL. As you can see the constraints are applied twice. Once by the query callback and once by the relationship. But since I cannot pass the correct argument to the relationship, it gets the default one. The 2 constraints collide and the query does not work.
"select * from `users`
where exists (
select *
from `user_relationships`
where `users`.`id` = `user_relationships`.`user_id`
and `user_relationships`.`superseded_at` > ?
and `user_relationships`.`created_at` <= ?
and `sponsor_id` = ?
and `user_relationships`.`superseded_at` > ?
and `user_relationships`.`created_at` <= ?
)
and `users`.`deleted_at` is null"
I don't think that its possible to pass variables to relationship methods when eager-loading like this.
But you can apply a sub-query to the wherehas:
$date = #$date ?: Carbon::now();
return self::whereHas('progenial_relation', function($q) use ($date) {
$q
->where('sponsor_id', $this->id)
->at( #$date ?: Carbon::now() );
}, $date);
Although I'm not sure what the ->at method/scope you added does.

Laravel pagination with problems

i have a simple question as i do not know which line i should add into to get laravel pagination. (->paginate(5).
public function index(Request $request)
{
$codeSearch = $request->get('code');
$descriptionSearch = $request->get('description');
//$tmp = Category::all()->toArray();
$tmp = Category::where('code','like','%' .$codeSearch. '%')->where('description','like','%' .$codeSearch. '%')->get()->toArray();
$category = array();
foreach ($tmp as $key => $row) {
$policy = Category::find($row['parent_id']);
$tmpResult = new Category();
$tmpResult->id = $row['id'];
$tmpResult->code = $row['code'];
$tmpResult->description = $row['description'];
$tmpResult->parent_id = $policy['description'];
$tmpResult->status = $row['status'];
array_push($category, $tmpResult);
}
return view('category.index', compact('category'));
}
Paginate can't be called on the collection, so you have to run it on the query, by simply replace ->get() with ->paginate(5) like this
$tmp = Category::where('code','like','%' .$codeSearch. '%')
->where('description','like','%' .$codeSearch. '%')
->paginate(5)
->toArray();
Try
$tmp = Category::where('code','like','%' .$codeSearch. '%')->where('description','like','%' .$codeSearch. '%')->paginate(5);
In view
#foreach($tmp as $tm)
//whatever operation you like to do
#endforeach
{{$tmp->links()}}
The following ->get(), ->first(), all() get the results from the database and so does ->paginate(5), so I would suggest that you replace ->get() with paginate(5) an you can get rid of toArray() because the result will be a collection with which you can use foreach() or get a value by index.
As you need an array in your condition, then you can simply convert the final array into a collection object and use the pagination concept.
$items = [
'item1',
'item2',
'item3',
'item4',
'item5',
'item6',
'item7',
'item8',
'item9',
'item10'
];
// Get current page form url e.x. &page=1
$currentPage = LengthAwarePaginator::resolveCurrentPage();
// Create a new Laravel collection from the array data
$itemCollection = collect($items);
// Define how many items we want to be visible in each page
$perPage = 1;
// Slice the collection to get the items to display in current page
$currentPageItems = $itemCollection->slice(($currentPage * $perPage) - $perPage, $perPage)->all();
// Create our paginator and pass it to the view
$paginatedItems= new LengthAwarePaginator($currentPageItems , count($itemCollection), $perPage);
// set url path for generted links
$paginatedItems->setPath($request->url());
return view('items_view', ['items' => $paginatedItems]);
It would probably be easier to have a relationship setup for a Category's parent. Then you can load the parent when you retrieve the categories.
class Category extends Model
{
...
public function parent()
{
return $this->belongsTo(self::class);
}
}
I feel like your search is probably trying to do an OR WHERE, search the code field for this value or the description:
$categories = Category::with('parent')
->where('code', 'like', '%' .$codeSearch. '%')
->orWhere('description', 'like', '%' .$codeSearch. '%')
->paginate(5);
Then in your view if you want the parent category's description you can get it through the relationship:
#foreach ($categories as $category)
parent description: {{ $category->parent->description }}
#endforeach

Laravel Database Query Builder error with table name

I'm making a "simple" api for laravel. This api has to handle with filters, pagination and sorting the result. To make this I use laravel query builder. The problem is that it's making a select without a table name, for example:
select * order by `id` asc
My code:
public function index()
{
$request = request();
$query = DB::table('customers')->newQuery();
// Orden
if (request()->has('sort')) {
// Multiorden
$sorts = explode(',', request()->sort);
foreach ($sorts as $sort) {
list($sortCol, $sortDir) = explode('|', $sort);
$query = $query->orderBy($sortCol, $sortDir);
}
} else {
$query = $query->orderBy('id', 'asc');
}
//Filtros
if ($request->exists('filter')) {
$query->where(function($q) use($request) {
$value = "%{$request->filter}%";
$q->where('name', 'like', $value)
->orWhere('address', 'like', $value);
});
}
$perPage = request()->has('per_page') ? (int) request()->per_page : null;
$pagination = $query->get()->paginate($perPage);
$pagination->appends([
'sort' => request()->sort,
'filter' => request()->filter,
'per_page' => request()->per_page
]);
return response()->json(
$pagination
);
}
Error:
Illuminate\Database\QueryException: SQLSTATE[HY000]: General error:
1096 No tables used (SQL: select * order by id asc) in file
C:\xampp\htdocs\iService\vendor\laravel\framework\src\Illuminate\Database\Connection.php
on line 664
UPDATE:
return DB::table('customers')->get();
If i use this, the api works fine, I have more apis working. The problem is that I need Query Builder to handle filters, sort, etc...
The problem was the way I instance a new query.
$query = DB::table('customers')->newQuery();
Correct:
$query = Model::query();
For my example:
$query = Customer::query();

Laravel Eloquent get() indexed by primary key id

I often find it very useful to index my results by the primary key id.
Example:
$out = [];
$users = User::where('created_at', '>=', '2015-01-01')->get();
foreach ($users as $user) {
$out[$user->id] = $user;
}
return $out;
Is there anyway to do this in one shot with Eloquent? It's not useful to use the 0...n index.
You can accomplish this by using getDictionary() on your collection.
Like so:
$users = User::where('created_at', '>=', '2015-01-01')->get()->getDictionary();
Note: in newer version of Laravel (5.2+), getDictionary() was removed; keyBy() can be used instead:
$users = User::where('created_at', '>=', '2015-01-01')->get()->keyBy('id');
I created my own solution by having a super Model that extends Eloquent.
Full solution:
https://gist.github.com/yadakhov/741173ae893c1042973b
/**
* Where In Hashed by primary key
*
* #param array $ids
* #return array
*/
public static function whereInHash(array $ids, $column = 'primaryKey')
{
$modelName = get_called_class();
$primaryKey = static::getPrimaryKey();
if ($column === 'primaryKey') {
$column = $primaryKey;
}
$rows = $modelName::whereIn($column, $ids)->get();
$out = [];
foreach ($rows as $row) {
$out[$row->$primaryKey] = $row;
}
return $out;
}
Not with eloquent but this is potentially nicer option than looping through all the results.
$users = Users::all();
return array_combine($users->modelKeys(), $users);
You can use keyBy()
$users = User::where('created_at', '>=', '2015-01-01')->get()->keyBy('id')->toArray();

Eloquent eager load Order by

I have problem with eloquent query. I am using eager loading (one to one Relationship) to get 'student' With the 'exam', Using the code below.
Student::with('exam')->orderBy('exam.result', 'DESC')->get()
And i want to order received rows by the 'result' column in 'exam'. I am using
->orderBy('exam.result', 'DESC')
But it is not working. Any ideas how to do it ?
Try this:
Student::with(array('exam' => function($query) {
$query->orderBy('result', 'DESC');
}))
->get();
If you need to order your students collection by the result column, you will need to join the tables.
Student::with('exam')
->join('exam', 'students.id', '=', 'exam.student_id')
->orderBy('exam.result', 'DESC')
->get()
In this case, assuming you have a column student_id and your exams table are named exam.
If you ALWAYS want it sorted by exam result, you can add the sortBy call directly in the relationship function on the model.
public function exam() {
return this->hasMany(Exam::class)->orderBy('result');
}
(credit for this answer goes to pfriendly - he answered it here: How to sort an Eloquent subquery)
tl;dr
Student::with('exam')->get()->sortByDesc('exam.result');
This will sort the results of the query after eager loading using collection methods and not by a MySQL ORDER BY.
Explanation
When you eager load you can't use an ORDER BY on the loaded relations because those will be requested and assembled as a result of a second query. As you can see it in the Laravel documentation eager loading happens in 2 query.
If you want to use MySQL's ORDER BY you have to join the related tables.
As a workaround, you can run your query and sort the resulting collection with sortBy, sortByDesc or even sort. This solution has advantages and disadvantages over the join solution:
Advantages:
You keep Eloquent functionality.
Shorter and more intuitive code.
Disadvantages:
Sorting will be done by PHP instead of the database engine.
You can sort only by a single column, unless you provide a custom closure for the sorter functions.
If you need only a part of the ordered results of a query (e.g. ORDER BY with LIMIT), you have to fetch everything, order it, then filter the ordered result, otherwise you will end up with only the filtered part being ordered (ordering will not consider the filtered out elements). So this solution is only acceptable when you would work on the whole data set anyway or the overhead is not a problem.
This worked for me:
$query = Student::select(['id','name']);
$query->has('exam')->with(['exam' => function ($query) {
return $query->orderBy('result','ASC');
}]);
return $query->get();
You could use \Illuminate\Database\Eloquent\Relations\Relation and query scopes to add far column through relationship, I wrote a traits for this, it misses HasOne o HasMany but having BelongsTo and BelongsToMany could easily adapted
Also the method could be enhanced to support more than depth 1 for multiple chained relationship, I made room for that
<?php
/**
* User: matteo.orefice
* Date: 16/05/2017
* Time: 10:54
*/
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Facades\DB;
use Illuminate\Database\Eloquent\Builder;
trait WithFarColumnsTrait
{
public function scopeWithFarColumns(Builder $query , $relationPath , $columns , $tableAliasPrefix = null)
{
$relationPath = array_wrap($relationPath);
$tableAliasPrefix = $tableAliasPrefix ?: WithFarColumnsTrait::randomStringAlpha(3);
$currentModel = $this;
$subQueries = [];
$relationIndex = 0;
foreach ($relationPath as $relationName) {
if (method_exists($currentModel , $relationName)) {
$relation = $currentModel->$relationName();
} else {
throw new BadMethodCallException("Relationship $relationName does not exist, cannot join.");
}
$currentTable = $currentModel->getTable();
if ($relationIndex == 0) {
$query->addSelect($currentTable . '.*');
}
$relatedModel = $relation->getRelated();
/**
* #var string
*/
$relatedTable = $relatedModel->getTable();
if ($relation instanceof BelongsTo) {
foreach ($columns as $alias => $column) {
$tableAlias = $tableAliasPrefix . $relationIndex;
$tableAndAlias = $relatedTable . ' AS ' . $tableAlias;
/**
* Al momento gestisce soltanto la prima relazione
* todo: navigare le far relationships e creare delle join composte
*/
if (!isset($subQueries[$alias])) {
$subQueries[$alias] = $currentQuery = DB::query()
->from($tableAndAlias)
->whereColumn(
$relation->getQualifiedForeignKey() , // 'child-table.fk-column'
'=' ,
$tableAlias . '.' . $relation->getOwnerKey() // 'parent-table.id-column'
)
->select($tableAlias . '.' . $column);
// se la colonna ha una chiave stringa e' un alias
/**
* todo: in caso di relazioni multiple aggiungere solo per la piu lontana
*/
if (is_string($alias)) {
$query->selectSub($currentQuery , $alias);
} else {
throw new \InvalidArgumentException('Columns must be an associative array');
}
}
else {
throw new \Exception('Multiple relation chain not implemented yet');
}
} // end foreach <COLUMNs>
} // endif
else if ($relation instanceof BelongsToMany) {
foreach ($columns as $alias => $column) {
$tableAlias = $tableAliasPrefix . $relationIndex;
$tableAndAlias = $relatedTable . ' AS ' . $tableAlias;
if (!isset($subQueries[$alias])) {
$pivotTable = $relation->getTable();
$subQueries[$alias] = $currentQuery = DB::query()
->from($tableAndAlias)
->select($tableAlias . '.' . $column)
// final table vs pivot table
->join(
$pivotTable , // tabelle pivot
$relation->getQualifiedRelatedKeyName() , // pivot.fk_related_id
'=' ,
$tableAlias . '.' . $relatedModel->getKeyName() // related_with_alias.id
)
->whereColumn(
$relation->getQualifiedForeignKeyName() ,
'=' ,
$relation->getParent()->getQualifiedKeyName()
);
if (is_string($alias)) {
$query->selectSub($currentQuery , $alias);
} else {
throw new \InvalidArgumentException('Columns must be an associative array');
}
}
else {
throw new \Exception('Multiple relation chain not implemented yet');
}
} // end foreach <COLUMNs>
} else {
throw new \InvalidArgumentException(
sprintf("Relation $relationName of type %s is not supported" , get_class($relation))
);
}
$currentModel = $relatedModel;
$relationIndex++;
} // end foreach <RELATIONs>
}
/**
* #param $length
* #return string
*/
public static function randomStringAlpha($length) {
$pool = array_merge(range('a', 'z'),range('A', 'Z'));
$key = '';
for($i=0; $i < $length; $i++) {
$key .= $pool[mt_rand(0, count($pool) - 1)];
}
return $key;
}
}
There is an alternative way of achieving the result you want to have without using joins. You can do the following to sort the students based on their exam's result. (Laravel 5.1):
$students = Student::with('exam')->get();
$students = $students->sortByDesc(function ($student, $key)
{
return $student->exam->result;
});

Resources