Site Logo
Find Your Local Branch

Software Development

Learn | Laravel: The PHP Framework for Web Artisans

Introduction to Laravel

Laravel is an open-source PHP framework created to make backend and full-stack web development faster, cleaner, and easier to maintain. It exists because plain PHP projects can become difficult to organize as they grow. Laravel solves this by giving developers a structured way to handle routing, database operations, user input, authentication, security, background jobs, and templating. In real life, Laravel is used to build company portals, e-commerce sites, booking systems, learning platforms, APIs for mobile apps, and internal admin dashboards.

At its foundation, Laravel follows the MVC pattern: Model, View, and Controller. Models represent data and business rules, Views handle presentation, and Controllers coordinate requests and responses. Laravel also includes Artisan for command-line tasks, Blade for templating, Eloquent ORM for working with databases using PHP classes, migrations for version-controlled schema changes, and middleware for request filtering such as authentication.

Laravel is popular because it balances beginner friendliness with professional power. You can start with simple routes and views, then grow into advanced topics like queues, events, caching, API resources, and testing without leaving the framework. If you understand what Laravel provides and why each tool exists, you can build applications more confidently and with less repeated code.

Step-by-Step Explanation

A Laravel application begins when a user sends a request, such as visiting a page in the browser. The request is matched to a route, usually defined in files like routes/web.php or routes/api.php. That route can return text directly, load a view, or send the request to a controller method.

Controllers are placed in app/Http/Controllers and keep request-handling logic organized. Models usually live in app/Models and represent database tables. Views are stored in resources/views and are commonly written using Blade syntax. Laravel also uses an .env file for environment-specific settings such as database credentials and app URLs.

Beginners should first understand this flow: request -> route -> controller or closure -> model if needed -> view or JSON response. Once that flow is clear, the rest of Laravel becomes much easier to learn.

Comprehensive Code Examples

Basic example
// routes/web.php
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
return 'Welcome to Laravel!';
});
Real-world example
// routes/web.php
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\PageController;

Route::get('/about', [PageController::class, 'about']);

// app/Http/Controllers/PageController.php
namespace App\Http\Controllers;

class PageController extends Controller
{
public function about()
{
return view('about', ['company' => 'Web Artisans']);
}
}

// resources/views/about.blade.php

About {{ $company }}

Advanced usage
// routes/web.php
use Illuminate\Support\Facades\Route;
use App\Models\Post;

Route::get('/posts', function () {
$posts = Post::latest()->take(5)->get();
return view('posts.index', compact('posts'));
});

Common Mistakes

  • Mixing all logic into route closures instead of using controllers. Fix: move request logic into controller classes as the app grows.

  • Editing configuration directly for every environment instead of using .env. Fix: keep secrets and environment values in the environment file.

  • Confusing Blade views with plain PHP files. Fix: store templates in resources/views and use the .blade.php extension.

Best Practices

  • Learn Laravel through its request lifecycle rather than memorizing isolated features.

  • Keep controllers focused and delegate data work to models or dedicated classes.

  • Use Artisan commands to generate files consistently and follow framework conventions.

  • Name routes, controllers, views, and models clearly to improve readability.

Practice Exercises

  • Create a new route that returns the text Learning Laravel in the browser.

  • Create a controller with one method that loads a Blade view called welcome-page.

  • Write a route for /contact that returns a view and passes one company email variable to it.

Mini Project / Task

Build a tiny company website starter with three pages: Home, About, and Contact. Use routes, one controller, and Blade views to display the page content.

Challenge (Optional)

Create a route that loads a list of sample posts from an array and displays them in a Blade view as a simple blog homepage.

How Laravel Works

Laravel is a modern PHP framework designed to make web development faster, cleaner, and more maintainable. It exists to solve common problems in PHP applications such as routing requests, organizing code, accessing databases, validating input, handling authentication, and rendering views. In real-life projects, Laravel is used for e-commerce platforms, dashboards, booking systems, APIs, CRMs, and SaaS products. At its core, Laravel follows the MVC pattern: Model, View, and Controller. A browser sends a request, Laravel routes that request, optional middleware checks or modifies it, a controller handles the logic, models interact with the database, and a response is returned as HTML or JSON. Another important part is the service container, which automatically resolves dependencies, making code easier to test and extend. Laravel also includes Blade for templating, Eloquent for database work, Artisan for command-line automation, and configuration systems for environments. Think of Laravel as a well-organized pipeline. Every request enters through a single public entry point, then flows through bootstrapping, routing, middleware, business logic, and response generation. Understanding this lifecycle helps beginners debug faster and write more structured applications.

Step-by-Step Explanation

Step 1: A user visits a URL such as /products in the browser.
Step 2: The web server sends that request to Laravel's public/index.php file, the front controller.
Step 3: Laravel boots the framework and loads configuration, service providers, and environment settings.
Step 4: The router checks routes/web.php or routes/api.php to find a matching route.
Step 5: Middleware may run before the request reaches the controller. Examples include authentication, CSRF protection, and logging.
Step 6: The matched controller method executes business logic.
Step 7: If needed, Eloquent models query or update the database.
Step 8: The controller returns a view, redirect, or JSON response.
Step 9: Laravel sends the final response back to the browser.

Comprehensive Code Examples

Basic example
use Illuminate\Support\Facades\Route;

Route::get('/hello', function () {
return 'Hello from Laravel';
});

This route listens for a GET request and returns plain text directly. It shows the simplest request-to-response cycle.

Real-world example
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::get('/products', [ProductController::class, 'index']);

namespace App\Http\Controllers;

use App\Models\Product;

class ProductController extends Controller
{
public function index()
{
$products = Product::latest()->get();
return view('products.index', compact('products'));
}
}

Here, the route points to a controller. The controller uses a model to fetch data, then passes that data to a Blade view. This is the standard Laravel flow in many applications.

Advanced usage
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware(['auth'])->get('/dashboard', function (Request $request) {
return response()->json([
'user' => $request->user()->name,
'message' => 'Welcome to your dashboard'
]);
});

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

return $next($request);
}
}

This example shows middleware and dependency injection. Laravel automatically injects the request object and applies authentication before generating a JSON response.

Common Mistakes

  • Putting too much logic in routes: Move complex logic into controllers or services for cleaner structure.
  • Confusing models and controllers: Models manage data, while controllers manage request handling and coordination.
  • Ignoring middleware flow: If a page fails unexpectedly, check whether authentication, authorization, or CSRF middleware blocked the request.
  • Not understanding request lifecycle: Trace the flow from route to controller to model to response when debugging.

Best Practices

  • Keep routes small and map them to controllers for maintainability.
  • Use middleware for cross-cutting concerns such as auth, rate limiting, and request filtering.
  • Use Eloquent models for database access instead of raw queries unless optimization requires otherwise.
  • Return the correct response type: views for web pages, JSON for APIs, redirects after form submissions.
  • Learn the request lifecycle early because it improves debugging and architecture decisions.

Practice Exercises

  • Create a route named /about that returns a simple text response from a closure.
  • Build a controller with a method that returns a Blade view called welcome-page.
  • Create a model-based route flow where a controller fetches all records from a table and passes them to a view.

Mini Project / Task

Build a small product listing page where a route sends a request to a controller, the controller fetches products using an Eloquent model, and the response is rendered in a Blade view.

Challenge (Optional)

Add middleware to the product listing so only logged-in users can access it, and return a JSON error or redirect if the user is not authenticated.

Installation and Setup

Laravel is a PHP web framework used to build everything from simple CRUD apps to large business platforms, APIs, dashboards, e-commerce systems, and SaaS products. Its purpose is to make web development faster and more organized by providing tools for routing, database access, authentication, queues, caching, testing, and command-line automation. In real projects, installation and setup matter because a bad environment causes version conflicts, missing extensions, broken dependencies, and deployment surprises. A proper Laravel setup usually includes PHP, Composer, a database such as MySQL or SQLite, Node.js for frontend assets, and a local web server environment. The most common ways to start are Laravel Installer, Composer create-project, or Laravel Sail for Docker-based development. You should also understand the role of the .env file, which stores environment-specific settings such as database credentials, application URL, debug mode, and app key. On Windows, developers often use XAMPP, Laragon, or Docker. On macOS and Linux, Valet, Herd, native PHP, or Docker are common choices. The goal is not just to install Laravel, but to create a repeatable local workflow that matches team and production expectations.

Step-by-Step Explanation

First, install PHP 8.2 or newer and confirm required extensions like OpenSSL, PDO, Mbstring, Tokenizer, XML, Ctype, and Fileinfo. Next, install Composer, which manages PHP packages. Then optionally install Node.js and npm if your project uses Vite for frontend assets. To create a new project with Composer, run composer create-project laravel/laravel myapp. Another option is the Laravel Installer after installing it globally with Composer. After project creation, move into the folder and copy environment values if needed. Generate the application key using php artisan key:generate; this is required for encryption and sessions. Configure the database in .env by setting DB_CONNECTION, DB_DATABASE, DB_USERNAME, and DB_PASSWORD. Start the development server with php artisan serve, then open the provided local URL. If the project includes frontend dependencies, run npm install and npm run dev. Finally, test the installation by loading the welcome page and running php artisan about to inspect the environment.

Comprehensive Code Examples

# Basic: create and run a Laravel project
composer create-project laravel/laravel blog-app
cd blog-app
php artisan key:generate
php artisan serve
# Real-world: configure environment for SQLite
APP_NAME=BlogApp
APP_ENV=local
APP_DEBUG=true
APP_URL=http://127.0.0.1:8000

DB_CONNECTION=sqlite
DB_DATABASE=/absolute/path/to/database/database.sqlite
# Advanced: full local setup with assets and migrations
composer create-project laravel/laravel crm-app
cd crm-app
cp .env.example .env
php artisan key:generate
npm install
npm run dev
php artisan migrate
php artisan serve

Common Mistakes

  • Missing PHP extensions: Laravel may fail during install or runtime. Fix by enabling required PHP extensions in your local PHP configuration.
  • Forgetting the app key: Sessions and encryption break if APP_KEY is missing. Run php artisan key:generate.
  • Wrong database settings: Migrations fail when .env values are incorrect. Double-check host, port, database name, and credentials.
  • Not installing frontend packages: CSS and JS may not load. Run npm install and npm run dev when needed.

Best Practices

  • Use the latest stable Laravel version that matches your team and hosting environment.
  • Keep .env out of version control and use .env.example for shared defaults.
  • Prefer SQLite for quick learning projects and MySQL or PostgreSQL for realistic app development.
  • Run php artisan about and php -v to verify your environment before debugging deeper issues.
  • Use Docker-based Sail when you need consistent environments across different machines.

Practice Exercises

  • Install Composer and create a fresh Laravel project named task-manager.
  • Configure the project to use SQLite and generate the application key.
  • Start the Laravel development server and verify the app loads in a browser.

Mini Project / Task

Create a local Laravel starter project for a small notes application. Set up the framework, configure the database, run initial migrations, and confirm the app and asset pipeline work correctly.

Challenge (Optional)

Set up the same Laravel project in two ways: one using native PHP and Composer, and one using Laravel Sail. Compare the setup steps, tooling, and portability of both approaches.

Laravel Directory Structure

Laravel applications follow a predictable directory structure so developers can quickly find where business logic, routes, templates, configuration, and public assets belong. This structure exists to keep projects clean, scalable, and easy to maintain as teams grow. In real-world applications, a clear folder layout helps backend developers manage controllers and models, frontend developers work with Blade templates and assets, and DevOps engineers handle logs, cache, and deployment settings. When you create a Laravel project, the framework generates folders such as app, routes, resources, config, database, public, and storage. Each serves a specific role. For example, app usually holds controllers, models, middleware, and service classes. The routes folder contains route files like web.php and api.php. The resources directory stores Blade views and source assets, while public is the web-accessible entry point containing index.php. The config folder defines application settings, and database contains migrations, seeders, and factories. Laravel also includes folders like bootstrap for framework bootstrapping, vendor for Composer packages, and tests for automated tests. Understanding these sub-parts helps you avoid putting code in random places and makes collaboration much smoother.

Step-by-Step Explanation

Start by creating a project with composer create-project laravel/laravel blog-app. Open the project and inspect the top-level folders. First, go to routes/web.php for browser routes. Second, open app/Http/Controllers to place controller classes. Third, visit resources/views for Blade templates. Fourth, check config/app.php for application settings. Fifth, use database/migrations to define database tables. Sixth, remember that anything directly accessible by a browser should live under public, not inside private folders like app or storage. Finally, use storage for logs, cached files, and uploaded content that may later be linked publicly through Laravel tools.

Comprehensive Code Examples

// Basic example: define a route in routes/web.php
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
return view('welcome');
});
// Real-world example: controller in app/Http/Controllers/ProductController.php
namespace App\Http\Controllers;

class ProductController extends Controller
{
public function index()
{
$products = ['Laptop', 'Mouse', 'Keyboard'];
return view('products.index', compact('products'));
}
}

// Route in routes/web.php
use App\Http\Controllers\ProductController;
Route::get('/products', [ProductController::class, 'index']);
// Advanced usage: migration in database/migrations
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('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->decimal('price', 8, 2);
$table->timestamps();
});
}
};

Common Mistakes

  • Mistake: Putting route logic in view files.
    Fix: Keep routing in routes and rendering in resources/views.
  • Mistake: Saving sensitive files in public.
    Fix: Only expose assets meant for browsers.
  • Mistake: Editing files inside vendor.
    Fix: Customize behavior in your own app code, not third-party packages.

Best Practices

  • Keep controllers thin and move business logic into services or models when needed.
  • Use naming conventions so folders and files stay predictable for teams.
  • Store environment-specific values in .env and read them through config files.
  • Organize Blade files into subfolders such as admin, auth, or products.

Practice Exercises

  • Open a fresh Laravel project and identify the purpose of app, routes, resources, and public.
  • Create a new route in routes/web.php that returns a Blade view from resources/views.
  • Create a controller in app/Http/Controllers and connect it to a route.

Mini Project / Task

Build a small “Company Info” page by placing a route in routes/web.php, a controller in app/Http/Controllers, and a Blade template in resources/views.

Challenge (Optional)

Map out the full directory structure of a fresh Laravel project and write a one-line purpose statement for at least ten folders or important files.

Artisan CLI Basics

Artisan is Laravel's built-in command-line interface. It exists to help developers perform common framework tasks quickly without manually creating every file or writing repetitive setup code. In real projects, Artisan is used to generate controllers, models, migrations, jobs, events, mail classes, seeders, and many other application components. It also runs framework utilities such as database migrations, cache clearing, route listing, queue workers, and custom commands. Instead of clicking through tools or copying boilerplate code by hand, developers use Artisan to standardize workflows and reduce errors.

At its core, Artisan works through terminal commands executed from the root of a Laravel project. Common command patterns include viewing available commands with php artisan list, reading help for a command with php artisan help migrate, generating code with make: commands, and running maintenance commands such as migrate or optimize:clear. A key idea for beginners is that Artisan is not separate from Laravel; it is one of the main tools used to interact with the framework efficiently. You will often use it every day during development.

Step-by-Step Explanation

First, open a terminal and move into your Laravel project folder. Most Artisan commands start with php artisan. The php part runs PHP, and the artisan file is Laravel's command entry point. To see what is available, run php artisan list. Laravel will display grouped commands such as cache, config, db, make, migrate, queue, route, and serve.

To understand one command in detail, use the help option. For example, php artisan help make:controller shows syntax, arguments, and optional flags. Flags modify behavior. For instance, --resource creates a resource-style controller, and --api creates API-friendly methods.

Artisan commands usually follow a pattern: command name, required values, then optional flags. Example: php artisan make:model Post -m. Here, make:model is the command, Post is the model name, and -m tells Laravel to also create a migration. Learning to read command structure is essential because many Laravel tasks build on this pattern.

Comprehensive Code Examples

Basic example
php artisan list
php artisan help migrate
php artisan route:list

These commands help you explore Artisan, inspect a specific command, and list application routes.

Real-world example
php artisan make:controller ProductController --resource
php artisan make:model Product -m
php artisan migrate

This workflow creates a controller, a model with a migration, and then applies the migration to the database. It reflects a common feature-building process in Laravel apps.

Advanced usage
php artisan make:command SendReport
php artisan optimize:clear
php artisan serve --host=127.0.0.1 --port=9000

This example creates a custom command, clears compiled and cached framework files, and starts the local development server on a chosen host and port.

Common Mistakes

  • Running commands outside the project folder: move into the Laravel root directory before using php artisan.
  • Forgetting command help: if a command fails or behaves unexpectedly, check php artisan help command:name.
  • Using generated files without reviewing them: Artisan creates boilerplate, but you still need to customize classes, methods, and database fields.
  • Ignoring flags and options: commands like make:controller become much more useful with options such as --resource or --api.

Best Practices

  • Use php artisan list regularly to discover available tools.
  • Prefer generator commands over manual file creation to follow Laravel conventions.
  • Read command help before memorizing syntax; it improves accuracy and confidence.
  • Combine commands logically, such as generating a model and migration together.
  • Clear caches with optimize:clear when changes seem inconsistent during development.

Practice Exercises

  • Open a Laravel project and use Artisan to list all available commands. Identify three command groups you recognize.
  • Generate a controller named OrderController and inspect the created file.
  • Create a model named Invoice with a migration, then locate both generated files in the project.

Mini Project / Task

Use Artisan to scaffold a small blog feature by generating a Post model, migration, and resource controller, then run the migration and list your routes.

Challenge (Optional)

Create a custom Artisan command for a fictional maintenance task such as sending a daily summary, then use Artisan help to inspect how Laravel registered and exposes your command.

Routing in Laravel

Routing in Laravel is the system that decides what should happen when a user visits a URL such as /home, submits a form, or calls an API endpoint. In real applications, routing connects browser requests to pages, controllers, business logic, and responses. For example, an e-commerce app may use routes for product listings, cart actions, checkout, and admin dashboards. Laravel makes routing expressive and readable so developers can define application behavior clearly inside files such as routes/web.php and routes/api.php.

Laravel supports several route styles. Basic routes return simple responses with closures. Controller routes send requests to controller methods for cleaner architecture. Parameterized routes accept dynamic values like IDs or slugs. Named routes make URL generation safer. Route groups help apply prefixes, middleware, or namespaces to many routes at once. Resource routes generate standard CRUD routes automatically. In practice, web routes usually handle browser-based sessions and CSRF protection, while API routes are typically stateless and designed for JSON responses.

Step-by-Step Explanation

A route usually starts with the Route facade, followed by an HTTP verb such as get, post, put, patch, or delete. The first argument is the URI, and the second is the action. The action can be a closure or a controller method array. Dynamic values are added with curly braces like /users/{id}. Optional parameters use a question mark, such as {slug?}. You can also constrain parameters with patterns using where().

Named routes are created with ->name() so links and redirects do not depend on hardcoded URLs. Middleware is attached with ->middleware() to protect routes, for example requiring authentication. Route groups wrap related routes together and reduce repetition by sharing middleware, prefixes, or name prefixes. Resource routes use Route::resource() to generate conventional endpoints like index, create, store, show, edit, update, and destroy.

Comprehensive Code Examples

Basic example
use Illuminate\Support\Facades\Route;

Route::get('/', function () {
return 'Welcome to Laravel';
});

Route::get('/about', function () {
return view('about');
});
Real-world example
use App\Http\Controllers\ProductController;
use Illuminate\Support\Facades\Route;

Route::get('/products', [ProductController::class, 'index'])->name('products.index');
Route::get('/products/{id}', [ProductController::class, 'show'])
->where('id', '[0-9]+')
->name('products.show');

Route::post('/cart', [ProductController::class, 'addToCart'])
->middleware('auth')
->name('cart.add');
Advanced usage
use App\Http\Controllers\Admin\UserController;
use Illuminate\Support\Facades\Route;

Route::prefix('admin')
->name('admin.')
->middleware(['auth', 'verified'])
->group(function () {
Route::get('/dashboard', function () {
return view('admin.dashboard');
})->name('dashboard');

Route::resource('users', UserController::class);
});

Common Mistakes

  • Using the wrong HTTP verb: A form submission should usually use post, not get. Match the request type to the action.
  • Forgetting route order: A dynamic route like /posts/{id} can capture paths meant for a specific route like /posts/create. Place specific routes first.
  • Hardcoding URLs: Use named routes instead of literal strings so links stay correct after URL changes.
  • Missing middleware: Protect sensitive routes such as dashboards or cart actions with authentication middleware.

Best Practices

  • Keep simple responses in closures, but move business logic into controllers.
  • Use named routes for redirects, links, and maintainability.
  • Group related routes with shared prefixes and middleware.
  • Use resource routes for CRUD modules to follow Laravel conventions.
  • Separate browser routes in web.php and API endpoints in api.php.

Practice Exercises

  • Create a GET route for /contact that returns a simple text response.
  • Create a route with a parameter like /category/{name} and display the category name.
  • Create a named route for /dashboard and protect it with auth middleware.

Mini Project / Task

Build a small blog routing setup with routes for home, post list, single post by slug, admin dashboard, and a resource controller for managing posts in the admin panel.

Challenge (Optional)

Create a route group for an admin area that uses a URL prefix, route name prefix, and multiple middleware rules, then add both custom routes and a resource route inside it.

Route Parameters


Route parameters are a fundamental concept in web development, especially within frameworks like Laravel, that allow you to capture dynamic segments in your URLs. Instead of defining a separate route for every single resource (e.g., /users/1, /users/2, /users/3), route parameters enable you to define a single, flexible route that can handle all these variations. This significantly reduces code duplication, improves maintainability, and makes your application's routing system much more scalable and organized. In real-world applications, route parameters are indispensable for displaying specific user profiles, retrieving individual product details, editing blog posts, or fetching any resource identified by a unique ID or slug. They form the backbone of RESTful API design, where resources are often accessed via URLs like
/api/products/{id}
or
/api/users/{username}
. Without them, you'd be forced into cumbersome query string parameters for every unique resource, leading to less clean and less intuitive URLs.

The core concept behind route parameters is defining placeholders within your route URI. When a request matches this URI, the values corresponding to these placeholders are then extracted and passed as arguments to your route's callback function or controller method. Laravel supports several types of route parameters, primarily focusing on required and optional parameters, and also offers regular expression constraints for more advanced validation.

Required parameters are the most common type. They are essential parts of the URL and if they are missing, the route will not match. For example, in
/users/{id}
, the
{id}
part is a required parameter. If a user tries to access
/users
, this route would not match.

Optional parameters, as the name suggests, are not strictly necessary for the route to match. They are defined by appending a
?
to the parameter name (e.g.,
/users/{id?}
). If an optional parameter is not provided in the URL, it will be passed to your route's callback or controller method with a
null
value, or a default value if one is specified in the method signature.

Step-by-Step Explanation


Let's break down how to define and use route parameters in Laravel.

1. Defining a Basic Required Parameter:
In your
routes/web.php
file, you define a route with a placeholder enclosed in curly braces. The placeholder name will be the variable name passed to your function.

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

Here,
{id}
is the parameter. When you visit
/users/123
,
'123'
will be passed as
$id
to the closure.

2. Defining Multiple Required Parameters:
You can have as many parameters as needed. Laravel matches them in order.

Route::get('/posts/{category}/{slug}', function (string $category, string $slug) {
return "Category: {$category}, Slug: {$slug}";
});

Visiting
/posts/tech/laravel-basics
would yield "Category: tech, Slug: laravel-basics".

3. Defining Optional Parameters:
Add a
?
to the parameter name. Remember to provide a default value in your function signature to avoid errors if the parameter is not present.

Route::get('/users/{id?}', function (?string $id = null) {
return $id ? 'User ID: ' . $id : 'No user ID provided.';
});

Visiting
/users/456
works, and visiting
/users
also works, passing
null
to
$id
.

4. Regular Expression Constraints:
You can restrict parameter formats using the
where()
method.

Route::get('/products/{id}', function (string $id) {
return 'Product ID: ' . $id;
})->where('id', '[0-9]+'); // 'id' must be one or more digits

This route will only match if
{id}
consists solely of digits. You can also use
whereAlpha()
,
whereNumber()
,
whereAlphaNumeric()
for common patterns, or
whereUuid()
for UUIDs.

5. Global Constraints:
For parameters that appear frequently, you can define global constraints in your
App\Providers\RouteServiceProvider.php
file within the
boot()
method.

public function boot(): void
{
Route::pattern('id', '[0-9]+');

// ... other route definitions
}

