Launch offer
Business website $150 USD Custom plugin $200 USD Ready in 5 days
Get a quote
Laravel

Laravel Complete Tutorial: Beginner to Advanced (27 Topics with Code Examples)

A complete beginner-friendly Laravel tutorial covering installation, routing, Blade, controllers, validation, Eloquent, migrations, relationships, middleware, localization, and more — with practical code examples.

Laravel Complete Tutorial: Beginner to Advanced (27 Topics with Code Examples) — a practical guide to Laravel complete tutorial with clear examples you can reuse in real projects.

This complete Laravel tutorial walks you through 27 essential topics — from installation to route model binding — with short explanations and clean, production-quality code examples. It is written for beginners, but useful as a quick reference for working Laravel developers.

What you will learn

Tip: Follow the topics in order if you are new to Laravel. Jump to any section from the list above if you already know the basics.

1. Introduction to Laravel and Installation

Short description

Laravel is a modern PHP framework that helps you build web applications faster with clean structure, routing, Blade templates, Eloquent ORM, validation, queues, and testing tools.

Steps

  1. Install PHP 8.2+ and Composer on your machine.
  2. Create a new Laravel project with Composer.
  3. Generate the application key and start the local server.

Create a Laravel project

composer create-project laravel/laravel blog-app
cd blog-app
php artisan key:generate
php artisan serve

Default welcome route

<?php

use IlluminateSupportFacadesRoute;

Route::get('/', function () {
    return view('welcome');
});

2. Directory Structure and Routing

Short description

Laravel keeps code organized. Routes map URLs to closures or controllers so browsers can open pages and APIs.

Steps

  1. Remember key folders: app/, routes/, resources/views/, database/, config/, and public/.
  2. Define routes in routes/web.php for browser pages.
  3. Prefer named routes so links stay stable when URLs change.

Basic and named routes

use AppHttpControllersPostController;
use IlluminateSupportFacadesRoute;

Route::get('/about', fn () => 'About page');

Route::get('/users/{id}', function (string $id) {
    return "User ID: {$id}";
})->name('users.show');

Route::get('/posts', [PostController::class, 'index'])->name('posts.index');

3. Blade Templates

Short description

Blade is Laravel’s templating engine. It supports layouts, escaping, loops, and conditions while compiling to plain PHP.

Steps

  1. Create a shared layout with @yield.
  2. Extend the layout from each page with @extends and @section.
  3. Always escape user input with {{ }} unless the HTML is trusted.

Layout + child view

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head><title>@yield('title', 'My App')</title></head>
<body>
    <main>@yield('content')</main>
</body>
</html>

{{-- resources/views/home.blade.php --}}
@extends('layouts.app')

@section('title', 'Home')

@section('content')
    <h1>Welcome, {{ $name }}</h1>

    @forelse ($posts as $post)
        <article>{{ $post->title }}</article>
    @empty
        <p>No posts yet.</p>
    @endforelse
@endsection

4. Controllers in Laravel

Short description

Controllers group request-handling logic so `routes/web.php` stays thin and readable.

Steps

  1. Generate a controller with Artisan.
  2. Move page logic into controller methods.
  3. Use resource controllers for standard CRUD routes.

Create and use a controller

php artisan make:controller PostController
php artisan make:controller ProductController --resource

Controller example

<?php

namespace AppHttpControllers;

use AppModelsPost;
use IlluminateHttpRequest;

class PostController extends Controller
{
    public function index()
    {
        $posts = Post::latest()->get();

        return view('posts.index', compact('posts'));
    }

    public function show(Post $post)
    {
        return view('posts.show', compact('post'));
    }

    public function store(Request $request)
    {
        // validate + save
    }
}

Route wiring

use AppHttpControllersPostController;
use AppHttpControllersProductController;

Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{post}', [PostController::class, 'show']);
Route::post('/posts', [PostController::class, 'store']);
Route::resource('products', ProductController::class);

5. Submitting Form Data and Validation

Short description

Laravel protects forms with CSRF tokens and validates input before you save anything to the database.

Steps

  1. Add @csrf inside every POST/PUT/PATCH/DELETE form.
  2. Validate with $request->validate() or a Form Request class.
  3. Redisplay old input and error messages after failed validation.

Blade form

