nathangross's avatar

Polymorphic trait?

I am building out a polymorphic (morphMany) class called Follow and a trait called Followable. I'm struggling with getting the right model passed how I'd like.

FWIW I'm semi-following along with Jeffery's Let's build a forum series, episode 18.

So here is where I am currently:

I have Events that can be followed using the Followable trait (and later, other things can be followed, like locations, other users, etc.) Currently, I'm using the following form in an event.show view:

<form method="POST" action="{{route('follow-event', ['model' => 'event', 'id' => $event->id])}}">
  @csrf
   <button type="submit">Follow</button>
</form>

And on my FollowController:

$attributes = [
  'user_id' => auth()->id(),
  'followable_type' => $model,
  'followable_id' => $id
];

Follow::create($attributes);
return back();

AppServiceProvider:

public function boot()
{
  Relation::morphMap([
    'event' => 'App\Event',
  ]);
}

And here is where I'd like to get to:

So far, this all works. But I'm hoping to be able to have a method on my Followable trait to help keep things clean and set some integrity constraints like only 1 follow per event (but keep it polymorphic) per user. Something like:

public function follow($model)
{
  $attributes = ['model' => $model, 'user_id' => auth()->id()];
  
  if (! $this->follows()->where($attributes)->exists()) {
    return $this->follows()->create($attributes);
  };
}

But since this is a trait, I can't figure out a good way to get this method to "understand" what its current class is.

I'd also like to DRY the button on my form so I can reuse it elsewhere. What is a good way to get and pass the model of whatever view it's on?

0 likes
13 replies
bobbybouwmann's avatar

If you only add the trait to models this should work correctly, right? You can always use $this because that points to the current model that has this trait.

I'm not sure what else you're trying to achieve here.

nathangross's avatar

@bobbybouwmann @s4muel Ah I don't think I totally follow. I believe I understand $this refers to the instance of whatever model this trait is applied to. But how do I get $this over to the controller—while keeping both the trait and controller polymorphic and DRY.

This hurts my brain :)

nathangross's avatar

Also, I guess I’m not sure on where you guys mean I can use $this

bobbybouwmann's avatar

Like @s4muel suggested, you can use get_class to get the current namespace of the class. So something like this

public function follow()
{
      $attributes = ['model' => get_class($this), 'user_id' => auth()->id()];
}
1 like
nathangross's avatar

@bobbybouwmann Ah ok, that makes sense. But now how about on my store method?

Before I was just creating a new Follow instance using Follow::create() and passing in all the attributes from the form. But now that I'm using a new follow() method on a followable trait, I have no idea how to pass that into my store method on my controller.

Before:

public function store($model, $id)
    {

        $attributes = [
            'user_id' => auth()->id(),
            'followable_type' => $model,
            'followable_id' => $id
        ];

        Follow::create($attributes);
        return back();
    }

Now??

public function store($this)
    {
        Follow::create($this);
        return back();
    }

I know that's wrong, but that's how confused I am

bobbybouwmann's avatar

Where do you call this store method? On what class do you call this?

1 like
nathangross's avatar

The store method is on a controller called FollowController (see below). The way I currently have everything set up:

Follow class

class Follow extends Model
{
    protected $fillable = ['followable_id', 'followable_type', 'user_id'];

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

Followable trait

trait Followable
{

  public function follows()
  {
    return $this->morphMany(Follow::class, 'followable');
  }


  public function follow()
  {
    $attributes = ['model' => get_class($this), 'model_id' => $this->id, 'user_id' => auth()->id()];
    if (! $this->follows()->where($attributes)->exists()) {
      return $this->follows()->create($attributes);
    }
  }
}

FollowController (this is how I was storing it. But looking to update this store method and utilize the Followable trait—I don't want to have to check the exists logic on the store method)

   public function store($model, $id)
    {
        $attributes = [
            'user_id' => auth()->id(),
            'followable_type' => $model,
            'followable_id' => $id
        ];

        Follow::create($attributes);
        return back();
    }

And a button on a view - could this be improved to make it work on any model (currently Event model)

<form method="POST" action="{{route('follow-event', ['model' => 'event', 'id' => $event->id])}}">
  @csrf
  <button type="submit" >Follow</button>
</form>

I may be a bit confused on if I should create the follow from the user, from the event or somehow from itself and just pass in the model and user attributes.

I hope that all makes sense :)

bobbybouwmann's avatar

I'm not sure what you mean, that you need to use the store method here. You already have this working with the follow method of the trait, right?

You can always do this

$anotherUser()->follow(); // The current logged in user is now following anotherUser

So your store method should simply be using the follow method o the model you have.

public function store($model, $id)
{
    $model->follow();

    return back();
}
1 like
nathangross's avatar

If I use the following store method on my FollowController, I get the following error: Call to a member function follow() on string

public function store($model, $id)
{
    $model->follow();

    return back();
}

I think my confusion is just about how all of these files are passing data through to each other and perhaps the order they go in.

So I get that the view is passing the $model (string: event) and an $id (a number) of that event to the controller's store function. The store() method is calling up the follow() method on the Followable trait. But the followable trait is being used by my Event model. From there I have a really hard time grasping what's happening.

bugsysha's avatar
bugsysha
Best Answer
Level 61
public function store(Request $request)
{
	Follow::query()->firstOrCreate([
		'followable_type' => $request->get('followable_type'),
		'followable_id' => $request->get('followable_id'),
		'user_id' => $request->user()->getKey(),
	]);
	return back();
}
davidifranco's avatar

I think the problem here starts with the route itself. Your route name is called 'follow-event' so you can type hint the event in the route url and pass it to your controller. Then at the controller level you can store the “follow” through the relationship. So your store method should look like this:

public function store(Request $request, Event $event)
{
    $event->follow();
}

The followable trait, which should be used on events, users any other Model’s you want to be followable. The trait should have a follow method.

public function follow()
{
    If(auth()->User()->follows()->where('followable_id', $this->id)->doesntExist())
   {
       Auth()->User()->follows()->save($this->id);
   }
   return;
 }

While I see that you want to have a single controller action to handle all types of follows.. the complexity/readability it adds is just not worth the savings.

Hope this helps!

nathangross's avatar

I really appreciate all the help in here—especially from @bobbybouwmann. This is an amazing community.

Ultimately @bugsysha helped talk me through it and I can't thank him enough.

@davidifranco I did end up using a trait for some of the followable methods since I'll have the same "follow" code on several models on my codebase but I take your point about the complexity cost. The complexity will end up somewhere, but I think my problem was trying push the magic of figuring out what model to associate with the follow to a method on the trait—when I think it makes more sense to keep it at the form and controller level.

So for posterity or anyone following along:

My route:

Route::post('/replies/{reply}/favorites', 'FavoritesController@store');

My form/view

<form method="POST" action="{{route('follow')}}">
  @csrf
  <input type="hidden" value="App\Event" name="followable_type">
 <input type="hidden" value="{{ $event->id }}" name="followable_id">
 <button type="submit" ">
  Follow
 </button>
</form>

My FollowController:

    public function store(Request $request)
    {
        $attributes = [
            'user_id' => auth()->id(),
            'followable_type' => $request->get('followable_type'),
            'followable_id' => $request->get('followable_id')
        ];

        Follow::query()->firstOrCreate($attributes);
        return back();
    }

Please or to participate in this conversation.