Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel default orderBy

Tags:

php

laravel

Is there a clean way to enable certain models to be ordered by a property by default? It could work by extending the laravel's QueryBuilder, but to do so, you'll have to rewire some of it's core features - bad practice.

reason

The main point of doing this is - one of my models get's heavily reused by many others and right now you have to resort the order over and over again. Even when using a closure for this - you still have to call it. It would be much better to be able to apply a default sorting, so everyone who uses this model, and does not provide custom sorting options, will receive records sorted by the default option. Using a repository is not an option here, because it get's eager loaded.

SOLUTION

Extending the base model:

protected $orderBy;
protected $orderDirection = 'ASC';

public function scopeOrdered($query)
{
    if ($this->orderBy)
    {
        return $query->orderBy($this->orderBy, $this->orderDirection);
    }

    return $query;
}

public function scopeGetOrdered($query)
{
    return $this->scopeOrdered($query)->get();
}

In your model:

protected $orderBy = 'property';
protected $orderDirection = 'DESC';

// ordering eager loaded relation
public function anotherModel()
{
    return $this->belongsToMany('SomeModel', 'some_table')->ordered();
}

In your controller:

MyModel::with('anotherModel')->getOrdered();
// or
MyModel::with('anotherModel')->ordered()->first();
like image 520
M K Avatar asked Dec 20 '13 10:12

M K


People also ask

What is default order by in laravel?

By default, Laravel Eloquent models return queries that are ordered by the id column of the model table. With this package, you can set default order by in your Eloquent model so you don't have to call the orderBy Eloquent builder.

What is orderBy in laravel?

The Laravel Orderby works by simply sorting the results of the query. So if the column has a list of 20 data, it can sort the list by the parameter provided. One can also create an order in an ascending or a Descending Order. By Ascending Order: $users = DB::table('users')->orderBy('name', 'asc')->get();


3 Answers

Before Laravel 5.2

Nowadays we can solve this problem also with global scopes, introduced in Laravel 4.2 (correct me if I'm wrong). We can define a scope class like this:

<?php namespace App;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ScopeInterface;

class OrderScope implements ScopeInterface {

    private $column;

    private $direction;

    public function __construct($column, $direction = 'asc')
    {
        $this->column = $column;
        $this->direction = $direction;
    }

    public function apply(Builder $builder, Model $model)
    {
        $builder->orderBy($this->column, $this->direction);

        // optional macro to undo the global scope
        $builder->macro('unordered', function (Builder $builder) {
            $this->remove($builder, $builder->getModel());
            return $builder;
        });
    }

    public function remove(Builder $builder, Model $model)
    {
        $query = $builder->getQuery();
        $query->orders = collect($query->orders)->reject(function ($order) {
            return $order['column'] == $this->column && $order['direction'] == $this->direction;
        })->values()->all();
        if (count($query->orders) == 0) {
            $query->orders = null;
        }
    }
}

Then, in your model, you can add the scope in the boot() method:

protected static function boot() {
    parent::boot();
    static::addGlobalScope(new OrderScope('date', 'desc'));
}

Now the model is ordered by default. Note that if you define the order also manually in the query: MyModel::orderBy('some_column'), then it will only add it as a secondary ordering (used when values of the first ordering are the same), and it will not override. To make it possible to use another ordering manually, I added an (optional) macro (see above), and then you can do: MyModel::unordered()->orderBy('some_column')->get().

Laravel 5.2 and up

Laravel 5.2 introduced a much cleaner way to work with global scopes. Now, the only thing we have to write is the following:

<?php namespace App;

use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;

class OrderScope implements Scope
{

    private $column;

    private $direction;

    public function __construct($column, $direction = 'asc')
    {
        $this->column = $column;
        $this->direction = $direction;
    }

    public function apply(Builder $builder, Model $model)
    {
        $builder->orderBy($this->column, $this->direction);
    }
}

Then, in your model, you can add the scope in the boot() method:

protected static function boot() {
    parent::boot();
    static::addGlobalScope(new OrderScope('date', 'desc'));
}

To remove the global scope, simply use:

MyModel::withoutGlobalScope(OrderScope::class)->get();

Solution without extra scope class

If you don't like to have a whole class for the scope, you can (since Laravel 5.2) also define the global scope inline, in your model's boot() method:

protected static function boot() {
    parent::boot();
    static::addGlobalScope('order', function (Builder $builder) {
        $builder->orderBy('date', 'desc');
    });
}

You can remove this global scope using this:

MyModel::withoutGlobalScope('order')->get();
like image 67
Jeroen Noten Avatar answered Oct 16 '22 09:10

Jeroen Noten


In Laravel 5.7, you can now simply use addGlobalScope inside the model's boot function:

use Illuminate\Database\Eloquent\Builder;

protected static function boot()
{
    parent::boot();

    static::addGlobalScope('order', function (Builder $builder) {
        $builder->orderBy('created_at', 'desc');
    });
}

In the above example, I order the model by created_at desc to get the most recent records first. You can change that to fit your needs.

like image 24
Jonathan Roy Avatar answered Oct 16 '22 07:10

Jonathan Roy


Another way of doing this could be by overriding the newQuery method in your model class. This only works if you never, ever want results to be ordered by another field (since adding another ->orderBy() later won't remove this default one). So this is probably not what you'd normally want to do, but if you have a requirement to always sort a certain way, then this will work:

protected $orderBy;
protected $orderDirection = 'asc';

/**
 * Get a new query builder for the model's table.
 *
 * @param bool $ordered
 * @return \Illuminate\Database\Eloquent\Builder
 */
public function newQuery($ordered = true)
{
    $query = parent::newQuery();    

    if (empty($ordered)) {
        return $query;
    }    

    return $query->orderBy($this->orderBy, $this->orderDirection);
}
like image 22
Joshua Jabbour Avatar answered Oct 16 '22 09:10

Joshua Jabbour