<form method="POST" action="{{ route('posts.store') }}">
    @csrf
    <input type="text" name="title" value="{{ old('title') }}">
    @error('title') <span>{{ $message }}</span> @enderror

    <textarea name="body">{{ old('body') }}</textarea>
    @error('body') <span>{{ $message }}</span> @enderror

    <button type="submit">Save</button>
</form>

Controller validation

public function store(Request $request)
{
    $validated = $request->validate([
        'title' => ['required', 'string', 'max:255'],
        'body'  => ['required', 'string', 'min:10'],
    ]);

    Post::create($validated);

    return redirect()
        ->route('posts.index')
        ->with('success', 'Post created');
}

Form Request (cleaner)

// php artisan make:request StorePostRequest

public function rules(): array
{
    return [
        'title' => ['required', 'max:255'],
        'body'  => ['required'],
    ];
}

public function store(StorePostRequest $request)
{
    Post::create($request->validated());

    return back()->with('success', 'Saved');
}

6. Blade Components

Short description

Components let you reuse UI pieces (alerts, buttons, cards) with props and slots instead of copying HTML.

Steps

  1. Generate a class-based component or create an anonymous Blade component.
  2. Pass props and content slots from parent views.
  3. Merge attributes for flexible CSS classes.

Alert component

{{-- php artisan make:component Alert --}}
{{-- resources/views/components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
    {{ $slot }}
</div>

{{-- Usage --}}
<x-alert type="success">
    Profile updated successfully.
</x-alert>

Anonymous button component

{{-- resources/views/components/button.blade.php --}}
<button {{ $attributes->merge(['class' => 'btn']) }}>
    {{ $slot }}
</button>

<x-button class="btn-primary" type="submit">Save</x-button>

7. Database Configuration and Migrations

Short description

Migrations are version-controlled PHP files that create and change database tables safely across local, staging, and production.

Steps

  1. Set database credentials in .env.
  2. Create migrations with Artisan.
  3. Run php artisan migrate to apply schema changes.

.env database settings

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=blog_app
DB_USERNAME=root
DB_PASSWORD=

Migration skeleton

public function up(): void
{
    Schema::create('posts', function (Blueprint $table) {
        $table->id();
        $table->string('title');
        $table->text('body');
        $table->timestamps();
    });
}

public function down(): void
{
    Schema::dropIfExists('posts');
}

Useful migrate commands

php artisan migrate
php artisan migrate:status
php artisan migrate:rollback

8. Creating Tables Using Migrations

Short description

Use Blueprint helpers to define columns, defaults, unique indexes, foreign keys, and soft deletes when creating tables.

Steps

  1. Create a migration for the new table.
  2. Define columns and constraints clearly.
  3. Use a separate migration when altering an existing table.

Rich posts table

Schema::create('posts', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->string('title');
    $table->string('slug')->unique();
    $table->text('body');
    $table->boolean('is_published')->default(false);
    $table->timestamp('published_at')->nullable();
    $table->timestamps();
    $table->softDeletes();
});

Alter an existing table

// php artisan make:migration add_excerpt_to_posts_table --table=posts

Schema::table('posts', function (Blueprint $table) {
    $table->string('excerpt')->nullable()->after('title');
});

9. Models in Laravel (Eloquent)

Short description

An Eloquent model represents a database table. (This is often written as “Modal” by mistake — in Laravel it is the Model.)

Steps

  1. Generate a model (optionally with a migration).
  2. Set $fillable (or $guarded) for mass assignment safety.
  3. Cast attributes like booleans and dates.

Create model + migration

php artisan make:model Post -m

Post model

<?php

namespace AppModels;

use IlluminateDatabaseEloquentModel;

class Post extends Model
{
    protected $fillable = [
        'user_id',
        'title',
        'slug',
        'body',
        'is_published',
    ];

    protected $casts = [
        'is_published' => 'boolean',
        'published_at' => 'datetime',
    ];
}

Create and update records

Post::create([
    'user_id' => auth()->id(),
    'title'   => 'Hello Laravel',
    'slug'    => 'hello-laravel',
    'body'    => 'My first post',
]);

$post = Post::findOrFail(1);
$post->update(['title' => 'Updated title']);

10. Querying with Eloquent ORM

Short description

Eloquent gives a fluent API for everyday SQL: find, filter, sort, paginate, and eager-load related data.

Steps

  1. Start with simple all(), find(), and where() queries.
  2. Chain conditions for readable filters.
  3. Use with() to avoid N+1 query problems.

Common Eloquent queries

$all = Post::all();
$one = Post::find(1);
$post = Post::where('slug', 'hello-laravel')->first();

$published = Post::where('is_published', true)
    ->orderByDesc('published_at')
    ->limit(10)
    ->get();

$results = Post::where('title', 'like', '%laravel%')->get();
$posts = Post::with('user')->latest()->get();

11. Routing Through Buttons and Anchor Tags

Short description

Use the `route()` helper in Blade so links and buttons always point to the correct named route.

Steps

  1. Name your routes.
  2. Generate URLs with route('name', $model).
  3. Use method spoofing (@method) for PUT/PATCH/DELETE from HTML forms.

Links and delete button

<a href="{{ route('posts.index') }}">All posts</a>
<a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
<a class="btn" href="{{ route('posts.create') }}">Create post</a>

<form action="{{ route('posts.destroy', $post) }}" method="POST" onsubmit="return confirm('Delete?')">
    @csrf
    @method('DELETE')
    <button type="submit">Delete</button>
</form>

12. Configuring Custom Helpers

Short description

Custom helpers are small global PHP functions for formatting and other utilities you reuse across controllers and Blade views.

Steps

  1. Create app/helpers.php.
  2. Register it under Composer autoload.files.
  3. Run composer dump-autoload.

helpers.php

<?php

if (! function_exists('format_inr')) {
    function format_inr(float $amount): string
    {
        return '₹' . number_format($amount, 2);
    }
}

composer.json autoload

{
  "autoload": {
    "psr-4": { "App": "app/" },
    "files": ["app/helpers.php"]
  }
}

Usage

<p>Total: {{ format_inr(1299.5) }}</p>

13. Accessors and Mutators

Short description

Accessors format values when you read them. Mutators transform values before they are saved to the database.

Steps

  1. Use the Attribute API on your Eloquent model.
  2. Define get and/or set callbacks.
  3. Append computed attributes when needed for APIs/views.

Title accessor + mutator

use IlluminateDatabaseEloquentCastsAttribute;

protected function title(): Attribute
{
    return Attribute::make(
        get: fn (string $value) => ucfirst($value),
        set: fn (string $value) => strtolower($value),
    );
}

$post->title = 'HELLO WORLD'; // stored as "hello world"
echo $post->title;            // reads as "Hello world"

14. Handling Sessions

Short description

Sessions store temporary data across requests — flash messages, cart counts, locale preferences, and more.

Steps

  1. Store and read values with the session() helper.
  2. Flash success messages for the next request only.
  3. Choose a session driver in .env (file, database, or redis).

Session store, flash, and clear

session(['cart_count' => 3]);
$count = session('cart_count', 0);

return redirect()
    ->route('posts.index')
    ->with('success', 'Post created successfully');

session()->forget('cart_count');
session()->flush();

Flash message in Blade

@if (session('success'))
    <div class="alert">{{ session('success') }}</div>
@endif

15. Soft Deletes

Short description

Soft deletes mark a row as deleted (`deleted_at`) without permanently removing it, so you can restore later.

Steps

  1. Add $table->softDeletes() in a migration.
  2. Use the SoftDeletes trait on the model.
  3. Query trashed records with onlyTrashed() / withTrashed().

Soft delete setup and usage

use IlluminateDatabaseEloquentSoftDeletes;

class Post extends Model
{
    use SoftDeletes;
}

$post->delete();      // soft delete
$post->restore();     // undo
$post->forceDelete(); // permanent

$trashed = Post::onlyTrashed()->get();
$withTrashed = Post::withTrashed()->find(1);

16. Converting an HTML Template into a Laravel Project

Short description

Turn a static HTML template into Laravel by extracting a Blade layout, splitting pages into views, and wiring Vite assets.

Steps

  1. Create a Laravel project and copy CSS/JS/images into resources/ and public/.
  2. Extract header/footer into a layout and partials.
  3. Replace each HTML page with a Blade view and named route.
  4. Swap hard-coded paths for route() and asset() / @vite.

Layout extraction

<!DOCTYPE html>
<html>
<head>
    @vite(['resources/css/app.css', 'resources/js/app.js'])
    <title>@yield('title')</title>
</head>
<body>
    @include('partials.header')
    @yield('content')
    @include('partials.footer')
</body>
</html>

@extends('layouts.app')
@section('title', 'Services')
@section('content')
    <h1>Our Services</h1>
@endsection

17. HTML Package in Laravel

Short description

Older projects may use `laravelcollective/html` form helpers. New Laravel apps usually prefer plain Blade forms and components.

Steps

  1. Know Collective HTML if you maintain legacy code.
  2. Prefer native Blade + CSRF for new projects.
  3. Keep form markup consistent with your design system.

Legacy Collective form

{!! Form::open(['route' => 'posts.store']) !!}
    {!! Form::label('title', 'Title') !!}
    {!! Form::text('title', old('title'), ['class' => 'form-control']) !!}
    {!! Form::submit('Save') !!}
{!! Form::close() !!}

Modern Blade alternative (recommended)

<form method="POST" action="{{ route('posts.store') }}">
    @csrf
    <input name="title" value="{{ old('title') }}">
    <button type="submit">Save</button>
</form>

18. File Uploading, Seeders, and Faker

Short description

Upload user files to storage, and use factories/Faker plus seeders to fill your development database with realistic demo data.

Steps

  1. Validate and store uploads on the public disk.
  2. Run php artisan storage:link so files are web-accessible.
  3. Create factories and seeders for demo records.

File upload

public function store(Request $request)
{
    $request->validate([
        'avatar' => ['required', 'image', 'max:2048'],
    ]);

    $path = $request->file('avatar')->store('avatars', 'public');

    auth()->user()->update(['avatar_path' => $path]);

    return back()->with('success', 'Uploaded');
}

Factory + seeder

// PostFactory
public function definition(): array
{
    return [
        'title' => fake()->sentence(),
        'body'  => fake()->paragraphs(3, true),
    ];
}

// PostSeeder
Post::factory()->count(50)->create();

// php artisan db:seed --class=PostSeeder

19. Searching and Pagination

Short description

Search filters results with query parameters. Pagination splits large lists into pages with Laravel’s built-in paginator.

Steps

  1. Read the search term from the request.
  2. Apply a conditional where / like filter.
  3. Use paginate() and keep query strings with withQueryString().

Search + paginate controller

public function index(Request $request)
{
    $q = $request->string('q')->toString();

    $posts = Post::query()
        ->when($q !== '', function ($query) use ($q) {
            $query->where(function ($inner) use ($q) {
                $inner->where('title', 'like', "%{$q}%")
                      ->orWhere('body', 'like', "%{$q}%");
            });
        })
        ->latest()
        ->paginate(10)
        ->withQueryString();

    return view('posts.index', compact('posts', 'q'));
}

Blade search UI

<form method="GET" action="{{ route('posts.index') }}">
    <input type="search" name="q" value="{{ $q }}">
    <button type="submit">Search</button>
</form>

@foreach ($posts as $post)
    <h2>{{ $post->title }}</h2>
@endforeach

{{ $posts->links() }}

20. Grouping Routes

Short description

Route groups apply shared settings — prefix, name, middleware, or controller — to many routes at once.

Steps

  1. Group admin routes under a prefix and name.
  2. Apply auth middleware to protected areas.
  3. Use controller groups to reduce repetition.

Route groups

Route::prefix('admin')->name('admin.')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
    Route::resource('posts', AdminPostController::class);
});

Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/profile', [ProfileController::class, 'edit']);
});

