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!
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
Page
)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 aMorphTo
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();
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With