NuOrder's avatar

How to sort Schools by eloquent courses promo ASC , price ASC and also all courses as promo_price ASC , price ASC

Hello,

Dear community people, this is my first post .. I was looking for answers here and other internet posts and founds some tips but no one works.

I will describe how SQL data works and what I need to do.

  1. Parent table is: "schools"

School model got method:

public function courses(): hasMany { return $this->hasMany(SchoolCourse::class); }

  1. Schools has many "courses" wich Is collect in table name "school_courses"

School_courses model got method:

public function tags(): belongsToMany { return $this->belongsToMany(Tag::class); }

public function calendar(): hasMany { return $this->hasMany(CourseCalendar::class) ->orderByRaw("DATE_FORMAT(startDate,'%Y-%m-%d') asc"); }

public function attributes(): hasOne { return $this->hasOne(CourseAttribute::class); }

in attributes we stores values like:

  • price ( price of course )
  • promo_price ( discounted course price)
  1. I need to retrieve all Schools "withWhereHas" courses with specific tags ID matched

a) I need to find only schools wich got courses as 'active' >= 1,

b) I need get all courses in every school sorted by promo_price ASC, (if promo_price not exist) by price ASC

c) I need also sorted Schools wich got lowest promo_price (if promo_price not exist) or price ASC

so mainly I do it like this:

$schools = School::query();

// $attr <-- is parsed POST data from search engine like this: $attr = request()->validate([....])

$schools->withWhereHas('courses', function ($course_query) use ($attr) { $course_query->where('active', '>=', 1) ->whereHas('tags', function ($query) use ($attr) { $query->whereIn('id', [$attr['attributes']['category'], $attr['attributes']['course_type']]); }, '=', 2)

// I try to use JON to make a chance to order courses by promo_price & price ASC

  $course_query->join('course_attributes', 'school_courses.id', '=', 'course_attributes.school_course_id')
    ->orderBy('promo_price')
    ->orderBy('price')
    ->with([' calendar', 'tags.category:id,name']);
});

When I make this join: $course_query->join('course_attributes', 'school_courses.id', '=', 'course_attributes.school_course_id')

Query result brake other eloquent methods: ->with([' calendar', 'tags.category:id,name']); In response there is no:

  • schools[0]->courses[0]->calendar
  • schools[0]->courses[0]->tags

but I see "price", "promo_price" and other parameters from "course_attributes" table

So my question is how can I retrieve all Schools wich got courses as "active" >=1 and this courses will be sorted by promo_price ASC , price ASC and then parents ( Schools) will be also sorted according to his courses lowest promo_price or price ?

I try also something strange (from internet):

$data = $schools->with([' calendar', 'tags.category:id,name', 'attributes' => fn($query) => { $query->orderBy('promo_price', 'ASC') ->orderBy('price', 'ASC') ])->get())->sortBy(function ($school, $key) { return $school->courses->min->attributes->price; })->values()

