190

I am looping over all comments posted by the Author of a particular post.

foreach($post->user->comments as $comment)
{
    echo "<li>" . $comment->title . " (" . $comment->post->id . ")</li>";
}

This gives me

I love this post (3)
This is a comment (5)
This is the second Comment (3)

How would I order by the post_id so that the above list is ordered as 3,3,5

miken32
  • 42,008
  • 16
  • 111
  • 154
PrestonDocks
  • 4,851
  • 9
  • 47
  • 82

5 Answers5

373

It is possible to extend the relation with query functions:

<?php
public function comments()
{
    return $this->hasMany('Comment')->orderBy('column');
}

[edit after comment]

<?php
class User
{
    public function comments()
    {
        return $this->hasMany('Comment');
    }
}

class Controller
{
    public function index()
    {
        $column = Input::get('orderBy', 'defaultColumn');
        $comments = User::find(1)->comments()->orderBy($column)->get();

        // use $comments in the template
    }
}

default User model + simple Controller example; when getting the list of comments, just apply the orderBy() based on Input::get(). (be sure to do some input-checking ;) )

Rob Gordijn
  • 6,381
  • 1
  • 22
  • 29
  • 1
    Thanks Rob, you put me on the right track. The actual answer was $comments = User::find(10)->comments()->orderBy('post_id')->get(); It seemed to need the get() method in order to work. If you can add get() to your answer I will mark it as the accepted answer. – PrestonDocks Aug 09 '13 at 13:35
  • 3
    This works well if you're retrieving a single record, but if you are retrieving multiple records you'll want something more along the lines of http://stackoverflow.com/a/26130907/1494454 – dangel Oct 03 '15 at 03:04
  • If you use mysql strict mode, which is default in Laravel 5.4 ,f.ex you'll receive SQLSTATE[42000]: Syntax error or access violation: 1140 Mixing of GROUP columns ... – Sabine Feb 18 '17 at 15:15
  • usefull guide for you https://www.w3schools.com/howto/howto_js_autocomplete.asp if does it help you please mark ... – Akbar Soft Aug 18 '18 at 09:56
39

In cases where you don't want the sort to be part of the relationship definition as in the accepted answer, you can also pass an array to the Eloquent::with() method. The key indicates the relationship name, and the value is a function that is passed the Builder object:

$sortDirection = 'desc';

$user->with([
    'comments' => fn ($query) => $query->orderBy('column', $sortDirection);
]);

That allows you to run arbitrary logic on each related comment record. You could have stuff in there like:

$query->where('timestamp', '<', $someTime)->orderBy('timestamp', $sortDirection);
miken32
  • 42,008
  • 16
  • 111
  • 154
agm1984
  • 15,500
  • 6
  • 89
  • 113
  • 7
    Nope. I have tried this, it doesn't work. Here's my case : I have two table (`appointments` and `schedules`), the query is simple : get `appointments` order by `schedules`.`datetime` descending. I have solution by adding new column in table `appointments` to store `datetime` from table `schedules`. And now I only need to order by `appointments`.`datetime` I know it's not best method, but it solve the problem. XD – ibnɘꟻ Jan 28 '20 at 10:37
  • 1
    That would only sort the subquery, not the parent query. – J. A. Streich Oct 21 '22 at 17:58
  • The same applies for `load()` method. – VeRJiL Jun 11 '23 at 07:49
22

Using sortBy... could help.

$users = User::all()->with('rated')->get()->sortByDesc('rated.rating');

Yunnosch
  • 26,130
  • 9
  • 42
  • 54
Harry Bosh
  • 3,611
  • 2
  • 36
  • 34
  • 18
    Note that this sorts the collection, and not in the query. You achieve the same result, but sorting is done in PHP and not SQL, which may have performance implications. – Qirel Apr 17 '21 at 17:43
  • Get this error........ SQLSTATE[42S22]: Column not found: 1054 Unknown column 'omsOptionDetails.sort' in 'order clause' – Siraj Ali Aug 25 '22 at 11:20
12

Try this solution.

