Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

j-p's avatar
Level 1

"Dynamically" sort relationship query or its result

For a client I am working on a simple e-commerce application.

One relationship in there is as follows (all hasMany): shop.items.accessorygroups.accessories

If a shop is requested it directly queries all its items etc.

The items and accessory tables both hold a category column based on which my client wants to have the output of the items/accessories sorted in the view very specifically.

Now here's some of my issues:

1.) Since he is so specific I cannot simply order it alphabetically in the query itself.

2.) He changes his mind a lot so making changes to the order should be a breeze.

3.) New categories will be added in the future so sorting should be kept rather dynamic for as far that's even possible.

I've been breaking my head over this the past couple of days but so far I have no idea on how to even approach the issue. Any pointers, examples, clues are welcome! I realise the question is rather broad but I'm getting a slightly desperate here.

0 likes
18 replies
pmall's avatar

You can create custom collections and add any sort method you want. I don't know if it will be useful because I didn't fully understood the problem.

<?php namespace App\Collections;

use Illuminate\Support\Collection;

class ItemsCollection extends Collection{

  public function sortByWhatever()
  {
    return $this->sort(function($a, $b)
    {
      // Classic php sort logic here
    }
  }

  // Any number of sort method you want

}
<?php

use App\Collections\ItemsCollection;
use Illuminate\Database\Eloquent\Model;

class Item extends Model{