Now, any route parameter named
{id}
will automatically be constrained to digits only.

Comprehensive Code Examples


Basic Example: User Profile
// routes/web.php
Route::get('/profile/{username}', function (string $username) {
return 'Welcome, ' . $username . '!';
});

When you navigate to
/profile/john.doe
, the output will be "Welcome, john.doe!".

Real-world Example: Product Details Page
// routes/web.php
use App\Models\Product;

Route::get('/products/{id}', function (string $id) {
$product = Product::find($id); // Assuming Eloquent model
if (!$product) {
abort(404); // Or redirect to a 404 page
}
return view('products.show', ['product' => $product]);
})->where('id', '[0-9]+'); // Ensure ID is numeric

This route fetches a product from the database based on its numeric ID and passes it to a view. If a non-numeric ID is provided, the route won't match.

Advanced Usage: Optional Paging and Sorting
// routes/web.php
Route::get('/articles/{page?}/{sortBy?}', function (?int $page = 1, ?string $sortBy = 'date') {
$output = "Displaying articles. Page: {$page}, Sorted by: {$sortBy}.";
// Imagine fetching articles from DB here
// e.g., Article::orderBy($sortBy)->paginate(10, ['*'], 'page', $page);
return $output;
})->where([
'page' => '[0-9]+',
'sortBy' => '[a-zA-Z_]+'
]);

This route can handle
/articles
(page 1, sorted by date),
/articles/5
(page 5, sorted by date), or
/articles/3/title
(page 3, sorted by title).

Common Mistakes


1. Forgetting Default Values for Optional Parameters: If you declare an optional parameter in your route (e.g.,
{id?}
) but don't provide a default value (e.g.,
null
) in your function signature, Laravel will throw an error when the parameter is not present in the URL.

Fix: Always provide a default value, typically
null
, for optional parameters in your route closure or controller method signature:
function (?string $id = null)
.

2. Incorrect Order of Parameters: When defining multiple parameters, the order in the route URL must match the order in your function signature. Swapping them will result in incorrect values being assigned.

Fix: Ensure the sequence of parameters in
Route::get('/{param1}/{param2}', function($param1, $param2) { ... })
strictly matches the order in the closure/method.

3. Overlapping Routes with Parameters: Defining a route like
/users/create
and then
/users/{id}
can lead to unintended behavior. Laravel's router processes routes in the order they are defined. If
/users/{id}
is defined before
/users/create
, Laravel might interpret "create" as an
{id}
parameter.

Fix: Always place more specific routes (like
/users/create
) before more general routes that use parameters (like
/users/{id}
).

Best Practices


1. Use Descriptive Parameter Names: Instead of generic
{id}
, use
{userId}
or
{productId}
when appropriate for clarity, especially with multiple parameters.

2. Apply Route Model Binding: For routes that fetch Eloquent models based on a parameter (e.g.,
/users/{user}
), leverage Laravel's Route Model Binding. This automatically injects the model instance into your controller method, simplifying your code.

// routes/web.php
use App\Models\User;

Route::get('/users/{user}', function (User $user) {
return $user->name;
});

Laravel automatically resolves
{user}
to an
App\Models\User
instance based on its ID.

3. Use Route Constraints for Validation: Always use
where()
clauses or global patterns to constrain your parameters. This adds another layer of validation and prevents unexpected input from reaching your application logic, enhancing security and robustness.

4. Group Routes with Common Parameters: If you have multiple routes sharing a common parameter, use route groups. This allows you to apply constraints or prefixes once for a set of routes.

Route::prefix('admin')->group(function () {
Route::get('/users/{id}', ...)->whereNumber('id');
Route::get('/products/{id}', ...)->whereNumber('id');
});

Practice Exercises


1. Basic User Greeting: Create a route
/greet/{name}
that takes a
name
parameter and returns a simple greeting like "Hello, [name]!".

2. Book Details: Define a route
/books/{isbn}
where
isbn
is a required parameter. Add a regular expression constraint to ensure
isbn
is exactly 13 digits long. The route should return "Fetching book with ISBN: [isbn]".

3. Optional Language Preference: Create a route
/welcome/{language?}
. If
language
is provided (e.g.,
/welcome/es
), return "Hola!". If not (e.g.,
/welcome
), return "Hello!".

Mini Project / Task


Build a simple blog post viewer. Create a route
/blog/{slug}
where
slug
is a unique identifier (e.g., "my-first-post"). The route should return a string like "Displaying blog post: [slug]". Additionally, add a route
/blog/{slug}/edit
that returns "Editing blog post: [slug]". Ensure your routes are correctly ordered to avoid conflicts.

Challenge (Optional)


Extend the blog post viewer. Implement a route
/blog/{year}/{month}/{day}/{slug}
that displays a blog post based on its publication date and slug. All date parameters (year, month, day) should be constrained to be numeric, and the slug should be alphanumeric with hyphens allowed. For example,
/blog/2023/10/26/my-latest-article
. Return a string indicating all captured parameters.

Named Routes

Named routes in Laravel let you assign a readable, unique name to a route so you can refer to it throughout your application without hardcoding URLs. Instead of writing literal paths like /dashboard or /products/15 in controllers, Blade templates, redirects, and tests, you use names such as dashboard or products.show. This matters because URLs often change over time, but route names can remain stable. In real-world applications, named routes improve maintainability, reduce broken links, and make your code easier to read. They are especially useful in navigation menus, form actions, redirects after saving data, email links, and permission-based dashboards.

A named route is created by chaining the name() method onto a route definition. Laravel stores that name internally and allows you to generate the correct URL using helpers like route() and redirect helpers. You can name simple GET routes, POST routes, grouped routes, controller routes, and resource routes. Resource controllers are particularly important because Laravel automatically generates conventional route names such as posts.index, posts.create, posts.store, and posts.show.

Step-by-Step Explanation

Start with a basic route in routes/web.php. Add a URL, define what should happen, then attach a name. For example, Route::get('/about', function () { ... })->name('about'); means the path is /about and the route name is about. To generate the URL anywhere in Laravel, call route('about').

If a route contains parameters, pass them as the second argument to route(). For a route like /users/{id}, you might use route('users.show', ['id' => 5]). For grouped routes, use prefixes or name prefixes to keep names organized, such as admin.users.index. This naming convention becomes very helpful as your app grows.

Laravel also lets you redirect to named routes with return redirect()->route('dashboard');. In Blade, links become cleaner with {{ route('contact') }}. If the actual path changes later, you only update the route file, not every template and controller.

Comprehensive Code Examples

// Basic example: naming a simple route
use Illuminate\Support\Facades\Route;

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

// Generate URL
route('about');
// Real-world example: route with parameter and redirect
Route::get('/products/{product}', function ($product) {
return 'Viewing product ' . $product;
})->name('products.show');

Route::post('/products', function () {
// Save product...
return redirect()->route('products.show', ['product' => 101]);
})->name('products.store');
// Advanced usage: controller routes, groups, and resource names
use App\Http\Controllers\Admin\UserController;

Route::prefix('admin')->name('admin.')->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
});

Route::resource('posts', App\Http\Controllers\PostController::class);

// Examples of generated names:
// posts.index, posts.create, posts.store, posts.show, posts.edit, posts.update, posts.destroy

Common Mistakes

  • Using duplicate names: Two routes with the same name can cause confusion or unexpected behavior. Fix it by making every route name unique.
  • Forgetting required parameters: Calling route('users.show') without the needed ID will fail. Pass all required parameters.
  • Hardcoding URLs instead of names: This makes future changes harder. Use route() and redirect()->route() consistently.
  • Unclear naming patterns: Random names like page1 are hard to maintain. Use structured names such as orders.index.

Best Practices

  • Use dot notation like users.index and admin.reports.show for readability.
  • Prefer named routes in Blade templates, controllers, notifications, and tests instead of literal URLs.
  • Group related routes with prefix() and name() to keep route organization consistent.
  • Follow Laravel resource naming conventions when building CRUD features.
  • Keep names descriptive and stable even if the visible URL changes later.

Practice Exercises

  • Create a GET route for /contact and name it contact. Then generate its URL using route().
  • Create a route named profile.show that accepts a user ID parameter and generate a link for user 7.
  • Build an admin route group with the name prefix admin. and add routes named admin.dashboard and admin.users.index.

Mini Project / Task

Build a small blog navigation system using named routes. Create routes for home, posts list, single post view, create post form, and store post action. Use named routes in all links and redirects so the application never relies on hardcoded URLs.

Challenge (Optional)

Create a route group for a dashboard area with authentication middleware, URL prefix, and name prefix. Then generate links to two routes inside the group and add one route that requires a dynamic parameter.

Route Groups and Prefixes

Route groups in Laravel let you organize multiple routes that share the same settings. Instead of repeating the same URL prefix, middleware, controller namespace style, or name prefix on every route, you define those shared rules once and place related routes inside the group. This exists to keep routing files clean, readable, and easier to maintain as applications grow. In real projects, route groups are heavily used for admin panels such as /admin, customer dashboards such as /dashboard, and APIs such as /api/v1 where many endpoints should follow the same structure.

The most common idea is the prefix, which adds a path segment before every route inside the group. Laravel also supports grouping by middleware, name prefixes using name(), and controller grouping. These options are often combined. For example, all admin routes may use the auth and is_admin middleware, start with /admin, and have route names beginning with admin.. This reduces duplication and makes route generation consistent across the app.

Step-by-Step Explanation

A route group starts with Route::prefix('something')->group(function () { ... });. Every route inside automatically receives that prefix. If you add name('admin.'), route names inside become admin.users.index or similar. If you add middleware('auth'), every route in the group requires authentication.

You can also nest groups. For example, one group may define prefix('admin'), and inside it another group may define prefix('reports'). The final URL becomes /admin/reports. Beginners should remember that prefixes affect URLs, while name prefixes affect route names used with helpers such as route().

Comprehensive Code Examples

Basic example
use Illuminate\Support\Facades\Route;

Route::prefix('admin')->group(function () {
Route::get('/dashboard', function () {
return 'Admin Dashboard';
});

Route::get('/users', function () {
return 'Admin Users List';
});
});

These routes become /admin/dashboard and /admin/users.

Real-world example
use App\Http\Controllers\Admin\UserController;
use Illuminate\Support\Facades\Route;

Route::prefix('admin')
->name('admin.')
->middleware(['auth', 'verified'])
->group(function () {
Route::get('/users', [UserController::class, 'index'])->name('users.index');
Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
});

Now the URLs are prefixed with /admin, and the route names become admin.users.index and admin.users.show.

Advanced usage
use App\Http\Controllers\Api\V1\ProductController;
use Illuminate\Support\Facades\Route;

Route::prefix('api')
->name('api.')
->middleware('auth:sanctum')
->group(function () {
Route::prefix('v1')
->name('v1.')
->controller(ProductController::class)
->group(function () {
Route::get('/products', 'index')->name('products.index');
Route::post('/products', 'store')->name('products.store');
Route::get('/products/{product}', 'show')->name('products.show');
});
});

This creates versioned API routes such as /api/v1/products and named routes like api.v1.products.index.

Common Mistakes

  • Confusing URL prefixes with route names: prefix('admin') changes the URL, not the route name. Use name('admin.') for names.
  • Adding extra slashes inconsistently: define the group prefix without leading or trailing confusion and keep inner routes simple such as /users.
  • Forgetting shared middleware: if all admin routes require login, place middleware on the group instead of repeating it per route.

Best Practices

  • Group routes by feature area like admin, account, and api for better maintenance.
  • Use name prefixes consistently so redirects and links are easier to generate.
  • Combine prefixes with middleware and controllers to reduce repeated code.
  • Use nested groups carefully; avoid deep nesting that hurts readability.

Practice Exercises

  • Create a route group with the prefix dashboard and add routes for /home and /settings.
  • Make an admin group that uses the auth middleware and a route name prefix of admin..
  • Build a nested API group for /api/v1 with routes for listing and showing posts.

Mini Project / Task

Create a small admin routing structure for a blog with grouped routes for dashboard, posts, and categories. Add an admin URL prefix, an admin. name prefix, and authentication middleware to the entire group.

Challenge (Optional)

Design a versioned API routing setup that supports both v1 and v2 using nested route groups, while keeping route names organized and avoiding duplicate middleware definitions.

Controllers Overview

In Laravel, controllers are classes that group related request-handling logic into a single, organized place. Instead of placing all application behavior directly inside route definitions, you send an incoming request from a route to a controller method. This makes code easier to read, test, scale, and maintain. In real projects, controllers are used for tasks such as showing a dashboard, saving a form submission, updating a product, or returning JSON from an API. Controllers exist because web applications grow quickly, and keeping logic inside routes becomes messy. Laravel supports several controller styles, including basic controllers, single-action controllers with an __invoke() method, resource controllers for CRUD operations, and API resource controllers for JSON-based endpoints. A controller usually works together with routes, requests, models, views, and services. For example, a product controller may receive a request, validate input, use an Eloquent model to query the database, and then return a Blade view or JSON response.

Step-by-Step Explanation

To create a controller, use Artisan: php artisan make:controller PageController. Laravel places it in app/Http/Controllers. Inside the class, define public methods such as index(), show(), or store(). Next, connect a route to the controller method in routes/web.php or routes/api.php. The route syntax commonly looks like Route::get('/home', [PageController::class, 'index']);. When a user visits /home, Laravel calls the index() method. A method can return a view using return view('home');, plain text, JSON, a redirect, or another response object. Resource controllers simplify CRUD routing with one command: php artisan make:controller ProductController --resource. Laravel then expects methods like index, create, store, show, edit, update, and destroy. Single-action controllers are useful when one controller handles one responsibility, such as sending a report or processing checkout. Create one and define __invoke(), then route directly to the class name.

Comprehensive Code Examples

namespace App\Http\Controllers;
use Illuminate\Http\Request;
class PageController extends Controller
{
public function index()
{
return view('welcome');
}
}
use App\Http\Controllers\PageController;
Route::get('/welcome', [PageController::class, 'index']);
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
class ProductController extends Controller
{
public function index()
{
$products = Product::latest()->get();
return view('products.index', compact('products'));
}

public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|max:255',
'price' => 'required|numeric'
]);

Product::create($validated);
return redirect('/products')->with('success', 'Product created');
}
}
use App\Http\Controllers\ProductController;
Route::resource('products', ProductController::class);
namespace App\Http\Controllers;
class ReportController extends Controller
{
public function __invoke()
{
return response()->json(['message' => 'Daily report generated']);
}
}
use App\Http\Controllers\ReportController;
Route::get('/report', ReportController::class);

Common Mistakes

  • Putting too much logic in routes: Move database and business logic into controllers or service classes.
  • Using the wrong route method: Use get for reading pages, post for creating data, put/patch for updates, and delete for deletion.
  • Forgetting imports: Add the correct use statement for controllers and models.
  • Returning the wrong response type: Return views for web pages and JSON for APIs when appropriate.

Best Practices

  • Keep controllers thin: Let controllers coordinate requests and responses, not hold heavy business logic.
  • Name methods clearly: Use standard resource names when building CRUD features.
  • Validate input early: Validate in the controller or dedicated form request classes.
  • Use resource controllers: They improve consistency and make routes predictable.
  • Prefer single-action controllers: Use them for isolated tasks with one clear responsibility.

Practice Exercises

  • Create a HomeController with an index() method that returns a simple view.
  • Create a ContactController with a method that returns JSON containing a company email and phone number.
  • Create a resource controller called BookController and register it with a resource route.

Mini Project / Task

Build a small Task Manager controller setup where one controller lists tasks, another method stores a new task from a form, and a third method deletes a task.

Challenge (Optional)

Create a single-action controller that accepts a request, checks for a query parameter like type=summary, and returns a different JSON message depending on the value.

Resource Controllers

Resource Controllers in Laravel provide a standardized way to handle common CRUD operations: creating, reading, updating, and deleting data. Instead of writing many separate routes and controller methods manually, Laravel lets you group them under one controller using conventional method names such as index, create, store, show, edit, update, and destroy.

They exist to make application structure predictable. In real projects such as blog systems, product catalogs, employee dashboards, and admin panels, many features follow the same CRUD pattern. Resource Controllers reduce repetition, improve readability, and align routes with developer expectations. When another developer opens your project and sees a resource controller, they immediately understand how requests are organized.

The main sub-types are full resource controllers and partial resource controllers. A full resource controller includes all seven standard methods. A partial resource controller is created when you register only selected actions using only() or except() on the route definition. Laravel also supports API resource controllers, which usually omit view-oriented methods like create and edit because APIs return JSON instead of HTML forms.

Step-by-Step Explanation

First, generate a resource controller with Artisan using php artisan make:controller PostController --resource. Laravel creates a controller class with the standard method stubs.

Next, register the controller in your routes file with Route::resource('posts', PostController::class);. This one line generates multiple routes automatically. For example, a GET request to /posts calls index(), while a POST request to /posts calls store().

Each method has a clear responsibility. index() lists records. create() shows a form. store() saves new data. show() displays one item. edit() shows an edit form. update() saves changes. destroy() deletes a record.

In beginner-friendly terms, the route decides which URL and HTTP method is being used, and the resource controller decides what logic should run for that request. This convention makes controllers clean and consistent.

Comprehensive Code Examples

// routes/web.php
use App\Http\Controllers\PostController;

Route::resource('posts', PostController::class);
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class PostController extends Controller
{
public function index()
{
return 'List of posts';
}

public function create()
{
return 'Post creation form';
}

public function store(Request $request)
{
return 'Save new post';
}

public function show(string $id)
{
return 'Viewing post ' . $id;
}

public function edit(string $id)
{
return 'Edit form for post ' . $id;
}

public function update(Request $request, string $id)
{
return 'Update post ' . $id;
}

public function destroy(string $id)
{
return 'Delete post ' . $id;
}
}
// Real-world example with Eloquent
namespace App\Http\Controllers;

use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
public function index()
{
$posts = Post::latest()->get();
return view('posts.index', compact('posts'));
}

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

Post::create($validated);

return redirect()->route('posts.index');
}

public function update(Request $request, Post $post)
{
$validated = $request->validate([
'title' => 'required|max:255',
'content' => 'required'
]);

$post->update($validated);

return redirect()->route('posts.show', $post);
}
}
// Advanced usage: partial resource routes
use App\Http\Controllers\PostController;

Route::resource('posts', PostController::class)->only(['index', 'show']);

// API resource
Route::apiResource('posts', PostController::class);

Common Mistakes

  • Using wrong method names: Laravel expects exact resource names like index and store. Fix this by following Laravel conventions strictly.
  • Forgetting route registration: Creating the controller alone does not expose URLs. Add Route::resource() in the routes file.
  • Mixing view logic and data logic poorly: Keep validation, queries, and redirects organized inside the correct methods instead of writing unrelated code everywhere.

Best Practices

  • Use route model binding so Laravel automatically resolves models like Post $post.
  • Validate input in store() and update() before saving data.
  • Keep each method focused on one action only.
  • Use partial resources when you do not need all seven actions.
  • Pair resource controllers with named routes for cleaner redirects and links.

Practice Exercises

  • Create a BookController as a resource controller and register it with Route::resource().
  • Add code to the index(), show(), and destroy() methods that returns simple text responses.
  • Create a partial resource route for a ProductController that only supports index, show, and update.

Mini Project / Task

Build a simple blog post management feature with a resource controller that supports listing posts, showing a form, saving a new post, editing an existing post, updating it, and deleting it.

Challenge (Optional)

Create an API-style resource controller for tasks and register it with Route::apiResource(). Then identify which standard resource methods are omitted and explain why.

Middleware Basics

Middleware in Laravel is a filtering layer that sits between an incoming HTTP request and your application logic. It exists so you can inspect, allow, deny, modify, or redirect requests before they reach a controller, and also adjust responses before they are returned to the browser. In real projects, middleware is used for authentication, checking user roles, forcing HTTPS, logging requests, blocking suspended users, rate limiting, and setting application context such as locale or tenant information. Think of it as a security guard and traffic controller for routes.

Laravel supports several practical middleware styles. Global middleware runs for every request and is useful for app-wide concerns like trimming input or handling proxies. Route middleware is attached to specific routes and is ideal when only certain pages need protection, such as admin dashboards. Middleware groups let you bundle multiple middleware together, which keeps route definitions clean and consistent. You may also pass parameters to middleware, which is useful for role or permission checks. Understanding these forms is important because choosing the right scope keeps your app organized and fast.

Step-by-Step Explanation

A middleware class usually contains a handle method. This method receives the request and a next callback. Inside handle, you decide whether to continue processing the request. If the request is acceptable, call $next($request). If not, return a redirect or response immediately. To create middleware, use Artisan with php artisan make:middleware EnsureUserIsAdmin. Laravel places it in the middleware directory. Then register it so Laravel can reference it by name, or add it to a group when appropriate. Finally, attach it to routes using the middleware() method.

The basic syntax flow is simple: request enters route, middleware runs, then controller executes if allowed. Middleware can also modify the outgoing response after $next($request) returns. This makes it useful not only for access control but also for headers, caching hints, and response inspection.

Comprehensive Code Examples

Basic example
// app/Http/Middleware/EnsureUserIsAdmin.php
namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;

class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next)
{
if (!auth()->check() || auth()->user()->role !== 'admin') {
return redirect('/login');
}

return $next($request);
}
}

// routes/web.php
Route::get('/admin', function () {
return 'Admin Area';
})->middleware('admin');
Real-world example
// app/Http/Middleware/CheckSubscription.php
class CheckSubscription
{
public function handle(Request $request, Closure $next)
{
$user = auth()->user();

if (!$user || !$user->is_active || !$user->subscription_active) {
return redirect('/billing')->with('error', 'Active subscription required.');
}

return $next($request);
}
}

Route::middleware(['auth', 'subscription'])->group(function () {
Route::get('/reports', [ReportController::class, 'index']);
});
Advanced usage
// app/Http/Middleware/EnsureRole.php
class EnsureRole
{
public function handle(Request $request, Closure $next, string $role)
{
if (!auth()->check() || auth()->user()->role !== $role) {
abort(403, 'Unauthorized');
}

$response = $next($request);
$response->headers->set('X-App-Role', $role);

return $response;
}
}

Route::get('/manager', function () {
return 'Manager Portal';
})->middleware('role:manager');

Common Mistakes

  • Forgetting to register middleware: if Laravel does not know its alias or group placement, route usage will fail. Register it correctly before attaching it.
  • Not returning $next($request): this stops the request pipeline and can cause blank or broken responses.
  • Placing business logic inside middleware: keep middleware focused on request filtering, not large database workflows or controller-like processing.
  • Using redirects in APIs carelessly: API routes often need JSON error responses instead of browser redirects.

Best Practices

  • Keep middleware small, single-purpose, and easy to test.
  • Use route groups to reduce repetition when many routes share the same protection.
  • Prefer descriptive names such as subscription or role over vague aliases.
  • Return the correct response type for web and API requests.
  • Use middleware parameters when rules vary by route, such as roles or permissions.

Practice Exercises

  • Create middleware that allows access only to authenticated users and redirects guests to /login.
  • Build middleware that checks whether a user has the role editor and aborts with 403 if not.
  • Write middleware that adds a custom response header called X-Learning-Stage with the value middleware-basics.

Mini Project / Task

Build a small admin section with three routes: dashboard, users, and settings. Protect all three using an admin middleware so only logged-in administrators can access them.

Challenge (Optional)

Create parameterized middleware that accepts a role name from the route and supports different protected areas such as admin, manager, and editor without duplicating middleware classes.

Custom Middleware

Custom middleware in Laravel lets you inspect, filter, or modify an HTTP request before it reaches a controller, and also change the response before it is sent back to the browser.
Think of middleware as a checkpoint in your application pipeline. In real projects, middleware is used for tasks like checking if a user is logged in, confirming email verification, blocking users without a subscription, logging requests, restricting access by role, or forcing API clients to send a custom header.
Laravel includes built-in middleware such as authentication and CSRF protection, but custom middleware is necessary when your business rules are unique. There are two common ways middleware is used: globally for every request, or assigned to specific routes, route groups, and controllers. Another useful distinction is before middleware, which runs before the request reaches your application logic, and after middleware, which runs after your controller has produced a response.
A middleware class usually contains a handle() method. This method receives the incoming $request and a $next callback. If the request should continue, call return $next($request);. If not, return a redirect, JSON error, or another response immediately.

Step-by-Step Explanation

