Reason
I got a legacy system with a table containing slugs.
When those records match, it represents some kind of page with a layout ID.
Because these pages can have different resource needs it depends on the layout ID which tables can be joined with.
I use Laravel's Eloquent models.
What I would like is to have a child model that holds the layout specific relations.
class Page extends Model {
// relation 1, 2, 3 that are always present
}
class ArticlePage extends Page {
// relation 4 and 5, that are only present on an ArticlePage
}
However in my controller, in order to know which layout I need, I already have to query:
url: /{slug}
$page = Slug::where('slug', $slug)->page;
if ($page->layout_id === 6) {
//transform $page (Page) to an ArticlePage
}
Because of this I get an instance of Page, but I would like to transform or cast it to an instance of ArticlePage to load it's additional relations. How can I achieve this?
You'll need to look into Polymorphic relations in Laravel to achieve this. A Polymorphic Relation would allow you to retrieve a different model based on the type of field it is. In your Slug
model you would need something like this:
public function page()
{
return $this->morphTo('page', 'layout_id', 'id');
}
In one of your service providers, e.g. AppServiceProvider
you would need to provide a Morph Map to tell Laravel to map certain IDs to certain model classes. For example:
Relation::morphMap([
1 => Page::class,
// ...
6 => ArticlePage::class,
]);
Now, whenever you use the page
relation, Laravel will check the type and give you the correct model back.
Note: I'm not 100% sure on the parameters etc. and I haven't tested but you should be able to work it out from the docs.
If your layout_id
is on the Page
model, the only solution I see is to add a method to your Page
model that is able to convert your existing page into an ArticlePage
, or other page type, based on its layout_id
property. You should be able to try something like this:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
const LAYOUT_ARTICLE = 6;
protected $layoutMappings = [
// Add your other mappings here
self::LAYOUT_ARTICLE => ArticlePage::class
];
public function toLayoutPage()
{
$class = $this->layoutMappings[$this->layout_id];
if (class_exists($class)) {
return (new $class())
->newInstance([], true)
->setRawAttributes($this->getAttributes());
}
throw new \Exception('Invalid layout.');
}
}
What this does is look for a mapping based on your layout_id
property, and then it creates a new class of the correct type, filling its attributes with those from the page you're creating from. This should be all you need, if you take a look at Laravel's Illuminate\Database\Eloquent::newFromBuilder()
method, which Laravel calls when it creates new model instances, you can see what's going on and how I've gotten the code above. You should be able to just use it like this:
$page = Slug::where('slug', $slug)
->first()
->page
->toLayoutPage();
That will give you an instance of ArticlePage
As far as I know there is no built in function for this. But you could do something like this.
$page = Slug::where('slug', $slug)->page;
if ($page->layout_id === 6) {
$page = ArticlePage::fromPage($page);
}
And then on the ArticlePage create the static method
public static function fromPage(Page $page)
{
$articlePage = new self();
foreach($page->getAttributes() as $key => $attribute) {
$articlePage->{$key} = $attribute;
}
return $articlePage
}
Depending on your use-case might be smart to create a static method that does this automatically on the relation page() for Slug.