Toolmingo
Guides21 min read

Laravel Tutorial for Beginners (2025): Learn Laravel Step by Step

Complete Laravel tutorial for absolute beginners. Learn Laravel from scratch: routing, controllers, Eloquent ORM, Blade templates, authentication, APIs, and build real projects. Free PHP guide with code examples.

Laravel is the most popular PHP framework in the world. It takes the pain out of web development by providing elegant syntax, powerful tools, and a thriving ecosystem — letting you build anything from simple blogs to large-scale enterprise applications.

This tutorial takes you from zero to building complete web applications and REST APIs with Laravel, covering everything you need for real-world development.

What you'll learn

Topic What you'll be able to do
Setup Install Laravel and run your first app
Routing Define routes for web and API
Controllers Handle HTTP requests with controllers
Blade Build dynamic HTML with Blade templates
Eloquent ORM Query a database with expressive syntax
Migrations Version-control your database schema
Authentication Add login/register in minutes with Laravel Breeze
Middleware Protect routes and transform requests
APIs Build a JSON REST API
Real projects 2 complete production-ready projects

Why Laravel?

Laravel Symfony CodeIgniter Slim Raw PHP
Learning curve Medium Steep Low Low None/Chaotic
Eloquent ORM Built-in Doctrine None None None
CLI (Artisan) Yes Yes (Console) Limited No No
Auth scaffolding Yes (Breeze/Jetstream) Limited No No No
Ecosystem Massive Large Small Minimal None
Best for Web apps, APIs, SaaS, e-commerce Enterprise, APIs Simple projects Micro-APIs Never
Job market 2025 #1 PHP framework #2 Declining Niche

Laravel wins because it combines developer happiness (expressive syntax, great docs) with production power (queues, caching, broadcasting, Horizon, Octane).


Prerequisites

Before starting, you should know:

  • PHP basics (variables, functions, arrays, classes)
  • HTML/CSS fundamentals
  • Basic SQL (SELECT, INSERT, WHERE)
  • Command line basics (cd, mkdir, ls)

1. Setup: Your First Laravel App

Requirements

  • PHP 8.2+
  • Composer (PHP package manager)
  • MySQL or PostgreSQL

Install Laravel via Composer

composer create-project laravel/laravel my-app
cd my-app
php artisan serve

Visit http://localhost:8000 — you'll see the Laravel welcome page.

Project Structure

my-app/
├── app/
│   ├── Http/
│   │   ├── Controllers/     # Handle HTTP requests
│   │   └── Middleware/      # Request/response filters
│   └── Models/              # Eloquent models
├── bootstrap/               # App bootstrapping
├── config/                  # Configuration files
├── database/
│   ├── migrations/          # Database schema versions
│   └── seeders/             # Test data seeders
├── public/                  # Web root (index.php, assets)
├── resources/
│   ├── views/               # Blade templates
│   └── css/, js/            # Frontend assets
├── routes/
│   ├── web.php              # Web routes
│   └── api.php              # API routes
├── storage/                 # Logs, cache, uploads
├── tests/                   # Unit and feature tests
├── .env                     # Environment variables
└── artisan                  # CLI tool

Environment Configuration

.env stores your app's secrets — never commit this file:

APP_NAME=MyApp
APP_ENV=local
APP_KEY=base64:...        # Generated automatically
APP_DEBUG=true
APP_URL=http://localhost

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=myapp
DB_USERNAME=root
DB_PASSWORD=secret

Generate app key (do this on every fresh install):

php artisan key:generate

2. Routing

Routes map HTTP requests to code. All web routes live in routes/web.php.

Basic Routes

use Illuminate\Support\Facades\Route;

// Return a string
Route::get('/', function () {
    return 'Hello, Laravel!';
});

// Return JSON
Route::get('/api/status', function () {
    return response()->json(['status' => 'ok']);
});

// Route with parameter
Route::get('/users/{id}', function (int $id) {
    return "User: {$id}";
});

// Optional parameter
Route::get('/posts/{slug?}', function (?string $slug = null) {
    return $slug ?? 'all posts';
});

HTTP Method Routes

Route::get('/posts', [PostController::class, 'index']);
Route::post('/posts', [PostController::class, 'store']);
Route::get('/posts/{id}', [PostController::class, 'show']);
Route::put('/posts/{id}', [PostController::class, 'update']);
Route::delete('/posts/{id}', [PostController::class, 'destroy']);

