chimit's avatar

API Resource and polymorphic relationships

I have a Comment resource which morphs to different resources like Post and Picture.

// App\Comment.php

public function resource()
{
    return $this->morphTo('resource');
}

So I need to dynamically load a needed API Resource class based on a related model type. How can I do it?

This, of course, doesn't work for cases when a ​resource is `Pictu:

// App\Http\Resources\MessageResource.php

public function toArray($request)
{
    return [
        'id' => $this->id,
        'user_id' => $this->user_id,
        'text' => $this->text,
        'created_at' => $this->created_at,
        'updated_at' => $this->updated_at,
        'resource' => new PostResource($this->whenLoaded('resource')),
    ];
}
0 likes
8 replies
devfrey's avatar

You can determine the type of the morph using instanceof. Something like this:

public function toArray($request)
{
    return [
        // ...
        'resource' => $this->when($this->relationLoaded('resource'), function () {
            if ($this->resource->resource instanceof App\Post) {
                return new PostResource($this->resource->resource);
            }

            if ($this->resource->resource instanceof App\Picture) {
                return new PictureResource($this->resource->resource);
            }
        }),
    ];
}

I haven't tested this, and I agree it looks a bit awkward with the $this->resource->resource, but I think it's the only way it would work. Normally 'this' (the resource) magically catches property access and forwards it to the underlying resource (the model). But since $resource is used for the resource object, which happens to also be the name of your relationship, you need to be explicit.

3 likes
chimit's avatar

@devfrey here is a working version of your idea:

public function toArray($request)
{
    return [
        // ...
        'resource' => $this->when($this->relationLoaded('resource'), function () {
            switch (true) {
                case $this->resource->resource instanceof \App\Post:
                    return new PostResource($this->resource->resource);
                    break;
                    
                case $this->resource->resource instanceof \App\Picture:
                    return new PictureResource($this->resource->resource);
                    break;
            }
        })
    ];
}

I had to use $this->resource->resource twice 😀 Seems it's the only way to go currently. Thanks!

1 like
chimit's avatar

@Ganesh Adhikari I don't clearly remember, but most likely it didn't work the other way around. I guess one is the name of my relationship, another is a Laravel convention.

devfrey's avatar

@CHIMIT - Funny, I started off with the exact same switch(true) construction. Anyway, if you return from a switch, it never reaches break. So you can safely remove the two lines with break;.

I've updated my code.

1 like
cameronwilby's avatar

I'm using morph maps so had to adapt this some, figured I'd wrap it in a trait:

<?php

namespace App\Traits;

use Illuminate\Database\Eloquent\Relations\Relation;

trait WhenMorphToLoaded
{
    public function whenMorphToLoaded($name, $map)
    {
        return $this->whenLoaded($name, function () use ($name, $map) {
            $morphType = $name . '_type';
            $morphAlias = $this->resource->$morphType;
            $morphClass = Relation::getMorphedModel($morphAlias);
            $morphResourceClass = $map[$morphClass];
            return new $morphResourceClass($this->resource->$name);
        });
    }
}

Usage

public function toArray() {
    return [
        'resource' => $this->whenMorphToLoaded('resource', [
            Post::class => PostResource::class,
            Picture::class => PictureResource::class
        ]);
    ];
}

11 likes
joaofranciscoguarda's avatar

@cameronwilby I know that the thread is a bit outdated, but what this function would look like in a many to many polymorphic relationship? It would be a map for the functions that calls the other model? Because there would be no 'something_type" in the actual model, only in the pivot.

I'm new to PHP / Laravel and I would like to understand and see from someone with more experience how to approach this problem, and because I'm in this pickle right now ahahah

cameronwilby's avatar

That is tricky! I didn't consider something_type was impossible to match without getting the items.

This version gets the items, and maps them to resources based on get_class($item). I'm thinking it should be the same code for morphMany, morphToMany, and morphedByMany, but test it first.

Please or to participate in this conversation.