Create middleware with Artisan using php artisan make:middleware EnsureUserIsAdmin.
Laravel stores it in app/Http/Middleware.
Inside handle(), inspect request data, authentication state, headers, or route parameters.
If the request passes your rule, call $next($request).
To attach it, register an alias in the application middleware configuration, then use that alias on routes or route groups. Middleware can also accept parameters, which is helpful for role checks like role:admin or role:editor.

Comprehensive Code Examples

Basic example
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;

class EnsureUserIsAdmin
{
public function handle(Request $request, Closure $next)
{
if (!auth()->check() || auth()->user()->role !== 'admin') {
return redirect('/dashboard')->with('error', 'Admins only.');
}

return $next($request);
}
}
use App\Http\Controllers\AdminController;
use Illuminate\Support\Facades\Route;

Route::get('/admin', [AdminController::class, 'index'])->middleware('admin');
Real-world example
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;

class EnsureSubscriptionIsActive
{
public function handle(Request $request, Closure $next)
{
$user = $request->user();

if (!$user || !$user->subscription_active) {
return redirect('/billing')->with('error', 'Activate your subscription first.');
}

return $next($request);
}
}
Route::middleware(['auth', 'subscribed'])->group(function () {
Route::get('/reports', function () {
return 'Premium reports';
});
});
Advanced usage
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;

class EnsureUserHasRole
{
public function handle(Request $request, Closure $next, string $role)
{
if (!$request->user() || $request->user()->role !== $role) {
abort(403, 'Unauthorized');
}

$response = $next($request);
$response->headers->set('X-Access-Checked', 'true');

return $response;
}
}
Route::get('/editor/tools', function () {
return 'Editor tools';
})->middleware('role:editor');

Common Mistakes

  • Forgetting to call $next($request): the request never reaches the controller. Always return it when access is allowed.
  • Using middleware alias without registration: Laravel cannot find it. Register the alias before using it on routes.
  • Redirecting API requests to HTML pages: APIs should usually return JSON errors like 401 or 403 instead of browser redirects.
  • Placing heavy business logic inside middleware: keep middleware focused on request filtering and access rules.

Best Practices

  • Keep each middleware responsible for one clear rule.
  • Use descriptive names such as EnsureSubscriptionIsActive.
  • Prefer route middleware for feature-specific checks instead of applying everything globally.
  • Return proper status codes for web and API contexts.
  • Combine middleware in groups for cleaner route files.
  • Test middleware behavior with authenticated and unauthenticated users.

Practice Exercises

  • Create a middleware that allows access only to logged-in users with the role manager.
  • Build a middleware that checks whether a request contains a custom header named X-App-Key.
  • Write a middleware that redirects users to /profile if their email is not verified.

Mini Project / Task

Build an admin-only dashboard section protected by custom middleware. If a non-admin user visits the route, redirect them to the main dashboard with an error message.

Challenge (Optional)

Create a parameterized middleware that accepts multiple allowed roles, such as role:admin,editor, and grants access if the current user matches any one of them.

Blade Templating Engine

Blade is Laravel’s built-in templating engine used to create dynamic HTML views in a clean and readable way. It exists to separate presentation from backend logic while still letting developers inject data, run simple conditions, loop through collections, and compose reusable layouts. In real applications, Blade is used for landing pages, dashboards, blog posts, ecommerce catalogs, user profiles, email templates, and admin panels. Instead of writing plain PHP directly inside HTML, Blade provides expressive directives such as @if, @foreach, @extends, @section, and @include. Key ideas include output escaping with {{ }}, raw output with {!! !!}, control structures, template inheritance, components, slots, stacks, and partials. Template inheritance lets you create a master layout and fill specific areas from child views. Components help you build reusable UI pieces like buttons, cards, alerts, and navigation bars. Blade compiles templates into optimized PHP, so it stays fast while remaining easy to write and maintain.

Step-by-Step Explanation

Blade files usually live in resources/views and use the .blade.php extension.
To print data safely, use {{ $name }}. Blade automatically escapes HTML to help prevent XSS issues.
To add conditions, use @if, @elseif, @else, and @endif.
To repeat content, use @foreach, @for, @while, or @forelse when a list may be empty.
For reusable layouts, create a base file with @yield placeholders, then in child views use @extends and @section.
For reusable fragments, use @include or Blade components such as .
Use @csrf in forms and @method('PUT') or similar for spoofing HTTP verbs in HTML forms.
Use comments with {{-- comment --}} so they do not appear in the rendered page.

Comprehensive Code Examples

Basic example

Welcome, {{ $user->name }}


@if($user->is_admin)

You have administrator access.


@else

You are logged in as a standard user.


@endif
Real-world example



@yield('title', 'My Store')


@include('partials.nav')
@yield('content')





@extends('layouts.app')

@section('title', 'Products')

@section('content')

Product List


@forelse($products as $product)

{{ $product->name }}


${{ $product->price }}



@empty

No products available.


@endforelse
@endsection
Advanced usage


{{ $slot }}




Profile updated successfully.


@push('scripts')

@endpush

Common Mistakes

  • Using raw output unnecessarily: Beginners use {!! $data !!} for normal text. Fix: prefer {{ $data }} unless you fully trust sanitized HTML.
  • Putting heavy business logic in views: Complex calculations make templates messy. Fix: prepare data in controllers, view models, or components.
  • Forgetting layout inheritance: Repeating headers and footers across files causes duplication. Fix: use @extends, @section, and shared layouts.
  • Missing empty-state handling: Lists may render blank pages. Fix: use @forelse or add fallback messages.

Best Practices

  • Keep Blade focused on presentation, not application rules.
  • Use components and partials for repeated UI patterns.
  • Escape output by default for security.
  • Name views clearly and organize them by feature folders.
  • Use layouts for consistency across pages.
  • Prefer readable directives over embedded raw PHP.

Practice Exercises

  • Create a Blade view that prints a user’s name and shows a different message depending on whether the user is active.
  • Build a page that loops through an array of courses and displays each course title. Show a fallback message if the array is empty.
  • Create a master layout with a page title and content area, then make a child view that fills both sections.

Mini Project / Task

Build a simple blog listing page using Blade. Create a base layout, include a navigation partial, loop through blog posts, display title and excerpt, and show a friendly message when there are no posts.

Challenge (Optional)

Create a reusable Blade component for a product card that accepts a product name, price, and stock status, then render it inside a loop on a catalog page.

Blade Directives

Blade directives are Laravel’s special template commands that begin with the @ symbol. They exist to make view files easier to read, faster to write, and safer than filling HTML with raw PHP. In real projects, Blade directives are used in pages such as dashboards, product listings, profile pages, admin panels, and forms where content must change based on data, authentication state, or loops. For example, you may show a welcome message only if a user is logged in, loop through orders in a table, or include a common layout for every page.

Blade includes several useful directive groups. Conditional directives include @if, @elseif, @else, and @unless for decision-making. Loop directives include @for, @foreach, @forelse, and @while for repeating markup. Template structure directives such as @extends, @section, @yield, and @include help split views into reusable parts. Authentication and environment directives like @auth, @guest, @env, and @production are also common. Blade compiles these templates into plain PHP behind the scenes, so you get a clean syntax without losing PHP power.

Step-by-Step Explanation

To use a Blade directive, place it directly inside a .blade.php file. A simple condition starts with @if(condition), then HTML, and ends with @endif. A loop such as @foreach($items as $item) repeats HTML for every item and closes with @endforeach. Layout directives connect pages together: @extends('layouts.app') tells a view which layout to use, @section('content') defines page content, and the layout prints that content with @yield('content'). Use {{ }} to safely echo data and avoid accidental raw output.

Beginners should remember one rule: Blade directives control presentation, not business logic. Keep database queries and heavy calculations in controllers, view models, or components, then pass clean data into the view.

Comprehensive Code Examples

Basic example

Welcome


@if($isMember)

Thanks for being a member.


@else

Please sign up.


@endif
Real-world example
@forelse($products as $product)

{{ $product->name }}


${{ $product->price }}


@if($product->in_stock)

In stock


@else

Out of stock


@endif

@empty

No products available.


@endforelse
Advanced usage



@include('partials.nav')
@yield('content')




@extends('layouts.app')

@section('content')
@auth

Hello, {{ auth()->user()->name }}


@endauth

@guest

Please log in to continue.


@endguest

@for($i = 1; $i <= 3; $i++)

Notification {{ $i }}


@endfor
@endsection

Common Mistakes

  • Forgetting closing directives: Always match @if with @endif and @foreach with @endforeach.
  • Using raw PHP unnecessarily: Prefer Blade syntax for readability unless raw PHP is truly needed.
  • Putting business logic in views: Move calculations, queries, and filtering into controllers or dedicated classes.
  • Confusing escaped and raw output: Use {{ }} for safe output; avoid unescaped output unless you trust the content.

Best Practices

  • Keep templates focused on display logic only.
  • Use layouts and includes to avoid repeating header, footer, and navigation markup.
  • Prefer @forelse when listing data that may be empty.
  • Use authentication directives like @auth and @guest for clarity.
  • Name sections consistently so layouts stay predictable and easy to maintain.

Practice Exercises

  • Create a Blade view that shows a different message for logged-in and guest users.
  • Loop through an array of course names and display each one inside a paragraph.
  • Build a product list using @forelse and show an empty-state message when no products exist.

Mini Project / Task

Create a simple dashboard page using @extends, @section, @include, @auth, and @foreach. Show a navigation partial, a personalized greeting for logged-in users, and a short list of recent notifications.

Challenge (Optional)

Build a reusable layout with a sidebar and content area, then create two different pages that extend the same layout and conditionally render different menu items based on whether the user is an admin.

Layouts and Components

Layouts and components in Laravel are Blade features used to build reusable, organized, and maintainable user interfaces. A layout acts like a shared page skeleton for pages such as dashboards, blogs, and admin panels. It usually contains common parts like the HTML head, navigation bar, footer, and content area. Components are smaller reusable UI pieces such as buttons, alerts, cards, modals, or form fields. In real projects, these tools reduce repeated code, improve consistency, and make design updates easier because you change one shared file instead of many separate views.

Laravel uses Blade directives like @extends, @section, @yield, and @include for layout-based composition. It also supports Blade components using tags like or . Layouts are ideal for page-level structure, while components are best for smaller reusable interface elements. Anonymous components live in the resources/views/components folder and are quick to create. Class-based components add PHP logic and are useful when data preparation becomes more complex.

Step-by-Step Explanation

Start by creating a base layout file such as resources/views/layouts/app.blade.php. Inside it, place shared markup and use @yield('content') where each page should inject its unique content. Then create a page like home.blade.php and connect it using @extends('layouts.app'). Define a matching section with @section('content') and close it with @endsection.

For components, create a Blade file such as resources/views/components/alert.blade.php. Inside it, use variables like {{ $type }} and {{ $slot }}. Render it in a page using the component tag syntax. If you need logic, generate a class-based component with Artisan and pass data from the component class into the view.

Comprehensive Code Examples

Basic example



My Laravel App



@yield('content')
Copyright 2026





@extends('layouts.app')

@section('content')

Welcome Home


This page uses the shared layout.


@endsection
Real-world example


{{ $slot }}



@extends('layouts.app')

@section('content')

Profile Settings



Your profile was updated successfully.

@endsection
Advanced usage


{{ $title }}


{{ $slot }}




@extends('layouts.app')

@section('content')

All systems operational.



5 new users registered today.

@endsection

Common Mistakes

  • Using the wrong section name: If @yield('content') is in the layout, the child view must use @section('content').
  • Forgetting component files or wrong paths: Anonymous components must be placed in resources/views/components with correct naming.
  • Repeating markup instead of reusing components: Move repeated alerts, cards, and buttons into components to keep views clean.

Best Practices

  • Keep layouts focused on structure: Put global page scaffolding in layouts and smaller UI blocks in components.
  • Use meaningful component names: Names like alert, card, and form-input make templates easier to read.
  • Prefer reusable design patterns: If the same HTML appears more than twice, consider extracting it into a component.
  • Keep business logic out of Blade views: Prepare data in controllers or class-based components.

Practice Exercises

  • Create a layout with a header, sidebar, and footer, then build a page that extends it.
  • Create an anonymous component named badge that displays colored labels like success or warning.
  • Build a page that uses two different reusable card components to show product information.

Mini Project / Task

Build a simple admin dashboard using one shared layout and at least three reusable components: a notification alert, a statistics card, and a user info panel.

Challenge (Optional)

Create a class-based component that accepts a collection of navigation links and renders an active menu state based on the current route.

Working with Forms

Forms are one of the most important parts of any Laravel application because they allow users to send data to the server. In real projects, forms are used for registration, login, contact pages, checkout flows, profile updates, search filters, and admin dashboards. Laravel makes form handling easier by combining Blade templates, routes, controllers, request objects, CSRF protection, validation, and old input helpers. A form usually has two sides: the frontend markup that collects input and the backend logic that receives, validates, and stores it. In Laravel, forms commonly use GET for reading data such as search terms and POST for submitting new data. You may also simulate PUT, PATCH, and DELETE for updates and deletions. Laravel also protects forms from cross-site request forgery with the @csrf directive, which is required for state-changing requests. Another important concept is validation. Before saving form data, Laravel checks whether required fields are present, whether email values are valid, and whether length or format rules are satisfied. When validation fails, errors are automatically returned to the view, making it easy to show feedback. Blade helpers such as old() help preserve previous input so users do not need to retype values. This creates a better user experience and is standard in professional web applications.

Step-by-Step Explanation

Start by creating a route that shows the form and another route that handles submission. Next, create a Blade view with HTML form elements such as text inputs, email fields, textareas, selects, checkboxes, or file inputs. Add the form action pointing to the submit route and choose the correct method. Include @csrf in every non-GET form. In the controller, access submitted values through the request object, then validate them using $request->validate(). If validation passes, process the data, such as saving a model or sending an email. If it fails, Laravel redirects back with errors and old input. In the Blade template, display errors with $errors and refill fields using old('field'). For updates, use method spoofing with @method('PUT') or @method('PATCH'). For file uploads, set enctype='multipart/form-data'.

Comprehensive Code Examples

// routes/web.php
use App\Http\Controllers\ContactController;

Route::get('/contact', [ContactController::class, 'create']);
Route::post('/contact', [ContactController::class, 'store']);


@csrf






@if ($errors->any())

    @foreach ($errors->all() as $error)
  • {{ $error }}

  • @endforeach

@endif
// app/Http/Controllers/ContactController.php
namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ContactController extends Controller
{
public function create()
{
return view('contact');
}

public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|min:2',
'email' => 'required|email',
'message' => 'required|min:10',
]);

return back()->with('success', 'Form submitted successfully.');
}
}


@csrf
@method('PUT')
username) }}">



@csrf


Common Mistakes

  • Forgetting CSRF protection: A POST form without @csrf will fail. Always include it.
  • Using the wrong method: Use GET for searches and POST/PUT/PATCH/DELETE for changing data.
  • Not preserving old input: Without old(), users must re-enter data after validation errors.
  • Ignoring validation errors: Always show feedback so users know what to fix.
  • Missing multipart encoding for files: File uploads require enctype='multipart/form-data'.

Best Practices

  • Keep form views simple and move business logic into controllers or form request classes.
  • Validate all incoming data on the server, even if client-side validation exists.
  • Use named routes instead of hardcoded URLs when possible.
  • Display success and error messages clearly for better usability.
  • Use method spoofing for updates and deletes to follow REST-style conventions.

Practice Exercises

  • Create a feedback form with name, email, and comment fields, then validate all fields.
  • Build a profile edit form that uses @method('PATCH') and fills inputs with existing user data.
  • Create a search form that uses the GET method and displays the submitted keyword.

Mini Project / Task

Build a contact form page that validates input, redisplays old values after errors, and shows a success message after a valid submission.

Challenge (Optional)

Create a product submission form with text inputs, a category select box, a checkbox for availability, and an image upload field, then validate and process all fields correctly.

CSRF Protection

CSRF, or Cross-Site Request Forgery, is a security attack where a malicious website tricks an authenticated user into submitting a request to another website without the user intentionally approving that action. In real applications, this could mean changing an account email, submitting a payment form, deleting data, or updating a password while the victim is still logged in. Laravel includes built-in CSRF protection to stop these attacks by verifying that state-changing requests come from trusted forms or trusted JavaScript requests generated by your own application.

Laravel uses a unique token tied to the user session. When a form sends a POST, PUT, PATCH, or DELETE request, Laravel checks whether the submitted token matches the token stored in the session. If the token is missing or invalid, Laravel rejects the request. In practice, you will most often add CSRF tokens in Blade forms with the @csrf directive. For AJAX or fetch requests, the token is commonly sent through a request header.

Although CSRF protection mainly applies to state-changing requests, understanding request types matters. GET requests should generally only read data and should not modify server state. Laravel’s middleware handles CSRF validation automatically for web routes, so developers mainly need to remember when and where to include the token.

Step-by-Step Explanation

First, define a route that shows a form and another route that processes the submitted data. Second, create a Blade view containing the form. Third, include the CSRF token using @csrf. Laravel converts that directive into a hidden input field containing the current session token. Fourth, submit the form. When the request reaches the application, the CSRF middleware compares the submitted token with the session token. If they match, the request continues. If they do not, Laravel returns a 419 error, often shown as Page Expired.

For JavaScript requests, place the token in a meta tag or read it from the page and send it in the X-CSRF-TOKEN header. This is common when using fetch, Axios, or custom frontend scripts. The key beginner rule is simple: every state-changing request in the web middleware group must carry a valid CSRF token.

Comprehensive Code Examples

Basic example
// routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

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

Route::post('/profile', function (Request $request) {
return 'Profile updated for: ' . $request->input('name');
});


@csrf


Real-world example
id) }}">
@csrf
@method('DELETE')

Advanced usage

Common Mistakes

  • Forgetting @csrf in forms: This causes 419 errors. Add @csrf to every non-GET Blade form.
  • Using GET for destructive actions: Never delete or update data with GET. Use POST, PUT, PATCH, or DELETE.
  • Ignoring AJAX headers: JavaScript requests also need the token. Send X-CSRF-TOKEN with the request.
  • Disabling protection unnecessarily: Avoid excluding routes from CSRF checks unless there is a clear reason, such as a separately secured webhook endpoint.

Best Practices

  • Always use the @csrf directive instead of manually writing hidden inputs when working in Blade.
  • Keep state changes in web routes protected by middleware so Laravel can validate tokens automatically.
  • Use proper HTTP verbs and pair them with @method when simulating PUT, PATCH, or DELETE in forms.
  • Standardize JavaScript setup so every AJAX request automatically includes the CSRF token.
  • Treat 419 errors as debugging signals that usually point to a missing token, expired session, or incorrect request flow.

Practice Exercises

  • Create a contact form with name and message fields and secure it with a CSRF token.
  • Build an edit profile form that submits a POST request and verify it works only when @csrf is present.
  • Create a delete button for a blog post using @csrf and @method('DELETE').

Mini Project / Task

Build a simple task manager page where users can add a task and delete a task. Protect both actions with Laravel CSRF protection in Blade forms, and test what happens when the token is removed.

Challenge (Optional)

Create a Blade page that submits a new comment through fetch instead of a normal form. Include the CSRF token in a meta tag and send it correctly in the request header so Laravel accepts the submission.

Form Validation

Form validation in Laravel is the process of checking user input before it is accepted, stored, or processed. It exists to keep applications secure, prevent bad data from entering the database, and give users helpful feedback when they make mistakes. In real projects, validation is used in registration forms, login screens, profile updates, contact forms, product creation pages, and payment-related workflows. Laravel makes this easy by offering readable validation rules, automatic redirection on failure, and structured error messages that integrate well with Blade views.

Laravel supports several validation approaches. You can validate directly inside a controller with the validate method, use the Validator facade for more control, or move rules into Form Request classes for cleaner and reusable code. Common rule types include required fields, string length, numeric checks, email format, uniqueness in the database, file validation, and conditional rules. This system helps developers separate business requirements from raw request handling while keeping code readable and maintainable.

Step-by-Step Explanation

To validate a form, first create an HTML form that sends data to a route. In the controller method handling the request, call $request->validate() and pass an array of field names and rules. Laravel checks each rule in order. If validation fails, the user is redirected back automatically and the errors are stored in the session. If validation passes, the method continues and you can safely use the validated data.

Rules are written as strings like required|email or as arrays when you need more clarity. You can also customize error messages by passing a second array. For larger applications, use a Form Request class created with Artisan. This moves authorization and validation logic into a dedicated class, making controllers smaller and easier to test.

Comprehensive Code Examples

Basic example
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|min:3|max:100',
'email' => 'required|email',
'password' => 'required|min:8'
]);

User::create($validated);

return redirect()->back()->with('success', 'User created successfully.');
}
Real-world example
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'price' => 'required|numeric|min:0',
'image' => 'nullable|image|mimes:jpg,jpeg,png|max:2048',
'sku' => 'required|string|unique:products,sku'
], [
'sku.unique' => 'This SKU already exists.'
]);

Product::create($validated);

return redirect()->route('products.index');
}
Advanced usage
use Illuminate\Support\Facades\Validator;

public function update(Request $request, User $user)
{
$validator = Validator::make($request->all(), [
'name' => ['required', 'string', 'max:100'],
'email' => ['required', 'email', 'unique:users,email,' . $user->id],
'phone' => ['nullable', 'regex:/^[0-9]{10,15}$/']
]);

if ($validator->fails()) {
return redirect()->back()->withErrors($validator)->withInput();
}

$user->update($validator->validated());

return redirect()->route('profile.show');
}

Common Mistakes

  • Skipping validation completely: Developers sometimes trust browser input. Fix this by always validating on the server.
  • Using the wrong unique rule during updates: This causes existing records to fail validation. Fix it by excluding the current model ID.
  • Forgetting to show errors in the view: Validation may fail silently for users. Fix it by displaying the error bag in Blade.
  • Accepting unsafe file uploads: Fix it by adding image, mimes, and max rules.

Best Practices

  • Use Form Request classes for large forms and reusable validation logic.
  • Keep rules close to business requirements and name fields clearly.
  • Always validate files, numeric ranges, and database uniqueness.
  • Return users with old input so they do not need to retype valid fields.
  • Customize messages when default errors are too technical for end users.

Practice Exercises

  • Create a registration validator with rules for name, email, and password confirmation.
  • Build a product form validator that checks title, price, description, and image upload.
  • Create an update form where email must remain unique except for the current user.

Mini Project / Task

Build a contact form in Laravel with fields for name, email, subject, and message. Validate all fields, return error messages to the Blade view, and show a success message when the submission is valid.

Challenge (Optional)

Create a Form Request class for a job application form that validates text fields, an uploaded resume file, and a portfolio URL only when the applicant selects a senior-level position.

Custom Validation Rules

Custom validation rules in Laravel let you create your own logic when built-in rules such as required, email, min, or unique are not enough. They exist because real applications often need business-specific checks, such as verifying that a username does not contain reserved words, ensuring a booking date is not on a holiday, or confirming that a coupon code matches a company format. In real life, these rules are used in registration forms, checkout systems, admin dashboards, and APIs where data quality matters. Laravel supports custom validation through rule objects and closure-based rules. Rule objects are reusable classes, which makes them ideal for larger applications and team projects. Closure rules are faster for very small cases but are less reusable. The main idea is simple: Laravel sends a field value to your rule, your rule decides whether it passes, and if it fails, Laravel returns a message to the user. This keeps validation logic clean and separate from controllers. Custom rules are especially useful when you want readable code, centralized business rules, and easier testing.

Step-by-Step Explanation

To create a reusable rule object, run Artisan to generate a rule class. Laravel places it in the app/Rules directory. In modern Laravel versions, the rule usually implements ValidationRule and contains a validate method. That method receives the attribute name, the value, and a failure callback. If your condition is not satisfied, call the callback with an error message. After that, attach the rule to a field inside a controller, form request, or manual validator. You can also combine it with normal rules such as required and string. Closure rules work similarly, but instead of making a class, you write an anonymous function directly in the rules array. Use closure rules for quick, one-off validation, and rule objects for logic you may reuse across multiple forms.

Comprehensive Code Examples

Basic example
php artisan make:rule UppercaseOnly
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class UppercaseOnly implements ValidationRule
{
public function validate(string $attribute, mixed $value, Closure $fail): void
{
if ($value !== strtoupper($value)) {
$fail('The '.$attribute.' must be uppercase.');
}
}
}
use App\Rules\UppercaseOnly;