Route::controller(PostController::class)->group(function () {
    Route::get('/posts', 'index');
    Route::post('/posts', 'store');
});

21. Localization

Short description

Localization stores language strings in files and displays the correct language based on the active locale.

Steps

  1. Create language files under lang/.
  2. Use __() or @lang in Blade.
  3. Set the locale from config, session, or middleware.

Language files and usage

// lang/en/messages.php
return ['welcome' => 'Welcome to our site'];

// lang/hi/messages.php
return ['welcome' => 'हमारी साइट पर आपका स्वागत है'];

App::setLocale('hi');
// or
session(['locale' => 'hi']);
app()->setLocale(session('locale', 'en'));

Blade translation

<h1>{{ __('messages.welcome') }}</h1>

22. Stubs in Laravel

Short description

Stubs are templates Artisan uses when generating classes. Customize them so every new file matches your team standards.

Steps

  1. Publish stubs with php artisan stub:publish.
  2. Edit files in the /stubs directory.
  3. Generate models/controllers as usual — Artisan uses your stubs.

Publish and use stubs

php artisan stub:publish
# edit stubs/model.stub (e.g. always include SoftDeletes)
php artisan make:model Comment

23. Laravel Migrations Deep Dive

Short description

Go beyond “create table”: learn rollbacks, batches, raw SQL, and safe habits for production deploys.

