Elie Faës
Elie Faës

Reputation: 3315

Retrieve parent class within morph relationship

I have this code

//ImageableTrait
trait ImageableTrait
{
    public function images()
    {
        return $this->morphMany(Image::class, 'imageable')
            ->orderBy('order', 'ASC');
    }
}

//User
class User extend Model
{
    use ImageableTrait;
}

//Post
class Post extend Model
{
    use ImageableTrait;
}

class ImageCollection extends Collection
{
    public function firstOrDefault()
    {
        if ($this->count() === 0) {
            $image = new Image();
            $image->id = 'default';
            $image->imageable_type = '/* I need the parent className here */';
            $image->imageable_id = '.';
        }

        return $this->first();
    }
}

//Image
class Image extend Model
{
    public function imageable
    {
        return $this->morphTo();
    }

    public function newCollection(array $models = [])
    {
        return new ImageCollection($models);
    }

    public function path($size)
    {
        //Here, there is some logic to build the image path and it needs
        //the imageable_type attribute no matter if there is
        //an image record in the database or not
        return;
    }
}

I want to be able to do so

$path = User::find($id)->images->firstOrDefault()->path('large');

But I can't figure out how to get the parent class name to build the path properly...

I tried with $morphClass or getMorphClass() but can't figure out how to use it properly or if it is even the right way to do it.

Any thoughts on that?

Upvotes: 0

Views: 3923

Answers (3)

oussama benounnas
oussama benounnas

Reputation: 99

i may be late for the party, but i kinda did a small trick for morph relationships where i had "media" as morph, i get the parent since "model_type" has the full string parent class string.

 $model = new $media->model_type;
 $media->model = $model->findOrFail($media->model_id);

Upvotes: 0

Elie Faës
Elie Faës

Reputation: 3315

Ok guys I've found something that seems to work pretty good for now so I'll stick with that.

In the Image model, I've added some code when I make the new collection:

public function newCollection(array $models = [])
{
    $morphClass = '';

    $parent = debug_backtrace(false, 2)[1];

    if (isset($parent['function']) AND $parent['function'] === 'initRelation') {
        if (isset($parent['args'][0][0])) {
            $morphClass = get_class($parent['args'][0][0]);
        }
    }
    return new ImageCollection($models, $morphClass);
}

I then simply retrieve the morphClass through the constructor of the ImageCollection

private $morphClass;

public function __construct($items = [], $morphClass)
{
    parent::__construct($items);
    $this->morphClass = $morphClass;
}

public function firstOrDefault()
{
    if ($this->count() === 0) {
        $image = new Image();
        $image->id = 'default';
        $image->imageable_type = $this->morphClass;
        $image->imageable_id = '.';
    }

    return $this->first();
}

This way, I can simply call the method like that

User::with('images')->get()->images->firstOrDefault()

This seems to work really great in many cases, if I have some issues at some times, I'll update this post.

Upvotes: 0

Thomas Kim
Thomas Kim

Reputation: 15911

I think you can keep it simple and drop the ImageCollection class because there is already a firstOrNew method that seems to be what you're looking for.

The firstOrNew method accepts an array of attributes that you want to match. If you don't care about the attributes, you can pass an empty array. If there are no matches in the database, it'll make a new instance with the proper parent type.

$path = User::find($id)->images()->firstOrNew([])->path('large');

Note: I am calling the images() method to get the MorphMany object so that I can call the firstOrNew method. In other words, you need to add the parentheses. Otherwise, you get a Collection.

Edit: If you want to make things a bit simpler by automatically setting some default attributes, you can add this to your ImageableTrait:

public function imagesOrDefault()
{
    $defaultAttributes = ['id' => 'default'];
    return $this->images()->firstOrNew($defaultAttributes);
}

Then, you can do something like this: $path = User::find($id)->imagesOrDefault()->path('large');

Note that your default attributes must be fillable for this to work. Also, imageable_id and imageable_type will automatically be set to your parent's id and type.

If you want to set the default value for imageable_id to a period and not the parent's id, then you have to alter it a bit, and it will look a lot like your original code except this will go inside your ImageableTrait.

public function imagesOrDefault()
{
    // First only gets one image.
    // If you want to get all images, then change it to get.
    // But if you do that, change the conditional check to a count.
    $image = $this->images()->first();

    if (is_null($image))
    {
        $image = new Image();
        $image->id = 'default';
        $image->imageable_type = $this->getMorphClass();
        $image->imageable_id = '.';
    }
    return $image;
}

Upvotes: 1

Related Questions