// Shorthand: all 7 RESTful routes at once
Route::resource('posts', PostController::class);

Named Routes

Route::get('/posts/{id}', [PostController::class, 'show'])->name('posts.show');

// Generate URL by name
$url = route('posts.show', ['id' => 42]);  // /posts/42

// Redirect to named route
return redirect()->route('posts.index');

Route Groups

// Prefix group
Route::prefix('admin')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'dashboard']);
    Route::get('/users', [AdminController::class, 'users']);
});
// URLs: /admin/dashboard, /admin/users

// Middleware group
Route::middleware(['auth'])->group(function () {
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::get('/settings', [SettingsController::class, 'show']);
});

// Prefix + middleware combined
Route::prefix('admin')
    ->middleware(['auth', 'admin'])
    ->name('admin.')
    ->group(function () {
        Route::get('/dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
        // URL: /admin/dashboard, name: admin.dashboard
    });

List All Routes

php artisan route:list

3. Controllers

Controllers group related request-handling logic into a class.

Create a Controller

php artisan make:controller PostController
# With resource methods:
php artisan make:controller PostController --resource

Basic Controller

<?php
// app/Http/Controllers/PostController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
    // GET /posts
    public function index()
    {
        return view('posts.index', ['posts' => Post::all()]);
    }

    // GET /posts/{id}
    public function show(int $id)
    {
        $post = Post::findOrFail($id);
        return view('posts.show', compact('post'));
    }

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

        $post = Post::create($validated);

        return redirect()->route('posts.show', $post->id)
                         ->with('success', 'Post created!');
    }

    // PUT /posts/{id}
    public function update(Request $request, Post $post)  // Route model binding
    {
        $validated = $request->validate([
            'title' => 'required|string|max:255',
            'body'  => 'required|string',
        ]);

        $post->update($validated);

        return redirect()->route('posts.show', $post->id)
                         ->with('success', 'Post updated!');
    }

    // DELETE /posts/{id}
    public function destroy(Post $post)
    {
        $post->delete();
        return redirect()->route('posts.index')
                         ->with('success', 'Post deleted!');
    }
}

Route Model Binding

Laravel automatically injects the model when the parameter name matches:

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

public function show(Post $post)  // Injects Post with that ID automatically
{
    return view('posts.show', compact('post'));
    // 404 automatically if not found
}

Request Validation

// Inline validation
$validated = $request->validate([
    'title'    => 'required|string|max:255',
    'body'     => 'required|string|min:10',
    'email'    => 'required|email|unique:users',
    'age'      => 'required|integer|min:18|max:120',
    'image'    => 'nullable|image|mimes:jpg,png,webp|max:2048',
    'tags'     => 'array',
    'tags.*'   => 'string|max:50',
]);

// Create a Form Request (reusable)
php artisan make:request StorePostRequest
<?php
// app/Http/Requests/StorePostRequest.php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class StorePostRequest extends FormRequest
{
    public function authorize(): bool
    {
        return true; // Or check $this->user()->can(...)
    }

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

    public function messages(): array
    {
        return [
            'title.required' => 'Post title cannot be empty.',
        ];
    }
}

// Usage in controller:
public function store(StorePostRequest $request)
{
    // $request->validated() contains only validated data
    Post::create($request->validated());
}

4. Blade Templates

Blade is Laravel's templating engine. Files use the .blade.php extension and live in resources/views/.

Blade Syntax

{{-- This is a comment --}}

{{-- Output (auto-escaped) --}}
<h1>{{ $title }}</h1>

{{-- Unescaped (dangerous — only for trusted HTML) --}}
<div>{!! $html !!}</div>

{{-- Conditionals --}}
@if ($user->isAdmin())
    <span>Admin</span>
@elseif ($user->isModerator())
    <span>Moderator</span>
@else
    <span>User</span>
@endif

{{-- Unless --}}
@unless ($user->isPremium())
    <a href="/upgrade">Upgrade</a>
@endunless

{{-- Loops --}}
@foreach ($posts as $post)
    <article>
        <h2>{{ $post->title }}</h2>
        <p>{{ $loop->index }} of {{ $loop->count }}</p>
        @if ($loop->first) <span>First post!</span> @endif
    </article>
