Home » Php » php – How Best to Link to a Polymorphic Relation in Laravel

php – How Best to Link to a Polymorphic Relation in Laravel

Posted by: admin July 12, 2020 Leave a comment

Questions:

Model A has a polymorphic relation to models X, Y, Z. Relevant fields in A are:

poly_id (integer foreign key)
poly_type (string App\X, App\Y or App\Z)

Given an instance of model A, I can successfully use $a->poly to retrieve the related object of type X, Y or Z. (E.g. {"id":1,"name":Object X}).

In a Blade template for A, how should I generate an show link to X such as ‘/x/1’? What springs to mind is URL::route('x.show', $a-poly_>id) however as far as I can see, we don’t actually have the ‘x’ part of the route available to us – only the poly_id, poly_type and both objects.

Am I missing something? A solution like taking the poly_type string ‘App\X’ and split off the last segment and lowercase to get ‘x’ but that doesn’t seem ideal, and potentially the defined route could be something else.

As an aside, in Rails I’m pretty sure you can do link_to($a->poly) and it would magically return the URL ‘/x/3’. Not sure if Laravel can do that. I tried url($a->poly) and it doesn’t work.

How to&Answers:

Use MorphToMany & MorphedByMany

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class A extends Model
{
    public function xs()
    {
        return $this->morphedByMany('App\X', 'poly_type');
    }

    public function ys()
    {
        return $this->morphedByMany('App\Y', 'poly_type');
    }

    public function zs()
    {
        return $this->morphedByMany('App\Z', 'poly_type');
    }

 }

Route:

Route::get('/a/show/{a}/{poly}', '[email protected]');

And Controller:

class AController extends Controller {

    public function index(A $a, $poly)
    {
         dd($a->{$poly}->first()); // a $poly Object
    }
}

So, /a/show/1/xs is a valid path

Answer:

This might seem overly simplistic but you could use something like this to solve your problem

I would create a controller something like this:

use App\Http\Controllers\Controller;
use A;
use X;
use Y;
use Z;    

class PolyController extends Controller {

public function aPoly(A $a)
{
    $poly = $a->ploy;
    switch ($ploy)
        case instance of X:
            return response()->redirectToRoute('X route name', $poly);
            break;
        case instance of Y:
            return response()->redirectToRoute('Y route name', $poly);
            break;
       case instance of Z:
            return response()->redirectToRoute('Y route name', $poly);
            break;
       default:
            report(new Exception('Failed to get route for ploy relationship');
   }
}

This would then allow you to use the following in your routes file:

Route::get('enter desired url/{a}', '[email protected]')->name('name for poly route');

And then in your controller you just do something like this:

<a href="{{ route('name for poly route', $a) }}">$a->ploy->name</a>

This is how I would like deal with the situation

Answer:

I think my solution to this is going to be as follows. All tables that are on the poly-end of a polymorphic relationship will have a property identifying the route or action (depending on how I go with this idea).

Model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Y extends Model
{
    /**
     * The route name associated with the model.
     *
     * @var string
     */
    protected $routeName= 'path.to.y.show';
}

Then, you could use code like this to find the route, regardless of the model at the end:

route($a->poly->routeName, $a->poly)

Or, for hasMany

@foreach($a->polys as $object) 
    <a href="{{route($object->routeName, [$object])}}">But what name?</a>
@endforeach

I don’t know if this is acceptable, though. If you haven’t defined the routeName on a model, then you’ll run into errors. I’m also not sure the model should know about routing!

In order to determine the name to be shown, should some sort of getGenericNameAttribute be defined that returns the appropriate property from the model?


I’m answering in the hope that somebody has a more elegant solution. For example, registering a service provider that then:

  • Allows the route to model mapping to be defined (a bit like policies?); and
  • Determines the correct route based on a passed model / model class.

It’s just I wouldn’t know how to do this!

Answer:

Laravel doesn’t have a simple solution for this like Rails does (at the moment). That’s because there is no implicit connection between route names and model names. There is a naming convention but it isn’t really applied in the code. I can’t ask for a model’s URL, I need to call route('model.show', $model).

Some other solutions here propose using a redirection controller but that’s inconvenient and poor as a user experience (and for SEO). You’re better off creating a helper function that can generate the route you need – that way the functionality is available anywhere in the app and it’s not tied to the model or controller layer.

If you wanted to have control over the actual page visited (i.e. not just the show route) then you could wrap the route helper that can take the action you want and generate the right URL.

use Illuminate\Database\Eloquent\Model;

function poly_route(string $route, Model $model): string
{
    return route($model->getTable() . '.' . $route, $model);
}

This would let you do something like poly_route('show', $poly);

Generally you’ll be placing helper functions in bootstrap/helpers.php and registering that file in your composer.json file – if you are using a helpers file already. Of course, if you’re not using a helpers file then this solution might already feel hacky.

I’d then suggest you explore moving the function to a method on the model.

class Poly extends Model
{
    public function route($name)
    {
        return route($this->getTable() . '.' . $route, $this);
    }
}

Then you can simply call $poly->route('show'). Again, neither of these solutions are totally elegant but they might beat having a heap of if/else or switch cases in your app supporting each use-case. Hopefully Laravel will provide better functionality for this sort of thing going forward.

Answer:

With Laravel don’t possible bind route and Model. But i have an idea for your problem.

You can add custom attribute with mutators to your A model that will be responsible decide which route using when an object is calling. For Example;

A Model;

/**
 * Get the route
 *
 * @return string
 */
public function getRouteAttribute()
{
    $route = null;
    switch ($this->poly_type){
        case 'App\X':
            $route = 'your_x_route_name';
            break;
        case 'App\Y':
            $route = 'your_y_route_name';
            break;
        case 'App\Z':
            $route = 'your_z_route_name';
            break;
    }
    return $route;
}

We can thinks it like a Factory method.

When we want use it, we can use a route following way.

@foreach($a->polys as $object) 
    <a href="{{ route($a->route, [your parameters.]) }}">But what name?</a>
@endforeach

That is may be not perfect practice, but i think it useful managing from one point.