Steps

  1. Use rollback carefully and test on staging.
  2. Prefer additive, reversible migrations.
  3. Never run migrate:fresh on production.

Migration commands and tips

php artisan migrate
php artisan migrate:rollback
php artisan migrate:rollback --step=1
php artisan migrate:status
# Dev only:
php artisan migrate:fresh --seed

Raw statement example

DB::statement('ALTER TABLE posts ADD FULLTEXT(title, content)');

24. One-to-One and One-to-Many Relationships

Short description

Eloquent relationships describe how models connect — for example, a user has one profile and many posts.

Steps

  1. Define hasOne / belongsTo for one-to-one.
  2. Define hasMany / belongsTo for one-to-many.
  3. Eager-load relationships with with() when listing records.

One-to-one

// User.php
public function profile()
{
    return $this->hasOne(Profile::class);
}

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

One-to-many

// User.php
public function posts()
{
    return $this->hasMany(Post::class);
}

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

$user->posts()->create([
    'title' => 'New post',
    'body'  => 'Content here',
]);

Post::with('user')->get();

25. Middleware

Short description

Middleware filters HTTP requests before they reach your controller — perfect for auth, roles, and custom checks.

Steps

  1. Create middleware with Artisan.
  2. Return $next($request) when allowed, or abort/redirect when not.
  3. Attach middleware to routes or route groups.

Admin middleware

// php artisan make:middleware EnsureIsAdmin

public function handle(Request $request, Closure $next)
{
    if (! $request->user()?->is_admin) {
        abort(403);
    }

    return $next($request);
}

Route::get('/admin', [AdminController::class, 'index'])
    ->middleware(['auth', EnsureIsAdmin::class]);

26. Custom Artisan Commands

Short description

Custom Artisan commands automate maintenance tasks, imports, and reports from the command line — and can be scheduled.

Steps

  1. Generate a command class.
  2. Define $signature and $description.
  3. Implement handle() and optionally schedule the command.

Custom command

// php artisan make:command SendWeeklyReport

protected $signature = 'report:weekly {--email=}';
protected $description = 'Send the weekly report';

public function handle(): int
{
    $email = $this->option('email') ?: config('mail.from.address');
    $this->info("Sending weekly report to {$email}");
    // Report logic...
    $this->info('Done');

    return self::SUCCESS;
}

// php artisan report:weekly --email=admin@example.com
// Schedule::command('report:weekly')->weeklyOn(1, '9:00');

27. Route Model Binding

Short description

Route model binding injects an Eloquent model into your route/controller automatically — no manual `findOrFail` for every show page.

Steps

  1. Type-hint the model in the controller method.
  2. Optionally bind by slug with getRouteKeyName().
  3. Use custom bindings when you need extra query constraints.

Implicit binding

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

public function show(Post $post)
{
    return view('posts.show', compact('post'));
}

Bind by slug + custom binding

public function getRouteKeyName(): string
{
    return 'slug';
}

Route::bind('post', function (string $value) {
    return Post::where('slug', $value)
        ->where('is_published', true)
        ->firstOrFail();
});

Conclusion

You now have a practical path through Laravel’s core features: install the framework, define routes, build Blade views and components, validate forms, work with migrations and Eloquent, secure requests with middleware, and automate work with Artisan. Practice each topic in a small demo project — that is the fastest way to remember Laravel’s patterns.

Next steps: build a blog or small CRUD app that uses forms, validation, relationships, soft deletes, search, and pagination together. Once that feels comfortable, explore queues, jobs, policies, and testing.

Leave a reply

Your email address will not be published. Required fields are marked *