<-- but sorting by price ASC not works properly :(

0 likes
3 replies
LaryAI's avatar
Level 58

Here's a clean solution for the requirements you've described — order schools (parent) by their lowest course promo_price (or price if promo_price is null), and also sort courses inside each school by promo_price or price ascending.
This is a common scenario (sorting on a relationship field), but note:
Eloquent does NOT support relationship-based sorting on parents in pure SQL.
You have to either join or do sorting in PHP after retrieval (using collection methods).

Ideal Solution:
Let's cover both — the right way for eager loading, and the performance-efficient approach if you have a large dataset.


1. Getting schools with their courses, tagged & filtered

We'll use withWhereHas for the query, and eager load the attributes, calendar, and tags.
We’ll also do sorting of courses using a custom relationship.

In your School model:

public function sortedCourses()
{
    return $this->hasMany(SchoolCourse::class)
        ->where('active', '>=', 1)
        ->join('course_attributes', 'school_courses.id', '=', 'course_attributes.school_course_id')
        ->orderByRaw('
            CASE 
                WHEN course_attributes.promo_price IS NOT NULL THEN course_attributes.promo_price 
                ELSE course_attributes.price 
            END ASC
        ')
        ->with(['calendar', 'tags.category:id,name', 'attributes']);
}

(Be careful: .with() doesn't work after a join, so we only eager load directly related data.)


2. Query & filter all schools with courses matching tag(s)

$attr = request()->validate([
    // your validation rules here; assuming structure...
    'attributes.category' => 'required|int',
    'attributes.course_type' => 'required|int',
]);

$schools = School::whereHas('courses', function ($course_query) use ($attr) {
    $course_query->where('active', '>=', 1)
        ->whereHas('tags', function ($query) use ($attr) {
            $query->whereIn('id', [
                $attr['attributes']['category'], 
                $attr['attributes']['course_type']
            ]);
        }, '=', 2);
})
// Eager-load the sortedCourses relationship we defined
->with(['sortedCourses' => function($q) use ($attr) {
    // you could add more filters if needed here, for tags
    $q->with(['calendar', 'tags.category:id,name', 'attributes']);
}])
->get();

3. Sort schools by the lowest promo_price (or price) among their courses

You cannot sort schools by related table fields in SQL unless you’re willing to build a complex join/subquery. So, do it in PHP after retrieval:

$schools = $schools->sortBy(function($school) {
    // Extracts all the courses' promo/price and finds the min for each school
    $prices = $school->sortedCourses
        ->map(function($course) {
            return $course->attributes 
                ? ($course->attributes->promo_price ?? $course->attributes->price) 
                : null;
        })
        ->filter(fn($v) => $v !== null);

    // Get the minimum price or a large number if no valid prices
    return $prices->isNotEmpty() ? $prices->min() : PHP_INT_MAX;
})->values();

Full Example


Key Points

  • You cannot eager load with join and use with() on the same relationship because joins return flat rows.
  • To sort schools by a computed value on a related table, fetch all, then sort in PHP with collections.
  • To sort courses within each school, define a custom relationship using a join and orderByRaw, or sort them in the collection after fetching.

Performance Note

If the number of Schools is LARGE, you need a native SQL approach for efficient sorting (using join, groupBy, and min() subquery).
But for most apps, eager-loading + PHP sort as above is clean, maintainable, and works well.


Let me know if you need the pure SQL version for the schools as a single paginatable query!

NuOrder's avatar

Unfortunately this code:

$schools = School::whereHas('courses', function ($course_query) use ($attr) { $course_query->where('active', '>=', 1) ->whereHas('tags', function ($query) use ($attr) { $query->whereIn('id', [ $attr['attributes']['category'], $attr['attributes']['course_type'] ]); }, '=', 2); }) // Eager-load the sortedCourses relationship we defined ->with(['sortedCourses' => function($q) use ($attr) { // you could add more filters if needed here, for tags $q->with(['calendar', 'tags.category:id,name', 'attributes']); }]) ->get();

does not work properly because it's return sorted ASC "sortedCourses" relation but without relation: $q->with(['calendar', 'tags.category:id,name', 'attributes']);

I need retrieve "sortedCourses" with other course relation: ->with(['calendar', 'tags.category:id,name',

How can I do that ?

In my opinion it looks like I need to refactor whole logic and move from courses.attributes :

  • price
  • promo_price

to parent "courses" table then I can do it easy like:

$schools->with(['courses' => function($q) { $q->with('attributes', 'calendar', 'last_discount'); $q->orderBy('promo_price) ->orderBy('price') }])

Jsanwo64's avatar

The issue is not that promo_price and price are in course_attributes. The main problem is that once you join() course_attributes inside the eager-loaded courses query, Eloquent may stop hydrating SchoolCourse properly unless you explicitly select the base table columns.

That is why relations like calendar, tags, and attributes appear to break.

The fix is to keep the join for sorting, but add:

->select('school_courses.*')

and sort with:

->orderByRaw('COALESCE(course_attributes.promo_price, course_attributes.price) ASC')

Then by logic, the eager-loaded courses query should look more like this:

->with([
    'courses' => function ($q) use ($attr) {
        $q->select('school_courses.*')
            ->where('active', '>=', 1)
            ->whereHas('tags', function ($tagQuery) use ($attr) {
                $tagQuery->whereIn('tags.id', [
                    $attr['attributes']['category'],
                    $attr['attributes']['course_type'],
                ]);
            }, '=', 2)
            ->leftJoin('course_attributes', 'course_attributes.school_course_id', '=', 'school_courses.id')
            ->orderByRaw('COALESCE(course_attributes.promo_price, course_attributes.price) ASC')
            ->with(['attributes', 'calendar', 'tags.category:id,name']);
    }
])

If you also want to sort the parent schools by the cheapest matching course, that is better done with a subquery using MIN(COALESCE(...)), not by moving the price columns onto school_courses.

So I would not refactor the schema just to make sorting easier. The current structure can handle it; the query just needs to be adjusted.

Also, sorting parents in PHP collections works for small datasets, but for proper DB-level sorting and pagination, a subquery is the cleaner approach.

1 like

Please or to participate in this conversation.