Search code examples
phplaraveleloquent-relationship

Laravel 12 relation MorphTo with unknown type value


I'm trying to use Polymorphic Relationships from this docs https://laravel.com/docs/12.x/eloquent-relationships#polymorphic-relationships

I have model Balance

class Balance
{
    public $subject;
    public $subject_id;
    
    public function subjectable(): MorphTo
    {
        return $this->morphTo(__FUNCTION__, 'subject', 'subject_id');
    }

}

Also I have simple mapping in AppServiceProvider

        Relation::morphMap([
            'shop' => 'App\Models\Shop',
            'merchant' => 'App\Models\Merchant',
        ]);

So if the field subject contains 'shop' or 'merchant' - it works perfect.

But database is external resource, it's not strict like the code, and there might be some strings, that is not in my models and my app structure. And I can't control database and change it.

If subject contains string 'abc' , I'm getting an error: Class "abc" not found.

So it's impossible to use ->morphTo() in this case.

Is there a way to limit some "allowed list" of subjects (types), and return null or empty collection, if this subject not in my list?

UPD: I can't make check $this->subject inside relation function, because it's null.

    public function subjectable() {
        var_dump($this->subject);
        if (in_array($this->subject, ['shop', 'merchant'])) {
            return $this->morphTo(__FUNCTION__, 'subject', 'subject_id');
        }
        return null;
    }

I'm trying:

$result = $query->with(['subjectable'])->get(); // $this->subject is null
// or
$result = $query->get(); 
$result->load(['subjectable']); // $this->subject is null

Solution

  • You can create a dummy model that will be used for all types of data for which you do not have models, let's call it DummyModel.

    You will need to extend Laravel's MorphTo model with your own version of the createModelByType method:

    public function createModelByType($type)
    {
        $class = Arr::get(Relation::morphMap() ?: [], $type, DummyClass::class);
    
        return tap(new $class, function ($instance) {
            if (! $instance->getConnectionName()) {
                $instance->setConnection($this->getConnection()->getName());
            }
        });
    }
    

    (The Laravel original version of this method has $class = Model::getActualClassNameForMorph($type);, and there is no easy way to replace the static method on Model.)

    This will return your DummyClass any time the morph map does not have a matching class.