Laravel: Returning the namespaced owner of a polymorphic relation

John Mellor picture John Mellor · Jan 9, 2015 · Viewed 18.9k times · Source

I can find a number of discussions regarding this but no clear solution. Here are two links, although I will cover everything in my own question here.

Github Issues

Laravel.io discussion

Simple Explanation

This is a simple explanation of my problem for anyone already familiar with Laravel's polymorphic relationships.

When using $morphClass, the contents of $morphClass which is saved in the database as the morph "type" is used for the classname when trying to find the owner of a polymorphic relation. This results in an error since the whole point of $morphClass is that it is not a fully namespaced name of the class.

How do you define the classname that the polymorphic relationship should use?

More Detailed Explanation

This is a more detailed explanation explaining exactly what i'm trying to do and why i'm trying to do it with examples.

When using Polymorphic relationships in Laravel whatever is saved as the "morph_type" type in the database is assumed to be the class.

So in this example:

class Photo extends Eloquent {

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

}

class Staff extends Eloquent {

    public function photos()
    {
        return $this->morphOne('Photo', 'imageable');
    }

}

class Order extends Eloquent {

    public function photos()
    {
        return $this->morphOne('Photo', 'imageable');
    }

}

The database would look like this:

staff

 - id - integer
 - name - string

orders

 - id - integer
 - price - integer

photos

 - id - integer
 - path - string
 - imageable_id - integer
 - imageable_type - string

Now the first row of photos might look like this:

id,path,imageable_id,imageable_type

1,image.png,1,Staff

Now I can either access the Photo from a Staff model or a Staff member from a Photo model.

//Find a staff member and dump their photos
$staff = Staff::find(1);

var_dump($staff->photos);

//Find a photo and dump the related staff member
$photo = Photo::find(1);

var_dump($photo->imageable);

So far so good. However when I namespace them I run into a problem.

namespace App/Store;
class Order {}

namespace App/Users;
class Staff {}

namespace App/Photos;
class Photo {}

Now what's saved in my database is this:

id,path,imageable_id,imageable_type

1,image.png,1,App/Users/Staff

But I don't want that. That's a terrible idea to have full namespaced class names saved in the database like that!

Fortunately Laravel has an option to set a $morphClass variable. Like so:

class Staff extends Eloquent {

    protected $morphClass = 'staff';

    public function photos()
    {
        return $this->morphOne('Photo', 'imageable');
    }

}

Now the row in my database will look like this, which is awesome!

id,path,imageable_id,imageable_type

1,image.png,1,staff

And getting the photos of a staff member works absolutely fine.

//Find a staff member and dump their photos
$staff = Staff::find(1);

//This works!
var_dump($staff->photos);

However the polymorphic magic of finding the owner of a photo doesn't work:

//Find a photo and dump the related staff member
$photo = Photo::find(1);

//This doesn't work!
var_dump($photo->imageable);

//Error: Class 'staff' not found

Presumably there must be a way to inform the polymorphic relationship of what classname to use when using $morphClass but I cannot find any reference to how this should work in the docs, in the source code or via Google.

Any help?

Answer

Jarek Tkaczyk picture Jarek Tkaczyk · Jan 12, 2015

There are 2 easy ways - one below, other one in @lukasgeiter's answer as proposed by Taylor Otwell, which I definitely suggest checking as well:

// app/config/app.php or anywhere you like
'aliases' => [
    ...
    'MorphOrder' => 'Some\Namespace\Order',
    'MorphStaff' => 'Maybe\Another\Namespace\Staff',
    ...
]

// Staff model
protected $morphClass = 'MorphStaff';

// Order model
protected $morphClass = 'MorphOrder';

done:

$photo = Photo::find(5);
$photo->imageable_type; // MorphOrder
$photo->imageable; // Some\Namespace\Order

$anotherPhoto = Photo::find(10);
$anotherPhoto->imageable_type; // MorphStaff
$anotherPhoto->imageable; // Maybe\Another\Namespace\Staff

I wouldn't use real class names (Order and Staff) to avoid possible duplicates. There's very little chance that something would be called MorphXxxx so it's pretty secure.

This is better than storing namespaces (I don't mind the looks in the db, however it would be inconvenient in case you change something - say instead of App\Models\User use Cartalyst\Sentinel\User etc) because all you need is to swap the implementation through aliases config.

However there is also downside - you won't know, what the model is, by just checking the db - in case it matters to you.