Published 7 Sep 2023

Demystifying Laravel Routes

Business

Introduction

Laravel is one of the most popular PHP web application frameworks. It offers a robust and flexible routing system that allows developers to define and manage application URLs effortlessly. In this blog, we'll dive deep into Laravel routes, exploring the fundamental concepts, types of routes, and best practices for working with them.

What Are Routes in Laravel?

Routes are a fundamental part of web development. They define the entry points to your application, mapping URLs to specific controllers and actions. Simply put, routes determine how your application responds to HTTP requests.

Defining Routes

To define routes in Laravel, you primarily work with the web.php and api.php files, located in the project's route folder. These files contain route definitions using Laravel's expressive routing methods.

Basic Route Definition

Here's a basic example of defining a route in Laravel:

Route::get('/welcome', function () {
    return 'Welcome to 91 Bytes!';
});

In this example, we're defining a route that responds to the GET request at the /welcome URL with a simple text response.

Route Parameters

Laravel allows you to capture and work with route parameters easily. For example:

Route::get('/user/{id}', function ($id) {
    return 'User ID: ' . $id;
});

In this case, the {id} placeholder captures the value from the URL, making it accessible as a parameter in the route's closure.

Router Methods

The router allows you to register routes that respond to any HTTP verb:

  1. GET Routes: Used for retrieving resources. For example, displaying a web page or fetching data from the server.
  2. POST Routes: Typically used for submitting data to the server, such as form submissions.
  3. PUT/PATCH Routes: Used to update existing resources. PUT is often used for full resource replacement, while PATCH is used for partial updates.
  4. DELETE Routes: Used to delete resources.
  5. You may even register a route that responds to all HTTP verbs using the any method.
  6. If you are defining a route that redirects to another URI, you may use the Route::redirect method.
  7. If your route only needs to return a view, you may use the Route::view method.
  8. Route::resource is a convenient method provided by Laravel for defining RESTful resource routes. It allows you to generate a set of standard CRUD (Create, Read, Update, Delete) routes for a resource controller with just a single line of code. This is especially useful for building applications that adhere to RESTful conventions.
  9. Sometimes, your application will have resources that may only have a single instance. For example, a user's profile can be edited or updated, but a user may not have more than one profile. Likewise, an image may have a single thumbnail. These resources are called singleton resources, meaning one and only one instance of the resource may exist. In these scenarios, you may register a singleton resource controller.

Route::get($uri, $callback);
Route::post($uri, $callback);
Route::put($uri, $callback);
Route::patch($uri, $callback);
Route::delete($uri, $callback);
Route::options($uri, $callback);
Route::any($uri,$callback);
Route::redirect('/here', '/there');
Route::view('/welcome', 'welcome');
Route::resource('products', 'ProductController');
Route::singleton('profile', ProfileController::class);

When defining multiple routes that share the same URI, routes using the get, post, put, patch, delete, and options methods should be defined before routes using the any, match, and redirect methods. This ensures the incoming request is matched with the correct route.

Named Routes

Named routes provide a convenient way to reference routes by name rather than their URLs. This is particularly useful when generating links or redirects. To define a named route, use the name method:

Route::get('/about', function () {
    return 'About Us';
})->name('about');

You can then generate URLs for named routes using the route function:

$url = route('about');

Route Middleware and Groups

Middleware is a powerful feature in Laravel that allows you to filter HTTP requests entering your application. You can attach middleware to routes, making it easy to perform tasks like authentication, authorization, logging, and more.

Route groups allow you to apply middleware, prefixes, and namespaces to a group of routes, making it easier to organize and manage routes for specific parts of your application.

Route::middleware(['first', 'second'])->group(function () {
    Route::get('/', function () {
        // Uses first & second middleware...
    });
 
    Route::get('/user/profile', function () {
        // Uses first & second middleware...
    });
});

Middleware are executed in the order they are listed in the array.

Cruddy by Design

Cruddy by Design is a software development philosophy or approach that emphasizes simplicity and efficiency in creating CRUD (Create, Read, Update, Delete) operations for database-backed applications. These operations are fundamental to many software applications, especially those that involve data management.

Resource Controllers

