Laravel 101: The Basics
Everything you need to know to stop pretending you know what a Service Provider is.
So, I’m going to talk about PHP. If you’re still in the “PHP sucks” camp but haven’t touched PHP 8 yet, you don’t get an opinion. Go sit in the corner.
Okay then. You do that, guy.
Yes, old PHP was (is) awful. No arguments there. I’d rather remove my eyeballs out with a rusty spoon than attempt to refactor some of the rotten PHP 5 (and below) codebases out there. I shit you not.
But modern PHP? Especially with Laravel in the mix? It’s pretty solid.
I use it quite a lot.
What is Laravel?
Laravel is one of the most popular PHP frameworks in the world. It's elegant, expressive, and takes a lot of the grunt work out of building web applications.
But if you’re just getting started, there’s quite a lot to take on board. It gives you a solid foundation for:
Routing
Authentication
Database access
APIs
Blade templating
Queues, jobs, mail, events… etc etc etc.
Go check out https://laravel.com/docs/12.x/installation - with a few commands you’ll be well on your way.
Here’s a brief overview:
Routing
Routing in Laravel defines how your application responds to different URLs. When a user visits a URL, Laravel checks your routes/web.php
or routes/api.php
file to determine what should happen.
Routes can return plain strings, views, JSON, or call controller methods.
// routes/web.php
Route::get('/hello', function () {
return 'Hello, World!';
});
Migrations
Migrations are version-controlled PHP files that let you define and modify your database schema.
Run this Artisan command to create a new migration file (for a new posts table).
php artisan make:migration create_posts_table
This generates a timestamped file in database/migrations/
.
Open that migration file and write your table structure using Laravel’s schema builder:
Schema::create('posts', function (Blueprint $table) {
$table->id(); // Primary key (auto-increment)
$table->string('title'); // VARCHAR column for the post title
$table->text('content')->nullable(); // Optional content field
$table->boolean('published')->default(false); // Publish status
$table->timestamps(); // created_at and updated_at columns
});
To apply migrations and create/update tables, run:
php artisan migrate
To undo your last changes.
php artisan migrate:rollback
Or refresh everything (rollback + migrate + seed again):
php artisan migrate:fresh --seed
Models
In Laravel, Eloquent Models represent database tables and provide an elegant, Active Record-style way to interact with your data.
Each model corresponds to a table (usually plural of the model name). For example, the Post
model corresponds to the posts
table.
First, create the model.
php artisan make:model Post
Add -m
if you want to create a migration along with it:
php artisan make:model Post -m
To protect against mass assignment vulnerabilities, define which fields are mass assignable:
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'content', 'published'];
}
You can create a record:
Post::create([
'title' => 'My first post',
'content' => 'Hello world!',
'published' => true,
]);
Retrieve data.
$posts = Post::all(); // Get all posts
$post = Post::find(1); // Get post with ID 1
Update a record.
$post = Post::find(1);
$post->title = 'Updated title';
$post->save();
Delete a record.
$post = Post::find(1);
$post->delete();
Also, models can define relationships between other models like hasMany
, belongsTo
, etc., but we can cover those another time!
Accessors and Mutators
Accessors and Mutators allow you to automatically transform Eloquent model attribute values when you retrieve or set them.
Accessor: Modifies a value when reading from the model.
Mutator: Modifies a value before saving it to the database.
Here’s an example. Say you wanted to automatically capitalise the first letter when accessing the title
attribute:
public function getTitleAttribute($value)
{
return ucfirst($value);
}
You’d access it like this:
$post = Post::find(1);
echo $post->title; // Outputs: 'Hello world' instead of 'hello world'
Want a mutator example?
Say we want to automatically convert the title to lowercase before saving to the database:
public function setTitleAttribute($value)
{
$this->attributes['title'] = strtolower($value);
}
Accessors start with
get
, end withAttribute
.Mutators start with
set
, end withAttribute
.The attribute name is camel-cased in between.
Example for attribute published_at
:
Accessor:
getPublishedAtAttribute($value)
Mutator:
setPublishedAtAttribute($value)
Traits
Traits in PHP let you reuse methods across multiple classes without inheritance. In Laravel, traits are often used to share reusable bits of logic or helper methods among models, controllers, or services.
trait Sluggable
{
public function slugify(string $value): string {
return Str::slug($value);
}
}
Why Use Traits?
Avoid repeating code in multiple places.
Add specific functionality in a modular way.
Keep classes clean and focused.
Example: A Sluggable
Trait
Suppose you want to generate URL-friendly slugs from titles across different models.
Create a trait like this:
namespace App\Traits;
use Illuminate\Support\Str;
trait Sluggable
{
public function slugify(string $value): string
{
return Str::slug($value);
}
}
Then use it in your model.
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use App\Traits\Sluggable;
class Post extends Model
{
use Sluggable;
protected $fillable = ['title'];
public function getSlugAttribute()
{
return $this->slugify($this->title);
}
}
Now if you wanted the slug.
$post = Post::find(1);
echo $post->slug; // Outputs: 'my-post-title'
Query Scopes
Query scopes let you define reusable query constraints in your Eloquent models, keeping your queries clean and DRY.
There are two types:
Local scopes: Applied when you call them explicitly.
Global scopes: Automatically applied to every query on the model.
public function scopePublished($query) {
return $query->where('published', true);
}
Here’s a local scope. If you added this method to your model:
public function scopePublished($query)
{
return $query->where('published', true);
}
You’d be able to use it like this:
$posts = Post::published()->get();
This fetches only posts where published
is true.
For global scopes, you’d create a new class.
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Scope;
class PublishedScope implements Scope
{
public function apply(Builder $builder, Model $model)
{
$builder->where('published', true);
}
}
And attach it to your model.
protected static function booted()
{
static::addGlobalScope(new PublishedScope);
}
Query scopes are perfect for reusable, consistent filtering logic.
Observers
Observers allow you to hook into model lifecycle events such as creating, updating, deleting, and more. They help keep your model code clean by moving event-related logic into dedicated classes.
Why use them?
Keep your models focused on business logic.
Separate concerns like logging, notifications, or cache clearing.
Automatically react to model changes without cluttering controllers.
public function created(Post $post) {
Log::info('Post created: ' . $post->id);
}
First, generate an observer class for a model:
php artisan make:observer PostObserver --model=Post
This creates app/Observers/PostObserver.php
with methods you can fill out for events like created()
, updated()
, deleted()
, etc.
Here’s a quick example, when a new Post
is created, this is going to trigger a log.
namespace App\Observers;
use App\Models\Post;
use Illuminate\Support\Facades\Log;
class PostObserver
{
public function created(Post $post)
{
Log::info('Post created with ID: ' . $post->id);
}
}
You’ll need to attach it to your model and register your observer in a service provider, typically in App\Providers\EventServiceProvider
. i.e:
use App\Models\Post;
use App\Observers\PostObserver;
public function boot()
{
Post::observe(PostObserver::class);
}
Now, whenever a Post is created, updated, or deleted, your observer methods will automatically run.
Controllers
Controllers are classes that group related request handling logic into a single place. They help keep your routes clean and organise your application’s behaviour.
You can generate one using using an artisan command:
php artisan make:controller PostController
Here’s an example of a controller in action.
namespace App\Http\Controllers;
use App\Models\Post;
class PostController extends Controller
{
public function index()
{
$posts = Post::all();
return view('posts.index', compact('posts'));
}
public function show($id)
{
$post = Post::findOrFail($id);
return view('posts.show', compact('post'));
}
}
With your controller set up, remember to route to them. Define routes that point to your controller actions:
In your routes/web.php
file.
use App\Http\Controllers\PostController;
Route::get('/posts', [PostController::class, 'index']);
Route::get('/posts/{id}', [PostController::class, 'show']);
Controllers can also use middleware, form requests for validation, and more to keep your code clean and maintainable.
Resources
Laravel Resources transform your models and collections into JSON responses, perfect for APIs. They let you control exactly what data is sent back to clients in a clean and reusable way.
Generate a resource class:
php artisan make:resource PostResource
Here’s a basic example:
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class PostResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'content' => $this->content,
'created_at' => $this->created_at->toDateTimeString(),
];
}
}
You’d then use it within your controller:
use App\Http\Resources\PostResource;
use App\Models\Post;
public function show($id)
{
$post = Post::findOrFail($id);
return new PostResource($post);
}
Resources help build clean, maintainable APIs by formatting responses consistently.
Requests
Form Requests in Laravel encapsulate validation and authorisation logic for incoming HTTP requests. Instead of cluttering controllers with validation code, you create a dedicated request class that handles it neatly.
php artisan make:request StorePostRequest
public function rules() {
return [ 'title' => 'required|string|max:255' ];
}
Why use them?
Keeps your controllers clean and focused.
Reuse validation rules easily.
Include authorization logic to protect routes.
Generate a new Form Request class:
php artisan make:request StorePostRequest
Then within your Request class, you define rules and auth logic.
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize()
{
// Return true to allow all users, or add authorization logic
return true;
}
public function rules()
{
return [
'title' => 'required|string|max:255',
'content' => 'nullable|string',
'published' => 'boolean',
];
}
}
You can then inject the class into your controller methods.
use App\Http\Requests\StorePostRequest;
public function store(StorePostRequest $request)
{
$validated = $request->validated();
Post::create($validated);
return redirect()->route('posts.index');
}
Laravel automatically runs validation before the controller method executes. If validation fails, it redirects back with errors.
Rules
Custom validation rules.
php artisan make:rule Uppercase
Macros
Macros allow you to extend Laravel’s core classes by adding your own custom methods at runtime. This is super handy when you want to add reusable functionality without modifying the original class.
Why use them?
Add custom helpers to classes like
Response
,Collection
,Request
, etc.Keep your code DRY by centralising common operations.
Enhance Laravel classes with your own domain-specific methods.
Here’s an example:
use Illuminate\Support\Facades\Response;
Response::macro('caps', function ($value) {
return Response::make(strtoupper($value));
});
return response()->caps('hello world'); // Outputs: HELLO WORLD
Mailables
Mailables in Laravel are classes that represent emails you send from your application. They help you organise email content, templates, and data cleanly.
Why use them?
Separate email logic from controllers or jobs.
Use Blade templates for email views.
Easily attach files, set subjects, and more.
Generate a new mailable class:
php artisan make:mail OrderShipped
And here’s what it could look like:
namespace App\Mail;
use App\Models\Order;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Queue\SerializesModels;
class OrderShipped extends Mailable
{
use Queueable, SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(
public Order $order,
) {}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'mail.orders.shipped',
);
}
}
You’d obviously need a mail view, for this example it’d be in resources/views/mail/orders/shipped.blade.php
:
<h1>Hello, {{ $order->user->name }}</h1>
<p>Your order has been shipped!</p>
And then, in your controller or job, you’d send it
use App\Mail\OrderShipped;
use Illuminate\Support\Facades\Mail;
Mail::to($user->email)->send(new OrderShipped($order));
Events
Events in Laravel provide a simple observer pattern implementation to decouple different parts of your application. Need a refresh? Click here.
They allow you to signal that something happened and have multiple listeners react to it.
Why use them?
Decouple logic for better maintainability.
Trigger multiple actions in response to one event.
Keep your code clean and modular.
First, we’d create an event class.
php artisan make:event PostPublished
And your class would look something like this:
namespace App\Events;
use App\Models\Post;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PostPublished
{
use Dispatchable, SerializesModels;
public function __construct(public Post $post)
{
}
}
That’s essentially it. All you need to do now is dispatch it.
use App\Events\PostPublished;
event(new PostPublished($post));
Listeners
Listeners handle the logic that should run in response to an event. They keep event-related actions organised and separate from other parts of your code.
Why use them?
Separate concerns by keeping event responses out of controllers or models.
Easily add or remove behaviour reacting to events.
Use queued listeners for performance-heavy tasks.
Generate a listener class:
php artisan make:listener SendPostNotification --event=PostPublished
Here’s an example:
namespace App\Listeners;
use App\Events\PostPublished;
use Illuminate\Support\Facades\Log;
class SendPostNotification
{
public function handle(PostPublished $event)
{
Log::info('Post published: ' . $event->post->id);
// Here you could send an email, notification, etc.
}
}
Now, you’d need to register the listener to an event. Listeners are registered in app/Providers/EventServiceProvider.php
:
protected $listen = [
\App\Events\PostPublished::class => [
\App\Listeners\SendPostNotification::class,
],
];
Every time the event is dispatched, this listener will fire.
You can add the ShouldQueue
interface to process listeners asynchronously for better performance:
use Illuminate\Contracts\Queue\ShouldQueue;
class SendPostNotification implements ShouldQueue
{
public function handle(PostPublished $event)
{
// ...
}
}
Listeners keep your event-driven logic clean, testable, and scalable.
Jobs
Jobs in Laravel represent tasks that can be dispatched for immediate or queued execution. They help you offload time-consuming or asynchronous operations from your request cycle.
Why use them?
Handle long-running processes without blocking user requests.
Easily queue tasks to run in the background.
Keep your controllers slim and focused.
Generate a job class:
php artisan make:job ProcessPodcast
Maybe it ends up looking something like this:
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessPodcast implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(protected Podcast $podcast)
{
}
public function handle()
{
// Process the podcast, e.g., encode audio, save metadata, etc.
}
}
With that place, you can dispatch it immediately:
ProcessPodcast::dispatch($podcast);
Or dispatch with a delay:
ProcessPodcast::dispatch($podcast)->delay(now()->addMinutes(10));
Jobs provide a clean, testable way to handle background work and improve app responsiveness.
Commands
Commands in Laravel are custom CLI tools you create to automate repetitive tasks, maintenance, or any logic you want to run from the terminal.
Why use them?
Automate tasks like data imports, cleanup, or reports.
Schedule commands to run periodically with Laravel Scheduler.
Interact with your application from the command line.
Generate a new command class:
php artisan make:command ImportData
Populate the class:
namespace App\Console\Commands;
use Illuminate\Console\Command;
class ImportData extends Command
{
// Command signature to call via CLI
protected $signature = 'data:import';
// Command description
protected $description = 'Import data from external source';
public function handle()
{
// Your import logic here
$this->info('Data import started.');
// ...
$this->info('Data import completed.');
}
}
Then run it in terminal:
php artisan data:import
You may define all of your scheduled tasks in your application's routes/console.php
file.
use Illuminate\Support\Facades\Schedule;
Schedule::command('data:import')->daily();
Gates
Gates are a simple, closure-based way to authorize user actions in Laravel. They determine if a user is authorized to perform a given action, typically used for quick authorisation logic
Why use them?
Implement fine-grained access control.
Check permissions without creating full policies.
Keep authorisation logic simple for specific actions.
Gates are defined in the boot()
method of your AuthServiceProvider
(app/Providers/AuthServiceProvider.php
):
use Illuminate\Support\Facades\Gate;
public function boot()
{
Gate::define('update-post', function ($user, $post) {
return $user->id === $post->user_id;
});
}
In your controllers, views, or elsewhere, you can check a gate like this:
if (Gate::allows('update-post', $post)) {
// The current user can update the post
}
Or deny access:
Gate::denies('update-post', $post);
Within Blade, you’ve got the can
directive:
@can('update-post', $post)
<a href="{{ route('posts.edit', $post) }}">Edit Post</a>
@endcan
Gates provide a straightforward way to handle authorisation for individual actions.
Policies
Policies are classes that organise authorisation logic around a particular model or resource. They offer a cleaner, more structured way to manage complex authorisation compared to Gates
.
Why use them?
Group authorisation rules by model.
Keep your code organised and maintainable.
Automatically integrate with Laravel’s authorisation features.
Generate a policy class for a model:
php artisan make:policy PostPolicy --model=Post
It could look something like this:
namespace App\Policies;
use App\Models\Post;
use App\Models\User;
class PostPolicy
{
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
public function delete(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}
This is basically saying that only the user attached to the given post can delete or update, quite cool! Whenever someone tries to update or delete the post, it’ll check this first.
Register the policy in AuthServiceProvider
(app/Providers/AuthServiceProvider.php
):
protected $policies = [
Post::class => PostPolicy::class,
];
In your controllers or elsewhere, you can check authorisation like this:
$this->authorize('update', $post);
Or in Blade templates, like this:
@can('delete', $post)
<form method="POST" action="{{ route('posts.destroy', $post) }}">
@csrf
@method('DELETE')
<button>Delete Post</button>
</form>
@endcan
Policies provide a powerful and scalable way to manage user permissions per resource.
Middleware
Middleware acts as a filter that inspects and manipulates HTTP requests entering your application. It’s used for tasks like authentication, logging, CORS handling, and more.
Remember the Chain of Responsibility article I wrote? No, well check it out here.
Why use them?
Control access to routes (e.g., require authentication).
Modify requests or responses globally or for specific routes.
Centralise common logic executed before/after requests.
First, generate a new middleware class:
php artisan make:middleware CheckAge
It’ll look something like this, maybe?
namespace App\Http\Middleware;
use Closure;
class CheckAge
{
public function handle($request, Closure $next)
{
if ($request->age <= 18) {
return redirect('home');
}
return $next($request);
}
}
If you want a middleware to run during every HTTP request to your application, you may append it to the global middleware stack in your application's bootstrap/app.php
file:
use App\Http\Middleware\CheckAge;
->withMiddleware(function (Middleware $middleware) {
$middleware->append(CheckAge::class);
})
Or if you would like to assign middleware to specific routes, you may invoke the middleware
method when defining the route:
use App\Http\Middleware\CheckAge;
Route::get('/profile', function () {
// ...
})->middleware(CheckAge::class);
Service Providers
The best till last. Service providers are the central place where Laravel bootstraps your application’s various services. They register bindings in the service container, event listeners, middleware, and more.
Why use them?
Register application services and dependencies.
Bootstrap packages and core features.
Organise bootstrapping logic cleanly.
Generate a new service provider with Artisan:
php artisan make:provider SocialServiceProvider
Basic example:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class CustomServiceProvider extends ServiceProvider
{
public function register()
{
// Bind services or classes to the container here
}
public function boot()
{
// Bootstrap any application services here
}
}
Within the register
method, you should only bind things into the service container. You should never attempt to register any event listeners, routes, or any other piece of functionality within the register
method.
The boot method is called after all other service providers have been registered, meaning you have access to all other services that have been registered by the framework:
All service providers are registered in the bootstrap/providers.php
configuration file. This file returns an array that contains the class names of your application's service providers:
return [
App\Providers\CustomServiceProvider::class,
];
There’s way too much to talk about here. Go read this.
Conclusion
Each section above is worthy of an article in itself. If this post gets some love, I’ll go through each of them one by one in greater depth.
Laravel is awesome.