request()->validate([
'code' => ['required', 'string', new UppercaseOnly],
]);
Real-world example
namespace App\Rules;

use Closure;
use Illuminate\Contracts\Validation\ValidationRule;

class NotReservedUsername implements ValidationRule
{
protected array $reserved = ['admin', 'root', 'support'];

public function validate(string $attribute, mixed $value, Closure $fail): void
{
if (in_array(strtolower($value), $this->reserved)) {
$fail('This username is reserved.');
}
}
}
request()->validate([
'username' => ['required', 'min:3', new \App\Rules\NotReservedUsername],
]);
Advanced usage
request()->validate([
'coupon' => [
'required',
function (string $attribute, mixed $value, Closure $fail) {
if (!preg_match('/^SALE-[A-Z0-9]{6}$/', $value)) {
$fail('The coupon format is invalid.');
}
},
],
]);

This advanced example uses a closure rule for a strict coupon pattern. It is useful when the rule is simple and local to one form.

Common Mistakes

  • Putting business validation in controllers only: move repeated logic into custom rules to keep controllers small.
  • Using closure rules everywhere: use rule classes for reusable or testable validation logic.
  • Forgetting to combine custom rules with basic rules: still use required, string, or max when needed.
  • Writing unclear error messages: return messages that tell users exactly what needs to be fixed.

Best Practices

  • Prefer rule objects for reuse, readability, and unit testing.
  • Keep one responsibility per rule so each class validates a single concept.
  • Use descriptive class names like ValidCouponFormat or NotReservedUsername.
  • Validate close to input boundaries using form requests or request validation before saving data.
  • Write tests for custom rules, especially when they enforce business-critical requirements.

Practice Exercises

  • Create a custom rule that allows only even numbers for a field named quantity.
  • Create a custom rule that rejects a product name if it contains the word test.
  • Write a closure rule that accepts an order code only if it starts with ORD-.

Mini Project / Task

Build a registration form that validates username with a custom rule rejecting reserved names and validates referral_code with a closure rule enforcing a company-specific format.

Challenge (Optional)

Create a custom validation rule that checks whether a selected appointment date falls on a weekday and rejects weekends with a clear custom error message.

Database Configuration


Database configuration in Laravel is a critical step that connects your application to its data storage. It defines how Laravel interacts with various database systems, allowing your application to store, retrieve, update, and delete information persistently. Without proper database configuration, your Laravel application cannot function as intended, as most modern web applications rely heavily on databases to manage user data, content, and application state. Laravel's configuration system is designed to be flexible and easy to manage, supporting multiple database drivers like MySQL, PostgreSQL, SQLite, and SQL Server out of the box. This abstraction layer means you can switch databases with minimal code changes, making your application more portable and adaptable to different environments. In real-world applications, database configuration is essential for everything from user authentication and e-commerce product catalogs to content management systems and analytical dashboards. It's the foundation upon which dynamic, data-driven web experiences are built.

Laravel stores its database configuration primarily in the .env file for environment-specific settings and in config/database.php for core configuration options. The .env file uses key-value pairs to store sensitive information like database credentials, ensuring they are not hardcoded into your application's codebase and can be easily changed per environment (development, staging, production). The config/database.php file defines all possible database connections, including their drivers, hosts, ports, database names, usernames, and passwords. It also specifies the default connection to be used if not explicitly stated elsewhere. Laravel's Eloquent ORM (Object-Relational Mapper) relies heavily on these configurations to facilitate object-oriented interaction with your database, abstracting away raw SQL queries and providing a more intuitive way to work with data.

Step-by-Step Explanation


Configuring your database in Laravel involves a few straightforward steps, primarily modifying the .env file and understanding the config/database.php file.

1. Locate the .env file: This file is at the root of your Laravel project. If you've just cloned a project, you might find a .env.example file. You should copy this file and rename it to .env.

2. Open .env and configure database credentials: Inside this file, you'll find variables related to your database connection. The most common ones for a MySQL database are:
  • DB_CONNECTION: Specifies the database driver (e.g., mysql, pgsql, sqlite, sqlsrv).
  • DB_HOST: The host address of your database server (e.g., 127.0.0.1 or localhost).
  • DB_PORT: The port number your database server is listening on (e.g., 3306 for MySQL, 5432 for PostgreSQL).
  • DB_DATABASE: The name of the database you want to connect to.
  • DB_USERNAME: The username for connecting to the database.
  • DB_PASSWORD: The password for the specified username.
Adjust these values to match your database setup.

3. Understand config/database.php (optional for basic setup): This file defines the actual connection configurations. The values from your .env file are loaded here using the env() helper function. For example, the mysql connection array will typically look like this:
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],

You generally don't need to modify this file unless you're setting up a custom database connection or specific driver options not exposed via .env variables.

4. Clear Configuration Cache (if applicable): If you've cached your configuration (e.g., in a production environment), you might need to clear it after making changes to your .env file to ensure the new values are loaded:
php artisan config:clear

5. Test the connection: The simplest way to test is to run a migration. If your database credentials are correct and the database exists, the migrations should run without errors.
php artisan migrate

Comprehensive Code Examples


Basic Example: MySQL Configuration in .env

This is the most common setup for a development environment.
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:YOUR_APP_KEY_HERE
APP_DEBUG=true
APP_URL=http://localhost

LOG_CHANNEL=stack
LOG_LEVEL=debug

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

Real-world Example: PostgreSQL Configuration

For production environments or when using PostgreSQL, the .env file would be updated accordingly. Note the different port and connection name.
APP_NAME=ProductionApp
APP_ENV=production
APP_KEY=base64:YOUR_PRODUCTION_APP_KEY
APP_DEBUG=false
APP_URL=https://myapp.com

LOG_CHANNEL=daily
LOG_LEVEL=error

DB_CONNECTION=pgsql
DB_HOST=your_prod_db_host.com
DB_PORT=5432
DB_DATABASE=prod_app_db
DB_USERNAME=prod_user
DB_PASSWORD=secure_prod_password

Advanced Usage: Multiple Database Connections

Sometimes you need to connect to more than one database (e.g., a primary application database and a separate analytics database). You define these in config/database.php and then specify their credentials in .env.

In .env:
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=app_db
DB_USERNAME=app_user
DB_PASSWORD=app_pass

ANALYTICS_DB_CONNECTION=pgsql
ANALYTICS_DB_HOST=analytics_db_host
ANALYTICS_DB_PORT=5432
ANALYTICS_DB_DATABASE=analytics_data
ANALYTICS_DB_USERNAME=analytics_user
ANALYTICS_DB_PASSWORD=analytics_pass

In config/database.php (add a new connection array):
'connections' => [

'sqlite' => [...],
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],

'analytics' => [
'driver' => env('ANALYTICS_DB_CONNECTION', 'pgsql'),
'host' => env('ANALYTICS_DB_HOST', '127.0.0.1'),
'port' => env('ANALYTICS_DB_PORT', '5432'),
'database' => env('ANALYTICS_DB_DATABASE', 'analytics'),
'username' => env('ANALYTICS_DB_USERNAME', 'forge'),
'password' => env('ANALYTICS_DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'schema' => 'public',
'sslmode' => 'prefer',
],

// ... other connections
],

To use the analytics connection, you would specify it when performing database operations:
// Using DB facade
$users = DB::connection('analytics')->table('users')->get();

// Using Eloquent (requires defining $connection property in model)
class AnalyticsUser extends Model
{
protected $connection = 'analytics';
protected $table = 'users';
}

Common Mistakes


  • Incorrect .env variables: Typos in variable names (e.g., DB_DATABSE instead of DB_DATABASE) or incorrect values for host, port, username, or password are common. Double-check all values against your actual database credentials.
  • Forgetting to clear config cache: In production environments, Laravel caches configuration for performance. If you update .env values and don't run php artisan config:clear, your application will continue to use the old cached values, leading to unexpected connection errors.
  • Database not running or accessible: Sometimes the issue isn't with Laravel's configuration but with the database server itself. Ensure your database server (e.g., MySQL, PostgreSQL) is running and accessible from where your Laravel application is hosted (e.g., check firewall rules, correct host address).

Best Practices


  • Use .env for credentials: Always store sensitive information like database passwords and API keys in your .env file, and ensure .env is excluded from version control (e.g., via .gitignore).
  • Specific database users: Create a dedicated database user for your application with only the necessary privileges, rather than using a superuser like root in production. This enhances security.
  • Test connection early: After configuring your database, run php artisan migrate or a simple database query early in development to confirm the connection is working correctly.
  • Backup your database: Regularly back up your database, especially before major deployments or schema changes. While not strictly configuration, it's a critical aspect of database management.

Practice Exercises


  • Exercise 1 (Beginner): Create a new Laravel project. Set up a MySQL database named my_first_app_db locally. Configure your .env file to connect to this database using the username laravel_user and password secret. Run php artisan migrate to verify the connection.
  • Exercise 2 (Intermediate): Change your database connection from MySQL to SQLite. Modify your .env file to use SQLite and ensure the database file is created in the database directory (e.g., database/database.sqlite). Run migrations again to populate the SQLite database.
  • Exercise 3 (Advanced): Imagine you have two separate PostgreSQL databases: one for users (users_db) and one for products (products_db). Modify your .env and config/database.php files to define two distinct PostgreSQL connections. Ensure you can connect to both from your Laravel application.

Mini Project / Task


Create a simple Laravel application that uses two different database connections. One connection should be your primary database (e.g., MySQL) for storing blog posts. The second connection should be a separate database (e.g., SQLite) used only for storing visitor analytics data (e.g., IP address, page visited, timestamp). Ensure your application can successfully write to both databases using their respective configurations.

Challenge (Optional)


Extend the previous mini-project. Implement a custom database connection driver in config/database.php that uses a different port for the analytics database if the application environment is 'local', but uses the default port if the environment is 'production'. This requires using env() with conditional logic or default values within your config/database.php file.

Migrations Basics


Laravel Migrations are a fundamental feature that allows you to manage your database schema using PHP code. Instead of writing raw SQL queries, migrations provide a version control system for your database, enabling teams to easily modify and share the application's database schema. This approach ensures that all developers working on a project have a consistent database structure, preventing 'it works on my machine' scenarios related to database discrepancies. Migrations are crucial for the lifecycle of any application, from initial development to deployment and future updates. They allow you to define tables, columns, indexes, and even modify existing structures without losing data, all while keeping a history of changes.

In real-world applications, migrations are used every single day. When a new feature requires a new table or a modification to an existing one, a migration is created. This migration is then run, applying the changes to the database. When a new developer joins a project, they simply run all outstanding migrations to get their local database up to date. This systematic approach eliminates manual database modifications and the errors that often come with them, making development faster, more reliable, and collaborative. They are especially powerful when combined with Laravel's Eloquent ORM, as your model definitions directly correspond to your database schema defined in migrations.

Step-by-Step Explanation


Migrations are typically created using an Artisan command. To create a new migration, you run: php artisan make:migration create_users_table. This command generates a new PHP file in the database/migrations directory. The filename usually includes a timestamp to ensure uniqueness and proper ordering. Inside this file, you'll find a class extending Illuminate\Database\Migrations\Migration with two main methods: up() and down(). The up() method is responsible for defining the database schema changes, such as creating tables, adding columns, or setting indexes. The down() method is used to reverse the operations performed in the up() method, allowing you to rollback migrations. For instance, if up() creates a table, down() should drop that table. Laravel provides a fluent schema builder that makes defining database structures intuitive. For example, to create a table, you use Schema::create('table_name', function (Blueprint $table) { ... });. To modify an existing table, you use Schema::table('table_name', function (Blueprint $table) { ... });. After writing your migration, you execute it using php artisan migrate. To undo the last batch of migrations, you use php artisan migrate:rollback. You can also reset all migrations and re-run them using php artisan migrate:reset or php artisan migrate:refresh (which also seeds the database).

Comprehensive Code Examples


Basic Example: Creating a new table

To create a products table:
php artisan make:migration create_products_table

This generates a file like 2023_10_27_123456_create_products_table.php:

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

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->decimal('price', 8, 2);
$table->integer('stock')->default(0);
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('products');
}
};

Real-world Example: Adding a column to an existing table

Suppose you need to add an is_featured column to the products table:
php artisan make:migration add_is_featured_to_products_table --table=products


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

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->boolean('is_featured')->default(false)->after('stock');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('is_featured');
});
}
};

Advanced Usage: Renaming a column and adding a foreign key

First, create a migration to rename a column (e.g., name to product_name):
php artisan make:migration rename_name_to_product_name_in_products_table --table=products


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::table('products', function (Blueprint $table) {
$table->renameColumn('name', 'product_name');
});
}

public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->renameColumn('product_name', 'name');
});
}
};

Next, create an categories table and add a foreign key to products:
php artisan make:migration create_categories_table


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('categories', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->timestamps();
});
}

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

Then, add the category_id foreign key to the products table:
php artisan make:migration add_category_id_to_products_table --table=products


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::table('products', function (Blueprint $table) {
$table->foreignId('category_id')
->nullable()
->constrained('categories')
->onDelete('set null');
});
}

public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropForeign(['category_id']); // Drop the foreign key constraint first
$table->dropColumn('category_id');
});
}
};


Common Mistakes



  • Forgetting down() method implementation or implementing it incorrectly: The down() method is crucial for rolling back changes. If you create a table in up(), you must drop it in down() using Schema::dropIfExists('table_name'). If you add a column, you must drop it using dropColumn('column_name'). Forgetting this can lead to issues when trying to rollback migrations.
    Fix: Always ensure your down() method perfectly reverses the operations in up(). For complex changes like foreign keys, remember to drop the foreign key constraint before dropping the column.

  • Modifying existing migration files instead of creating new ones: Once a migration has been run on a shared environment (like staging or production), you should never modify it. Changing an already executed migration can lead to inconsistencies between environments. Migrations are designed to be immutable history.
    Fix: If you need to make further changes to your database schema, always create a new migration file. For example, to add a new column, create a new 'add_column_to_table' migration, don't edit the original 'create_table' migration.

  • Not understanding the order of operations when rolling back foreign keys: When dropping a table that has foreign key constraints referencing it, or dropping a column that is a foreign key, you must drop the foreign key constraint first before dropping the table or column. Otherwise, you'll encounter SQL errors.
    Fix: In your down() method, use Schema::table('your_table', function (Blueprint $table) { $table->dropForeign(['column_name']); }); before $table->dropColumn('column_name');. When dropping a parent table, ensure all child tables referencing it are either dropped first or have their foreign keys removed/set to null.



Best Practices



  • Descriptive Migration Names: Use clear and descriptive names for your migration files (e.g., create_posts_table, add_status_to_users_table). This makes it easy to understand what each migration does at a glance.

  • Small, Focused Migrations: Each migration should ideally perform a single, logical change (e.g., create one table, add one column, rename one column). Avoid making many unrelated changes in a single migration. This makes rollbacks easier and reduces the chance of errors.

  • Test Migrations Locally: Always run your migrations locally and test their up() and down() methods using php artisan migrate and php artisan migrate:rollback before pushing them to a shared environment.

  • Use dropIfExists and dropColumn: When reversing table or column creations, use Schema::dropIfExists() and dropColumn() to prevent errors if the table/column doesn't exist during a rollback (e.g., if you're rolling back multiple steps).

  • Foreign Key Considerations: When adding foreign keys, consider using constrained() for convention-based names and onDelete() or onUpdate() actions for referential integrity. Always drop foreign keys before dropping the column they reside on in the down() method.



Practice Exercises



  1. Create a new Laravel project. Generate a migration to create a posts table with columns for title (string, unique), content (text), and published_at (timestamp, nullable). Run the migration and then rollback only this migration.

  2. Generate a new migration to add an author_id column (foreign key to the users table, assuming it exists) to your posts table. Ensure the foreign key has an onDelete('cascade') action. Run this migration.

  3. Create a migration to rename the content column in the posts table to body. After running this migration, try rolling back the last batch of migrations and observe the changes.



Mini Project / Task


Design and implement the database schema for a simple blog application using Laravel Migrations. Your schema should include:

  • A users table (with standard Laravel fields like name, email, password).

  • A posts table with at least title, slug (unique), body, and a foreign key to users.id.

  • A categories table with name (unique) and slug (unique).

  • A pivot table (many-to-many) to link posts and categories.


Ensure all foreign key relationships are correctly defined with appropriate onDelete actions. Run all migrations, then perform a migrate:refresh to ensure everything works as expected.

Challenge (Optional)


Extend the blog application schema from the mini-project. Add a comments table that can be polymorphic, meaning a comment can belong to either a Post or a Comment itself (for nested comments). Implement the necessary columns (body, user_id, commentable_id, commentable_type) and foreign key constraints using migrations. Think about how to handle the down() method for polymorphic relations and nested structures.

Creating and Running Migrations


Migrations in Laravel are like version control for your database schema. They allow you to define and modify your database tables using PHP code, rather than directly writing SQL. This provides several critical advantages: it keeps your database structure in sync across development environments, makes it easy to track changes, and allows teams to collaborate on schema modifications without conflicts. In real-world applications, migrations are indispensable for managing database evolution as features are added, removed, or modified. Imagine a scenario where you need to add a new column to a user table, or create an entirely new table for products. Instead of manually executing SQL commands on each developer's machine and on production servers, you write a migration. Laravel then handles the execution, ensuring consistency and providing a clear history of schema changes.

At its core, a migration is a simple PHP class with two main methods: up() and down(). The up() method defines the changes you want to apply to the database (e.g., creating a table, adding a column), while the down() method defines how to reverse those changes (e.g., dropping a table, removing a column). This rollback capability is crucial for development and deployment, allowing you to easily undo mistakes or revert to a previous state.

There are primarily two types of migrations based on their purpose: Schema Creation Migrations and Schema Modification Migrations. Schema creation migrations are used to build new tables, often including their initial columns and indexes. Schema modification migrations are used to alter existing tables, such as adding new columns, changing column types, or dropping columns. Laravel's Schema Facade provides a fluent and expressive API for interacting with your database schema, abstracting away the underlying SQL syntax and making database management intuitive.

Step-by-Step Explanation


Creating a migration involves using the Artisan command-line tool. To create a new migration, you run php artisan make:migration create_users_table. Laravel will automatically generate a new migration file in your database/migrations directory, named with a timestamp and the migration name. The --create or --table options can be used to pre-fill the migration with boilerplate code for creating or modifying a specific table. For example, php artisan make:migration create_products_table --create=products will generate a migration to create a products table, and php artisan make:migration add_price_to_products_table --table=products will generate one to modify the products table. Once the migration file is created, you edit the up() and down() methods to define your schema changes using the Schema Facade. Finally, to apply these changes to your database, you run php artisan migrate. To reverse the last batch of migrations, you use php artisan migrate:rollback. For more controlled rollbacks, php artisan migrate:rollback --step=1 allows you to rollback a specific number of migrations.

Comprehensive Code Examples


Basic example: Creating a new table

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

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->timestamps();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};

Real-world example: Adding a column and making it nullable

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

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('products', function (Blueprint $table) {
$table->string('sku')->unique()->after('name');
$table->text('description')->nullable()->change();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('products', function (Blueprint $table) {
$table->dropColumn('sku');
// Revert 'description' to not nullable if it was originally not nullable
// This might require knowing its original state, or just leaving it for simplicity
// For a robust rollback, you might need to specify the original type.
// $table->text('description')->nullable(false)->change(); <-- Careful with this if original state is unknown
});
}
};

Advanced usage: Renaming a column and adding foreign keys

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

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->renameColumn('user_id', 'customer_id');
$table->foreignId('product_id')->constrained()->onDelete('cascade');
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('orders', function (Blueprint $table) {
$table->dropForeign(['product_id']);
$table->dropColumn('product_id');
$table->renameColumn('customer_id', 'user_id');
});
}
};

Common Mistakes



  • Forgetting down() method: While not strictly required for php artisan migrate to run, a missing or incorrect down() method prevents you from rolling back migrations, making development and error recovery difficult. Fix: Always implement the down() method to perfectly reverse the changes made in up().

  • Modifying already run migrations: After a migration has been run on a shared environment (like staging or production), you should never modify its file. This can lead to inconsistencies and errors if others have already run the original version. Fix: If you need to make a change, create a new migration to apply the additional modification.

  • Incorrect column type or constraints: Choosing the wrong data type (e.g., integer instead of bigInteger for IDs) or forgetting essential constraints like nullable(), unique(), or foreign keys can cause data integrity issues or application errors. Fix: Carefully consider the data you'll be storing and apply appropriate types and constraints. Always test migrations locally before deploying.


Best Practices



  • One change per migration: Keep migrations focused. Each migration should ideally perform a single, logical change (e.g., create one table, add one column). This makes them easier to understand, manage, and rollback.

  • Descriptive naming: Use clear, descriptive names for your migration files, reflecting their purpose (e.g., create_products_table, add_email_to_users_table).

  • Use Schema::dropIfExists() in down(): When dropping tables, use Schema::dropIfExists('table_name') to prevent errors if the table doesn't exist (e.g., if a rollback was partially successful).

  • Order of operations for foreign keys: When adding foreign keys, ensure the referenced table and column already exist. When dropping tables with foreign keys, drop the foreign key constraint first, then the table. When dropping columns, drop foreign key constraints on that column first.

  • Use --create and --table flags: Leverage the Artisan flags --create=table_name and --table=table_name when generating migrations to get pre-filled boilerplate code, reducing manual effort and potential errors.


Practice Exercises



  • Create a migration to add a new table named categories with columns for id, name (string, unique), and description (text, nullable). Ensure timestamps are included.

  • Create a second migration to add a foreign key column category_id to your existing posts table, referencing the id column on the categories table. Set it up so that if a category is deleted, all associated posts are also deleted (cascade).

  • Create a third migration to rename the description column in the categories table to details.


Mini Project / Task


Design and implement a basic e-commerce database schema using migrations. Create migrations for the following tables:

  • products: id, name (string), description (text, nullable), price (decimal), stock (integer), created_at, updated_at.

  • orders: id, user_id (foreign key to users table), total_amount (decimal), status (string, default 'pending'), created_at, updated_at.

  • order_items: id, order_id (foreign key to orders), product_id (foreign key to products), quantity (integer), price (decimal), created_at, updated_at.


Ensure all foreign key relationships are correctly defined with appropriate onDelete actions.

Challenge (Optional)


After completing the mini-project, create an additional migration that adds a discount_percentage column (decimal, nullable, default 0.00) to the products table. Then, create a separate migration to add a promo_code column (string, unique, nullable) to the orders table and also add an applied_discount (decimal, nullable) column to track the actual discount applied to an order. Ensure all rollbacks work correctly.

Seeding and Factories

Seeding and factories are Laravel tools used to fill a database with sample, test, or starter data. In real projects, developers often need users, posts, categories, orders, or other records before they can properly test features. Entering that data manually is slow and error-prone, so Laravel provides factories to define how fake model data should look, and seeders to insert that data into the database in a repeatable way. Factories are commonly used in development and automated testing, while seeders are used for initial setup, demo content, and reference data such as roles or settings.

A factory is a class that describes the default attributes for a model using realistic fake values generated by Faker. For example, a User factory can create names, emails, and passwords. Factories can also define states, which are named variations like admin users, inactive accounts, or verified users. A seeder is a class that runs code to insert records into the database. Seeders may call factories for bulk data generation or insert fixed records directly. In practice, teams often use both: fixed seeders for required data and factories for larger random datasets.

Step-by-Step Explanation

First, create a factory if your model does not already have one. Laravel commonly creates a factory for User by default. A factory extends the Factory class and returns an array from the definition() method. Each key matches a database column. Next, create a seeder with Artisan. Inside the seeder, call the model factory and specify how many records to generate. Finally, run migrations with seeding or execute the seeder directly. The main commands are php artisan make:factory, php artisan make:seeder, php artisan db:seed, and php artisan migrate:fresh --seed.

Factories can be chained with methods like count(), state(), has(), and for(). These help generate related records. For example, a user can have many posts, or a post can belong to a category. Seeders usually live in database/seeders, while factories live in database/factories. The central DatabaseSeeder class is used to call other seeders in the proper order.

Comprehensive Code Examples

Basic example

// database/factories/PostFactory.php
namespace Database\Factories;

use App\Models\Post;
use Illuminate\Database\Eloquent\Factories\Factory;

class PostFactory extends Factory
{
protected $model = Post::class;

public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraph(),
'published' => true,
];
}
}

// database/seeders/PostSeeder.php
namespace Database\Seeders;

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

