Laravel Controller Tutorial with Example

Laravel 12 controller tutorial — PHP class connecting routes to application logic

This tutorial covers how controllers work in Laravel 12: how to create one, define methods, connect them to routes, pass URL parameters, handle form input, apply middleware, and use resource controllers for CRUD operations.

Version note: The routing syntax and controller structure shown here applies to Laravel 9 through Laravel 12. The main difference from Laravel 8 is how middleware is defined inside a controller — covered in the middleware section below.

What is a Laravel Controller?

A controller is a PHP class that handles incoming HTTP requests. When a user hits a URL, Laravel’s router matches it to a controller method, which runs the appropriate logic, querying the database, processing a form, returning a view, or sending a JSON response.

Think of it as the traffic director in the MVC pattern: routes tell Laravel where to send a request, and the controller decides what to do with it.

Laravel Controllers find in app/Http/Controllers by default.

A practical example: a UserController might have one method to list users, another to show a single profile, one to save a new user, and one to delete, all grouped in a single, focused file.

How to Create a Controller in Laravel 12

From your project root, run:

php artisan make:controller LoginController

This creates app/Http/Controllers/LoginController.php with a bare class stub ready for your methods.

Basic Controller Structure

The generated file looks like this:

<?php

namespace App\Http\Controllers;

class LoginController extends Controller
{
    //
}

It extends Controller – the base class at app/Http/Controllers/Controller.php. In Laravel 12 this is optional, but it is still a good default to keep because it is where you would add shared methods across all your controllers.

Now add a public method:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Response;

class LoginController extends Controller
{
    /**
     * Show the login page.
     */
    public function show(): Response
    {
        return response('Hello from the Login Controller');
    }
}

The : Response return type hint is not required, but it is good practice, it makes the code self-documenting and helps static analysis tools catch mistakes early.

Read this: Laravel Interview Questions for Experienced Developers

Connecting a Controller Method to a Route

Open routes/web.php and register a route pointing to the show() method:

<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\LoginController;

Route::get('/login', [LoginController::class, 'show']);

The array syntax [LoginController::class, 'show'] is the standard way to reference a controller method in Laravel 8 and later. The first element is the fully qualified class name, the second is the method name.

Visit http://localhost/login (or your equivalent local URL) and you will see the response.

Returning a View

Most laravel controller methods return a Blade view rather than a plain string:

<?php

namespace App\Http\Controllers;

use Illuminate\View\View;

class LoginController extends Controller
{
    /**
     * Show the login form.
     */
    public function show(): View
    {
        return view('auth.login');
    }
}

This renders the Blade template at resources/views/auth/login.blade.php. You can pass data to the view as a second argument:

return view('auth.login', ['title' => 'Sign In']);

Inside the Blade file, $title is then available automatically.

Passing URL Parameters to a Controller

To capture a value from the URL – such as a user ID – define a route parameter with curly braces, then declare it as an argument in the method:

Route:

Route::get('/user/{id}', [LoginController::class, 'show']);

Controller method:

public function show(string $id): Response
{
    return response('User ID: ' . $id);
}

Visiting /user/42 passes 42 into $id.

Multiple parameters work the same way – just make sure their order in the method signature matches the order in the URI:

// Route
Route::get('/post/{category}/{id}', [PostController::class, 'show']);

// Controller method
public function show(string $category, string $id): Response
{
    // both values are available here
}

Check our detailed guide on: Laravel REST API with Sanctum: Complete Tutorial

Resource Laravel Controllers (CRUD Example)

When building a standard CRUD feature, a resource controller gives you all seven actions wired up automatically.

Generate one:

php artisan make:controller PostController --resource

Register it with a single route declaration:

use App\Http\Controllers\PostController;

Route::resource('posts', PostController::class);

That one line registers seven routes:

HTTP VerbURIController MethodNamed RoutePurpose
GET/postsindex()posts.indexList all posts
GET/posts/createcreate()posts.createShow create form
POST/postsstore()posts.storeSave a new post
GET/posts/{post}show()posts.showShow a single post
GET/posts/{post}/editedit()posts.editShow edit form
PUT/PATCH/posts/{post}update()posts.updateSave changes to a post
DELETE/posts/{post}destroy()posts.destroyDelete a post

