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

Sturm's avatar
Level 5

Polymorphic Headache

TL;DR: I need to know how I can retrieve models, but have them be sorted in a specific way based on related models.

I've got four related tables, three of which are used in a polymorphic relationship. Here's a simplified version of them:

checkcalls
	id - integer
	loadid - integer
	type - string
	stopid - integer

pickups
	id - integer
	loadid - integer
	checkedin - boolean
	loaded - boolean
	pickuporder - integer

drops
	id - integer
	loadid - integer
	checkedin - boolean
	delivered - boolean
	droporder - integer

checkcallstatus
	id - integer
	status - string
	checkorder - integer

I've created a polymorphic relationship whereby the Checkcall is a child of either Pickup or Drop based on its type column. In addition, I've added a couple of accessors to unify the stops models further:

class Checkcall extends Model
{
    public function stop(): MorphTo
    {
        return $this->morphTo(__FUNCTION__, "type", "stopid");
    }
}

class Pickup extends Model
{
    protected $appends = ["completed", "order"];
    public function getCompletedAttribute()
    {
        return $this->attributes["loaded"];
    }
    public function getOrderAttribute()
    {
        return $this->attributes["pickuporder"];
    }
    public function checkcalls(): MorphMany
    {
        return $this->morphMany(Checkcall::class, "stop");
    }
}

class Drop extends Model
{
    protected $appends = ["completed", "order"];
    public function getCompletedAttribute()
    {
        return $this->attributes["delivered"];
    }
    public function getOrderAttribute()
    {
        return $this->attributes["droporder"];
    }
    public function checkcalls(): MorphMany
    {
        return $this->morphMany(Checkcall::class, "stop");
    }
}

And, of course, to make this polymorphic relationship work, I have to add a bit to App\Providers\AppServiceProvider.php:

    public function boot(): void
    {
        Relation::enforceMorphMap([
            'PC' => 'App\API\v2\Models\Pickup',
            'LD' => 'App\API\v2\Models\Pickup',
            'DC' => 'App\API\v2\Models\Drop',
            'DE' => 'App\API\v2\Models\Drop',
        ]);
    }

Those are four possible values for Checkcall's type column and the only ones we're concerned with here.

Now, I aim to retrieve a bunch of checkcalls for a given loadid, but they need to be in a specific order. There can be multiple pickups and multiple drops, but they should be in PC -> LD -> DC -> DE order, with all the pickup checkcalls first, then all the drop checkcalls second, and in order of the stops' pickuporder/droporder columns (which can be retrieved through their new order accessor).

I've got a start on it here, but the sortBy() just doesn't work as expected:

        $checkcalls = Checkcall::with("stop")
            ->with("checkcallStatus")
            ->where("loadid", $loadID)
//            ->orderBy("checkcallStatus.checkorder", "asc")
            ->get()
            ->sortBy([
                function ($checkCall) {
                    return 'App\API\v2\Models\Pickup' === Relation::getMorphedModel($checkCall->stop->getMorphClass()) ? 0 : 1;
                },
                ["stop.order", "asc"],
                ["checkcallStatus.checkorder", "asc"],
            ]);

The expected order of Checkcalls for a load that has 2 Pickups and 2 Drops should be as follows:

0 => Pickup #1, PC
1 => Pickup #1, LD
2 => Pickup #2, PC
3 => Pickup #2, LD
4 => Drop #1, DC
5 => Drop #1, DE
6 => Drop #2, DC
7 => Drop #2, DE

I do not know how to write an orderBy() or sortBy() in order to get this output. Can anyone please help me?

I apologize for the length and complexity of this request, but I did not know how else to explain the situation and illustrate the intended outcome. Please let me know if I need to clarify the situation further.

0 likes
1 reply
rodrigo.pedra's avatar

You can use PHP array_search to retrieve the morph class order:

$order = ['PC', 'LD', 'DC', 'DE'];

$checkcalls = Checkcall::query()
    ->with(['stop', 'checkcallStatus'])
    ->where('loadid', $loadID)
    ->get()
    ->sortBy([
        function ($one, $other) use ($order) {
            $one = array_search($one->stop->getMorphClass(), $order);
            $other = array_search($other->stop->getMorphClass(), $order);
        
            return $one <=> $other;
        },
        ['stop.order', 'asc'],
        ['checkcallStatus.checkorder', 'asc'],
    ]);

But note that when sorting by multiple attributes, a closure should return a comparison between two elements (-1 if first value is "less than" the second, 0 if values are equal, +1 otherwise).

This is what the PHP's spaceship operator is doing inside the closure.

references:

Please or to participate in this conversation.