class PostSeeder extends Seeder
{
public function run(): void
{
Post::factory()->count(10)->create();
}
}

Real-world example

// database/factories/UserFactory.php
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => bcrypt('password'),
'role' => 'customer',
];
}

public function admin(): static
{
return $this->state(fn () => ['role' => 'admin']);
}

// database/seeders/DatabaseSeeder.php
use App\Models\User;

public function run(): void
{
User::factory()->create([
'name' => 'Site Admin',
'email' => '[email protected]',
'role' => 'admin',
]);

User::factory()->count(20)->create();
}

Advanced usage

// Create users with related posts
use App\Models\User;

User::factory()
->count(5)
->hasPosts(3)
->create();

// Or with explicit relationship definition
User::factory()
->count(3)
->has(
\App\Models\Post::factory()->count(2)->state([
'published' => false,
]),
'posts'
)
->create();

Common Mistakes

  • Forgetting unique fields: using random emails without unique() can cause duplicate key errors. Add fake()->unique().
  • Not calling seeders: creating a seeder file is not enough. Register it in DatabaseSeeder or run it directly.
  • Using factories for required fixed data: roles or system settings should often be seeded explicitly instead of randomly generated.

Best Practices

  • Keep factories focused on realistic defaults and use states for variations.
  • Use dedicated seeders for lookup tables, admin accounts, and essential app data.
  • Run migrate:fresh --seed during development to rebuild a clean test dataset.
  • Create relationship-aware factories so your sample data matches real application behavior.

Practice Exercises

  • Create a factory for a Category model with a name and description.
  • Build a seeder that inserts 15 categories using the factory.
  • Add a factory state called draft to a PostFactory that sets published to false.

Mini Project / Task

Build a blog demo dataset that creates 1 admin user, 10 regular users, 5 categories, and 30 posts distributed across those users and categories.

Challenge (Optional)

Create a seeding setup where each user gets a random number of posts, and some posts are published while others are drafts using factory states.

Eloquent ORM Introduction

Eloquent ORM is Laravel’s built-in system for interacting with databases using PHP classes called models. Instead of manually writing SQL queries for every insert, update, delete, or select operation, developers work with objects that represent database tables. This approach makes code cleaner, easier to read, and simpler to maintain. In real-world projects, Eloquent is used in applications such as blogs, e-commerce stores, school portals, booking systems, and dashboards where developers constantly read and change database records.

At its core, Eloquent follows the Active Record pattern. Each model usually maps to one table, and each object instance usually maps to one row. For example, a Post model connects to a posts table, while a User model connects to a users table. Eloquent supports common operations like creating records, fetching single or multiple rows, updating data, deleting records, filtering with conditions, sorting, and working with timestamps. It also supports relationships such as one-to-one, one-to-many, many-to-many, and polymorphic relationships, which are essential in real applications where data is connected across multiple tables.

Step-by-Step Explanation

To use Eloquent, first create a model. Laravel often generates it with Artisan using a command like php artisan make:model Post -m. This creates a model file and optionally a migration. The model class usually lives in app/Models. Eloquent automatically assumes the table name is the plural form of the model name, so Post becomes posts.

Next, define which attributes can be mass assigned using $fillable. This protects your application from unsafe input. Then use Eloquent methods such as all(), find(), where(), create(), save(), update(), and delete(). These methods allow you to work with rows as objects. You can also chain query methods to filter data before retrieving it.

Comprehensive Code Examples

Basic example
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
protected $fillable = ['title', 'content'];
}

$posts = Post::all();
$post = Post::find(1);
Real-world example
use App\Models\Post;

$post = Post::create([
'title' => 'Getting Started with Laravel',
'content' => 'Eloquent makes database work easier.'
]);

$publishedPosts = Post::where('title', 'like', '%Laravel%')
->orderBy('id', 'desc')
->get();
Advanced usage
use App\Models\Post;

$post = Post::findOrFail(1);
$post->title = 'Updated Title';
$post->save();

Post::where('id', '>', 10)->update([
'content' => 'Bulk updated content'
]);

$oldPost = Post::find(15);
if ($oldPost) {
$oldPost->delete();
}

Common Mistakes

  • Forgetting fillable fields: If $fillable is not defined, mass assignment with create() may fail. Add allowed column names to the model.
  • Using the wrong table name: If your table does not follow Laravel naming conventions, define protected $table in the model.
  • Calling methods on null: find() can return null. Use findOrFail() or check before accessing properties.
  • Confusing query builder with collection methods: Methods before get() build the query; methods after get() work on a collection.

Best Practices

  • Use meaningful model names that clearly represent business data, such as Order, Invoice, or Student.
  • Protect mass assignment with $fillable or carefully managed guarded fields.
  • Keep business logic organized by placing database-related logic in models, scopes, or service classes when needed.
  • Use relationships instead of manual joins when working with connected data in a Laravel-style application.
  • Prefer readable chained queries so your intent is clear to other developers.

Practice Exercises

  • Create a Book model with fillable fields for title and author, then retrieve all books.
  • Insert three new records into a products table using Eloquent and display products with an ID greater than 1.
  • Find a single record by ID, update one field, and save the change using Eloquent.

Mini Project / Task

Build a simple article manager using a Post model where users can create articles, list all articles, edit a selected article title, and delete an article.

Challenge (Optional)

Create a model for Task and write Eloquent code to show only tasks whose title contains a specific word, sort them by latest ID, and update all matching tasks with a new status.

Eloquent Models

Eloquent Models are Laravel classes that represent database tables and allow you to work with data using clean, object-oriented PHP instead of writing raw SQL for every task. In real projects, models are used for handling users, products, orders, blog posts, comments, and almost any structured data in an application. Eloquent exists to make database interaction expressive, readable, and faster to maintain. For example, instead of manually composing a query to fetch published posts, you can call methods directly on a model such as Post::where('published', true)->get(). Each model usually maps to one table, like Post to posts, and each row becomes a model instance. Common concepts include table mapping, primary keys, timestamps, mass assignment, attribute casting, and relationships. You can customize the table name, define which fields may be filled, convert values like JSON and dates automatically, and attach business rules close to the data itself. Eloquent also supports retrieval styles such as single records with find(), collections with get(), filtered queries with where(), and record creation through create() or save(). In practice, this means cleaner controllers, reusable logic, and easier testing.

Step-by-Step Explanation

Start by generating a model with Artisan using php artisan make:model Post. Laravel creates a class in app/Models. A basic model extends Illuminate\Database\Eloquent\Model. By default, Laravel assumes the table name is the plural form of the class name and the primary key is id. If your table uses a different name, set protected $table. To allow safe mass assignment, define protected $fillable with allowed columns such as title and content. Without this, bulk creation can fail. To create a record, instantiate the model, assign attributes, and call save(), or use create() when fillable fields are configured. To read data, use methods like all(), find(), first(), and query constraints like where(). To update, change properties and save again. To delete, call delete(). You can also define casts with protected $casts so fields like is_published become booleans and metadata becomes an array automatically.

Comprehensive Code Examples

Basic example
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published'];
}

$post = Post::create([
'title' => 'My first post',
'content' => 'Learning Eloquent',
'is_published' => true,
]);
Real-world example
$publishedPosts = Post::where('is_published', true)
->orderBy('created_at', 'desc')
->get();

foreach ($publishedPosts as $post) {
echo $post->title;
}
Advanced usage
class Post extends Model
{
protected $fillable = ['title', 'content', 'is_published', 'metadata'];

protected $casts = [
'is_published' => 'boolean',
'metadata' => 'array',
];
}

$post = Post::find(1);
$post->metadata = ['author_note' => 'Reviewed', 'reading_time' => 5];
$post->save();

Common Mistakes

  • Forgetting fillable fields: create() may fail or ignore input. Fix it by defining $fillable properly.
  • Using the wrong table name: Laravel guesses plural names. Fix it with protected $table = 'your_table'; when needed.
  • Calling methods on null: find() can return null. Check the result before accessing properties.
  • Ignoring casts: JSON or booleans may behave unexpectedly. Add $casts for predictable data types.

Best Practices

  • Keep model names singular and descriptive, such as Product or Invoice.
  • Use $fillable to protect against unsafe mass assignment.
  • Place reusable query logic in local scopes or dedicated methods when models grow.
  • Use casts for booleans, arrays, dates, and JSON fields to reduce manual conversion.
  • Keep controllers thin by moving data-related logic into models or service classes.

Practice Exercises

  • Create a Book model with fillable fields for title, author, and price.
  • Insert three records using Eloquent and display all books ordered by title.
  • Add a boolean field called is_featured and cast it correctly in the model.

Mini Project / Task

Build a simple blog post manager using a Post model that can create posts, list published posts, update a title, and delete an unwanted post.

Challenge (Optional)

Create a model for Product with a JSON specifications column, cast it to an array, and write a query that fetches only active products sorted by newest first.

CRUD Operations with Eloquent


CRUD operations are the fundamental building blocks of almost any web application. The acronym stands for Create, Read, Update, and Delete. In the context of web development, these operations refer to the basic functions for interacting with a database. Laravel, a powerful PHP framework, simplifies these operations dramatically through its elegant ORM (Object-Relational Mapper) called Eloquent. Eloquent provides an expressive, ActiveRecord implementation for working with your database. Instead of writing raw SQL queries, you interact with your database tables as if they were plain PHP objects, making your code more readable, maintainable, and less prone to SQL injection vulnerabilities.

Eloquent exists to bridge the gap between your application's object-oriented structure and the relational structure of your database. It allows developers to define "models" that represent database tables, and through these models, perform common database tasks. In real-life applications, CRUD operations are ubiquitous. Think of a blog where you create new posts (Create), view existing posts (Read), edit your posts (Update), and remove old posts (Delete). Similarly, an e-commerce site needs to create product listings, read customer orders, update inventory, and delete outdated items. User management systems, content management systems, and almost every interactive web application relies heavily on these core operations, and Eloquent makes implementing them a breeze in Laravel.

Step-by-Step Explanation


Eloquent models typically reside in the `app/Models` directory. Each model corresponds to a database table. By convention, the model name is singular (e.g., `Post`) and the table name is plural (e.g., `posts`).

To create a model, you can use the Artisan command:
php artisan make:model Post

This will create `app/Models/Post.php`. Inside this file, you can define relationships, fillable attributes, and more.

Create (Insert Data):
To create a new record, you instantiate a model, set its attributes, and then call the `save()` method. Alternatively, you can use the `create()` method, which is a mass assignment friendly approach.

Read (Retrieve Data):
Eloquent offers various methods to retrieve data. You can fetch all records, find a record by its primary key, or query records based on specific conditions using methods like `where()`, `first()`, `get()`, `find()`, etc.

Update (Modify Data):
To update an existing record, you first retrieve it, modify its attributes, and then call the `save()` method. You can also update multiple records that match a query using the `update()` method.

Delete (Remove Data):
To delete a record, you retrieve it and then call the `delete()` method. You can also delete multiple records based on a query.

Comprehensive Code Examples


Basic Example: Managing a Simple 'Task' Model
Let's assume you have a `tasks` table and a `Task` model.
// app/Models/Task.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Task extends Model
{
protected $fillable = ['title', 'description', 'is_completed'];
}

// Create a new task
$task = new App\Models\Task();
$task->title = 'Learn Laravel Eloquent';
$task->description = 'Read documentation and practice CRUD operations.';
$task->is_completed = false;
$task->save();
// Or using create method
$task = App\Models\Task::create([
'title' => 'Build a simple blog',
'description' => 'Use Eloquent for posts and comments.',
'is_completed' => false,
]);

// Read all tasks
$tasks = App\Models\Task::all();
foreach ($tasks as $task) {
echo $task->title . "\n";
}

// Read a single task by ID
$task = App\Models\Task::find(1);
if ($task) {
echo 'Found task: ' . $task->title . "\n";
}

// Read tasks with conditions
$incompleteTasks = App\Models\Task::where('is_completed', false)->get();
foreach ($incompleteTasks as $task) {
echo 'Incomplete: ' . $task->title . "\n";
}

// Update a task
$task = App\Models\Task::find(1);
if ($task) {
$task->is_completed = true;
$task->save();
echo 'Task 1 updated to completed.\n';
}

// Update multiple tasks
App\Models\Task::where('is_completed', false)->update(['description' => 'Revised description']);
echo 'All incomplete tasks descriptions revised.\n';

// Delete a task
$task = App\Models\Task::find(2);
if ($task) {
$task->delete();
echo 'Task 2 deleted.\n';
}

// Delete multiple tasks
App\Models\Task::where('is_completed', true)->delete();
echo 'All completed tasks deleted.\n';


Real-world Example: Managing 'Products' in an E-commerce Application
Imagine an e-commerce application where you need to manage products.
// app/Models/Product.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $fillable = ['name', 'description', 'price', 'stock', 'is_active'];
}

// Create a new product
$product = Product::create([
'name' => 'Laptop X',
'description' => 'High performance laptop with 16GB RAM.',
'price' => 1200.00,
'stock' => 50,
'is_active' => true,
]);

// Read all active products, ordered by price
$activeProducts = Product::where('is_active', true)->orderBy('price', 'asc')->get();
foreach ($activeProducts as $prod) {
echo "Product: {$prod->name}, Price: {$prod->price}\n";
}

// Update product price and stock
$laptopX = Product::where('name', 'Laptop X')->first();
if ($laptopX) {
$laptopX->price = 1150.00;
$laptopX->stock = 45;
$laptopX->save();
echo "Price of Laptop X updated to {$laptopX->price}.\n";
}

// Deactivate products with zero stock
Product::where('stock', 0)->update(['is_active' => false]);
echo "Deactivated products with zero stock.\n";


Advanced Usage: Soft Deletes and Mass Assignment Protection
Eloquent supports "soft deletes," which means instead of truly removing records from your database, they are simply marked as deleted. This is useful for auditing or recovery.
// app/Models/Comment.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; // Import SoftDeletes trait

class Comment extends Model
{
use SoftDeletes; // Use the trait

protected $fillable = ['content', 'user_id', 'post_id'];
protected $dates = ['deleted_at']; // Define deleted_at as a Carbon instance
}

// Create a comment
$comment = Comment::create(['content' => 'Great post!', 'user_id' => 1, 'post_id' => 5]);

// Soft delete a comment
$comment->delete(); // This sets 'deleted_at' timestamp

// Retrieve all comments, including soft-deleted ones
$allComments = Comment::withTrashed()->get();

// Retrieve only soft-deleted comments
$deletedComments = Comment::onlyTrashed()->get();

// Restore a soft-deleted comment
$deletedComment = Comment::onlyTrashed()->find(1);
if ($deletedComment) {
$deletedComment->restore();
echo 'Comment restored.\n';
}

// Permanently delete a comment (force delete)
$comment = Comment::find(1); // Assuming it's restored or not soft-deleted
if ($comment) {
$comment->forceDelete();
echo 'Comment permanently deleted.\n';
}

// Mass Assignment Protection: $fillable and $guarded
// In app/Models/User.php (example)
// protected $fillable = ['name', 'email', 'password']; // Only these can be mass-assigned
// protected $guarded = ['id', 'is_admin']; // All EXCEPT these can be mass-assigned
// Using $fillable is generally safer as it's an opt-in approach.


Common Mistakes



  • Forgetting Mass Assignment Protection: Trying to use `create()` or `update()` with attributes not defined in `$fillable` or `$guarded` will result in `MassAssignmentException` or silently ignore the attributes.
    Fix: Always define `$fillable` or `$guarded` in your models to explicitly allow or disallow mass assignment for certain attributes. `$fillable` is recommended for security.

  • N+1 Query Problem: When fetching a list of models and then looping through them to access a related model (e.g., fetching 10 posts and then querying the author for each post individually). This leads to N+1 queries (1 for posts, N for authors).
    Fix: Use eager loading with the `with()` method. Example: `Post::with('author')->get();`

  • Not Handling `find()` on Non-existent Records: `Model::find($id)` returns `null` if the record doesn't exist. Directly trying to access properties on a `null` object will cause an error.
    Fix: Always check if the model was found: `if ($post = Post::find($id)) { ... }`. For routes, consider `findOrFail()` which automatically throws a 404 error if not found.


Best Practices



  • Use `$fillable` for Mass Assignment: Explicitly define which attributes can be mass-assigned. This is a crucial security measure against unexpected data changes.

  • Eager Loading with `with()`: Prevent the N+1 query problem by eager loading relationships that you know you'll need.

  • Utilize Model Events: For actions that need to happen before or after a model is created, updated, or deleted (e.g., sending notifications, logging), use model events (`creating`, `created`, `updating`, `updated`, `deleting`, `deleted`).

  • Use Query Scopes: Encapsulate common query logic into reusable methods on your models. Example: `public function scopeActive($query) { return $query->where('is_active', true); }` then `User::active()->get();`

  • Handle Exceptions: Wrap your CRUD operations in `try-catch` blocks, especially for `create` and `update` operations, to gracefully handle database errors or validation failures.

  • Keep Controllers Thin: Delegate complex business logic to services or repositories, keeping your controllers focused on handling requests and returning responses.


Practice Exercises



  1. Create a 'Category' Model:
    Create a new Eloquent model named `Category` with `name` and `slug` attributes. Then, create three new categories using the `create()` method.

  2. List and Filter Products:
    Assuming you have the `Product` model from the example, write code to retrieve all products with a price greater than 500, ordered by their `name` in descending order.

  3. Update and Delete a User:
    Create a `User` model (if you don't have one) with `name` and `email` attributes. Create a new user. Then, find that user by their email, update their name, and finally, delete the user.


Mini Project / Task


Build a simple "Contact List" application. Create an Eloquent model `Contact` with attributes like `name`, `email`, and `phone`. Implement the following functionalities:

  • Ability to add a new contact.

  • Ability to view all contacts.

  • Ability to view a single contact by its ID.

  • Ability to update a contact's email or phone number.

  • Ability to delete a contact.


Challenge (Optional)


Extend the "Contact List" application to include soft deletes for contacts. Implement a feature to view only deleted contacts and another feature to restore a soft-deleted contact. Additionally, add a custom query scope to the `Contact` model to easily retrieve contacts whose names start with a specific letter (e.g., `Contact::startsWith('A')->get();`).

Eloquent Relationships One to One

A one-to-one relationship in Laravel Eloquent connects one database record to exactly one related record. It exists to model situations where data belongs together but is better stored in separate tables for organization, flexibility, and normalization. In real applications, this appears when a User has one Profile, an Employee has one IDCard, or an Order has one Invoice. Instead of storing every field in one huge table, Laravel lets you link models cleanly using relationship methods.

In Eloquent, the most common one-to-one methods are hasOne() and belongsTo(). The parent model usually defines hasOne(), while the related child model defines belongsTo(). For example, if one user has one profile, the users table is usually the parent, and the profiles table contains the foreign key such as user_id. Laravel then uses that key to match the records automatically.

This relationship matters because it keeps code readable and expressive. Rather than writing manual SQL joins every time, you can call $user->profile and work with related data naturally. Laravel also supports eager loading, creation through relationships, custom foreign keys, and safe handling when a related record does not exist. Understanding one-to-one relationships is essential because many bigger relationship patterns build on the same Eloquent ideas.

Step-by-Step Explanation

To create a one-to-one relationship, start with two models and two tables. Assume a user has one profile.

First, create the tables so the child table contains the foreign key. The profiles table should include user_id.

Second, define the relationship in the parent model using hasOne(Profile::class).

Third, define the inverse relationship in the child model using belongsTo(User::class).

Fourth, access the relation like a property: $user->profile. If you need query builder behavior, call it like a method: $user->profile().

Laravel guesses foreign keys by convention. hasOne(Profile::class) assumes user_id exists on the profiles table. If your database uses custom keys, you can pass them manually.

You can also create related records through the relationship itself. This automatically fills the foreign key, reducing mistakes and making code cleaner.

Comprehensive Code Examples

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

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

// usage
$user = User::find(1);
$profile = $user->profile;
Real-world example
// migration for profiles table
Schema::create('profiles', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->onDelete('cascade');
    $table->string('phone')->nullable();
    $table->text('bio')->nullable();
    $table->timestamps();
});

// create profile for a user
$user = User::find(1);
$user->profile()->create([
    'phone' => '123-456-7890',
    'bio' => 'Laravel developer'
]);
Advanced usage
// custom foreign and local keys
public function profile()
{
    return $this->hasOne(Profile::class, 'account_owner_id', 'id');
}

// eager loading to avoid extra queries
$users = User::with('profile')->get();

foreach ($users as $user) {
    echo optional($user->profile)->phone;
}

Common Mistakes

  • Missing foreign key: Beginners often forget to add user_id to the child table. Fix it by creating the correct foreign key in the migration.
  • Using the wrong relationship type: Some use hasMany() when only one record should exist. Use hasOne() for one related record.
  • Confusing method and property access: $user->profile returns the related model, while $user->profile() returns the relationship builder. Use the correct form for the task.
  • Not handling null values: A user may not have a profile yet. Use checks or optional() before reading attributes.

Best Practices

  • Follow Laravel naming conventions like user_id whenever possible.
  • Add database foreign key constraints for data integrity.
  • Use eager loading with with('profile') when loading many parent records.
  • Create related records through the relationship method to auto-fill keys safely.
  • Keep one-to-one data in separate tables only when separation improves design and maintainability.

Practice Exercises

  • Create User and Profile models with a one-to-one relationship, then fetch a user and display the profile bio.
  • Add a migration where the profile table includes user_id, phone, and address, then create a related profile through Eloquent.
  • Load multiple users with their profiles using eager loading and print each user name with the profile phone number.

Mini Project / Task

Build a simple account details feature where each registered user has exactly one profile containing phone, bio, and date of birth. Define the models, migration, and relationship methods, then display the profile information on a user details page.

Challenge (Optional)

Modify the one-to-one setup so it uses a custom foreign key name instead of user_id, then query and display the related data correctly using explicit key definitions in both models.

One to Many Relationships

In Laravel, a one-to-many relationship is used when a single record in one table is connected to multiple records in another table. A common real-life example is one user having many posts, one author writing many books, or one category containing many products. This relationship exists because applications often model ownership or grouping. Instead of storing repeated user details inside every post, you store the user once and connect posts to that user through a foreign key. Laravel makes this easy with Eloquent ORM using the hasMany() and belongsTo() methods.

The parent model is the record that owns many related items, and the child model is the record that belongs to one parent. For example, User is the parent and Post is the child. In database terms, the child table usually contains the foreign key such as user_id. This structure improves consistency, simplifies querying, and supports scalable application design. You will use one-to-many relationships in blogs, e-commerce catalogs, school systems, CRM tools, and project management apps.

Laravel supports both sides of this relationship. The parent model defines hasMany(), while the child model defines belongsTo(). You can also customize foreign keys and local keys when your naming does not follow Laravel conventions. Understanding both sides is important because most projects need to retrieve all children from a parent and also access the parent from a child.

Step-by-Step Explanation

Start by creating two tables, such as users and posts. The posts table should include a user_id column that points to the id of the user. Next, define the relationship methods in both models. In the User model, create a method like posts() that returns $this->hasMany(Post::class). In the Post model, create a method like user() that returns $this->belongsTo(User::class).

Once defined, you can access related records using dynamic properties and query builder features. For example, $user->posts returns all posts for a user, and $post->user returns the owner of a post. You can also eager load relationships with with() to avoid excessive database queries.

Comprehensive Code Examples

// Basic example: migrations
Schema::create('users', function ($table) {
$table->id();
$table->string('name');
$table->timestamps();
});

Schema::create('posts', function ($table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->string('title');
$table->text('body');
$table->timestamps();
});
// Models
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
}

class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
// Real-world example: creating related posts
$user = User::find(1);

$user->posts()->create([
'title' => 'My First Laravel Post',
'body' => 'Learning relationships in Eloquent.'
]);

$posts = User::with('posts')->find(1);
// Advanced usage: custom foreign key and filtering
class Category extends Model
{
public function products()
{
return $this->hasMany(Product::class, 'cat_id', 'id');
}
}

$categories = Category::with(['products' => function ($query) {
$query->where('is_active', true)->latest();
}])->get();

Common Mistakes

  • Missing foreign key: Beginners forget to add user_id to the child table. Fix it by adding the foreign key in the migration.
  • Wrong relationship type: Using hasMany() in both models is incorrect. The child model should use belongsTo().
  • N+1 query problem: Loading users and then posts one by one causes many queries. Fix it with eager loading using with('posts').
  • Incorrect method names: Naming relationship methods poorly makes code harder to read. Use plural names for collections like posts().