Instead of defining all of your request-handling logic as closures in your route files, you may wish to organize this behavior using "controller" classes. Controllers can group related request-handling logic into a single class.

4 RULES

These were mentioned by Adam Wathan in his Laracon conference. We follow the rules mentioned by him in our code. Let us checkout the rules:

Dedicate a controller for a nested resource.

Let's say you have a podcasts application and each podcast contains episodes. If you want to create a route to retrieve the episodes of an individual podcast, it might look like this. It will be a nested resource. So, which controller will be used to perform the necessary action? Let's take a look.

/podcasts/{id}/episodes

Use PodcastsController?

We can see that /podcasts are at the top level. So, to perform nested index actions, the first thought might be to use the PodcastsController. But, if we use PodcastsController for that, we end up defining custom actions that aren't part of the CRUD actions mentioned above. We always want to have the CRUD actions and not custom actions. So, this isn't a good approach. Let's think of other approaches.

Use EpisodesController?

We already have an EpisodesController responsible for handling various actions related to episodes, such as listing episodes, displaying episode details, editing episodes, and updating episodes.

Since the above action should give the list of episodes for the given podcast, you might be tempted to point the route to EpisodesController@index.

But here's where the problem comes in, the EpisodesController@index lists all episodes across all podcasts. So to fix that, you need to pass $id as a parameter to the function in the controller and check if it is null and proceed to perform

  • listing of all episodes across all the podcasts if it is null
  • Otherwise, list the episodes of a single podcast.

Here we are reusing the same EpisodesController@index twice and the use cases are different. They don't have anything in common as they load different models, and return different views,

That usually looks like this in a routes file:

Route::get('/episodes','EpisodesController@index');
Route::get('/podcasts/{id}/episodes', 'EpisodesController@index');

Let's take a look at the controller:

class EpisodesController
{
    public function index($id = null)
    {
        if ($id === null) {
            $episodes = Episode::with('podcast')->recent()->paginate(25);

            return view('episodes.index', [
                'episodes' => $episodes,
            ]);
        } else {

            $podcast = Podcast::with('episodes')->findOrFail($id);

            abort_unless($podcast->isVisibleTo(Auth::user()), 404);

            return view('podcasts.list-episodes', [
                'podcast' => $podcast,
            ]);
        }
    }
}

This doesn't simplify the code, instead, it complicates it even more by using the same controller for two different actions. So, this method isn't good as well.

The answer is - Create a PodcastEpisodesController

If we iterate over the list of standard actions, the index is the most suitable action for our case. We need the index of individual podcast episodes. So, let's create a new PodcastEpisodesController.

Route::get('/podcasts/{id}/episodes', 'PodcastEpisodesController@index');

Here's the PodcastEpisodesController:

class PodcastEpisodesController extends Controller
{
    public function index($id)
    {
        $podcast = Podcast::with('episodes')->findOrFail($id);

        abort_unless($podcast->isVisibleTo(Auth::user()), 404);

        return view('podcast-episodes', [
            'podcast' => $podcast,
        ]);
    }
}

If you have any other routes related to episodes nested under podcasts resource, their actions can also be handled by PodacastEpisodesController.

Treat properties edited independently as separate resources

Each podcast might have a cover image, title, created date etc. as its properties. Let's take a look at the cover image of a podcast.

Here's the endpoint:

Route::post('/podcasts/{id}/update-cover-image', 'PodcastsController@updateCoverImage');Route::post('/podcasts/{id}/update-cover-image', 'PodcastsController@updateCoverImage');

If you look at the endpoint, it is pretty clear that the action isn't standard CRUD action. If you iterate over the CRUD actions, the update is the fitting one. For updating an image, all we are doing is updating a field on the podcasts table.

You might think that what we are doing here is an "update" action against our podcasts resource, but it already has an update action which is updating the title,description and website and it doesn't include the cover image. They hit different end points as well. So what should we do?

Route::post('/podacsts/{id}/update', 'PodcastsController@update');
Route::post('/podcasts/{id}/update-cover-image', 'PodcastsController@updateCoverImage');

public function update($id)
{
    $podcast = Auth::user()->podcasts()->findOrFail($id);

    request()->validate([
        'title' => ['required', 'max:150'],
        'description' => ['max:500'],
        'website' => ['url'],
    ]);

    $podcast->update(request([
        'title',
        'description',
        'website',
    ]));

    return redirect("/podcasts/{$podcast->id}");
}

