Home > Net >  Sorting of paginator object through a model relation column
Sorting of paginator object through a model relation column

Time:09-22

I have three tables: products, product_inventories and product_inventory_details. The ORM of each model is shown below,

Product Model

class Product extends Model{
    ...

    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        ...,
        'title', 
        'selected_inventory_id', 
        ...
    ];

    /**
     * Get the inventories those belongs to this model.
     */
    public function inventory(){
        return $this->hasMany('App\Models\ProductInventory');
    }

    /**
     * Get the selected product_inventory_detail that owns this model.
     */
    public function selected(){
        return $this->hasOne('App\Models\ProductInventoryDetail', 'id', 'selected_inventory_id');
    }
    
    ...
}

ProductInventory Model

class ProductInventory extends Model{
    ...
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_id', 
        ...
    ];


    /**
     * Get the inventory_details those belongs to this model.
     */
    public function items(){
        return $this->hasMany('App\Models\ProductInventoryDetail');
    }

    ...
}

ProductInventoryDetail Model

class ProductInventoryDetail extends Model{
    ...
    /**
     * The attributes that are mass assignable.
     *
     * @var array
     */
    protected $fillable = [
        'product_inventory_id',  
        'price', 
        ...
    ];
}

I'm sorting and limiting the results through user input of Sort by dropdown and Show per page dropdown. When sorting by Alphabetical: High to Low option I'm running the query builder method to order the results:

$products = $products->orderBy($sort['column'], $sort['order'])->paginate($limit);

Now with sorting by Price, I can't run the query builder method orderBy() since I'm not using joins and getting the data through relationship properties. Instead I'm using the Laravel's collection method to sort it out:

$products = $products->paginate($limit);

$products = $products->sortBy(function($prod, $key){
    return $prod->selected->price;
});

The above block is working fine if I don't use pagination methods. But, I need to use the pagination as well since the user can also limit the results per page. I'm also using a Paginator object's method to append some parameters to each page URL:

$products->appends($paramsArr);

Since running the sortBy() method returns a collection instead of Paginator object, it's giving me undefined method exception.

My Question

How can I sort the result set by price in my current scenario without having to implement the joins? Is there a way to achieve that??

CodePudding user response:

I would use QueryBuilder package of Spatie. It will make your life easier for creating sortable and filterable grid table. You use that package this way:

$query = Product::with(['inventory', 'selected']);
$products = \Spatie\QueryBuilder\QueryBuilder::for($query)
    ->allowedFilters([
          'name' => 'name', // name column in the products DB table.
          'selected.price' => 'product_inventory_details.column_price', // price column in the product_inventory_details DB table.
     ])
     ->defaultSort('name')
     ->allowedSorts([
          'name',
          \Spatie\QueryBuilder\AllowedSort::custom('selected.price', new SortSelectedPrice())
     ])
     ->paginate(20)
     ->appends(request()->query());

External custom sort class looks like this:

 class SortSelectedPrice implements \Spatie\QueryBuilder\Sorts\Sort
 {
      public function __invoke(Builder $query, bool $descending, string $property)
      {
           $direction = $descending ? 'DESC' : 'ASC';
           $query->leftJoin('product_inventory_details', 'products.id', '=', 'product_inventory_details.product_id');
           $query->orderBy('product_inventory_details.column_price', direction);
      }
  }

Make sure your URL containing the query string like this for sorting name from A to Z and sorting price from 1xxxxxxxx to 0:

domain.com/products?sort=name,-selected.price

I installed the Spatie package using composer. Don't forget to do that.

CodePudding user response:

I found a way to handle it without having to implement the joins. I added a new variable and stored the Paginator object's items() method result set into it.

$products = $products->paginate($limit);

$productItems = $products->items(); // returns the items array from paginator

And sort that particular variable instead of sorting the whole paginator object. That way my links and URLs are untouched and unmodified in the $products variable and the data of the products are in a separate variable.

if($sort['column'] == 'price'){
    if($sort['order'] == 'DESC'){
        $productItems = $products->sortByDesc(function($prod, $key){
            return $prod->selected->price;
        });
    } else{
        $productItems = $products->sortBy(function($prod, $key){
            return $prod->selected->price;
        });
    }
}

I also had to change my rendering variable from $products to $productItems and accessed the pagination links from the old $products variable.

@forelse ($productItems as $product)
    @include('site.components.product-grid')
@empty
    <div class="col text-center">...</div>
@endforelse

...

{{ $products->links() }}

I'm posting it here for the community to benefit/discuss/criticize if there is a better way.

  • Related