Best Practices

  • Follow Laravel naming conventions so you rarely need custom keys.
  • Use foreign key constraints to protect data integrity.
  • Prefer eager loading when listing parents and children together.
  • Keep relationship methods focused and free from unrelated logic.
  • Use model factories and seeders to test relationships with realistic data.

Practice Exercises

  • Create Author and Book models where one author has many books.
  • Build Category and Product tables, then display all products for a selected category.
  • Create Customer and Order models and write a query to load customers with their orders.

Mini Project / Task

Build a simple blog module where one user can create many posts. Add migrations, models, relationship methods, and a route that shows a user with all of their posts.

Challenge (Optional)

Create a page that lists all users and the number of posts each user has, while avoiding unnecessary queries by using Laravel relationship counting features.

Many to Many Relationships

A many-to-many relationship is used when one record can belong to many records of another model, and that second model can also belong to many of the first. In Laravel, this is commonly used for examples like users having many roles while each role belongs to many users, or posts having many tags while each tag can be attached to many posts. This relationship exists because storing multiple IDs in a single column is messy, hard to query, and breaks good database design. Instead, Laravel uses a third table called a pivot table, such as role_user or post_tag, to connect the two models cleanly.

The main concept is the belongsToMany() method inside both related models. Laravel expects a pivot table named in alphabetical singular order, but you can customize that if needed. Many-to-many relationships may be simple, where the pivot table only stores the two foreign keys, or advanced, where the pivot table also stores extra fields like assignment date, status, or permissions level. That makes this relationship type very useful in real business systems.

Step-by-Step Explanation

First, create two tables for the main entities, such as users and roles. Then create a pivot table with foreign keys for both models. In the model classes, define the relationship using belongsToMany(). Once defined, you can fetch related records, attach IDs, detach them, or sync a full set of related items. If the pivot table contains extra columns, use withPivot() so Laravel includes those values when loading the relationship.

Typical syntax in a model looks like this: the User model returns $this->belongsToMany(Role::class), and the Role model returns $this->belongsToMany(User::class). The loaded related records are accessed like a collection, for example $user->roles.

Comprehensive Code Examples

Basic example
// User.php
public function roles()
{
return $this->belongsToMany(Role::class);
}

// Role.php
public function users()
{
return $this->belongsToMany(User::class);
}

// Attach a role to a user
$user = User::find(1);
$user->roles()->attach(2);

// Read roles
foreach ($user->roles as $role) {
echo $role->name;
}
Real-world example
// Post.php
public function tags()
{
return $this->belongsToMany(Tag::class);
}

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

$post = Post::create(['title' => 'Laravel Tips']);
$post->tags()->attach([1, 3, 5]);

// Replace old tags with a new set
$post->tags()->sync([2, 4]);
Advanced usage
// course_student pivot has enrolled_at and status
public function courses()
{
return $this->belongsToMany(Course::class)
->withPivot('enrolled_at', 'status')
->withTimestamps();
}

$student->courses()->attach(3, [
'enrolled_at' => now(),
'status' => 'active'
]);

foreach ($student->courses as $course) {
echo $course->title;
echo $course->pivot->status;
}

Common Mistakes

  • Wrong pivot table name: Laravel expects alphabetical singular naming like role_user. Fix it by renaming the table or passing the table name manually to belongsToMany().
  • Using hasMany() instead of belongsToMany(): This causes incorrect queries. Use the correct relationship type on both models.
  • Forgetting withPivot(): Extra pivot columns will not be available unless you explicitly load them.
  • Using attach() repeatedly without checking duplicates: This may insert duplicate rows. Use syncWithoutDetaching() when needed.

Best Practices

  • Name pivot tables clearly and follow Laravel conventions whenever possible.
  • Add foreign key constraints to protect data integrity.
  • Use sync() when updating a full set of related items from forms.
  • Use eager loading like User::with('roles') to avoid N+1 query problems.
  • Store only relationship-specific fields in the pivot table, not unrelated business data.

Practice Exercises

  • Create a many-to-many relationship between Student and Course using a pivot table.
  • Attach three categories to a product and display all category names for that product.
  • Add an extra pivot column called assigned_by to a user-role relationship and print it.

Mini Project / Task

Build a simple blog tagging system where each post can have many tags and each tag can belong to many posts. Add forms to attach tags to a post and update them using sync().

Challenge (Optional)

Create a course enrollment feature where students can join many courses and the pivot table stores enrollment date and completion status. Display only active enrollments and allow updating the pivot status without removing the relationship.

Query Builder Basics

Laravel Query Builder is a fluent, database-focused interface for writing SQL-like queries in PHP without manually concatenating raw SQL strings. It exists to make database access easier to read, safer to write, and more portable across supported database systems. In real projects, it is used for listing products, filtering orders, searching users, generating reports, and building admin dashboards. Query Builder sits between raw SQL and Eloquent ORM: it gives you expressive control over queries while staying lightweight and fast for custom data retrieval.

The most common starting point is the DB facade. With it, you can select rows, filter with where(), sort using orderBy(), limit results, join tables, and insert, update, or delete records. Important result methods include get() for multiple rows, first() for one row, pluck() for a single column, count() for totals, and value() for one scalar value. You will also encounter filtering variations such as whereIn(), whereNull(), and grouped conditions with closures. These tools let you build clear queries step by step instead of writing one large SQL statement manually.

Step-by-Step Explanation

Start by importing the facade: use Illuminate\Support\Facades\DB;. A query usually begins with DB::table('table_name'). This selects the database table to work with. Then add methods to shape the query. For example, select('id','name') chooses columns, where('active',1) filters rows, and orderBy('name') sorts results. Finally, execute the query using a terminal method such as get() or first().

Think of the builder as a chain. Each method adds another instruction. Nothing is fetched until the final execution method runs. This makes queries easy to compose dynamically. For example, you can add a where() only if a filter exists in the request. This pattern is common in search forms, dashboards, and reporting tools.

Comprehensive Code Examples

Basic example
use Illuminate\Support\Facades\DB;$users = DB::table('users')    ->select('id', 'name', 'email')    ->where('active', 1)    ->orderBy('name', 'asc')    ->get();
Real-world example
use Illuminate\Support\Facades\DB;$orders = DB::table('orders')    ->where('status', 'pending')    ->whereDate('created_at', '>=', now()->subDays(7))    ->orderBy('created_at', 'desc')    ->get();
Advanced usage
use Illuminate\Support\Facades\DB;$products = DB::table('products')    ->select('id', 'name', 'price', 'stock')    ->where('is_active', 1)    ->where(function ($query) {        $query->where('stock', '>', 0)              ->orWhere('allow_backorder', 1);    })    ->whereIn('category_id', [1, 2, 3])    ->orderBy('price', 'asc')    ->limit(10)    ->get();$totalActiveProducts = DB::table('products')    ->where('is_active', 1)    ->count();

Common Mistakes

  • Forgetting the execution method: building a query without get(), first(), or another terminal method returns a builder instance, not data. Add the correct execution call.
  • Using get() when only one row is needed: this returns a collection. Use first() or value() for a single result.
  • Writing unsafe raw SQL unnecessarily: prefer Query Builder methods like where() and whereIn() to reduce risk and improve readability.
  • Selecting all columns by default: avoid fetching unused data. Use select() for better performance.

Best Practices

  • Use clear, chained queries with one concern per line for readability.
  • Select only the columns you need instead of using everything by default.
  • Use conditional query parts carefully when handling filters from forms or APIs.
  • Choose first(), value(), pluck(), or count() when they better match the expected result.
  • Keep complex business rules out of controllers when possible by moving query logic into dedicated methods or repositories.

Practice Exercises

  • Write a query to fetch all active users and sort them by newest registration date first.
  • Write a query to get the first product whose price is greater than 100.
  • Write a query to count how many orders have the status completed.

Mini Project / Task

Build a simple admin report query that lists the latest 15 support tickets with the columns id, subject, status, and created_at, filtered to show only open or pending tickets.

Challenge (Optional)

Create a product search query that optionally filters by category, minimum price, and stock availability, then sorts the results by price ascending.

Authentication with Laravel Breeze

Authentication is the process of identifying users and controlling access to protected parts of an application. In Laravel, authentication is common in dashboards, admin panels, e-commerce sites, SaaS platforms, and any app where users must register, sign in, reset passwords, or manage their profile. Laravel Breeze exists to give developers a lightweight, official starting point for authentication without forcing a large feature set. It installs routes, controllers, Blade views, validation, middleware integration, and simple frontend assets so you can start with a working auth system quickly.

Breeze is ideal for learners because it shows how Laravel auth works using clean and readable code. It supports registration, login, logout, password reset, email verification when enabled, and profile-related flows depending on the stack. The most common Breeze setup uses Blade with Tailwind CSS, but it can also be installed with API or other frontend options in some Laravel versions. In practice, Breeze works together with Laravel sessions, the auth middleware, CSRF protection, password hashing, and the users table. This makes it a strong foundation before moving to larger tools like Jetstream or Sanctum.

Step-by-Step Explanation

Start by creating a Laravel project and configuring the database in the .env file. Then install Breeze with Composer and run the Breeze installer. After that, install frontend dependencies and compile assets. Finally, run migrations so the users table is created.

The typical flow is: install package, publish auth scaffolding, build assets, migrate database, and test registration and login pages. Breeze adds routes such as /register, /login, and /dashboard. Protected routes are usually wrapped with middleware like auth so only signed-in users can access them. Guests can access login and register pages through guest middleware. When a user submits login credentials, Laravel validates them, checks the hashed password, creates a session, and redirects the user. Logging out destroys the authenticated session.

Important pieces include controllers for handling requests, Blade templates for forms, middleware for access control, and migrations for user storage. Beginners should also understand that passwords are never stored in plain text. Laravel hashes them automatically when using the built-in auth actions correctly.

Comprehensive Code Examples

Basic example
composer create-project laravel/laravel breeze-app
cd breeze-app
composer require laravel/breeze --dev
php artisan breeze:install
npm install
npm run build
php artisan migrate
php artisan serve
Real-world example
use Illuminate\Support\Facades\Route;

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

Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');
Advanced usage
use Illuminate\Support\Facades\Route;

Route::middleware(['auth'])->group(function () {
Route::get('/account', function () {
return view('account');
})->name('account');

Route::get('/orders', function () {
return 'Protected order history';
})->name('orders');
});

@csrf



Common Mistakes

  • Forgetting to run migrations: If the users table does not exist, registration fails. Run php artisan migrate.
  • Not building frontend assets: Breeze views may look broken if assets are missing. Run npm install and npm run build.
  • Protecting routes incorrectly: If you forget auth middleware, guests may access private pages. Wrap sensitive routes with middleware(['auth']).
  • Editing auth logic carelessly: Manually storing plain passwords is unsafe. Always use Laravel’s built-in hashing flow.

Best Practices

  • Keep Breeze as a clean starter and customize gradually.
  • Use middleware consistently for protected pages.
  • Validate all form input on the server side.
  • Test registration, login, logout, and password reset flows after changes.
  • Store secrets in .env and never commit them publicly.

Practice Exercises

  • Install Laravel Breeze in a fresh project and confirm that registration and login work.
  • Create a protected route named /profile that only authenticated users can access.
  • Add a public home page and a private dashboard, then test guest and logged-in behavior.

Mini Project / Task

Build a small member area where users can register, log in, log out, and access a protected dashboard that displays a welcome message with their name.

Challenge (Optional)

Extend the Breeze setup by adding a second protected page for account settings and ensure unauthenticated users are always redirected to the login page.

Authorization and Policies

Authorization in Laravel decides whether an authenticated user is allowed to perform a specific action. Authentication answers, who are you? Authorization answers, what are you allowed to do? In real applications, this is used everywhere: only an author should edit their post, only an admin should delete users, and only a team member should view private project data. Laravel provides two main authorization tools: gates and policies. Gates are useful for simple, action-based checks, while policies group authorization rules around a model such as Post, Project, or Order. Policies keep your code organized by moving permission logic out of controllers and into dedicated classes. This makes applications easier to maintain, test, and secure. A common pattern is to allow administrators broad access while regular users are restricted to their own records. Laravel integrates authorization deeply into controllers, Blade templates, middleware, and model-based workflows, so you can enforce access rules consistently across your application.

Gates are closures or class callbacks defined for abilities like view-reports. Policies are classes containing methods like view, create, update, and delete. Policy methods usually accept the current user and the target model. For example, a PostPolicy can check whether a user owns a post before allowing an update. Laravel can automatically discover policies or you can register them manually. In practice, developers often use policies for model-related authorization and gates for app-wide rules such as accessing an admin dashboard.

Step-by-Step Explanation

First, create a policy with Artisan using a command like php artisan make:policy PostPolicy --model=Post. Laravel generates a class with standard methods. Next, define the logic inside methods. For example, the update method can compare $user->id with $post->user_id. Then call authorization from a controller using $this->authorize('update', $post). If the user is not allowed, Laravel automatically throws a 403 response. In Blade, you can conditionally show buttons using directives such as @can('update', $post). For simpler rules not tied to a model, define a gate and check it with Gate::allows() or the @can directive. You can also use methods like authorizeResource in resource controllers to map policy methods automatically.

Comprehensive Code Examples

// Basic example: Policy method
public function update(User $user, Post $post): bool
{
return $user->id === $post->user_id;
}
// Controller usage
public function edit(Post $post)
{
$this->authorize('update', $post);
return view('posts.edit', compact('post'));
}
// Real-world example: Admin or owner can delete
public function delete(User $user, Post $post): bool
{
return $user->role === 'admin' || $user->id === $post->user_id;
}
// Blade example
@can('delete', $post)

@endcan
// Advanced usage: Gate definition
use Illuminate\Support\Facades\Gate;

Gate::define('view-admin-dashboard', function (User $user) {
return $user->role === 'admin';
});

if (Gate::allows('view-admin-dashboard')) {
// show dashboard
}

Common Mistakes

  • Putting authorization logic directly in controllers: Move repeated checks into policies to keep code clean.
  • Checking only in the view: Hiding a button is not enough; always enforce authorization in controllers or routes too.
  • Forgetting model ownership rules: Verify the current user actually owns or has access to the record being modified.
  • Using gates for everything: Prefer policies for model-based actions so your rules stay organized.

Best Practices

  • Use policies for models like Post, Comment, Project, and Order.
  • Keep policy methods short, readable, and focused on one rule.
  • Use role checks and ownership checks together when appropriate.
  • Call authorize() early in controller actions.
  • Use Blade directives only for interface control, not as your only security layer.
  • Write tests for allowed and denied scenarios.

Practice Exercises

  • Create a PostPolicy with view, update, and delete methods.
  • Restrict post editing so only the post owner can access the edit page.
  • Create a gate that allows only admins to access a reporting page.

Mini Project / Task

Build a small blog permission system where any user can view posts, only logged-in users can create posts, only owners can edit their own posts, and only admins or owners can delete posts.

Challenge (Optional)

Create a project policy where users can update a project only if they are the project owner or belong to the same team with a manager role.

Laravel Request Lifecycle


The Laravel Request Lifecycle describes the sequence of events that occur from the moment an HTTP request enters your application until a response is sent back to the user. Understanding this lifecycle is fundamental to comprehending how Laravel works under the hood, enabling you to debug effectively, optimize performance, and extend the framework's functionality. In essence, it's the journey of a request through various components like the web server, the `public/index.php` entry point, the kernel, service providers, middleware, routing, and finally, controller execution and response generation. This intricate dance allows Laravel to provide its elegant and powerful features, from dependency injection to database interaction, in a structured and efficient manner. Real-life applications extensively rely on this flow; for instance, when a user accesses a URL like `/products/1`, the lifecycle ensures that the request is correctly routed, authenticated (if necessary), data is fetched from the database, and the appropriate view is rendered and returned.

The core concept revolves around a series of bootstrappers and components that process the incoming request. When a request hits your application, it first lands on the web server (e.g., Nginx or Apache), which then directs it to the `public/index.php` file. This file acts as the single entry point for all requests, loading the Composer autoloader and then retrieving an instance of the application from the `bootstrap/app.php` file. The application instance then boots up the HTTP kernel (or Console kernel for CLI requests). The kernel is responsible for handling the request, which involves several crucial steps: registering service providers, booting them, passing the request through global middleware, dispatching the request to the router, executing route-specific middleware, calling the controller action, and finally, sending the response back. Each of these steps plays a vital role in setting up the application environment, processing the request, and generating the final output.

Step-by-Step Explanation


Let's break down the Laravel Request Lifecycle step-by-step:

1. Entry Point (`public/index.php`): All requests are directed here. It loads Composer's autoloader and retrieves the Laravel application instance from `bootstrap/app.php`.
2. HTTP Kernel (`app/Http/Kernel.php`): The application instance sends the request to the HTTP kernel. The kernel extends `Illuminate\Foundation\Http\Kernel` and is responsible for handling the request. It defines global middleware and route-level middleware groups.
3. Bootstrappers: Before handling the request, the kernel runs a series of bootstrappers defined in `Illuminate\Foundation\Http\Kernel`. These include:

  • LoadEnvironmentVariables: Loads environment variables from the .env file.

  • LoadConfiguration: Loads all configuration files.

  • HandleExceptions: Registers the error and exception handling facilities.

  • RegisterFacades: Registers all facades.

  • RegisterProviders: Registers all service providers.

  • BootProviders: Boots all service providers, calling their boot() methods.


4. Service Providers: These are crucial for bootstrapping the application. They register services, bind interfaces to implementations, and configure various components (e.g., database, session, authentication). Their `register()` methods are called first, then their `boot()` methods.
5. Middleware: The request passes through global HTTP middleware defined in `$middleware` property of `app/Http/Kernel.php`. Middleware can inspect, modify, or even terminate requests (e.g., authentication, CSRF protection, logging).
6. Routing: After passing through global middleware, the request is dispatched to the router (`Illuminate\Routing\Router`). The router matches the incoming URL to a defined route in `routes/web.php` or `routes/api.php`.
7. Route Middleware: If the matched route has specific middleware assigned to it (e.g., `'auth'`), these middleware are executed.
8. Controller/Closure Execution: Once all middleware are passed, the route's associated controller method or closure is executed. This is where your application's business logic resides, interacting with models, services, and other components.
9. Response: The controller or closure returns a response (e.g., a view, a JSON response, a redirect). This response is then sent back through the middleware stack in reverse order, allowing middleware to modify the outgoing response.
10. Send Response: Finally, the HTTP kernel sends the response back to the user's browser.

Comprehensive Code Examples


Basic Example: The `public/index.php` entry point
This file is the very first script executed by your web server for every request. You generally don't modify it, but it's crucial to understand its role.

define('LARAVEL_START', microtime(true));

/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, auto-loading mechanism for our applications.
| We just need to utilize it! We'll simply require it into the script here so
| that we don't have to worry about manually loading any of our classes.
|
require __DIR__.'/../vendor/autoload.php';

/*
|--------------------------------------------------------------------------
| Run The Application
|--------------------------------------------------------------------------
|
| Once we have the application, we can handle the incoming request using
| the application's HTTP kernel. Then, we will send the response back
| to this client and then to the browser typically through a web server.
|$app = require_once __DIR__.'/../bootstrap/app.php';

$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);

$response = $kernel->handle(
$request = Illuminate\Http\Request::capture()
)->send();

$kernel->terminate($request, $response);

Real-world Example: Custom Middleware
Let's create a simple middleware to log every incoming request's path and method before it reaches the controller.

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;

class LogRequestDetails
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return \Illuminate\Http\Response
*/
public function handle(Request $request, Closure $next)
{
Log::info('Incoming Request: ' . $request->method() . ' ' . $request->path());

return $next($request);
}
}

Register this middleware in `app/Http/Kernel.php` globally or for specific routes:
// In app/Http/Kernel.php
protected $middleware = [
// ... other global middleware
\App\Http\Middleware\LogRequestDetails::class,
];

Now, every request will be logged to `storage/logs/laravel.log` before it proceeds.

Advanced Usage: Custom Service Provider
Let's say you have a `PaymentGateway` interface and a `StripePaymentGateway` implementation. You can bind this in a service provider.

namespace App\Providers;

use App\Contracts\PaymentGateway;
use App\Services\StripePaymentGateway;
use Illuminate\Support\ServiceProvider;

class PaymentServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PaymentGateway::class, function ($app) {
return new StripePaymentGateway(config('services.stripe.secret'));
});
}

public function boot()
{
//
}
}

Then, register it in `config/app.php` within the `providers` array:
'providers' => [
// ... other providers
App\Providers\PaymentServiceProvider::class,
],

Now, anywhere in your application, you can type-hint `PaymentGateway`, and Laravel's service container will automatically inject `StripePaymentGateway`.

Common Mistakes



  • Misunderstanding Middleware Order:
    Mistake: Expecting route-specific middleware to run before global middleware, or not understanding the reverse order for response handling.
    Fix: Remember that global middleware (`$middleware` in `Kernel.php`) run first, then route group middleware (`$middlewareGroups`), then route-specific middleware (`$routeMiddleware`). For responses, the order is reversed.

  • Incorrect Service Provider Registration:
    Mistake: Creating a service provider but forgetting to register it in `config/app.php`.
    Fix: Always add your custom service providers to the `providers` array in `config/app.php` so Laravel knows to load and boot them.

  • Debugging `index.php` Directly:
    Mistake: Trying to add `dd()` or `echo` statements directly into `public/index.php` for debugging.
    Fix: While it can show if the file is hit, it's generally not the place for application-level debugging. Use `dd()` in controllers, middleware, or service providers, or leverage Laravel's robust logging system (`Log::info()`) and Xdebug for more effective debugging.



Best Practices



  • Leverage Middleware Judiciously: Use middleware for cross-cutting concerns like authentication, logging, CSRF protection, and request manipulation. Avoid putting heavy business logic in middleware; that belongs in controllers or services.

  • Utilize Service Providers for Bootstrapping: Use service providers to register services, bind interfaces to implementations, and configure application components. This keeps your application loosely coupled and easy to maintain.

  • Understand the Role of `index.php` and Kernel: Recognize that `public/index.php` is merely the entry point, and the `Kernel` is the orchestrator of the request lifecycle. Avoid modifying these core files unless absolutely necessary for advanced framework extension.

  • Profile and Optimize: Use tools like Laravel Debugbar or Xdebug to profile your application and understand where time is spent during the request lifecycle, helping identify bottlenecks.



Practice Exercises



  • Beginner-friendly: Create a new middleware named `CheckAge` that redirects users under 18 to a `/restricted` page. Apply this middleware to a test route.

  • Intermediate: Create a custom service provider that registers a singleton instance of a `CurrencyConverter` class. This class should take an API key in its constructor (from `.env`). Inject and use this `CurrencyConverter` in a controller.

  • Advanced: Modify the global HTTP Kernel to add a custom bootstrapper that runs before `LoadConfiguration` and simply logs a message indicating that the application is starting to load environment variables.



Mini Project / Task


Build a simple API endpoint `/api/status` that returns a JSON response indicating the current server time and a custom header `X-App-Version: 1.0`. Implement this by creating a controller, defining a route, and using a middleware to add the custom header to the response.

Challenge (Optional)


Investigate how Laravel's exception handling (`app/Exceptions/Handler.php`) integrates into the request lifecycle. Create a custom exception, throw it from a controller, and then modify the `Handler` to render a specific custom view for that exception type, ensuring it gracefully handles the error within the lifecycle flow.

Service Container and Providers

The Laravel service container is the system responsible for creating and resolving class dependencies. Instead of manually building every object your application needs, Laravel can automatically instantiate classes and inject their dependencies where required. This is especially useful in controllers, middleware, jobs, listeners, and console commands. In real projects, the container helps you swap implementations, centralize configuration, and keep code loosely coupled. Service providers work closely with the container. They are the main place where Laravel registers bindings, singletons, event listeners, and other startup logic. Almost every core Laravel feature is loaded through a provider, which makes them essential for organizing application bootstrapping.

The service container supports several important patterns. A binding tells Laravel how to resolve an abstract type or key. A singleton creates one shared instance for the full request lifecycle. Automatic resolution means Laravel can construct many concrete classes without explicit bindings if their dependencies are type-hinted. Interface-to-implementation binding is common in real applications because controllers can depend on contracts instead of hardcoded classes. Service providers have two key methods: register for binding things into the container and boot for running logic after all services are registered.

