Laravel Advanced Features: Database, Auth, Queues, Events, and the Service Container
Use Laravel's advanced application-building tools — migrations, factories/seeders, authentication and authorization, queues and events, notifications, caching, scheduling, file storage, testing, API resources, and the service container — to build a complete, production-shaped application.
Once routing, controllers, Blade, and basic Eloquent are comfortable, the rest of Laravel's feature set is what turns a simple CRUD app into a production-grade application: reliably managed database schema, real authentication and fine-grained authorization, background job processing, decoupled event-driven code, notifications across multiple channels, caching, scheduled tasks, file storage abstractions, automated tests, clean API responses, and a service container that ties custom services together. Each of these is a broad topic on its own, so this lesson deliberately moves faster and shows one or two sharp, realistic examples per area rather than exhaustive coverage — enough to recognize each tool, know when to reach for it, and know where to dig deeper.
Migrations, Factories, and Seeders
A migration is a version-controlled PHP class describing one change to the database schema — creating a table, adding a column, adding an index — using Laravel's schema builder instead of hand-written, database-specific SQL. Running php artisan migrate applies every migration that hasn't run yet, in order, and php artisan migrate:rollback reverses the most recent batch; because migrations live in the codebase, every developer and every environment (local, staging, production) can reach an identical schema by running the same commands, rather than manually keeping databases in sync.
Factories define how to generate a fake-but-realistic instance of a model, typically using the Faker library under the hood, and are indispensable for both automated tests and quickly populating a local database with realistic-looking data. A seeder is a class that uses factories (or explicit data) to actually insert rows — php artisan db:seed runs them, commonly to set up default roles, an admin user, or sample content in a fresh environment.
• Migrations: version-controlled schema changes, applied with php artisan migrate, reversed with migrate:rollback. • Schema builder methods like $table->string('title'), $table->foreignId('user_id')->constrained() avoid hand-written, DB-specific SQL. • Factories generate realistic fake model instances (via Faker) for tests and local seeding. • Seeders (php artisan db:seed) use factories or fixed data to populate a database, e.g. with a default admin user.
Authentication and Authorization: Guards, Gates, and Policies
Laravel doesn't force you to hand-write login forms and password hashing: starter kits like Laravel Breeze scaffold a complete, working authentication flow (registration, login, password reset, email verification) in minutes, Laravel Fortify provides the same backend logic headlessly for a custom frontend, and Laravel Sanctum issues lightweight API tokens for SPA or mobile clients authenticating against the same application. Once set up, the Auth facade and helpers like auth()->user(), auth()->check(), and Auth::attempt($credentials) are how the rest of the app interacts with the currently logged-in user.
Authentication answers 'who are you?'; authorization answers 'are you allowed to do this?' — Laravel separates the two cleanly with Gates and Policies. A Gate is a simple closure-based rule for a single action, defined in AppServiceProvider and checked with Gate::allows('edit-settings') or the Blade directive @can('edit-settings'). A Policy groups all authorization rules for one specific model into a dedicated class (php artisan make:policy PostPolicy --model=Post), with methods like update(User $user, Post $post): bool — checked in a controller with $this->authorize('update', $post), which automatically throws a 403 response if the check fails.
• Breeze/Fortify: scaffold or provide login/register/password-reset flows; Sanctum: lightweight API token authentication. • Auth::attempt(), auth()->user(), auth()->check() — the everyday Auth facade API. • Gates: closure-based rules for a single action (Gate::allows('edit-settings')). • Policies: a class of rules scoped to one model ($this->authorize('update', $post)), auto-403 on failure.
Queues and Jobs
A Job is a class representing one unit of work meant to run outside the normal request/response cycle — sending a welcome email, generating a PDF invoice, calling a slow third-party API — created with php artisan make:job ProcessPodcast and dispatched with ProcessPodcast::dispatch($podcast);. Dispatching pushes a serialized job onto a queue (backed by a database table, Redis, or a managed service like Amazon SQS) instead of running it immediately, so the user's request returns fast while the actual work happens separately.
A queue worker — a long-running process started with php artisan queue:work — continuously pulls jobs off the queue and executes them. If a job throws an exception, Laravel retries it automatically (a configurable number of times) before marking it as failed and storing it in a failed_jobs table, from which it can be inspected and retried manually with php artisan queue:retry. In production, a process manager (like Supervisor) keeps queue workers running continuously and restarts them if they crash.
• php artisan make:job Name creates a job; Name::dispatch($data) queues it instead of running inline. • php artisan queue:work runs a worker process that pulls and executes queued jobs. • Failed jobs are retried automatically (configurable), then logged to failed_jobs for inspection/manual retry. • Production queue workers are kept alive by a process supervisor (e.g. Supervisor).
Events, Listeners, and Notifications
Events and listeners decouple 'something happened' from 'here's everything that should happen as a result'. An event class (like OrderShipped) is fired with event(new OrderShipped($order)) or OrderShipped::dispatch($order), and any number of listener classes registered for that event run in response — one might send an email, another might update analytics, another might notify a warehouse system — without the code that fired the event needing to know any of that happens. This keeps a controller focused on 'an order shipped' rather than accumulating a growing list of unrelated side effects inline.
Notifications are Laravel's unified way to alert a user across multiple channels — mail, database (for an in-app notification bell), SMS, or broadcast (real-time, via websockets) — from a single notification class. A class extending Notification defines a via() method returning which channels to use and a matching method (toMail(), toDatabase(), toArray()) per channel; sending it is as simple as $user->notify(new InvoicePaid($invoice));, and Laravel handles dispatching it through every configured channel.
Sending plain email directly (without the Notification system) uses Mailable classes: php artisan make:mail OrderShipped --markdown=emails.orders.shipped creates a class you populate with data and send via Mail::to($user)->send(new OrderShipped($order));, rendered through its own Blade view (often using Laravel's built-in Markdown mail components for consistent styling).
• Events (event(new X())) decouple 'something happened' from listeners reacting to it. • Notifications: one class, multiple channels (mail, database, SMS, broadcast) via via() + per-channel methods. • $user->notify(new SomeNotification(...)) sends across every channel that notification defines. • Mailable classes (Mail::to($user)->send(new SomeMailable(...))) for direct, non-notification email.
Cache, Scheduling, and Filesystem Storage
The Cache facade provides a single API over any configured cache backend (file, database, Redis, Memcached): Cache::remember('top-posts', 3600, fn () => Post::orderByDesc('views')->take(10)->get()); runs the closure and caches its result for an hour only if the key isn't already cached, which is the single most common caching pattern in Laravel apps — avoiding repeated expensive queries for data that doesn't need to be perfectly real-time.
The task scheduler replaces a server full of individual cron entries with one: a single cron entry runs php artisan schedule:run every minute, and the actual schedule — what runs and how often — is defined in PHP (in routes/console.php in modern Laravel, or app/Console/Kernel.php in earlier versions) using a fluent API: Schedule::command('reports:daily')->dailyAt('01:00'); or Schedule::job(new CleanupOldRecords)->weekly();. This keeps the entire schedule versioned alongside the application code instead of scattered across server configuration.
The Storage facade abstracts file storage behind named 'disks' — a local disk on the server's filesystem, an s3 disk on Amazon S3, and others — so application code can call Storage::disk('s3')->put('avatars/1.jpg', $contents) without caring about the underlying implementation; switching an entire application from local storage to S3 in production is often just a configuration change, not a code change, because every call already goes through the same Storage abstraction.
• Cache::remember($key, $ttl, $callback) — the standard 'compute once, reuse for a while' caching pattern. • Scheduler: one cron entry (schedule:run every minute) drives a PHP-defined schedule (->dailyAt(), ->weekly(), etc.). • Storage::disk('local'|'s3'|...) abstracts file storage so the backend can change via configuration alone. • Scheduled tasks and cache usage are both defined in versioned PHP code, not ad hoc server configuration.
Testing, API Resources, and the Service Container
Laravel's testing tools let you simulate real HTTP requests against the application without a browser: a feature test extends the framework's base test case, uses the RefreshDatabase trait to reset the database to a clean migrated state before each test, and calls $this->post('/posts', [...]) or $this->get('/posts'), chaining assertions like ->assertStatus(201) or ->assertSee('Post created') directly off the response. This makes it realistic to test an entire request end to end — routing, middleware, controller, database, and response — in a single, fast, repeatable test.
When building an API, returning raw Eloquent models directly as JSON risks leaking internal columns (password hashes, timestamps you don't want exposed) and makes the response shape brittle to database changes. An API Resource class (php artisan make:resource PostResource) defines exactly which fields to expose and how, via a toArray() method, decoupling the JSON contract from the database schema — return new PostResource($post); or PostResource::collection($posts) for a list.
The service container is the object responsible for constructing class instances and their dependencies throughout the framework, and it's directly usable in application code too: $this->app->bind(PaymentGateway::class, StripeGateway::class) tells the container 'whenever something asks for the PaymentGateway interface, give it a StripeGateway instance', while $this->app->singleton(...) does the same but reuses a single shared instance for the entire request instead of constructing a new one every time. This is typically configured inside a service provider's register() method, and it's what lets a controller simply type-hint an interface in its constructor and receive a fully working implementation without ever calling new itself — making it trivial to swap StripeGateway for a fake implementation in tests.
• Feature tests: $this->get()/post() simulate real HTTP requests; RefreshDatabase resets state between tests. • API Resources (php artisan make:resource) control exactly what JSON shape a model produces, decoupled from its columns. • $this->app->bind(Interface::class, Implementation::class) — a new instance per resolution. • $this->app->singleton(...) — one shared instance reused for the life of the request/application. • Binding interfaces to implementations in a service provider makes classes depend on abstractions, easing testing and swapping.
A Migration, Factory, and Policy Check Together
->constrained()->cascadeOnDelete() sets up the foreign key and deletes posts automatically when their user is deleted. User::factory() inside PostFactory creates a related user automatically if one isn't provided.
<?php
// database/migrations/xxxx_create_posts_table.php
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('title');
$table->text('body');
$table->timestamps();
});
// database/factories/PostFactory.php
class PostFactory extends Factory
{
public function definition(): array
{
return [
'title' => fake()->sentence(),
'body' => fake()->paragraphs(3, true),
'user_id' => User::factory(),
];
}
}
// PostController.php
public function update(Request $request, Post $post)
{
$this->authorize('update', $post); // runs PostPolicy::update()
$post->update($request->validate(['title' => 'required', 'body' => 'required']));
return redirect()->route('posts.show', $post);
}Code Examples
The heavy work (PDF generation, uploading to S3, sending a notification) happens in a queued job, so the checkout request returns immediately with a friendly status message instead of making the customer wait for a PDF to render.
RefreshDatabase gives the test a clean, migrated database; actingAs() simulates an authenticated user; assertJsonPath checks the shape PostResource produces rather than the raw Eloquent model, and assertDatabaseHas confirms the row was actually persisted. Expected result: PASS Tests\Feature\PostApiTest ✓ authenticated user can create post
<?php
// app/Jobs/GenerateInvoicePdf.php
class GenerateInvoicePdf implements ShouldQueue
{
use Dispatchable, Queueable;
public function __construct(public Order $order) {}
public function handle(): void
{
$pdf = Pdf::view('invoices.pdf', ['order' => $this->order]);
Storage::disk('s3')->put("invoices/{$this->order->id}.pdf", $pdf->output());
$this->order->user->notify(new InvoiceReady($this->order));
}
}
// Triggering it from a controller after checkout completes
GenerateInvoicePdf::dispatch($order);
return redirect()->route('orders.show', $order)
->with('status', 'Your order is confirmed — invoice generating.');<?php
class PostApiTest extends TestCase
{
use RefreshDatabase;
public function test_authenticated_user_can_create_post(): void
{
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson('/api/posts', [
'title' => 'Hello World',
'body' => 'My first post.',
]);
$response->assertStatus(201)
->assertJsonPath('data.title', 'Hello World');
$this->assertDatabaseHas('posts', ['title' => 'Hello World']);
}
}
// app/Http/Resources/PostResource.php
class PostResource extends JsonResource
{
public function toArray($request): array
{
return [
'id' => $this->id,
'title' => $this->title,
'body' => $this->body,
'author'=> $this->author->name,
];
}
}Where Each Advanced Feature Sits in a Request's Lifetime
Queues, events, and the scheduler run work outside the synchronous request/response path shown at the top.
Request comes in
│
▼
Middleware / Policy check (authorize)
│
▼
Controller ── dispatch(Job) ──▶ Queue ──▶ Worker: handle() ──▶ Notification (mail/db/broadcast)
│
▼
Eloquent (Cache::remember around expensive reads)
│
▼
API Resource / Blade view ──▶ Response
(meanwhile, Schedule::command(...) runs independently every minute via cron)Important Rules
Never run slow, non-essential work (email, PDF generation, third-party API calls) synchronously in a request — dispatch a job instead.
Check authorization with Policies/Gates in every controller action that mutates data, not just at the route/middleware level.
Always return API Resources instead of raw Eloquent models from JSON endpoints to control exactly what's exposed.
Use RefreshDatabase (or a transaction-based equivalent) in feature tests so tests never depend on leftover state from a previous test.
Bind interfaces, not concrete classes, in constructors for anything you'll want to fake or swap in tests.
Queues and Events at Scale
A typical e-commerce checkout on Laravel fires an OrderPlaced event the instant payment succeeds; separate listeners queued from that single event independently email a receipt, decrement inventory, notify a fulfillment warehouse, and update analytics — so the checkout page responds to the customer in milliseconds while several unrelated background systems each do their own job without the checkout code needing to know any of them exist.
Mail::to($user)->send(new WelcomeEmail($user)); // sent synchronously in the registration request class WelcomeEmail extends Mailable implements ShouldQueue {... } // or: dispatch a job that sends it
Sending mail synchronously ties the user's registration response time to the mail server's response time (which can be slow or occasionally fail); implementing ShouldQueue (or wrapping the send in a queued job) lets the request return immediately while the email sends in the background.
return $post; // returning a raw Eloquent model directly from an API route return new PostResource($post);
Returning the raw model risks leaking columns never meant to be public (internal flags, foreign keys, timestamps) and couples the API's JSON shape directly to the database schema, so a column rename breaks every API consumer.
public function update(Request $request, Post $post) { $post->update($request->all()); } public function update(Request $request, Post $post) { $this->authorize('update', $post); $post->update($request->validate(['title' => 'required'])); }
Skipping the authorize() check means any logged-in user could edit any post by guessing its URL, not just their own; skipping validate() also means $request->all() could mass-assign unexpected fields straight from the request.
What is the main benefit of dispatching a Job instead of doing the work directly inside a controller method? A. Jobs run faster than any other PHP code B. The work is moved off the request/response cycle so the user gets a fast response while the job runs in the background C. Jobs automatically fix any bugs in the code D. Jobs replace the need for a database
B. The work is moved off the request/response cycle so the user gets a fast response while the job runs in the background — Dispatching a Job pushes the work onto a queue that a separate worker process executes, so the original request can return immediately instead of waiting for slow work like sending email or generating a file.
What does the RefreshDatabase trait do in a Laravel feature test? A. It deletes the production database B. It resets the test database to a clean, migrated state before each test runs C. It speeds up Eloquent queries permanently D. It disables all database queries during testing
B. It resets the test database to a clean, migrated state before each test runs — RefreshDatabase ensures each test starts from a known, clean schema state, preventing one test's leftover data from affecting another test's results.
What is the difference between $this->app->bind() and $this->app->singleton() in the service container? A. bind() and singleton() are identical in every way B. bind() resolves a new instance every time it's requested; singleton() reuses one shared instance C. singleton() only works with Eloquent models D. bind() is used only for testing, singleton() only for production
B. bind() resolves a new instance every time it's requested; singleton() reuses one shared instance — bind() registers a fresh instance on every resolution from the container, while singleton() constructs the instance once and returns that same instance for every subsequent request within the application's lifecycle.
Why use an API Resource class instead of returning an Eloquent model directly from a JSON API endpoint? A. API Resources make queries run faster B. They let you control exactly which fields are exposed, decoupling the JSON response shape from the database schema C. Eloquent models cannot be converted to JSON at all D. API Resources are required by PHP's JSON extension
B. They let you control exactly which fields are exposed, decoupling the JSON response shape from the database schema — An API Resource's toArray() method defines precisely which fields appear in the response, preventing accidental exposure of internal columns and insulating API consumers from database schema changes.
What does a Policy's update(User $user, Post $post): bool method combined with $this->authorize('update', $post) accomplish? A. It validates the post's title and body fields B. It checks whether the given user is allowed to update the given post, and automatically returns a 403 response if not C. It automatically updates the post in the database D. It sends a notification to the post's author
B. It checks whether the given user is allowed to update the given post, and automatically returns a 403 response if not — Policies define per-model authorization rules; authorize() calls the matching policy method and throws an AuthorizationException (rendered as a 403 response) if it returns false, centralizing 'is this allowed?' logic away from the controller body.
Sketch (in comments/pseudocode) the pieces needed for a 'user uploads a video' feature: a migration for a videos table, a queued Job that processes the video and stores it via the Storage facade, a Notification sent to the user when processing completes, and a Policy method controlling who can delete a video. Hint: The job's handle() method should call Storage::disk(...)->put(...) and then $video->user->notify(new VideoProcessed($video)); the Policy needs a delete(User $user, Video $video): bool method comparing $video->user_id to $user->id.
Try it yourself — there's no single right answer here; compare your result against this lesson's examples.