Verify registered routes at any time:

php artisan route:list

Limiting Resource Routes

If your feature only needs some of these, use only() or except():

// Only these two routes
Route::resource('posts', PostController::class)->only(['index', 'show']);

// All routes except delete
Route::resource('posts', PostController::class)->except(['destroy']);

API Resource Controllers

For APIs, skip the create and edit routes (which exist only to render HTML forms) using apiResource:

php artisan make:controller PostController --api
Route::apiResource('posts', PostController::class);

Register multiple at once:

Route::apiResources([
    'posts'    => PostController::class,
    'comments' => CommentController::class,
]);

Applying Middleware to Controllers

In Laravel 12 (Recommended)

Laravel 12 uses the HasMiddleware interface to define middleware inside a controller. This replaces the $this->middleware() constructor call used in Laravel 8:

<?php

namespace App\Http\Controllers;

use Illuminate\Routing\Controllers\HasMiddleware;
use Illuminate\Routing\Controllers\Middleware;

class LoginController extends Controller implements HasMiddleware
{
    /**
     * Define middleware for this controller's actions.
     */
    public static function middleware(): array
    {
        return [
            'auth',                                          // applies to all methods
            new Middleware('verified', only: ['store']),     // only the store method
            new Middleware('throttle:5,1', except: ['show']), // all except show
        ];
    }

    public function show(): Response { /* ... */ }

    public function store(Request $request): RedirectResponse { /* ... */ }
}

On the Route (Alternative)

You can also apply middleware directly to the route definition, useful for one-off cases:

Route::get('/login', [LoginController::class, 'show'])->middleware('guest');

Or on a resource route targeting specific methods:

Route::resource('posts', PostController::class)
    ->middleware(['auth', 'verified']);

Migrating from Laravel 8? If your controllers use $this->middleware('auth') inside __construct(), replace that with the HasMiddleware interface shown above. The constructor approach is deprecated in newer releases.

Dependency Injection in Controllers

Laravel’s service container resolves type-hinted dependencies automatically – both in constructors and in methods.

Constructor Injection

Use this when a dependency is needed across multiple methods in the same controller:

<?php

namespace App\Http\Controllers;

use App\Services\UserService;
use Illuminate\View\View;

class UserController extends Controller
{
    public function __construct(
        protected UserService $userService
    ) {}

    public function index(): View
    {
        $users = $this->userService->getAll();
        return view('users.index', compact('users'));
    }

    public function show(string $id): View
    {
        $user = $this->userService->findOrFail($id);
        return view('users.show', compact('user'));
    }
}

Method Injection

Use this when a dependency is only needed for a single method:

public function store(Request $request, UserService $userService): RedirectResponse
{
    $userService->create($request->validated());
    return redirect('/users');
}

Combining Injected Dependencies with Route Parameters

If a method needs both a type-hinted dependency and a route parameter, list the dependencies first, the route parameter name must match what is defined in the route:

// Route
Route::put('/user/{id}', [UserController::class, 'update']);

// Controller method — Request is injected, $id comes from the route
public function update(Request $request, string $id): RedirectResponse
{
    // update logic...
    return redirect('/users');
}

Common Errors and How to Fix Them

Target class [App\Http\Controllers\LoginController] does not exist

The class name in the use statement in web.php does not match the actual file. Check for typos in the namespace or class name. Also run composer dump-autoload if you created the file manually instead of with Artisan.

405 Method Not Allowed

The HTTP verb your request is using (GET, POST, etc.) does not match what the route expects. Check your form’s method attribute and confirm the route is using the right verb (Route::post, Route::put, etc.).

Route [posts.index] not defined

The named route does not exist — either it was not registered, or the name is wrong. Run php artisan route:list --name=posts to see all routes with “posts” in the name.

Class "App\Http\Controllers\LoginController" not found after moving the file

The file’s namespace declaration must match its directory. If the file is in app/Http/Controllers/Auth/, the namespace must be App\Http\Controllers\Auth, not App\Http\Controllers. Run composer dump-autoload after fixing the namespace.

Similar Posts