8

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.

Karl Hill
  • 12,937
  • 5
  • 58
  • 95
Space
  • 2,022
  • 1
  • 19
  • 29
  • 1
    I'm not aware of a "correct" way, but I think a fairly clean approach would be to extend your idea above: in your blade use {{ URL::route($a->poly_type . '.show', $a->poly_id) }}, and in your route file add a definition for each type: Route::get('x/{id}', ['as' => 'App\X.show', 'uses' => 'XController@show']); Thoughts? If you don't like using the full namespace, you could use $a->poly->getTable(), or set the Eloquent property $a->poly->morph_class = "x". You might update the $morph_class value anyway to make writing custom queries easier. – mattcrowe Jun 11 '16 at 23:19
  • 1
    @mattcrowe thanks - some good workarounds. I wasn't aware of getTable - this seems easiest as it can reference an existing route `{{ URL::route($a->poly->getTable() . '.show', $a->poly_id) }}`. – Space Jun 12 '16 at 04:32
  • In my mind, you could either set properties / lookup methods on all models that you might "discover", such that you can determine the route and title. Or, you could create a mapping somewhere, e.g. in a service provider, that can find the handler for any given model? – Elliot Jan 21 '19 at 02:53
  • The answer to this question might also be useful for generating breadcrumbs (look up the tree, finding URLs for each model as you go, no matter what sort of model is encountered). – Elliot Jan 21 '19 at 02:53
  • Not sure this is appropriate, but could send to an endpoint that forms the right link by calculating the other parameters and redirects (e.g. /redirector/x/2 => /project/a/model-x/2). What would this be, a massive switch? – Elliot Jan 21 '19 at 23:57

5 Answers5

5

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}', 'AController@index');

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

Murat Tutumlu
  • 762
  • 6
  • 16
  • The morphMap allows you to store aliases for each polymorphic model. It would be neat to use these instead of mappings in the model. Otherwise this is a pretty good solution. – Elliot Jan 22 '19 at 23:41
  • What about defining a method in the model like function poly($model) { return $this->morphedByMany('App\' . $model, 'poly_type'); } and using it in the controller as dd($a->poly($poly)->first()). So paths like /a/show/1/X or /a/like/show/Y etc. will be valid. It will work without one-by-one mapping. – Murat Tutumlu Jan 24 '19 at 16:37
0

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!

Elliot
  • 1,457
  • 13
  • 40
  • Maybe the Models could extend a class or use a trait that handles names / routes gracefully? – Elliot Jan 21 '19 at 02:54
  • Thinking about it, this doesn't handle situations where the route is nested. You'd need some sort of translator for that too. e.g. turns `(X:Class, ID=1)` into `/grandparent/a/parent/b/x/1` – Elliot Jan 21 '19 at 03:05
0

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}', 'PolyController@aPoly')->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

Josh
  • 1,316
  • 10
  • 26
  • This wouldn't work. $a is an id; and Laravel is going to find an object for it by using A::findOrFail($a)... which will return a model (if one exists) of type A; when we know it should in fact be of type x, y or z. And if you don't Type Hint it, all you're getting is an integer and no idea of the model/poly type. – Elliot Jan 22 '19 at 02:17
  • @Kurucu there has to be a model of A or else you would not be able to create a polymorphic relationship. I am not fully averse with the restrictions of using 5.2 but I am confident that it would work in 5.5 and beyond. – Josh Jan 22 '19 at 02:41
  • So in the problem A is the thing we already have (we don't need a link to it). It has a polymorphic one to many relationship, and in this case $a has a $x, a $y and a $z. So the question was, how to link to $x, $y, or $z when they are each of different types (X, Y and Z, respectively). – Elliot Jan 22 '19 at 02:44
  • Now I'm understanding how the question was interpreted, your code would work. But only for 1:1 relationships. – Elliot Jan 22 '19 at 02:47
  • Unless I am mistaken polymorphic relationships can only be one to one. – Josh Jan 22 '19 at 05:02
  • No, they can be one-to-many in 5.2 (https://laravel.com/docs/5.2/eloquent-relationships#polymorphic-relations - see morphMany). That said, I still think you're right about this question being one-to-one. – Elliot Jan 22 '19 at 05:05
  • Whilst your version works for this scenario, I still feel like there's a more elegant solution? I might look into how ruby does it. Some sort of model->route mapper provider; which knows how to reconstruct the route for *any* passed model? – Elliot Jan 22 '19 at 05:07
  • @Kurucu I would love to see what solution you come up with. Please keep me updated – Josh Jan 24 '19 at 22:33
0

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.

Dwight
  • 12,120
  • 6
  • 51
  • 64
0

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.

FGDeveloper
  • 1,030
  • 9
  • 23