@endforeach

@forelse ($posts as $post)
    <li>{{ $post->title }}</li>
@empty
    <li>No posts found.</li>
@endforelse

{{-- $loop variable properties --}}
{{-- $loop->index, $loop->iteration, $loop->first, $loop->last, $loop->count --}}

Layouts with @extends and @section

{{-- resources/views/layouts/app.blade.php --}}
<!DOCTYPE html>
<html>
<head>
    <title>@yield('title', 'My App')</title>
    @stack('styles')
</head>
<body>
    <nav>
        <a href="/">Home</a>
        <a href="/posts">Posts</a>
    </nav>

    <main>
        @yield('content')
    </main>

    <footer>&copy; 2025 My App</footer>
    @stack('scripts')
</body>
</html>
{{-- resources/views/posts/index.blade.php --}}
@extends('layouts.app')

@section('title', 'All Posts')

@section('content')
    <h1>Posts</h1>
    @foreach ($posts as $post)
        <div>
            <h2><a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a></h2>
            <p>{{ $post->excerpt }}</p>
        </div>
    @endforeach
@endsection

@push('scripts')
    <script src="/js/posts.js"></script>
@endpush

Components (Laravel 8+)

php artisan make:component Alert
<?php
// app/View/Components/Alert.php
namespace App\View\Components;

use Illuminate\View\Component;

class Alert extends Component
{
    public function __construct(
        public string $type = 'info',
        public string $message = ''
    ) {}

    public function render()
    {
        return view('components.alert');
    }
}
{{-- resources/views/components/alert.blade.php --}}
<div class="alert alert-{{ $type }}">
    {{ $message }}
    {{ $slot }}  {{-- Default slot for arbitrary content --}}
</div>
{{-- Usage --}}
<x-alert type="success" message="Saved!" />

<x-alert type="danger">
    Something went wrong. <a href="#">Try again</a>.
</x-alert>

5. Eloquent ORM

Eloquent is Laravel's ActiveRecord ORM. Each model maps to a database table.

Create a Model + Migration

php artisan make:model Post -m   # -m creates migration too
php artisan make:model Post -mcr  # -c controller, -r resource

Migration

<?php
// database/migrations/2025_01_01_000000_create_posts_table.php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();                          // Auto-increment BIGINT primary key
            $table->foreignId('user_id')->constrained()->cascadeOnDelete();
            $table->string('title');               // VARCHAR(255)
            $table->string('slug')->unique();
            $table->text('body');
            $table->text('excerpt')->nullable();
            $table->boolean('published')->default(false);
            $table->timestamp('published_at')->nullable();
            $table->timestamps();                  // created_at + updated_at
            $table->softDeletes();                 // deleted_at (soft delete support)
        });
    }

    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};
php artisan migrate          # Run pending migrations
php artisan migrate:status   # Show migration status
php artisan migrate:rollback # Rollback last batch
php artisan migrate:fresh    # Drop all + re-run (dev only!)

Model Definition

<?php
// app/Models/Post.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory;

class Post extends Model
{
    use HasFactory, SoftDeletes;

    // Mass-assignable fields
    protected $fillable = ['title', 'slug', 'body', 'excerpt', 'published', 'published_at', 'user_id'];

    // Cast types automatically
    protected $casts = [
        'published'    => 'boolean',
        'published_at' => 'datetime',
    ];

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

    public function comments(): HasMany
    {
        return $this->hasMany(Comment::class);
    }

    public function tags(): BelongsToMany
    {
        return $this->belongsToMany(Tag::class);
    }

    // Local scope
    public function scopePublished($query)
    {
        return $query->where('published', true)
                     ->whereNotNull('published_at');
    }

    // Accessor
    public function getExcerptAttribute(): string
    {
        return $this->attributes['excerpt'] ?? substr($this->body, 0, 150) . '...';
    }
}

Eloquent Queries

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

// READ
Post::all();                           // All posts (use with caution on large tables)
Post::find(1);                         // By primary key (null if not found)
Post::findOrFail(1);                   // By primary key (throws 404 if not found)
Post::where('published', true)->get(); // Collection
Post::where('published', true)->first(); // Single model

