Tip 3: Treat pivot models as their own resource#3
Conversation
|
Hi Adam, I ended up with a question after reading this tip. It is about the subscriptions endpoint. If the application has subscriptions for another concept, say news, now we have 2 things users can subscribe. The first thing that comes to my mind is to nest the subscriptions resource under its own parent resource: At first it does look fine. No need to pass the podcast in the request body but this is a nested resource and URL is a little longer. I think another approach could be to create a single resource that represents the actual concept without be a nested resource: This second option feels better to me as it is simpler and more expressive than the first one. What do you think about both options? Thanks Adam! |
|
Hi @adamwathan , I'm just wondering wheather calling Regards, s~ |
|
I think it will do it only if it was defined in the migration. |
|
@uxweb Why not go with polymorphic relation for subscription. then you can pass a subscription type and keep endpoint as same. |
|
Hey @uxweb, I like this approach: You could stick with that approach even if there was only one subscription type if you wanted, nothing wrong with that. If the idea of "news subscriptions" had no ID associated with it, then I'd probably just do: Cheers! |
|
@zippex You shouldn't have any problems with cascading deletes unless you set up your migrations that way specifically, but even then I think cascading deletes usually work in the other direction, ie. deleting a user would delete all of that user's subscriptions. |
|
Hi @adamwathan , We love your tip of using a dedicated resource for managing pivot tables. Sometimes you want to do a bulk action: assign an array of ids instead of one id (i.e, postcast_id is an array of postcast_ids you want to assign or remove to that user , and the amount of ids in the array is quite big). What do you suggest in this case? |
|
@dailos I think the post endpoint can pass along an array of podcast_ids instead of a single id. This will still look quite clean and allow the use of dedicated pivot model and controller. @adamwathan What do you think of this though? |
|
@biblicalph we got the same conclusion: an array of ids. In the other hand, sending a new request per id looks cleaner but if the amount of ids is big enough it will be a waste of resources. |
|
I think I would try to figure out a way to move bulk updates to it's own controller, maybe by exposing a
Otherwise it sounds like you'd have to do a bunch of messy conditional logic in an existing action. |
|
Hi @adamwathan Since BulkSubscriptionsController only have 1 method would a single action controller be better? |
|
Hey @ziming! I still like using resourceful action names even if a controller only needs one action. I find if I don't impose that constraint, it gets too easy to start making single action controllers for every tricky action (like SubscribeToPodcastController) and I lose the benefits of uncovering new concepts in my domain like Subscriptions. |
|
Hello @adamwathan Beautiful tips, thanks for everything. In this line of the destroy method
Can I use this?
Cheers. |
The next custom actions I'd like to look at are
PodcastsController@subscribeandPodcastsController@unsubscribe.Here are the endpoints:
Modelling these endpoints as standard REST actions is a little trickier than our previous examples, because how do we translate
subscribeto something likestore,update, ordestroy?Let's try modelling it as a
storeaction first. The trick is to ask yourself this question:"After subscribing, what do I have now that I didn't have before?"
If you can answer that question reasonably, it's probably possible to model the custom action you're refactoring as a
storeaction.In our case, after subscribing to a podcast, we now have a subscription, which is something we didn't have before. Let's create a new controller!
✅ Create a dedicated
SubscriptionsControllerIf we are creating a new subscription, the standard endpoint structure would be something like this:
Something worth noticing here is that previously our endpoint had a route parameter, the
{id}of the podcast we were subscribing to. Since our new endpoint doesn't have that parameter, we'll have to pass it through in the request body.Here's what our new controller action would look like now:
Modelling
unsubscribeLet's tackle
unsubscribenext. Ifsubscribebecomes creating a subscription, then maybeunsubscribeshould become destroying a subscription!The standard endpoint structure for destroying a resource would look something like this:
But wait a minute: notice how our
/subscriptionsendpoint there has an{id}parameter? Subscriptions aren't real models in our system; we don't have the concept of anidfor a particular subscription.How do we delete a subscription by
idif subscriptions don't have IDs?Uncovering a new model
If you look at our implementation, you'll see that subscribing to a podcast creates a new record in our
podcast_userpivot table:If you think about it, doesn't each one of those records represent a user's subscription to a podcast? Each one of those records has an ID, but how can we get it?
Well if we rename
podcast_usertosubscriptions, we can also create an Eloquent model for working with that table directly calledSubscription. Since this table has foreign keys back tousersandpodcasts, we could even define those asbelongsTorelationships on the new model:Remember the line in our controller where we added a pivot record through the
attachmethod? What if we just explicitly create a new subscription instead?Now that we are able to expose a
Subscriptionas it's own resource, it's not so far fetched to expect someone to have theidof the subscription they want to destroy when unsubscribing.Here's what the controller action could look like:
Here's what we're left with after this refactoring:
PodcastsController, with 7 standard actions and 2 custom actionsEpisodesController, with 4 standard actions and no custom actionsPodcastEpisodesController, with 3 standard actions and no custom actionsPodcastCoverImageController, with 1 standard action and no custom actionsSubscriptionsController, with 2 standard actions and no custom actionsNext up: Think of different states as different resources