  public function newCollection(Array $models = [])
  {
    return new ItemsCollection($models);
  }

}

then :

$shop = Shop::findOrFail(1);
$items_sorted_by_whatever = $shop->items->sortByWhatever();
j-p's avatar
Level 1

Hi @JarekTkaczyk!

Thanks for your reply. I was away for some appointments today, hence the late reply.

I'll try to define my question(s) more clearly. For the sake of keeping it simple lets forget about the aforementioned accessorygroups and accesories for now.

When in the app a shop page is visited, a collection is fetched and sent to the view. That collection contains the shop and its items; i.e. a shop has many items.

Here's a visual example of what I think the structure would look like. Of course there's many more items and some of them have the same category.

ShopA
    name: Someshop
    address: SomeAddress
    [items]:
        [item1]:
            name: Item1
            price: 1000
            category: Category1
        [item2]:
            name: Item2
            price: 1200
            category: Category2
        [item3]:
            name: Item3
            price: 1200
            category: Category3

My client is very specific about how to order the items on the page based on their category. So I can't just order it with the query itself ('asc' or 'desc').

So question 1: How can I order the items in the collection based on their category to how my client wants the order to be?

He also changes his mind about the order a lot and lets me a add/remove categories often.

So question 2: How can should I even approach the design of this so I can build such a functionality as "dynamic" as possible so I can change the order with a minimum of changes to the code.

I hope this is more clear?

With kind regards.

JarekTkaczyk's avatar

@ilmarinen There must be some pattern and rules of the order, you must describe it for me to tell you anything, even if it is subject to change.

j-p's avatar
Level 1

Well one week the pattern could be as easy as [Category1, Category2, Category3, Category4, Category5].

The other week (with categories removed) it could be [Category5, Category3, Category4].

Then a day later it could be (with categories removed and added) [Category6, Category1, Category3, Category7, Category2].

That's my problem here... It changes so much that I need some single place where to define the order of the categories so I can just quickly change it to what he wants it to be.

He might and probably will at some point even request that each store will have their own ordering of the categories.

jekinney's avatar

I add a column to the tables with an integer type named order (or sort_order) and set up a edit sort order view to allow the client to set the sort order as they wish dynamically.

Not sure if that answered your question or not. I'm on my phone but can add a code view latter if you need it

JarekTkaczyk's avatar
Level 53

@ilmarinen Then definitely add order column.

Next, in order to sort by related table, you need to join it. There's no way to do this using Eloquent relations:

$shop->load(['items' => function ($q) {
  $q->join('categories', 'categories.id', '=', 'items.category_id')
       ->select('items.*')
       ->orderBy('categories.order');
}, 'items.items.accessorygroups.accessories'])
1 like
j-p's avatar
Level 1

Thanks @JarekTkaczyk and @jekinney!

@JarekTkaczyk: If I understand your codesnippet correctly the categories column needs to be extracted from the items table to its own table with a relationship to the items table?

jekinney's avatar

This is my typical migrations for a e-commerce site (small one)

Schema::create('categories', function(Blueprint $table)
        {
            $table->increments('id');
            $table->string('name', 60)->unique();
            $table->string('description');
            $table->boolean('show_description')->default(0) // default do not show
            $table->integer('order')->default(1) // Default for testing and seeding, need logic to ensure the order is set to next number
            $table->timestamps();
        });

Schema::create('products', function(Blueprint $table) // Items, or what ever you want to call the table
        {
            $table->increments('id');
            $table->integer('category_id'); // if you want many categories to many products, use a pivot table and hasManyThrough in each model
            $table->string('name')->unique();
            $table->string('over_view'); // Small description
            $table->text('full_description'); // full blown description
            $table->text('specifications'); // Optional, and can continue adding as required
            $table->integer('order')->default(1) // Default for testing and seeding, need logic to ensure the order is set to next number
            $table->dateTime('sell_at'); // So you can pre-load products before the sell date
            $table->timestamps();
        });

Schema::create('prices', function(Blueprint $table)
        {
            $table->increments('id');
            $table->integer('product_id');
            $table->decimal('base_price', 5, 2); // This will allow prices up to 99999.99 (5 numbers left of decimal, 2 after)
            $table->decimal('sale_price', 5, 2); // set a sale price if or when an item goes on sale
            $table->dateTime('sale_start');
            $table->dateTime('sale_end');
            $table->timestamps();
        });

Schema::create('product_information', function(Blueprint $table)
        {
            $table->increments('id');
            $table->integer('product_id');
            $table->decimal('cost', 5, 2); // cost per unit
            $table->integer('in_stock'); // running tally of amount in stock 
            $table->integer('low_stock_alert')->default(0); // set unit stock amount to trigger notification to reorder, default 0 so null space isn't used if not required
            $table->boolean('stock_alert')->default(0); // set alerts on or off, default off
            $table->timestamps();
        });

this is a quick copy and past. Well I have more tables holding reports and so on to show margins, stock cost, profitability, units sold per month etc.

But this is the basic set up I start with and scale it as required. I use a separate pricing table because many times it runs on a pivot table with many to many as re-ordering a unit might cost more and when the less expensive stock runs out the client will automatically be able to raise the price when the more expensive unit is up for sale (if that makes sense).

1 like
JarekTkaczyk's avatar

@ilmarinen well, is it not the case? I didn't suspect that category is just a field on the items table :)

If that's your setup, then there's nothing special, just add order field to the items table and sort by that field:

$shop->load(['items' => function ($q) {
       ->orderBy('order');
}, 'items.items.accessorygroups.accessories'])
j-p's avatar
Level 1

@JarekTkaczyk at the moment the category is just a column on the items table.

Your answer did really help me though. Since there will be multiple shops with all their own items the list of items will get quite long. Maybe not such a good idea to do the ordering there but actually leave that up to a separate categories table.

I'll give that a go. Thanks so much!

j-p's avatar
Level 1

@jekinney thank you for your example too! When I first saw your answer it didn't display (no code was shown) so I thought that you accidentally made an empty post. I only saw your response just now.

This would indeed be my new direction at the moment combined with @JarekTkaczyk's suggestions.

Just one question to you. I see that you're using decimals for your prices. Isn't it easier to just store the prices in cents? Or am I thinking to easy here and could that cause any troubles in the future somehow?

jekinney's avatar

@JarekTkaczyk That depends, I know never really a definitive answer, cents would be great too and use an integer.

j-p's avatar
Level 1

I'll accept @JarekTkaczyk's reponse as answer since that was more focused on ordering the result. Nevertheless many thanks to you too @jekinney!

1 like
j-p's avatar
Level 1

@JarekTkaczyk I've been trying to fix it the way you suggested and I am getting good results. However I don't know while using the load method ($shop->load()) how to define which store should be loaded? How can I provide it with a store id?

JarekTkaczyk's avatar

@ilmarinen load() is used to load reltionship on already fetched model, if you don't have it already, then use with() method with exactly the same code:

$shop = Shop::with( ... )->find($id);
j-p's avatar
Level 1

Thanks @JarekTkaczyk! Wasn't able to reply earlier since I've been travelling but I've got it completely working now!

Please or to participate in this conversation.