Step-by-Step Explanation

First, create a class such as PaymentService. If it has no complex dependencies, Laravel can resolve it automatically. Next, if a class depends on an interface, bind that interface to a concrete class inside a service provider using $this->app->bind() or singleton(). After that, type-hint the dependency in a controller or another class constructor. Laravel reads the type hint, checks the container, and injects the correct object. In a provider, use register() to define bindings because this method is meant for container setup. Use boot() only for code that needs other services already loaded. This separation keeps startup logic predictable and clean.

Comprehensive Code Examples

Basic example
namespace App\Services;

class MessageService
{
public function send()
{
return 'Message sent';
}
}

namespace App\Http\Controllers;

use App\Services\MessageService;

class NotificationController extends Controller
{
public function index(MessageService $messageService)
{
return $messageService->send();
}
}
Real-world example
namespace App\Contracts;

interface PaymentGateway
{
public function charge(float $amount);
}

namespace App\Services;

use App\Contracts\PaymentGateway;

class StripePaymentService implements PaymentGateway
{
public function charge(float $amount)
{
return "Charged {$amount} using Stripe";
}
}

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Contracts\PaymentGateway;
use App\Services\StripePaymentService;

class PaymentServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->bind(PaymentGateway::class, StripePaymentService::class);
}
}

namespace App\Http\Controllers;

use App\Contracts\PaymentGateway;

class CheckoutController extends Controller
{
public function store(PaymentGateway $gateway)
{
return $gateway->charge(99.99);
}
}
Advanced usage
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use App\Services\ReportService;

class ReportServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(ReportService::class, function ($app) {
return new ReportService(config('app.name'));
});
}

public function boot()
{
// boot logic here
}
}

Common Mistakes

  • Using boot() for bindings: Put bindings in register() so services are available correctly during startup.
  • Forgetting interface bindings: If you type-hint an interface without binding it, Laravel cannot resolve it.
  • Using singleton when a fresh instance is needed: Shared instances can cause unexpected state issues.
  • Not registering a custom provider: Make sure the provider is loaded by Laravel if required in your setup.

Best Practices

  • Depend on interfaces or contracts instead of concrete classes.
  • Keep providers focused on one domain, such as payments, reports, or notifications.
  • Use singletons only for stateless or intentionally shared services.
  • Let Laravel auto-resolve simple concrete classes instead of overbinding everything.
  • Keep heavy runtime logic out of providers unless necessary.

Practice Exercises

  • Create a LoggerService and inject it into a controller using automatic resolution.
  • Define an interface named SmsSender, create a concrete class, and bind it in a service provider.
  • Create a singleton service that stores the application name from configuration and return it from a route.

Mini Project / Task

Build a small notification module where a controller depends on a NotificationChannel interface, and a service provider binds that interface to an email notification service.

Challenge (Optional)

Create a provider that conditionally binds different payment gateway implementations based on an environment variable, then test the behavior by switching configurations.

Dependency Injection

Dependency Injection is a design technique where a class receives the objects it depends on instead of creating them internally. In Laravel, this concept is deeply connected to the service container, which automatically resolves and injects dependencies for controllers, middleware, jobs, event listeners, console commands, and custom services. It exists to reduce tight coupling, improve testability, and make code easier to maintain. In real projects, a controller may need a payment service, an email sender, or a repository. Rather than creating those objects with new inside the controller, Laravel can provide them automatically. This makes your code more flexible because you can swap implementations without rewriting business logic.

There are several common forms of dependency injection in Laravel. Constructor injection places dependencies in a class constructor and is the most common pattern for services used across many methods. Method injection provides dependencies directly to a specific method, often used in controller actions or job handlers. Interface-based injection is especially powerful: you depend on an abstraction such as PaymentGatewayInterface, then bind a concrete class in a service provider. This allows the application to remain stable even if the implementation changes from Stripe to PayPal. Laravel's container can resolve concrete classes automatically, but interfaces and custom bindings usually require manual registration.

Step-by-Step Explanation

Start by creating a service class. For example, a ReportService may generate sales data. Next, inject it into a controller constructor. Laravel inspects the constructor type hint, asks the service container for that class, creates it if possible, and passes it in. If your class depends on an interface, you must bind the interface to a concrete implementation in a service provider using $this->app->bind() or singleton(). Use bind when you want a new instance each time and singleton when one shared instance is appropriate. Method injection works similarly: type-hint the dependency in the method signature, and Laravel resolves it during execution.

Comprehensive Code Examples

namespace App\Services;
class ReportService
{
public function summary(): array
{
return ['sales' => 1200, 'orders' => 45];
}
}

namespace App\Http\Controllers;
use App\Services\ReportService;

class DashboardController extends Controller
{
protected ReportService $reportService;

public function __construct(ReportService $reportService)
{
$this->reportService = $reportService;
}

public function index()
{
return response()->json($this->reportService->summary());
}
}
namespace App\Contracts;
interface PaymentGatewayInterface
{
public function charge(float $amount): string;
}

namespace App\Services;
use App\Contracts\PaymentGatewayInterface;

class StripePaymentService implements PaymentGatewayInterface
{
public function charge(float $amount): string
{
return "Charged $amount via Stripe";
}
}

namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Contracts\PaymentGatewayInterface;
use App\Services\StripePaymentService;

class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->bind(PaymentGatewayInterface::class, StripePaymentService::class);
}
}

namespace App\Http\Controllers;
use App\Contracts\PaymentGatewayInterface;

class CheckoutController extends Controller
{
public function __construct(protected PaymentGatewayInterface $gateway) {}

public function store()
{
return $this->gateway->charge(99.99);
}
}
use App\Services\ReportService;

Route::get('/reports', function (ReportService $reportService) {
return $reportService->summary();
});

Common Mistakes

  • Creating dependencies manually: Writing new ReportService() inside controllers increases coupling. Let Laravel resolve it.
  • Forgetting interface bindings: If you inject an interface without binding it, Laravel cannot resolve it. Register it in a service provider.
  • Putting too many dependencies in one class: A constructor with many services often means the class has too many responsibilities. Split the logic.
  • Using singleton carelessly: Shared state can cause unexpected behavior. Use singletons only when appropriate.

Best Practices

  • Depend on interfaces for business services when future implementation changes are likely.
  • Keep services focused on one responsibility so injection stays clean and readable.
  • Use constructor injection for required dependencies and method injection for action-specific needs.
  • Register bindings in service providers to centralize container configuration.
  • Write tests by swapping real services with mocks or fakes through the container.

Practice Exercises

  • Create a GreetingService and inject it into a controller that returns a welcome message.
  • Create an interface called LoggerInterface, bind it to a custom logger service, and inject it into a controller.
  • Use method injection in a route closure to resolve a service and return sample data.

Mini Project / Task

Build a simple order checkout feature where a controller receives a payment gateway through dependency injection and processes a fixed payment amount.

Challenge (Optional)

Create two payment implementations, bind one by default, then change the binding so the same controller works with the second implementation without modifying controller code.

File Storage and Uploads

File storage and uploads in Laravel allow applications to receive files from users, save them in organized locations, and retrieve them later when needed. This feature exists because modern applications often need more than plain text data. Real systems store profile pictures, PDF invoices, resumes, product images, spreadsheets, and generated reports. Laravel simplifies this process through its filesystem layer, which provides a clean API for local storage and cloud services such as Amazon S3. Instead of writing low-level PHP file handling logic everywhere, you use a consistent set of tools that are easier to read, test, and maintain.

The main concepts are disks, uploaded files, validation, visibility, and storage paths. A disk is a configured storage driver defined in config/filesystems.php. Common disks include local, public, and cloud disks like s3. The local disk is usually private and lives in storage/app. The public disk is intended for files that users can access through the browser after creating a symbolic link with php artisan storage:link. Uploaded files arrive through HTTP requests and are accessed with methods like file(), hasFile(), and store().

Step-by-Step Explanation

First, create a form using method="POST" and set enctype="multipart/form-data", otherwise the browser will not send the file correctly. Second, define a route and controller method to receive the request. Third, validate the file using rules such as required, file, image, mimes:jpg,png,pdf, and max:2048. Fourth, store the file using methods like store(), storeAs(), or the Storage facade. Fifth, save the returned path in the database if the file belongs to a model such as a user, product, or document. Finally, display or download the file using Laravel helpers or the storage URL system.

Laravel supports several usage styles. Basic upload stores a file with an auto-generated name. Named upload uses storeAs() when you need a custom filename. Public file storage is useful for images that appear in the UI, while private storage is better for contracts, reports, or sensitive documents that should only be downloaded through authorized routes.

Comprehensive Code Examples

// Basic example: image upload
Route::post('/avatar', [ProfileController::class, 'upload']);

public function upload(Request $request)
{
$request->validate([
'avatar' => 'required|image|mimes:jpg,jpeg,png|max:2048',
]);

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

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

return back();
}
// Real-world example: document upload with custom name
public function store(Request $request)
{
$request->validate([
'document' => 'required|file|mimes:pdf,doc,docx|max:5120',
]);

$file = $request->file('document');
$filename = time().'_'.str_replace(' ', '_', $file->getClientOriginalName());
$path = $file->storeAs('documents', $filename, 'private');

Document::create([
'user_id' => auth()->id(),
'file_path' => $path,
]);
}
// Advanced usage: delete old file before replacing
use Illuminate\Support\Facades\Storage;

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

$user = auth()->user();

if ($user->avatar && Storage::disk('public')->exists($user->avatar)) {
Storage::disk('public')->delete($user->avatar);
}

$user->avatar = $request->file('avatar')->store('avatars', 'public');
$user->save();
}

Common Mistakes

  • Forgetting multipart/form-data: the request arrives without the file. Add the correct form encoding.
  • Skipping validation: unsafe or oversized files may be accepted. Always validate type and size.
  • Using the wrong disk: files meant for public display may be stored privately. Choose public or private storage intentionally.
  • Not deleting old files: replacing uploads without cleanup wastes storage. Remove old paths when updating.

Best Practices

  • Store only the file path in the database, not the raw file contents.
  • Use generated or sanitized filenames to avoid collisions and unsafe names.
  • Separate public assets from sensitive private documents.
  • Use authorization before serving private files.
  • Organize files into folders such as avatars, invoices, and reports.

Practice Exercises

  • Create a form that uploads a profile image and stores it on the public disk.
  • Build a document upload feature that accepts only PDF files under 2 MB.
  • Update a user avatar and delete the previous image when a new one is uploaded.

Mini Project / Task

Build a simple employee document portal where users can upload a resume, save the file path in the database, and list uploaded files on a dashboard.

Challenge (Optional)

Create a secure download route for private files so that only the owner of the document can access it.

Mail and Notifications

Mail and Notifications in Laravel are tools for sending messages to users when important events happen in an application. Mail focuses specifically on email delivery, while Notifications provide a broader system that can send messages through multiple channels like email, database records, broadcast, Slack, and more. In real applications, these features are used for welcome emails, password resets, order confirmations, invoice alerts, shipping updates, approval messages, and security warnings. Laravel makes this easier by providing clean classes, reusable templates, queue support, and channel-based delivery logic. Instead of writing raw mailing code every time, developers create Mailables for structured emails and Notification classes for flexible event messaging. This keeps communication code organized, testable, and easier to maintain.

Laravel Mail is built around mail drivers configured in the application, such as SMTP, Mailgun, Postmark, Resend, or log-based testing in development. A Mailable class defines subject, recipients, and the view used to render the email. Notifications are more flexible because one notification can decide how it should be sent using methods like via(), toMail(), and toArray(). Common notification types include mail notifications for direct email, database notifications stored for in-app display, and broadcast notifications for real-time UI updates. This separation is useful: use Mail when you are sending a dedicated email message, and use Notifications when the same event may need multiple delivery channels.

Step-by-Step Explanation

To send email in Laravel, first configure your mail driver in the .env file using values such as MAIL_MAILER, MAIL_HOST, MAIL_PORT, and credentials. Next, generate a Mailable with Artisan. Inside the class, define the content and subject. Then send it using the Mail facade. For notifications, generate a notification class and define delivery channels in the via() method. If email is one channel, implement toMail(). If storing in the database, add database to channels and define toArray(). Finally, send the notification using the notifiable model, often the User model, which already uses the Notifiable trait.

Comprehensive Code Examples

Basic example
// app/Mail/WelcomeMail.php
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;

class WelcomeMail extends Mailable
{
use Queueable;

public function build()
{
return $this->subject('Welcome to our app')
->view('emails.welcome');
}
}

// sending
use Illuminate\Support\Facades\Mail;
Mail::to($user->email)->send(new WelcomeMail());
Real-world example
// app/Notifications/OrderShipped.php
use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;

class OrderShipped extends Notification
{
use Queueable;

public function via($notifiable)
{
return ['mail', 'database'];
}

public function toMail($notifiable)
{
return (new MailMessage)
->subject('Your order has shipped')
->line('Your order is on the way.')
->action('Track Order', url('/orders/123'));
}

public function toArray($notifiable)
{
return ['message' => 'Your order has shipped'];
}
}

$user->notify(new OrderShipped());
Advanced usage
// Queue a notification for better performance
use Illuminate\Contracts\Queue\ShouldQueue;

class InvoicePaid extends Notification implements ShouldQueue
{
use Queueable;

public function via($notifiable)
{
return ['mail', 'database'];
}

public function toMail($notifiable)
{
return (new MailMessage)
->subject('Invoice Paid')
->line('We received your payment successfully.');
}
}

Common Mistakes

  • Wrong mail configuration: Emails fail because SMTP settings in .env are incorrect. Fix by verifying host, port, username, password, and encryption.
  • Forgetting the Notifiable trait: Notifications will not send from a model that does not use Notifiable. Add the trait to the User model.
  • Sending heavy notifications synchronously: This slows requests. Fix by implementing ShouldQueue and running a queue worker.

Best Practices

  • Use Mailables for structured emails and Notifications for event-driven multi-channel messaging.
  • Queue all non-critical email and notification jobs in production.
  • Keep message content clear, short, and action-oriented.
  • Store database notifications for in-app history when users may miss emails.
  • Test with log or array drivers locally before using live providers.

Practice Exercises

  • Create a Mailable that sends a welcome email after a new user registers.
  • Build a notification that sends both an email and a database notification when a profile is approved.
  • Convert an existing notification to use queues so it does not block the request.

Mini Project / Task

Build a small order update system where a customer receives an email and an in-app notification when an order status changes to shipped.

Challenge (Optional)

Create a notification that chooses channels dynamically, sending email for high-priority alerts and only database notifications for low-priority updates.

Laravel Mix and Vite

Laravel applications usually include CSS, JavaScript, images, fonts, and frontend frameworks that must be prepared before the browser can use them efficiently. That is where Laravel Mix and Vite come in. Both tools help developers bundle assets, compile preprocessors such as Sass, optimize files, and improve the development experience. Laravel Mix is built on top of Webpack and was the standard solution in older Laravel projects. Vite is the newer default in modern Laravel versions and focuses on extremely fast development startup, instant hot module replacement, and simpler configuration. In real-world applications, these tools are used to compile admin dashboards, marketing sites, authentication screens, SPA integrations with Vue or React, and custom JavaScript behavior for forms and interactive UI elements.

Laravel Mix exists because configuring Webpack manually can be complex. Mix provides a fluent API like mix.js() and mix.sass() so Laravel developers can define frontend builds quickly. Vite exists because modern frontend development demands faster feedback loops and leaner tooling. In practice, you may still maintain older projects that use Mix, while new Laravel applications generally use Vite. Understanding both helps you upgrade legacy codebases and work confidently across teams.

Core concepts

Laravel Mix uses a webpack.mix.js file and typically outputs compiled files into the public directory. Common features include JavaScript bundling, Sass or Less compilation, source maps, versioning, and copying static files. Vite uses a vite.config.js file and integrates with Laravel through the Vite plugin. It serves assets in development from a local dev server and builds optimized static assets for production. The most important sub-types to understand are development mode, where files reload quickly while you code, and production mode, where files are minimized, hashed, and optimized for deployment.

Step-by-Step Explanation

For Laravel Mix, install dependencies with npm, define asset entry points in webpack.mix.js, and run commands such as npm run dev or npm run prod. In Blade, reference built files with Laravel helpers like mix('css/app.css').

For Vite, install frontend dependencies, configure vite.config.js, and define input files such as resources/css/app.css and resources/js/app.js. During development, run npm run dev and Vite serves files instantly. In Blade, include assets using the @vite directive. For production, run npm run build, which generates optimized files in the build directory. Beginners should remember that source files usually live in resources, while browser-ready files are generated automatically.

Comprehensive Code Examples

// Basic Laravel Mix example
const mix = require('laravel-mix');

mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css');
// Blade with Mix
<link rel="stylesheet" href="{{ mix('css/app.css') }}">
<script src="{{ mix('js/app.js') }}" defer></script>
// Basic Vite example: vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';

export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
],
});
// Blade with Vite
@vite(['resources/css/app.css', 'resources/js/app.js'])
// Real-world app.js using Vite or Mix entry file
import './bootstrap';

document.addEventListener('DOMContentLoaded', () => {
const button = document.querySelector('#menu-toggle');

if (button) {
button.addEventListener('click', () => {
document.body.classList.toggle('menu-open');
});
}
});
// Advanced Mix versioning
mix.js('resources/js/app.js', 'public/js')
.sass('resources/sass/app.scss', 'public/css')
.version();

Common Mistakes

  • Editing files in public directly instead of source files in resources. Fix: always edit source assets and rebuild.

  • Using mix() in a Vite project or @vite in an older Mix-only project. Fix: match the Blade helper to the tool used by the project.

  • Forgetting to run the dev server or production build. Fix: use npm run dev while developing and npm run build or npm run prod before deployment.

Best Practices

  • Prefer Vite for new Laravel projects because it is faster and better aligned with current Laravel defaults.

  • Keep entry files small and organize modules into clear folders such as resources/js/components and resources/css.

  • Use versioning or hashed builds in production to avoid browser caching problems.

  • Document whether a project uses Mix or Vite so team members know which commands and helpers to use.

Practice Exercises

  • Create a Laravel asset setup that compiles one CSS file and one JavaScript file using Vite.

  • In a sample legacy project, write a webpack.mix.js file that compiles Sass and JavaScript into the public folder.

  • Add a button interaction in resources/js/app.js and verify that the browser updates correctly during development.

Mini Project / Task

Build a small Laravel landing page with a custom stylesheet, a JavaScript-powered mobile menu toggle, and a production-ready asset build using Vite or Mix depending on the project version.

Challenge (Optional)

Take a simple Laravel Mix configuration from an older project and outline how you would migrate it to Vite while keeping the same CSS and JavaScript entry points working correctly.

API Development with Laravel

API development in Laravel is the process of building endpoints that allow applications to exchange data, usually in JSON format. Instead of returning Blade views, API routes return structured responses for mobile apps, JavaScript frontends, third-party systems, and other services. Laravel exists to simplify this work by providing expressive routing, controllers, request validation, Eloquent ORM, authentication, rate limiting, and resource responses. In real life, a Laravel API might power a shopping app, a booking platform, or a dashboard that reads and updates data from multiple clients.

The most common API style in Laravel is REST. A resource usually supports actions such as listing records, showing one record, creating, updating, and deleting. API routes are typically defined in routes/api.php and are automatically grouped with the /api prefix. Controllers keep logic organized, models represent database tables, and request validation ensures incoming data is safe and consistent. Laravel also supports API Resources, which transform model data into clean JSON responses, making output more stable for consumers.

Step-by-Step Explanation

Start by creating an API route inside routes/api.php. You can point a route to a controller method using Route::get(), post(), put(), or delete(). For grouped CRUD actions, Route::apiResource() is the fastest option. Next, create a controller with methods like index, store, show, update, and destroy. Inside those methods, use Eloquent to query or save data. Validate requests with $request->validate() before writing to the database. Finally, return JSON using response()->json() or Resource classes for cleaner formatting.

For security, APIs often use authentication middleware such as Laravel Sanctum. Protected routes require a valid token before access is granted. You should also return proper HTTP status codes like 200 for success, 201 for created records, 404 for missing resources, and 422 for validation errors.

Comprehensive Code Examples

Basic example
// routes/api.php
use Illuminate\Support\Facades\Route;

Route::get('/message', function () {
return response()->json(['message' => 'Laravel API is working']);
});
Real-world example
// routes/api.php
use App\Http\Controllers\Api\ProductController;
Route::apiResource('products', ProductController::class);

// app/Http/Controllers/Api/ProductController.php
namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\Product;
use Illuminate\Http\Request;

class ProductController extends Controller
{
public function index() { return Product::all(); }

public function store(Request $request) {
$data = $request->validate([
'name' => 'required|string|max:255',
'price' => 'required|numeric'
]);
return response()->json(Product::create($data), 201);
}
}
Advanced usage
// app/Http/Resources/ProductResource.php
namespace App\Http\Resources;

use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;

class ProductResource extends JsonResource
{
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'name' => $this->name,
'price' => number_format($this->price, 2),
'created_at' => $this->created_at->toDateTimeString()
];
}
}

Common Mistakes

  • Using web routes instead of API routes: Put API endpoints in routes/api.php so they use the correct prefix and middleware.
  • Skipping validation: Always validate incoming data before saving it.
  • Returning inconsistent JSON: Use Resources or a standard response format for predictable output.
  • Ignoring status codes: Return the correct HTTP status for each result.

Best Practices

  • Use apiResource for standard CRUD endpoints.
  • Separate business logic from controllers when applications grow.
  • Protect private endpoints with Sanctum or another token system.
  • Use pagination for large lists to improve performance.
  • Version your API when making breaking changes.

Practice Exercises

  • Create a /api/hello route that returns a JSON welcome message.
  • Build a TaskController with index and store methods for tasks.
  • Add validation so a task requires a title and an optional description.

Mini Project / Task

Build a simple Book API with endpoints to list books, add a book, view one book, update a book, and delete a book. Return all responses in JSON and validate the title and author fields.

Challenge (Optional)

Enhance the Book API by adding token-based authentication and pagination so only logged-in users can create, update, or delete books while all users can still read paginated results.

Laravel Sanctum for API Auth

Laravel Sanctum is Laravel’s lightweight authentication package for APIs, single-page applications, and mobile clients. It exists because many projects do not need the full complexity of OAuth, yet still require secure ways to identify users and protect private endpoints. In real life, Sanctum is commonly used when building a frontend with Vue, React, or mobile apps that need login, logout, and access to user-specific data such as profiles, orders, or settings.

Sanctum supports two common approaches. First, cookie-based authentication for first-party SPAs, where the frontend and backend work closely together and use Laravel session security. Second, API token authentication, where each user can generate personal access tokens that are sent with requests. A token may also have abilities, sometimes called scopes, such as orders:read or orders:write, which helps restrict what a client can do.

Step-by-Step Explanation

Start by installing Sanctum in a Laravel project using Composer, then publish and run its migrations so Laravel can store issued tokens. After installation, make sure your API routes that need authentication use the auth:sanctum middleware. For token-based APIs, a user logs in with email and password, your application verifies the credentials, and then creates a token with createToken(). The plain text token is returned once and must be stored safely by the client. Later, the client sends it in the Authorization: Bearer TOKEN header.

When Laravel receives the request, Sanctum checks the token, finds the matching user, and makes that user available through auth()->user() or $request->user(). You can also inspect token abilities with methods like tokenCan(). To log out from one device, delete the current access token. To log out everywhere, delete all tokens for that user.

Comprehensive Code Examples

Basic example
// routes/api.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
return $request->user();
});
// app/Http/Controllers/AuthController.php
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;

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)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}

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

return response()->json(['token' => $token, 'user' => $user]);
}
Real-world example
Route::middleware('auth:sanctum')->group(function () {
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
});

