Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Laravel Eloquent and Namespacing Issues

I am building a CMS package for Laravel.

All of my models in this package are bound to, and resolved from, the IoC container so that they can be easily overwritten in any individual deployment of the package.

For non-polymorphic relationships, this has worked a charm.

For instance, a Page has many PageModules, so its relationship changed from:

// \Angel\Core\Page

public function modules()
{
    return $this->hasMany('PageModule');
}

to:

// \Angel\Core\Page

public function modules()
{
    return $this->hasMany(App::make('PageModule'));
}

But I haven't been able to figure out how to do the same thing with polymorphic relationships.

For instance, Menus contain MenuItems, and each MenuItem can be tied to a single other model, such as a Page or BlogPost.

To accomplish this the Laravel way, I've added the following relationship to MenuItem:

// \Angel\Core\MenuItem

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

And this relationship to LinkableModel, which all models such as Page and BlogPost extend:

// \Angel\Core\LinkableModel

public function menuItem()
{
    return $this->morphOne(App::make('MenuItem'), 'linkable');
}

And the menus_items table (which MenuItems use) has these rows:

linkable_type      |  linkable_id
-------------------|--------------
\Angel\Core\Page   |   11
\Angel\Core\Page   |   4

This works great, but I need the linkable_type to say 'Page' instead of '\Angel\Core\Page', and to be resolved from the IoC's 'Page' instead of being hard-coded to a particular namespaced class.

What I've Tried:

According to this question, it should be as easy as defining a $morphClass property to the linkable() classes, like so:

// \Angel\Core\Page
protected $morphClass = 'Page';

But when I apply this, and change the menus_items table to look like this:

linkable_type  |  linkable_id
---------------|--------------
Page           |   11
Page           |   4

...I simply get a Class 'Page' not found. error whenever linkable() is called on MenuItem.

This is the exact line in Eloquent that throws the error.

So, I dug into Eloquent and thought I might be able to get away with something like this:

// \Angel\Core\MenuItem

public function linkable()
{
    return $this->morphTo(null, App::make($this->linkable_type));
}

...this feels so close, but alas: Eloquent calls linkable() before it's filled the rest of the MenuItem's attributes / columns, so $this->linkable_type is null and therefore will not resolve anything from the IoC.

Thank you so much in advance for any guidance you might have!

like image 297
Leng Avatar asked Jul 12 '14 16:07

Leng


1 Answers

public function linkable()
{
    return $this->morphTo(null, App::make($this->linkable_type));
}

This will not work in any case, because morphTo() in Illuminate\Database\Eloquent\Model expects

  1. The name of the polymorphic relationship ('linkable')
  2. The type of the object to be morphed ('Page', not an instance of Page)
  3. The id of the object to be morphed

If they are not provided, Laravel is smart enough to guess them and then accordingly return a Illuminate\Database\Eloquent\MorphTo object.

Also, $this->linkable_type and $this->linkable_id should actually not be null in that context.

Let's have a quick look at the relevant part of the morphTo() function:

$instance = new $class;

return new MorphTo(
    $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name
);

Note: This is the code from version 4.2.6, the code linked above seems to be from a later version and is slightly different and the function returns a BelongsTo instead of a MorphTo object.

The problem is specifically the $instance = new $class; - the class is simply instanciated and not resolved. But you can just grab that part of the magic and handle it yourself:

public function linkable()
{
    $instance = App::make($this->linkable_type);

    $id = 'linkable_id';
    $type = 'linkable_type';
    $name = 'linkable';

    return new MorphTo(
        $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name
    );
}

This should actually work (haven't tested it), but I'm not sure about any side effects it might cause in some edge cases.

Or maybe you could also just override the whole function in your MenuItem-model and just adjust the relevant part:

public function morphTo($name = null, $type = null, $id = null)
{
    if (is_null($name))
    {
        list(, $caller) = debug_backtrace(false);

        $name = snake_case($caller['function']);
    }

    list($type, $id) = $this->getMorphs($name, $type, $id);

    //eager loading
    if (is_null($class = $this->$type))
    {
        return new MorphTo(
            $this->newQuery(), $this, $id, null, $type, $name
        );
    }

    // normal lazy loading
    else
    {
        // this is the changed part
        $instance = \App::make($class); // new $class;

        return new MorphTo(
            $instance->newQuery(), $this, $id, $instance->getKeyName(), $type, $name
        );
    }
}

Note: This works well for lazy-loading, but does not work for eager-loading. An issue has been raised seeking a solution for eager-loading here.

The relationship would then be as usual:

public function linkable()
{
    return $this->morphTo();
}
like image 74
Quasdunk Avatar answered Nov 12 '22 08:11

Quasdunk