// Chaining
Post::published()                      // Use scope
    ->with('user', 'tags')             // Eager load (prevents N+1)
    ->orderBy('published_at', 'desc')
    ->paginate(15);                    // Paginate (returns LengthAwarePaginator)

// UPDATE
$post->update(['title' => 'New Title']);          // On instance
Post::where('published', false)->update(['published' => true]); // Bulk

// DELETE
$post->delete();    // Soft delete if model uses SoftDeletes
$post->forceDelete(); // Permanent delete

// RESTORE (soft deletes)
Post::withTrashed()->find(1)->restore();
Post::onlyTrashed()->get(); // Only deleted records

Eloquent Column Types Reference

Type Migration Method PHP Type After Cast
Auto-increment PK $table->id() int
Integer $table->integer('count') int (with cast)
String $table->string('name') string
Long text $table->text('body') string
Decimal $table->decimal('price', 10, 2) float (with cast)
Boolean $table->boolean('active') bool (with cast)
Datetime $table->timestamp('published_at') Carbon (with cast)
JSON $table->json('metadata') array (with cast)
Foreign key $table->foreignId('user_id')->constrained() int

Relationships

// ONE-TO-MANY: User has many Posts
// User model:
public function posts(): HasMany
{
    return $this->hasMany(Post::class);
}

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

// Usage:
$user->posts;          // Collection of posts
$post->user;           // User instance
$user->posts()->create(['title' => 'New', ...]); // Create via relationship

// MANY-TO-MANY: Post has many Tags, Tag has many Posts
// Requires pivot table: post_tag (post_id, tag_id)
$post->tags()->attach($tagId);        // Add tag
$post->tags()->detach($tagId);        // Remove tag
$post->tags()->sync([1, 2, 3]);       // Replace all tags
$post->tags;                          // Collection of tags

// HAS-MANY-THROUGH: Country → Users → Posts
public function posts(): HasManyThrough
{
    return $this->hasManyThrough(Post::class, User::class);
}

Eager Loading (Prevent N+1)

// BAD — N+1 problem: 1 query for posts + N queries for each user
$posts = Post::all();
foreach ($posts as $post) {
    echo $post->user->name; // Query per iteration
}

// GOOD — Eager load: 2 queries total
$posts = Post::with('user')->get();
foreach ($posts as $post) {
    echo $post->user->name; // No extra query
}

// Multiple relationships
$posts = Post::with(['user', 'tags', 'comments.user'])->get();

// Conditional eager loading
$posts = Post::with(['comments' => function ($query) {
    $query->where('approved', true)->orderBy('created_at', 'desc');
}])->get();

6. Seeders and Factories

Factory

php artisan make:factory PostFactory
<?php
// database/factories/PostFactory.php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;

class PostFactory extends Factory
{
    public function definition(): array
    {
        $title = fake()->sentence(4);
        return [
            'user_id'      => \App\Models\User::factory(),
            'title'        => $title,
            'slug'         => Str::slug($title),
            'body'         => fake()->paragraphs(5, true),
            'published'    => fake()->boolean(70), // 70% true
            'published_at' => fake()->optional()->dateTimeBetween('-1 year', 'now'),
        ];
    }
}

Seeder

php artisan make:seeder PostSeeder
<?php
// database/seeders/PostSeeder.php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Post;

class PostSeeder extends Seeder
{
    public function run(): void
    {
        Post::factory()->count(50)->create();
        Post::factory()->count(10)->create(['published' => false]);
    }
}
php artisan db:seed                    # Run DatabaseSeeder
php artisan db:seed --class=PostSeeder # Run specific seeder
php artisan migrate:fresh --seed       # Fresh migrate + seed

7. Authentication with Laravel Breeze

Laravel Breeze scaffolds complete auth (login, register, password reset) in minutes.

composer require laravel/breeze --dev
php artisan breeze:install blade  # Or: react, vue, api
npm install && npm run build
php artisan migrate

This generates:

  • routes/auth.php — auth routes (login, register, logout, password reset)
  • app/Http/Controllers/Auth/ — auth controllers
  • resources/views/auth/ — Blade views
  • Middleware integration

Protecting Routes

// routes/web.php
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard');
    Route::resource('posts', PostController::class)->except(['index', 'show']);
});

Accessing the Authenticated User

use Illuminate\Support\Facades\Auth;

