lsvagusa's avatar

Contract/Interface for Eloquent models

Hello.

I have created a number of different tables that when having an entry created in them all need to go through a queue job.

So they all need some kind of 'status' field (and others that are very application specific so won't mention).

Then when being processed by the job the 'status' field would get updated. Of course this is something the job shouldn't care about, so I thought well this is where a Contract/Interface would come in handy.

Is this a concept? Basically a Contract/Interface for Eloquent models.

Or am I just not seeing a simpler solution?

1 like
9 replies
Glukinho's avatar

I don't see how interface concept fits here. An Eloquent model doesn't have its own contract, it follows an underlying table columns. Also PHP interfaces define methods, not properties, while Eloquent models operates with both methods and properties.

Maybe you want some sort of migrations contracts so tables all have desired set of columns?

2 likes
lsvagusa's avatar

Yes, could you point to any resources on that. I think that's exactly what I am looking for.

1 like
Glukinho's avatar

Sorry nothing comes to mind at the moment. Migrations are living structures, they evolve in time, they can squash into SQL file, so I can't even imagine where such contract may be applied.

What are you trying to achieve exactly? IDE autocompletion or just certainty about your models?

1 like
lsvagusa's avatar

Just certainty about models.

1 like
Glukinho's avatar

Look here: https://www.php.net/manual/en/language.oop5.interfaces.php

Properties

As of PHP 8.4.0, interfaces may also declare properties.

I didn't know that; maybe this is your solution? Apply an interface HasStatus to desired models, the interface should declare status property and status() method. There wil be some struggle about types though.

I can't say I like this solution but it's doable.

2 likes
Glukinho's avatar

I thought this is wrong solution as you have to have actual realization of methods and properties declared in an interface. Thinking this is not what you want as soon as they are hidden inside Laravel logic...

2 likes
lsvagusa's avatar

Hmm, looks like it. For the time being I'll just let it stay as is, the amount of tables this would affect is rather small. But it is a good brain teaser.

1 like
martinbean's avatar

Then when being processed by the job the 'status' field would get updated. Of course this is something the job shouldn't care about, so I thought well this is where a Contract/Interface would come in handy.

Is this a concept? Basically a Contract/Interface for Eloquent models.

@lsvagusa Yes. That’s the very definition of depend on interfaces, not implementations (the “L” in “SOLID”).

You would type-hint the interface in your job’s constructor, and then your job would call methods defined by that interface without knowing the actual class implementing that interface.

interface HasStatus
{
    public function updateStatus(string $newStatus);
}
class UpdateStatusJob implements ShouldQueue
{
    use Queueable;

    public HasStatus $model;

    public function __construct(HasStatus $model)
    {
        $this->model = $model;
    }

    public function handle(): void
    {
        $this->model->updateStatus('complete');
    }
}

Your job now doesn’t care if it’s working with an Eloquent model, or if it’s working with an Eloquent model at all. Just so long as the class it receives implements the HasStatus interface.

2 likes
ghabriel25's avatar

I often use interface on eloquent models when it has polymorphic relationship. For example,

interface ToggleableActivity
{
    public function activity(): MorphOne;
}
class Bookmark extends Model implements ToggleableActivity
{
    public function activity(): MorphOne
    {
        return $this->morphOne(UserActivity::class, 'subject');
    }
}

class PostLike extends Model implements ToggleableActivity
{
    public function activity(): MorphOne
    {
        return $this->morphOne(UserActivity::class, 'subject');
    }
}

and other such thing which I can use inside my service class

final class ActivityService
{
    public function toggleActivity(ToggleableActivity $model): void
    {
        UserActivity::withoutEvents(fn () => $model->activity()
            ->withTrashed()
            ->updateOrCreate(
                [
                    'user_id' => $model->user_id,
                ],
                [
                    'type' => $model->getActivityType(),
                    'reward' => $this->pointsToMaxLevel($model),
                    'deleted_at' => null,
                    'created_at' => now(),
                    'updated_at' => now(),
                ]
            ));
    }
}

This is to help IDE to determine model relationship without type-hinted the real model class

Please or to participate in this conversation.