58

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();
M K
  • 9,138
  • 7
  • 43
  • 44

8 Answers8

66

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();
Community
  • 1
  • 1
Jeroen Noten
  • 3,574
  • 1
  • 17
  • 25
  • Note that as of Laravel 5.2, you should use `Scope` instead of `ScopeInterface`. – A.J. Brown Jul 09 '16 at 22:24
  • Thanks! Indeed. Actually, Laravel 5.2 offers a more clean way to handle global scopes. I updated my answer to include this. Especially, the inline global scope definition is very useful for these simple things. – Jeroen Noten Jul 10 '16 at 23:02
  • @JeroenNoten : I am using "Solution without extra scope class". It works but there is a little issue, my sorting doesn't work on the created_at column after this.Is there a way to solve it. I think it always becomes created_at desc and the same data comes again and again. Other column sorting works fine. – Mukesh Joshi Feb 26 '18 at 07:45
30

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.

HosseyNJF
  • 491
  • 1
  • 6
  • 18
Jonathan Roy
  • 461
  • 5
  • 4
  • This was a pretty hard thing to stumble across in the docs. Great answer. Thanks for sharing. – JamesNZ Feb 13 '19 at 02:59
  • @jonathan-roy how can I use this at one central place so that no need to put this in individual model?? – Palak Jadav Aug 12 '19 at 06:46
  • Do not forget to add namespace ```use Illuminate\Database\Eloquent\Builder;``` – Rudolph Jan 16 '20 at 03:16
  • @PalakJadav you could create a `trait` or you can create an abstract model and extend it. – FabianoLothor Jul 06 '20 at 19:26
  • 1
    I have created a model. Inherited a different model from her. Called the all method. And it doesn't sort, why? Please provide an example of use. I use laravel 8. – doox911 Dec 07 '20 at 10:59
22

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);
}
Joshua Jabbour
  • 592
  • 4
  • 12
  • 1
    @RoboRobok unfortunately, this implementation could cause a lot of errors. For example, running a count() on this model will not work if every query has an orderBy statement. The correct answer to this question is that there is no good solution to having a default ordering because not all queries can be ordered. – TonyArra Feb 05 '15 at 14:39
  • @TonyArra invalid statement it will work, I use `orderBy` in my counts, if I want to get the top 3 entries, I want to order by the field then take the first 3. – Bradley Weston May 07 '15 at 20:18
  • This should be the accepted answer just because it answers the actual question (doing this by default). It is important to understand the consequences of doing this, but it should be left up to the developer to decide if it's the best thing to do. This achieves what was asked, the accepted answer is the "best alternative". As an example, I want this for a lookup/fact table that has no practical use when sorted in any other way than alphabetically. The advantage of forcing the order for such a table is that no one needs to remember to sort it to guarantee the order. – A.J. Brown Jul 09 '16 at 22:19
  • hi @Joshua Jabbour, thanks for your answer, it solves my problem. But i have a doubt why do we need to pass **$ordered = true** parameter. I dont understand on that part – Vijay Wilson May 16 '17 at 06:46
  • @VijayWilson That parameter is so that you can create queries that are not ordered, when you need to do that (or when you need to order manually by another field). Because this is overriding the central `newQuery` method, *every* query goes through here. This isn't ideal, but there's no other way to do it. However, sometimes you don't want a query to be ordered, so the only way to turn it off is with this parameter. – Joshua Jabbour May 17 '17 at 20:17
  • @JoshuaJabbour : Thanks for your simple solution. consider adding tablename before orderBy to Prevent ambiguous column error on joins. – Shahrokhian Jul 02 '17 at 15:35
17

Yes you would need to extend Eloquent to always do this as standard for any query. What's wrong with adding an order by statement to the query when you need it ordered? That is the cleanest way, ie, you dont need to 'unhack' Eloquent to get results by natural order.

MyModel::orderBy('created_at', 'asc')->get();

Other than that the closest thing to what you want would be to create query scopes in your models.