// In controller
$user = Auth::user();
$user = auth()->user();
$userId = auth()->id();
$isLoggedIn = auth()->check();

// In Blade
@auth
    <span>Welcome, {{ auth()->user()->name }}!</span>
    <a href="{{ route('logout') }}">Logout</a>
@endauth

@guest
    <a href="{{ route('login') }}">Login</a>
@endguest

8. Middleware

Middleware filters HTTP requests entering your application.

php artisan make:middleware EnsureUserIsAdmin
<?php
// app/Http/Middleware/EnsureUserIsAdmin.php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class EnsureUserIsAdmin
{
    public function handle(Request $request, Closure $next): Response
    {
        if (! $request->user()?->isAdmin()) {
            return redirect('/dashboard')->with('error', 'Access denied.');
        }

        return $next($request);
    }
}

Register and use:

// bootstrap/app.php (Laravel 11)
->withMiddleware(function (Middleware $middleware) {
    $middleware->alias([
        'admin' => EnsureUserIsAdmin::class,
    ]);
})

// routes/web.php
Route::middleware(['auth', 'admin'])->prefix('admin')->group(function () {
    Route::get('/dashboard', [AdminController::class, 'dashboard']);
});

Built-in Middleware

Middleware Purpose
auth Require authenticated user
auth:sanctum API token authentication
guest Redirect authenticated users
verified Require email verification
throttle:60,1 Rate limit (60 req/min)
cache.headers Set HTTP cache headers

9. Building a REST API

Laravel makes building APIs straightforward. Use routes/api.php (routes are prefixed /api automatically).

API Controller

php artisan make:controller Api/PostController --api
<?php
// app/Http/Controllers/Api/PostController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Http\Resources\PostResource;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class PostController extends Controller
{
    public function index(): JsonResponse
    {
        $posts = Post::with('user')
            ->published()
            ->paginate(15);

        return PostResource::collection($posts)->response();
    }

    public function show(Post $post): JsonResponse
    {
        return new PostResource($post->load('user', 'tags'));
    }

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

        $post = $request->user()->posts()->create($validated);

        return (new PostResource($post))
            ->response()
            ->setStatusCode(201);
    }

    public function update(Request $request, Post $post): JsonResponse
    {
        $this->authorize('update', $post); // Policy check

        $validated = $request->validate([
            'title' => 'sometimes|string|max:255',
            'body'  => 'sometimes|string',
        ]);

        $post->update($validated);

        return new PostResource($post);
    }

    public function destroy(Post $post): JsonResponse
    {
        $this->authorize('delete', $post);
        $post->delete();
        return response()->json(null, 204);
    }
}

API Resources (Response Transformation)

php artisan make:resource PostResource
<?php
// app/Http/Resources/PostResource.php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class PostResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id'           => $this->id,
            'title'        => $this->title,
            'slug'         => $this->slug,
            'excerpt'      => $this->excerpt,
            'published_at' => $this->published_at?->toISOString(),
            'author'       => [
                'id'   => $this->user->id,
                'name' => $this->user->name,
            ],
            'tags'         => $this->whenLoaded('tags', fn () =>
                $this->tags->pluck('name')
            ),
            'created_at'   => $this->created_at->toISOString(),
        ];
    }
}

API Authentication with Sanctum

composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
php artisan migrate
// routes/api.php
use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful;

Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/user', fn (Request $r) => $r->user());
    Route::post('/logout', [AuthController::class, 'logout']);
    Route::apiResource('posts', PostController::class);
});
<?php
// app/Http/Controllers/Api/AuthController.php

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
    public function register(Request $request)
    {
        $validated = $request->validate([
            'name'     => 'required|string|max:255',
            'email'    => 'required|email|unique:users',
            'password' => 'required|min:8|confirmed',
        ]);

        $user = User::create([
            'name'     => $validated['name'],
            'email'    => $validated['email'],
            'password' => Hash::make($validated['password']),
        ]);

        $token = $user->createToken('api')->plainTextToken;

        return response()->json(['token' => $token, 'user' => $user], 201);
    }

    public function login(Request $request)
    {
        $request->validate([
            'email'    => 'required|email',
            'password' => 'required',
        ]);

        $user = User::where('email', $request->email)->first();

        if (! $user || ! Hash::check($request->password, $user->password)) {
            throw ValidationException::withMessages([
                'email' => ['The provided credentials are incorrect.'],
            ]);
        }

        $token = $user->createToken('api')->plainTextToken;

        return response()->json(['token' => $token, 'user' => $user]);
    }

    public function logout(Request $request)
    {
        $request->user()->currentAccessToken()->delete();
        return response()->json(['message' => 'Logged out']);
    }
}