Create a PodcastCoverImageController

Here we are using two forms to update the podcast properties:

  • form1 - to update the details of the podcast
  • form2 - to update the cover image of the podcast

In cases like these, it is better to use separate controllers for each.

So instead of making a POST request to update-cover-image, let's make a PUT request that treats the cover image like its own resource:

 Route::post('/podcasts/{id}/update-cover-image', 
            'PodcastsController@updateCoverImage');

Treat pivot models as their own resource

Since it is a podcast application, you can subscribe to the podcasts you like and unsubscribe if you lose interest in them. Let's look at how it's being handled.

So, we need to look at PodcastsController@subscribe and PodcastsController@unsubscribe. Here are the endpoints:

Route::post('/podcasts/{id}/subscribe',   'PodcastsController@subscribe');
Route::post('/podcasts/{id}/unsubscribe', 'PodcastsController@unsubscribe');

Where does subscribing fit in the CRUD operations we have? It would fit in the store, since we would be storing the podcast if subscribed else we will be deleting that. Let's create a new controller called Subscription Controller. Then the endpoint would be:

Route::post('/subscriptions','SubscriptionsController@store')

If we were using the podcast controller, we were using the id as the route parameter which is not available here. So, we have to send the id as a request.

class SubscriptionsController
{
    public function store()
    {
        $podcast = Podcast::published()->findOrFail(request('podcast_id'));

        $podcast->users()->attach(Auth::user());

        return response('', 204);
    }
}

In the above implementation, the attack method associates the podcast with the user by creating a new record in the podcast_user table. And each of this record has an id. So, if we rename podcast_user to subscriptions, we can also create a model for working with the table directly called Subscription.

Since this table has foreign keys back to users and podcasts, we could even define those as belongsTo relationships on the new model:

class Subscription extends Model
{
    protected $guarded = [];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function podcast()
    {
        return $this->belongsTo(Podcast::class);
    }
}

Instead of using the attach method, we can just create a new subscription.

class SubscriptionsController extends Controller
{
    public function store()
    {
        $podcast = Podcast::published()->findOrFail(request('podcast_id'));

        $subscription = Subscription::create([
            'user_id' => Auth::user()->id,
            'podcast_id' => $podcast->id,
        ]);

        return $subscription;
    }
}

So, for unsubscribing,

public function destroy($id)
{
    $subscription = Auth::user()->subscriptions()->findOrFail($id);

    $subscription->delete();

    return response('', 204);
}

Think of different states as different resources

The last custom actions we have are PodcastsController@publish and PodcastsController@unpublish.Here are the endpoints:

Route::post('/podcasts/{id}/publish',   'PodcastsController@publish');
Route::post('/podcasts/{id}/unpublish', 'PodcastsController@unpublish');

Again, they are not in standard CRUD, since we update published_at column, one possiblity is to use PodcastsController@update for it. But similar to Episodes, doing two different actions in one complicates the code. So, create a dedicated PublishedPodcastController.

In these situations, it can often be helpful to think of a resource in a certain state as it's own independent resource.

Just like subscribe, if publish becomes creating a published podcast, then unpublish could become destroying a published podcast.

So, here's what the PublishedPodcastsController looks like:

class PublishedPodcastsController extends Controller
{
    public function store()
    {
        $podcast = Auth::user()->podcasts()->findOrFail(request('podcast_id'));

        $podcast->publish();

        return $podcast;
    }

    public function destroy($id)
    {
        $podcast = Auth::user()->podcasts()->findOrFail($id);

        $podcast->unpublish();

        return $podcast;
    }
}

Conclusion

Laravel routes are a vital component of building web applications. They provide a clear and efficient way to define how your application responds to various HTTP requests. With features like route parameters, named routes, middleware, and route groups, Laravel offers a comprehensive and flexible routing system that simplifies the development process.

Credits

https://www.youtube.com/watch?v=MF0jFKvS4SI&list=LL&index=1&ab_channel=AdamWathan

https://github.com/adamwathan/laracon2017


91bytes logoCopyright 2024 - 91bytes technologies llp. All rights reserved.