public function scopeOrdered($query)
{
    return $query->orderBy('created_at', 'asc')->get();
}

You can then call ordered as a method instead of get to retrieve your ordered results.

$data = MyModel::where('foo', '=', 'bar')->ordered();

If you wanted this across different models you could create a base class and just extend it to the models you want to have access to this scoped method.

David Barker
  • 14,484
  • 3
  • 48
  • 77
  • 21
    this is not by default – M K Dec 20 '13 at 10:18
  • Read the answer... I recommend you don't do it by default. – David Barker Dec 20 '13 at 10:19
  • I wrote this in my question "Even when using a closure for this - you still have to call it." – M K Dec 20 '13 at 10:33
  • 1
    Yeah you do, I said in the first line... you would have to hack Eloquent for this... and as you writely said: bad practice. I've given you the closest solution to what you want... sorry can't give much more than that. – David Barker Dec 20 '13 at 10:35
  • That last part of your answer is a satisfying solution! Thanks :) – M K Dec 20 '13 at 10:36
  • 1
    It doesn't answer the question. Having records ordered by default is something really useful sometimes, especially when you have your results ordered by `weight` column or similar. Without `orderBy()` you can't expect any particular order, which is fine for some models, but sometimes having them **always** in order would be great. I'd love it if Laravel had some special method for default scope and another one to quit it if needed. That would work with `where` as well, when we have some records that are usually not meant to be accessible by users. Kinda like soft deleting, but more flexible. – Robo Robok Jan 14 '15 at 10:49
  • @RoboRobok You might already know this by now, but Laravel 4.2 exactly provides the feature you describe. See my answer. – Jeroen Noten Jun 05 '15 at 08:59
  • The accepted answer should be changed. The answer by Jeroen Noten does what was asked without "hacking the framework" – A.J. Brown Jul 09 '16 at 22:26
  • This answer does not "hack the framework" as you put it. I do however agree with you that the other answer is a better answer to this question. – David Barker Jul 09 '16 at 22:28
16

you should use eloquent global scope that can apply to all queries(also you can set parameter for it).

And for relations you can use this useful trick:

class Category extends Model {
    public function posts(){
        return $this->hasMany('App\Models\Post')->orderBy('title');
    }
}

this will add order by to all posts when we get them from a category. If you add an order by to your query, this default order by will cancel!

10

An slightly improved answer given by Joshua Jabbour

you can use the code he offered in a Trait, and then add that trait to the models where you want them to be ordered.

<?php

namespace App\Traits;

trait AppOrdered {
    protected $orderBy = 'created_at';
    protected $orderDirection = 'desc';

    public function newQuery($ordered = true)
    {
        $query = parent::newQuery();

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

        return $query->orderBy($this->orderBy, $this->orderDirection);
    }
}

then in whichever model you want the data to be ordered you can use use :

class PostsModel extends Model {

    use AppOrdered;
    ....

now everytime you request that model, data will be ordered, that's somehow more organized, but my answers is Jabbour's answer.

T. Gungordu
  • 119
  • 1
  • 5
Emad Ha
  • 1,203
  • 15
  • 33
3

I built a mini Laravel package that can add default orderBy in your Eloquent model.

Using the DefaultOrderBy trait of this package, you can set the default column you want to orderBy.

use Stephenjude/DefaultModelSorting/Traits/DefaultOrderBy;

class Article extends Model
{
    use DefaultOrderBy;

    protected static $orderByColumn = 'title';
}

You can also set the default orderBy direction by setting the $orderByColumnDirection property.

protected static $orderByColumnDirection = 'desc';
2

A note from my experience, never to use orderBy and GroupBy such term on global scope. Otherwise you will easily face database errors while fetching related models in other places.

Error may be something like:

"ORDER BY "created_at" is ambiguous"

In such case the solution can be giving table name before column names in your query scope.

"ORDER BY posts.created_at"

Thanks.

Hasnat Babur
  • 322
  • 5
  • 14