10. Artisan Commands

Artisan is Laravel's CLI. Run php artisan list for all commands.

Command Description
php artisan serve Start dev server
php artisan make:model Post -mcr Model + migration + controller (resource)
php artisan make:controller PostController Create controller
php artisan make:middleware AdminOnly Create middleware
php artisan make:request StorePostRequest Form request
php artisan make:resource PostResource API resource
php artisan make:seeder PostSeeder Database seeder
php artisan make:factory PostFactory Model factory
php artisan make:policy PostPolicy --model=Post Authorization policy
php artisan make:job SendEmail Queue job
php artisan make:event PostPublished Event
php artisan make:listener NotifySubscribers Event listener
php artisan migrate Run migrations
php artisan migrate:fresh --seed Reset + seed DB
php artisan db:seed Run seeders
php artisan route:list List all routes
php artisan route:cache Cache routes (production)
php artisan config:cache Cache config (production)
php artisan queue:work Start queue worker
php artisan tinker Interactive REPL

Custom Artisan Command

php artisan make:command SendDailyDigest
<?php
// app/Console/Commands/SendDailyDigest.php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class SendDailyDigest extends Command
{
    protected $signature = 'digest:send {--dry-run : Preview without sending}';
    protected $description = 'Send daily email digest to all subscribers';

    public function handle(): int
    {
        $this->info('Starting digest...');

        if ($this->option('dry-run')) {
            $this->warn('DRY RUN — no emails sent.');
            return Command::SUCCESS;
        }

        // ... actual logic

        $this->info('Done!');
        return Command::SUCCESS;
    }
}
php artisan digest:send
php artisan digest:send --dry-run

11. Queues and Jobs

Queues let you defer slow tasks (emails, image processing) to background workers.

# Setup queue table (for database driver)
php artisan queue:table
php artisan migrate

# Create a job
php artisan make:job SendWelcomeEmail
<?php
// app/Jobs/SendWelcomeEmail.php

namespace App\Jobs;

use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3;
    public int $timeout = 60;

    public function __construct(public User $user) {}

    public function handle(): void
    {
        Mail::to($this->user)->send(new WelcomeMail($this->user));
    }

    public function failed(\Throwable $exception): void
    {
        // Log failure, notify admin...
    }
}
// Dispatch a job
SendWelcomeEmail::dispatch($user);
SendWelcomeEmail::dispatch($user)->delay(now()->addMinutes(5));
SendWelcomeEmail::dispatch($user)->onQueue('emails');

// Run the worker
php artisan queue:work
php artisan queue:work --queue=emails --tries=3

12. Testing

Laravel is built for testing with PHPUnit and its own TestCase.

php artisan make:test PostTest --unit   # Unit test
php artisan make:test PostTest          # Feature test
php artisan test                        # Run all tests
php artisan test --filter=PostTest      # Run specific test

Feature Test Example

<?php
// tests/Feature/PostTest.php

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostTest extends TestCase
{
    use RefreshDatabase; // Wraps each test in a DB transaction

    public function test_guests_cannot_create_posts(): void
    {
        $response = $this->postJson('/api/posts', [
            'title' => 'Test Post',
            'body'  => 'Test body content here.',
        ]);

        $response->assertStatus(401);
    }

    public function test_authenticated_user_can_create_post(): void
    {
        $user = User::factory()->create();

        $response = $this->actingAs($user, 'sanctum')
            ->postJson('/api/posts', [
                'title' => 'My New Post',
                'body'  => 'Some content here...',
            ]);

        $response->assertStatus(201)
                 ->assertJsonPath('data.title', 'My New Post');

        $this->assertDatabaseHas('posts', [
            'title'   => 'My New Post',
            'user_id' => $user->id,
        ]);
    }

    public function test_post_list_returns_only_published(): void
    {
        Post::factory()->count(5)->create(['published' => true]);
        Post::factory()->count(3)->create(['published' => false]);

        $response = $this->getJson('/api/posts');

        $response->assertStatus(200)
                 ->assertJsonCount(5, 'data');
    }

    public function test_user_cannot_delete_others_post(): void
    {
        $owner = User::factory()->create();
        $attacker = User::factory()->create();
        $post = Post::factory()->for($owner)->create();

        $response = $this->actingAs($attacker, 'sanctum')
            ->deleteJson("/api/posts/{$post->id}");

        $response->assertStatus(403);
        $this->assertDatabaseHas('posts', ['id' => $post->id]);
    }
}

