Laravel Framework Development
By Himanshu Shekhar | 04 Feb 2022 | (0 Reviews)
Suggest Improvement on Laravel Framework Development β Click here
PHP & Laravel Foundations β Deep Introduction
Laravel is one of the most powerful PHP frameworks used for building secure, scalable, and enterprise-grade web applications. In this module from NotesTime.in, you will understand how PHP works internally, why Laravel is preferred by enterprises, and how requests flow through the Laravel framework. This module builds the foundation for professional Laravel development.
1.1 What is Laravel & Why Enterprises Use It
Laravel is a modern, open-source PHP framework designed to make web application development clean, secure, and maintainable.
- β‘ Built on MVC architecture
- π Strong security by default
- π§± Modular & scalable
- π Developer productivity focused
Laravel is used by startups, SaaS platforms, fintech apps, e-commerce systems, and enterprise portals.
Clean architecture, long-term maintainability, security, and a massive ecosystem.
1.2 PHP Execution Model & Request Flow
PHP is a server-side scripting language. Every request follows a lifecycle before a response is returned.
- Client sends HTTP request
- Web server (Nginx/Apache) receives request
- PHP engine executes the script
- Response is generated and sent back
1.3 MVC Architecture (Laravel vs Traditional MVC)
| Layer | Role | Laravel Advantage |
|---|---|---|
| Model | Business logic & database | Eloquent ORM |
| View | UI presentation | Blade templating |
| Controller | Request handling | Thin controllers |
1.4 Laravel Folder Structure
- app/ β Core application logic
- bootstrap/ β Framework bootstrapping
- config/ β Configuration files
- routes/ β Web & API routes
- resources/ β Views & assets
- storage/ β Logs, cache, uploads
1.5 Laravel Request Lifecycle
Every Laravel request follows a strict pipeline:
Browser β public/index.php β HTTP Kernel β Middleware β Controller β Response
1.6 Service Container & Dependency Injection
Laravel uses a powerful Service Container to manage class dependencies automatically.
- Reduces tight coupling
- Improves testability
- Supports clean architecture
1.7 Installing Laravel
- Using Composer
- Using Laravel Installer
- Version management
1.8 Environment Configuration
Laravel uses a .env file for environment-specific settings.
- Database credentials
- API keys
- Environment modes
1.9 Laravel CLI (Artisan Commands)
Artisan is Laravelβs command-line interface.
- Create controllers, models, migrations
- Run migrations & seeders
- Clear cache & optimize app
π Module 02 : Routing, Controllers & Views (Advanced) Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Routing, Controllers & Views in Laravel (Advanced)
Routing, Controllers, and Views form the requestβresponse backbone of every Laravel application. In this module from NotesTime.in, you will learn how Laravel handles HTTP requests internally, how to design clean controllers, and how to build scalable, reusable views using Blade. This module separates beginner Laravel users from professional backend engineers.
2.1 Routing Internals & HTTP Verbs
Routing defines how incoming HTTP requests are mapped to application logic. Laravelβs routing system is fast, expressive, and deeply integrated with middleware and controllers.
π Common HTTP Verbs
- GET β Retrieve data
- POST β Create data
- PUT / PATCH β Update data
- DELETE β Remove data
2.2 Route Model Binding (Implicit & Explicit)
Route Model Binding automatically resolves route parameters into Eloquent models.
πΉ Implicit Binding
Laravel resolves models automatically based on route parameters.
πΉ Explicit Binding
You manually define how parameters map to models.
2.3 Named Routes & URL Generation
Named routes allow applications to generate URLs without hardcoding paths.
- Improves maintainability
- Safe for refactoring
- Required for large applications
2.4 Middleware (Global, Group & Route)
Middleware acts as a filter between the request and the controller.
π‘ Types of Middleware
- Global β Runs on every request
- Group β Applied to route groups
- Route β Applied to specific routes
2.5 Controllers (Thin Controllers Principle)
Controllers should act as traffic managers, not business logic containers.
- Receive request
- Delegate to services
- Return response
2.6 Resource Controllers & RESTful Design
Laravel resource controllers map CRUD actions to RESTful routes automatically.
| Method | Purpose |
|---|---|
| index | List resources |
| store | Create resource |
| show | View single resource |
| update | Update resource |
| destroy | Delete resource |
2.7 Blade Templating Engine (Directives)
Blade is Laravelβs templating engine designed for clean, reusable views.
- Template inheritance
- Control structures
- Automatic escaping
2.8 Blade Components, Slots & View Composers
Components and slots allow modular, reusable UI building.
- Reusable layouts
- Dynamic content injection
- Cleaner views
2.9 Localization & Multi-Language Views
Localization allows applications to support multiple languages.
- Language files
- Dynamic locale switching
- SEO & global reach
You now understand Laravel routing internals, middleware, clean controllers, RESTful design, and advanced Blade templating β a critical step toward enterprise Laravel mastery.
2.10 URL Redirection & Redirect Responses
Redirection is the central nervous system of any interactive web application. In Laravel,
every redirect is a meticulously crafted HTTP response that not only sends a Location
header but also carries optional flash data, cookies, session state, and even custom macros.
This chapter dissects every layer: from the simplest redirect('/') to enterpriseβgrade
redirect management with caching, middleware pipelines, database-driven redirects, and security hardening.
1. Conceptual Foundation β HTTP Redirects and Laravelβs Abstraction
1.1 HTTP Redirect Fundamentals
An HTTP redirect is a response with a 3xx status code that instructs the client (browser, API consumer, or crawler) to make a new request to a different URL. The most common status codes are:
| Status Code | Name | Use Case | Cache Behavior |
|---|---|---|---|
| 301 | Moved Permanently | SEO changes, old URLs replaced forever, domain migrations | Permanently cached by browsers and search engines |
| 302 | Found (Temporary) | Default for Laravel redirects, after form submissions, temporary maintenance | Not cached by default, clients should continue using original URL |
| 303 | See Other | POST/redirect/GET pattern β forces GET after POST | Not cached, always changes method to GET |
| 307 | Temporary Redirect | Like 302 but guarantees HTTP method unchanged (POST stays POST) | Not cached, method preserved |
| 308 | Permanent Redirect | Like 301 but method unchanged (RFC 7538) | Permanently cached, method preserved |
1.2 Laravel's Redirect Architecture
Laravel's RedirectResponse class extends Symfony's Response and adds session flashing,
fluent chaining, and route generation. The redirect() helper returns an instance of
Illuminate\Routing\Redirector, which acts as a factory for RedirectResponse objects.
// Facade style (Illuminate\Support\Facades\Redirect)
use Illuminate\Support\Facades\Redirect;
// or simply helper
$redirect = redirect()->to('/dashboard'); // instance of RedirectResponse
return $redirect;
// What happens internally:
// 1. redirect() helper calls app('redirect')
// 2. The Redirector instance uses UrlGenerator to build URLs
// 3. Creates RedirectResponse with Location header and status code
// 4. If with() was called, flashes data to session
// 5. Returns response to browser
The Redirector holds references to:
- UrlGenerator: For generating absolute URLs from paths, routes, or actions
- Session store: For flashing data across requests
- Request instance: For accessing current request data (used in back() and intended())
2. Deep Internals: How Named Route Redirects Are Resolved
When you call redirect()->route('profile', $user), a complex resolution chain executes:
- Route name lookup: Laravel's
Routermaintains a name-to-route mapping array - Parameter binding: Route parameters are replaced with values (either positional or named)
- URL generation:
UrlGeneratorbuilds absolute URL considering:- Application URL from config
- Force HTTPS settings
- URL defaults (e.g., locale prefixes)
- Signed URL parameters if requested
- RedirectResponse creation: Sets Location header and flashes data
// What happens behind the scenes
public function route($route, $parameters = [], $status = 302, $headers = [])
{
$url = $this->generator->route($route, $parameters);
return $this->to($url, $status, $headers);
}
// The generator uses:
// 1. RouteCollection->getByName($name)
// 2. Compiles route with parameters
// 3. Applies any root URL and secure flag
// 4. Returns string URL
php artisan route:cache).
3. All Possible Redirect Creation Methods β Complete Reference
| Method / Helper | Signature | Description | Internal Implementation |
|---|---|---|---|
redirect($to = null, $status = 302, $headers = [], $secure = null) |
string|null, int, array, bool|null |
Universal redirect helper β if $to is null returns Redirector instance | Creates RedirectResponse via Redirector->to() |
redirect()->to($to, $status, $headers, $secure) |
string, int, array, bool|null |
Redirect to specific path or full URL | Calls UrlGenerator->to() and creates response |
redirect()->route($name, $parameters = [], $status = 302, $headers = []) |
string, array|mixed, int, array |
Redirect to named route (most maintainable approach) | UrlGenerator->route() then to() |
redirect()->action($action, $parameters = [], $status = 302, $headers = []) |
string|array, array|mixed, int, array |
Redirect to controller action (supports string or array syntax) | UrlGenerator->action() then to() |
redirect()->back($status = 302, $headers = [], $fallback = null) |
int, array, string |
Redirect to previous page using HTTP_REFERER or session | Uses request->headers->get('referer') or session previous URL |
redirect()->away($url, $status = 302, $headers = []) |
string, int, array |
Redirect to external domain without URL validation | Skips UrlGenerator, directly creates response with URL |
redirect()->guest($path, $status = 302, $headers = []) |
string, int, array |
Store current URL in session and redirect to login page | Saves 'url.intended' in session, then redirects to $path |
redirect()->intended($default = '/', $status = 302, $headers = []) |
string, int, array |
Redirect to originally intended page after authentication | Retrieves 'url.intended' from session, falls back to $default |
redirect()->refresh($status = 302, $headers = []) |
int, array |
Redirect to the same page (reload current URL) | Uses request->url() as destination |
redirect()->secure($path, $status = 302, $headers = []) |
string, int, array |
Redirect to HTTPS version of path | Forces secure parameter true in UrlGenerator |
redirect()->home() |
int, array |
Redirect to application home page (/) | Alias for redirect('/') |
4. How to Add URL Redirection in Every Layer of Your Application
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|max:255',
'body' => 'required',
]);
$post = Post::create($validated);
// Basic redirect
return redirect('/posts');
// Named route with flash message
return redirect()->route('posts.show', $post)
->with('success', 'Post created successfully!')
->with('post_id', $post->id);
// With cookie
return redirect()->route('dashboard')
->withCookie(cookie('last_visit', now(), 60));
// Conditional redirects
if ($request->wantsJson()) {
return response()->json($post, 201);
}
return redirect()->back()->with('warning', 'Something went wrong');
}
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class PostRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'title' => 'required|max:255',
'body' => 'required',
];
}
/**
* Get the URL to redirect to on validation failure.
*/
protected function getRedirectUrl()
{
return $this->redirector->getUrlGenerator()->route('posts.create');
}
/**
* Get the response for a forbidden operation.
*/
public function forbiddenResponse()
{
return redirect()->route('home')
->with('error', 'You are not authorized to create posts.');
}
}
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class RedirectIfNotActive
{
public function handle(Request $request, Closure $next)
{
if ($request->user() && !$request->user()->isActive()) {
// Logout inactive user
auth()->logout();
return redirect()->route('login')
->with('error', 'Your account has been deactivated.')
->with('deactivated', true);
}
return $next($request);
}
}
// In Kernel.php - register as route middleware
protected $routeMiddleware = [
'active' => \App\Http\Middleware\RedirectIfNotActive::class,
];
// In routes/web.php
Route::middleware(['auth', 'active'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});
// Simple redirect
Route::redirect('/here', '/there', 301);
// Redirect with parameters (Laravel 8+)
Route::redirect('/old/{id}', '/new/{id}', 301);
// Permanent redirect macro
Route::permanentRedirect('/old-about', '/about');
// Redirect using controller (for complex logic)
Route::get('/redirect/{code}', [RedirectController::class, 'handle']);
// Group of redirects
Route::prefix('legacy')->group(function () {
Route::redirect('/users', '/admin/users');
Route::redirect('/posts', '/blog');
});
Note: Redirects only work in HTTP context. In jobs or listeners, you can queue redirect URLs for later use:
class UserRegisteredListener
{
public function handle(UserRegistered $event)
{
// Can't redirect directly, but can:
// 1. Store redirect URL in session
session(['post_registration_redirect' => route('welcome')]);
// 2. Queue a notification with redirect URL
$event->user->notify(new WelcomeNotification());
// 3. Update user with redirect flag
$event->user->update([
'requires_redirect' => true,
'redirect_to' => route('profile.complete')
]);
}
}
// app/Exceptions/Handler.php
public function render($request, Throwable $exception)
{
// Redirect unauthenticated users
if ($exception instanceof AuthenticationException) {
return redirect()->guest('login');
}
// Redirect on 404 for legacy URLs
if ($exception instanceof NotFoundHttpException) {
$legacyRedirect = $this->findLegacyRedirect($request->path());
if ($legacyRedirect) {
return redirect($legacyRedirect, 301);
}
}
// Redirect on maintenance mode exceptions
if ($exception instanceof HttpException && $exception->getStatusCode() === 503) {
return redirect()->route('maintenance');
}
return parent::render($request, $exception);
}
5. How to Remove / Delete a Redirect β Step-by-Step Guide
5.1 Finding Redirect Sources
# Search in controllers
grep -r "redirect(" app/Http/Controllers/
# Search in routes
grep -r "Route::redirect" routes/
# Search in middleware
grep -r "redirect()->" app/Http/Middleware/
# Search in views (rare, but possible)
grep -r "redirect()->" resources/views/
5.2 Safe Removal Process
- Phase 1: Monitor (1-4 weeks)
- Add logging to track redirect usage
- Monitor access logs for the old URL
- Check Google Search Console for indexed URLs
- Phase 2: Change to 302 (if currently 301)
- Temporary redirects prevent browser caching
- Update status code from 301 to 302
- Continue monitoring for 1-2 weeks
- Phase 3: Conditional redirect
// Instead of direct redirect, add debug logging Route::get('/old-url', function () { Log::info('Redirect hit', [ 'user_agent' => request()->userAgent(), 'referer' => request()->header('referer') ]); return redirect('/new-url', 302); }); - Phase 4: Remove completely
- Delete the redirect code
- Clear route cache:
php artisan route:clear - If using database redirects, soft-delete or archive
- Monitor 404 errors for unexpected traffic
6. Managing Redirects at Scale β Enterprise Patterns
In large applications (1000+ routes), scattered redirects become unmaintainable. Here are enterprise-grade patterns:
6.1 Centralized Redirect Configuration
// config/redirects.php
return [
'redirects' => [
['from' => '/old-about', 'to' => '/about', 'status' => 301],
['from' => '/old-contact', 'to' => '/contact', 'status' => 301],
['from' => '/blog/*', 'to' => '/articles', 'status' => 302],
],
'external' => [
['from' => '/fb', 'to' => 'https://facebook.com/company', 'status' => 302],
],
'wildcards' => [
'/product/*' => '/shop',
'/category/([0-9]+)' => '/categories/$1',
],
];
// App\Providers\RouteServiceProvider.php
public function boot()
{
parent::boot();
// Register static redirects
foreach (config('redirects.redirects') as $redirect) {
Route::redirect($redirect['from'], $redirect['to'], $redirect['status']);
}
// Register external redirects
foreach (config('redirects.external') as $redirect) {
Route::permanentRedirect($redirect['from'], $redirect['to']);
}
// Register wildcard redirects using Route::get with regex
foreach (config('redirects.wildcards') as $pattern => $target) {
Route::get($pattern, function ($param = null) use ($target) {
return redirect(str_replace('$1', $param, $target), 301);
});
}
}
6.2 Database-Driven Redirect Manager
// Migration
Schema::create('redirects', function (Blueprint $table) {
$table->id();
$table->string('from_path')->unique();
$table->string('to_url');
$table->unsignedSmallInteger('status_code')->default(301);
$table->boolean('is_active')->default(true);
$table->integer('hit_count')->default(0);
$table->timestamp('last_hit_at')->nullable();
$table->json('metadata')->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['is_active', 'from_path']);
});
// app/Models/Redirect.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class Redirect extends Model
{
protected $fillable = [
'from_path', 'to_url', 'status_code', 'is_active', 'metadata'
];
protected $casts = [
'is_active' => 'boolean',
'metadata' => 'array',
'last_hit_at' => 'datetime',
];
protected static function booted()
{
static::saved(function () {
Cache::forget('active_redirects');
});
}
public function incrementHit()
{
$this->increment('hit_count');
$this->update(['last_hit_at' => now()]);
}
}
// app/Http/Middleware/CheckRedirects.php
namespace App\Http\Middleware;
use App\Models\Redirect;
use Closure;
use Illuminate\Support\Facades\Cache;
class CheckRedirects
{
public function handle($request, Closure $next)
{
$path = $request->path();
// Cache active redirects for performance
$redirects = Cache::rememberForever('active_redirects', function () {
return Redirect::where('is_active', true)
->pluck('to_url', 'from_path')
->toArray();
});
// Check exact match
if (isset($redirects[$path])) {
$redirect = Redirect::where('from_path', $path)->first();
$redirect->incrementHit();
return redirect($redirect->to_url, $redirect->status_code);
}
// Check wildcard patterns
foreach ($redirects as $pattern => $to) {
if (str_contains($pattern, '*')) {
$pattern = str_replace('*', '(.*)', preg_quote($pattern, '/'));
if (preg_match("/^{$pattern}$/", $path, $matches)) {
$url = str_replace('*', $matches[1] ?? '', $to);
$redirect = Redirect::where('from_path', $pattern)->first();
if ($redirect) {
$redirect->incrementHit();
}
return redirect($url, $redirect->status_code ?? 301);
}
}
}
return $next($request);
}
}
// Register in Kernel.php
protected $middleware = [
// ...
\App\Http\Middleware\CheckRedirects::class,
];
6.3 Redirect Management UI with Filament/Nova
// Using Filament Admin
namespace App\Filament\Resources;
use App\Filament\Resources\RedirectResource\Pages;
use App\Models\Redirect;
use Filament\Forms;
use Filament\Resources\Form;
use Filament\Resources\Resource;
use Filament\Resources\Table;
use Filament\Tables;
class RedirectResource extends Resource
{
protected static ?string $model = Redirect::class;
protected static ?string $navigationIcon = 'heroicon-o-switch-horizontal';
public static function form(Form $form): Form
{
return $form
->schema([
Forms\Components\TextInput::make('from_path')
->required()
->unique(ignoreRecord: true)
->helperText('The URL path to redirect from (e.g., /old-page)'),
Forms\Components\TextInput::make('to_url')
->required()
->helperText('The URL to redirect to (can be full URL or path)'),
Forms\Components\Select::make('status_code')
->options([
301 => '301 - Moved Permanently',
302 => '302 - Found (Temporary)',
307 => '307 - Temporary Redirect',
308 => '308 - Permanent Redirect',
])
->default(301),
Forms\Components\Toggle::make('is_active')
->default(true),
Forms\Components\KeyValue::make('metadata')
->keyLabel('Header')
->valueLabel('Value')
->helperText('Additional headers to send with redirect'),
]);
}
public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('from_path')
->searchable()
->sortable(),
Tables\Columns\TextColumn::make('to_url')
->searchable()
->limit(50),
Tables\Columns\BadgeColumn::make('status_code')
->colors([
'success' => 301,
'warning' => 302,
]),
Tables\Columns\BooleanColumn::make('is_active'),
Tables\Columns\TextColumn::make('hit_count')
->sortable(),
Tables\Columns\TextColumn::make('last_hit_at')
->dateTime(),
])
->filters([
Tables\Filters\SelectFilter::make('status_code')
->options([
301 => '301 Permanent',
302 => '302 Temporary',
]),
Tables\Filters\TernaryFilter::make('is_active'),
])
->actions([
Tables\Actions\EditAction::make(),
Tables\Actions\DeleteAction::make(),
])
->bulkActions([
Tables\Actions\DeleteBulkAction::make(),
]);
}
}
7. Advanced Redirect Control: Macros, Custom Status, Fragments
7.1 Custom Redirect Macros
// App\Providers\AppServiceProvider.php
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
public function boot()
{
// Add macro to RedirectResponse
RedirectResponse::macro('withAbort', function ($message, $code = 400) {
return $this->with('error', $message)
->with('abort_code', $code)
->with('should_abort', true);
});
// Add macro to Redirector (redirect() helper)
Redirector::macro('switchedDomain', function ($newDomain) {
$current = $this->getUrlGenerator()->to('/');
$newUrl = str_replace(
parse_url($current, PHP_URL_HOST),
$newDomain,
$current
);
return $this->away($newUrl, 301);
});
// Add macro for redirect with cache headers
RedirectResponse::macro('withCache', function ($seconds = 3600) {
return $this->header('Cache-Control', "public, max-age={$seconds}");
});
}
// Usage
return redirect()->route('dashboard')
->withAbort('Invalid state detected', 422);
return redirect()->switchedDomain('new.example.com');
return redirect('/new-page')
->withCache(86400)
->withCookie(cookie('visited', true));
7.2 Custom Status Codes and Fragments
// Custom 308 Permanent Redirect (RFC 7538)
return redirect('/new-address', 308);
// Redirect with fragment (#)
return redirect()->route('settings') . '#billing-section';
// Redirect with query parameters
return redirect()->route('search', ['q' => 'laravel', 'page' => 2]);
// Redirect with array parameters
return redirect()->route('profile', ['id' => $user->id, 'tab' => 'posts']);
// Conditional status based on environment
$status = app()->environment('production') ? 301 : 302;
return redirect('/new', $status);
7.3 Signed Redirects
// Generate signed URL
use Illuminate\Support\Facades\URL;
$url = URL::temporarySignedRoute(
'unsubscribe',
now()->addDays(7),
['user' => $user->id, 'email' => $user->email]
);
// Redirect to signed URL
return redirect($url);
// Verify in controller
public function unsubscribe(Request $request)
{
if (!$request->hasValidSignature()) {
abort(401, 'Invalid or expired link.');
}
// Process unsubscribe
return redirect('/')->with('success', 'Unsubscribed successfully');
}
8. Deep Security β Open Redirect Prevention & Best Practices
https://yourapp.com/redirect?url=http://evil.com/phishing.
If your code does return redirect($request->input('url'));,
the browser is sent to evil.com while the user thinks they clicked a trusted link.
8.1 Safe Practices
// UNSAFE - Never do this!
return redirect($request->input('return_url'));
// SAFE - Whitelist approach
$allowed = ['/dashboard', '/profile', '/settings'];
$destination = $request->input('return_url');
if (in_array($destination, $allowed, true)) {
return redirect($destination);
}
return redirect('/dashboard');
// SAFE - Domain validation
function isSafeUrl($url)
{
$parsed = parse_url($url);
// If no host, it's a relative path - safe
if (!isset($parsed['host'])) {
return true;
}
// Check if host matches application domain
$appHost = parse_url(config('app.url'), PHP_URL_HOST);
return $parsed['host'] === $appHost;
}
// SAFE - Using intended() which only uses session-stored URLs
return redirect()->intended('/dashboard');
// SAFE - Using Laravel's URL validation helper
use Illuminate\Support\Str;
if (Str::startsWith($url, config('app.url'))) {
return redirect($url);
}
8.2 Redirect Security Middleware
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Str;
class ValidateRedirectUrl
{
protected $allowedDomains = [
'example.com',
'api.example.com',
];
protected $allowedPaths = [
'/dashboard',
'/profile/*',
'/settings',
];
public function handle($request, Closure $next)
{
$response = $next($request);
// Only inspect redirect responses
if ($response instanceof RedirectResponse) {
$targetUrl = $response->getTargetUrl();
if (!$this->isAllowed($targetUrl)) {
// Log potential attack
logger()->warning('Blocked potential open redirect', [
'target' => $targetUrl,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
// Redirect to safe fallback
return redirect('/');
}
}
return $response;
}
protected function isAllowed($url)
{
$parsed = parse_url($url);
// Allow relative URLs
if (!isset($parsed['host'])) {
return $this->isPathAllowed($parsed['path'] ?? '/');
}
// Check domain
if (!in_array($parsed['host'], $this->allowedDomains)) {
return false;
}
// Check path if specified
return $this->isPathAllowed($parsed['path'] ?? '/');
}
protected function isPathAllowed($path)
{
foreach ($this->allowedPaths as $allowed) {
if (str_contains($allowed, '*')) {
$pattern = str_replace('*', '.*', preg_quote($allowed, '/'));
if (preg_match("/^{$pattern}$/", $path)) {
return true;
}
} elseif ($path === $allowed) {
return true;
}
}
return false;
}
}
8.3 Security Checklist
- β Never use raw user input as redirect destination
- β Always validate redirect URLs against whitelist
- β
Use
redirect()->intended()for post-login redirects - β Log suspicious redirect attempts
- β Set secure cookie flags on redirect responses
- β Use HTTPS for all redirects in production
- β Implement rate limiting on redirect endpoints
- β Regular security audits of redirect logic
9. Redirect Testing β Comprehensive Assertions
9.1 PHPUnit / Pest Redirect Assertions
use Tests\TestCase;
class RedirectTest extends TestCase
{
/** @test */
public function it_redirects_to_dashboard_after_login()
{
$user = User::factory()->create();
$response = $this->post('/login', [
'email' => $user->email,
'password' => 'password',
]);
// Basic redirect assertions
$response->assertRedirect('/dashboard');
$response->assertRedirect(route('dashboard'));
// Laravel 10+ specific
$response->assertRedirectToRoute('dashboard');
$response->assertRedirectToSignedRoute('dashboard');
// Assert status code
$response->assertStatus(302);
// Assert flash data
$response->assertSessionHas('success', 'Welcome back!');
$response->assertSessionHasAll([
'success' => 'Welcome back!',
'user_id' => $user->id,
]);
// Assert session has input (for redirect back with errors)
$response->assertSessionHasInput();
// Assert no errors
$response->assertSessionHasNoErrors();
// Assert specific errors
$response->assertSessionHasErrors(['email']);
}
/** @test */
public function it_handles_validation_redirects()
{
$response = $this->post('/posts', []);
$response->assertSessionHasErrors(['title', 'body']);
$response->assertSessionHasErrors([
'title' => 'The title field is required.'
]);
}
/** @test */
public function it_redirects_with_cookies()
{
$response = $this->get('/set-cookie-and-redirect');
$response->assertRedirect('/');
$response->assertCookie('name', 'value');
$response->assertCookieExpired('name', false);
$response->assertCookieNotExpired('name');
}
/** @test */
public function it_prevents_open_redirects()
{
$response = $this->get('/redirect', ['url' => 'http://evil.com']);
$response->assertRedirect();
$this->assertStringStartsWith(
config('app.url'),
$response->headers->get('Location')
);
}
/** @test */
public function it_tracks_redirect_chains()
{
$response = $this->followingRedirects()
->get('/redirect-chain');
$response->assertOk();
$response->assertSee('Final Destination');
// Test redirect history
$this->assertEquals(3, count($response->redirectHistory()));
}
}
9.2 Custom Redirect Test Helpers
// Tests/CreatesApplication.php
namespace Tests;
trait RedirectAssertions
{
protected function assertRedirectPath($response, $expectedPath)
{
$location = $response->headers->get('Location');
$this->assertStringEndsWith($expectedPath, $location);
}
protected function assertRedirectToExternalDomain($response, $domain)
{
$location = $response->headers->get('Location');
$parsed = parse_url($location);
$this->assertEquals($domain, $parsed['host'] ?? null);
}
protected function assertRedirectHasHeader($response, $key, $value)
{
$this->assertEquals($value, $response->headers->get($key));
}
}
10. Performance Optimization for Redirects
10.1 Caching Redirects
use Illuminate\Support\Facades\Cache;
class RedirectService
{
public function find($path)
{
return Cache::remember("redirect.{$path}", 86400, function () use ($path) {
return Redirect::where('from_path', $path)
->where('is_active', true)
->first();
});
}
public function clearCache($path = null)
{
if ($path) {
Cache::forget("redirect.{$path}");
} else {
Cache::tags(['redirects'])->flush();
}
}
}
10.2 Redirect Performance Benchmarks
| Redirect Type | Time (ms) | Memory (KB) |
|---|---|---|
| Route::redirect() - cached routes | 0.5 | 0.5 |
| Controller redirect | 5-10 | 2-3 |
| Database redirect (no cache) | 15-25 | 4-6 |
| Database redirect (with cache) | 1-2 | 0.5 |
10.3 Optimizing Redirect Middleware
public function handle($request, Closure $next)
{
// Early return for excluded paths
if ($this->shouldExclude($request)) {
return $next($request);
}
// Check cache first
$redirect = Cache::remember("redirect.{$request->path()}", 3600, function () {
return $this->findRedirect($request->path());
});
if ($redirect) {
// Increment hit asynchronously (queue to avoid delay)
dispatch(new IncrementRedirectHit($redirect->id));
return redirect($redirect->to_url, $redirect->status_code);
}
return $next($request);
}
11. Debugging Redirect Issues
11.1 Common Pitfalls and Solutions
| Problem | Cause | Solution |
|---|---|---|
| Redirect loops | Condition always true on target page | Add exception check, use session flag |
| Flash data lost | Reading flash before redirect, or multiple redirects | Read with session('key'), ensure single redirect |
| Cache issues | Browser/proxy caching 301 redirects | Use 302 for temporary, add cache headers |
| Middleware order | Redirect before session started | Ensure session middleware runs first |
11.2 Debugging Tools
// Log all redirects
// App\Http\Middleware\LogRedirects.php
public function handle($request, Closure $next)
{
$response = $next($request);
if ($response instanceof RedirectResponse) {
logger()->info('Redirect executed', [
'from' => $request->fullUrl(),
'to' => $response->getTargetUrl(),
'status' => $response->getStatusCode(),
'session' => session()->all(),
]);
}
return $response;
}
// Debug bar integration
// config/debugbar.php
'collectors' => [
'redirects' => true,
]
12. Real-World Redirect Patterns
12.1 A/B Testing Redirects
public function handleABTest($request)
{
$experiment = Experiment::active('landing_page');
if (!$experiment || $request->cookie('ab_test_assigned')) {
return $next($request);
}
$variant = $experiment->getVariant();
return redirect($variant->url)
->withCookie(cookie('ab_test_assigned', $variant->id, 43200))
->withCookie(cookie('ab_test_name', $experiment->name, 43200));
}
12.2 Geo-Location Based Redirects
public function handleGeoRedirect($request)
{
$country = geoip($request->ip())->country;
$redirect = match ($country) {
'US' => '/us',
'GB' => '/uk',
'DE' => '/de',
default => '/intl'
};
return redirect($redirect, 302);
}
12.3 Maintenance Mode with Exception
public function handleMaintenance($request)
{
if (app()->isDownForMaintenance()) {
// Allow specific IPs
$allowed = ['192.168.1.1', '10.0.0.1'];
if (!in_array($request->ip(), $allowed)) {
return redirect()->route('maintenance')
->with('retry_after', 300);
}
}
return $next($request);
}
12.4 Multi-tenant Subdomain Redirects
public function handleTenantRedirect($request)
{
$host = $request->getHost();
$subdomain = explode('.', $host)[0];
$tenant = Tenant::where('subdomain', $subdomain)->first();
if (!$tenant) {
return redirect('https://app.example.com/not-found', 404);
}
if ($tenant->custom_domain) {
return redirect("https://{$tenant->custom_domain}", 301);
}
return $next($request);
}
13. Redirect Analytics and Monitoring
13.1 Tracking Redirect Metrics
class RedirectTracker
{
public function track(Redirect $redirect, Request $request)
{
RedirectHit::create([
'redirect_id' => $redirect->id,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
'referer' => $request->header('referer'),
'query_params' => $request->query(),
'cookies' => $request->cookies->all(),
]);
$redirect->increment('hits');
$redirect->update(['last_hit_at' => now()]);
}
}
// Analytics dashboard query
$topRedirects = Redirect::withCount('hits')
->orderBy('hits_count', 'desc')
->take(10)
->get();
$brokenRedirects = Redirect::where('to_url', 'LIKE', '%404%')
->orWhereRaw('hits > 0 AND last_hit_at < ?', [now()->subDays(30)])
->get();
13.2 Real-time Redirect Monitoring with WebSockets
class RedirectBroadcast
{
public function broadcastHit(Redirect $redirect, Request $request)
{
broadcast(new RedirectHitEvent($redirect, [
'country' => geoip($request->ip())->country,
'path' => $redirect->from_path,
'timestamp' => now(),
]));
}
}
14. Summary β Full Lifecycle Management
- Controllers
- Routes (Route::redirect)
- Middleware
- Form Requests
- Exception Handlers
- Database records
- Config files
- Database + Cache
- Admin UI
- Analytics tracking
- Testing suite
- Monitoring tools
- Delete/comment code
- Deactivate DB records
- Clear route cache
- Monitor 404s
- Update external links
- SEO reconsideration
- Never trust user input
- Use intended()
- Domain whitelist
- Rate limiting
- Log suspicious activity
- Security audits
π Module 02 : Routing, Controllers & Views (Advanced) Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Database, Eloquent & Data Modeling β Complete Mastery Guide
Database design and Eloquent ORM form the foundation of every Laravel application. This comprehensive module from NotesTime.in takes you from basic database connections to advanced Eloquent relationships, performance optimization, and enterprise-grade data modeling patterns. Master how Laravel interacts with databases, write elegant queries, and design scalable data architectures.
Database operations account for 70-80% of backend application performance. Proper data modeling and Eloquent usage can make your application 10x faster and more maintainable.
3.1 Database Connections & Multi-Database Setup (Production Ready)
Laravel simplifies database connections through a unified configuration system.
The config/database.php file manages all database connections,
allowing you to switch between MySQL, PostgreSQL, SQLite, SQL Server, and more
without changing your application code.
π Default Configuration Structure
// config/database.php
return [
'default' => env('DB_CONNECTION', 'mysql'),
'connections' => [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'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', ''),
'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'),
]) : [],
],
'pgsql' => [ /* PostgreSQL configuration */ ],
'sqlite' => [ /* SQLite configuration */ ],
'sqlsrv' => [ /* SQL Server configuration */ ],
],
];
Enterprise applications often require multiple database connections for:
- Read/Write Splitting: Separate databases for read and write operations
- Multi-tenant Applications: Each tenant has their own database
- Legacy Integration: Connect to existing databases alongside your main one
- Reporting Database: Dedicated database for analytics and reporting
- Geographic Distribution: Different databases for different regions
π§ Configuring Multiple Connections
// config/database.php
'connections' => [
// Primary write database
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_DATABASE', 'main_db'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
],
// Read replica for scaling
'mysql_read' => [
'driver' => 'mysql',
'host' => env('DB_READ_HOST', '192.168.1.100'),
'database' => env('DB_DATABASE', 'main_db'),
'username' => env('DB_READ_USERNAME', 'readonly'),
'password' => env('DB_READ_PASSWORD', ''),
],
// Analytics/reporting database
'analytics' => [
'driver' => 'mysql',
'host' => env('ANALYTICS_DB_HOST', '127.0.0.1'),
'database' => env('ANALYTICS_DB_DATABASE', 'analytics'),
'username' => env('ANALYTICS_DB_USERNAME', 'analytics_user'),
'password' => env('ANALYTICS_DB_PASSWORD', ''),
],
// Legacy application database
'legacy' => [
'driver' => 'mysql',
'host' => env('LEGACY_DB_HOST', '192.168.1.200'),
'database' => env('LEGACY_DB_DATABASE', 'legacy_app'),
'username' => env('LEGACY_DB_USERNAME', 'legacy_user'),
'password' => env('LEGACY_DB_PASSWORD', ''),
'charset' => 'latin1', // Legacy databases may use older charset
],
],
π Using Multiple Connections in Models
// app/Models/User.php
class User extends Authenticatable
{
// Use default connection (mysql)
protected $connection = 'mysql';
// Rest of the model...
}
// app/Models/Analytics/PageView.php
namespace App\Models\Analytics;
use Illuminate\Database\Eloquent\Model;
class PageView extends Model
{
// Use analytics connection
protected $connection = 'analytics';
protected $table = 'page_views';
// This model doesn't use timestamps for performance
public $timestamps = false;
}
// Using connection at runtime
$users = DB::connection('mysql_read')->table('users')->get();
$report = DB::connection('analytics')->select('SELECT * FROM daily_reports');
π Automatic Read/Write Separation
// config/database.php
'connections' => [
'mysql' => [
'driver' => 'mysql',
'write' => [
'host' => env('DB_WRITE_HOST', 'master.cluster.com'),
],
'read' => [
'host' => [
env('DB_READ_HOST_1', 'replica1.cluster.com'),
env('DB_READ_HOST_2', 'replica2.cluster.com'),
],
],
'sticky' => true, // Ensures read-after-write consistency
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
// ... other configuration
],
];
// Laravel automatically routes SELECT queries to read servers
// and INSERT/UPDATE/DELETE to write servers
3.2 Migrations, Rollbacks & Version Control (Database as Code)
Migrations are like Git for your database schema. They allow teams to modify and share the database schema in a consistent, version-controlled way. Each migration is a PHP class that describes changes to the database.
π― Why Migrations Are Essential
- Team Collaboration: All developers have identical database structures
- Version Control: Track database changes in Git alongside application code
- Deployment Safety: Automatically apply changes in production
- Rollback Capability: Revert problematic changes
- Environment Consistency: Dev, staging, and production stay in sync
π Basic Migration Structure
// database/migrations/2024_01_01_000000_create_posts_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id(); // Auto-incrementing primary key
$table->string('title'); // VARCHAR column
$table->string('slug')->unique(); // Unique index
$table->text('content'); // TEXT column
$table->longText('body')->nullable(); // LONGTEXT, nullable
$table->foreignId('user_id') // Foreign key
->constrained() // References users.id
->onDelete('cascade'); // Cascade on delete
$table->integer('views')->default(0); // Integer with default
$table->boolean('is_published')->default(false);
$table->json('metadata')->nullable(); // JSON column
$table->timestamp('published_at')->nullable();
$table->softDeletes(); // Adds deleted_at column
$table->timestamps(); // Adds created_at and updated_at
// Composite index
$table->index(['user_id', 'is_published']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('posts');
}
};
π§ Migration Commands Reference
| Command | Description | Use Case |
|---|---|---|
php artisan make:migration create_posts_table |
Create a new migration | Start of new feature |
php artisan migrate |
Run all pending migrations | Daily development, deployment |
php artisan migrate:rollback |
Rollback last batch of migrations | Fix mistakes, undo changes |
php artisan migrate:reset |
Rollback all migrations | Reset local development DB |
php artisan migrate:refresh |
Rollback and migrate again | Rebuild entire database |
php artisan migrate:fresh |
Drop tables and re-migrate | Complete rebuild (loses data) |
php artisan migrate:status |
Show migration status | Check which migrations ran |
π Advanced Migration Operations
// Modifying existing tables
Schema::table('users', function (Blueprint $table) {
// Add new column
$table->string('phone')->nullable()->after('email');
// Modify existing column
$table->string('name', 100)->change();
// Rename column
$table->renameColumn('name', 'full_name');
// Drop column
$table->dropColumn('legacy_field');
// Add foreign key
$table->foreign('role_id')
->references('id')
->on('roles')
->onDelete('set null');
// Drop foreign key
$table->dropForeign(['role_id']);
// Add indexes
$table->index('email');
$table->unique('username');
// Drop indexes
$table->dropIndex(['email']);
$table->dropUnique(['username']);
});
// Conditional migrations (check if table/column exists)
if (!Schema::hasTable('backups')) {
Schema::create('backups', function (Blueprint $table) {
$table->id();
$table->string('filename');
$table->timestamps();
});
}
if (!Schema::hasColumn('users', 'avatar')) {
Schema::table('users', function (Blueprint $table) {
$table->string('avatar')->nullable();
});
}
- Always backup your database before running migrations in production
- Test migrations on staging first
- Use
--pretend flag to see SQL without executing: php artisan migrate --pretend- Consider using
migrate:rollback cautiously in production
3.3 Seeders & Factories β Realistic Test Data Generation
Seeders provide a structured way to populate your database with test data. They're essential for development, testing, and initial production setup.
π Creating a Seeder
// database/seeders/UserSeeder.php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run(): void
{
// Create single user
User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
'is_admin' => true,
]);
// Create multiple users
for ($i = 1; $i <= 10; $i++) {
User::create([
'name' => "User {$i}",
'email' => "user{$i}@example.com",
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
}
}
}
// database/seeders/DatabaseSeeder.php
class DatabaseSeeder extends Seeder
{
public function run(): void
{
$this->call([
UserSeeder::class,
PostSeeder::class,
CommentSeeder::class,
]);
}
}
Factories generate large amounts of realistic test data using Faker. They're perfect for testing, benchmarking, and development environments.
π Creating a Factory
// database/factories/UserFactory.php
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class UserFactory extends Factory
{
protected $model = User::class;
public function definition(): array
{
return [
'name' => fake()->name(),
'email' => fake()->unique()->safeEmail(),
'email_verified_at' => now(),
'password' => Hash::make('password'), // password
'remember_token' => Str::random(10),
'bio' => fake()->paragraph(),
'age' => fake()->numberBetween(18, 80),
'is_active' => fake()->boolean(90), // 90% true
'role' => fake()->randomElement(['user', 'editor', 'admin']),
'settings' => [
'notifications' => fake()->boolean(),
'theme' => fake()->randomElement(['light', 'dark']),
],
];
}
/**
* Indicate that the user is an admin.
*/
public function admin(): static
{
return $this->state(fn (array $attributes) => [
'role' => 'admin',
'is_active' => true,
]);
}
/**
* Indicate that the email should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
}
π Using Factories
// Create a single user
$user = User::factory()->create();
// Create a user with specific attributes
$admin = User::factory()->admin()->create([
'email' => 'custom@example.com',
]);
// Create multiple users
$users = User::factory()->count(50)->create();
// Create a user with relationships
$user = User::factory()
->has(Post::factory()->count(5))
->has(Profile::factory())
->create();
// Create and persist to database
User::factory()->count(100)->create();
// Make without persisting (just returns model instances)
$userModels = User::factory()->count(5)->make();
// Create related models
$user = User::factory()
->hasPosts(3) // Creates 3 posts
->hasComments(5) // Creates 5 comments
->create();
// In seeder
public function run(): void
{
User::factory()
->count(50)
->has(Post::factory()->count(3))
->create();
}
DatabaseTransactions trait in tests.
3.4 Eloquent ORM Internals β Active Record Pattern Deep Dive
Eloquent is Laravel's implementation of the Active Record pattern. Each database table has a corresponding "Model" that interacts with that table.
π Basic Model Structure
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes; // Adds soft delete functionality
// Table name (optional - Laravel uses plural snake_case)
protected $table = 'blog_posts';
// Primary key (default: 'id')
protected $primaryKey = 'uuid';
// Key type (default: 'int')
protected $keyType = 'string';
// Disable auto-incrementing (for UUIDs)
public $incrementing = false;
// Timestamps (default: true)
public $timestamps = true;
// Date format for serialization
protected $dateFormat = 'Y-m-d H:i:s';
// Connection name for multiple databases
protected $connection = 'mysql';
// Mass assignable attributes
protected $fillable = [
'title', 'content', 'user_id', 'category_id', 'status'
];
// Attributes that should be hidden from JSON
protected $hidden = ['deleted_at'];
// Attributes that should be cast
protected $casts = [
'is_published' => 'boolean',
'published_at' => 'datetime',
'metadata' => 'array',
'views' => 'integer',
];
// Default attribute values
protected $attributes = [
'status' => 'draft',
'views' => 0,
];
// Model events
protected static function booted()
{
static::creating(function ($post) {
$post->uuid = (string) Str::uuid();
});
static::saving(function ($post) {
$post->slug = Str::slug($post->title);
});
}
}
Eloquent models fire several events during their lifecycle, allowing you to hook into various points:
| Event | Description | Use Case |
|---|---|---|
| retrieved | When a model is retrieved from database | Log access, modify data |
| creating | Before a model is first saved | Generate UUID, set defaults |
| created | After a model is first saved | Send notifications, create related records |
| updating | Before an existing model is saved | Validate changes, update timestamps |
| updated | After an existing model is saved | Clear cache, log changes |
| saving | Before a model is saved (create or update) | Generate slug, sanitize input |
| saved | After a model is saved (create or update) | Fire events, update search index |
| deleting | Before a model is deleted | Check permissions, backup data |
| deleted | After a model is deleted | Cleanup related data |
| restoring | Before a soft-deleted model is restored | Validate restoration |
| restored | After a soft-deleted model is restored | Restore related data |
π Event Listener Examples
// In model
protected static function booted()
{
// Using closures
static::created(function ($user) {
// Send welcome email
Mail::to($user->email)->send(new WelcomeMail($user));
// Create default settings
$user->settings()->create([]);
});
static::updated(function ($user) {
// Clear cache
Cache::forget('user_' . $user->id);
// Log changes
ActivityLog::create([
'user_id' => $user->id,
'action' => 'updated',
'changes' => $user->getChanges()
]);
});
}
// Using Observer class
// app/Observers/UserObserver.php
class UserObserver
{
public function created(User $user)
{
// Handle created event
}
public function updated(User $user)
{
// Handle updated event
}
}
// Register in AppServiceProvider
public function boot()
{
User::observe(UserObserver::class);
}
3.5 CRUD Operations, Soft Deletes & Timestamps
β Create Operations
// Method 1: Create new instance and save
$post = new Post();
$post->title = 'My First Post';
$post->content = 'This is the content';
$post->user_id = 1;
$post->save();
// Method 2: Create using attributes (mass assignment)
$post = Post::create([
'title' => 'My First Post',
'content' => 'This is the content',
'user_id' => 1,
]);
// Method 3: First or create (avoid duplicates)
$post = Post::firstOrCreate(
['title' => 'My First Post'], // attributes to find
['content' => 'Content', 'user_id' => 1] // additional attributes
);
// Method 4: Update or create
$post = Post::updateOrCreate(
['title' => 'My First Post'],
['content' => 'Updated content']
);
// Method 5: Insert multiple records (faster, no model events)
Post::insert([
['title' => 'Post 1', 'content' => '...', 'user_id' => 1],
['title' => 'Post 2', 'content' => '...', 'user_id' => 1],
['title' => 'Post 3', 'content' => '...', 'user_id' => 1],
]);
π Read Operations
// Find by primary key
$post = Post::find(1);
$post = Post::findOrFail(1); // Throws ModelNotFoundException
// Find by multiple IDs
$posts = Post::find([1, 2, 3]);
// First record
$post = Post::first();
$post = Post::firstOrFail(); // Throws exception if no records
// First or create (if no record exists)
$post = Post::firstOrCreate(
['title' => 'Specific Title'],
['content' => '...', 'user_id' => 1]
);
// First or new (creates model without saving)
$post = Post::firstOrNew(['title' => 'Title']);
// All records
$posts = Post::all();
// Where clauses
$posts = Post::where('user_id', 1)
->where('is_published', true)
->orderBy('created_at', 'desc')
->limit(10)
->get();
// Advanced where
$posts = Post::where('title', 'like', '%Laravel%')
->orWhere(function ($query) {
$query->where('views', '>', 100)
->where('is_published', true);
})
->get();
// Aggregates
$count = Post::where('user_id', 1)->count();
$avg = Post::avg('views');
$sum = Post::sum('views');
$max = Post::max('views');
βοΈ Update Operations
// Method 1: Find and update
$post = Post::find(1);
$post->title = 'Updated Title';
$post->save();
// Method 2: Mass update
Post::where('user_id', 1)
->update(['is_published' => true]);
// Method 3: Update using model
Post::find(1)->update(['title' => 'New Title']);
// Method 4: Update or create
$post = Post::updateOrCreate(
['title' => 'Unique Title'],
['content' => '...', 'user_id' => 1]
);
// Method 5: Increment/Decrement
Post::find(1)->increment('views');
Post::find(1)->increment('views', 5);
Post::find(1)->decrement('views');
Post::where('user_id', 1)->increment('views');
ποΈ Delete Operations
// Method 1: Find and delete
$post = Post::find(1);
$post->delete();
// Method 2: Delete by query
Post::where('is_published', false)->delete();
// Method 3: Destroy by ID
Post::destroy(1);
Post::destroy([1, 2, 3]);
// Method 4: Truncate table (removes all records and resets auto-increment)
Post::truncate();
π Soft Deletes β Safe Deletion
// In model
use Illuminate\Database\Eloquent\SoftDeletes;
class Post extends Model
{
use SoftDeletes;
protected $dates = ['deleted_at'];
}
// Soft delete a record
$post = Post::find(1);
$post->delete(); // Sets deleted_at timestamp, doesn't remove from DB
// Include soft deleted records in queries
$posts = Post::withTrashed()->get();
// Only soft deleted records
$deletedPosts = Post::onlyTrashed()->get();
// Restore soft deleted record
$post = Post::withTrashed()->find(1);
$post->restore();
// Force delete (permanently remove)
$post = Post::withTrashed()->find(1);
$post->forceDelete();
β° Custom Timestamps
class Post extends Model
{
// Custom timestamp field names
const CREATED_AT = 'created_at';
const UPDATED_AT = 'updated_at';
// Disable timestamps
public $timestamps = false;
// Custom timestamp format
protected $dateFormat = 'U'; // Unix timestamp
}
3.6 Eloquent Relationships β Complete Guide
// User.php - Each user has one profile
class User extends Model
{
public function profile()
{
return $this->hasOne(Profile::class);
}
// With custom foreign and local keys
public function profile()
{
return $this->hasOne(Profile::class, 'user_id', 'id');
}
}
// Profile.php - Each profile belongs to one user
class Profile extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
}
// Usage
$profile = User::find(1)->profile;
$user = Profile::find(1)->user;
// Create related record
$user = User::find(1);
$profile = $user->profile()->create([
'bio' => 'Developer',
'avatar' => 'avatar.jpg'
]);
// User.php - One user has many posts
class User extends Model
{
public function posts()
{
return $this->hasMany(Post::class);
}
// With custom keys
public function posts()
{
return $this->hasMany(Post::class, 'author_id', 'local_id');
}
}
// Post.php - Each post belongs to one user
class Post extends Model
{
public function user()
{
return $this->belongsTo(User::class);
}
// With custom keys
public function user()
{
return $this->belongsTo(User::class, 'author_id', 'local_id');
}
}
// Usage
$posts = User::find(1)->posts()->where('is_published', true)->get();
$user = Post::find(1)->user;
// Querying relationship existence
$usersWithPosts = User::has('posts')->get();
$usersWithAtLeast3Posts = User::has('posts', '>=', 3)->get();
$usersWithPublishedPosts = User::whereHas('posts', function ($query) {
$query->where('is_published', true);
})->get();
// Counting related records
$usersWithPostCount = User::withCount('posts')->get();
foreach ($usersWithPostCount as $user) {
echo $user->posts_count;
}
// Eager loading
$users = User::with('posts')->get();
foreach ($users as $user) {
foreach ($user->posts as $post) {
// No additional queries
}
}
// Nested eager loading
$users = User::with('posts.comments')->get();
// User.php - Users can have many roles
class User extends Model
{
public function roles()
{
return $this->belongsToMany(Role::class);
}
// With custom pivot table and keys
public function roles()
{
return $this->belongsToMany(
Role::class, // Related model
'user_roles', // Pivot table name
'user_id', // Foreign key on pivot
'role_id', // Related key on pivot
'id', // Local key on users
'id' // Local key on roles
)->withTimestamps(); // Add timestamps to pivot
}
// With pivot data
public function teams()
{
return $this->belongsToMany(Team::class)
->withPivot('role', 'joined_at')
->withTimestamps()
->wherePivot('active', true);
}
}
// Role.php
class Role extends Model
{
public function users()
{
return $this->belongsToMany(User::class);
}
}
// Create pivot table migration
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->string('assigned_by')->nullable();
$table->timestamps();
$table->unique(['user_id', 'role_id']);
});
// Usage
$user = User::find(1);
// Attach roles
$user->roles()->attach(1);
$user->roles()->attach([2, 3, 4]);
$user->roles()->attach(1, ['assigned_by' => 'admin']);
// Sync roles (removes old, adds new)
$user->roles()->sync([1, 2, 3]);
// Sync without detaching
$user->roles()->syncWithoutDetaching([4, 5]);
// Detach roles
$user->roles()->detach(1);
$user->roles()->detach(); // Detach all
// Toggle (attach if not exists, detach if exists)
$user->roles()->toggle([1, 2, 3]);
// Query through pivot
$admins = User::whereHas('roles', function ($query) {
$query->where('name', 'admin');
})->get();
// Access pivot data
foreach ($user->teams as $team) {
echo $team->pivot->role;
echo $team->pivot->joined_at;
}
// Get posts for all users in a country
class Country extends Model
{
public function posts()
{
return $this->hasManyThrough(
Post::class,
User::class,
'country_id', // Foreign key on users table
'user_id', // Foreign key on posts table
'id', // Local key on countries table
'id' // Local key on users table
);
}
}
// Usage
$country = Country::find(1);
$posts = $country->posts; // All posts from users in this country
// Get a user's latest order status through their orders
class User extends Model
{
public function latestOrderStatus()
{
return $this->hasOneThrough(
OrderStatus::class,
Order::class,
'user_id', // Foreign key on orders table
'order_id', // Foreign key on order_statuses table
'id', // Local key on users table
'id' // Local key on orders table
)->latest('orders.created_at');
}
}
3.7 Polymorphic Relationships β Flexible Model Associations
Polymorphic relationships allow a model to belong to multiple other models on a single association. Perfect for comments, tags, likes, etc.
// Migration for polymorphic table
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('content');
$table->morphs('commentable'); // Creates commentable_id and commentable_type
$table->timestamps();
});
// Models
class Post extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Video extends Model
{
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
}
class Comment extends Model
{
public function commentable()
{
return $this->morphTo();
}
}
// Usage
$post = Post::find(1);
$comment = $post->comments()->create(['content' => 'Great post!']);
$video = Video::find(1);
$video->comments()->create(['content' => 'Nice video!']);
$comment = Comment::find(1);
$commentable = $comment->commentable; // Returns either Post or Video
// Migration for polymorphic many-to-many
Schema::create('taggables', function (Blueprint $table) {
$table->id();
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->morphs('taggable');
$table->timestamps();
});
class Tag extends Model
{
public function posts()
{
return $this->morphedByMany(Post::class, 'taggable');
}
public function videos()
{
return $this->morphedByMany(Video::class, 'taggable');
}
}
class Post extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
class Video extends Model
{
public function tags()
{
return $this->morphToMany(Tag::class, 'taggable');
}
}
// Usage
$post = Post::find(1);
$post->tags()->attach([1, 2, 3]);
$tag = Tag::find(1);
$postsWithTag = $tag->posts; // All posts with this tag
3.8 Query Scopes & Repository Pattern
class Post extends Model
{
// Local scope
public function scopePublished($query)
{
return $query->where('is_published', true)
->where('published_at', '<=', now());
}
public function scopePopular($query, $threshold = 100)
{
return $query->where('views', '>=', $threshold);
}
public function scopeOfCategory($query, $category)
{
return $query->where('category_id', $category);
}
public function scopeSearch($query, $search)
{
return $query->where(function ($q) use ($search) {
$q->where('title', 'like', "%{$search}%")
->orWhere('content', 'like', "%{$search}%");
});
}
// Dynamic scope
public function scopeWhereStatus($query, $status)
{
return $query->where('status', $status);
}
}
// Usage
$publishedPosts = Post::published()->get();
$popularPosts = Post::popular(500)->get();
$recentPopular = Post::published()
->popular()
->latest()
->take(10)
->get();
$searchResults = Post::search('Laravel')
->published()
->ofCategory(5)
->get();
// app/Scopes/ActiveScope.php
namespace App\Scopes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class ActiveScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('is_active', true);
}
}
// Model
class User extends Model
{
protected static function booted()
{
static::addGlobalScope(new ActiveScope);
// Anonymous global scope
static::addGlobalScope('age', function (Builder $builder) {
$builder->where('age', '>=', 18);
});
}
// Remove global scope for a query
public function scopeWithoutActive($query)
{
return $query->withoutGlobalScope(ActiveScope::class);
}
}
// Usage
$users = User::all(); // Only active users
$allUsers = User::withoutGlobalScopes()->get();
$usersWithoutAge = User::withoutGlobalScope('age')->get();
// app/Contracts/PostRepositoryInterface.php
namespace App\Contracts;
interface PostRepositoryInterface
{
public function all();
public function find($id);
public function findBySlug($slug);
public function create(array $data);
public function update($id, array $data);
public function delete($id);
public function getPublished();
public function getByUser($userId);
public function search($query);
}
// app/Repositories/PostRepository.php
namespace App\Repositories;
use App\Models\Post;
use App\Contracts\PostRepositoryInterface;
class PostRepository implements PostRepositoryInterface
{
protected $model;
public function __construct(Post $model)
{
$this->model = $model;
}
public function all()
{
return $this->model->with('user', 'category')->latest()->get();
}
public function find($id)
{
return $this->model->findOrFail($id);
}
public function findBySlug($slug)
{
return $this->model->where('slug', $slug)->firstOrFail();
}
public function create(array $data)
{
return $this->model->create($data);
}
public function update($id, array $data)
{
$post = $this->find($id);
$post->update($data);
return $post;
}
public function delete($id)
{
return $this->find($id)->delete();
}
public function getPublished()
{
return $this->model->published()
->with('user')
->latest('published_at')
->get();
}
public function getByUser($userId)
{
return $this->model->where('user_id', $userId)
->with('category')
->latest()
->get();
}
public function search($query)
{
return $this->model->search($query)
->published()
->with('user')
->get();
}
}
// AppServiceProvider.php
public function register()
{
$this->app->bind(PostRepositoryInterface::class, PostRepository::class);
}
// Controller usage
class PostController extends Controller
{
protected $postRepository;
public function __construct(PostRepositoryInterface $postRepository)
{
$this->postRepository = $postRepository;
}
public function index()
{
$posts = $this->postRepository->getPublished();
return view('posts.index', compact('posts'));
}
public function show($slug)
{
$post = $this->postRepository->findBySlug($slug);
return view('posts.show', compact('post'));
}
}
3.9 Database Optimization & Indexing β Performance Mastery
Indexes are like a book's index β they help the database find data without scanning entire tables. Proper indexing can make queries 100x faster.
π Types of Indexes
- Primary Key Index: Automatically created, unique, clustered
- Unique Index: Ensures all values are unique
- Regular Index: Speeds up WHERE clauses and JOINs
- Composite Index: Index on multiple columns
- Full-Text Index: For text search optimization
// Migration with indexes
Schema::create('products', function (Blueprint $table) {
$table->id(); // Primary key index
// Unique index
$table->string('sku')->unique();
// Regular indexes
$table->index('category_id');
$table->index('status');
// Composite index
$table->index(['category_id', 'status']);
// Full-text index (MySQL only)
$table->fullText(['name', 'description']);
// Spatial index (for geometry columns)
$table->spatialIndex('coordinates');
});
// Adding indexes to existing table
Schema::table('products', function (Blueprint $table) {
$table->index('created_at'); // For date range queries
$table->index(['user_id', 'created_at']); // For user's recent items
});
// Dropping indexes
Schema::table('products', function (Blueprint $table) {
$table->dropIndex(['category_id']); // Drop regular index
$table->dropUnique(['sku']); // Drop unique index
});
1οΈβ£ N+1 Problem Prevention
// β Bad - N+1 queries
$users = User::all();
foreach ($users as $user) {
echo $user->profile->bio; // Queries profile for each user
}
// β
Good - Eager loading
$users = User::with('profile')->get();
foreach ($users as $user) {
echo $user->profile->bio; // No additional queries
}
// β
Best - Lazy eager loading when needed
$users = User::all();
if ($someCondition) {
$users->load('profile');
}
2οΈβ£ Selecting Only Needed Columns
// β Bad - Selects all columns
$users = User::all();
// β
Good - Select only needed columns
$users = User::select('id', 'name', 'email')->get();
// With relationships
$users = User::with('profile:id,user_id,bio')
->select('id', 'name')
->get();
3οΈβ£ Chunking Large Datasets
// Process 100,000 records without memory issues
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// Process user
}
});
// Chunk by ID for updates
User::where('active', true)
->chunkById(1000, function ($users) {
foreach ($users as $user) {
User::where('id', $user->id)
->update(['last_processed' => now()]);
}
});
4οΈβ£ Caching Expensive Queries
use Illuminate\Support\Facades\Cache;
// Cache query results
$users = Cache::remember('active_users', 3600, function () {
return User::with('profile')
->where('active', true)
->orderBy('name')
->get();
});
// Cache with tags
Cache::tags(['users', 'profiles'])->remember('users_list', 3600, function () {
return User::with('profile')->get();
});
// Invalidate cache when data changes
User::saved(function ($user) {
Cache::tags(['users'])->flush();
});
5οΈβ£ Database Query Logging
// Enable query log
\DB::enableQueryLog();
// Run your queries
$users = User::where('active', true)->get();
// Get executed queries
$queries = \DB::getQueryLog();
dd($queries);
// In development, use Laravel Debugbar
Tools for Database Optimization
- Laravel Debugbar: Real-time query monitoring
- Laravel Telescope: Production monitoring
- MySQL EXPLAIN: Analyze query execution
- Slow Query Log: Identify problematic queries
// Using EXPLAIN in Laravel
$explain = DB::select('EXPLAIN SELECT * FROM users WHERE email = ?', ['test@example.com']);
dd($explain);
// Log slow queries in MySQL
// Add to my.cnf:
// slow_query_log = 1
// long_query_time = 2
// log_queries_not_using_indexes = 1
You now have comprehensive knowledge of Laravel's database layer, from basic connections to advanced optimization techniques. You can design efficient database schemas, write optimized queries, leverage all Eloquent relationships, and implement enterprise-grade data patterns. This expertise is essential for senior Laravel developers.
π Module 03 : Database, Eloquent & Data Modeling Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Forms, Validation & Application Security β Enterprise-Grade Protection
Security is not an afterthought β it's a fundamental requirement for any production application. This comprehensive module from NotesTime.in covers every aspect of form handling, data validation, and application security in Laravel. Learn how to build secure forms, validate user input, prevent common vulnerabilities, and harden your Laravel applications against attacks. These patterns are used by Fortune 500 companies to protect sensitive data and maintain compliance with security standards.
4.1 Secure Form Handling β Best Practices & Patterns
A secure form involves multiple layers of protection: CSRF tokens, proper HTTP methods, input sanitization, validation, and secure transmission. Laravel provides built-in tools for each of these layers.
π Basic Secure Form Structure
<!-- resources/views/forms/secure-form.blade.php -->
<form method="POST" action="{{ route('form.submit') }}">
<!-- CSRF Protection - MANDATORY for all POST forms -->
@csrf
<!-- POST spoofing for PUT/PATCH/DELETE -->
@method('PUT')
<!-- Form fields with proper naming and old input -->
<div class="form-group">
<label for="name">Name</label>
<input type="text"
name="name"
id="name"
class="form-control @error('name') is-invalid @enderror"
value="{{ old('name') }}"
required
maxlength="255">
@error('name')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<!-- Honeypot field to catch bots -->
<div style="display: none;">
<input type="text" name="honeypot" value="">
</div>
<!-- Form timestamp to prevent replay attacks -->
<input type="hidden" name="form_timestamp" value="{{ time() }}">
<button type="submit" class="btn btn-primary">Submit</button>
</form>
π‘οΈ Form Request with Security Validation
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
class SecureFormRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Rate limiting check
$key = Str::lower($this->input('email')) . '|' . $this->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
return false; // Too many attempts
}
// Verify form timestamp (prevent replay attacks within 5 minutes)
$timestamp = $this->input('form_timestamp');
if (abs(time() - $timestamp) > 300) {
return false; // Form expired
}
// Honeypot check
if (!empty($this->input('honeypot'))) {
return false; // Bot detected
}
return true;
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255|regex:/^[a-zA-Z\s]+$/',
'email' => 'required|email|max:255|unique:users',
'password' => [
'required',
'string',
'min:12',
'max:100',
'confirmed',
'regex:/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{12,}$/',
],
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'name.regex' => 'Name may only contain letters and spaces.',
'password.regex' => 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.',
];
}
/**
* Handle a passed validation attempt.
*/
protected function passedValidation()
{
// Clear rate limiter on successful validation
$key = Str::lower($this->input('email')) . '|' . $this->ip();
RateLimiter::clear($key);
}
}
HTML forms only support GET and POST methods. Laravel provides method spoofing to use PUT, PATCH, and DELETE in forms.
<!-- Update form using PUT -->
<form action="{{ route('users.update', $user) }}" method="POST">
@csrf
@method('PUT')
<input type="text" name="name" value="{{ $user->name }}">
<button type="submit">Update</button>
</form>
<!-- Delete form using DELETE -->
<form action="{{ route('users.destroy', $user) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">Delete</button>
</form>
<!-- Alternative using Blade directives -->
@can('update', $user)
<a href="{{ route('users.edit', $user) }}">Edit</a>
@endcan
@can('delete', $user)
<form action="{{ route('users.destroy', $user) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
@endcan
File uploads are a critical attack vector. Always validate file types, sizes, and store files outside the webroot.
<!-- File upload form -->
<form action="{{ route('files.upload') }}" method="POST" enctype="multipart/form-data">
@csrf
<div class="form-group">
<label for="document">Upload Document</label>
<input type="file"
name="document"
id="document"
class="form-control @error('document') is-invalid @enderror"
accept=".pdf,.doc,.docx,.jpg,.png"
required>
@error('document')
<div class="invalid-feedback">{{ $message }}</div>
@enderror
</div>
<button type="submit">Upload</button>
</form>
<!-- File upload validation -->
public function upload(Request $request)
{
$request->validate([
'document' => [
'required',
'file',
'mimes:pdf,doc,docx,jpg,png', // Restrict file types
'max:10240', // Max 10MB
'dimensions:min_width=100,min_height=100,max_width=2000,max_height=2000', // For images
],
]);
// Store file securely
$path = $request->file('document')->store(
'uploads/' . date('Y/m/d'),
's3' // Store on private S3 bucket
);
// Generate secure filename
$filename = Str::random(40) . '.' . $request->file('document')->getClientOriginalExtension();
// Scan for malware (integrate with ClamAV)
if (!$this->scanFile($request->file('document'))) {
return back()->with('error', 'File failed security scan.');
}
// Store file with random name
$path = $request->file('document')->storeAs(
'uploads',
$filename,
'private'
);
return back()->with('success', 'File uploaded securely.');
}
4.2 Validation Rules β Complete Reference & Custom Rules
Laravel provides over 100 built-in validation rules. Here's a comprehensive categorized reference:
π€ String Rules
string | Must be a string |
min:8 | Minimum length |
max:255 | Maximum length |
email | Valid email format |
url | Valid URL |
active_url | Valid DNS record |
alpha | Alphabetic characters only |
alpha_num | Alpha-numeric only |
alpha_dash | Alpha-numeric, dashes, underscores |
json | Valid JSON string |
ip | Valid IP address |
ipv4 | Valid IPv4 |
ipv6 | Valid IPv6 |
mac_address | Valid MAC address |
π’ Numeric Rules
numeric | Must be numeric |
integer | Must be integer |
digits:5 | Exactly 5 digits |
digits_between:4,8 | Between 4-8 digits |
min:0 | Minimum value |
max:100 | Maximum value |
between:1,10 | Between values |
gt:0 | Greater than |
lt:100 | Less than |
π Date & Time Rules
date | Valid date |
date_format:Y-m-d | Specific format |
after:today | After given date |
before:tomorrow | Before given date |
after_or_equal | After or equal |
before_or_equal | Before or equal |
timezone | Valid timezone |
πΎ Database Rules
unique:users,email | Unique in table |
exists:users,id | Exists in table |
unique_with:table,field1,field2 | Composite unique |
π§ Advanced Validation Examples
$rules = [
// Conditional validation
'payment_method' => 'required|in:credit_card,paypal,bank_transfer',
'card_number' => 'required_if:payment_method,credit_card|credit_card',
'paypal_email' => 'required_if:payment_method,paypal|email',
// Array validation
'products' => 'required|array|min:1|max:10',
'products.*.id' => 'required|exists:products,id',
'products.*.quantity' => 'required|integer|min:1|max:100',
// Password confirmation
'password' => 'required|confirmed|min:12',
// Custom validation using rules
'username' => [
'required',
'string',
'min:3',
'max:50',
'regex:/^[a-zA-Z0-9_]+$/', // Alphanumeric + underscore
'not_regex:/admin|root/i', // Exclude certain words
'unique:users,username',
],
// File validation
'avatar' => [
'required',
'image',
'mimes:jpeg,png,gif',
'max:2048',
'dimensions:min_width=100,min_height=100,max_width=500,max_height=500',
],
];
Method 1: Closure-Based Rules
use Illuminate\Support\Facades\Validator;
$validator = Validator::make($request->all(), [
'coupon_code' => [
'required',
'string',
function ($attribute, $value, $fail) {
// Check if coupon exists and is valid
$coupon = Coupon::where('code', $value)
->where('expires_at', '>', now())
->where('usage_count', '<', 'max_usage')
->first();
if (!$coupon) {
$fail('The selected coupon code is invalid or expired.');
}
// Check if user already used this coupon
if ($coupon->users()->where('user_id', auth()->id())->exists()) {
$fail('You have already used this coupon.');
}
},
],
]);
Method 2: Custom Rule Class
// Create rule using Artisan
// php artisan make:rule StrongPassword
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class StrongPassword implements Rule
{
protected $minLength;
protected $requireNumbers;
protected $requireSpecialChars;
public function __construct($minLength = 12, $requireNumbers = true, $requireSpecialChars = true)
{
$this->minLength = $minLength;
$this->requireNumbers = $requireNumbers;
$this->requireSpecialChars = $requireSpecialChars;
}
/**
* Determine if the validation rule passes.
*/
public function passes($attribute, $value): bool
{
// Check minimum length
if (strlen($value) < $this->minLength) {
return false;
}
// Check for uppercase
if (!preg_match('/[A-Z]/', $value)) {
return false;
}
// Check for lowercase
if (!preg_match('/[a-z]/', $value)) {
return false;
}
// Check for numbers
if ($this->requireNumbers && !preg_match('/\d/', $value)) {
return false;
}
// Check for special characters
if ($this->requireSpecialChars && !preg_match('/[@$!%*#?&]/', $value)) {
return false;
}
// Check for common patterns
$common = ['password', '123456', 'qwerty'];
foreach ($common as $pattern) {
if (stripos($value, $pattern) !== false) {
return false;
}
}
return true;
}
/**
* Get the validation error message.
*/
public function message(): string
{
return "The :attribute must be at least {$this->minLength} characters and contain at least one uppercase letter, one lowercase letter" .
($this->requireNumbers ? ", one number" : "") .
($this->requireSpecialChars ? ", and one special character" : "") . ".";
}
}
// Usage
$request->validate([
'password' => ['required', new StrongPassword(12, true, true)],
]);
Method 3: Rule Objects (Laravel 10+)
use Illuminate\Validation\Rules\Password;
$request->validate([
'password' => [
'required',
Password::min(12)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised(), // Check if password has been compromised in data leaks
],
'email' => [
'required',
'email',
new \App\Rules\CustomEmailRule(),
],
]);
$validator = Validator::make($request->all(), [
'user_type' => 'required|in:individual,company',
'first_name' => 'required_if:user_type,individual|string|max:255',
'last_name' => 'required_if:user_type,individual|string|max:255',
'company_name' => 'required_if:user_type,company|string|max:255',
'tax_id' => 'required_if:user_type,company|string|size:9',
'age' => 'required|integer|min:18|max:120',
'terms' => 'accepted', // Must be checked
'g-recaptcha-response' => 'required|captcha', // Google reCAPTCHA
])->after(function ($validator) use ($request) {
// Custom validation after main validation
if ($request->age < 18 && $request->has('parental_consent')) {
$validator->errors()->add('age', 'Minors must have parental consent.');
}
// Business logic validation
if ($this->isBlacklisted($request->email)) {
$validator->errors()->add('email', 'This email is blacklisted.');
}
});
4.3 Form Request Classes β Clean Validation & Authorization
Form Request classes encapsulate validation logic, authorization, and sometimes post-validation processing. They keep controllers clean and DRY.
// php artisan make:request StoreUserRequest
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rule;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
// Check if user has permission to create users
return Auth::user()->can('create', User::class);
}
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => [
'required',
'email',
Rule::unique('users')->where(function ($query) {
return $query->where('account_id', $this->account_id);
}),
],
'password' => [
'required',
'confirmed',
Password::min(12)
->mixedCase()
->numbers()
->symbols()
->uncompromised(),
],
'role_id' => 'required|exists:roles,id',
'account_id' => 'required|exists:accounts,id',
'notify_user' => 'boolean',
];
}
/**
* Get custom messages for validator errors.
*/
public function messages(): array
{
return [
'email.unique' => 'A user with this email already exists in your account.',
'password.uncompromised' => 'This password has been exposed in a data breach. Please choose a different password.',
];
}
/**
* Prepare the data for validation.
*/
protected function prepareForValidation(): void
{
// Merge default values
$this->merge([
'account_id' => $this->account_id ?? Auth::user()->account_id,
'created_by' => Auth::id(),
]);
}
/**
* Handle a passed validation attempt.
*/
protected function passedValidation(): void
{
// Hash password after validation
$this->replace([
'password' => Hash::make($this->password),
]);
}
/**
* Get custom attributes for validator errors.
*/
public function attributes(): array
{
return [
'email' => 'email address',
'role_id' => 'role',
];
}
}
π Update Request with Different Rules
namespace App\Http\Requests;
class UpdateUserRequest extends FormRequest
{
public function authorize(): bool
{
return Auth::user()->can('update', $this->user);
}
public function rules(): array
{
return [
'name' => 'sometimes|string|max:255',
'email' => [
'sometimes',
'email',
Rule::unique('users')->ignore($this->user->id),
],
'password' => [
'sometimes',
'confirmed',
Password::min(12)->mixedCase()->numbers()->symbols(),
],
'role_id' => 'sometimes|exists:roles,id',
];
}
/**
* Configure the validator instance.
*/
public function withValidator($validator): void
{
$validator->after(function ($validator) {
// Check if user is trying to change their own role without permission
if ($this->user->id == Auth::id() && $this->has('role_id')) {
$validator->errors()->add('role_id', 'You cannot change your own role.');
}
});
}
}
π’ Using Form Requests in Controllers
namespace App\Http\Controllers;
use App\Http\Requests\StoreUserRequest;
use App\Http\Requests\UpdateUserRequest;
use App\Models\User;
class UserController extends Controller
{
public function store(StoreUserRequest $request)
{
// Request is already validated and authorized
$user = User::create($request->validated());
// Access validated data only
$validated = $request->validated();
// Access specific input
$name = $request->input('name');
return redirect()->route('users.show', $user)
->with('success', 'User created successfully.');
}
public function update(UpdateUserRequest $request, User $user)
{
// Update only validated fields
$user->update($request->validated());
return redirect()->route('users.show', $user)
->with('success', 'User updated successfully.');
}
}
4.4 CSRF, XSS & SQL Injection β Comprehensive Protection
CSRF attacks trick authenticated users into submitting malicious requests. Laravel provides automatic CSRF protection for all state-changing requests.
π How Laravel CSRF Works
// In VerifyCsrfToken middleware (app/Http/Middleware/VerifyCsrfToken.php)
protected $addHttpCookie = true;
protected $except = [
'stripe/*', // Exclude webhook endpoints
'api/*', // API routes typically use tokens instead
];
// Token generation in session
// Every session gets a unique CSRF token stored in session
// The token is regenerated on login/logout
// In forms, always include:
// Blade directive that generates:
<input type="hidden" name="_token" value="dtqTktE4CvvwKmtYnFPHukHhNX4cWqr8FU4dAmPM">
// For AJAX requests, include token in headers
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
// In meta tags
<meta name="csrf-token" content="dtqTktE4CvvwKmtYnFPHukHhNX4cWqr8FU4dAmPM">
// Token verification happens automatically
// VerifyCsrfToken middleware checks the token on POST, PUT, PATCH, DELETE
π« Excluding Routes from CSRF
// app/Http/Middleware/VerifyCsrfToken.php
class VerifyCsrfToken extends Middleware
{
protected $except = [
'webhook/*', // External services can't provide CSRF token
'api/*', // API routes use token authentication
'payment/callback', // Payment provider callbacks
];
}
XSS attacks inject malicious scripts into web pages viewed by other users. Laravel provides multiple layers of XSS protection.
π‘οΈ Blade Auto-Escaping
<!-- Auto-escaped (safe by default) -->
{{ $userInput }} <!-- Converts <script> to <script> -->
<!-- Unescaped output (dangerous - use with caution) -->
{!! $trustedHtml !!} <!-- Only use with trusted content -->
<!-- JSON in scripts (auto-escaped) -->
<script>
var user = @json($user);
// Converts to JSON and escapes properly
</script>
<!-- HTML attributes (auto-escaped) -->
<div data-user="{{ $user->name }}">Safe</div>
π Input Sanitization
use Illuminate\Support\Str;
use HTMLPurifier;
// Basic sanitization
$clean = strip_tags($input); // Remove HTML tags
$clean = htmlspecialchars($input, ENT_QUOTES, 'UTF-8'); // Encode special chars
// Laravel's e() helper (equivalent to htmlspecialchars)
$clean = e($input);
// Using HTML Purifier for trusted HTML input
composer require mews/purifier
// config/purifier.php
return [
'settings' => [
'default' => [
'HTML.Allowed' => 'p,br,strong,em,a[href],ul,ol,li',
'URI.AllowedSchemes' => ['http' => true, 'https' => true],
],
],
];
// In controller
$cleanHtml = Purifier::clean($request->content);
π‘οΈ Content Security Policy (CSP)
// middleware/ContentSecurityPolicy.php
public function handle($request, Closure $next)
{
$response = $next($request);
$response->header('Content-Security-Policy',
"default-src 'self'; " .
"script-src 'self' 'unsafe-inline' https://trusted-cdn.com; " .
"style-src 'self' 'unsafe-inline'; " .
"img-src 'self' data: https:; " .
"font-src 'self'; " .
"frame-ancestors 'none'; " .
"base-uri 'self'; " .
"form-action 'self'"
);
return $response;
}
SQL injection attacks execute malicious SQL queries through user input. Laravel's Eloquent ORM and query builder use parameter binding to prevent injection.
β Safe Practices (Using Parameter Binding)
// Eloquent - SAFE
User::where('email', $request->email)->first();
// Query Builder - SAFE (uses ? placeholders)
DB::table('users')
->where('email', $request->email)
->where('active', true)
->get();
// Raw queries with parameter binding - SAFE
DB::select('SELECT * FROM users WHERE email = ?', [$request->email]);
// Named bindings - SAFE
DB::select('SELECT * FROM users WHERE email = :email', [
'email' => $request->email
]);
β Dangerous Practices β NEVER DO THIS
// β DANGEROUS - String concatenation
DB::select("SELECT * FROM users WHERE email = '{$request->email}'");
// β DANGEROUS - Raw SQL without binding
DB::statement("DELETE FROM users WHERE id = " . $request->id);
// β DANGEROUS - Using DB::raw unsafely
User::whereRaw("email = '" . $request->email . "'")->get();
// β DANGEROUS - Table/column names from user input
$users = DB::table($request->tableName)->get(); // Never do this!
π Safe Dynamic Table/Column Names
// Whitelist approach for dynamic table names
$allowedTables = ['users', 'posts', 'comments'];
if (in_array($request->table, $allowedTables)) {
$results = DB::table($request->table)->get();
}
// Whitelist for column names
$allowedColumns = ['name', 'email', 'created_at'];
$column = in_array($request->sort, $allowedColumns) ? $request->sort : 'id';
User::orderBy($column, $request->direction ?? 'asc')->get();
4.5 Encryption, Hashing & Secure Storage
Use encryption for data that needs to be decrypted later (PII, payment details, API keys). Laravel uses OpenSSL with AES-256 encryption.
βοΈ Configuration
// .env - Set your encryption key (must be 32 characters base64 encoded)
APP_KEY=base64:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// config/app.php
'cipher' => 'AES-256-CBC', // Default encryption cipher
π Basic Encryption Usage
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Encrypt;
// Encrypt data
$encrypted = Crypt::encryptString($sensitiveData);
// Returns: eyJpdiI6Imx...
// Decrypt data
try {
$decrypted = Crypt::decryptString($encrypted);
} catch (Illuminate\Contracts\Encryption\DecryptException $e) {
// Handle decryption failure
}
// Encrypt arrays/objects (automatically serialized)
$encryptedArray = Crypt::encrypt(['user_id' => 1, 'role' => 'admin']);
$decryptedArray = Crypt::decrypt($encryptedArray);
πΎ Eloquent Encryption (Automatic)
// In model
class User extends Model
{
protected $encryptable = [
'credit_card_number',
'ssn',
'api_key',
];
// Or using casts
protected $casts = [
'payment_details' => 'encrypted', // Laravel 7+
'secret_notes' => 'encrypted:array',
];
}
// Usage - automatically encrypts when saving, decrypts when accessing
$user = User::find(1);
$user->credit_card_number = '4111111111111111'; // Auto-encrypted
$user->save();
echo $user->credit_card_number; // Auto-decrypted
// Querying encrypted fields - cannot use WHERE clauses directly
// Instead, search by other criteria
π Database Encryption at Rest
// Migration for encrypted column
Schema::table('users', function (Blueprint $table) {
// Encrypted data needs TEXT or LONGTEXT
$table->text('encrypted_data')->nullable();
// For indexed search, store a hash for lookups
$table->string('email_hash')->index();
});
// Save both encrypted and hashed versions
$user->encrypted_email = Crypt::encryptString($request->email);
$user->email_hash = hash('sha256', $request->email);
$user->save();
// Search by hash (can't search encrypted field directly)
$user = User::where('email_hash', hash('sha256', $searchEmail))->first();
Use hashing for data that never needs to be decrypted (passwords, tokens). Laravel uses Bcrypt by default with Argon2 as an option.
β‘ Available Hash Drivers
- Bcrypt: Default, widely compatible, configurable cost
- Argon2i: More resistant to GPU cracking (PHP 7.2+)
- Argon2id: Hybrid version, best for password hashing (PHP 7.3+)
π§ Configuration
// config/hashing.php
return [
'driver' => 'bcrypt', // bcrypt, argon2i, argon2id
'bcrypt' => [
'rounds' => 12, // Cost factor (higher = slower but more secure)
],
'argon' => [
'memory' => 1024, // Memory cost (KB)
'threads' => 2, // Thread count
'time' => 2, // Time cost
],
];
π Hashing Examples
use Illuminate\Support\Facades\Hash;
// Hash a password
$hashedPassword = Hash::make('user-password');
// Check password
if (Hash::check('user-input-password', $user->password)) {
// Password matches
}
// Check if password needs rehash (for upgrading security)
if (Hash::needsRehash($user->password)) {
$user->password = Hash::make('new-password');
$user->save();
}
// Using Bcrypt directly
$hash = password_hash('password', PASSWORD_BCRYPT, ['cost' => 12]);
// Using Argon2
$hash = password_hash('password', PASSWORD_ARGON2ID);
π Password Rehashing Middleware
// app/Http/Middleware/RehashPassword.php
public function handle($request, Closure $next)
{
if (Hash::needsRehash($request->user()->password)) {
$request->user()->password = Hash::make($request->user()->password);
$request->user()->save();
}
return $next($request);
}
// In Kernel.php
protected $routeMiddleware = [
'rehash' => \App\Http\Middleware\RehashPassword::class,
];
// On routes that need password verification
Route::post('/settings/password', function (Request $request) {
// Update password
})->middleware(['auth', 'rehash']);
// config/filesystems.php
'disks' => [
'private' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'visibility' => 'private',
],
'secure_uploads' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_SECURE_BUCKET'),
'visibility' => 'private', // Not publicly accessible
'encryption' => 'AES256', // Server-side encryption
],
];
// Store file securely
$path = $request->file('document')->store('documents', 'private');
// Generate temporary URL for secure access (S3 only)
$url = Storage::disk('s3')->temporaryUrl(
'documents/'.$filename,
now()->addMinutes(5)
);
// Serve private files through controller
Route::get('/secure-files/{path}', function ($path) {
$user = auth()->user();
// Check permissions
if (!$user->can('view', $path)) {
abort(403);
}
return Storage::disk('private')->download($path);
})->middleware('auth');
4.6 Security Headers & Laravel Hardening β Production-Ready Security
Security headers tell browsers how to behave when handling your application. They provide protection against XSS, clickjacking, MIME sniffing, and more.
// app/Http/Middleware/SecurityHeaders.php
namespace App\Http\Middleware;
use Closure;
class SecurityHeaders
{
private $unwantedHeaders = [
'X-Powered-By',
'Server',
];
public function handle($request, Closure $next)
{
$response = $next($request);
// Remove sensitive headers
foreach ($this->unwantedHeaders as $header) {
header_remove($header);
}
// Add security headers
$response->headers->set('X-Frame-Options', 'SAMEORIGIN'); // Prevent clickjacking
$response->headers->set('X-Content-Type-Options', 'nosniff'); // Prevent MIME sniffing
$response->headers->set('X-XSS-Protection', '1; mode=block'); // Enable XSS filter
// Strict Transport Security (force HTTPS)
$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
// Referrer Policy
$response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin');
// Content Security Policy (customize based on your needs)
$response->headers->set('Content-Security-Policy',
"default-src 'self'; " .
"script-src 'self' https://trusted-cdn.com; " .
"style-src 'self' 'unsafe-inline'; " .
"img-src 'self' data: https:; " .
"font-src 'self'; " .
"connect-src 'self' https://api.example.com; " .
"frame-ancestors 'none';"
);
// Feature Policy / Permissions Policy
$response->headers->set('Permissions-Policy',
'geolocation=(), microphone=(), camera=(), payment=()'
);
return $response;
}
}
// Register in Kernel.php
protected $middlewareGroups = [
'web' => [
// ... other middleware
\App\Http\Middleware\SecurityHeaders::class,
],
];
π File & Directory Security
- β
Set proper permissions:
chmod -R 755 storage bootstrap/cache - β
Move
.envoutside public webroot - β Disable directory browsing in server config
- β
Keep
storageandbootstrap/cachewritable
π Authentication Hardening
- β Enable 2FA for admin accounts
- β Implement account lockout after failed attempts
- β Force password change on first login
- β Log all authentication attempts
βοΈ Configuration Hardening
// config/app.php
'debug' => env('APP_DEBUG', false), // Always false in production
'url' => env('APP_URL', 'https://yourdomain.com'), // Use HTTPS
// config/session.php
'driver' => env('SESSION_DRIVER', 'redis'), // Use Redis for better security
'secure' => true, // Only send cookies over HTTPS
'http_only' => true, // Prevent JavaScript access to cookies
'same_site' => 'strict', // Prevent CSRF from external sites
// config/cors.php
'paths' => ['api/*'], // Only enable CORS for API routes
'allowed_methods' => ['GET', 'POST', 'PUT', 'DELETE'],
'allowed_origins' => [env('FRONTEND_URL', 'https://app.example.com')],
'supports_credentials' => true,
// config/database.php
// Use SSL for database connections in production
'mysql' => [
// ...
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT => true,
]) : [],
];
π Security Monitoring & Logging
// config/logging.php
'channels' => [
'security' => [
'driver' => 'daily',
'path' => storage_path('logs/security.log'),
'level' => 'info',
'days' => 30,
],
];
// Log security events
use Illuminate\Support\Facades\Log;
class SecurityLogger
{
public static function log($event, $data = [])
{
Log::channel('security')->info($event, array_merge([
'user_id' => auth()->id(),
'ip' => request()->ip(),
'user_agent' => request()->userAgent(),
'timestamp' => now(),
], $data));
}
}
// Usage
SecurityLogger::log('failed_login', ['email' => $request->email]);
π¨ Rate Limiting & Brute Force Protection
// app/Http/Kernel.php
protected $middlewareGroups = [
'api' => [
'throttle:60,1', // 60 requests per minute
// ...
],
];
// Custom rate limiting for login
Route::post('/login', function (Request $request) {
$key = 'login:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return response()->json([
'error' => "Too many attempts. Try again in {$seconds} seconds."
], 429);
}
// Attempt login
if (Auth::attempt($request->only('email', 'password'))) {
RateLimiter::clear($key);
return redirect()->intended();
}
RateLimiter::hit($key, 60); // Lock for 60 seconds after 5 attempts
return back()->withErrors(['email' => 'Invalid credentials.']);
})->middleware('guest');
Recommended Security Packages
{
"require-dev": {
"laravel/horizon": "^5.0", // Queue monitoring
"spatie/laravel-csp": "^2.0", // Content Security Policy
"spatie/laravel-cookie-consent": "^3.0", // GDPR cookie consent
"spatie/laravel-activitylog": "^4.0", // User activity logging
"owen-it/laravel-auditing": "^13.0", // Model auditing
"pragmarx/google2fa": "^8.0", // 2FA implementation
"darkaonline/l5-swagger": "^8.0" // API documentation
}
}
// Activity Log example
use Spatie\Activitylog\Traits\LogsActivity;
class User extends Model
{
use LogsActivity;
protected static $logAttributes = ['name', 'email', 'role'];
protected static $logOnlyDirty = true;
protected static $logName = 'user';
public function getDescriptionForEvent(string $eventName): string
{
return "User was {$eventName}";
}
}
π‘οΈ Security Checklist
- β Keep Laravel and PHP updated
- β Use HTTPS exclusively (force redirect)
- β Implement proper authentication with rate limiting
- β Validate all user input
- β Use parameter binding or Eloquent
- β Escape output in Blade (default)
- β Set secure cookie flags (HttpOnly, Secure, SameSite)
- β Encrypt sensitive data at rest
- β Implement proper file upload validation
- β Regular security audits and dependency updates
- β Monitor logs for suspicious activity
- β Have an incident response plan
You now have comprehensive knowledge of Laravel's security features and best practices. You can build applications that resist common attacks, protect sensitive data, and meet enterprise security standards. This expertise is essential for any professional Laravel developer working on production applications.
π Module 04 : Forms, Validation & Application Security Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Authentication, Authorization & Access Control β Complete Implementation Guide
Authentication and authorization are the gatekeepers of your application. This comprehensive module provides step-by-step code examples and implementation guides for every authentication scenario: from basic login to multi-guard systems, API tokens, OAuth2, and complex RBAC. All code is production-ready and follows Laravel best practices.
5.1 Laravel Auth Architecture β Complete Code Implementation
# Install Laravel Breeze (simplest authentication scaffold)
composer require laravel/breeze --dev
# Install Breeze with Blade (choose one)
php artisan breeze:install blade
# OR with React
php artisan breeze:install react
# OR with Vue
php artisan breeze:install vue
# OR with API only
php artisan breeze:install api
# Install and build frontend dependencies
npm install
npm run dev
# Run migrations
php artisan migrate
# Test the authentication
php artisan serve
# Visit: http://localhost:8000/register
# Visit: http://localhost:8000/login
# Visit: http://localhost:8000/dashboard
// config/auth.php - Complete Auth Configuration
return [
'defaults' => [
'guard' => 'web', // Default guard for web requests
'passwords' => 'users', // Default password broker
],
// Available authentication guards
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token', // API token driver (simple)
'provider' => 'users',
'hash' => false,
],
'sanctum' => [ // Laravel Sanctum (SPA/mobile)
'driver' => 'sanctum',
'provider' => 'users',
],
],
// User providers (how to retrieve users)
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// Using database query builder instead of Eloquent
'users_db' => [
'driver' => 'database',
'table' => 'users',
],
],
// Password reset configuration
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
],
// Password confirmation timeout
'password_timeout' => 10800,
];
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'name',
'email',
'password',
'is_active',
'email_verified_at',
'last_login_at',
'last_login_ip',
];
/**
* The attributes that should be hidden for serialization.
*/
protected $hidden = [
'password',
'remember_token',
'two_factor_secret',
'two_factor_recovery_codes',
];
/**
* The attributes that should be cast.
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'is_active' => 'boolean',
'last_login_at' => 'datetime',
'settings' => 'array',
];
/**
* Update last login information
*/
public function updateLastLogin(): void
{
$this->last_login_at = now();
$this->last_login_ip = request()->ip();
$this->save();
}
/**
* Check if user is active
*/
public function isActive(): bool
{
return $this->is_active && !is_null($this->email_verified_at);
}
/**
* Send password reset notification
*/
public function sendPasswordResetNotification($token): void
{
$url = url(route('password.reset', [
'token' => $token,
'email' => $this->email,
], false));
$this->notify(new \App\Notifications\CustomResetPassword($url));
}
}
// app/Http/Controllers/Auth/LoginController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
/**
* Show login form
*/
public function showLoginForm()
{
return view('auth.login');
}
/**
* Handle login request
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
// Attempt to log in with credentials
if (Auth::attempt([
'email' => $request->email,
'password' => $request->password,
'is_active' => true, // Only allow active users
], $request->boolean('remember'))) {
$request->session()->regenerate();
// Update last login info
Auth::user()->updateLastLogin();
// Log successful login
activity()->log('User logged in successfully');
// Redirect intended or default
return redirect()->intended(route('dashboard'));
}
// If login fails, increment rate limiter
RateLimiter::hit($this->throttleKey($request));
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
/**
* Get the rate limiting throttle key
*/
protected function throttleKey(Request $request): string
{
return strtolower($request->input('email')) . '|' . $request->ip();
}
/**
* Handle logout
*/
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
}
5.2 Session-Based Authentication β Complete Implementation
// config/session.php
return [
// Use secure, encrypted cookies
'driver' => env('SESSION_DRIVER', 'database'),
// Cookie name
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
// How long to remember sessions (in minutes)
'lifetime' => env('SESSION_LIFETIME', 120),
// Expire on browser close
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
// Encrypt session data
'encrypt' => false,
// Session file location (file driver)
'files' => storage_path('framework/sessions'),
// Database connection for session table
'connection' => env('SESSION_CONNECTION'),
// Session table name (database driver)
'table' => 'sessions',
// Cache store (cache driver)
'store' => env('SESSION_STORE'),
// Lottery for garbage collection
'lottery' => [2, 100],
// Cookie path
'path' => '/',
// Cookie domain
'domain' => env('SESSION_DOMAIN'),
// Secure cookies (HTTPS only)
'secure' => env('SESSION_SECURE_COOKIE'),
// HTTP only (no JavaScript access)
'http_only' => true,
// Same-site policy
'same_site' => 'lax',
// Partitioned cookies
'partitioned' => false,
];
# Create session migration
php artisan session:table
# Run migration
php artisan migrate
// database/migrations/xxxx_xx_xx_xxxxxx_create_sessions_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('sessions', function (Blueprint $table) {
$table->string('id')->primary();
$table->foreignId('user_id')->nullable()->index();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->longText('payload');
$table->integer('last_activity')->index();
});
}
public function down()
{
Schema::dropIfExists('sessions');
}
};
// routes/web.php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
Route::middleware('guest')->group(function () {
Route::get('register', [RegisteredUserController::class, 'create'])
->name('register');
Route::post('register', [RegisteredUserController::class, 'store']);
Route::get('login', [AuthenticatedSessionController::class, 'create'])
->name('login');
Route::post('login', [AuthenticatedSessionController::class, 'store']);
Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
->name('password.request');
Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
->name('password.email');
Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
->name('password.reset');
Route::post('reset-password', [NewPasswordController::class, 'store'])
->name('password.store');
});
Route::middleware('auth')->group(function () {
Route::get('verify-email', EmailVerificationPromptController::class)
->name('verification.notice');
Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
->middleware(['signed', 'throttle:6,1'])
->name('verification.verify');
Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
->middleware('throttle:6,1')
->name('verification.send');
Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
->name('password.confirm');
Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
Route::put('password', [PasswordController::class, 'update'])
->name('password.update');
Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
->name('logout');
});
// app/Http/Kernel.php
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
// app/Http/Middleware/Authenticate.php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
// Store intended URL before redirect
session()->put('url.intended', url()->current());
return route('login');
}
}
}
// app/Http/Middleware/RedirectIfAuthenticated.php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
public function handle(Request $request, Closure $next, string ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
// If user is already logged in, redirect to dashboard
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
5.3 Multi-Guard Authentication β Complete Implementation
// config/auth.php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
'api' => [
'driver' => 'sanctum',
'provider' => 'users',
],
'admin-api' => [
'driver' => 'sanctum',
'provider' => 'admins',
],
'vendor' => [
'driver' => 'session',
'provider' => 'vendors',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
'vendors' => [
'driver' => 'eloquent',
'model' => App\Models\Vendor::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'admin_password_resets',
'expire' => 60,
'throttle' => 60,
],
'vendors' => [
'provider' => 'vendors',
'table' => 'vendor_password_resets',
'expire' => 60,
'throttle' => 60,
],
],
];
// app/Models/Admin.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class Admin extends Authenticatable
{
use HasApiTokens, Notifiable;
protected $fillable = [
'name',
'email',
'password',
'role', // super_admin, moderator, editor
'permissions',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'permissions' => 'array',
'password' => 'hashed',
];
/**
* Check if admin has specific permission
*/
public function hasPermission(string $permission): bool
{
if ($this->role === 'super_admin') {
return true;
}
return in_array($permission, $this->permissions ?? []);
}
/**
* Check if admin has role
*/
public function hasRole(string $role): bool
{
return $this->role === $role;
}
}
// app/Http/Controllers/Admin/Auth/LoginController.php
namespace App\Http\Controllers\Admin\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
public function __construct()
{
// Use admin guard for this controller
$this->middleware('guest:admin')->except('logout');
}
/**
* Show admin login form
*/
public function showLoginForm()
{
return view('admin.auth.login');
}
/**
* Handle admin login
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string',
]);
// Attempt login with admin guard
if (Auth::guard('admin')->attempt([
'email' => $request->email,
'password' => $request->password,
], $request->boolean('remember'))) {
$request->session()->regenerate();
// Log admin login
activity('admin')->log('Admin logged in');
return redirect()->intended(route('admin.dashboard'));
}
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
/**
* Handle admin logout
*/
public function logout(Request $request)
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('admin.login');
}
}
// routes/admin.php
use App\Http\Controllers\Admin\Auth\LoginController;
use App\Http\Controllers\Admin\DashboardController;
use App\Http\Controllers\Admin\UserController;
Route::prefix('admin')->name('admin.')->group(function () {
// Guest routes (not logged in)
Route::middleware('guest:admin')->group(function () {
Route::get('login', [LoginController::class, 'showLoginForm'])
->name('login');
Route::post('login', [LoginController::class, 'login']);
});
// Authenticated admin routes
Route::middleware('auth:admin')->group(function () {
Route::post('logout', [LoginController::class, 'logout'])
->name('logout');
Route::get('dashboard', [DashboardController::class, 'index'])
->name('dashboard');
// Admin-only user management
Route::resource('users', UserController::class)
->middleware('can:manage-users');
});
});
// app/Http/Kernel.php - Add custom middleware
protected $routeMiddleware = [
// ... other middleware
'auth.admin' => \App\Http\Middleware\AuthenticateAdmin::class,
'guest.admin' => \App\Http\Middleware\RedirectIfAuthenticatedAdmin::class,
];
// app/Http/Middleware/AuthenticateAdmin.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class AuthenticateAdmin
{
public function handle($request, Closure $next)
{
if (!Auth::guard('admin')->check()) {
return redirect()->route('admin.login');
}
return $next($request);
}
}
// app/Http/Controllers/DashboardController.php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
class DashboardController extends Controller
{
/**
* Show appropriate dashboard based on guard
*/
public function index()
{
// Check which guard is authenticated
if (Auth::guard('admin')->check()) {
return redirect()->route('admin.dashboard');
}
if (Auth::guard('vendor')->check()) {
return redirect()->route('vendor.dashboard');
}
if (Auth::guard('web')->check()) {
return view('user.dashboard');
}
return redirect()->route('login');
}
/**
* Get current authenticated user across guards
*/
public function getCurrentUser()
{
$user = Auth::user(); // web guard
$admin = Auth::guard('admin')->user();
$vendor = Auth::guard('vendor')->user();
return response()->json([
'user' => $user,
'admin' => $admin,
'vendor' => $vendor,
]);
}
/**
* Switch between guards programmatically
*/
public function switchToAdmin()
{
if (Auth::guard('admin')->check()) {
Auth::shouldUse('admin');
return Auth::user(); // Returns admin user
}
}
}
5.4 Authorization β Gates & Policies Complete Implementation
// app/Providers/AuthServiceProvider.php
namespace App\Providers;
use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
$this->registerPolicies();
// Define gates using closures
Gate::define('view-dashboard', function (User $user) {
return $user->is_admin || $user->hasPermission('view-dashboard');
});
// Gate with multiple conditions
Gate::define('update-post', function (User $user, Post $post) {
return $user->id === $post->user_id || $user->is_admin;
});
// Gate with custom response
Gate::define('delete-post', function (User $user, Post $post) {
if ($user->is_admin) {
return true;
}
if ($user->id === $post->user_id) {
return $post->created_at->diffInHours(now()) < 24;
}
return false;
});
// Gate using before callback (super admin)
Gate::before(function (User $user, $ability) {
if ($user->is_super_admin) {
return true;
}
});
// Gate after callback
Gate::after(function (User $user, $ability, $result) {
// Log authorization attempts
activity()->log("User {$user->id} attempted {$ability}: " . ($result ? 'allowed' : 'denied'));
});
// Dynamic gates
Gate::define('manage-resource', function (User $user, $resourceType) {
return $user->hasPermission("manage-{$resourceType}");
});
}
}
// Using Gates in Controllers
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Gate;
class PostController extends Controller
{
public function index()
{
// Check gate before showing all posts
if (Gate::denies('view-dashboard')) {
abort(403, 'Unauthorized access.');
}
$posts = Post::all();
return view('posts.index', compact('posts'));
}
public function edit(Post $post)
{
// Check gate with model
if (Gate::allows('update-post', $post)) {
return view('posts.edit', compact('post'));
}
abort(403);
}
public function update(Post $post)
{
// Authorize or fail (throws AuthorizationException)
$this->authorize('update-post', $post);
// Update post...
}
public function delete(Post $post)
{
// Check gate with custom response
$response = Gate::inspect('delete-post', $post);
if ($response->allowed()) {
$post->delete();
return redirect()->route('posts.index');
} else {
return back()->with('error', $response->message());
}
}
}
// Using Gates in Blade Views
@can('view-dashboard')
<a href="{{ route('dashboard') }}">Dashboard</a>
@endcan
@cannot('update-post', $post)
<p>You cannot edit this post</p>
@endcannot
@can('delete-post', $post)
<form action="{{ route('posts.destroy', $post) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
@else
<button disabled>Delete (Not Authorized)</button>
@endcan
<!-- Check multiple abilities -->
@canany(['update-post', 'delete-post'], $post)
<div>You can manage this post</div>
@endcanany
# Generate a policy
php artisan make:policy PostPolicy --model=Post
# Generate policy with all CRUD methods
php artisan make:policy PostPolicy --model=Post --resource
// app/Policies/PostPolicy.php
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
class PostPolicy
{
use HandlesAuthorization;
/**
* Determine if user can view any posts (index)
*/
public function viewAny(User $user): bool
{
return $user->hasPermission('view-posts');
}
/**
* Determine if user can view a specific post
*/
public function view(User $user, Post $post): bool
{
return $user->id === $post->user_id
|| $user->hasPermission('view-any-post');
}
/**
* Determine if user can create posts
*/
public function create(User $user): bool
{
return $user->hasVerifiedEmail()
&& !$user->is_banned;
}
/**
* Determine if user can update a post
*/
public function update(User $user, Post $post): Response
{
if ($user->id !== $post->user_id) {
return Response::deny('You do not own this post.');
}
if ($post->is_locked) {
return Response::deny('This post is locked and cannot be edited.');
}
return Response::allow();
}
/**
* Determine if user can delete a post
*/
public function delete(User $user, Post $post): bool
{
return $user->id === $post->user_id
|| $user->hasRole('admin');
}
/**
* Determine if user can restore a soft-deleted post
*/
public function restore(User $user, Post $post): bool
{
return $user->hasRole('admin');
}
/**
* Determine if user can permanently delete a post
*/
public function forceDelete(User $user, Post $post): bool
{
return $user->hasRole('super-admin');
}
/**
* Determine if user can publish a post
*/
public function publish(User $user, Post $post): bool
{
return $user->hasRole('editor') || $user->hasPermission('publish-posts');
}
/**
* Before method - runs before all policy methods
*/
public function before(User $user, $ability)
{
if ($user->is_super_admin) {
return true;
}
}
}
// Register Policies in AuthServiceProvider
// app/Providers/AuthServiceProvider.php
use App\Models\Post;
use App\Policies\PostPolicy;
use App\Models\Comment;
use App\Policies\CommentPolicy;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Post::class => PostPolicy::class,
Comment::class => CommentPolicy::class,
User::class => UserPolicy::class,
];
public function boot()
{
$this->registerPolicies();
// Additional gates...
}
}
// Using Policies in Controllers
namespace App\Http\Controllers;
use App\Models\Post;
use App\Models\User;
class PostController extends Controller
{
public function index()
{
// Authorize with policy
$this->authorize('viewAny', Post::class);
$posts = Post::all();
return view('posts.index', compact('posts'));
}
public function show(Post $post)
{
// Authorize specific post
$this->authorize('view', $post);
return view('posts.show', compact('post'));
}
public function store(Request $request)
{
$this->authorize('create', Post::class);
// Create post...
}
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$post->update($request->all());
return redirect()->route('posts.show', $post);
}
public function destroy(Post $post)
{
$this->authorize('delete', $post);
$post->delete();
return redirect()->route('posts.index');
}
public function publish(Post $post)
{
$this->authorize('publish', $post);
$post->publish();
return response()->json(['message' => 'Post published']);
}
}
// Using Policies with User model
// app/Policies/UserPolicy.php
namespace App\Policies;
use App\Models\User;
class UserPolicy
{
public function viewAny(User $user): bool
{
return $user->is_admin;
}
public function view(User $authenticatedUser, User $targetUser): bool
{
return $authenticatedUser->id === $targetUser->id
|| $authenticatedUser->is_admin;
}
public function update(User $authenticatedUser, User $targetUser): bool
{
return $authenticatedUser->id === $targetUser->id
|| $authenticatedUser->hasRole('admin');
}
public function delete(User $authenticatedUser, User $targetUser): bool
{
return $authenticatedUser->is_admin
&& $authenticatedUser->id !== $targetUser->id;
}
public function ban(User $user, User $targetUser): bool
{
return $user->is_admin && !$targetUser->is_admin;
}
}
5.5 RBAC β Complete Role-Based Access Control Implementation
# Create migrations
php artisan make:migration create_roles_table
php artisan make:migration create_permissions_table
php artisan make:migration create_role_user_table
php artisan make:migration create_permission_role_table
// database/migrations/xxxx_xx_xx_create_roles_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('guard_name')->default('web');
$table->boolean('is_system')->default(false); // System roles cannot be deleted
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('roles');
}
};
// database/migrations/xxxx_xx_xx_create_permissions_table.php
Schema::create('permissions', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('group')->nullable(); // Group permissions by module
$table->string('guard_name')->default('web');
$table->timestamps();
});
// database/migrations/xxxx_xx_xx_create_role_user_table.php
Schema::create('role_user', function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['role_id', 'user_id']);
});
// database/migrations/xxxx_xx_xx_create_permission_role_table.php
Schema::create('permission_role', function (Blueprint $table) {
$table->id();
$table->foreignId('permission_id')->constrained()->onDelete('cascade');
$table->foreignId('role_id')->constrained()->onDelete('cascade');
$table->timestamps();
$table->unique(['permission_id', 'role_id']);
});
// app/Models/Role.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Role extends Model
{
protected $fillable = [
'name', 'slug', 'description', 'guard_name', 'is_system'
];
protected $casts = [
'is_system' => 'boolean',
];
/**
* Users that have this role
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->withTimestamps();
}
/**
* Permissions assigned to this role
*/
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class)->withTimestamps();
}
/**
* Check if role has a specific permission
*/
public function hasPermission(string $permissionSlug): bool
{
return $this->permissions()
->where('slug', $permissionSlug)
->exists();
}
/**
* Assign permission to role
*/
public function givePermissionTo($permission): static
{
if (is_string($permission)) {
$permission = Permission::where('slug', $permission)->firstOrFail();
}
$this->permissions()->syncWithoutDetaching($permission);
return $this;
}
/**
* Remove permission from role
*/
public function revokePermissionTo($permission): static
{
if (is_string($permission)) {
$permission = Permission::where('slug', $permission)->firstOrFail();
}
$this->permissions()->detach($permission);
return $this;
}
/**
* Sync permissions for role
*/
public function syncPermissions(array $permissions): static
{
$permissionIds = Permission::whereIn('slug', $permissions)->pluck('id');
$this->permissions()->sync($permissionIds);
return $this;
}
}
// app/Models/Permission.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Permission extends Model
{
protected $fillable = [
'name', 'slug', 'description', 'group', 'guard_name'
];
/**
* Roles that have this permission
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)->withTimestamps();
}
}
// app/Models/User.php (with RBAC methods)
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class User extends Authenticatable
{
// ... existing code ...
/**
* Roles assigned to user
*/
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class)->withTimestamps();
}
/**
* Get all permissions through roles
*/
public function permissions(): BelongsToMany
{
return $this->roles()->with('permissions');
}
/**
* Check if user has a specific role
*/
public function hasRole(string $roleSlug): bool
{
return $this->roles()->where('slug', $roleSlug)->exists();
}
/**
* Check if user has any of the given roles
*/
public function hasAnyRole(array $roleSlugs): bool
{
return $this->roles()->whereIn('slug', $roleSlugs)->exists();
}
/**
* Check if user has all given roles
*/
public function hasAllRoles(array $roleSlugs): bool
{
$userRoleSlugs = $this->roles()->pluck('slug')->toArray();
return empty(array_diff($roleSlugs, $userRoleSlugs));
}
/**
* Check if user has a specific permission
*/
public function hasPermission(string $permissionSlug): bool
{
// Super admin bypass
if ($this->hasRole('super-admin')) {
return true;
}
foreach ($this->roles as $role) {
if ($role->hasPermission($permissionSlug)) {
return true;
}
}
return false;
}
/**
* Check if user has any of the given permissions
*/
public function hasAnyPermission(array $permissionSlugs): bool
{
if ($this->hasRole('super-admin')) {
return true;
}
foreach ($permissionSlugs as $permissionSlug) {
if ($this->hasPermission($permissionSlug)) {
return true;
}
}
return false;
}
/**
* Check if user has all given permissions
*/
public function hasAllPermissions(array $permissionSlugs): bool
{
if ($this->hasRole('super-admin')) {
return true;
}
foreach ($permissionSlugs as $permissionSlug) {
if (!$this->hasPermission($permissionSlug)) {
return false;
}
}
return true;
}
/**
* Assign role to user
*/
public function assignRole($role): static
{
if (is_string($role)) {
$role = Role::where('slug', $role)->firstOrFail();
}
$this->roles()->syncWithoutDetaching($role);
return $this;
}
/**
* Remove role from user
*/
public function removeRole($role): static
{
if (is_string($role)) {
$role = Role::where('slug', $role)->firstOrFail();
}
$this->roles()->detach($role);
return $this;
}
/**
* Sync roles for user
*/
public function syncRoles(array $roles): static
{
$roleIds = Role::whereIn('slug', $roles)->pluck('id');
$this->roles()->sync($roleIds);
return $this;
}
}
// database/seeders/RbacSeeder.php
namespace Database\Seeders;
use App\Models\Role;
use App\Models\Permission;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class RbacSeeder extends Seeder
{
public function run()
{
// Create permissions grouped by module
$permissions = [
'users' => [
'view-users',
'create-users',
'edit-users',
'delete-users',
],
'posts' => [
'view-posts',
'create-posts',
'edit-posts',
'delete-posts',
'publish-posts',
],
'roles' => [
'view-roles',
'create-roles',
'edit-roles',
'delete-roles',
],
'settings' => [
'view-settings',
'edit-settings',
],
'dashboard' => [
'view-dashboard',
],
];
// Create permissions
foreach ($permissions as $group => $groupPermissions) {
foreach ($groupPermissions as $permission) {
Permission::create([
'name' => ucfirst(str_replace('-', ' ', $permission)),
'slug' => $permission,
'group' => $group,
'guard_name' => 'web',
]);
}
}
// Create roles
$superAdmin = Role::create([
'name' => 'Super Admin',
'slug' => 'super-admin',
'description' => 'Has unrestricted access to all system features',
'is_system' => true,
]);
$admin = Role::create([
'name' => 'Admin',
'slug' => 'admin',
'description' => 'Has administrative access with some restrictions',
]);
$editor = Role::create([
'name' => 'Editor',
'slug' => 'editor',
'description' => 'Can manage content but not system settings',
]);
$user = Role::create([
'name' => 'User',
'slug' => 'user',
'description' => 'Regular application user',
]);
// Assign permissions to roles
// Admin gets all permissions except system-critical ones
$admin->syncPermissions([
'view-users', 'create-users', 'edit-users',
'view-posts', 'create-posts', 'edit-posts', 'delete-posts', 'publish-posts',
'view-roles',
'view-settings', 'edit-settings',
'view-dashboard',
]);
// Editor gets content-related permissions
$editor->syncPermissions([
'view-posts', 'create-posts', 'edit-posts', 'publish-posts',
'view-dashboard',
]);
// Regular user gets minimal permissions
$user->syncPermissions([
'view-posts',
]);
// Create super admin user
$superAdminUser = User::create([
'name' => 'Super Admin',
'email' => 'superadmin@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$superAdminUser->assignRole('super-admin');
// Create admin user
$adminUser = User::create([
'name' => 'Admin User',
'email' => 'admin@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$adminUser->assignRole('admin');
// Create editor user
$editorUser = User::create([
'name' => 'Editor User',
'email' => 'editor@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$editorUser->assignRole('editor');
// Create regular user
$regularUser = User::create([
'name' => 'Regular User',
'email' => 'user@example.com',
'password' => Hash::make('password'),
'email_verified_at' => now(),
]);
$regularUser->assignRole('user');
}
}
// app/Http/Middleware/CheckRole.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckRole
{
public function handle(Request $request, Closure $next, ...$roles)
{
if (!Auth::check()) {
return redirect()->route('login');
}
$user = Auth::user();
foreach ($roles as $role) {
if ($user->hasRole($role)) {
return $next($request);
}
}
abort(403, 'Unauthorized. Required role: ' . implode(', ', $roles));
}
}
// app/Http/Middleware/CheckPermission.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class CheckPermission
{
public function handle(Request $request, Closure $next, $permission)
{
if (!Auth::check()) {
return redirect()->route('login');
}
if (!Auth::user()->hasPermission($permission)) {
abort(403, 'Unauthorized. Required permission: ' . $permission);
}
return $next($request);
}
}
// Register in Kernel.php
protected $routeMiddleware = [
// ... other middleware
'role' => \App\Http\Middleware\CheckRole::class,
'permission' => \App\Http\Middleware\CheckPermission::class,
];
// Using RBAC in Routes
// routes/web.php
// Routes accessible only by users with specific role
Route::middleware(['auth', 'role:admin'])->group(function () {
Route::get('/admin/users', [UserController::class, 'index'])->name('admin.users');
Route::get('/admin/settings', [SettingController::class, 'index'])->name('admin.settings');
});
// Multiple roles allowed
Route::middleware(['auth', 'role:admin,editor'])->group(function () {
Route::get('/posts/create', [PostController::class, 'create'])->name('posts.create');
Route::post('/posts', [PostController::class, 'store'])->name('posts.store');
});
// Permission-based routes
Route::middleware(['auth', 'permission:edit-posts'])->group(function () {
Route::get('/posts/{post}/edit', [PostController::class, 'edit'])->name('posts.edit');
Route::put('/posts/{post}', [PostController::class, 'update'])->name('posts.update');
});
// Combined role and permission
Route::get('/admin/reports', [ReportController::class, 'index'])
->middleware(['auth', 'role:admin', 'permission:view-reports']);
// Using RBAC in Blade Views
@role('admin')
<!-- Admin only content -->
<a href="{{ route('admin.settings') }}">Settings</a>
@endrole
@role('admin|editor')
<!-- Content for admins and editors -->
<a href="{{ route('posts.create') }}">Create Post</a>
@endrole
@hasrole('super-admin')
<!-- Only super admin can see this -->
<button onclick="deleteSystem()">Delete System</button>
@endhasrole
@permission('edit-posts')
<!-- Show edit button if user has permission -->
<a href="{{ route('posts.edit', $post) }}">Edit</a>
@endpermission
@can('view-dashboard')
<!-- Using Laravel's can directive with RBAC -->
<a href="{{ route('dashboard') }}">Dashboard</a>
@endcan
5.6 Laravel Sanctum vs Passport β Complete Implementation
# Install Sanctum
composer require laravel/sanctum
# Publish Sanctum migration and config
php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider"
# Run migrations
php artisan migrate
# Add Sanctum trait to User model
// app/Models/User.php
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
// ...
}
// config/sanctum.php
return [
// Domain for SPA authentication
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
env('APP_URL') ? ','.parse_url(env('APP_URL'), PHP_URL_HOST) : ''
))),
// Token expiration (in minutes)
'expiration' => null,
// Token prefix
'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
// Sanctum middleware
'middleware' => [
'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
],
];
// API Routes with Sanctum
// routes/api.php
use App\Http\Controllers\Api\AuthController;
use App\Http\Controllers\Api\PostController;
// Public API routes
Route::post('/register', [AuthController::class, 'register']);
Route::post('/login', [AuthController::class, 'login']);
// Protected API routes (require token)
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', [AuthController::class, 'logout']);
Route::get('/user', [AuthController::class, 'user']);
Route::apiResource('posts', PostController::class);
// Token management
Route::get('/tokens', [AuthController::class, 'tokens']);
Route::delete('/tokens/{id}', [AuthController::class, 'revokeToken']);
});
// app/Http/Controllers/Api/AuthController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\ValidationException;
class AuthController extends Controller
{
/**
* Register new user
*/
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
// Create token for new user
$token = $user->createToken('auth_token')->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
], 201);
}
/**
* Login user and create token
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Revoke old tokens (optional - limit to 5 active tokens)
if ($user->tokens()->count() >= 5) {
$user->tokens()->oldest()->first()->delete();
}
// Create token with abilities
$token = $user->createToken('auth_token', ['post:create', 'post:read'])->plainTextToken;
return response()->json([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
]);
}
/**
* Logout user (revoke token)
*/
public function logout(Request $request)
{
// Revoke current token
$request->user()->currentAccessToken()->delete();
return response()->json([
'message' => 'Logged out successfully'
]);
}
/**
* Get authenticated user
*/
public function user(Request $request)
{
return response()->json($request->user());
}
/**
* List all user tokens
*/
public function tokens(Request $request)
{
$tokens = $request->user()->tokens()->get()->map(function ($token) {
return [
'id' => $token->id,
'name' => $token->name,
'last_used' => $token->last_used_at,
'created_at' => $token->created_at,
];
});
return response()->json($tokens);
}
/**
* Revoke specific token
*/
public function revokeToken(Request $request, $id)
{
$token = $request->user()->tokens()->find($id);
if (!$token) {
return response()->json(['message' => 'Token not found'], 404);
}
$token->delete();
return response()->json(['message' => 'Token revoked successfully']);
}
/**
* Create token with specific abilities
*/
public function createApiToken(Request $request)
{
$request->validate([
'token_name' => 'required|string',
'abilities' => 'array',
]);
$abilities = $request->abilities ?? ['*'];
$token = $request->user()->createToken(
$request->token_name,
$abilities
);
return response()->json([
'token' => $token->plainTextToken,
'abilities' => $abilities,
]);
}
}
// SPA Authentication with Sanctum
// routes/web.php (for SPA)
Route::get('/spa/login', [SpaAuthController::class, 'showLogin'])->name('spa.login');
Route::get('/spa/dashboard', [SpaAuthController::class, 'dashboard'])->middleware('auth:sanctum');
// JavaScript SPA Example (Vue/React)
// api-client.js
import axios from 'axios';
const api = axios.create({
baseURL: 'http://localhost:8000/api',
withCredentials: true, // Required for Sanctum SPA authentication
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
// Add token to requests
api.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Login function
async function login(email, password) {
// First, get CSRF cookie
await axios.get('http://localhost:8000/sanctum/csrf-cookie');
// Then login
const response = await api.post('/login', { email, password });
if (response.data.token) {
localStorage.setItem('token', response.data.token);
}
return response.data;
}
// Register function
async function register(userData) {
const response = await api.post('/register', userData);
if (response.data.token) {
localStorage.setItem('token', response.data.token);
}
return response.data;
}
// Logout function
async function logout() {
await api.post('/logout');
localStorage.removeItem('token');
}
// Get authenticated user
async function getUser() {
const response = await api.get('/user');
return response.data;
}
# Install Passport
composer require laravel/passport
# Run migrations
php artisan migrate
# Install Passport (creates encryption keys)
php artisan passport:install
# Add Passport trait to User model
// app/Models/User.php
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
// ...
}
// app/Providers/AuthServiceProvider.php
use Laravel\Passport\Passport;
public function boot()
{
$this->registerPolicies();
// Set token expiration
Passport::tokensExpireIn(now()->addDays(15));
Passport::refreshTokensExpireIn(now()->addDays(30));
Passport::personalAccessTokensExpireIn(now()->addMonths(6));
// Enable implicit grant (optional)
Passport::enableImplicitGrant();
// Define token abilities (scopes)
Passport::tokensCan([
'view-profile' => 'View user profile',
'edit-profile' => 'Edit user profile',
'manage-posts' => 'Create, edit, delete posts',
'manage-users' => 'Manage other users (admin only)',
]);
// Set default scope
Passport::setDefaultScope([
'view-profile',
]);
}
// config/auth.php - Configure Passport guard
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
// routes/api.php - Passport OAuth Routes
Route::post('/oauth/token', [
'uses' => '\Laravel\Passport\Http\Controllers\AccessTokenController@issueToken',
'middleware' => 'throttle',
])->name('passport.token');
// Protected API routes with Passport
Route::middleware('auth:api')->group(function () {
Route::get('/user', function (Request $request) {
return $request->user();
});
Route::apiResource('posts', PostController::class);
});
// Creating OAuth Clients
// app/Console/Commands/CreateOAuthClient.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Laravel\Passport\ClientRepository;
class CreateOAuthClient extends Command
{
protected $signature = 'oauth:create-client {name} {--redirect=} {--confidential}';
protected $description = 'Create OAuth client';
public function handle(ClientRepository $clients)
{
$name = $this->argument('name');
$redirect = $this->option('redirect') ?? url('/oauth/callback');
$confidential = $this->option('confidential') ?? false;
$client = $clients->create(
null, // User ID (null for personal access clients)
$name,
$redirect,
null, // Provider
$confidential
);
$this->info('Client created successfully:');
$this->table(
['ID', 'Secret', 'Name', 'Redirect'],
[[$client->id, $client->secret, $client->name, $client->redirect]]
);
}
}
// Password Grant Tokens
use Illuminate\Http\Request;
use Laravel\Passport\Client;
public function getPasswordToken(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
$client = Client::where('password_client', true)->first();
if (!$client) {
return response()->json(['error' => 'Password client not found'], 500);
}
$http = new \GuzzleHttp\Client;
try {
$response = $http->post(url('/oauth/token'), [
'form_params' => [
'grant_type' => 'password',
'client_id' => $client->id,
'client_secret' => $client->secret,
'username' => $request->email,
'password' => $request->password,
'scope' => '*',
],
]);
return json_decode((string) $response->getBody(), true);
} catch (\Exception $e) {
return response()->json([
'error' => 'Invalid credentials',
], 401);
}
}
// Creating Personal Access Tokens
use App\Models\User;
$user = User::find(1);
// Create token with specific scopes
$token = $user->createToken('Personal Access Token', ['view-profile', 'edit-profile']);
// Get the token string
$tokenString = $token->accessToken;
// Validate token with scopes
if ($user->tokenCan('edit-profile')) {
// User can edit profile
}
| Feature | Sanctum | Passport | Recommendation |
|---|---|---|---|
| SPA Authentication | β Excellent (cookie-based) | β Not designed for SPAs | Use Sanctum for SPAs |
| Mobile App Authentication | β Yes (token-based) | β Yes | Both work well |
| API Tokens | β Simple tokens | β Full OAuth2 tokens | Sanctum for simple, Passport for complex |
| OAuth2 Support | β No | β Full OAuth2 server | Passport for OAuth2 |
| Token Scopes | β Yes (simple) | β Yes (comprehensive) | Both support scopes |
| Complexity | β Low | ββββ High | Start with Sanctum |
| Third-party API access | β No | β Yes | Passport for third-party clients |
- Use Sanctum for: SPAs, mobile apps, simple API tokens, first-party clients
- Use Passport for: OAuth2 server, third-party API access, multiple client types, complex token requirements
5.7 OAuth2, JWT & API Tokens β Complete Implementation
# Install JWT Auth
composer require tymon/jwt-auth
# Publish config
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
# Generate JWT secret
php artisan jwt:secret
// config/jwt.php
return [
'secret' => env('JWT_SECRET'),
'keys' => [
'public' => env('JWT_PUBLIC_KEY'),
'private' => env('JWT_PRIVATE_KEY'),
'passphrase' => env('JWT_PASSPHRASE'),
],
'ttl' => env('JWT_TTL', 60), // minutes
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160), // minutes (14 days)
'algo' => env('JWT_ALGO', 'HS256'),
'required_claims' => ['iss', 'iat', 'exp', 'nbf', 'sub', 'jti'],
'persistent_claims' => [],
'lock_subject' => true,
'leeway' => env('JWT_LEEWAY', 0),
'blacklist_enabled' => env('JWT_BLACKLIST_ENABLED', true),
'blacklist_grace_period' => env('JWT_BLACKLIST_GRACE_PERIOD', 0),
'decrypt_cookies' => false,
'providers' => [
'user' => 'Tymon\JWTAuth\Providers\User\EloquentUserAdapter',
'jwt' => 'Tymon\JWTAuth\Providers\JWT\Lcobucci',
'auth' => 'Tymon\JWTAuth\Providers\Auth\Illuminate',
'storage' => 'Tymon\JWTAuth\Providers\Storage\Illuminate',
],
];
// app/Models/User.php
use Tymon\JWTAuth\Contracts\JWTSubject;
class User extends Authenticatable implements JWTSubject
{
// ... existing code ...
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*/
public function getJWTCustomClaims()
{
return [
'role' => $this->role,
'permissions' => $this->getAllPermissions(),
];
}
}
// app/Http/Controllers/Api/JwtAuthController.php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Tymon\JWTAuth\Facades\JWTAuth;
use Tymon\JWTAuth\Exceptions\JWTException;
class JwtAuthController extends Controller
{
/**
* Register a new user
*/
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
$token = JWTAuth::fromUser($user);
return response()->json([
'success' => true,
'user' => $user,
'token' => $token,
'token_type' => 'bearer',
'expires_in' => config('jwt.ttl') * 60,
], 201);
}
/**
* Login and get JWT token
*/
public function login(Request $request)
{
$credentials = $request->only('email', 'password');
$validator = Validator::make($credentials, [
'email' => 'required|email',
'password' => 'required|string',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
try {
if (!$token = JWTAuth::attempt($credentials)) {
return response()->json([
'success' => false,
'message' => 'Invalid credentials',
], 401);
}
} catch (JWTException $e) {
return response()->json([
'success' => false,
'message' => 'Could not create token',
], 500);
}
return $this->respondWithToken($token);
}
/**
* Get authenticated user
*/
public function me()
{
try {
$user = JWTAuth::parseToken()->authenticate();
if (!$user) {
return response()->json([
'success' => false,
'message' => 'User not found',
], 404);
}
} catch (JWTException $e) {
return response()->json([
'success' => false,
'message' => 'Invalid token',
], 400);
}
return response()->json([
'success' => true,
'user' => $user,
]);
}
/**
* Logout (invalidate token)
*/
public function logout()
{
try {
JWTAuth::parseToken()->invalidate();
return response()->json([
'success' => true,
'message' => 'Successfully logged out',
]);
} catch (JWTException $e) {
return response()->json([
'success' => false,
'message' => 'Failed to logout',
], 500);
}
}
/**
* Refresh a token
*/
public function refresh()
{
try {
$newToken = JWTAuth::parseToken()->refresh();
return $this->respondWithToken($newToken);
} catch (JWTException $e) {
return response()->json([
'success' => false,
'message' => 'Token refresh failed',
], 401);
}
}
/**
* Response with token
*/
protected function respondWithToken($token)
{
return response()->json([
'success' => true,
'token' => $token,
'token_type' => 'bearer',
'expires_in' => config('jwt.ttl') * 60,
]);
}
}
// routes/api.php - JWT Routes
Route::prefix('auth')->group(function () {
Route::post('register', [JwtAuthController::class, 'register']);
Route::post('login', [JwtAuthController::class, 'login']);
Route::post('refresh', [JwtAuthController::class, 'refresh']);
Route::middleware('auth:api')->group(function () {
Route::get('me', [JwtAuthController::class, 'me']);
Route::post('logout', [JwtAuthController::class, 'logout']);
});
});
# Install Laravel Socialite
composer require laravel/socialite
// config/services.php
return [
// ... other services ...
'github' => [
'client_id' => env('GITHUB_CLIENT_ID'),
'client_secret' => env('GITHUB_CLIENT_SECRET'),
'redirect' => env('GITHUB_REDIRECT_URI'),
],
'google' => [
'client_id' => env('GOOGLE_CLIENT_ID'),
'client_secret' => env('GOOGLE_CLIENT_SECRET'),
'redirect' => env('GOOGLE_REDIRECT_URI'),
],
'facebook' => [
'client_id' => env('FACEBOOK_CLIENT_ID'),
'client_secret' => env('FACEBOOK_CLIENT_SECRET'),
'redirect' => env('FACEBOOK_REDIRECT_URI'),
],
'twitter' => [
'client_id' => env('TWITTER_CLIENT_ID'),
'client_secret' => env('TWITTER_CLIENT_SECRET'),
'redirect' => env('TWITTER_REDIRECT_URI'),
],
];
// app/Http/Controllers/Auth/SocialAuthController.php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
use Laravel\Socialite\Facades\Socialite;
class SocialAuthController extends Controller
{
/**
* Redirect to provider
*/
public function redirectToProvider($provider)
{
return Socialite::driver($provider)->redirect();
}
/**
* Handle provider callback
*/
public function handleProviderCallback($provider)
{
try {
$socialUser = Socialite::driver($provider)->user();
} catch (\Exception $e) {
return redirect('/login')->with('error', 'Authentication failed');
}
// Check if user exists with this email
$user = User::where('email', $socialUser->getEmail())->first();
if (!$user) {
// Create new user
$user = User::create([
'name' => $socialUser->getName() ?? $socialUser->getNickname(),
'email' => $socialUser->getEmail(),
'password' => Hash::make(Str::random(24)),
'email_verified_at' => now(),
'provider' => $provider,
'provider_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar(),
]);
} else {
// Update provider info for existing user
$user->update([
'provider' => $provider,
'provider_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar(),
]);
}
// Log the user in
Auth::login($user, true);
return redirect()->intended('/dashboard');
}
/**
* API version for social login
*/
public function handleProviderCallbackApi($provider)
{
try {
$socialUser = Socialite::driver($provider)->stateless()->user();
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => 'Authentication failed',
], 401);
}
$user = User::where('email', $socialUser->getEmail())->first();
if (!$user) {
$user = User::create([
'name' => $socialUser->getName() ?? $socialUser->getNickname(),
'email' => $socialUser->getEmail(),
'password' => Hash::make(Str::random(24)),
'email_verified_at' => now(),
'provider' => $provider,
'provider_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar(),
]);
}
// Generate token for API
$token = $user->createToken('social-login')->plainTextToken;
return response()->json([
'success' => true,
'user' => $user,
'token' => $token,
]);
}
}
// routes/web.php - Social Login Routes
Route::get('auth/{provider}', [SocialAuthController::class, 'redirectToProvider'])
->name('social.redirect');
Route::get('auth/{provider}/callback', [SocialAuthController::class, 'handleProviderCallback'])
->name('social.callback');
// routes/api.php - Social Login API Routes
Route::get('auth/{provider}/callback', [SocialAuthController::class, 'handleProviderCallbackApi']);
<!-- Blade View with Social Login Buttons -->
<div class="social-login-buttons">
<a href="{{ route('social.redirect', 'github') }}" class="btn btn-dark">
<i class="fab fa-github"></i> Login with GitHub
</a>
<a href="{{ route('social.redirect', 'google') }}" class="btn btn-danger">
<i class="fab fa-google"></i> Login with Google
</a>
<a href="{{ route('social.redirect', 'facebook') }}" class="btn btn-primary">
<i class="fab fa-facebook"></i> Login with Facebook
</a>
</div>
You now have complete, production-ready code for every authentication and authorization scenario in Laravel:
- β Session-based authentication with multiple guards
- β Role-Based Access Control (RBAC) with permissions
- β Gates and Policies for fine-grained authorization
- β API authentication with Sanctum and Passport
- β JWT tokens and OAuth2 with Socialite
π Module 03 : Database, Eloquent & Data Modeling Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
APIs, Frontend & Modern Integration β Complete Implementation Guide
Modern web applications are built on APIs. This comprehensive module provides step-by-step code examples for building RESTful APIs, integrating with frontend frameworks, handling webhooks, and consuming third-party services. All code is production-ready and follows Laravel best practices.
6.1 REST API Architecture β Complete Implementation
// routes/api.php - Complete API Route Structure
use App\Http\Controllers\Api\V1\AuthController;
use App\Http\Controllers\Api\V1\PostController;
use App\Http\Controllers\Api\V1\UserController;
use App\Http\Controllers\Api\V1\CategoryController;
use App\Http\Controllers\Api\V1\CommentController;
// API Versioning - V1
Route::prefix('v1')->name('api.v1.')->group(function () {
// Public routes (no authentication required)
Route::prefix('auth')->name('auth.')->group(function () {
Route::post('register', [AuthController::class, 'register'])->name('register');
Route::post('login', [AuthController::class, 'login'])->name('login');
Route::post('forgot-password', [AuthController::class, 'forgotPassword'])->name('forgot');
Route::post('reset-password', [AuthController::class, 'resetPassword'])->name('reset');
});
// Public resources
Route::get('posts', [PostController::class, 'index'])->name('posts.index');
Route::get('posts/{post}', [PostController::class, 'show'])->name('posts.show');
Route::get('categories', [CategoryController::class, 'index'])->name('categories.index');
// Protected routes (require authentication)
Route::middleware('auth:sanctum')->group(function () {
// User profile
Route::prefix('user')->name('user.')->group(function () {
Route::get('/', [UserController::class, 'profile'])->name('profile');
Route::put('/', [UserController::class, 'update'])->name('update');
Route::post('avatar', [UserController::class, 'uploadAvatar'])->name('avatar');
Route::post('change-password', [UserController::class, 'changePassword'])->name('password');
});
// Posts (authenticated actions)
Route::post('posts', [PostController::class, 'store'])->name('posts.store');
Route::put('posts/{post}', [PostController::class, 'update'])->name('posts.update');
Route::delete('posts/{post}', [PostController::class, 'destroy'])->name('posts.destroy');
// Comments
Route::apiResource('comments', CommentController::class)->except(['index', 'show']);
// Logout
Route::post('auth/logout', [AuthController::class, 'logout'])->name('auth.logout');
});
// Admin routes
Route::middleware(['auth:sanctum', 'role:admin'])->prefix('admin')->name('admin.')->group(function () {
Route::apiResource('users', UserController::class)->except(['create', 'edit']);
Route::apiResource('categories', CategoryController::class)->except(['index', 'show']);
Route::get('dashboard', [AdminController::class, 'dashboard'])->name('dashboard');
Route::get('stats', [AdminController::class, 'stats'])->name('stats');
});
});
// API Versioning - V2 (future)
Route::prefix('v2')->name('api.v2.')->group(function () {
// Future API improvements
});
// app/Http/Controllers/Api/V1/ApiController.php
namespace App\Http\Controllers\Api\V1;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class ApiController extends Controller
{
/**
* Success response method
*/
protected function successResponse($data = null, string $message = null, int $code = 200): JsonResponse
{
return response()->json([
'success' => true,
'message' => $message,
'data' => $data,
], $code);
}
/**
* Error response method
*/
protected function errorResponse(string $message = null, int $code = 400, $errors = null): JsonResponse
{
return response()->json([
'success' => false,
'message' => $message,
'errors' => $errors,
], $code);
}
/**
* Created response (201)
*/
protected function createdResponse($data = null, string $message = 'Resource created successfully'): JsonResponse
{
return $this->successResponse($data, $message, Response::HTTP_CREATED);
}
/**
* No content response (204)
*/
protected function noContentResponse(): JsonResponse
{
return response()->json(null, Response::HTTP_NO_CONTENT);
}
/**
* Paginated response
*/
protected function paginatedResponse($paginator, $resourceClass = null): JsonResponse
{
$data = $resourceClass
? $resourceClass::collection($paginator->items())
: $paginator->items();
return response()->json([
'success' => true,
'data' => $data,
'meta' => [
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(),
'total' => $paginator->total(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
],
'links' => [
'first' => $paginator->url(1),
'last' => $paginator->url($paginator->lastPage()),
'prev' => $paginator->previousPageUrl(),
'next' => $paginator->nextPageUrl(),
],
]);
}
}
// app/Http/Controllers/Api/V1/AuthController.php
namespace App\Http\Controllers\Api\V1;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class AuthController extends ApiController
{
/**
* Register a new user
*/
public function register(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:8|confirmed',
'device_name' => 'nullable|string',
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
'email_verified_at' => now(),
]);
// Create token
$deviceName = $request->device_name ?? $request->userAgent() ?? 'unknown';
$token = $user->createToken($deviceName)->plainTextToken;
return $this->createdResponse([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
], 'User registered successfully');
}
/**
* Login user
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required|string',
'device_name' => 'nullable|string',
]);
$user = User::where('email', $request->email)->first();
if (!$user || !Hash::check($request->password, $user->password)) {
throw ValidationException::withMessages([
'email' => ['The provided credentials are incorrect.'],
]);
}
// Check if user is active
if (!$user->is_active) {
return $this->errorResponse('Your account is deactivated.', 403);
}
// Revoke old tokens (optional - keep last 5)
if ($user->tokens()->count() >= 5) {
$user->tokens()->orderBy('created_at', 'asc')->first()->delete();
}
// Create token
$deviceName = $request->device_name ?? $request->userAgent() ?? 'unknown';
$token = $user->createToken($deviceName, ['*'])->plainTextToken;
// Update last login
$user->update([
'last_login_at' => now(),
'last_login_ip' => $request->ip(),
]);
return $this->successResponse([
'user' => $user,
'token' => $token,
'token_type' => 'Bearer',
], 'Login successful');
}
/**
* Logout user
*/
public function logout(Request $request)
{
// Revoke current token
$request->user()->currentAccessToken()->delete();
return $this->successResponse(null, 'Logged out successfully');
}
/**
* Logout from all devices
*/
public function logoutAll(Request $request)
{
// Revoke all tokens
$request->user()->tokens()->delete();
return $this->successResponse(null, 'Logged out from all devices');
}
/**
* Send password reset link
*/
public function forgotPassword(Request $request)
{
$request->validate(['email' => 'required|email']);
$status = Password::sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return $this->successResponse(null, 'Password reset link sent to your email');
}
return $this->errorResponse('Unable to send reset link', 400);
}
/**
* Reset password
*/
public function resetPassword(Request $request)
{
$request->validate([
'token' => 'required',
'email' => 'required|email',
'password' => 'required|string|min:8|confirmed',
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
}
);
if ($status === Password::PASSWORD_RESET) {
return $this->successResponse(null, 'Password reset successfully');
}
return $this->errorResponse('Invalid token or email', 400);
}
/**
* Refresh token
*/
public function refresh(Request $request)
{
$user = $request->user();
// Revoke current token
$request->user()->currentAccessToken()->delete();
// Create new token
$token = $user->createToken($request->userAgent() ?? 'unknown')->plainTextToken;
return $this->successResponse([
'token' => $token,
'token_type' => 'Bearer',
], 'Token refreshed');
}
}
6.2 API Resources & Transformers β Complete Implementation
# Generate API Resources
php artisan make:resource UserResource
php artisan make:resource UserCollection
php artisan make:resource PostResource
php artisan make:resource PostCollection
php artisan make:resource CommentResource
// app/Http/Resources/UserResource.php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource
{
/**
* Whether to include sensitive data
*/
private bool $includeSensitive = false;
/**
* Create a new resource instance with sensitive data option
*/
public function withSensitiveData(bool $include = true): self
{
$this->includeSensitive = $include;
return $this;
}
/**
* Transform the resource into an array.
*/
public function toArray(Request $request): array
{
$data = [
'id' => $this->id,
'name' => $this->name,
'username' => $this->username,
'avatar' => $this->avatar ? asset('storage/' . $this->avatar) : null,
'bio' => $this->bio,
'role' => $this->role,
'posts_count' => $this->when($this->posts_count !== null, $this->posts_count),
'followers_count' => $this->followers_count ?? 0,
'following_count' => $this->following_count ?? 0,
'is_verified' => (bool) $this->email_verified_at,
'created_at' => $this->created_at?->toISOString(),
'updated_at' => $this->updated_at?->toISOString(),
];
// Add sensitive data for admin or owner
if ($this->includeSensitive || $request->user()?->isAdmin() || $request->user()?->id === $this->id) {
$data['email'] = $this->email;
$data['phone'] = $this->phone;
$data['last_login_at'] = $this->last_login_at?->toISOString();
$data['last_login_ip'] = $this->last_login_ip;
$data['is_active'] = (bool) $this->is_active;
$data['email_verified_at'] = $this->email_verified_at?->toISOString();
}
// Include relationships when loaded
if ($this->relationLoaded('posts')) {
$data['posts'] = PostResource::collection($this->posts);
}
if ($this->relationLoaded('latest_post')) {
$data['latest_post'] = new PostResource($this->latest_post);
}
return $data;
}
/**
* Get additional data that should be returned with the resource array.
*/
public function with(Request $request): array
{
return [
'meta' => [
'version' => '1.0.0',
'timestamp' => now()->toISOString(),
],
];
}
}
// app/Http/Resources/UserCollection.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
class UserCollection extends ResourceCollection
{
/**
* The resource that this resource collects.
*/
public $collects = UserResource::class;
/**
* Transform the resource collection into an array.
*/
public function toArray($request): array
{
return [
'data' => $this->collection,
'meta' => [
'total' => $this->total(),
'count' => $this->count(),
'per_page' => $this->perPage(),
'current_page' => $this->currentPage(),
'total_pages' => $this->lastPage(),
],
'links' => [
'first' => $this->url(1),
'last' => $this->url($this->lastPage()),
'prev' => $this->previousPageUrl(),
'next' => $this->nextPageUrl(),
],
];
}
}
// app/Http/Resources/PostResource.php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
/**
* Transform the resource into an array.
*/
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'slug' => $this->slug,
'excerpt' => Str::limit($this->content, 200),
'content' => $this->when(
$request->routeIs('api.v1.posts.show'),
$this->content
),
'featured_image' => $this->featured_image
? asset('storage/' . $this->featured_image)
: null,
'views' => $this->views,
'likes_count' => $this->likes_count ?? 0,
'comments_count' => $this->comments_count ?? 0,
'status' => $this->status,
'is_featured' => (bool) $this->is_featured,
'created_at' => $this->created_at?->diffForHumans(),
'updated_at' => $this->updated_at?->toISOString(),
// Relationships
'author' => new UserResource($this->whenLoaded('user')),
'category' => new CategoryResource($this->whenLoaded('category')),
'tags' => TagResource::collection($this->whenLoaded('tags')),
'comments' => CommentResource::collection($this->whenLoaded('comments')),
'latest_comments' => CommentResource::collection(
$this->whenLoaded('latestComments')
),
// Additional URLs
'urls' => [
'public' => url('/posts/' . $this->slug),
'api' => route('api.v1.posts.show', $this->id),
'edit' => $this->when(
$request->user()?->can('update', $this->resource),
route('api.v1.posts.update', $this->id)
),
],
];
}
/**
* Customize the outgoing response for the resource.
*/
public function withResponse($request, $response)
{
$response->header('X-Post-Version', '1.0');
parent::withResponse($request, $response);
}
}
// app/Http/Controllers/Api/V1/PostController.php
namespace App\Http\Controllers\Api\V1;
use App\Models\Post;
use App\Http\Resources\PostResource;
use App\Http\Resources\PostCollection;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class PostController extends ApiController
{
/**
* Display a listing of posts.
*/
public function index(Request $request)
{
$query = Post::with(['user', 'category', 'tags'])
->where('status', 'published');
// Filter by category
if ($request->has('category')) {
$query->whereHas('category', function ($q) use ($request) {
$q->where('slug', $request->category);
});
}
// Filter by tag
if ($request->has('tag')) {
$query->whereHas('tags', function ($q) use ($request) {
$q->where('slug', $request->tag);
});
}
// Search
if ($request->has('search')) {
$query->where(function ($q) use ($request) {
$q->where('title', 'like', '%' . $request->search . '%')
->orWhere('content', 'like', '%' . $request->search . '%');
});
}
// Sort
$sortField = $request->get('sort_by', 'created_at');
$sortDirection = $request->get('sort_direction', 'desc');
$query->orderBy($sortField, $sortDirection);
// Pagination
$perPage = $request->get('per_page', 15);
$posts = $query->paginate($perPage);
return $this->paginatedResponse($posts, PostResource::class);
}
/**
* Store a newly created post.
*/
public function store(Request $request)
{
$request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
'category_id' => 'required|exists:categories,id',
'featured_image' => 'nullable|image|max:2048',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'status' => 'in:draft,published',
]);
DB::beginTransaction();
try {
$post = new Post();
$post->user_id = $request->user()->id;
$post->title = $request->title;
$post->slug = Str::slug($request->title);
$post->content = $request->content;
$post->category_id = $request->category_id;
$post->status = $request->status ?? 'draft';
// Handle featured image
if ($request->hasFile('featured_image')) {
$path = $request->file('featured_image')
->store('posts/' . date('Y/m'), 'public');
$post->featured_image = $path;
}
$post->save();
// Attach tags
if ($request->has('tags')) {
$post->tags()->attach($request->tags);
}
DB::commit();
return $this->createdResponse(
new PostResource($post->load(['user', 'category', 'tags'])),
'Post created successfully'
);
} catch (\Exception $e) {
DB::rollBack();
return $this->errorResponse('Failed to create post: ' . $e->getMessage(), 500);
}
}
/**
* Display the specified post.
*/
public function show(Post $post)
{
// Increment views
$post->increment('views');
// Load relationships
$post->load(['user', 'category', 'tags', 'comments.user']);
return $this->successResponse(
new PostResource($post),
'Post retrieved successfully'
);
}
/**
* Update the specified post.
*/
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
$request->validate([
'title' => 'sometimes|string|max:255',
'content' => 'sometimes|string',
'category_id' => 'sometimes|exists:categories,id',
'featured_image' => 'nullable|image|max:2048',
'tags' => 'nullable|array',
'tags.*' => 'exists:tags,id',
'status' => 'in:draft,published',
]);
DB::beginTransaction();
try {
if ($request->has('title')) {
$post->title = $request->title;
$post->slug = Str::slug($request->title);
}
if ($request->has('content')) {
$post->content = $request->content;
}
if ($request->has('category_id')) {
$post->category_id = $request->category_id;
}
if ($request->has('status')) {
$post->status = $request->status;
}
// Handle featured image
if ($request->hasFile('featured_image')) {
// Delete old image
if ($post->featured_image) {
\Storage::disk('public')->delete($post->featured_image);
}
$path = $request->file('featured_image')
->store('posts/' . date('Y/m'), 'public');
$post->featured_image = $path;
}
$post->save();
// Sync tags
if ($request->has('tags')) {
$post->tags()->sync($request->tags);
}
DB::commit();
return $this->successResponse(
new PostResource($post->load(['user', 'category', 'tags'])),
'Post updated successfully'
);
} catch (\Exception $e) {
DB::rollBack();
return $this->errorResponse('Failed to update post: ' . $e->getMessage(), 500);
}
}
/**
* Remove the specified post.
*/
public function destroy(Post $post)
{
$this->authorize('delete', $post);
DB::beginTransaction();
try {
// Delete featured image
if ($post->featured_image) {
\Storage::disk('public')->delete($post->featured_image);
}
// Detach tags
$post->tags()->detach();
// Delete comments
$post->comments()->delete();
// Delete post
$post->delete();
DB::commit();
return $this->noContentResponse();
} catch (\Exception $e) {
DB::rollBack();
return $this->errorResponse('Failed to delete post: ' . $e->getMessage(), 500);
}
}
/**
* Get posts by current user.
*/
public function myPosts(Request $request)
{
$posts = Post::where('user_id', $request->user()->id)
->with(['category', 'tags'])
->orderBy('created_at', 'desc')
->paginate($request->get('per_page', 15));
return $this->paginatedResponse($posts, PostResource::class);
}
}
6.3 API Authentication & Rate Limiting β Complete Implementation
// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('auth', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
RateLimiter::for('posts', function (Request $request) {
return $request->user()
? Limit::perMinute(30)->by($request->user()->id)
: Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('uploads', function (Request $request) {
return Limit::perHour(10)->by($request->user()?->id ?: $request->ip());
});
RateLimiter::for('webhooks', function (Request $request) {
return Limit::perMinute(100)->by($request->ip());
});
// Dynamic rate limiting based on user role
RateLimiter::for('api-dynamic', function (Request $request) {
if ($request->user() && $request->user()->isSubscribed()) {
return Limit::perMinute(1000)->by($request->user()->id);
}
return Limit::perMinute(60)->by($request->ip());
});
}
// app/Http/Kernel.php - Apply rate limiting to API routes
protected $middlewareGroups = [
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
// routes/api.php - Apply specific rate limits
Route::middleware(['throttle:auth'])->group(function () {
Route::post('login', [AuthController::class, 'login']);
Route::post('register', [AuthController::class, 'register']);
});
Route::middleware(['auth:sanctum', 'throttle:posts'])->group(function () {
Route::apiResource('posts', PostController::class);
});
Route::middleware(['auth:sanctum', 'throttle:uploads'])->group(function () {
Route::post('upload', [UploadController::class, 'store']);
});
// app/Http/Middleware/ThrottleWithMetadata.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Cache\RateLimiter;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
class ThrottleWithMetadata
{
protected $limiter;
public function __construct(RateLimiter $limiter)
{
$this->limiter = $limiter;
}
public function handle(Request $request, Closure $next, $maxAttempts = 60, $decayMinutes = 1)
{
$key = $this->resolveRequestSignature($request);
if ($this->limiter->tooManyAttempts($key, $maxAttempts)) {
return $this->buildResponse($key, $maxAttempts);
}
$this->limiter->hit($key, $decayMinutes * 60);
$response = $next($request);
return $this->addHeaders(
$response,
$maxAttempts,
$this->calculateRemainingAttempts($key, $maxAttempts)
);
}
protected function resolveRequestSignature(Request $request)
{
return sha1(implode('|', [
$request->method(),
$request->root(),
$request->path(),
$request->ip(),
$request->user()?->id ?? 'guest',
]));
}
protected function calculateRemainingAttempts($key, $maxAttempts)
{
$attempts = $this->limiter->attempts($key);
return $attempts < $maxAttempts ? $maxAttempts - $attempts : 0;
}
protected function buildResponse($key, $maxAttempts)
{
$retryAfter = $this->limiter->availableIn($key);
return response()->json([
'success' => false,
'message' => 'Too many attempts. Please try again later.',
'errors' => [
'rate_limit' => [
'max_attempts' => $maxAttempts,
'retry_after' => $retryAfter,
'retry_after_seconds' => $retryAfter,
]
]
], 429)->withHeaders([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => 0,
'Retry-After' => $retryAfter,
]);
}
protected function addHeaders(Response $response, $maxAttempts, $remainingAttempts)
{
return $response->withHeaders([
'X-RateLimit-Limit' => $maxAttempts,
'X-RateLimit-Remaining' => $remainingAttempts,
]);
}
}
// app/Http/Controllers/Api/V1/TokenController.php
namespace App\Http\Controllers\Api\V1;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
class TokenController extends ApiController
{
/**
* List all user tokens
*/
public function index(Request $request)
{
$tokens = $request->user()->tokens->map(function ($token) {
return [
'id' => $token->id,
'name' => $token->name,
'abilities' => $token->abilities,
'last_used_at' => $token->last_used_at?->diffForHumans(),
'created_at' => $token->created_at?->diffForHumans(),
'expires_at' => $token->expires_at?->diffForHumans(),
];
});
return $this->successResponse($tokens);
}
/**
* Create new token
*/
public function store(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'abilities' => 'nullable|array',
'expires_in_days' => 'nullable|integer|min:1|max:365',
]);
$abilities = $request->abilities ?? ['*'];
$token = $request->user()->createToken(
$request->name,
$abilities,
$request->expires_in_days ? now()->addDays($request->expires_in_days) : null
);
return $this->successResponse([
'id' => $token->accessToken->id,
'name' => $token->accessToken->name,
'token' => $token->plainTextToken,
'abilities' => $abilities,
'expires_at' => $token->accessToken->expires_at,
], 'Token created successfully');
}
/**
* Revoke specific token
*/
public function destroy(Request $request, $tokenId)
{
$token = $request->user()->tokens()->find($tokenId);
if (!$token) {
return $this->errorResponse('Token not found', 404);
}
$token->delete();
return $this->successResponse(null, 'Token revoked successfully');
}
/**
* Revoke all tokens except current
*/
public function revokeAllExceptCurrent(Request $request)
{
$currentTokenId = $request->user()->currentAccessToken()->id;
$request->user()->tokens()
->where('id', '!=', $currentTokenId)
->delete();
return $this->successResponse(null, 'All other tokens revoked');
}
/**
* Update token abilities
*/
public function updateAbilities(Request $request, $tokenId)
{
$request->validate([
'abilities' => 'required|array',
]);
$token = $request->user()->tokens()->find($tokenId);
if (!$token) {
return $this->errorResponse('Token not found', 404);
}
$token->abilities = $request->abilities;
$token->save();
return $this->successResponse([
'id' => $token->id,
'abilities' => $token->abilities,
], 'Token abilities updated');
}
}
6.4 AJAX, Axios & Fetch API β Complete Frontend Integration
// resources/js/api/axios.js
import axios from 'axios';
// Create axios instance with default config
const apiClient = axios.create({
baseURL: '/api/v1',
timeout: 30000,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
});
// Request interceptor - add token
apiClient.interceptors.request.use(
(config) => {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
// Add CSRF token for web routes
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
if (csrfToken) {
config.headers['X-CSRF-TOKEN'] = csrfToken;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// Response interceptor - handle errors
apiClient.interceptors.response.use(
(response) => {
return response;
},
async (error) => {
const originalRequest = error.config;
// Handle 401 Unauthorized - token expired
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
try {
// Attempt to refresh token
const refreshResponse = await apiClient.post('/auth/refresh');
const newToken = refreshResponse.data.token;
localStorage.setItem('token', newToken);
apiClient.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
return apiClient(originalRequest);
} catch (refreshError) {
// Refresh failed - redirect to login
localStorage.removeItem('token');
window.location.href = '/login';
return Promise.reject(refreshError);
}
}
// Handle 403 Forbidden
if (error.response?.status === 403) {
// Show unauthorized message
alert('You do not have permission to perform this action.');
}
// Handle 422 Validation Errors
if (error.response?.status === 422) {
// Return validation errors for form handling
return Promise.reject(error.response.data.errors);
}
// Handle 429 Too Many Requests
if (error.response?.status === 429) {
alert('Too many requests. Please try again later.');
}
// Handle 500 Server Error
if (error.response?.status >= 500) {
alert('Server error. Please try again later.');
}
return Promise.reject(error);
}
);
export default apiClient;
// resources/js/api/auth.js
import apiClient from './axios';
export const auth = {
async register(userData) {
const response = await apiClient.post('/auth/register', userData);
if (response.data.token) {
localStorage.setItem('token', response.data.token);
}
return response.data;
},
async login(credentials) {
const response = await apiClient.post('/auth/login', credentials);
if (response.data.token) {
localStorage.setItem('token', response.data.token);
}
return response.data;
},
async logout() {
const response = await apiClient.post('/auth/logout');
localStorage.removeItem('token');
return response.data;
},
async getUser() {
const response = await apiClient.get('/user');
return response.data;
},
async updateProfile(userData) {
const response = await apiClient.put('/user', userData);
return response.data;
},
async changePassword(passwordData) {
const response = await apiClient.post('/user/change-password', passwordData);
return response.data;
},
async uploadAvatar(formData) {
const response = await apiClient.post('/user/avatar', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
},
};
// resources/js/api/posts.js
import apiClient from './axios';
export const posts = {
async getAll(params = {}) {
const response = await apiClient.get('/posts', { params });
return response.data;
},
async getById(id) {
const response = await apiClient.get(`/posts/${id}`);
return response.data;
},
async getBySlug(slug) {
const response = await apiClient.get(`/posts/slug/${slug}`);
return response.data;
},
async create(postData) {
const response = await apiClient.post('/posts', postData);
return response.data;
},
async update(id, postData) {
const response = await apiClient.put(`/posts/${id}`, postData);
return response.data;
},
async delete(id) {
const response = await apiClient.delete(`/posts/${id}`);
return response.data;
},
async getMyPosts(params = {}) {
const response = await apiClient.get('/posts/my-posts', { params });
return response.data;
},
async like(id) {
const response = await apiClient.post(`/posts/${id}/like`);
return response.data;
},
async unlike(id) {
const response = await apiClient.delete(`/posts/${id}/like`);
return response.data;
},
};
// resources/js/api/index.js
export { auth } from './auth';
export { posts } from './posts';
export { comments } from './comments';
export { users } from './users';
export { uploads } from './uploads';
// resources/js/components/Posts/PostList.vue
<template>
<div class="post-list">
<div v-if="loading" class="loading">Loading...</div>
<div v-else-if="error" class="error">
{{ error }}
</div>
<div v-else>
<div class="posts-grid">
<PostCard
v-for="post in posts"
:key="post.id"
:post="post"
@like="handleLike"
@delete="handleDelete"
/>
</div>
<Pagination
:meta="meta"
@page-change="loadPosts"
/>
</div>
</div>
</template>
<script>
import { posts } from '../../api';
import PostCard from './PostCard.vue';
import Pagination from '../Common/Pagination.vue';
export default {
name: 'PostList',
components: {
PostCard,
Pagination,
},
props: {
filters: {
type: Object,
default: () => ({})
}
},
data() {
return {
posts: [],
meta: {},
loading: false,
error: null,
};
},
mounted() {
this.loadPosts();
},
watch: {
filters: {
deep: true,
handler() {
this.loadPosts(1);
}
}
},
methods: {
async loadPosts(page = 1) {
this.loading = true;
this.error = null;
try {
const params = {
page,
...this.filters,
};
const response = await posts.getAll(params);
this.posts = response.data;
this.meta = response.meta;
} catch (error) {
this.error = 'Failed to load posts. Please try again.';
console.error('Error loading posts:', error);
} finally {
this.loading = false;
}
},
async handleLike(postId) {
try {
const response = await posts.like(postId);
// Update post in list
const index = this.posts.findIndex(p => p.id === postId);
if (index !== -1) {
this.posts[index].likes_count = response.data.likes_count;
this.posts[index].is_liked = response.data.is_liked;
}
} catch (error) {
alert('Failed to like post. Please try again.');
}
},
async handleDelete(postId) {
if (!confirm('Are you sure you want to delete this post?')) {
return;
}
try {
await posts.delete(postId);
this.posts = this.posts.filter(p => p.id !== postId);
} catch (error) {
alert('Failed to delete post. Please try again.');
}
},
},
};
</script>
// resources/js/components/Auth/LoginForm.vue
<template>
<form @submit.prevent="handleSubmit" class="login-form">
<div v-if="error" class="error-message">
{{ error }}
</div>
<div class="form-group">
<label for="email">Email</label>
<input
id="email"
v-model="form.email"
type="email"
required
:disabled="loading"
/>
</div>
<div class="form-group">
<label for="password">Password</label>
<input
id="password"
v-model="form.password"
type="password"
required
:disabled="loading"
/>
</div>
<button type="submit" :disabled="loading">
{{ loading ? 'Logging in...' : 'Login' }}
</button>
</form>
</template>
<script>
import { auth } from '../../api';
export default {
name: 'LoginForm',
data() {
return {
form: {
email: '',
password: '',
},
loading: false,
error: null,
};
},
methods: {
async handleSubmit() {
this.loading = true;
this.error = null;
try {
const response = await auth.login(this.form);
// Emit success event
this.$emit('login-success', response.user);
// Redirect to dashboard
this.$router.push('/dashboard');
} catch (error) {
if (error.email) {
this.error = error.email[0];
} else if (error.message) {
this.error = error.message;
} else {
this.error = 'Login failed. Please try again.';
}
} finally {
this.loading = false;
}
},
},
};
</script>
// resources/js/react/contexts/AuthContext.jsx
import React, { createContext, useState, useContext, useEffect } from 'react';
import { auth } from '../api';
const AuthContext = createContext();
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Check if user is logged in
const token = localStorage.getItem('token');
if (token) {
loadUser();
} else {
setLoading(false);
}
}, []);
const loadUser = async () => {
try {
const response = await auth.getUser();
setUser(response.data);
} catch (error) {
console.error('Failed to load user:', error);
localStorage.removeItem('token');
} finally {
setLoading(false);
}
};
const login = async (credentials) => {
setError(null);
try {
const response = await auth.login(credentials);
setUser(response.user);
return response;
} catch (error) {
setError(error.message || 'Login failed');
throw error;
}
};
const register = async (userData) => {
setError(null);
try {
const response = await auth.register(userData);
setUser(response.user);
return response;
} catch (error) {
setError(error.message || 'Registration failed');
throw error;
}
};
const logout = async () => {
try {
await auth.logout();
} finally {
setUser(null);
localStorage.removeItem('token');
}
};
const value = {
user,
loading,
error,
login,
register,
logout,
};
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
// resources/js/react/components/PostList.jsx
import React, { useState, useEffect } from 'react';
import { posts } from '../../api';
import PostCard from './PostCard';
import Pagination from './Pagination';
const PostList = ({ filters = {} }) => {
const [postsData, setPostsData] = useState({
items: [],
meta: {},
loading: true,
error: null,
});
const loadPosts = async (page = 1) => {
setPostsData(prev => ({ ...prev, loading: true, error: null }));
try {
const params = { page, ...filters };
const response = await posts.getAll(params);
setPostsData({
items: response.data,
meta: response.meta,
loading: false,
error: null,
});
} catch (error) {
setPostsData(prev => ({
...prev,
loading: false,
error: 'Failed to load posts. Please try again.',
}));
}
};
useEffect(() => {
loadPosts();
}, [filters]);
const handleLike = async (postId) => {
try {
const response = await posts.like(postId);
// Update post in list
setPostsData(prev => ({
...prev,
items: prev.items.map(post =>
post.id === postId
? { ...post, likes_count: response.data.likes_count }
: post
),
}));
} catch (error) {
alert('Failed to like post. Please try again.');
}
};
const handleDelete = async (postId) => {
if (!window.confirm('Are you sure you want to delete this post?')) {
return;
}
try {
await posts.delete(postId);
setPostsData(prev => ({
...prev,
items: prev.items.filter(post => post.id !== postId),
}));
} catch (error) {
alert('Failed to delete post. Please try again.');
}
};
if (postsData.loading) {
return <div className="loading">Loading...</div>;
}
if (postsData.error) {
return <div className="error">{postsData.error}</div>;
}
return (
<div className="post-list">
<div className="posts-grid">
{postsData.items.map(post => (
<PostCard
key={post.id}
post={post}
onLike={handleLike}
onDelete={handleDelete}
/>
))}
</div>
<Pagination
meta={postsData.meta}
onPageChange={loadPosts}
/>
</div>
);
};
export default PostList;
6.5 Laravel + Vue / React / Inertia β Complete Setup
# Install Laravel Breeze with Vue
composer require laravel/breeze --dev
php artisan breeze:install vue
# Or manually install Vue
npm install vue@next @vitejs/plugin-vue
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
vue({
template: {
transformAssetUrls: {
base: null,
includeAbsolute: false,
},
},
}),
],
});
// resources/js/app.js
import { createApp } from 'vue';
import { createRouter, createWebHistory } from 'vue-router';
import { createPinia } from 'pinia';
import App from './App.vue';
import routes from './routes';
import './bootstrap';
const app = createApp(App);
// Router
const router = createRouter({
history: createWebHistory(),
routes,
});
// State management
const pinia = createPinia();
app.use(router);
app.use(pinia);
app.mount('#app');
// resources/js/stores/auth.js (Pinia store)
import { defineStore } from 'pinia';
import { auth } from '../api';
export const useAuthStore = defineStore('auth', {
state: () => ({
user: null,
token: localStorage.getItem('token'),
loading: false,
error: null,
}),
getters: {
isAuthenticated: (state) => !!state.token,
isAdmin: (state) => state.user?.role === 'admin',
userName: (state) => state.user?.name,
},
actions: {
async login(credentials) {
this.loading = true;
this.error = null;
try {
const response = await auth.login(credentials);
this.user = response.user;
this.token = response.token;
localStorage.setItem('token', response.token);
return response;
} catch (error) {
this.error = error.message || 'Login failed';
throw error;
} finally {
this.loading = false;
}
},
async logout() {
await auth.logout();
this.user = null;
this.token = null;
localStorage.removeItem('token');
},
async loadUser() {
if (!this.token) return;
this.loading = true;
try {
const response = await auth.getUser();
this.user = response.data;
} catch (error) {
this.user = null;
this.token = null;
localStorage.removeItem('token');
} finally {
this.loading = false;
}
},
},
});
# Install Laravel Breeze with React
composer require laravel/breeze --dev
php artisan breeze:install react
# Or manually install React
npm install react react-dom @vitejs/plugin-react
// vite.config.js
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
laravel({
input: ['resources/css/app.css', 'resources/js/app.jsx'],
refresh: true,
}),
react(),
],
});
// resources/js/app.jsx
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
import './bootstrap';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(
<React.StrictMode>
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>
</React.StrictMode>
);
// resources/js/store/slices/authSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { auth } from '../../api';
export const login = createAsyncThunk(
'auth/login',
async (credentials, { rejectWithValue }) => {
try {
const response = await auth.login(credentials);
localStorage.setItem('token', response.token);
return response;
} catch (error) {
return rejectWithValue(error.message || 'Login failed');
}
}
);
export const loadUser = createAsyncThunk(
'auth/loadUser',
async (_, { rejectWithValue }) => {
try {
const response = await auth.getUser();
return response.data;
} catch (error) {
localStorage.removeItem('token');
return rejectWithValue(error.message);
}
}
);
const authSlice = createSlice({
name: 'auth',
initialState: {
user: null,
token: localStorage.getItem('token'),
loading: false,
error: null,
},
reducers: {
logout: (state) => {
state.user = null;
state.token = null;
localStorage.removeItem('token');
},
clearError: (state) => {
state.error = null;
},
},
extraReducers: (builder) => {
builder
.addCase(login.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(login.fulfilled, (state, action) => {
state.loading = false;
state.user = action.payload.user;
state.token = action.payload.token;
})
.addCase(login.rejected, (state, action) => {
state.loading = false;
state.error = action.payload;
})
.addCase(loadUser.fulfilled, (state, action) => {
state.user = action.payload;
});
},
});
export const { logout, clearError } = authSlice.actions;
export default authSlice.reducer;
# Install Inertia.js with Vue
composer require inertiajs/inertia-laravel
npm install @inertiajs/vue3
# Or with React
npm install @inertiajs/react
// app/Http/Middleware/HandleInertiaRequests.php
namespace App\Http\Middleware;
use Illuminate\Http\Request;
use Inertia\Middleware;
class HandleInertiaRequests extends Middleware
{
protected $rootView = 'app';
public function version(Request $request): ?string
{
return parent::version($request);
}
public function share(Request $request): array
{
return array_merge(parent::share($request), [
'auth' => [
'user' => $request->user(),
'permissions' => $request->user()?->getAllPermissions(),
],
'flash' => [
'success' => fn () => $request->session()->get('success'),
'error' => fn () => $request->session()->get('error'),
],
'app' => [
'name' => config('app.name'),
'env' => config('app.env'),
],
]);
}
}
// routes/web.php (Inertia routes)
use Inertia\Inertia;
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
]);
});
Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dashboard', function () {
return Inertia::render('Dashboard', [
'stats' => [
'posts_count' => Post::count(),
'users_count' => User::count(),
]
]);
})->name('dashboard');
Route::get('/posts', function () {
return Inertia::render('Posts/Index', [
'posts' => Post::with('user')
->latest()
->paginate(10)
->through(fn ($post) => [
'id' => $post->id,
'title' => $post->title,
'author' => $post->user->name,
'created_at' => $post->created_at->diffForHumans(),
]),
]);
})->name('posts.index');
});
// resources/js/Pages/Posts/Index.vue (Inertia Vue)
<template>
<Layout>
<Head title="Posts" />
<div class="posts-container">
<h1>Posts</h1>
<div v-if="$page.props.flash.success" class="alert-success">
{{ $page.props.flash.success }}
</div>
<div class="posts-grid">
<div v-for="post in posts.data" :key="post.id" class="post-card">
<h3>{{ post.title }}</h3>
<p>By {{ post.author }} on {{ post.created_at }}</p>
<Link :href="`/posts/${post.id}`">Read More</Link>
</div>
</div>
<Pagination :links="posts.links" />
</div>
</Layout>
</template>
<script setup>
import Layout from '@/Layouts/Layout.vue';
import { Head, Link } from '@inertiajs/vue3';
import Pagination from '@/Components/Pagination.vue';
defineProps({
posts: {
type: Object,
required: true,
},
});
</script>
// resources/js/Pages/Posts/Create.vue (Inertia with form)
<template>
<Layout>
<Head title="Create Post" />
<form @submit.prevent="submit">
<div class="form-group">
<label for="title">Title</label>
<input
id="title"
v-model="form.title"
type="text"
/>
<div v-if="form.errors.title" class="error">
{{ form.errors.title }}
</div>
</div>
<div class="form-group">
<label for="content">Content</label>
<textarea
id="content"
v-model="form.content"
rows="10"
></textarea>
<div v-if="form.errors.content" class="error">
{{ form.errors.content }}
</div>
</div>
<button type="submit" :disabled="form.processing">
{{ form.processing ? 'Creating...' : 'Create Post' }}
</button>
</form>
</Layout>
</template>
<script setup>
import Layout from '@/Layouts/Layout.vue';
import { Head } from '@inertiajs/vue3';
import { useForm } from '@inertiajs/vue3';
const form = useForm({
title: '',
content: '',
});
const submit = () => {
form.post('/posts', {
onSuccess: () => form.reset(),
});
};
</script>
6.6 Webhooks & Third-Party APIs β Complete Implementation
// app/Http/Controllers/WebhookController.php
namespace App\Http\Controllers;
use App\Models\WebhookLog;
use App\Models\WebhookSubscription;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Crypt;
class WebhookController extends Controller
{
/**
* Handle incoming webhook from Stripe
*/
public function handleStripe(Request $request)
{
$payload = $request->getContent();
$sigHeader = $request->header('Stripe-Signature');
$endpointSecret = config('services.stripe.webhook_secret');
try {
$event = \Stripe\Webhook::constructEvent(
$payload, $sigHeader, $endpointSecret
);
} catch (\UnexpectedValueException $e) {
// Invalid payload
Log::error('Stripe webhook: Invalid payload', ['error' => $e->getMessage()]);
return response()->json(['error' => 'Invalid payload'], 400);
} catch (\Stripe\Exception\SignatureVerificationException $e) {
// Invalid signature
Log::error('Stripe webhook: Invalid signature', ['error' => $e->getMessage()]);
return response()->json(['error' => 'Invalid signature'], 400);
}
// Log webhook for audit
WebhookLog::create([
'provider' => 'stripe',
'event_type' => $event->type,
'payload' => $event->data->toArray(),
'processed' => false,
]);
// Handle the event
switch ($event->type) {
case 'payment_intent.succeeded':
$this->handlePaymentSucceeded($event->data->object);
break;
case 'payment_intent.payment_failed':
$this->handlePaymentFailed($event->data->object);
break;
case 'customer.subscription.created':
$this->handleSubscriptionCreated($event->data->object);
break;
case 'customer.subscription.updated':
$this->handleSubscriptionUpdated($event->data->object);
break;
case 'customer.subscription.deleted':
$this->handleSubscriptionDeleted($event->data->object);
break;
default:
Log::info('Unhandled Stripe event', ['type' => $event->type]);
}
return response()->json(['received' => true]);
}
/**
* Handle GitHub webhook
*/
public function handleGitHub(Request $request)
{
// Verify signature
$signature = $request->header('X-Hub-Signature-256');
$payload = $request->getContent();
$secret = config('services.github.webhook_secret');
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expectedSignature, $signature)) {
Log::warning('Invalid GitHub webhook signature');
return response()->json(['error' => 'Invalid signature'], 401);
}
$event = $request->header('X-GitHub-Event');
$delivery = $request->header('X-GitHub-Delivery');
Log::info('GitHub webhook received', [
'event' => $event,
'delivery' => $delivery,
]);
switch ($event) {
case 'push':
$this->handleGitHubPush($request->all());
break;
case 'pull_request':
$this->handleGitHubPullRequest($request->all());
break;
case 'issues':
$this->handleGitHubIssue($request->all());
break;
}
return response()->json(['received' => true]);
}
/**
* Generic webhook receiver with signature verification
*/
public function handleGeneric(Request $request, $provider)
{
$webhook = WebhookSubscription::where('provider', $provider)
->where('active', true)
->first();
if (!$webhook) {
return response()->json(['error' => 'Webhook not found'], 404);
}
// Verify signature
$signature = $request->header('X-Webhook-Signature');
$payload = $request->getContent();
$expectedSignature = hash_hmac('sha256', $payload, $webhook->secret);
if (!hash_equals($expectedSignature, $signature)) {
Log::warning('Invalid webhook signature', ['provider' => $provider]);
return response()->json(['error' => 'Invalid signature'], 401);
}
// Process webhook
$data = $request->all();
// Queue for processing
ProcessWebhookJob::dispatch($webhook, $data);
return response()->json(['received' => true]);
}
}
// app/Jobs/ProcessWebhookJob.php
namespace App\Jobs;
use App\Models\WebhookLog;
use App\Models\WebhookSubscription;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProcessWebhookJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 120;
public $tries = 3;
protected $webhook;
protected $payload;
public function __construct(WebhookSubscription $webhook, array $payload)
{
$this->webhook = $webhook;
$this->payload = $payload;
}
public function handle()
{
// Log webhook
$log = WebhookLog::create([
'provider' => $this->webhook->provider,
'event_type' => $this->payload['event'] ?? 'unknown',
'payload' => $this->payload,
'processed' => false,
]);
try {
// Process based on webhook type
switch ($this->webhook->provider) {
case 'payment':
$this->processPaymentWebhook();
break;
case 'notification':
$this->processNotificationWebhook();
break;
case 'crm':
$this->processCrmWebhook();
break;
default:
$this->processCustomWebhook();
}
// Mark as processed
$log->update(['processed' => true, 'processed_at' => now()]);
} catch (\Exception $e) {
$log->update([
'error' => $e->getMessage(),
'retry_count' => $this->attempts(),
]);
if ($this->attempts() < $this->tries) {
// Release with delay
$this->release(30);
} else {
// Notify admins after max retries
\Notification::route('mail', config('app.admin_email'))
->notify(new WebhookFailedNotification($this->webhook, $e));
}
throw $e;
}
}
protected function processPaymentWebhook()
{
$event = $this->payload['event'] ?? null;
switch ($event) {
case 'payment_succeeded':
// Update payment status
Payment::where('transaction_id', $this->payload['transaction_id'])
->update(['status' => 'paid']);
break;
case 'payment_failed':
// Handle failed payment
Payment::where('transaction_id', $this->payload['transaction_id'])
->update(['status' => 'failed']);
break;
}
}
protected function processNotificationWebhook()
{
// Send notification to users
$users = User::whereIn('id', $this->payload['user_ids'])->get();
\Notification::send($users, new WebhookNotification($this->payload));
}
protected function processCrmWebhook()
{
// Sync customer data with CRM
Customer::updateOrCreate(
['email' => $this->payload['customer']['email']],
$this->payload['customer']
);
}
protected function processCustomWebhook()
{
// Custom processing logic
event('webhook.received', [$this->webhook, $this->payload]);
}
}
// app/Services/PaymentGatewayService.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class PaymentGatewayService
{
protected $baseUrl;
protected $apiKey;
protected $secretKey;
public function __construct()
{
$this->baseUrl = config('services.payment.base_url');
$this->apiKey = config('services.payment.api_key');
$this->secretKey = config('services.payment.secret_key');
}
/**
* Create payment intent
*/
public function createPaymentIntent(array $data)
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
])->post($this->baseUrl . '/v1/payment_intents', [
'amount' => $data['amount'],
'currency' => $data['currency'] ?? 'usd',
'payment_method_types' => ['card'],
'metadata' => [
'order_id' => $data['order_id'],
'customer_id' => $data['customer_id'],
],
]);
if ($response->failed()) {
Log::error('Payment intent creation failed', [
'response' => $response->json(),
'data' => $data,
]);
throw new \Exception('Payment gateway error: ' . $response->json('error.message'));
}
return $response->json();
}
/**
* Confirm payment
*/
public function confirmPayment(string $paymentIntentId, array $data)
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->post($this->baseUrl . "/v1/payment_intents/{$paymentIntentId}/confirm", [
'payment_method' => $data['payment_method_id'],
]);
if ($response->failed()) {
throw new \Exception('Payment confirmation failed: ' . $response->json('error.message'));
}
return $response->json();
}
/**
* Refund payment
*/
public function refundPayment(string $paymentIntentId, ?int $amount = null)
{
$data = ['payment_intent' => $paymentIntentId];
if ($amount) {
$data['amount'] = $amount;
}
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->post($this->baseUrl . '/v1/refunds', $data);
if ($response->failed()) {
throw new \Exception('Refund failed: ' . $response->json('error.message'));
}
return $response->json();
}
/**
* Get payment details
*/
public function getPayment(string $paymentIntentId)
{
// Cache response for 5 minutes
return Cache::remember("payment.{$paymentIntentId}", 300, function () use ($paymentIntentId) {
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->get($this->baseUrl . "/v1/payment_intents/{$paymentIntentId}");
if ($response->failed()) {
throw new \Exception('Failed to fetch payment: ' . $response->json('error.message'));
}
return $response->json();
});
}
}
// app/Services/ApiClient.php
namespace App\Services;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ApiClient
{
protected PendingRequest $client;
protected int $maxRetries = 3;
protected int $retryDelay = 100; // milliseconds
public function __construct(array $config)
{
$this->client = Http::baseUrl($config['base_url'])
->withHeaders($config['headers'] ?? [])
->withOptions($config['options'] ?? [])
->timeout($config['timeout'] ?? 30)
->retry($this->maxRetries, $this->retryDelay);
}
/**
* Make GET request
*/
public function get(string $endpoint, array $query = [])
{
return $this->request('get', $endpoint, $query);
}
/**
* Make POST request
*/
public function post(string $endpoint, array $data = [])
{
return $this->request('post', $endpoint, $data);
}
/**
* Make PUT request
*/
public function put(string $endpoint, array $data = [])
{
return $this->request('put', $endpoint, $data);
}
/**
* Make DELETE request
*/
public function delete(string $endpoint)
{
return $this->request('delete', $endpoint);
}
/**
* Execute request with logging
*/
protected function request(string $method, string $endpoint, array $data = [])
{
$startTime = microtime(true);
try {
$response = $this->client->$method($endpoint, $data);
$this->logRequest($method, $endpoint, $data, $response, $startTime);
if ($response->failed()) {
throw new \Exception("API request failed: {$response->status()}");
}
return $response->json();
} catch (\Exception $e) {
$this->logError($method, $endpoint, $data, $e, $startTime);
throw $e;
}
}
/**
* Log successful request
*/
protected function logRequest($method, $endpoint, $data, $response, $startTime)
{
$duration = microtime(true) - $startTime;
Log::channel('api')->info('API Request', [
'method' => strtoupper($method),
'endpoint' => $endpoint,
'status' => $response->status(),
'duration' => round($duration * 1000, 2) . 'ms',
'request_size' => strlen(json_encode($data)),
'response_size' => strlen($response->body()),
]);
}
/**
* Log failed request
*/
protected function logError($method, $endpoint, $data, $exception, $startTime)
{
$duration = microtime(true) - $startTime;
Log::channel('api')->error('API Request Failed', [
'method' => strtoupper($method),
'endpoint' => $endpoint,
'error' => $exception->getMessage(),
'duration' => round($duration * 1000, 2) . 'ms',
'request_data' => $data,
]);
}
}
You now have complete, production-ready code for building modern APIs and integrating with frontend frameworks:
- β RESTful API architecture with versioning
- β API Resources and Transformers for consistent responses
- β API Authentication with Sanctum and rate limiting
- β Frontend integration with Axios, Vue, and React
- β Inertia.js for full-stack SPAs
- β Webhook handling and third-party API integration
π Module 03 : Database, Eloquent & Data Modeling Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Files, Mail, Events & Queues β Complete Implementation Guide
Modern applications require robust file handling, communication, and background processing. This comprehensive module provides step-by-step code examples for file uploads, cloud storage, email systems, notifications, events, listeners, and queue management. All code is production-ready and follows Laravel best practices.
7.1 File Uploads & Cloud Storage β Complete Implementation
// config/filesystems.php
return [
'default' => env('FILESYSTEM_DISK', 'local'),
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app'),
'throw' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
],
'uploads' => [
'driver' => 'local',
'root' => public_path('uploads'),
'url' => env('APP_URL').'/uploads',
'visibility' => 'public',
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
'throw' => false,
'visibility' => 'private', // Default to private for security
],
'digitalocean' => [
'driver' => 's3',
'key' => env('DIGITALOCEAN_KEY'),
'secret' => env('DIGITALOCEAN_SECRET'),
'region' => env('DIGITALOCEAN_REGION'),
'bucket' => env('DIGITALOCEAN_BUCKET'),
'endpoint' => env('DIGITALOCEAN_ENDPOINT'),
],
],
'links' => [
public_path('storage') => storage_path('app/public'),
public_path('uploads') => storage_path('app/uploads'),
],
];
// app/Http/Controllers/FileUploadController.php
namespace App\Http\Controllers;
use App\Models\File;
use App\Jobs\ProcessFileJob;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Intervention\Image\Facades\Image;
class FileUploadController extends Controller
{
/**
* Single file upload
*/
public function uploadSingle(Request $request)
{
$request->validate([
'file' => 'required|file|max:10240|mimes:jpg,jpeg,png,pdf,doc,docx',
'folder' => 'nullable|string',
]);
$file = $request->file('file');
$folder = $request->folder ?? 'uploads/' . date('Y/m/d');
// Generate secure filename
$filename = Str::random(40) . '.' . $file->getClientOriginalExtension();
// Store file
$path = $file->storeAs($folder, $filename, 'public');
// Create file record
$fileRecord = File::create([
'user_id' => auth()->id(),
'original_name' => $file->getClientOriginalName(),
'filename' => $filename,
'path' => $path,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
'folder' => $folder,
'disk' => 'public',
'metadata' => [
'width' => $this->getImageWidth($file),
'height' => $this->getImageHeight($file),
],
]);
// Queue processing for large files or images
if ($file->getSize() > 5 * 1024 * 1024 || str_starts_with($file->getMimeType(), 'image/')) {
ProcessFileJob::dispatch($fileRecord);
}
return response()->json([
'success' => true,
'file' => [
'id' => $fileRecord->id,
'url' => Storage::disk('public')->url($path),
'name' => $file->getClientOriginalName(),
'size' => $this->formatBytes($file->getSize()),
],
]);
}
/**
* Multiple files upload
*/
public function uploadMultiple(Request $request)
{
$request->validate([
'files' => 'required|array',
'files.*' => 'required|file|max:10240|mimes:jpg,jpeg,png,pdf',
]);
$uploadedFiles = [];
foreach ($request->file('files') as $file) {
$filename = Str::random(40) . '.' . $file->getClientOriginalExtension();
$path = $file->storeAs('uploads/' . date('Y/m/d'), $filename, 'public');
$fileRecord = File::create([
'user_id' => auth()->id(),
'original_name' => $file->getClientOriginalName(),
'filename' => $filename,
'path' => $path,
'mime_type' => $file->getMimeType(),
'size' => $file->getSize(),
]);
$uploadedFiles[] = [
'id' => $fileRecord->id,
'url' => Storage::disk('public')->url($path),
'name' => $file->getClientOriginalName(),
];
}
return response()->json([
'success' => true,
'files' => $uploadedFiles,
'count' => count($uploadedFiles),
]);
}
/**
* Chunked file upload for large files
*/
public function uploadChunked(Request $request)
{
$request->validate([
'file' => 'required|file',
'chunk' => 'required|integer',
'chunks' => 'required|integer',
'identifier' => 'required|string',
]);
$file = $request->file('file');
$identifier = $request->identifier;
$chunk = $request->chunk;
$chunks = $request->chunks;
$tmpPath = storage_path('app/tmp/chunks/' . $identifier);
if (!file_exists($tmpPath)) {
mkdir($tmpPath, 0777, true);
}
// Save chunk
$file->move($tmpPath, "chunk-{$chunk}");
// Check if all chunks uploaded
$uploadedChunks = count(glob($tmpPath . '/*'));
if ($uploadedChunks == $chunks) {
// Combine chunks
$finalPath = storage_path('app/uploads/' . date('Y/m/d'));
if (!file_exists($finalPath)) {
mkdir($finalPath, 0777, true);
}
$filename = Str::random(40) . '.' . $file->getClientOriginalExtension();
$finalFile = fopen($finalPath . '/' . $filename, 'wb');
for ($i = 1; $i <= $chunks; $i++) {
$chunk = file_get_contents($tmpPath . "/chunk-{$i}");
fwrite($finalFile, $chunk);
}
fclose($finalFile);
// Cleanup chunks
array_map('unlink', glob("$tmpPath/*"));
rmdir($tmpPath);
// Create file record
$fileRecord = File::create([
'user_id' => auth()->id(),
'original_name' => $request->input('name', $file->getClientOriginalName()),
'filename' => $filename,
'path' => 'uploads/' . date('Y/m/d') . '/' . $filename,
'size' => filesize($finalPath . '/' . $filename),
'mime_type' => mime_content_type($finalPath . '/' . $filename),
]);
return response()->json([
'success' => true,
'file' => [
'id' => $fileRecord->id,
'url' => Storage::disk('public')->url($fileRecord->path),
],
'completed' => true,
]);
}
return response()->json([
'success' => true,
'uploaded' => $uploadedChunks,
'total' => $chunks,
]);
}
/**
* Base64 file upload
*/
public function uploadBase64(Request $request)
{
$request->validate([
'file_data' => 'required|string',
'file_name' => 'nullable|string',
]);
$base64Data = $request->file_data;
// Extract file info from base64
if (preg_match('/^data:image\/(\w+);base64,/', $base64Data, $type)) {
$data = substr($base64Data, strpos($base64Data, ',') + 1);
$type = strtolower($type[1]); // jpg, png, gif
if (!in_array($type, ['jpg', 'jpeg', 'gif', 'png'])) {
return response()->json(['error' => 'Invalid image type'], 400);
}
$data = base64_decode($data);
if ($data === false) {
return response()->json(['error' => 'Invalid base64 data'], 400);
}
} else {
return response()->json(['error' => 'Invalid data URL'], 400);
}
$filename = Str::random(40) . '.' . $type;
$path = 'uploads/' . date('Y/m/d') . '/' . $filename;
Storage::disk('public')->put($path, $data);
$fileRecord = File::create([
'user_id' => auth()->id(),
'original_name' => $request->file_name ?? 'image.' . $type,
'filename' => $filename,
'path' => $path,
'size' => strlen($data),
'mime_type' => 'image/' . $type,
]);
return response()->json([
'success' => true,
'file' => [
'id' => $fileRecord->id,
'url' => Storage::disk('public')->url($path),
],
]);
}
/**
* Get file download
*/
public function download($id)
{
$file = File::findOrFail($id);
// Check permissions
if ($file->user_id !== auth()->id() && !auth()->user()->isAdmin()) {
abort(403);
}
if (!Storage::disk($file->disk)->exists($file->path)) {
abort(404);
}
return Storage::disk($file->disk)->download($file->path, $file->original_name);
}
/**
* Delete file
*/
public function destroy($id)
{
$file = File::findOrFail($id);
// Check permissions
if ($file->user_id !== auth()->id() && !auth()->user()->isAdmin()) {
return response()->json(['error' => 'Unauthorized'], 403);
}
// Delete from storage
if (Storage::disk($file->disk)->exists($file->path)) {
Storage::disk($file->disk)->delete($file->path);
}
// Delete thumbnails if any
if ($file->thumbnails) {
foreach ($file->thumbnails as $thumbnail) {
Storage::disk($file->disk)->delete($thumbnail);
}
}
$file->delete();
return response()->json(['success' => true]);
}
/**
* Helper: Format bytes
*/
protected function formatBytes($bytes, $precision = 2)
{
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, $precision) . ' ' . $units[$pow];
}
/**
* Helper: Get image width
*/
protected function getImageWidth($file)
{
if (str_starts_with($file->getMimeType(), 'image/')) {
try {
list($width) = getimagesize($file->getRealPath());
return $width;
} catch (\Exception $e) {
return null;
}
}
return null;
}
/**
* Helper: Get image height
*/
protected function getImageHeight($file)
{
if (str_starts_with($file->getMimeType(), 'image/')) {
try {
list(, $height) = getimagesize($file->getRealPath());
return $height;
} catch (\Exception $e) {
return null;
}
}
return null;
}
}
// database/migrations/xxxx_xx_xx_create_files_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('files', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('original_name');
$table->string('filename');
$table->string('path');
$table->string('disk')->default('public');
$table->string('mime_type')->nullable();
$table->unsignedBigInteger('size')->default(0);
$table->string('folder')->nullable();
$table->json('thumbnails')->nullable();
$table->json('metadata')->nullable();
$table->string('collection')->nullable()->index();
$table->integer('sort_order')->default(0);
$table->text('description')->nullable();
$table->string('alt_text')->nullable();
$table->unsignedInteger('download_count')->default(0);
$table->unsignedInteger('view_count')->default(0);
$table->boolean('is_public')->default(false);
$table->timestamp('expires_at')->nullable();
$table->softDeletes();
$table->timestamps();
$table->index(['user_id', 'collection']);
$table->index('created_at');
});
}
public function down()
{
Schema::dropIfExists('files');
}
};
// app/Models/File.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
class File extends Model
{
use SoftDeletes;
protected $fillable = [
'user_id',
'original_name',
'filename',
'path',
'disk',
'mime_type',
'size',
'folder',
'thumbnails',
'metadata',
'collection',
'sort_order',
'description',
'alt_text',
'is_public',
'expires_at',
];
protected $casts = [
'thumbnails' => 'array',
'metadata' => 'array',
'is_public' => 'boolean',
'expires_at' => 'datetime',
'size' => 'integer',
];
/**
* Get the URL for the file
*/
public function getUrlAttribute(): string
{
return Storage::disk($this->disk)->url($this->path);
}
/**
* Get thumbnail URL if exists
*/
public function getThumbnailUrl($size = 'thumb'): ?string
{
if ($this->thumbnails && isset($this->thumbnails[$size])) {
return Storage::disk($this->disk)->url($this->thumbnails[$size]);
}
return $this->url;
}
/**
* Get human-readable size
*/
public function getReadableSizeAttribute(): string
{
$bytes = $this->size;
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
/**
* Check if file is an image
*/
public function getIsImageAttribute(): bool
{
return $this->mime_type && str_starts_with($this->mime_type, 'image/');
}
/**
* Check if file is a video
*/
public function getIsVideoAttribute(): bool
{
return $this->mime_type && str_starts_with($this->mime_type, 'video/');
}
/**
* Check if file is a PDF
*/
public function getIsPdfAttribute(): bool
{
return $this->mime_type === 'application/pdf';
}
/**
* Get file extension
*/
public function getExtensionAttribute(): string
{
return pathinfo($this->filename, PATHINFO_EXTENSION);
}
/**
* Scope by collection
*/
public function scopeInCollection($query, $collection)
{
return $query->where('collection', $collection);
}
/**
* Scope public files
*/
public function scopePublic($query)
{
return $query->where('is_public', true)
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
}
/**
* Scope by mime type
*/
public function scopeOfType($query, $type)
{
return $query->where('mime_type', 'LIKE', $type . '%');
}
/**
* Increment download count
*/
public function incrementDownloadCount()
{
$this->increment('download_count');
$this->touch();
}
/**
* Increment view count
*/
public function incrementViewCount()
{
$this->increment('view_count');
}
}
// app/Jobs/ProcessFileJob.php
namespace App\Jobs;
use App\Models\File;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;
class ProcessFileJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 300;
public $tries = 3;
protected $file;
public function __construct(File $file)
{
$this->file = $file;
}
public function handle()
{
if ($this->file->is_image) {
$this->processImage();
} elseif ($this->file->is_video) {
$this->processVideo();
} elseif ($this->file->is_pdf) {
$this->processPdf();
}
}
protected function processImage()
{
$disk = Storage::disk($this->file->disk);
$imagePath = $this->file->path;
if (!$disk->exists($imagePath)) {
$this->fail(new \Exception('Image file not found'));
return;
}
$image = Image::make($disk->path($imagePath));
// Generate thumbnails
$thumbnails = [];
$sizes = [
'thumb' => [150, 150],
'small' => [300, 300],
'medium' => [600, 600],
'large' => [1200, 1200],
];
foreach ($sizes as $size => [$width, $height]) {
$thumbnailPath = 'thumbnails/' . $size . '/' . $this->file->filename;
$thumbnail = $image->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$disk->put($thumbnailPath, (string) $thumbnail->encode());
$thumbnails[$size] = $thumbnailPath;
}
// Extract EXIF data for images
$exif = @exif_read_data($disk->path($imagePath));
$metadata = [
'width' => $image->width(),
'height' => $image->height(),
'exif' => $exif ?: null,
];
$this->file->update([
'thumbnails' => $thumbnails,
'metadata' => array_merge($this->file->metadata ?? [], $metadata),
]);
}
protected function processVideo()
{
// Use FFmpeg to generate video thumbnails
// This would require the pbmedia/laravel-ffmpeg package
$metadata = [
'duration' => $this->getVideoDuration(),
'thumbnail' => $this->generateVideoThumbnail(),
];
$this->file->update(['metadata' => array_merge($this->file->metadata ?? [], $metadata)]);
}
protected function processPdf()
{
// Generate PDF thumbnails using imagick
$disk = Storage::disk($this->file->disk);
$pdfPath = $disk->path($this->file->path);
try {
$imagick = new \Imagick();
$imagick->setResolution(300, 300);
$imagick->readImage($pdfPath . '[0]');
$imagick->setImageFormat('jpg');
$thumbnailPath = 'thumbnails/pdf/' . $this->file->filename . '.jpg';
$disk->put($thumbnailPath, (string) $imagick);
$this->file->update([
'thumbnails' => ['pdf' => $thumbnailPath],
'metadata' => array_merge($this->file->metadata ?? [], [
'pages' => $imagick->getNumberImages(),
]),
]);
$imagick->clear();
} catch (\Exception $e) {
\Log::error('Failed to process PDF', ['error' => $e->getMessage()]);
}
}
protected function getVideoDuration()
{
// Implement video duration extraction
return null;
}
protected function generateVideoThumbnail()
{
// Implement video thumbnail generation
return null;
}
public function failed(\Throwable $exception)
{
\Log::error('File processing failed', [
'file_id' => $this->file->id,
'error' => $exception->getMessage(),
]);
}
}
7.2 Laravel Filesystem β S3 & CDN Complete Implementation
// app/Services/S3Service.php
namespace App\Services;
use Aws\S3\S3Client;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class S3Service
{
protected $client;
protected $bucket;
protected $disk;
public function __construct()
{
$this->disk = Storage::disk('s3');
$this->bucket = config('filesystems.disks.s3.bucket');
$this->client = new S3Client([
'version' => 'latest',
'region' => config('filesystems.disks.s3.region'),
'credentials' => [
'key' => config('filesystems.disks.s3.key'),
'secret' => config('filesystems.disks.s3.secret'),
],
]);
}
/**
* Upload file to S3
*/
public function upload(UploadedFile $file, string $path = '', array $options = [])
{
$filename = Str::random(40) . '.' . $file->getClientOriginalExtension();
$fullPath = trim($path . '/' . $filename, '/');
$result = $this->disk->put($fullPath, fopen($file->getRealPath(), 'r'), [
'visibility' => $options['visibility'] ?? 'private',
'ContentType' => $file->getMimeType(),
'Metadata' => [
'original_name' => $file->getClientOriginalName(),
'uploaded_by' => auth()->id() ?? 'system',
],
]);
if (!$result) {
throw new \Exception('Failed to upload file to S3');
}
return [
'path' => $fullPath,
'url' => $this->disk->url($fullPath),
'filename' => $filename,
];
}
/**
* Generate temporary signed URL for private files
*/
public function temporaryUrl(string $path, \DateTimeInterface $expiration)
{
return $this->disk->temporaryUrl($path, $expiration);
}
/**
* Generate multiple signed URLs
*/
public function temporaryUrls(array $paths, \DateTimeInterface $expiration)
{
$urls = [];
foreach ($paths as $path) {
$urls[$path] = $this->disk->temporaryUrl($path, $expiration);
}
return $urls;
}
/**
* Upload with progress tracking
*/
public function uploadWithProgress(UploadedFile $file, string $path, callable $progressCallback)
{
$filename = Str::random(40) . '.' . $file->getClientOriginalExtension();
$fullPath = trim($path . '/' . $filename, '/');
$result = $this->client->putObject([
'Bucket' => $this->bucket,
'Key' => $fullPath,
'SourceFile' => $file->getRealPath(),
'ContentType' => $file->getMimeType(),
'@http' => [
'progress' => function ($totalDownload, $downloadedBytes, $totalUpload, $uploadedBytes) use ($progressCallback) {
if ($totalUpload > 0) {
$percentage = ($uploadedBytes / $totalUpload) * 100;
$progressCallback($percentage, $uploadedBytes, $totalUpload);
}
},
],
]);
return [
'path' => $fullPath,
'url' => $this->disk->url($fullPath),
'etag' => trim($result['ETag'], '"'),
];
}
/**
* Multipart upload for large files
*/
public function initiateMultipartUpload(string $path, string $mimeType)
{
$result = $this->client->createMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $path,
'ContentType' => $mimeType,
]);
return [
'upload_id' => $result['UploadId'],
'key' => $path,
];
}
/**
* Upload part for multipart upload
*/
public function uploadPart(string $path, string $uploadId, int $partNumber, $body)
{
$result = $this->client->uploadPart([
'Bucket' => $this->bucket,
'Key' => $path,
'UploadId' => $uploadId,
'PartNumber' => $partNumber,
'Body' => $body,
]);
return [
'part_number' => $partNumber,
'etag' => trim($result['ETag'], '"'),
];
}
/**
* Complete multipart upload
*/
public function completeMultipartUpload(string $path, string $uploadId, array $parts)
{
$result = $this->client->completeMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $path,
'UploadId' => $uploadId,
'MultipartUpload' => [
'Parts' => $parts,
],
]);
return $result['Location'];
}
/**
* Abort multipart upload
*/
public function abortMultipartUpload(string $path, string $uploadId)
{
$this->client->abortMultipartUpload([
'Bucket' => $this->bucket,
'Key' => $path,
'UploadId' => $uploadId,
]);
}
/**
* Copy file within S3
*/
public function copy(string $fromPath, string $toPath)
{
return $this->disk->copy($fromPath, $toPath);
}
/**
* Move file within S3
*/
public function move(string $fromPath, string $toPath)
{
return $this->disk->move($fromPath, $toPath);
}
/**
* Delete file from S3
*/
public function delete(string $path)
{
return $this->disk->delete($path);
}
/**
* Delete multiple files
*/
public function deleteMultiple(array $paths)
{
return $this->disk->delete($paths);
}
/**
* Get file metadata
*/
public function getMetadata(string $path)
{
$result = $this->client->headObject([
'Bucket' => $this->bucket,
'Key' => $path,
]);
return [
'size' => $result['ContentLength'],
'mime_type' => $result['ContentType'],
'etag' => trim($result['ETag'], '"'),
'last_modified' => $result['LastModified'],
'metadata' => $result['Metadata'] ?? [],
];
}
/**
* Set file visibility
*/
public function setVisibility(string $path, string $visibility)
{
return $this->disk->setVisibility($path, $visibility);
}
/**
* Get file visibility
*/
public function getVisibility(string $path)
{
return $this->disk->getVisibility($path);
}
/**
* Generate a pre-signed POST for direct browser uploads
*/
public function generatePresignedPost(string $path, array $conditions = [], $expiration = '+30 minutes')
{
$formInputs = [
'key' => $path,
'Content-Type' => '', // Will be filled by browser
];
$options = [
['bucket' => $this->bucket],
['starts-with', '$key', $path],
['content-length-range', 1, 100 * 1024 * 1024], // 100MB max
];
$options = array_merge($options, $conditions);
$postObject = $this->client->createPresignedPost([
'Bucket' => $this->bucket,
'Key' => $path,
'Expires' => $expiration,
'Conditions' => $options,
'FormInputs' => $formInputs,
]);
return [
'attributes' => $postObject['fields'],
'url' => $postObject['url'],
];
}
}
// app/Services/CDNService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class CDNService
{
protected $cdnUrl;
protected $apiKey;
protected $zoneId;
public function __construct()
{
$this->cdnUrl = config('services.cloudflare.url');
$this->apiKey = config('services.cloudflare.api_key');
$this->zoneId = config('services.cloudflare.zone_id');
}
/**
* Purge single file from CDN
*/
public function purgeFile(string $path)
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
'Content-Type' => 'application/json',
])->post("https://api.cloudflare.com/client/v4/zones/{$this->zoneId}/purge_cache", [
'files' => [$this->cdnUrl . '/' . ltrim($path, '/')],
]);
if ($response->failed()) {
\Log::error('CDN purge failed', [
'path' => $path,
'response' => $response->json(),
]);
throw new \Exception('Failed to purge CDN cache: ' . $response->json('errors.0.message'));
}
// Clear cache
Cache::forget('cdn.file.' . md5($path));
return $response->json();
}
/**
* Purge multiple files
*/
public function purgeFiles(array $paths)
{
$urls = array_map(function ($path) {
return $this->cdnUrl . '/' . ltrim($path, '/');
}, $paths);
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->post("https://api.cloudflare.com/client/v4/zones/{$this->zoneId}/purge_cache", [
'files' => $urls,
]);
if ($response->failed()) {
throw new \Exception('Failed to purge CDN cache');
}
// Clear cache for all files
foreach ($paths as $path) {
Cache::forget('cdn.file.' . md5($path));
}
return $response->json();
}
/**
* Purge entire CDN cache
*/
public function purgeAll()
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->post("https://api.cloudflare.com/client/v4/zones/{$this->zoneId}/purge_cache", [
'purge_everything' => true,
]);
if ($response->failed()) {
throw new \Exception('Failed to purge CDN cache');
}
return $response->json();
}
/**
* Get CDN file status
*/
public function getFileStatus(string $path)
{
return Cache::remember('cdn.file.' . md5($path), 300, function () use ($path) {
$url = $this->cdnUrl . '/' . ltrim($path, '/');
$response = Http::withHeaders([
'User-Agent' => 'Mozilla/5.0 (compatible; CDN Checker)',
])->get($url);
return [
'url' => $url,
'status' => $response->status(),
'headers' => $response->headers(),
'cached_at' => now()->toIso8601String(),
];
});
}
/**
* Get CDN statistics
*/
public function getStatistics()
{
$response = Http::withHeaders([
'Authorization' => 'Bearer ' . $this->apiKey,
])->get("https://api.cloudflare.com/client/v4/zones/{$this->zoneId}/analytics/dashboard");
if ($response->failed()) {
throw new \Exception('Failed to get CDN statistics');
}
return $response->json('result.totals');
}
/**
* Get cached file URL with optimization parameters
*/
public function getOptimizedUrl(string $path, array $options = [])
{
$url = $this->cdnUrl . '/' . ltrim($path, '/');
$params = [];
if (isset($options['width'])) {
$params['width'] = $options['width'];
}
if (isset($options['height'])) {
$params['height'] = $options['height'];
}
if (isset($options['quality'])) {
$params['quality'] = $options['quality'];
}
if (isset($options['format'])) {
$params['format'] = $options['format'];
}
if (!empty($params)) {
$url .= '?' . http_build_query($params);
}
return $url;
}
}
7.3 Email System β Complete SMTP & Queue Implementation
// config/mail.php
return [
'default' => env('MAIL_MAILER', 'smtp'),
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
'auth_mode' => null,
'verify_peer' => true,
],
'ses' => [
'transport' => 'ses',
],
'mailgun' => [
'transport' => 'mailgun',
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
'scheme' => 'https',
],
'postmark' => [
'transport' => 'postmark',
'token' => env('POSTMARK_TOKEN'),
],
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
],
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
'reply_to' => [
'address' => env('MAIL_REPLY_TO_ADDRESS'),
'name' => env('MAIL_REPLY_TO_NAME'),
],
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];
// app/Mail/WelcomeMail.php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Queue\SerializesModels;
class WelcomeMail extends Mailable
{
use Queueable, SerializesModels;
public $user;
public $loginUrl;
/**
* Create a new message instance.
*/
public function __construct(User $user)
{
$this->user = $user;
$this->loginUrl = route('login');
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
from: new Address(config('mail.from.address'), config('mail.from.name')),
replyTo: [
new Address('support@example.com', 'Support Team'),
],
subject: 'Welcome to ' . config('app.name'),
tags: ['welcome', 'new-user'],
metadata: [
'user_id' => $this->user->id,
],
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'emails.welcome',
text: 'emails.welcome-text',
with: [
'userName' => $this->user->name,
'loginUrl' => $this->loginUrl,
'appName' => config('app.name'),
],
);
}
/**
* Get the attachments for the message.
*/
public function attachments(): array
{
return [
Attachment::fromStorage('documents/welcome-guide.pdf')
->as('getting-started.pdf')
->withMime('application/pdf'),
];
}
}
// app/Mail/OrderConfirmationMail.php
namespace App\Mail;
use App\Models\Order;
use Barryvdh\DomPDF\Facade\Pdf;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class OrderConfirmationMail extends Mailable
{
use Queueable, SerializesModels;
public $order;
public function __construct(Order $order)
{
$this->order = $order;
}
public function build()
{
// Generate PDF invoice
$pdf = PDF::loadView('pdfs.invoice', ['order' => $this->order]);
return $this->from(config('mail.from.address'), config('mail.from.name'))
->to($this->order->customer_email, $this->order->customer_name)
->subject('Order Confirmation #' . $this->order->order_number)
->view('emails.order-confirmation')
->with([
'order' => $this->order,
'orderUrl' => route('orders.show', $this->order->id),
])
->attachData($pdf->output(), 'invoice-' . $this->order->order_number . '.pdf', [
'mime' => 'application/pdf',
]);
}
}
// app/Http/Controllers/MailController.php
namespace App\Http\Controllers;
use App\Jobs\SendBulkEmailJob;
use App\Mail\WelcomeMail;
use App\Mail\OrderConfirmationMail;
use App\Mail\NewsletterMail;
use App\Models\User;
use App\Models\EmailLog;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
class MailController extends Controller
{
/**
* Send welcome email immediately
*/
public function sendWelcome(User $user)
{
try {
Mail::to($user->email)->send(new WelcomeMail($user));
$this->logEmail($user->email, 'welcome', 'sent');
return response()->json(['message' => 'Welcome email sent successfully']);
} catch (\Exception $e) {
$this->logEmail($user->email, 'welcome', 'failed', $e->getMessage());
return response()->json(['error' => 'Failed to send email: ' . $e->getMessage()], 500);
}
}
/**
* Send welcome email via queue
*/
public function queueWelcome(User $user)
{
Mail::to($user->email)->queue(new WelcomeMail($user));
$this->logEmail($user->email, 'welcome', 'queued');
return response()->json(['message' => 'Welcome email queued successfully']);
}
/**
* Send welcome email with delay
*/
public function sendDelayedWelcome(User $user)
{
$when = now()->addMinutes(30);
Mail::to($user->email)->later($when, new WelcomeMail($user));
$this->logEmail($user->email, 'welcome', 'scheduled', null, $when);
return response()->json(['message' => 'Welcome email scheduled for ' . $when->toDateTimeString()]);
}
/**
* Send order confirmation with attachment
*/
public function sendOrderConfirmation($orderId)
{
$order = Order::findOrFail($orderId);
Mail::to($order->customer_email)->queue(new OrderConfirmationMail($order));
return response()->json(['message' => 'Order confirmation queued']);
}
/**
* Send bulk emails via job
*/
public function sendBulkEmails(Request $request)
{
$request->validate([
'subject' => 'required|string',
'content' => 'required|string',
'user_ids' => 'sometimes|array',
'all_users' => 'sometimes|boolean',
]);
if ($request->all_users) {
$users = User::where('subscribed_to_emails', true)->get();
} else {
$users = User::whereIn('id', $request->user_ids)->get();
}
$jobId = SendBulkEmailJob::dispatch($users, $request->subject, $request->content);
return response()->json([
'message' => 'Bulk emails queued successfully',
'job_id' => $jobId,
'recipients' => $users->count(),
]);
}
/**
* Send test email
*/
public function sendTestEmail(Request $request)
{
$request->validate(['email' => 'required|email']);
try {
Mail::raw('This is a test email from ' . config('app.name'), function ($message) use ($request) {
$message->to($request->email)
->subject('Test Email');
});
return response()->json(['message' => 'Test email sent successfully']);
} catch (\Exception $e) {
return response()->json(['error' => $e->getMessage()], 500);
}
}
/**
* Get email logs
*/
public function getEmailLogs(Request $request)
{
$logs = EmailLog::orderBy('created_at', 'desc')
->when($request->email, function ($query, $email) {
return $query->where('recipient', 'like', "%{$email}%");
})
->when($request->type, function ($query, $type) {
return $query->where('type', $type);
})
->when($request->status, function ($query, $status) {
return $query->where('status', $status);
})
->paginate($request->per_page ?? 20);
return response()->json($logs);
}
/**
* Log email activity
*/
protected function logEmail($recipient, $type, $status, $error = null, $scheduledAt = null)
{
EmailLog::create([
'recipient' => $recipient,
'type' => $type,
'status' => $status,
'error' => $error,
'scheduled_at' => $scheduledAt,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
]);
}
}
<!-- resources/views/emails/welcome.blade.php -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to {{ $appName }}</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f4f4f4;
margin: 0;
padding: 0;
}
.container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 28px;
}
.content {
padding: 40px 30px;
}
.button {
display: inline-block;
padding: 12px 30px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white !important;
text-decoration: none;
border-radius: 25px;
font-weight: bold;
margin-top: 20px;
}
.footer {
background-color: #f8f9fa;
padding: 20px;
text-align: center;
font-size: 14px;
color: #666;
border-top: 1px solid #dee2e6;
}
.social-links {
margin-top: 15px;
}
.social-links a {
color: #667eea;
text-decoration: none;
margin: 0 10px;
}
.features {
background-color: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
}
.feature-item {
margin-bottom: 15px;
}
.feature-item strong {
color: #667eea;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Welcome to {{ $appName }}! π</h1>
</div>
<div class="content">
<p>Hello <strong>{{ $userName }}</strong>,</p>
<p>We're thrilled to have you on board! Thank you for joining our community. Your account has been successfully created and you're now ready to explore all the features we have to offer.</p>
<div class="features">
<h3>What you can do now:</h3>
<div class="feature-item">
<strong>β¨ Explore Features</strong> - Discover all the tools and features available to you
</div>
<div class="feature-item">
<strong>π§ Customize Profile</strong> - Make your profile unique and personal
</div>
<div class="feature-item">
<strong>π€ Connect with Others</strong> - Join our thriving community
</div>
</div>
<p>To get started, simply click the button below to log in to your account:</p>
<div style="text-align: center;">
<a href="{{ $loginUrl }}" class="button">Log In to Your Account</a>
</div>
<p>If you have any questions or need assistance, don't hesitate to reach out to our support team. We're here to help!</p>
<p>Best regards,<br>
The {{ $appName }} Team</p>
</div>
<div class="footer">
<p>Β© {{ date('Y') }} {{ $appName }}. All rights reserved.</p>
<div class="social-links">
<a href="#">Twitter</a>
<a href="#">Facebook</a>
<a href="#">LinkedIn</a>
</div>
<p>If you didn't create an account, please ignore this email.</p>
</div>
</div>
</body>
</html>
7.4 Notifications β Complete Multi-Channel Implementation
// database/migrations/xxxx_xx_xx_create_notifications_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('notifications', function (Blueprint $table) {
$table->uuid('id')->primary();
$table->string('type');
$table->morphs('notifiable');
$table->text('data');
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index(['notifiable_id', 'notifiable_type']);
$table->index('read_at');
});
}
public function down()
{
Schema::dropIfExists('notifications');
}
};
// app/Notifications/OrderStatusNotification.php
namespace App\Notifications;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Messages\NexmoMessage;
use Illuminate\Notifications\Messages\SlackMessage;
use Illuminate\Notifications\Messages\BroadcastMessage;
use Illuminate\Notifications\Notification;
class OrderStatusNotification extends Notification implements ShouldQueue
{
use Queueable;
protected $order;
protected $status;
public function __construct(Order $order, string $status)
{
$this->order = $order;
$this->status = $status;
}
/**
* Get the notification's delivery channels.
*/
public function via($notifiable): array
{
$channels = ['database', 'broadcast'];
if ($notifiable->email_verified_at && $notifiable->notification_preferences['email'] ?? true) {
$channels[] = 'mail';
}
if ($notifiable->phone && $notifiable->notification_preferences['sms'] ?? false) {
$channels[] = 'nexmo';
}
if ($notifiable->slack_webhook_url) {
$channels[] = 'slack';
}
return $channels;
}
/**
* Get the mail representation of the notification.
*/
public function toMail($notifiable): MailMessage
{
$statusMessages = [
'processing' => 'is being processed',
'shipped' => 'has been shipped',
'delivered' => 'has been delivered',
'cancelled' => 'has been cancelled',
];
$message = (new MailMessage)
->subject('Order #' . $this->order->order_number . ' ' . ucfirst($this->status))
->greeting('Hello ' . $notifiable->name . '!')
->line('Your order #' . $this->order->order_number . ' ' . $statusMessages[$this->status] . '.')
->line('Order Total: $' . number_format($this->order->total, 2))
->action('View Order', route('orders.show', $this->order->id))
->line('Thank you for shopping with us!');
// Add tracking info for shipped orders
if ($this->status === 'shipped' && $this->order->tracking_number) {
$message->line('Tracking Number: ' . $this->order->tracking_number);
$message->action('Track Package', $this->order->tracking_url);
}
return $message;
}
/**
* Get the SMS representation of the notification.
*/
public function toNexmo($notifiable): NexmoMessage
{
return (new NexmoMessage)
->content('Your order #' . $this->order->order_number . ' has been ' . $this->status . '.')
->from(config('app.name'));
}
/**
* Get the Slack representation of the notification.
*/
public function toSlack($notifiable): SlackMessage
{
$statusColors = [
'processing' => 'warning',
'shipped' => 'good',
'delivered' => 'good',
'cancelled' => 'danger',
];
return (new SlackMessage)
->from(config('app.name'), ':package:')
->to('#orders')
->attachment(function ($attachment) use ($statusColors) {
$attachment->title('Order #' . $this->order->order_number, route('orders.show', $this->order->id))
->fields([
'Status' => ucfirst($this->order->status),
'Customer' => $this->order->customer_name,
'Total' => '$' . number_format($this->order->total, 2),
'Items' => $this->order->items_count,
])
->color($statusColors[$this->status] ?? 'good');
});
}
/**
* Get the array representation of the notification.
*/
public function toArray($notifiable): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'status' => $this->status,
'message' => 'Order #' . $this->order->order_number . ' has been ' . $this->status,
'total' => $this->order->total,
'action_url' => route('orders.show', $this->order->id),
'icon' => $this->getStatusIcon(),
'color' => $this->getStatusColor(),
];
}
/**
* Get the broadcastable representation of the notification.
*/
public function toBroadcast($notifiable): BroadcastMessage
{
return new BroadcastMessage([
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'status' => $this->status,
'message' => 'Order #' . $this->order->order_number . ' has been ' . $this->status,
'time' => now()->toIso8601String(),
]);
}
protected function getStatusIcon(): string
{
return match($this->status) {
'processing' => 'π',
'shipped' => 'π¦',
'delivered' => 'β
',
'cancelled' => 'β',
default => 'π',
};
}
protected function getStatusColor(): string
{
return match($this->status) {
'processing' => 'blue',
'shipped' => 'green',
'delivered' => 'green',
'cancelled' => 'red',
default => 'gray',
};
}
}
// app/Http/Controllers/NotificationController.php
namespace App\Http\Controllers;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Notifications\DatabaseNotification;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
/**
* Get user notifications
*/
public function index(Request $request)
{
$user = Auth::user();
$notifications = $user->notifications()
->when($request->type, function ($query, $type) {
return $query->where('type', 'like', '%' . $type . '%');
})
->orderBy('created_at', 'desc')
->paginate($request->per_page ?? 20);
return response()->json([
'data' => $notifications,
'unread_count' => $user->unreadNotifications->count(),
]);
}
/**
* Mark notification as read
*/
public function markAsRead($id)
{
$notification = Auth::user()->notifications()->findOrFail($id);
$notification->markAsRead();
return response()->json(['message' => 'Notification marked as read']);
}
/**
* Mark all notifications as read
*/
public function markAllAsRead()
{
Auth::user()->unreadNotifications->markAsRead();
return response()->json(['message' => 'All notifications marked as read']);
}
/**
* Delete notification
*/
public function destroy($id)
{
$notification = Auth::user()->notifications()->findOrFail($id);
$notification->delete();
return response()->json(['message' => 'Notification deleted']);
}
/**
* Delete all notifications
*/
public function destroyAll()
{
Auth::user()->notifications()->delete();
return response()->json(['message' => 'All notifications deleted']);
}
/**
* Send test notification
*/
public function sendTest(Request $request)
{
$user = Auth::user();
$user->notify(new \App\Notifications\TestNotification());
return response()->json(['message' => 'Test notification sent']);
}
/**
* Get notification preferences
*/
public function getPreferences()
{
$user = Auth::user();
$defaultPreferences = [
'email' => true,
'sms' => false,
'push' => true,
'marketing' => false,
];
$preferences = array_merge(
$defaultPreferences,
$user->notification_preferences ?? []
);
return response()->json($preferences);
}
/**
* Update notification preferences
*/
public function updatePreferences(Request $request)
{
$request->validate([
'email' => 'boolean',
'sms' => 'boolean',
'push' => 'boolean',
'marketing' => 'boolean',
]);
$user = Auth::user();
$user->notification_preferences = $request->only(['email', 'sms', 'push', 'marketing']);
$user->save();
return response()->json([
'message' => 'Notification preferences updated',
'preferences' => $user->notification_preferences,
]);
}
}
// config/broadcasting.php
return [
'default' => env('BROADCAST_DRIVER', 'pusher'),
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'encrypted' => true,
'host' => 'api-' . env('PUSHER_APP_CLUSTER') . '.pusher.com',
'port' => 443,
'scheme' => 'https',
'curl_options' => [
CURLOPT_SSL_VERIFYHOST => 0,
CURLOPT_SSL_VERIFYPEER => 0,
],
],
],
'ably' => [
'driver' => 'ably',
'key' => env('ABLY_KEY'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
'log' => [
'driver' => 'log',
],
],
];
// resources/js/bootstrap.js
import Echo from 'laravel-echo';
import Pusher from 'pusher-js';
window.Pusher = Pusher;
window.Echo = new Echo({
broadcaster: 'pusher',
key: import.meta.env.VITE_PUSHER_APP_KEY,
cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER,
forceTLS: true,
authEndpoint: '/broadcasting/auth',
auth: {
headers: {
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
},
},
});
// Listen for notifications
window.Echo.private('App.Models.User.' + userId)
.notification((notification) => {
console.log('New notification:', notification);
showNotification(notification);
});
// resources/js/components/Notifications.vue
<template>
<div class="notifications-dropdown">
<button @click="toggleDropdown" class="notification-bell">
<i class="fas fa-bell"></i>
<span v-if="unreadCount > 0" class="badge">{{ unreadCount }}</span>
</button>
<div v-if="showDropdown" class="dropdown-menu">
<div class="dropdown-header">
<h4>Notifications</h4>
<button @click="markAllAsRead" v-if="unreadCount > 0">
Mark all as read
</button>
</div>
<div class="notifications-list">
<div v-for="notification in notifications"
:key="notification.id"
class="notification-item"
:class="{ 'unread': !notification.read_at }"
@click="markAsRead(notification)">
<div class="notification-icon">
{{ notification.data.icon || 'π' }}
</div>
<div class="notification-content">
<p>{{ notification.data.message }}</p>
<small>{{ formatTime(notification.created_at) }}</small>
</div>
</div>
<div v-if="notifications.length === 0" class="no-notifications">
No notifications yet
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import Echo from 'laravel-echo';
import moment from 'moment';
export default {
data() {
return {
notifications: [],
unreadCount: 0,
showDropdown: false,
userId: null,
};
},
mounted() {
this.userId = document.querySelector('meta[name="user-id"]').content;
this.loadNotifications();
this.listenForNotifications();
},
methods: {
async loadNotifications() {
try {
const response = await axios.get('/api/notifications');
this.notifications = response.data.data;
this.unreadCount = response.data.unread_count;
} catch (error) {
console.error('Failed to load notifications:', error);
}
},
listenForNotifications() {
window.Echo.private(`App.Models.User.${this.userId}`)
.notification((notification) => {
this.notifications.unshift({
id: notification.id,
data: notification,
created_at: new Date(),
read_at: null,
});
this.unreadCount++;
this.showToast(notification);
});
},
async markAsRead(notification) {
if (!notification.read_at) {
try {
await axios.post(`/api/notifications/${notification.id}/read`);
notification.read_at = new Date();
this.unreadCount--;
} catch (error) {
console.error('Failed to mark as read:', error);
}
}
if (notification.data.action_url) {
window.location.href = notification.data.action_url;
}
},
async markAllAsRead() {
try {
await axios.post('/api/notifications/read-all');
this.notifications.forEach(n => n.read_at = new Date());
this.unreadCount = 0;
} catch (error) {
console.error('Failed to mark all as read:', error);
}
},
showToast(notification) {
if (typeof toastr !== 'undefined') {
toastr.info(notification.message, 'New Notification');
}
},
formatTime(timestamp) {
return moment(timestamp).fromNow();
},
toggleDropdown() {
this.showDropdown = !this.showDropdown;
},
},
};
</script>
7.5 Events, Listeners & Observers β Complete Implementation
// app/Events/OrderCreated.php
namespace App\Events;
use App\Models\Order;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class OrderCreated implements ShouldBroadcast
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $order;
/**
* Create a new event instance.
*/
public function __construct(Order $order)
{
$this->order = $order;
}
/**
* Get the channels the event should broadcast on.
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('admin.orders'),
new PrivateChannel('user.' . $this->order->user_id),
];
}
/**
* The event's broadcast name.
*/
public function broadcastAs(): string
{
return 'order.created';
}
/**
* Get the data to broadcast.
*/
public function broadcastWith(): array
{
return [
'order_id' => $this->order->id,
'order_number' => $this->order->order_number,
'total' => $this->order->total,
'customer' => $this->order->customer_name,
'time' => now()->toIso8601String(),
];
}
}
// app/Events/UserRegistered.php
namespace App\Events;
use App\Models\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class UserRegistered
{
use Dispatchable, SerializesModels;
public $user;
public $ipAddress;
public $userAgent;
/**
* Create a new event instance.
*/
public function __construct(User $user, string $ipAddress, string $userAgent)
{
$this->user = $user;
$this->ipAddress = $ipAddress;
$this->userAgent = $userAgent;
}
}
// app/Listeners/SendOrderNotifications.php
namespace App\Listeners;
use App\Events\OrderCreated;
use App\Notifications\OrderStatusNotification;
use App\Services\SmsService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class SendOrderNotifications implements ShouldQueue
{
use InteractsWithQueue;
public $timeout = 60;
public $tries = 3;
protected $smsService;
/**
* Create the event listener.
*/
public function __construct(SmsService $smsService)
{
$this->smsService = $smsService;
}
/**
* Handle the event.
*/
public function handle(OrderCreated $event): void
{
$order = $event->order;
try {
// Send email notification
$order->user->notify(new OrderStatusNotification($order, 'created'));
// Send SMS if phone number exists
if ($order->user->phone) {
$this->smsService->send(
$order->user->phone,
"Your order #{$order->order_number} has been received. Total: $" . number_format($order->total, 2)
);
}
// Send push notification
if ($order->user->push_token) {
// Send push notification logic
}
Log::info('Order notifications sent', ['order_id' => $order->id]);
} catch (\Exception $e) {
Log::error('Failed to send order notifications', [
'order_id' => $order->id,
'error' => $e->getMessage(),
]);
$this->release(30); // Retry after 30 seconds
}
}
/**
* Handle a job failure.
*/
public function failed(OrderCreated $event, \Throwable $exception): void
{
Log::error('Order notifications failed permanently', [
'order_id' => $event->order->id,
'error' => $exception->getMessage(),
]);
// Notify admin about failure
Mail::raw(
"Failed to send notifications for order #{$event->order->order_number}\nError: {$exception->getMessage()}",
function ($message) {
$message->to(config('mail.admin_address'))
->subject('Order Notification Failure');
}
);
}
}
// app/Listeners/LogUserRegistration.php
namespace App\Listeners;
use App\Events\UserRegistered;
use App\Models\UserActivityLog;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class LogUserRegistration implements ShouldQueue
{
use InteractsWithQueue;
/**
* Handle the event.
*/
public function handle(UserRegistered $event): void
{
UserActivityLog::create([
'user_id' => $event->user->id,
'action' => 'registered',
'ip_address' => $event->ipAddress,
'user_agent' => $event->userAgent,
'metadata' => [
'email' => $event->user->email,
'name' => $event->user->name,
],
]);
}
}
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use App\Events\OrderCreated;
use App\Events\UserRegistered;
use App\Listeners\SendOrderNotifications;
use App\Listeners\LogUserRegistration;
use App\Listeners\UpdateInventory;
use App\Listeners\SendWelcomeEmail;
use App\Models\Order;
use App\Models\User;
use App\Observers\OrderObserver;
use App\Observers\UserObserver;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*/
protected $listen = [
OrderCreated::class => [
SendOrderNotifications::class,
UpdateInventory::class,
],
UserRegistered::class => [
SendWelcomeEmail::class,
LogUserRegistration::class,
],
'Illuminate\Auth\Events\Login' => [
'App\Listeners\LogSuccessfulLogin',
],
'Illuminate\Auth\Events\Logout' => [
'App\Listeners\LogSuccessfulLogout',
],
];
/**
* The model observers to register.
*/
protected $observers = [
Order::class => [OrderObserver::class],
User::class => [UserObserver::class],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
parent::boot();
// Register events programmatically
\Event::listen('event.*', function ($eventName, array $data) {
\Log::info('Event fired', ['event' => $eventName, 'data' => $data]);
});
// Queueable anonymous event listener
\Event::listen(function (OrderCreated $event) {
// This runs in the queue by default
\Log::info('Order created event received', ['order' => $event->order->id]);
});
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return true;
}
}
// app/Observers/OrderObserver.php
namespace App\Observers;
use App\Models\Order;
use App\Events\OrderCreated;
use App\Services\InventoryService;
use Illuminate\Support\Facades\Cache;
class OrderObserver
{
protected $inventoryService;
public function __construct(InventoryService $inventoryService)
{
$this->inventoryService = $inventoryService;
}
/**
* Handle the Order "creating" event.
*/
public function creating(Order $order): void
{
// Generate order number before creation
$order->order_number = $this->generateOrderNumber();
// Set initial status
$order->status = 'pending';
}
/**
* Handle the Order "created" event.
*/
public function created(Order $order): void
{
// Fire event for notifications
event(new OrderCreated($order));
// Update inventory
$this->inventoryService->reserveItems($order->items);
// Clear relevant caches
Cache::tags(['orders', 'user-' . $order->user_id])->flush();
// Log activity
activity()
->performedOn($order)
->causedBy($order->user)
->log('Order created');
}
/**
* Handle the Order "updating" event.
*/
public function updating(Order $order): void
{
// Track original values before update
$order->original_status = $order->getOriginal('status');
}
/**
* Handle the Order "updated" event.
*/
public function updated(Order $order): void
{
// Check if status changed
if ($order->wasChanged('status')) {
$this->handleStatusChange($order);
}
// Clear cache
Cache::forget('order_' . $order->id);
}
/**
* Handle the Order "deleting" event.
*/
public function deleting(Order $order): void
{
// Prevent deletion of completed orders
if (in_array($order->status, ['completed', 'shipped'])) {
return false;
}
}
/**
* Handle the Order "deleted" event.
*/
public function deleted(Order $order): void
{
// Release inventory
$this->inventoryService->releaseItems($order->items);
// Clear cache
Cache::forget('order_' . $order->id);
}
/**
* Handle the Order "restored" event.
*/
public function restored(Order $order): void
{
// Re-reserve inventory
$this->inventoryService->reserveItems($order->items);
}
/**
* Handle status change
*/
protected function handleStatusChange(Order $order): void
{
$oldStatus = $order->getOriginal('status');
$newStatus = $order->status;
// Log status change
activity()
->performedOn($order)
->withProperties([
'old' => $oldStatus,
'new' => $newStatus,
])
->log('Order status changed');
// Send status update notification
if ($order->user) {
$order->user->notify(new \App\Notifications\OrderStatusNotification($order, $newStatus));
}
}
/**
* Generate unique order number
*/
protected function generateOrderNumber(): string
{
$prefix = 'ORD';
$year = date('Y');
$month = date('m');
$lastOrder = Order::whereYear('created_at', $year)
->whereMonth('created_at', $month)
->orderBy('id', 'desc')
->first();
if ($lastOrder) {
$lastNumber = intval(substr($lastOrder->order_number, -4));
$sequence = str_pad($lastNumber + 1, 4, '0', STR_PAD_LEFT);
} else {
$sequence = '0001';
}
return "{$prefix}-{$year}{$month}-{$sequence}";
}
}
// app/Http/Controllers/OrderController.php
use App\Events\OrderCreated;
use App\Models\Order;
class OrderController extends Controller
{
public function store(Request $request)
{
$order = Order::create($request->validated());
// Fire event manually (Observer will also fire automatically)
event(new OrderCreated($order));
return response()->json(['order' => $order], 201);
}
public function register(Request $request)
{
$user = User::create($request->validated());
// Fire event with additional data
event(new UserRegistered($user, $request->ip(), $request->userAgent()));
return response()->json(['user' => $user], 201);
}
}
7.6 Queues, Jobs & Redis β Complete Implementation
// config/queue.php
return [
'default' => env('QUEUE_CONNECTION', 'redis'),
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => false,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => 5,
'after_commit' => false,
],
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
],
'beanstalkd' => [
'driver' => 'beanstalkd',
'host' => 'localhost',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 0,
'after_commit' => false,
],
],
'batching' => [
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'job_batches',
],
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],
];
// database/migrations/xxxx_xx_xx_create_jobs_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}
public function down()
{
Schema::dropIfExists('jobs');
}
};
// database/migrations/xxxx_xx_xx_create_failed_jobs_table.php
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
// database/migrations/xxxx_xx_xx_create_job_batches_table.php
Schema::create('job_batches', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name');
$table->integer('total_jobs');
$table->integer('pending_jobs');
$table->integer('failed_jobs');
$table->longText('failed_job_ids');
$table->mediumText('options')->nullable();
$table->integer('cancelled_at')->nullable();
$table->integer('created_at');
$table->integer('finished_at')->nullable();
});
// app/Jobs/ProcessPodcast.php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\RateLimited;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class ProcessPodcast implements ShouldQueue, ShouldBeUnique
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 300; // 5 minutes
public $tries = 3;
public $maxExceptions = 2;
public $backoff = [5, 10, 30]; // Retry delays in seconds
public $deleteWhenMissingModels = true;
protected $podcast;
/**
* Create a new job instance.
*/
public function __construct(Podcast $podcast)
{
$this->podcast = $podcast;
}
/**
* Get the unique ID for the job.
*/
public function uniqueId(): string
{
return $this->podcast->id;
}
/**
* Get the number of seconds before a unique job lock expires.
*/
public function uniqueFor(): int
{
return 3600; // 1 hour
}
/**
* Get the middleware the job should pass through.
*/
public function middleware(): array
{
return [
new RateLimited('podcasts'),
(new ThrottlesExceptions(10, 5))->backoff(2),
];
}
/**
* Execute the job.
*/
public function handle(AudioProcessor $processor): void
{
try {
$result = $processor->process($this->podcast);
if (!$result['success']) {
throw new \Exception('Processing failed: ' . $result['error']);
}
Log::info('Podcast processed successfully', [
'podcast_id' => $this->podcast->id,
'duration' => $result['duration'],
]);
} catch (\Exception $e) {
Log::error('Podcast processing failed', [
'podcast_id' => $this->podcast->id,
'error' => $e->getMessage(),
]);
throw $e; // Fail the job
}
}
/**
* Handle a job failure.
*/
public function failed(\Throwable $exception): void
{
Log::critical('Podcast processing permanently failed', [
'podcast_id' => $this->podcast->id,
'error' => $exception->getMessage(),
]);
// Notify admin
\Mail::raw(
"Podcast #{$this->podcast->id} processing failed permanently.\nError: {$exception->getMessage()}",
function ($message) {
$message->to(config('mail.admin_address'))
->subject('Critical: Podcast Processing Failure');
}
);
}
}
// app/Jobs/SendBulkEmailJob.php
namespace App\Jobs;
use App\Mail\BulkEmail;
use App\Models\User;
use Illuminate\Bus\Batchable;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class SendBulkEmailJob implements ShouldQueue
{
use Batchable, Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $timeout = 60;
public $tries = 3;
protected $user;
protected $subject;
protected $content;
public function __construct(User $user, string $subject, string $content)
{
$this->user = $user;
$this->subject = $subject;
$this->content = $content;
}
public function handle()
{
if ($this->batch()->cancelled()) {
return;
}
try {
Mail::to($this->user->email)->send(new BulkEmail($this->user, $this->subject, $this->content));
Log::info('Bulk email sent', ['user_id' => $this->user->id]);
} catch (\Exception $e) {
Log::error('Failed to send bulk email', [
'user_id' => $this->user->id,
'error' => $e->getMessage(),
]);
throw $e;
}
}
}
// app/Jobs/Middleware/RateLimited.php
namespace App\Jobs\Middleware;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
protected $key;
protected $maxAttempts;
protected $decaySeconds;
public function __construct(string $key, int $maxAttempts = 10, int $decaySeconds = 60)
{
$this->key = $key;
$this->maxAttempts = $maxAttempts;
$this->decaySeconds = $decaySeconds;
}
public function handle($job, $next)
{
$key = 'rate_limit:' . $this->key;
$current = Redis::incr($key);
if ($current == 1) {
Redis::expire($key, $this->decaySeconds);
}
if ($current > $this->maxAttempts) {
$job->release($this->decaySeconds);
return;
}
$next($job);
}
}
// app/Http/Controllers/JobController.php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Jobs\SendBulkEmailJob;
use App\Models\Podcast;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Queue;
class JobController extends Controller
{
/**
* Dispatch single job
*/
public function dispatchJob(Podcast $podcast)
{
// Simple dispatch
ProcessPodcast::dispatch($podcast);
// Dispatch with delay
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));
// Dispatch after response (for immediate processing)
ProcessPodcast::dispatchAfterResponse($podcast);
// Dispatch with specific queue
ProcessPodcast::dispatch($podcast)->onQueue('high');
// Dispatch with chain
Bus::chain([
new ProcessPodcast($podcast),
new OptimizePodcast($podcast),
new NotifySubscribers($podcast),
])->catch(function ($e) {
Log::error('Chain failed', ['error' => $e->getMessage()]);
})->dispatch();
return response()->json(['message' => 'Job dispatched']);
}
/**
* Dispatch batch jobs
*/
public function dispatchBatch(Request $request)
{
$users = User::where('subscribed', true)->get();
$batch = Bus::batch([]);
foreach ($users->chunk(100) as $chunk) {
foreach ($chunk as $user) {
$batch->add(new SendBulkEmailJob(
$user,
$request->subject,
$request->content
));
}
}
$batch->name('Bulk Email Campaign')
->then(function ($batch) {
Log::info('Bulk email campaign completed', [
'total' => $batch->totalJobs,
'batch_id' => $batch->id,
]);
})
->catch(function ($batch, $e) {
Log::error('Bulk email campaign failed', [
'error' => $e->getMessage(),
'batch_id' => $batch->id,
]);
})
->finally(function ($batch) {
// Send summary report
dispatch(new SendBatchReportJob($batch->id));
})
->dispatch();
return response()->json([
'message' => 'Batch job dispatched',
'batch_id' => $batch->id,
'total_jobs' => $users->count(),
]);
}
/**
* Get batch status
*/
public function batchStatus($batchId)
{
$batch = Bus::findBatch($batchId);
if (!$batch) {
return response()->json(['error' => 'Batch not found'], 404);
}
return response()->json([
'id' => $batch->id,
'name' => $batch->name,
'total_jobs' => $batch->totalJobs,
'pending_jobs' => $batch->pendingJobs,
'failed_jobs' => $batch->failedJobs,
'progress' => $batch->progress(),
'finished' => $batch->finished(),
'cancelled' => $batch->cancelled(),
'created_at' => $batch->createdAt,
'finished_at' => $batch->finishedAt,
]);
}
/**
* Cancel batch
*/
public function cancelBatch($batchId)
{
$batch = Bus::findBatch($batchId);
if ($batch && !$batch->finished()) {
$batch->cancel();
return response()->json(['message' => 'Batch cancelled']);
}
return response()->json(['error' => 'Batch not found or already finished'], 404);
}
/**
* Get failed jobs
*/
public function failedJobs()
{
$failedJobs = Queue::failed()->take(100)->get();
return response()->json($failedJobs);
}
/**
* Retry failed job
*/
public function retryFailedJob($id)
{
Queue::retry($id);
return response()->json(['message' => 'Job queued for retry']);
}
/**
* Retry all failed jobs
*/
public function retryAllFailed()
{
Queue::retryAll();
return response()->json(['message' => 'All failed jobs queued for retry']);
}
/**
* Forget failed job
*/
public function forgetFailedJob($id)
{
Queue::forget($id);
return response()->json(['message' => 'Failed job removed']);
}
/**
* Flush all failed jobs
*/
public function flushFailedJobs()
{
Queue::flush();
return response()->json(['message' => 'All failed jobs flushed']);
}
}
# Queue worker commands
php artisan queue:work # Start worker (foreground)
php artisan queue:listen # Listen for jobs (alternative)
php artisan queue:restart # Restart workers after deployment
php artisan queue:retry all # Retry all failed jobs
php artisan queue:retry 5 # Retry specific job by ID
php artisan queue:failed # List failed jobs
php artisan queue:flush # Delete all failed jobs
php artisan queue:forget 5 # Delete specific failed job
php artisan queue:clear # Delete all jobs from queue
php artisan queue:prune-batches --hours=24 # Prune old batches
# Start worker with specific queue
php artisan queue:work --queue=high,default --tries=3 --timeout=60
# Supervisor configuration ( /etc/supervisor/conf.d/laravel-worker.conf )
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --queue=high,default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600
// config/database.php (Redis section)
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', 'laravel_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
'queues' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', '2'),
],
];
// app/Console/Commands/MonitorQueue.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Log;
class MonitorQueue extends Command
{
protected $signature = 'queue:monitor {queue?}';
protected $description = 'Monitor queue size and performance';
public function handle()
{
$queue = $this->argument('queue') ?? 'default';
$size = Redis::connection('queues')->llen("queues:{$queue}");
$this->info("Queue '{$queue}' size: {$size}");
if ($size > 1000) {
Log::warning('Queue size is large', ['queue' => $queue, 'size' => $size]);
}
// Monitor failed jobs
$failed = \DB::table('failed_jobs')->count();
$this->info("Failed jobs: {$failed}");
return Command::SUCCESS;
}
}
You now have complete, production-ready code for handling files, emails, notifications, events, and queues in Laravel:
- β File uploads with multiple methods (single, multiple, chunked, base64)
- β Cloud storage integration (S3, CDN) with signed URLs
- β Email system with queuing, attachments, and templates
- β Multi-channel notifications (mail, SMS, Slack, broadcast)
- β Events, listeners, and observers with queued handling
- β Queue system with jobs, batches, rate limiting, and monitoring
π Module 03 : Database, Eloquent & Data Modeling Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Performance, Scaling & Security β Complete Implementation Guide
Performance optimization and security hardening are critical for production applications. This comprehensive module provides step-by-step guidance on caching strategies, Redis integration, performance profiling, rate limiting, and monitoring. Learn how to make your Laravel application lightning fast, highly scalable, and secure against attacks.
8.1 Caching Layers β Complete Implementation Guide
Laravel provides multiple caching layers that can significantly improve application performance. Each layer serves a specific purpose and should be implemented based on your application's needs.
| Cache Type | Purpose | Performance Gain | When to Use |
|---|---|---|---|
| Configuration Caching | Combines all config files into one | High (reduces file operations) | Production only, after every config change |
| Route Caching | Caches route registration | High (faster route matching) | Production only, not for closure-based routes |
| View Caching | Compiles Blade templates to PHP | Medium (reduces compilation) | Always in production |
| Event Caching | Caches event-listener mappings | Low to Medium | Production with many events |
| Application Caching | Combines multiple optimizations | High | Production deployment |
What is Configuration Caching?
Configuration caching combines all configuration files from the config/ directory
into a single file (bootstrap/cache/config.php). This reduces the number of file
operations Laravel needs to perform on each request, typically improving performance by 5-10ms per request.
How to Implement
# Cache all configuration files
php artisan config:cache
# This creates: bootstrap/cache/config.php
# Size: Typically 50-200KB depending on config complexity
# Clear configuration cache
php artisan config:clear
# Check if config is cached (returns true/false)
php artisan config:isCached
Best Practices
- Only use in production - During development, uncached config allows immediate changes
- Run after every config change - Forgetting to recache can lead to inconsistent behavior
- Never use with environment-dependent config - All environments will use the same cached values
- Include in deployment script - Always recache during deployment
Deployment Script Example
#!/bin/bash
# deploy.sh
echo "π Starting deployment..."
# Enable maintenance mode
php artisan down --retry=60 --render="errors.503"
# Pull latest code
git pull origin main
# Install dependencies (no dev packages in production)
composer install --no-dev --optimize-autoloader
# Run migrations
php artisan migrate --force
# Clear old caches
php artisan config:clear
php artisan route:clear
php artisan view:clear
# Rebuild caches
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache
# Restart queue workers
php artisan queue:restart
# Disable maintenance mode
php artisan up
echo "β
Deployment complete!"
config:cache during deployment if your
.env file contains sensitive data that differs per environment. The cached config
will use the values from the environment where the command was run.
What is Route Caching?
Route caching converts your route definitions into a highly optimized array lookup table. This dramatically speeds up route matching, especially for applications with hundreds of routes. Performance improvement can be 5-10x faster route resolution.
How to Implement
# Cache routes
php artisan route:cache
# This creates: bootstrap/cache/routes-v7.php
# Lists all routes in a serialized format for O(1) lookup
# Clear route cache
php artisan route:clear
# List all routes (useful for debugging)
php artisan route:list
# List routes with specific columns
php artisan route:list --columns=method,uri,name,action,middleware
# Export routes to JSON (useful for documentation)
php artisan route:list --json > routes.json
Important Limitations
- Closure-based routes (use controllers instead)
- Routes using
Route::redirectwith closures - Routes with grouped namespace closures
- Routes that use serialized closures in middlewares
Refactoring for Route Caching
// β BAD - Will not work with route caching
Route::get('/profile', function () {
return view('profile');
})->name('profile');
// β
GOOD - Use controller instead
Route::get('/profile', [ProfileController::class, 'show'])->name('profile');
// β BAD - Closure in group
Route::prefix('admin')->group(function () {
Route::get('/', function () {
return view('admin.dashboard');
});
});
// β
GOOD - Use route to controller
Route::prefix('admin')->group(function () {
Route::get('/', [AdminController::class, 'dashboard']);
});
// β BAD - Complex closure-based routes
Route::get('/user/{id}', function ($id) {
return User::findOrFail($id);
});
// β
GOOD - Move to controller
Route::get('/user/{id}', [UserController::class, 'show']);
Route Caching Performance Comparison
// Without route cache (200 routes)
// Time to match route: ~5-10ms
// Memory usage: ~2-3MB
// With route cache (200 routes)
// Time to match route: ~0.5-1ms
// Memory usage: ~1-2MB
// Performance improvement: 5-10x faster!
php artisan route:list to verify all your routes
are compatible with caching before enabling it in production.
What is View Caching?
Laravel's Blade engine compiles templates into plain PHP code. View caching stores these compiled templates, eliminating the need to recompile on every request. This reduces CPU usage and improves response times by 2-5ms per view.
How View Caching Works
// Original Blade template: resources/views/welcome.blade.php
<h1>Welcome, {{ $name }}!</h1>
@if($user->isAdmin())
<p>Admin Panel</p>
@endif
// Compiled to: storage/framework/views/abc123.php
<h1>Welcome, <?php echo e($name); ?>!</h1>
<?php if($user->isAdmin()): ?>
<p>Admin Panel</p>
<?php endif; ?>
Commands
# Clear compiled views
php artisan view:clear
# Pre-compile all views (Laravel 10+)
php artisan view:cache
# Check view cache status
php artisan view:isCached
# View all compiled views
ls storage/framework/views/
View Caching Best Practices
- Always cache views in production - No downside to view caching
- Clear view cache after Blade changes - Old compiled views won't reflect changes
- Use in CI/CD pipelines - Pre-compile views during deployment
- Monitor cache size - Compiled views can accumulate over time
View Cache Monitoring Command
// app/Console/Commands/MonitorViewCache.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
class MonitorViewCache extends Command
{
protected $signature = 'view:monitor-cache';
protected $description = 'Monitor view cache usage';
public function handle()
{
$path = storage_path('framework/views');
$files = File::files($path);
$totalSize = 0;
$oldestFile = null;
$newestFile = null;
foreach ($files as $file) {
$size = $file->getSize();
$totalSize += $size;
if (!$oldestFile || $file->getMTime() < $oldestFile['time']) {
$oldestFile = [
'name' => $file->getFilename(),
'time' => $file->getMTime(),
'size' => $size,
];
}
if (!$newestFile || $file->getMTime() > $newestFile['time']) {
$newestFile = [
'name' => $file->getFilename(),
'time' => $file->getMTime(),
'size' => $size,
];
}
}
$this->info('π View Cache Statistics:');
$this->table(['Metric', 'Value'], [
['Total Files', count($files)],
['Total Size', round($totalSize / 1024 / 1024, 2) . ' MB'],
['Oldest File', $oldestFile ? date('Y-m-d H:i:s', $oldestFile['time']) : 'N/A'],
['Newest File', $newestFile ? date('Y-m-d H:i:s', $newestFile['time']) : 'N/A'],
]);
return Command::SUCCESS;
}
}
What is Event Caching?
Event caching creates a manifest of all events and their listeners, reducing the overhead
of event discovery. This is especially useful for applications with hundreds of events.
The cache file is stored at bootstrap/cache/events.php.
How to Implement
# Cache events
php artisan event:cache
# This creates: bootstrap/cache/events.php
# Contains serialized event/listener mappings
# Clear event cache
php artisan event:clear
# List all events and listeners
php artisan event:list
# List events for specific listener
php artisan event:list --event=OrderShipped
Event Service Provider with Caching
// app/Providers/EventServiceProvider.php
namespace App\Providers;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
class EventServiceProvider extends ServiceProvider
{
/**
* The event handler mappings for the application.
*/
protected $listen = [
'App\Events\OrderShipped' => [
'App\Listeners\SendShipmentNotification',
'App\Listeners\UpdateOrderStatus',
'App\Listeners\UpdateInventory',
],
'App\Events\UserRegistered' => [
'App\Listeners\SendWelcomeEmail',
'App\Listeners\AssignDefaultRoles',
'App\Listeners\CreateUserProfile',
],
'App\Events\PaymentProcessed' => [
'App\Listeners\SendPaymentReceipt',
'App\Listeners\UpdateAccountBalance',
'App\Listeners\NotifyAccounting',
],
];
/**
* The subscriber classes to register.
*/
protected $subscribe = [
'App\Listeners\UserEventSubscriber',
'App\Listeners\OrderEventSubscriber',
];
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false; // Set to false when using event caching
}
/**
* Get the listener directories that should be used to discover events.
*/
protected function discoverEventsWithin(): array
{
return [
$this->app->path('Listeners'),
];
}
}
Combined Optimization Command
Laravel provides a single command that performs multiple optimizations at once. This should be run as part of your deployment process.
# Optimize for production
php artisan optimize
# This command does:
# 1. Clears all caches (config, route, view, event)
# 2. Rebuilds all caches
# 3. Optimizes Composer autoloader
# 4. Caches compiled services and packages
# Clear all optimizations
php artisan optimize:clear
# Check optimization status
php artisan optimize:status
Complete Production Optimization Checklist
| Step | Command | Frequency | Impact |
|---|---|---|---|
| 1. Update dependencies | composer install --no-dev --optimize-autoloader |
Every deployment | High |
| 2. Clear old caches | php artisan optimize:clear |
Every deployment | Medium |
| 3. Cache config | php artisan config:cache |
Every deployment | High |
| 4. Cache routes | php artisan route:cache |
Every deployment | High |
| 5. Cache views | php artisan view:cache |
Every deployment | Medium |
| 6. Cache events | php artisan event:cache |
Every deployment | Low |
| 7. Optimize Composer | composer dump-autoload --optimize |
Every deployment | Medium |
| 8. Restart workers | php artisan queue:restart |
Every deployment | High |
Production Deployment Script with All Optimizations
#!/bin/bash
# deploy-production.sh
set -e # Exit on error
echo "π Starting production deployment..."
# Configuration
APP_DIR="/var/www/html"
BACKUP_DIR="/backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 1. Enable maintenance mode
echo "π¦ Enabling maintenance mode..."
php artisan down --retry=60 --render="errors.503"
# 2. Backup database
echo "πΎ Backing up database..."
php artisan backup:run --only-db --quiet
cp storage/app/backup/*.zip $BACKUP_DIR/backup_$TIMESTAMP.zip
# 3. Pull latest code
echo "π₯ Pulling latest code..."
git pull origin main
# 4. Install dependencies
echo "π¦ Installing dependencies..."
composer install --no-dev --optimize-autoloader --no-interaction
# 5. Run database migrations
echo "π Running migrations..."
php artisan migrate --force
# 6. Clear all caches
echo "π§Ή Clearing caches..."
php artisan optimize:clear
# 7. Cache everything
echo "β‘ Caching configurations..."
php artisan optimize
# 8. Restart queue workers
echo "π Restarting queue workers..."
php artisan queue:restart
# 9. Clear opcache (if using OPcache)
echo "π Resetting OPcache..."
touch public/opcache-reset.php
# 10. Warm up cache (optional)
echo "π₯ Warming up cache..."
php artisan cache: warmup
# 11. Disable maintenance mode
echo "β
Disabling maintenance mode..."
php artisan up
echo "β
Deployment completed successfully at $(date)"
# Monitor logs
echo "π Monitoring logs (Ctrl+C to exit)..."
tail -f storage/logs/laravel.log
Cache Warmup Command
// app/Console/Commands/WarmupCache.php
namespace App\Console\Commands;
use App\Models\Post;
use App\Models\User;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
class WarmupCache extends Command
{
protected $signature = 'cache:warmup';
protected $description = 'Warm up the application cache';
public function handle()
{
$this->info('π₯ Warming up cache...');
// Warm up popular queries
$this->warmupUsers();
$this->warmupPosts();
$this->warmupConfig();
$this->info('β
Cache warmup completed!');
return Command::SUCCESS;
}
protected function warmupUsers()
{
$this->info('Warming up users...');
// Cache active users
$users = User::where('active', true)
->with('profile')
->take(100)
->get();
foreach ($users as $user) {
Cache::put('user.' . $user->id, $user, now()->addHours(24));
}
$this->line(' Cached ' . count($users) . ' users');
}
protected function warmupPosts()
{
$this->info('Warming up posts...');
// Cache recent posts
$posts = Post::with('user', 'comments')
->latest()
->take(200)
->get();
foreach ($posts as $post) {
Cache::put('post.' . $post->id, $post, now()->addHours(12));
}
$this->line(' Cached ' . count($posts) . ' posts');
}
protected function warmupConfig()
{
$this->info('Warming up config...');
// Cache frequently accessed config values
$configs = [
'app.name' => config('app.name'),
'app.env' => config('app.env'),
'app.url' => config('app.url'),
];
Cache::put('config.app', $configs, now()->addDay());
$this->line(' Cached application config');
}
}
8.2 Redis & Cache Invalidation β Complete Implementation Guide
What is Redis?
Redis is an in-memory data structure store used as a database, cache, and message broker. It's extremely fast (sub-millisecond latency) and perfect for Laravel caching, sessions, and queues. Redis can handle millions of requests per second with minimal hardware.
Installation on Ubuntu/Debian
# Update package list
sudo apt-get update
# Install Redis server
sudo apt-get install redis-server -y
# Start Redis and enable on boot
sudo systemctl enable redis-server
sudo systemctl start redis-server
# Check Redis status
sudo systemctl status redis-server
# Test Redis connection
redis-cli ping
# Should return: PONG
# Redis configuration file location
sudo nano /etc/redis/redis.conf
Redis Configuration for Production
# /etc/redis/redis.conf - Production settings
# Memory management
maxmemory 2gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
# Persistence
save 900 1
save 300 10
save 60 10000
# Security
requirepass your-strong-password-here
rename-command CONFIG ""
bind 127.0.0.1
# Performance
tcp-backlog 511
timeout 0
tcp-keepalive 300
# Logging
loglevel notice
logfile /var/log/redis/redis-server.log
# Snapshots
dir /var/lib/redis
dbfilename dump.rdb
# AOF persistence (optional)
appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec
Install PHP Redis Extension
# Using PECL
pecl install redis
# Ubuntu (PHP 8.2 example)
sudo apt-get install php8.2-redis
# Restart PHP-FPM
sudo systemctl restart php8.2-fpm
# Verify installation
php -m | grep redis
Laravel Redis Configuration
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', 'laravel_database_'),
'read_write_timeout' => 60,
'compression' => true,
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'read_write_timeout' => 60,
'timeout' => 5,
'retry_interval' => 100,
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
'persistent' => true, // Persistent connections for better performance
],
'sessions' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '2'),
],
'queues' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_QUEUE_DB', '3'),
'read_write_timeout' => -1, // No timeout for queue operations
],
],
Environment Configuration
# .env - Redis Configuration
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=your-strong-password-here
REDIS_PORT=6379
REDIS_CLIENT=phpredis
# Database selection (different for each use case)
REDIS_DB=0 # Default
REDIS_CACHE_DB=1
REDIS_SESSION_DB=2
REDIS_QUEUE_DB=3
# For cache driver
CACHE_DRIVER=redis
# For sessions
SESSION_DRIVER=redis
# For queues
QUEUE_CONNECTION=redis
# Redis cluster (if using Redis Cluster)
REDIS_CLUSTER=redis
REDIS_CLUSTER_SEEDS="node1:7000,node2:7001,node3:7002"
Basic Cache Operations
use Illuminate\Support\Facades\Cache;
class UserController extends Controller
{
/**
* Get user with caching
*/
public function getUser($id)
{
// Simple cache with 1 hour expiration
$user = Cache::remember('user.' . $id, 3600, function () use ($id) {
return User::with('posts', 'profile')->find($id);
});
return response()->json($user);
}
/**
* Cache with tags (for organized invalidation)
*/
public function getUserProfile($id)
{
$profile = Cache::tags(['users', 'profiles'])->remember('profile.' . $id, 3600, function () use ($id) {
return User::with('profile')->find($id);
});
return response()->json($profile);
}
/**
* Update user and clear cache
*/
public function updateUser(Request $request, $id)
{
$user = User::find($id);
$user->update($request->all());
// Clear specific cache
Cache::forget('user.' . $id);
// Clear by tag
Cache::tags(['users'])->flush();
// Clear multiple keys
Cache::deleteMultiple(['user.' . $id, 'profile.' . $id]);
return response()->json(['message' => 'User updated']);
}
}
Advanced Caching Patterns
// app/Services/CacheService.php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class CacheService
{
protected $hitCount = 0;
protected $missCount = 0;
/**
* Cache-aside pattern (lazy loading) with monitoring
*/
public function remember($key, $ttl, $callback)
{
$start = microtime(true);
if (Cache::has($key)) {
$this->hitCount++;
$value = Cache::get($key);
$hit = true;
} else {
$this->missCount++;
$value = $callback();
Cache::put($key, $value, $ttl);
$hit = false;
}
$duration = (microtime(true) - $start) * 1000;
$this->logOperation($key, $hit, $duration);
return $value;
}
/**
* Write-through cache pattern
*/
public function put($key, $value, $ttl = null)
{
Cache::put($key, $value, $ttl);
// Update database
$this->updateDatabase($key, $value);
return $value;
}
/**
* Write-behind cache pattern (async)
*/
public function putAsync($key, $value, $ttl = null)
{
Cache::put($key, $value, $ttl);
// Queue database update
\App\Jobs\UpdateDatabaseJob::dispatch($key, $value);
return $value;
}
/**
* Cache warming for popular items
*/
public function warmUp(array $keys)
{
$start = microtime(true);
$warmed = 0;
foreach ($keys as $key => $callback) {
if (!Cache::has($key)) {
$value = $callback();
Cache::put($key, $value, now()->addHours(24));
$warmed++;
}
}
Log::info('Cache warmed up', [
'warmed' => $warmed,
'total' => count($keys),
'duration_ms' => round((microtime(true) - $start) * 1000, 2),
]);
return $warmed;
}
/**
* Distributed lock implementation
*/
public function withLock($key, $callback, $timeout = 10)
{
$lock = Cache::lock($key, $timeout);
if ($lock->get()) {
try {
return $callback();
} finally {
$lock->release();
}
}
throw new \Exception('Could not acquire lock for key: ' . $key);
}
/**
* Get cache statistics
*/
public function getStats()
{
$total = $this->hitCount + $this->missCount;
$hitRate = $total > 0 ? round(($this->hitCount / $total) * 100, 2) : 0;
return [
'hits' => $this->hitCount,
'misses' => $this->missCount,
'total' => $total,
'hit_rate' => $hitRate . '%',
];
}
protected function logOperation($key, $hit, $duration)
{
Log::channel('cache')->info('Cache operation', [
'key' => $key,
'hit' => $hit,
'duration_ms' => round($duration, 2),
'memory_mb' => round(memory_get_usage() / 1024 / 1024, 2),
]);
}
protected function updateDatabase($key, $value)
{
// Implement database update logic
}
}
Cache Tag Management with Model Events
// app/Models/Post.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Cache;
class Post extends Model
{
protected $fillable = ['title', 'content', 'user_id', 'category_id'];
protected static function booted()
{
static::saved(function ($post) {
// Clear related caches when post is updated
Cache::tags(['posts', 'user.' . $post->user_id])->flush();
// Clear specific post cache
Cache::forget('post.' . $post->id);
// Clear list caches
Cache::forget('posts.recent');
Cache::forget('posts.popular');
// Clear category cache if changed
if ($post->wasChanged('category_id')) {
Cache::forget('category.' . $post->category_id);
}
});
static::deleted(function ($post) {
// Clear all related caches
Cache::tags(['posts'])->flush();
// Clear user's post cache
Cache::forget('user.posts.' . $post->user_id);
});
}
/**
* Get cached posts for user
*/
public static function getCachedForUser($userId)
{
return Cache::tags(['posts', 'user.' . $userId])
->remember('user.posts.' . $userId, 3600, function () use ($userId) {
return self::where('user_id', $userId)
->with('category')
->latest()
->get();
});
}
/**
* Get cached recent posts
*/
public static function getCachedRecent($limit = 10)
{
return Cache::tags(['posts'])->remember('posts.recent.' . $limit, 1800, function () use ($limit) {
return self::with('user', 'category')
->latest()
->take($limit)
->get();
});
}
}
// app/Models/User.php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Support\Facades\Cache;
class User extends Authenticatable
{
protected static function booted()
{
static::saved(function ($user) {
// Clear user cache
Cache::forget('user.' . $user->id);
// Clear users list cache
Cache::forget('users.active');
});
}
/**
* Get cached user with profile
*/
public static function getCachedWithProfile($id)
{
return Cache::tags(['users', 'profiles'])
->remember('user.' . $id, 3600, function () use ($id) {
return self::with('profile')->find($id);
});
}
}
Controller with Cache Implementation
// app/Http/Controllers/PostController.php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Services\CacheService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
class PostController extends Controller
{
protected $cacheService;
public function __construct(CacheService $cacheService)
{
$this->cacheService = $cacheService;
}
/**
* Display a listing of posts with caching
*/
public function index(Request $request)
{
$page = $request->get('page', 1);
$limit = $request->get('limit', 15);
$posts = Cache::tags(['posts'])->remember('posts.page.' . $page . '.limit.' . $limit, 1800, function () use ($limit) {
return Post::with('user', 'category')
->latest()
->paginate($limit);
});
return view('posts.index', compact('posts'));
}
/**
* Display the specified post with caching
*/
public function show($id)
{
// Try to get from cache first
$post = Cache::tags(['posts', 'post.' . $id])->remember('post.' . $id, 3600, function () use ($id) {
return Post::with(['user', 'category', 'comments.user'])
->findOrFail($id);
});
// Increment view count asynchronously
if (!Cache::has('post.viewed.' . $id . '.' . request()->ip())) {
Cache::put('post.viewed.' . $id . '.' . request()->ip(), true, 60);
\App\Jobs\IncrementPostViews::dispatch($id);
}
return view('posts.show', compact('post'));
}
/**
* Store a newly created post
*/
public function store(Request $request)
{
$post = Post::create($request->validated());
// Clear relevant caches
Cache::tags(['posts'])->flush();
Cache::forget('user.posts.' . auth()->id());
return redirect()->route('posts.index')
->with('success', 'Post created successfully.');
}
/**
* Update the specified post
*/
public function update(Request $request, Post $post)
{
$post->update($request->validated());
// Clear specific post cache
Cache::forget('post.' . $post->id);
Cache::tags(['posts'])->flush();
return redirect()->route('posts.show', $post)
->with('success', 'Post updated successfully.');
}
/**
* Remove the specified post
*/
public function destroy(Post $post)
{
$post->delete();
// Clear all related caches
Cache::forget('post.' . $post->id);
Cache::tags(['posts'])->flush();
Cache::forget('user.posts.' . $post->user_id);
return redirect()->route('posts.index')
->with('success', 'Post deleted successfully.');
}
}
1. Time-Based Invalidation (TTL)
The simplest strategy - cache expires after a set time. Good for data that doesn't need real-time updates.
// Cache expires after specified time
Cache::put('key', 'value', now()->addMinutes(10));
Cache::remember('key', 600, fn() => 'value'); // 600 seconds
// Different TTLs for different data types
Cache::put('user.' . $id, $user, now()->addHours(24)); // User data - longer TTL
Cache::put('post.' . $id, $post, now()->addHours(12)); // Posts - medium TTL
Cache::put('comment.' . $id, $comment, now()->addMinutes(30)); // Comments - short TTL
2. Event-Based Invalidation
Invalidate cache when related data changes. Provides real-time consistency.
// app/Listeners/InvalidatePostCache.php
namespace App\Listeners;
use App\Events\PostUpdated;
use App\Events\PostCreated;
use App\Events\PostDeleted;
use Illuminate\Support\Facades\Cache;
class InvalidatePostCache
{
/**
* Handle post created events
*/
public function handlePostCreated(PostCreated $event)
{
$this->invalidateAllPostCaches($event->post);
}
/**
* Handle post updated events
*/
public function handlePostUpdated(PostUpdated $event)
{
$this->invalidateSpecificPostCache($event->post);
$this->invalidateListCaches();
$this->invalidateUserPostCaches($event->post->user_id);
}
/**
* Handle post deleted events
*/
public function handlePostDeleted(PostDeleted $event)
{
$this->invalidateSpecificPostCache($event->post);
$this->invalidateListCaches();
$this->invalidateUserPostCaches($event->post->user_id);
}
/**
* Invalidate specific post cache
*/
protected function invalidateSpecificPostCache($post)
{
Cache::forget('post.' . $post->id);
Cache::tags(['post.' . $post->id])->flush();
logger()->info('Post cache invalidated', ['post_id' => $post->id]);
}
/**
* Invalidate all post list caches
*/
protected function invalidateListCaches()
{
Cache::tags(['posts'])->flush();
Cache::forget('posts.recent');
Cache::forget('posts.popular');
Cache::forget('posts.featured');
}
/**
* Invalidate user's post caches
*/
protected function invalidateUserPostCaches($userId)
{
Cache::forget('user.posts.' . $userId);
Cache::tags(['user.' . $userId])->flush();
}
/**
* Invalidate all post caches (heavy operation)
*/
protected function invalidateAllPostCaches($post = null)
{
Cache::tags(['posts'])->flush();
if ($post) {
Cache::forget('post.' . $post->id);
}
}
}
// Register listeners in EventServiceProvider
protected $listen = [
PostCreated::class => [
'App\Listeners\InvalidatePostCache@handlePostCreated',
],
PostUpdated::class => [
'App\Listeners\InvalidatePostCache@handlePostUpdated',
],
PostDeleted::class => [
'App\Listeners\InvalidatePostCache@handlePostDeleted',
],
];
3. Version-Based Invalidation
Use version numbers to invalidate cache without tracking individual keys. Perfect for APIs and read-heavy applications.
// app/Models/Post.php
class Post extends Model
{
/**
* Get cache version for this post
*/
public function getCacheVersion()
{
return Cache::remember('post.version.' . $this->id, 86400, function () {
return $this->updated_at->timestamp;
});
}
/**
* Increment cache version when post changes
*/
public function incrementCacheVersion()
{
Cache::increment('post.version.' . $this->id);
Cache::forget('post.version.' . $this->id); // Alternative: delete and let next request recalculate
}
}
// app/Models/Category.php
class Category extends Model
{
protected static function booted()
{
static::saved(function ($category) {
// Increment version for all posts in this category
Cache::increment('category.posts.version.' . $category->id);
});
}
/**
* Get version for category posts
*/
public function getPostsVersion()
{
return Cache::remember('category.posts.version.' . $this->id, 3600, function () {
return now()->timestamp;
});
}
}
// Version-based caching in controller
class PostController extends Controller
{
public function show($id)
{
$post = Post::findOrFail($id);
$version = $post->getCacheVersion();
// Cache with version in key - automatically invalidates when version changes
return Cache::remember("post.{$id}.v{$version}", 3600, function () use ($post) {
return $post->load(['user', 'comments', 'category']);
});
}
public function index(Request $request)
{
$category = Category::find($request->category_id);
$version = $category ? $category->getPostsVersion() : 'all';
return Cache::remember("posts.list.category.{$version}." . md5(json_encode($request->all())), 1800, function () use ($request) {
return Post::with('user', 'category')
->filter($request->all())
->paginate(15);
});
}
public function update(Request $request, $id)
{
$post = Post::findOrFail($id);
$oldCategoryId = $post->category_id;
$post->update($request->all());
// Invalidate by incrementing versions
$post->incrementCacheVersion();
if ($oldCategoryId != $post->category_id) {
// Invalidate both old and new category post lists
Category::find($oldCategoryId)?->incrementPostsVersion();
Category::find($post->category_id)?->incrementPostsVersion();
}
return response()->json($post);
}
}
4. Tag-Based Invalidation (Redis Only)
Redis tags allow you to group related cache items and invalidate them together. Perfect for complex relationships and hierarchical data.
// Setting up tag-based caching
class ProductController extends Controller
{
public function show($id)
{
// Cache product with tags for category and brand
$product = Cache::tags(['products', 'category.' . $product->category_id, 'brand.' . $product->brand_id])
->remember('product.' . $id, 3600, function () use ($id) {
return Product::with(['category', 'brand', 'reviews'])->findOrFail($id);
});
return response()->json($product);
}
public function byCategory($categoryId)
{
// Cache category product list
$products = Cache::tags(['products', 'category.' . $categoryId])
->remember('category.products.' . $categoryId, 1800, function () use ($categoryId) {
return Product::where('category_id', $categoryId)
->with('brand')
->paginate(20);
});
return response()->json($products);
}
public function update(Request $request, $id)
{
$product = Product::findOrFail($id);
$oldCategoryId = $product->category_id;
$oldBrandId = $product->brand_id;
$product->update($request->all());
// Invalidate using tags
Cache::tags(['product.' . $id])->flush();
if ($oldCategoryId != $product->category_id) {
Cache::tags(['category.' . $oldCategoryId, 'category.' . $product->category_id])->flush();
}
if ($oldBrandId != $product->brand_id) {
Cache::tags(['brand.' . $oldBrandId, 'brand.' . $product->brand_id])->flush();
}
return response()->json($product);
}
}
5. Cache Invalidation Patterns Comparison
| Strategy | Pros | Cons | Best For | Implementation Complexity |
|---|---|---|---|---|
| Time-based (TTL) | Simple, automatic, no tracking needed | Stale data until expiry, can't guarantee freshness | Infrequently changing data, static content | β Low |
| Event-based | Immediate consistency, precise control | More complex, event overhead, must track all changes | Frequently updated data, real-time systems | βββ Medium |
| Version-based | Perfect consistency, no TTL needed, atomic | Extra version lookups, version storage overhead | APIs, read-heavy apps, distributed systems | ββ Medium |
| Tag-based | Group invalidation, efficient for relationships | Redis-only feature, can't use with other drivers | Related data sets, hierarchical data | ββ Medium |
| Write-through | Always consistent, simple read logic | Slower writes, write amplification | Read-heavy with occasional writes | ββ Medium |
| Write-behind | Fast writes, eventual consistency | May have stale reads, queue dependency | High-write scenarios, analytics | βββ Medium-High |
- Time-based TTL for rarely changed reference data
- Event-based for user profiles and frequently updated content
- Version-based for API responses
- Tag-based for complex relationships and lists
Configuring Redis Sessions
Using Redis for sessions enables shared sessions across multiple servers, faster session access, and automatic session expiration.
// config/session.php
return [
'driver' => env('SESSION_DRIVER', 'redis'),
'connection' => 'sessions', // Use the sessions Redis connection
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
'encrypt' => env('SESSION_ENCRYPT', false),
'lottery' => [2, 100],
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
'path' => '/',
'domain' => env('SESSION_DOMAIN'),
'secure' => env('SESSION_SECURE_COOKIE'),
'http_only' => true,
'same_site' => 'lax',
];
Benefits of Redis Sessions
- Shared sessions across multiple servers - Essential for load-balanced applications
- Fast read/write - Much faster than database sessions (microseconds vs milliseconds)
- Automatic expiration - Redis handles TTL automatically with minimal overhead
- Persistence options - Can persist sessions across restarts with RDB/AOF
- Atomic operations - Redis supports atomic session operations
- Scalability - Can handle millions of concurrent sessions
Session Monitoring and Management
// app/Console/Commands/MonitorSessions.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class MonitorSessions extends Command
{
protected $signature = 'sessions:monitor {--user-id=} {--list} {--kill=}';
protected $description = 'Monitor and manage active sessions';
public function handle()
{
if ($this->option('kill')) {
return $this->killSession($this->option('kill'));
}
if ($this->option('list')) {
return $this->listSessions();
}
if ($this->option('user-id')) {
return $this->showUserSessions($this->option('user-id'));
}
return $this->showStats();
}
protected function showStats()
{
$info = Redis::connection('sessions')->info();
$this->info('π Session Statistics:');
$this->table(['Metric', 'Value'], [
['Total Sessions', Redis::connection('sessions')->dbsize()],
['Memory Used', $this->formatBytes($info['used_memory'])],
['Memory Peak', $this->formatBytes($info['used_memory_peak'])],
['Connected Clients', $info['connected_clients']],
['Total Connections', $info['total_connections_received']],
['Commands Processed', $info['total_commands_processed']],
['Keyspace Hits', $info['keyspace_hits']],
['Keyspace Misses', $info['keyspace_misses']],
['Hit Rate', $this->calculateHitRate($info) . '%'],
]);
return Command::SUCCESS;
}
protected function listSessions()
{
$keys = Redis::connection('sessions')->keys('*');
$sessions = [];
foreach ($keys as $key) {
$data = Redis::connection('sessions')->get($key);
$payload = unserialize($data);
$sessions[] = [
'session_id' => str_replace('laravel_database_sessions_', '', $key),
'user_id' => $payload['user_id'] ?? 'Guest',
'ip' => $payload['ip_address'] ?? 'Unknown',
'last_activity' => date('Y-m-d H:i:s', $payload['last_activity'] ?? 0),
'user_agent' => substr($payload['user_agent'] ?? 'Unknown', 0, 50),
];
}
if (empty($sessions)) {
$this->warn('No active sessions found.');
return Command::SUCCESS;
}
$this->table(['Session ID', 'User ID', 'IP', 'Last Activity', 'User Agent'], $sessions);
$this->info('Total active sessions: ' . count($sessions));
return Command::SUCCESS;
}
protected function showUserSessions($userId)
{
$keys = Redis::connection('sessions')->keys('*');
$sessions = [];
foreach ($keys as $key) {
$data = Redis::connection('sessions')->get($key);
$payload = unserialize($data);
if (($payload['user_id'] ?? null) == $userId) {
$sessions[] = [
'session_id' => str_replace('laravel_database_sessions_', '', $key),
'ip' => $payload['ip_address'] ?? 'Unknown',
'last_activity' => date('Y-m-d H:i:s', $payload['last_activity'] ?? 0),
'user_agent' => substr($payload['user_agent'] ?? 'Unknown', 0, 50),
];
}
}
if (empty($sessions)) {
$this->warn("No active sessions found for user ID: {$userId}");
return Command::SUCCESS;
}
$this->table(['Session ID', 'IP', 'Last Activity', 'User Agent'], $sessions);
$this->info("Total sessions for user {$userId}: " . count($sessions));
return Command::SUCCESS;
}
protected function killSession($sessionId)
{
$key = 'laravel_database_sessions_' . $sessionId;
if (Redis::connection('sessions')->exists($key)) {
Redis::connection('sessions')->del($key);
$this->info("Session {$sessionId} has been terminated.");
} else {
$this->error("Session {$sessionId} not found.");
}
return Command::SUCCESS;
}
protected function formatBytes($bytes)
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes > 1024) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
protected function calculateHitRate($info)
{
$hits = $info['keyspace_hits'] ?? 0;
$misses = $info['keyspace_misses'] ?? 0;
$total = $hits + $misses;
if ($total === 0) {
return 0;
}
return round(($hits / $total) * 100, 2);
}
}
Middleware for Session Monitoring
// app/Http/Middleware/SessionMonitor.php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class SessionMonitor
{
public function handle($request, Closure $next)
{
$response = $next($request);
if (app()->environment('production')) {
$this->trackSession($request);
}
return $response;
}
protected function trackSession($request)
{
if ($request->hasSession() && $request->session()->isStarted()) {
$sessionId = $request->session()->getId();
// Update session metadata
Redis::connection('sessions')->hset(
'session_metadata:' . $sessionId,
'last_activity',
time()
);
// Track unique visitors (simplified)
Redis::connection('sessions')->pfadd('unique_visitors:today', $request->ip());
}
}
}
Redis Commands for Monitoring
# Check Redis server info
redis-cli INFO
# Monitor real-time commands (use carefully in production)
redis-cli MONITOR
# Check memory usage
redis-cli INFO memory
# List all keys (avoid in production with many keys)
redis-cli KEYS *
# Check key TTL
redis-cli TTL keyname
# Check Redis slow log
redis-cli SLOWLOG GET 10
# Check client list
redis-cli CLIENT LIST
# Check database size
redis-cli DBSIZE
# Check Redis health
redis-cli PING
# Flush all databases (careful!)
redis-cli FLUSHALL
# Flush current database
redis-cli FLUSHDB
# Get Redis config
redis-cli CONFIG GET *
# Set Redis config (temporary)
redis-cli CONFIG SET maxmemory 2gb
Laravel Redis Monitoring Command
// app/Console/Commands/RedisMonitor.php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Redis;
class RedisMonitor extends Command
{
protected $signature = 'redis:monitor {--alert-threshold=1000} {--memory-warning=80}';
protected $description = 'Monitor Redis performance and health';
public function handle()
{
$info = Redis::info();
$this->displayServerInfo($info);
$this->displayMemoryInfo($info);
$this->displayStats($info);
$this->checkAlerts($info);
return Command::SUCCESS;
}
protected function displayServerInfo($info)
{
$this->info('π Redis Server Information:');
$this->table(['Metric', 'Value'], [
['Version', $info['Server']['redis_version']],
['Uptime', $info['Server']['uptime_in_seconds'] . ' seconds (' . round($info['Server']['uptime_in_seconds'] / 86400, 2) . ' days)'],
['Mode', $info['Server']['redis_mode']],
['OS', $info['Server']['os']],
['Process ID', $info['Server']['process_id']],
['TCP Port', $info['Server']['tcp_port']],
]);
}
protected function displayMemoryInfo($info)
{
$usedMemory = $info['Memory']['used_memory'];
$usedMemoryHuman = $this->formatBytes($usedMemory);
$maxMemory = $info['Memory']['maxmemory'] ?? 0;
$maxMemoryHuman = $maxMemory > 0 ? $this->formatBytes($maxMemory) : 'No limit';
$usedMemoryPeak = $info['Memory']['used_memory_peak'];
$usedMemoryPeakHuman = $this->formatBytes($usedMemoryPeak);
$memFragmentation = $info['Memory']['mem_fragmentation_ratio'] ?? 'N/A';
$this->info('π Redis Memory Information:');
$this->table(['Metric', 'Value'], [
['Used Memory', $usedMemoryHuman],
['Peak Memory', $usedMemoryPeakHuman],
['Max Memory', $maxMemoryHuman],
['Fragmentation Ratio', $memFragmentation],
['Memory Usage %', $maxMemory > 0 ? round(($usedMemory / $maxMemory) * 100, 2) . '%' : 'N/A'],
]);
}
protected function displayStats($info)
{
$stats = $info['Stats'];
$hitRate = $this->calculateHitRate($info);
$this->info('π Redis Statistics:');
$this->table(['Metric', 'Value'], [
['Connected Clients', $info['Clients']['connected_clients']],
['Total Connections', $stats['total_connections_received']],
['Total Commands', $stats['total_commands_processed']],
['Keyspace Hits', $stats['keyspace_hits']],
['Keyspace Misses', $stats['keyspace_misses']],
['Hit Rate', $hitRate . '%'],
['Expired Keys', $stats['expired_keys']],
['Evicted Keys', $stats['evicted_keys']],
]);
// Display keyspace info
$this->info('ποΈ Keyspace Information:');
$keyspace = [];
foreach ($info as $key => $value) {
if (str_starts_with($key, 'db')) {
$keyspace[] = [$key, $value];
}
}
if (!empty($keyspace)) {
$this->table(['Database', 'Info'], $keyspace);
} else {
$this->line('No keyspace information available.');
}
}
protected function checkAlerts($info)
{
$threshold = $this->option('alert-threshold');
$memoryWarning = $this->option('memory-warning');
$alerts = [];
// Check connected clients
if ($info['Clients']['connected_clients'] > $threshold) {
$alerts[] = "β οΈ High number of connected clients: {$info['Clients']['connected_clients']}";
}
// Check hit rate
$hitRate = $this->calculateHitRate($info);
if ($hitRate < 50) {
$alerts[] = "β οΈ Low cache hit rate: {$hitRate}%";
}
// Check memory usage
$maxMemory = $info['Memory']['maxmemory'] ?? 0;
if ($maxMemory > 0) {
$usedMemory = $info['Memory']['used_memory'];
$usagePercent = ($usedMemory / $maxMemory) * 100;
if ($usagePercent > $memoryWarning) {
$alerts[] = "β οΈ High memory usage: {$usagePercent}%";
}
}
// Check for evictions
if ($info['Stats']['evicted_keys'] > 0) {
$alerts[] = "β οΈ Keys are being evicted: {$info['Stats']['evicted_keys']} evictions";
}
if (!empty($alerts)) {
$this->warn('π¨ Alerts:');
foreach ($alerts as $alert) {
$this->warn(' ' . $alert);
}
} else {
$this->info('β
Redis is healthy!');
}
}
protected function formatBytes($bytes)
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = 0;
while ($bytes > 1024 && $i < count($units) - 1) {
$bytes /= 1024;
$i++;
}
return round($bytes, 2) . ' ' . $units[$i];
}
protected function calculateHitRate($info)
{
$hits = $info['Stats']['keyspace_hits'] ?? 0;
$misses = $info['Stats']['keyspace_misses'] ?? 0;
$total = $hits + $misses;
if ($total === 0) {
return 0;
}
return round(($hits / $total) * 100, 2);
}
}
Cache Hit Rate Optimization Tips
- Aim for 90%+ hit rate - Low hit rate means cache isn't effective
- Analyze cache misses - Log and review what's not being cached
- Adjust TTLs - Longer TTLs for stable data, shorter for volatile data
- Pre-warm cache - Load popular items on deployment
- Use appropriate cache strategies - Different data needs different strategies
- Monitor cache churn - Too many writes/evictions indicates problems
- Size your cache properly - Ensure maxmemory is set appropriately
Redis Performance Tuning
# /etc/redis/redis.conf - Performance tuning
# Memory management
maxmemory 80% # Use percentage of available RAM
maxmemory-policy volatile-lru # Remove least recently used keys with TTL
maxmemory-samples 10 # Accuracy vs performance trade-off
# Persistence (tune for performance vs durability)
save "" # Disable RDB if not needed
appendonly no # Disable AOF if not needed
# Connection handling
timeout 300 # Close idle connections after 300 seconds
tcp-keepalive 60 # Check connection health
maxclients 10000 # Maximum concurrent clients
# Lazy freeing (Redis 4.0+)
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
replica-lazy-flush yes
# I/O threads (Redis 6.0+)
io-threads 4
io-threads-do-reads yes
# Slow log
slowlog-log-slower-than 10000 # Log queries slower than 10ms
slowlog-max-len 128
π Module 03 : Database, Eloquent & Data Modeling Successfully Completed
You have successfully completed this module of Laravel Framework Development.
Keep building your expertise step by step β Learn Next Module β
Testing, Deployment & DevOps
Building applications is only half the job.Testing, deployment, and DevOps practices ensure your Laravel applications are reliable, scalable, and safe in production. In this module from NotesTime.in, you will learn how professionals test Laravel apps, deploy them to servers, automate workflows, and move projects from local to live environments.
9.1 Unit, Feature & Integration Testing
Testing ensures your application behaves correctly before it reaches production. Laravel provides first-class support for automated testing.
- Unit Tests β Test individual classes or methods
- Feature Tests β Test HTTP requests and responses
- Integration Tests β Test multiple components together
9.2 PHPUnit, Pest & Mocking
Laravel uses PHPUnit by default and also supports the modern Pest testing framework.
| Tool | Purpose |
|---|---|
| PHPUnit | Traditional, powerful testing framework |
| Pest | Cleaner syntax, developer-friendly |
| Mocking | Simulate services like APIs, mail, queues |
9.3 Server Setup (Apache, Nginx, PHP-FPM)
Laravel applications run on Linux servers using modern web server stacks.
- Apache β Easy configuration, .htaccess support
- Nginx β High performance, event-driven
- PHP-FPM β Fast PHP process manager
9.4 Dockerizing Laravel Apps
Docker allows you to package your Laravel application with its environment into containers.
- Consistent environments
- No dependency conflicts
- Easy scaling
9.5 CI/CD Pipelines (GitHub Actions)
CI/CD automates testing and deployment whenever code is pushed to a repository.
- Code pushed to GitHub
- Tests run automatically
- Build and deploy to server
9.6 Laravel Project Offline to Online Deployment
Deploying a Laravel application from a local (offline) environment to a live production server is a critical phase in the software lifecycle. A poorly executed deployment can cause downtime, broken features, security issues, or data loss. This section explains a safe, professional, and repeatable Laravel deployment process used in real-world projects.
Move code from local β server with optimized performance, correct configuration, and zero unexpected errors.
1οΈβ£ Pre-Deployment Preparation
Before uploading your Laravel project, the application must be prepared for a production environment. Production servers differ from local machines in operating system, PHP extensions, permissions, and performance constraints.
- Ensure .env is configured for production
- Disable debug mode (
APP_DEBUG=false) - Verify database credentials
- Confirm required PHP extensions are installed
2οΈβ£ Composer Dependency Management
Composer manages Laravelβs dependencies. In production, only required packages should be installed. Development packages increase attack surface and memory usage.
composer install --no-dev --optimize-autoloader
composer dump-autoload -o
- --no-dev β excludes testing and debugging packages
- --optimize-autoloader β faster class loading
- dump-autoload -o β optimized class map
composer update in production
unless you fully understand the dependency impact.
3οΈβ£ Clearing Old Cache & Compiled Files
Laravel aggressively caches configuration, routes, and views. Old cached files can cause runtime errors after code or environment changes.
php artisan cache:clear
php artisan config:clear
php artisan route:clear
php artisan view:clear
php artisan event:clear
php artisan clear-compiled
4οΈβ£ Production Optimization & Caching
After clearing old cache, Laravel should be optimized for speed and performance. Cached configuration and routes significantly reduce request execution time.
php artisan config:cache
php artisan route:cache
php artisan event:cache
php artisan optimize
- Configuration cached into a single file
- Routes precompiled for faster matching
- Event listeners optimized
5οΈβ£ Queue & Worker Restart
Queue workers load application code into memory. After deployment, workers must be restarted to load the latest version of the codebase.
php artisan queue:restart
6οΈβ£ File Permissions & Storage
Laravel requires write access to specific directories. Incorrect permissions are a common cause of production deployment failures.
storage/β logs, cache, sessionsbootstrap/cache/β optimized framework files
7οΈβ£ Safe Deployment Checklist
- Backup database
- Upload code to server
- Install production dependencies
- Clear old cache
- Optimize application
- Restart queues
- Verify application health
A structured deployment process prevents crashes, security leaks, and performance issues in production Laravel applications.
8οΈβ£ All-in-One Production Deployment Command
In some hosting environments, PHP extensions or system libraries may differ from local development machines. The following command sequence is a safe, structured, and commonly used production deployment flow that clears old cache, optimizes the application, and restarts background workers.
composer update --ignore-platform-reqs
composer dump-autoload
composer update
php artisan queue:restart
php artisan cache:clear
php artisan route:clear
php artisan config:clear
php artisan view:clear
php artisan event:clear
php artisan clear-compiled
php artisan route:cache
php artisan config:cache
php artisan event:cache
php artisan optimize
- ignore-platform-reqs β bypasses local vs server PHP differences
- queue:restart β reloads workers with latest code
- clear commands β removes stale cached files
- cache commands β rebuilds optimized caches
- optimize β final performance boost
Clean deployment, fresh cache, optimized performance, and zero stale code running in production.