$mainModelData = mainModel::where('column', $value)
    ->join('relationModal', 'main_table_name.relation_table_column', '=', 'relation_table.id')
    ->orderBy('relation_table.title', 'ASC')
    ->with(['relationModal' => function ($q) {
        $q->where('column', 'value');
    }])->get();

Example:

$user = User::where('city', 'kullu')
    ->join('salaries', 'users.id', '=', 'salaries.user_id')
    ->orderBy('salaries.amount', 'ASC')
    ->with(['salaries' => function ($q) {
        $q->where('amount', '>', '500000');
    }])->get();

You can change the column name in join() as per your database structure.

Mr.Singh
  • 1,421
  • 6
  • 21
  • 46
PHP Worm...
  • 4,109
  • 1
  • 25
  • 48
1

I made a trait to order on a relation field. I had this issues with webshop orders that have a status relation, and the status has a name field.

Example of the situation

Ordering on with "joins" of eloquent models is not possible since they are not joins. They are query's that are running after the first query is completed. So what i did is made a lil hack to read the eloquent relation data (like table, joining keys and additional wheres if included) and joined it on the main query. This only works with one to one relationships.

The first step is to create a trait and use it on a model. In that trait you have 2 functions. The first one:

/**
 * @param string $relation - The relation to create the query for
 * @param string|null $overwrite_table - In case if you want to overwrite the table (join as)
 * @return Builder
 */
public static function RelationToJoin(string $relation, $overwrite_table = false) {
    $instance = (new self());
    if(!method_exists($instance, $relation))
        throw new \Error('Method ' . $relation . ' does not exists on class ' . self::class);
    $relationData = $instance->{$relation}();
    if(gettype($relationData) !== 'object')
        throw new \Error('Method ' . $relation . ' is not a relation of class ' . self::class);
    if(!is_subclass_of(get_class($relationData), Relation::class))
        throw new \Error('Method ' . $relation . ' is not a relation of class ' . self::class);
    $related = $relationData->getRelated();
    $me = new self();
    $query = $relationData->getQuery()->getQuery();
    switch(get_class($relationData)) {
        case HasOne::class:
            $keys = [
                'foreign' => $relationData->getForeignKeyName(),
                'local' => $relationData->getLocalKeyName()
            ];
        break;
        case BelongsTo::class:
            $keys = [
                'foreign' => $relationData->getOwnerKeyName(),
                'local' => $relationData->getForeignKeyName()
            ];
        break;
        default:
            throw new \Error('Relation join only works with one to one relationships');
    }
    $checks = [];
    $other_table = ($overwrite_table ? $overwrite_table : $related->getTable());
    foreach($keys as $key) {
        array_push($checks, $key);
        array_push($checks, $related->getTable() . '.' . $key);
    }
    foreach($query->wheres as $key => $where)
        if(in_array($where['type'], ['Null', 'NotNull']) && in_array($where['column'], $checks))
            unset($query->wheres[$key]);
    $query = $query->whereRaw('`' . $other_table . '`.`' . $keys['foreign'] . '` = `' . $me->getTable() . '`.`' . $keys['local'] . '`');
    return (object) [
        'query' => $query,
        'table' => $related->getTable(),
        'wheres' => $query->wheres,
        'bindings' => $query->bindings
    ];
}

This is the "detection" function that reads the eloquent data.

The second one:

/**
 * @param Builder $builder
 * @param string $relation - The relation to join
 */
public function scopeJoinRelation(Builder $query, string $relation) {
    $join_query = self::RelationToJoin($relation, $relation);
    $query->join($join_query->table . ' AS ' . $relation, function(JoinClause $builder) use($join_query) {
        return $builder->mergeWheres($join_query->wheres, $join_query->bindings);
    });
    return $query;
}

This is the function that adds a scope to the model to use within query's. Now just use the trait on your model and you can use it like this:

Order::joinRelation('status')->select([
    'orders.*',
    'status.name AS status_name'
])->orderBy('status_name')->get();
Sebastiaan
  • 11
  • 2