Unit Test Example

<?php
// tests/Unit/PostTest.php

namespace Tests\Unit;

use App\Models\Post;
use Tests\TestCase;

class PostTest extends TestCase
{
    public function test_excerpt_defaults_to_truncated_body(): void
    {
        $post = new Post([
            'body' => str_repeat('a', 300),
        ]);

        $this->assertStringEndsWith('...', $post->excerpt);
        $this->assertLessThanOrEqual(153, strlen($post->excerpt));
    }
}

Project 1: Blog API

Build a complete blog REST API with CRUD, auth, and pagination.

laravel new blog-api
cd blog-api
composer require laravel/sanctum
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
// database/migrations: create posts, comments, tags, post_tag tables
// Models: User, Post (hasMany Comments, belongsToMany Tags), Comment, Tag
// Controllers: Api\AuthController, Api\PostController, Api\CommentController
// Resources: PostResource, CommentResource
// routes/api.php:

Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);

Route::middleware('auth:sanctum')->group(function () {
    Route::post('/logout', [AuthController::class, 'logout']);
    Route::apiResource('posts', PostController::class);
    Route::apiResource('posts.comments', CommentController::class)->shallow();
});

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

Key endpoints:

GET    /api/posts              List published posts (paginated)
POST   /api/posts              Create post (auth required)
GET    /api/posts/{id}         Get single post
PUT    /api/posts/{id}         Update post (author only)
DELETE /api/posts/{id}         Delete post (author only)
GET    /api/posts/{id}/comments List comments
POST   /api/posts/{id}/comments Add comment (auth required)

Project 2: Task Manager (Web App)

Full web app with Blade UI, auth, and CRUD.

laravel new task-manager
cd task-manager
composer require laravel/breeze --dev
php artisan breeze:install blade
npm install && npm run build
// Migration: tasks (user_id, title, description, status enum, due_date, priority)
// Model: Task (belongsTo User, scopes: pending/completed/overdue)
// Controller: TaskController (full CRUD + mark complete)
// Views: tasks/index.blade.php (list with filter), tasks/create.blade.php, tasks/edit.blade.php

// routes/web.php
Route::middleware('auth')->group(function () {
    Route::resource('tasks', TaskController::class);
    Route::patch('/tasks/{task}/complete', [TaskController::class, 'complete'])->name('tasks.complete');
});

Features:

  • Create/edit/delete tasks with due dates and priorities
  • Filter by status (pending, in-progress, completed)
  • Mark task as complete with one click
  • Dashboard showing overdue and due-today tasks

Common Mistakes

Mistake Problem Fix
Not using $fillable or $guarded Mass assignment vulnerability Always define $fillable on models
Eager loading forgotten N+1 query problem, slow pages Use ->with('relation')
Returning Model::all() in API Returns all rows + all columns Use pagination + resources
Storing secrets in code Exposed credentials Use .env + never commit
Business logic in controllers Fat controllers, hard to test Move to Services or Actions
migrate:fresh in production Destroys all data Only use in dev; use migrate in prod
Skipping validation Security vulnerabilities Always validate in FormRequest or inline
Using {!! $var !!} for user input XSS vulnerability Use {{ $var }} (auto-escaped)

Laravel vs Other PHP Frameworks

Laravel Symfony CodeIgniter 4 Slim 4
ORM Eloquent (ActiveRecord) Doctrine (DataMapper) None built-in None
Templating Blade Twig None built-in None
Auth scaffolding Breeze/Jetstream SecurityBundle None None
Queue system Built-in Messenger None None
Real-time Broadcasting (Pusher/Reverb) Mercure None None
File storage Flysystem abstraction Filesystem None None
Learning curve Medium Steep Easy Easy
Best for Full web apps, SaaS, APIs Enterprise, APIs Simple apps Micro-APIs
Version (2025) Laravel 11 Symfony 7 4.x 4.x
Weekly npm-like installs 500k+/week 400k+/week 100k+/week 80k+/week