public function store(Request $request)
{
if (! $request->user()->currentAccessToken()->can('orders:write')) {
abort(403, 'Token cannot create orders');
}

return $request->user()->orders()->create($request->all());
}
Advanced usage
public function login(Request $request)
{
$user = User::where('email', $request->email)->firstOrFail();

if (! Hash::check($request->password, $user->password)) {
return response()->json(['message' => 'Invalid credentials'], 401);
}

$token = $user->createToken('admin-panel', ['orders:read', 'orders:write'], now()->addDays(7));

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

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

Common Mistakes

  • Forgetting auth:sanctum middleware: protected routes remain public. Always attach the middleware.
  • Returning the token multiple times later: plain text tokens are only visible once. Store them securely on the client when created.
  • Not checking abilities: authenticated users may still do too much. Use token abilities for finer control.
  • Mixing SPA and token auth concepts: understand whether your app uses cookies or bearer tokens, then configure consistently.

Best Practices

  • Use HTTPS in production so tokens are never exposed in transit.
  • Create tokens with meaningful names like device or app names for easier management.
  • Limit token abilities to the smallest required permissions.
  • Revoke tokens on logout and remove old unused tokens regularly.
  • Validate login input and return clear API error responses.

Practice Exercises

  • Create a login endpoint that returns a Sanctum token after valid credentials.
  • Protect a /api/profile route so only authenticated users can access it.
  • Issue a token with a custom ability and block access to a route when that ability is missing.

Mini Project / Task

Build a small authenticated notes API with endpoints for login, logout, listing notes, and creating notes. Only authenticated users should access notes, and creation should require a token ability such as notes:write.

Challenge (Optional)

Add multi-device token management so a user can view all active tokens and revoke one specific token without logging out from every device.

Task Scheduling

Task Scheduling in Laravel is a clean way to define recurring jobs inside your application instead of spreading cron entries across a server. In traditional PHP apps, you might create many separate cron jobs for backups, report generation, email cleanup, subscription renewals, or database maintenance. Laravel improves this by letting you define all scheduled behavior in one central place, usually the console scheduling configuration. In real projects, task scheduling is used for sending daily summaries, processing recurring invoices, clearing stale records, syncing with third-party APIs, and running health checks. The main idea is simple: your server cron calls Laravel every minute, and Laravel decides what should run at that moment.

Laravel scheduling supports several task types. You can schedule Artisan commands, queued jobs, shell commands, and closures. Common timing methods include everyMinute(), hourly(), daily(), weekly(), and custom cron expressions through cron(). You can also add conditions such as weekdays(), between(), when(), and protection features like withoutOverlapping() and onOneServer() for distributed systems.

Step-by-Step Explanation

First, make sure your server cron runs Laravel’s scheduler every minute. The typical cron entry points to php artisan schedule:run. Second, define scheduled tasks in your Laravel application. In modern Laravel projects, scheduling is commonly configured in the console routing or bootstrap setup depending on the version. The pattern is always the same: choose the task, choose its frequency, then add optional constraints.

The syntax reads almost like English. For example, Schedule::command('reports:daily')->dailyAt('08:00'); means run an Artisan command every day at 8 AM. If a task might take longer than expected, add withoutOverlapping() so Laravel does not start a second copy before the first finishes. If you only want a task in production, use environments(['production']).

Comprehensive Code Examples

Basic example
use Illuminate\Support\Facades\Schedule;

Schedule::command('inspire')->hourly();
Real-world example
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send-digest')
->weekdays()
->dailyAt('07:30')
->withoutOverlapping()
->runInBackground();
Advanced usage
use App\Jobs\SyncVendorInventory;
use Illuminate\Support\Facades\Schedule;

Schedule::job(new SyncVendorInventory)
->everyFifteenMinutes()
->between('06:00', '22:00')
->when(fn () => app()->environment('production'))
->onOneServer()
->withoutOverlapping(30);

You can test scheduling logic locally with php artisan schedule:list to view upcoming tasks and php artisan schedule:run to trigger due tasks manually.

Common Mistakes

  • Forgetting the system cron job: Laravel schedules do nothing unless the server calls schedule:run every minute.
  • Using closures for important business logic: prefer Artisan commands or jobs because they are easier to test and maintain.
  • Ignoring overlapping tasks: long-running jobs can start multiple times; fix this with withoutOverlapping().
  • Wrong timezone assumptions: server time may differ from business time; configure timezone clearly.

Best Practices

  • Keep scheduled tasks small and move heavy work into queued jobs.
  • Use descriptive command names like subscriptions:renew or reports:generate-monthly.
  • Add logging or notifications for critical scheduled processes.
  • Use environment restrictions so development machines do not run production tasks.
  • Review task frequency carefully to avoid unnecessary database or API load.

Practice Exercises

  • Create a scheduled Artisan command that runs every day at 9:00 AM.
  • Schedule a cleanup command to run every Sunday and prevent overlapping.
  • Create a job that runs every 15 minutes only on weekdays between 8:00 AM and 6:00 PM.

Mini Project / Task

Build a scheduled maintenance workflow for a small ecommerce app that clears expired discount codes nightly, sends a morning sales summary email on weekdays, and syncs inventory from an external vendor every 30 minutes.

Challenge (Optional)

Design a scheduling strategy for a multi-server production app where some tasks must run on only one server, some should queue background jobs, and others should be restricted to business hours only.

Queues and Background Jobs

Queues and background jobs allow Laravel applications to move slow or resource-heavy work out of the main request cycle and process it later. This matters because users should not wait for tasks like sending emails, resizing images, generating invoices, importing CSV files, or calling external APIs. In real applications, a customer may submit an order and instantly receive confirmation on screen while the system quietly sends emails, updates inventory, and notifies administrators in the background.

Laravel provides a clean queue system with drivers such as database, Redis, SQS, and sync. The sync driver runs jobs immediately and is useful for local testing, while drivers like database and redis store jobs so workers can process them asynchronously. A job is usually a class that contains a unit of work. You dispatch the job, Laravel places it on a queue, and a worker pulls it off and executes its handle() method.

Retries, delays, timeouts, failed job logging, and queue prioritization are important sub-types of queue behavior. Retries help with temporary failures such as network issues. Delays let you run work later. Failed jobs can be reviewed and retried. Multiple queues let you prioritize urgent work over less important tasks.

Step-by-Step Explanation

First, configure a queue connection in .env, for example QUEUE_CONNECTION=database. Next, create the jobs table and failed jobs table using Artisan migration commands. Then create a job class with php artisan make:job. Inside the generated class, place business logic in handle(). Dispatch the job from a controller, event listener, command, or service class. Finally, start a worker using php artisan queue:work so queued jobs are processed.

When a job needs model data, pass the model or its ID into the constructor. Laravel can serialize Eloquent models, but keeping payloads small is usually better. You can also set job properties like $tries, $timeout, and use methods like delay() or onQueue().

Comprehensive Code Examples

// Basic example: create and dispatch a job
class SendWelcomeEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

public function __construct(public int $userId) {}

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

// In controller
SendWelcomeEmail::dispatch($user->id);
// Real-world example: delayed order processing
class ProcessOrderReceipt implements ShouldQueue
{
use Dispatchable, Queueable, SerializesModels;

public function __construct(public int $orderId) {}

public function handle(): void
{
$order = Order::with('user')->findOrFail($this->orderId);
Mail::to($order->user->email)->send(new OrderReceiptMail($order));
}
}

ProcessOrderReceipt::dispatch($order->id)->delay(now()->addMinutes(5));
// Advanced usage: retries and custom queue
class SyncInventory implements ShouldQueue
{
use Dispatchable, Queueable, SerializesModels;

public $tries = 3;
public $timeout = 120;

public function __construct(public int $productId) {}

public function handle(): void
{
$product = Product::findOrFail($this->productId);
app(InventorySyncService::class)->push($product);
}
}

SyncInventory::dispatch($product->id)->onQueue('high');

Common Mistakes

  • Using the sync driver in production: this removes the benefit of background processing. Use database, redis, or another async driver.
  • Forgetting to run a worker: dispatching jobs alone is not enough. Start queue:work or use a process manager.
  • Putting huge payloads in jobs: large serialized data increases memory usage. Pass IDs and reload fresh data in handle().
  • Ignoring failed jobs: always configure failed job storage and monitor exceptions.

Best Practices

  • Keep each job focused on one responsibility.
  • Pass lightweight data such as IDs instead of full collections when possible.
  • Use retries only for temporary failures, not permanent validation problems.
  • Separate urgent and non-urgent work with named queues.
  • Use supervisors or process managers in production so workers restart automatically.
  • Log failures and create alerts for critical background tasks.

Practice Exercises

  • Create a job that writes a message to the log file and dispatch it from a route.
  • Build a job that sends a delayed email 2 minutes after user registration.
  • Create two queues named high and low, then dispatch different jobs to each.

Mini Project / Task

Build a background notification system where a newly placed order dispatches a job that emails the customer and logs the order ID to a separate queue for internal processing.

Challenge (Optional)

Create a job that calls an external API, retries up to 3 times on failure, and stores unsuccessful attempts in the failed jobs table for later review.

Error Handling and Logging

Error handling and logging in Laravel help developers detect, report, understand, and respond to failures in an application. In real projects, errors can come from invalid user input, missing database records, broken third-party APIs, permission issues, or unexpected bugs. Laravel provides a structured exception system and a flexible logging layer built on top of Monolog. Together, they allow you to show safe messages to users while storing useful technical details for developers. This matters in production because users should never see sensitive stack traces, but your team still needs enough information to investigate problems quickly.

Laravel uses exceptions to represent failures. Some exceptions are thrown by the framework, such as authentication or validation failures, while others can be created manually in your code. Logging records events to channels such as a single file, daily files, Slack, syslog, or stacked combinations of channels. Common real-life uses include tracking failed payments, capturing API timeouts, logging suspicious login attempts, and recording system health warnings before issues become outages.

Core ideas include exception reporting, exception rendering, log levels, and log channels. Reporting means sending error details to logs or monitoring services. Rendering means converting an exception into an HTTP response, such as a JSON error for an API or a friendly error page for a website. Log levels include emergency, alert, critical, error, warning, notice, info, and debug. Higher severity levels indicate more urgent problems. Channels define where logs are written, and stacks let you write one message to multiple destinations at once.

Step-by-Step Explanation

Start with configuration in the config/logging.php file. Laravel reads the default log channel from the LOG_CHANNEL value in .env. For local work, developers often use stack or single. Next, use the Log facade to write messages. Choose a suitable level based on severity. Then handle exceptions in your application logic with try and catch when you expect a risky operation. For global exception behavior, Laravel centralizes this in the exception handler so unexpected failures can still be logged and transformed into clean responses.

When writing APIs, return structured JSON for errors. When building web pages, redirect back with a friendly message for recoverable problems. For critical failures, log context such as user ID, order ID, route name, and request data, but never store secrets like passwords or full payment details. In production, disable debug output with APP_DEBUG=false so internal details stay hidden from users.

Comprehensive Code Examples

use Illuminate\Support\Facades\Log;

Route::get('/basic-log', function () {
Log::info('Basic log example visited');
return 'Logged successfully';
});
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;

Route::get('/weather', function () {
try {
$response = Http::timeout(3)->get('https://api.example.com/weather');

if ($response->failed()) {
Log::warning('Weather API returned failure', [
'status' => $response->status(),
'url' => 'https://api.example.com/weather'
]);

return response()->json(['message' => 'Weather service unavailable'], 503);
}

return $response->json();
} catch (\Throwable $e) {
Log::error('Weather API exception', [
'message' => $e->getMessage()
]);

return response()->json(['message' => 'Unexpected server error'], 500);
}
});
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Facades\Log;
use App\Models\Order;

Route::get('/orders/{id}', function ($id) {
try {
$order = Order::findOrFail($id);
return $order;
} catch (ModelNotFoundException $e) {
Log::notice('Order not found', ['order_id' => $id]);
return response()->json(['message' => 'Order not found'], 404);
} catch (\Throwable $e) {
Log::critical('Order lookup failed', [
'order_id' => $id,
'exception' => $e->getMessage()
]);
return response()->json(['message' => 'Server error'], 500);
}
});

Common Mistakes

  • Leaving debug mode on in production: Set APP_DEBUG=false in production to avoid exposing stack traces.
  • Logging too little context: Add useful identifiers like user ID or resource ID so issues can be traced later.
  • Logging sensitive data: Never log passwords, tokens, card numbers, or private personal data.
  • Using the wrong log level: Use warning for recoverable issues and error or critical for serious failures.

Best Practices

  • Use structured context arrays in logs instead of unclear plain text only.
  • Prefer friendly user-facing messages and detailed developer-facing logs.
  • Use different channels for local development and production monitoring needs.
  • Catch expected exceptions close to the risky operation and let unexpected ones be handled globally.
  • Review logs regularly and connect critical channels to alerting tools when possible.

Practice Exercises

  • Create a route that writes an info log when visited and returns a success message.
  • Build a route that uses try and catch to handle a missing model and return a 404 JSON response.
  • Configure a custom log message with context containing a user ID and action name.

Mini Project / Task

Build a small order status lookup endpoint that fetches an order by ID, logs missing orders as notices, logs unexpected failures as errors, and returns safe JSON messages to the client.

Challenge (Optional)

Create a feature where failed external API calls are logged with a correlation ID, and every response sent back to the client includes that ID for easier troubleshooting across logs and support tickets.

Testing with Pest and PHPUnit

Testing in Laravel is the process of automatically checking that your application behaves the way you expect. Instead of manually clicking through forms, routes, and APIs after every change, you write tests that confirm your code still works. This matters in real projects because applications evolve constantly: new features are added, old logic is refactored, and bugs can appear unexpectedly. Automated tests help teams move faster with confidence. Laravel supports PHPUnit by default and works beautifully with Pest, a modern testing framework built on top of PHPUnit. PHPUnit is widely used in the PHP ecosystem and provides the foundation for assertions, test runners, and reporting. Pest offers a cleaner, more expressive syntax that many developers find easier to read and write. In Laravel projects, you will commonly write feature tests for routes, controllers, middleware, authentication, and APIs, and unit tests for small isolated classes such as services, helpers, or value objects. Understanding both tools is useful because Pest improves developer experience, while PHPUnit concepts still power the underlying system and often appear in documentation, CI pipelines, and legacy projects. Laravel testing typically includes unit tests, feature tests, database testing, HTTP testing, mocking, and test data generation with factories. Unit tests focus on one class or method with minimal dependencies. Feature tests verify complete request-response behavior and are often the most valuable for web apps. Pest and PHPUnit are used in real life to validate checkout flows, login systems, permissions, invoice calculations, queueable jobs, notification logic, and API contracts before deployment.

Step-by-Step Explanation

Start by creating a fresh test file. Laravel stores tests in the tests/Unit and tests/Feature directories. Pest uses functions like test() or it(), while PHPUnit uses class-based methods beginning with test. A typical flow is: create the test, prepare data, perform an action, assert the result, and run the suite with php artisan test or ./vendor/bin/pest. For database-related tests, Laravel provides traits such as RefreshDatabase so every test starts clean. Use model factories to create records quickly. In HTTP tests, you can simulate requests with methods like get(), post(), and assert response status, redirects, validation errors, JSON structure, and database state. When testing authenticated routes, sign in a user with actingAs(). If a class depends on another service, use mocks or fakes to isolate behavior. The important idea is that tests should be deterministic: the same input should produce the same result every time.

Comprehensive Code Examples

// Basic Pest feature test
use function Pest\Laravel\get;

test('homepage loads successfully', function () {
get('/')->assertStatus(200);
});
// Real-world PHPUnit test with database
namespace Tests\Feature;

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

class ProfileUpdateTest extends TestCase
{
use RefreshDatabase;

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

$response = $this->actingAs($user)->post('/profile', [
'name' => 'Ava Stone',
]);

$response->assertRedirect();
$this->assertDatabaseHas('users', ['id' => $user->id, 'name' => 'Ava Stone']);
}
}
// Advanced Pest API test
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;

uses(RefreshDatabase::class);

it('returns a list of users as json', function () {
User::factory()->count(3)->create();

$this->getJson('/api/users')
->assertOk()
->assertJsonCount(3, 'data');
});

Common Mistakes

  • Testing too much in one test. Fix: keep each test focused on one behavior.

  • Forgetting database reset traits. Fix: use RefreshDatabase for isolation.

  • Using real external services in tests. Fix: fake or mock mail, queues, events, and APIs.

  • Writing vague assertions like only checking status 200. Fix: also assert data, redirects, session, or database changes.

Best Practices

  • Prefer feature tests for user-visible behavior and business flows.

  • Name tests clearly so failures read like documentation.

  • Use factories and seed only the minimum required data.

  • Run tests often with php artisan test during development.

  • Keep tests independent, repeatable, and fast.

Practice Exercises

  • Write a Pest test that checks a public route returns status 200.

  • Create a PHPUnit feature test that verifies a guest is redirected from a protected dashboard route.

  • Build a database test that creates a user with a factory and confirms the record exists in the database.

Mini Project / Task

Create a small test suite for a blog post feature that verifies guests can view posts, authenticated users can create posts, and invalid input shows validation errors.

Challenge (Optional)

Write tests for an API endpoint that requires authentication, returns paginated JSON data, and rejects invalid query parameters with proper validation errors.

Deployment Basics

Deployment is the process of taking a Laravel application from a local development machine and making it available on a live server so real users can access it. In real projects, deployment includes preparing the server, installing dependencies, configuring environment variables, optimizing the application, running database migrations, and verifying that the app is healthy after release. Laravel applications are commonly deployed to VPS platforms, cloud servers, managed Laravel hosting, or container-based environments. Understanding deployment basics matters because a well-built app can still fail in production if permissions, caching, environment settings, or database connections are handled incorrectly.

A beginner should know that Laravel needs PHP, Composer, a web server such as Nginx or Apache, and usually a database like MySQL or PostgreSQL. The application also depends on an .env file for production settings such as database credentials, app URL, mail configuration, and cache driver. Another important idea is that deployment is not just uploading files. It is a repeatable release process. Common deployment styles include manual deployment with SSH and Git, automated deployment through CI/CD, and managed platform deployment. No matter the platform, the same core tasks appear repeatedly: pull code, install dependencies, configure environment values, build assets if needed, run migrations carefully, clear and rebuild caches, and restart services if necessary.

Step-by-Step Explanation

First, provision a server with the correct PHP version and required extensions. Second, clone or upload the Laravel project into the server directory. Third, install PHP dependencies with composer install --no-dev --optimize-autoloader. Fourth, create the production .env file and set values such as APP_ENV=production, APP_DEBUG=false, and database credentials. Fifth, generate an application key if one does not already exist. Sixth, run database migrations. Seventh, cache configuration, routes, events, and views for better performance. Finally, ensure storage and cache directories have correct write permissions, then point the web server to the public directory.

Comprehensive Code Examples

Basic example
# connect to server
ssh user@your-server

# go to project directory
cd /var/www/laravel-app

# get latest code
git pull origin main

# install production dependencies
composer install --no-dev --optimize-autoloader

# run migrations
php artisan migrate --force

# optimize application
php artisan config:cache
php artisan route:cache
php artisan view:cache
Real-world example
APP_NAME=Laravel
APP_ENV=production
APP_DEBUG=false
APP_URL=https://example.com

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_prod
DB_USERNAME=laravel_user
DB_PASSWORD=strong-password

CACHE_DRIVER=file
QUEUE_CONNECTION=database
SESSION_DRIVER=file
Advanced usage
# safe production optimization sequence
php artisan down
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan config:clear
php artisan cache:clear
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan up

Common Mistakes

  • Leaving debug mode enabled: Set APP_DEBUG=false in production to avoid exposing sensitive errors.
  • Using the wrong document root: Point the web server to Laravel’s public folder, not the project root.
  • Forgetting write permissions: Ensure storage and bootstrap/cache are writable by the web server user.
  • Running migrations without care: Use --force in production and review schema changes before release.

Best Practices

  • Keep secrets only in the production .env file and never commit them to Git.
  • Use Git-based, repeatable deployments instead of manual file copying.
  • Cache config, routes, and views after every deployment for better performance.
  • Back up the database before major releases or risky migrations.
  • Verify logs, queue workers, scheduler, and file permissions after deployment.

Practice Exercises

  • Create a checklist for deploying a basic Laravel app to a production server.
  • Write a sample production .env file for an app that uses MySQL and a public domain.
  • List the Artisan commands you would run after pulling new code to optimize a Laravel application.

Mini Project / Task

Prepare a small Laravel project for deployment by writing a step-by-step release script that pulls code, installs dependencies, runs migrations, rebuilds caches, and places the app back online safely.

Challenge (Optional)

Design a zero-surprise deployment plan for a Laravel app with a database update, including pre-deployment checks, rollback ideas, and post-deployment verification steps.

Final Project

The final project is where you combine the major Laravel skills learned throughout the course into one working application. Its purpose is to move you from isolated lessons into real product development, where routing, controllers, models, migrations, validation, authentication, file handling, views, and business logic all work together. In real life, Laravel is used to build admin panels, booking platforms, inventory systems, learning portals, internal company tools, and customer-facing SaaS products. A final project simulates that professional workflow by asking you to design, build, test, and present a complete feature-rich application instead of only writing small examples.

For this course, a strong final project could be a Task Management System, Course Portal, Inventory Tracker, or Appointment Booking App. The key idea is not the theme, but the structure. Your app should include user authentication, CRUD operations, form validation, database relationships, protected routes, reusable Blade layouts, seed data, and clean error handling. You should also think in layers: routes send requests to controllers, controllers coordinate logic, models manage data, migrations define schema, and Blade renders the UI. If you include roles such as admin and regular user, you also practice authorization and middleware. This project exists because employers and clients care less about whether you memorized syntax and more about whether you can organize a full application responsibly.

Step-by-Step Explanation

Start by defining the app goal and user stories. Example: users can register, log in, create tasks, edit tasks, mark them complete, and admins can manage all users. Next, sketch the database. A task app might need users, projects, and tasks tables with relationships such as one user has many projects and one project has many tasks.

After planning, create the Laravel project, configure the database in .env, and run migrations. Build models and relationships, then generate controllers for CRUD features. Define routes with middleware so only authenticated users can access protected pages. Create Blade views with a shared layout for navigation, alerts, and forms. Add validation inside controller methods or form request classes. Seed the database with test records so you can verify flows quickly. Finally, test core features manually and, if possible, with feature tests. A complete project should feel coherent: naming should be consistent, folders should be organized, and every feature should support the original goal.

Comprehensive Code Examples

// Basic example: routes/web.php
Route::get('/', function () {
    return view('welcome');
});

Route::middleware('auth')->group(function () {
    Route::resource('projects', ProjectController::class);
    Route::resource('tasks', TaskController::class);
});
// Real-world example: app/Models/Project.php
class Project extends Model
{
    protected $fillable = ['name', 'description', 'user_id'];

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

    public function tasks()
    {
        return $this->hasMany(Task::class);
    }
}

// app/Http/Controllers/TaskController.php
public function store(Request $request)
{
    $validated = $request->validate([
        'project_id' => 'required|exists:projects,id',
        'title' => 'required|string|max:255',
        'status' => 'required|in:pending,completed',
    ]);

    Task::create($validated);

    return redirect()->route('tasks.index')->with('success', 'Task created.');
}
// Advanced usage: authorization + eager loading
public function index()
{
    $projects = Project::with('tasks')
        ->where('user_id', auth()->id())
        ->latest()
        ->get();

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

public function update(Task $task, Request $request)
{
    abort_unless($task->project->user_id === auth()->id(), 403);

    $task->update($request->validate([
        'title' => 'required|max:255',
        'status' => 'required|in:pending,completed'
    ]));

    return back()->with('success', 'Task updated.');
}

Common Mistakes

  • Building without planning: Fix it by defining features, tables, and user flows before coding.
  • Ignoring validation: Fix it by validating every form submission and handling invalid input clearly.
  • Mixing too much logic into views: Fix it by keeping business logic in controllers, models, or dedicated classes.
  • Forgetting authorization: Fix it by checking ownership and protecting routes with middleware or policies.

Best Practices

  • Start with a small, complete scope instead of an oversized app.
  • Use resource controllers and consistent route names.
  • Create reusable Blade components or partials for forms and alerts.
  • Use migrations, seeders, and factories to make setup repeatable.
  • Write readable code and meaningful commit messages as if working on a real team.

Practice Exercises

  • Plan a Laravel project with at least three features, two models, and one relationship.
  • Create CRUD routes and controller methods for one resource such as tasks or products.
  • Add authentication and make sure only logged-in users can access the dashboard.

Mini Project / Task

Build a simple Project and Task Manager where users can register, create projects, add tasks to each project, edit them, and mark tasks as completed.

Challenge (Optional)

Extend the project by adding an admin role, dashboard statistics, and a filter system that shows tasks by status, project, and due date.

Get a Free Quote!

Fill out the form below and we'll get back to you shortly.

(Minimum characters 0 of 100)

Illustration

Fast Response

Get a quote within 24 hours

💰

Best Prices

Competitive rates guaranteed

No Obligation

Free quote with no commitment