Laravel Ecosystem

Package Purpose
Laravel Breeze Minimal auth scaffolding (login, register, password reset)
Laravel Jetstream Full auth with teams, 2FA, API tokens
Laravel Sanctum SPA / mobile API authentication
Laravel Passport Full OAuth2 server
Laravel Horizon Redis queue monitoring dashboard
Laravel Telescope Debug assistant (requests, queries, jobs, logs)
Laravel Reverb First-party WebSocket server
Laravel Octane Supercharge app with Swoole/RoadRunner
Laravel Forge Server management & deployment
Laravel Vapor Serverless deployment on AWS
Livewire Full-stack components without writing JS
Inertia.js SPA-like apps using React/Vue with Laravel backend
Filament Admin panel builder
Spatie Packages Permissions, media library, activity log, and 200+ more

32-Week Learning Path

Week Topic Goal
1–2 Setup + Routing + Controllers Build simple routes with controllers
3–4 Blade Templates Dynamic views with layouts
5–6 Eloquent ORM + Migrations CRUD with a database
7–8 Relationships One-to-many, many-to-many
9–10 Authentication (Breeze) Login/register system
11–12 Middleware + Authorization (Policies) Route protection
13–14 APIs + Sanctum REST API with token auth
15–16 API Resources + Pagination Proper API responses
17–18 Queues + Jobs + Email Background processing
19–20 Testing (Feature + Unit) TDD workflow in Laravel
21–22 Caching (Redis) Performance optimization
23–24 Events + Listeners + Broadcasting Real-time features
25–26 File Storage (S3/local) Uploads and media
27–28 Livewire or Inertia.js Modern frontend integration
29–30 Performance + Optimization Eager loading, caching, Octane
31–32 Deployment (Forge/Vapor/VPS) Ship to production

Laravel Developer Roles & Salary

Role Experience Salary (US)
Junior Laravel Developer 0–2 years $55k–$85k
Mid-level Laravel Developer 2–5 years $85k–$120k
Senior Laravel Developer 5+ years $120k–$160k
Laravel/PHP Tech Lead 7+ years $150k–$200k
Full Stack (Laravel + Vue/React) 3–7 years $100k–$150k

Frequently Asked Questions

Is Laravel good for beginners?

Laravel is one of the best frameworks for PHP beginners because of its exceptional documentation, expressive syntax, and strong community. The learning curve is gentler than Symfony, and the built-in scaffolding (Breeze, Sanctum) handles complex patterns automatically. If you know basic PHP, you can build real apps within weeks.

Laravel vs WordPress: which should I choose?

WordPress is a CMS — great for content sites and blogs without custom development. Laravel is a full-stack PHP framework for building custom web applications. Choose WordPress for content-heavy sites with minimal custom logic. Choose Laravel for custom SaaS products, APIs, booking systems, dashboards, or anything with complex business logic.

Can Laravel be used for APIs?

Absolutely. Laravel is excellent for building REST APIs. Use routes/api.php, API controllers, Eloquent API Resources for response transformation, and Laravel Sanctum for authentication. For high-traffic APIs, consider Laravel Octane (Swoole) for significantly better throughput.

What database does Laravel work with?

Laravel Eloquent supports MySQL, PostgreSQL, SQLite, and SQL Server out of the box. MySQL and PostgreSQL are the most common choices in production. SQLite is perfect for local development and testing with RefreshDatabase.

Is Laravel still relevant in 2025?

Yes — Laravel remains the most popular PHP framework worldwide. With Laravel 11, Reverb (WebSockets), Octane, Horizon, and a massive ecosystem of first-party and Spatie packages, it covers everything from simple blogs to enterprise SaaS. The job market remains strong with 500k+ weekly Composer installs.

How do I deploy a Laravel app?

Common options: Laravel Forge (provision a VPS + auto-deploy from GitHub — easiest), Laravel Vapor (serverless on AWS), or manually on a VPS (Ubuntu + Nginx + PHP-FPM + MySQL). You'll need to set .env in production, run php artisan config:cache and php artisan route:cache, and point your web server to public/.

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools