Skip to content

Instantly share code, notes, and snippets.

@wsydney76
Last active August 27, 2026 08:54
Show Gist options
  • Select an option

  • Save wsydney76/2ce4ec74404f1a986538ef5e0a49a20a to your computer and use it in GitHub Desktop.

Select an option

Save wsydney76/2ce4ec74404f1a986538ef5e0a49a20a to your computer and use it in GitHub Desktop.
Laravel/Craft 6 Concepts

Laravel/Craft 6 Concepts Overview

A useful way to understand these Laravel concepts is by the role they play in the application, because several belong to MVC, several belong to Laravel's infrastructure, and others are architectural patterns that Laravel does not prescribe.

Version scope: This revision targets Laravel 13.x and Livewire 4.x conventions as of August 2026. Ecosystem packages such as Filament, Flux, and Blaze evolve independently, so check their version-specific documentation before copying API examples.

Table of Contents

Glossary

Alphabetical index of every concept covered in this document, with a short description and its classification. Click a concept's name to jump to where it's explained in detail.

Concept Short description Classification Official Laravel docs
Accessor / Cast Transforms an attribute's value when it's read from or written to a model. Laravel/Eloquent-specific abstraction Mutators & Casting
Action A class representing one application use case, e.g. CreateOrder. Generic architectural pattern/convention
Authentication Identifies the current user through guards/providers and related auth infrastructure. Laravel-specific authentication infrastructure Authentication
Blade Component Reusable Blade markup accepting props/slots, referenced as <x-...>. Laravel/Blade-specific view-reuse abstraction Blade Components
Broadcasting Pushes server-side events to connected clients in real time. Laravel-specific abstraction Broadcasting
Cache Stores expensive-to-compute values for fast repeated retrieval. Laravel-specific facade over pluggable stores Cache
Collection Fluent in-memory wrapper around arrays/iterables. Laravel-specific abstraction Collections
Command A CLI entry point (usually Artisan) that invokes application logic. Generic CLI concept; Artisan implementation Laravel-specific Artisan Console
Components and Data Objects Craft's configurable/validatable object abstraction for non-Eloquent data. Craft CMS-specific convenience abstraction
Concern A reusable PHP trait mixed into related classes for shared behavior. Generic PHP pattern/convention
Concurrency Runs multiple PHP closures at the same time and collects their results. Laravel-specific abstraction over the Process facade Concurrency
Configuration Application settings defined in config/*.php. Laravel-specific convention Configuration
Context Captures key/value data and attaches it to log entries across a request/job/command. Laravel-specific abstraction Context
Contract An interface defining an API that implementations must follow. Generic interface pattern; Laravel heavily uses contracts Contracts
Controller Coordinates an HTTP request: delegates work and returns a response. Generic MVC; Laravel implementation Controllers
Craft 6 Concept Mapping Compact reference table translating Craft-specific concepts to generic Laravel/PHP concepts. Craft CMS reference table
Craft Actions and Atomized Operations Small invokable classes for focused operations, reusable across controllers/commands/jobs. Craft's adoption of the generic Action pattern
Craft Commands and Queues Craft console commands are Artisan commands; Craft operations can use Laravel's queue system. Direct use of Laravel commands/queues Artisan Console · Queues
Craft Configuration vs Laravel Configuration vs Project Config Distinguishes Laravel runtime config, Craft/plugin settings, and version-controlled Project Config. Craft CMS architectural convention Configuration
Craft Controllers and Routes Craft controllers/routes are ordinary Laravel routing; no required Craft base controller. Craft's use of Laravel's HTTP stack Routing
Craft Events and Laravel Events Craft events are ordinary Laravel events/listeners carrying Craft-domain information. Craft's use of Laravel's event system Events
Craft Filesystems and Laravel Disks Craft filesystems wrap Laravel's filesystem disks, adding CMS asset-volume configuration. Craft CMS wrapper over Laravel's filesystem File Storage
Craft Model vs Eloquent Model vs Component Clarifies that Craft 6 models are Eloquent models, while validatable data objects are Components. Craft CMS terminology built on Laravel Eloquent
Craft Services Ordinary injectable PHP services, often singletons, resolved through Laravel's container. Craft CMS convention over Laravel's service container Service Container
Craft Templates: Twig and Blade Craft supports both its Twig templating ecosystem and Laravel's Blade views. Craft CMS-specific templating environment Blade Templates
Craft Users, Authentication, and Permissions Craft users are Elements; authentication uses Laravel, permissions layer on top. Craft CMS abstraction over Laravel authentication Authentication
Craft Validation Craft increasingly validates HTTP input at the boundary using Laravel validation/Form Requests. Craft's use of Laravel validation Validation
CSRF Protection Protects authenticated state-changing web requests from cross-site forgery. Web security concept; Laravel implementation CSRF Protection
Database Transaction Groups database operations atomically. Generic DB concept; Laravel abstraction Database Transactions
DTO Groups related data to transport it between application layers. Generic architectural pattern
Eager Loading Pre-loads relationships alongside the parent query to avoid the N+1 query problem. Laravel/Eloquent-specific abstraction Eager Loading
Elements Craft's CMS-managed content abstraction (entries, assets, users...) with no direct Laravel equivalent. Craft CMS-specific abstraction
Element Queries Craft's content-query API for locating Elements, distinct from the Eloquent query builder. Craft CMS-specific abstraction
Eloquent Laravel's built-in ORM tying models, the query builder, relationships, casts, and scopes together. Laravel-specific implementation of the ActiveRecord pattern Eloquent: Getting Started
Enum Represents a finite set of valid values/states. PHP language feature / generic domain modelling
Event Announces that something happened, e.g. OrderPlaced. Generic Observer/event-driven pattern; Laravel implementation Events
Exception Handling Reports failures and renders appropriate responses. Generic concept; Laravel implementation Error Handling
Facade Static-looking proxy to a service resolved from the container, e.g. Cache::. Generic design-pattern term, but Laravel Facades are Laravel-specific Facades
Factory Generates model instances/test data, often using Faker. Generic pattern; Laravel implementation specific Eloquent: Factories
Fields and Field Layouts Define configurable CMS content attached to Elements and the UI used to edit it. Craft CMS-specific abstraction
Filament Admin-panel package building CRUD screens out of Livewire components. Independent Laravel ecosystem package built on Livewire Filament Docs (third-party)
Flux Component Pre-built <flux:*> Blade UI kit wired for Livewire (wire:model, errors, ...). Third-party (Flux) UI kit built on top of Livewire/Blade components Flux Docs (third-party)
Form Request Encapsulates HTTP input validation (and optionally authorization). Laravel-specific Form Request Validation
Fortify Front-end-agnostic package implementing authentication routes/logic. First-party Laravel package Fortify
Gate A closure-based authorization check, simpler than a Policy. Laravel-specific authorization mechanism Gates
Helper A small, stateless, reusable utility function, e.g. money(). Generic Helpers
HTTP Client Fluent API for calling external HTTP services. Laravel-specific abstraction HTTP Client
Inertia Bridges Laravel routing/controllers to a Vue/React/Svelte SPA without an API. Third-party package, alternative to Blade/Livewire for the frontend Inertia Docs (third-party)
Job Represents work to perform, often dispatched to a queue. Generic queue/task pattern; Laravel implementation Queues
Listener Reacts to a dispatched event, e.g. sending a confirmation email. Generic event-driven concept; Laravel implementation Events: Listeners
Livewire Component A server-driven reactive component with PHP state and Blade markup. Livewire ecosystem package; tightly integrated with Laravel, but not Laravel core Livewire Docs (first-party, separate docs site)
Localization Translates user-facing strings via __() and language files. Laravel-specific Localization
Macro / Mixin Adds new method(s) to an existing (usually framework) class at runtime. Laravel-specific mechanism (Macroable trait) for runtime method injection Collections: Extending
Mass Assignment Controls which Eloquent attributes may be assigned in bulk. Laravel/Eloquent security mechanism Mass Assignment
Mailable A class representing a single email's content, envelope, and delivery. Laravel-specific Mail
Middleware Code that runs before/after a request reaches the controller. Generic web architecture; Laravel implementation Middleware
Migration Defines versioned database schema changes. Generic DB pattern; Laravel implementation Migrations
Model A class representing domain/application data, usually backed by a DB table. Generic MVC concept; Eloquent implementation Laravel-specific Eloquent: Getting Started
Notification Represents a user-facing message deliverable across channels (email, SMS...). Laravel abstraction Notifications
Observer Groups handlers for an Eloquent model's lifecycle events. Generic Observer pattern; Laravel Eloquent implementation Eloquent: Observers
Package A reusable library/module distributed via Composer. Generic Composer/library concept; Laravel package integration specific Package Development
Pagination Splits a large result set into pages via ->paginate()/->links(). Laravel-specific abstraction Pagination
Plugin Concerns and Declarative Registration Traits that declaratively register plugin capabilities such as routes, commands, and listeners. Craft CMS-specific convention
Plugins Distributable Craft extensions whose base class is also a Laravel Service Provider. Craft CMS-specific package built on Laravel Service Providers
Policy Determines whether a user may perform an action on a resource. Laravel authorization abstraction Policies
Practical architectural rule for Craft 6 applications Decision guide for choosing the right abstraction (Model, Element, Action, Service...) in Craft 6 code. Craft CMS architectural guidance
Process Invokes and manages external command-line processes via a fluent API. Laravel-specific abstraction over Symfony Process Processes
Project Code vs Plugins vs Former Modules Clarifies that project-specific code belongs under App\, replacing Craft 5's module concept. Craft CMS architectural convention
Project Config Version-controlled CMS schema/administrative state, distinct from Laravel's runtime config. Craft CMS-specific abstraction
Queue Defers/distributes work (jobs) to background workers instead of the current request. Laravel-specific abstraction Queues
Query Builder Illuminate's fluent, database-agnostic SQL query construction API. Laravel-specific Query Builder
Rate Limiting Restricts repeated actions within a time window. Laravel-specific abstraction Rate Limiting
Registries A validated catalog of related Craft component types; not the Laravel service container. Craft CMS-specific extension mechanism
Relationship Declares how one Eloquent model relates to another (hasMany, belongsTo...). Laravel/Eloquent-specific abstraction Eloquent: Relationships
Repository Abstracts data retrieval/persistence behind an interface. Generic architectural pattern
Reporter Separates observing/reporting an operation's progress, results, or errors from its business logic. Generic architectural pattern
Resource Transforms models/data into a defined API (JSON) representation. Laravel API Resource abstraction Eloquent: Resources
Response Object Represents the structured result (success/failure) of an operation. Generic architectural pattern
Route Maps an HTTP method + URI to a controller/action/closure. Generic web-framework concept; Laravel implementation Routing
Route Model Binding Automatically resolves a model instance from a route parameter. Laravel-specific Route Model Binding
Rule Encapsulates a single reusable validation constraint. Laravel validation abstraction Custom Validation Rules
Sanctum SPA/session authentication and personal API tokens. First-party Laravel package Sanctum
Scope Encapsulates a reusable Eloquent query constraint. Generic query idea; Eloquent scopes Laravel-specific Eloquent: Query Scopes
Seeder Inserts predefined/sample data into the database. Laravel-specific abstraction Seeding
Service A class holding reusable business/application logic or capability. Generic architectural pattern
Service Container Resolves classes and automatically injects their dependencies. Laravel-specific Service Container
Service Provider Registers bindings and bootstraps services into Laravel. Laravel-specific Service Providers
Session Persists small pieces of data across requests for one user. Laravel-specific facade over pluggable stores HTTP Session
Soft Deletes Retains deleted Eloquent rows using deleted_at. Laravel/Eloquent abstraction Soft Deleting
Storage Abstracts file storage across local/cloud "disks". Laravel-specific File Storage
Task Scheduling Defines recurring jobs/commands run by a single cron entry. Laravel-specific Task Scheduling
Tests Automated code verifying application behavior (unit/feature). Generic software-engineering practice; Laravel tooling specific Testing
The Yii Adapter Optional compatibility package for migrating legacy Craft 5/Yii plugin code to Craft 6/Laravel. Craft CMS migration/compatibility layer
Value Object Models a domain value (e.g. Money) with rules/behavior, not just data. Generic domain/DDD pattern
View A presentation template that turns data into HTML/UI output. Generic MVC; Blade Laravel-specific Views
View Composer Attaches data-preparation logic to a view, run whenever it renders. Laravel-specific abstraction View Composers

1. MVC and the HTTP request/response layer

These are the components closest to the classic Model--View--Controller structure.

Concept What it is Role Laravel-specific?
Model A PHP class representing application/domain data, usually backed by a database table through Eloquent. Reads/writes data, defines relationships, casts, scopes, etc. Laravel implementation of a generic MVC concept
View A presentation template, usually a Blade file. Turns application data into HTML/UI output. Generic MVC concept; Blade is Laravel-specific
Controller A class whose methods handle incoming requests. Coordinates the request: validates/receives input, invokes application logic, returns a response. Generic MVC concept with Laravel integration
Route Maps an HTTP method + URI to a controller/action/closure. Entry point into the application for HTTP requests. Framework concept; Laravel implementation is specific
Request / Form Request Laravel's representation of an HTTP request. A Form Request adds validation and authorization rules. Extracts and validates incoming HTTP data before business logic runs. Laravel-specific implementation
Middleware Code executed around an HTTP request before/after it reaches the controller. Cross-cutting request concerns such as authentication, rate limiting, CORS, locale selection, etc. Generic middleware pattern; Laravel implementation specific
Resource Usually an JsonResource class that transforms models/data into API output. Defines the public representation of data returned by an API. Laravel-specific abstraction
View Composer A callback or class bound to one or more views, run whenever those views are rendered. Supplies shared/derived data to a view without the controller knowing about it. Laravel-specific abstraction

Laravel routes dispatch incoming requests, controllers receive them, and middleware can inspect/filter the request before it reaches application logic. (Laravel)

A typical request flow is:

HTTP Request
     │
     ▼
   Route
     │
     ▼
 Middleware
     │
     ▼
 Form Request validation
     │
     ▼
 Controller
     │
     ▼
 Action / Service
     │
     ▼
 Model / Repository
     │
     ▼
 Resource or View
     │
     ▼
HTTP Response

Model

Laravel models normally extend:

Illuminate\Database\Eloquent\Model

Example:

class Order extends Model
{
    public function customer()
    {
        return $this->belongsTo(Customer::class);
    }
}

Eloquent is Laravel's ORM, where a model normally corresponds to a database table and provides querying, relationships, attribute casting, persistence, etc. (Laravel)

Models should generally represent data and domain behavior closely associated with that data.

They should not become dumping grounds for every workflow in the application.


View

Usually:

resources/views/orders/show.blade.php

A view answers:

How should this information be presented?

For example:

<h1>Order #{{ $order->id }}</h1>

<p>{{ $order->total }}</p>

Views should contain presentation logic, not database queries or major business workflows.


Controller

A controller is an HTTP coordinator.

class OrderController
{
    public function store(
        StoreOrderRequest $request,
        CreateOrder $createOrder
    ) {
        $order = $createOrder->handle(
            $request->validated()
        );

        return new OrderResource($order);
    }
}

A good controller tends to remain thin:

Receive request
      ↓
delegate work
      ↓
return response

rather than containing 150 lines of business logic.


Routes

Routes define which code handles which URL.

Route::post('/orders', [OrderController::class, 'store']);

They answer:

When POST /orders happens, what handles it?

Routes can also attach middleware:

Route::middleware('auth')->group(function () {
    Route::post('/orders', [OrderController::class, 'store']);
});

Route Model Binding

Instead of manually looking up a model from a route parameter:

Route::get('/orders/{id}', function (string $id) {
    $order = Order::findOrFail($id);
});

Laravel can resolve it automatically, based on the parameter's type-hint:

Route::get('/orders/{order}', function (Order $order) {
    return $order;
});

By default, this matches the route segment against the model's primary key. A model can customize the lookup column:

public function getRouteKeyName(): string
{
    return 'slug';
}

Scoped bindings can also constrain a nested parameter to belong to its parent:

Route::get('/customers/{customer}/orders/{order}', ...)
    ->scopeBindings();

This removes repetitive "find or 404" boilerplate from controllers, while keeping the same underlying findOrFail() semantics (a 404 is thrown automatically when no match exists).


Form Requests

A Laravel Form Request encapsulates validation and optionally authorization.

class StoreOrderRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'product_id' => ['required', 'integer'],
            'quantity' => ['required', 'integer', 'min:1'],
        ];
    }
}

Controller:

public function store(StoreOrderRequest $request)
{
    $data = $request->validated();
}

Its responsibility is primarily:

"Is this incoming HTTP input structurally valid?"

not:

"Can the warehouse actually fulfill this order?"

The latter is business logic.

Laravel's validation system supports both inline validation and dedicated Form Request classes. (Laravel)

When validation fails, Laravel automatically redirects back with the errors flashed to the session, and any Blade view can display them with the @error directive (or $errors->first('field')):

<input type="text" name="quantity" value="{{ old('quantity') }}">

@error('quantity')
    <span class="text-red-600 text-sm">{{ $message }}</span>
@enderror

This same @error directive works identically for Livewire components --- see Displaying validation errors in the Livewire chapter for the Flux-based alternative.


Middleware

Middleware wraps the HTTP pipeline.

For example:

Request
   ↓
Authenticate
   ↓
Check subscription
   ↓
Rate limit
   ↓
Controller

Typical middleware concerns include:

authentication
authorization
rate limiting
CORS
session handling
tenant identification
localization
logging

Middleware is best suited for rules applying to many requests, rather than business operations specific to one controller. (Laravel)


Resource

API Resources define how models and other data are transformed into JSON responses.

Instead of returning an Eloquent model directly:

return $user;

you might use:

return new UserResource($user);

and define its public representation explicitly:

class UserResource extends JsonResource
{
    public function toArray($request): array
    {
        return [
            'id' => $this->id,
            'name' => $this->name,
        ];
    }
}

This creates a transformation layer between:

internal model representation

and

public API representation

Rather than exposing the model's serialized shape directly, the resource defines the API contract explicitly.

This decoupling becomes especially valuable as an application evolves: the underlying model can change without necessarily changing the structure exposed to API consumers.

Example usage:

Route::get('/users/{user}', function (User $user) {
    return new UserResource($user);
})->middleware('can:update,user');

View Composers

A view composer lets you attach data-preparation logic to a view (or a group of views), so that data is available every time that view is rendered --- without the controller having to remember to pass it in.

class NavigationComposer
{
    public function compose(View $view): void
    {
        $view->with('categories', Category::orderBy('name')->get());
    }
}

Registered in a service provider:

public function boot(): void
{
    View::composer('layouts.navigation', NavigationComposer::class);

    // Or attach to several views at once, including wildcards:
    View::composer(['dashboard', 'reports.*'], NavigationComposer::class);

    // A closure works too, for something small:
    View::composer('layouts.navigation', function (View $view) {
        $view->with('categories', Category::orderBy('name')->get());
    });
}

View::creator() is a close sibling --- it runs as soon as the view is instantiated, before any other data is added, rather than immediately before rendering.

View composers answer a narrow but common problem:

Several unrelated controllers all render a view (e.g. a shared sidebar/navigation partial) --- where should the code that fetches "the data that partial needs" live?

Without a composer, every controller that renders that partial has to remember to fetch and pass the same data:

return view('dashboard', [
    'categories' => Category::orderBy('name')->get(),
    // ...page-specific data
]);

With a composer, the view itself declares its data dependency once, and every controller is freed from knowing about it:

return view('dashboard', [
    // ...page-specific data only
]);

Use view composers sparingly and mainly for cross-cutting, view-specific data (navigation, shared widgets, global counts) --- not as a general substitute for passing data explicitly from a controller. Overusing them can make it hard to trace where a variable used in a Blade view actually comes from.


2. UI components: Blade, Livewire, and Flux

Laravel's view layer is not limited to plain Blade templates. Three related but distinct "component" concepts show up in modern Laravel applications: Blade components (static/server-rendered markup reuse), Livewire components (stateful, interactive server-driven UI), and Flux components (a pre-built Livewire-native UI kit). A fourth term, Blaze, is worth knowing about as well.

Concept What it is Interactive? Laravel-specific?
Blade component Reusable Blade markup, either an anonymous .blade.php file or a class + view pair. No (unless paired with Alpine/Livewire) Laravel/Blade-specific
Livewire component A PHP class (or single-file component) rendering a Blade view with reactive state. Yes (AJAX round-trips, no full reload) First-party package, deeply integrated with Blade
Flux component A Livewire UI kit: pre-built <flux:*> Blade components (inputs, tables…). Depends on the component First-party package, built on top of Livewire/Blade
Blaze A package that pre-compiles Blade/Livewire components ahead of time to speed up first render. N/A (a build/perf tool, not a UI kit) First-party build optimization

Blade components

A Blade component is the most basic form of UI reuse: a chunk of markup that accepts props and slots.

Laravel supports two flavors:

Anonymous component
    resources/views/components/alert.blade.php
    → referenced as <x-alert>

Class-based component
    app/View/Components/Alert.php + resources/views/components/alert.blade.php
    → referenced as <x-alert>

An anonymous component is just a Blade file that declares its expected props with @props:

{{-- resources/views/components/alert.blade.php --}}
@props([
    'type' => 'info',
])

<div {{ $attributes->merge(['class' => "alert alert-{$type}"]) }}>
    {{ $slot }}
</div>

used as:

<x-alert type="error">
    Something went wrong.
</x-alert>

A class-based component pairs a small PHP class with the view, which is useful once a component needs more than trivial logic to compute its data:

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

    public function render(): View
    {
        return view('components.alert');
    }
}

Blade components are ideal for purely presentational, mostly static markup --- buttons, cards, form rows, layout partials. They can still sprinkle in Alpine.js for tiny bits of client-side interactivity, but they have no server-side state of their own and cannot make round-trips back to the server without a separate mechanism (a form submit, an Alpine fetch, or embedding a Livewire component inside them).

Blade component namespaces can also be registered for packages/modules, so a feature's own components can be referenced under a dedicated prefix instead of the global resources/views/components folder:

<x-billing::invoice-row :invoice="$invoice" />

which keeps feature-specific Blade components colocated with the feature that owns them.

Blaze

Blaze is a performance-oriented package in the Livewire ecosystem for optimizing supported anonymous Blade components. It can compile component templates into optimized PHP functions, and its optional folding mode can pre-render suitable components into static HTML.

It is not a general compiler for all Blade and Livewire components, and class-based Blade components are not supported by Blaze. Treat it as an optional build/performance optimization rather than as a component-authoring model.

Livewire components

A Livewire component looks like a Blade component from the outside (<livewire:counter /> or @livewire('counter')), but internally it is backed by a PHP class with reactive public properties and server-side methods. Livewire keeps a "shadow" of the component's state between requests and re-renders only the diffed HTML, giving SPA-like interactivity without writing JavaScript.

See the dedicated Livewire chapter below for a full treatment.

Flux components

Flux is a Livewire UI component library, built by the Livewire team specifically to pair with Livewire's reactivity model. Instead of hand-rolling <input wire:model="..."> + utility classes every time, Flux provides polished, accessible <flux:*> Blade components that already understand wire:model, validation error display, loading states, icons, dark mode, etc.

Example usage:

<flux:card class="space-y-8">
    <flux:input
        type="text"
        placeholder="Add a new item"
        wire:model="newItem"
        wire:keyup.enter="addItem"
    />

    <flux:table class="w-full">
        <flux:table.rows>
            @foreach ($items as $index => $item)
                <flux:table.row wire:key="item-{{ $index }}">
                    <flux:table.cell>
                        <flux:checkbox wire:model.live="items.{{ $index }}.completed" />
                    </flux:table.cell>
                    <flux:table.cell>{{ $item->title }}</flux:table.cell>
                    <flux:table.cell>
                        <flux:button
                            icon="pencil"
                            variant="primary"
                            wire:click="editItem({{ $item->id }})"
                        >
                            Update
                        </flux:button>
                </flux:table.row>
            @endforeach
        </flux:table.rows>
    </flux:table>

    <flux:error name="save" />
</flux:card>

Notice that Flux components are just Blade components under the hood (<flux:input>, <flux:table.row>, etc.), but they are purpose-built to be dropped straight onto Livewire-bound properties (wire:model, wire:click, flux:error). Conceptually:

Blade component
    ↓
generic reusable markup

Flux component
    ↓
Blade component + design system + first-class Livewire wiring

Livewire component
    ↓
stateful PHP class rendering (often Flux-based) Blade markup

A typical page therefore layers all three: a Livewire component class holds the state and behavior, its Blade view is built from Flux components for the UI chrome, and small reusable bits of plain Blade components are composed in for feature-specific fragments.

Filament (admin panels on top of Livewire)

Filament is a package for rapidly building admin panels and back-office CRUD screens. It is not a separate rendering strategy like Inertia --- under the hood, a Filament panel is simply a collection of Livewire components (and Alpine for small client-side bits), wired together by a higher-level set of PHP classes so you rarely hand-write Blade or Livewire yourself for standard CRUD.

The core building block is a Resource, which describes how a single Eloquent model should be listed, created, edited, and viewed:

class OrderResource extends Resource
{
    protected static ?string $model = Order::class;

    public static function form(Schema $schema): Schema
    {
        return $schema->components([
            TextInput::make('total')->numeric()->required(),
            Select::make('status')->options(OrderStatus::class),
        ]);
    }

    public static function table(Table $table): Table
    {
        return $table
            ->columns([
                TextColumn::make('id'),
                TextColumn::make('status')->badge(),
            ])
            ->filters([
                // ...
            ]);
    }
}

Filament reuses many of the same concepts already covered above rather than inventing new ones:

Resource   → uses Policies for authorization, Form Requests-equivalent validation via ->rules()
Form       → schema of typed fields (TextInput, Select, ...), similar in spirit to a Form Request
Table      → columns/filters/actions built from Eloquent queries and Scopes
Widget     → a small Livewire component (charts, stats) embedded on a dashboard

Because Filament is built on Livewire, many Livewire concepts remain relevant: mount(), public properties, #[Computed], actions, and validation all still work --- Filament just provides a declarative, higher-level API so you don't assemble them by hand for routine admin CRUD. Reach for Filament when you need an internal/admin panel quickly; reach for hand-rolled Livewire/Flux (or Inertia) when the UI is customer-facing and needs full control over design and UX.

Inertia (a Blade/Livewire alternative)

Inertia.js is a different approach to the same problem Blade/Livewire solve: building a modern, SPA-like frontend without a separate REST/GraphQL API layer. Instead of returning Blade views, Laravel controllers return Inertia responses that render a Vue, React, or Svelte component, passing data as props --- routing and navigation stay server-driven (Laravel routes, Eloquent, Form Requests), while the actual UI is built entirely in JavaScript components rather than Blade:

return Inertia::render('Orders/Show', [
    'order' => $order,
]);

Briefly, the practical distinction is:

Blade / Livewire
    ↓
HTML rendered (partially or fully) on the server, hydrated with Alpine/Livewire

Inertia
    ↓
JSON props passed to a full JavaScript component (Vue/React/Svelte),
which renders the page entirely client-side

Inertia is not covered in depth here. Inertia and Livewire are normally alternative rendering strategies for a particular page or feature, but they are not mutually exclusive at the application level: one Laravel application can contain both.


3. Livewire in depth

Livewire is a full-stack framework for building dynamic, reactive UIs in PHP without leaving Blade or writing a separate JavaScript SPA. A Livewire component renders normal HTML, but Livewire wires up the DOM so that user interactions (clicks, input, form submits) trigger AJAX requests back to the same PHP class, which reruns, updates its state, and returns just the HTML diff to patch into the page.

Browser
   │  user types / clicks (wire:model, wire:click, ...)
   ▼
Livewire JS runtime
   │  AJAX request with component snapshot + changed data
   ▼
Livewire PHP component (server)
   │  re-hydrate → mutate public properties → call methods → render()
   ▼
Diffed HTML
   │
   ▼
DOM morphed in the browser (no full page reload)

Component authoring styles

Livewire supports a few ways to define a component:

1. Single-file component (SFC) --- class and view live in one file, split by the ?> boundary:

<?php

use Livewire\Component;

new class extends Component
{
    //
};
?>

<div>
    Hello World!
</div>

2. Multi-file component (MFC) --- a colocated component directory containing separate PHP and Blade files, with optional JavaScript, CSS, and tests:

resources/views/components/task-list/
├── task-list.php
├── task-list.blade.php
├── task-list.js       # optional
└── task-list.css      # optional

3. Class-based component --- the traditional Laravel-style split used heavily by Livewire v2/v3 and still supported in v4:

app/Livewire/TaskList.php
resources/views/livewire/task-list.blade.php

All three formats represent Livewire components; the difference is primarily file organization and authoring style.

Reactive properties

Public properties on a Livewire component automatically become part of its client-side state and can be bound with wire:model:

class TaskList extends Component
{
    public array $tasks = [];
    public string $filter = 'all';
    public bool $allCompleted = false;
    public string $newTask = '';
}
<flux:checkbox wire:model.live="allCompleted" />
<flux:input wire:model="newTask" wire:keyup.enter="addTask" />

Key modifiers worth knowing:

wire:model              → sync on form submit / next network request
wire:model.live         → sync immediately on every change
wire:model.live.blur    → sync when the field loses focus
wire:model.live.debounce.500ms → sync after a pause in typing

Attributes

Livewire ships a set of PHP attributes that declaratively add behavior to properties/methods:

#[Title('Task List')]   // sets the <title> for this page component
#[Session]                // persists a property's value in the session between requests
#[Computed]                // memoizes an expensive derived value for the current request
#[Url]                     // binds a property to the query string
#[Locked]                  // prevents client-side tampering with a property
#[Validate('required|min:3')] // validates a property automatically as it's updated
#[On('task-created')]      // listens for a dispatched event

Lifecycle hooks

Livewire components have well-known lifecycle hooks that let you react to specific moments in a request:

mount()            → runs once, when the component is first created
updating($prop)    → runs before a specific property is updated
updated($prop)     → runs after a specific property is updated
                       (Livewire also supports property-specific hooks like
                        updatedAllCompleted($value))
render()           → returns the Blade view (optional for SFC/MFC — inferred)

Example:

public function mount(): void
{
    $this->tasks = $this->taskService()->loadTasks();
}

public function updatedAllCompleted(bool $value): void
{
    foreach ($this->tasks as $index => $task) {
        $this->tasks[$index]->completed = $value;
    }
}

Actions (public methods)

Any public method on a Livewire component is callable directly from the browser via wire:click, wire:submit, etc. These are conceptually similar to controller actions, but scoped to one component:

public function addTask(): void
{
    $title = trim($this->newTask);
    if (!$title) {
        return;
    }

    $this->tasks[] = new Task($title);
    $this->reset('newTask');
}
<flux:input wire:model="newTask" wire:keyup.enter="addTask" />

Livewire also exposes magic actions directly in the markup without a dedicated method, such as $set('tasks.0.title', 'Updated title') to update a single field's value from a button click.

Computed properties

The #[Computed] attribute memoizes a method's result for the duration of a single request, similar to an Eloquent accessor but scoped to the component:

#[Computed]
public function filteredTasks(): array
{
    return match ($this->filter) {
        'pending' => $this->pendingTasks()->all(),
        'completed' => $this->completedTasks()->all(),
        default => $this->tasks,
    };
}

accessed in the view as a property via $this->:

@foreach ($this->filteredTasks as $index => $task)

Validation and authorization inside components

Livewire components can call the same validation and authorization primitives used elsewhere in Laravel:

private function authorizeUpdate(): void
{
    $this->authorize('update', $this->currentTask());
}

private function validateForSave(): bool
{
    $validator = $this->taskService()->validate($this->tasks);

    if ($validator->fails()) {
        $this->addError('save', implode(' ', $validator->errors()->unique()));
        return false;
    }

    $this->resetErrorBag('save');
    return true;
}
<flux:error name="save" />

This keeps Policies and validation rules as the single source of truth for "is this allowed / valid?", while the component simply surfaces the result.

The #[Validate] attribute

Rather than calling $this->validate() manually with a rules array, Livewire lets you attach validation rules directly on the property with the #[Validate] attribute. Livewire then validates that property automatically whenever it changes (e.g. via wire:model.live) and again when you call $this->validate():

use Livewire\Attributes\Validate;

class TaskList extends Component
{
    #[Validate('required|min:3|max:255')]
    public string $newTask = '';

    #[Validate('required|email')]
    public string $notifyEmail = '';

    public function addTask(): void
    {
        $this->validate();

        $this->tasks[] = new Task($this->newTask);
        $this->reset('newTask');
    }
}
<flux:input wire:model="newTask" />
<flux:error name="newTask" />

You can also give a rule a custom attribute name or message inline:

#[Validate('required|min:3', as: 'task title', message: 'Give the task a proper title.')]
public string $newTask = '';

The tradeoff is similar to any declarative-vs-imperative choice: #[Validate] keeps simple, single-property rules colocated with the property (easy to scan), while a dedicated validateForSave()/Form Request-style method is still preferable once rules depend on multiple properties together or need to run conditionally.

Displaying validation errors

Regardless of whether rules come from #[Validate], $this->validate(), or a Form Request, Laravel/Livewire ultimately populate the same underlying MessageBag of errors --- so the same two options are available for displaying them in a Blade view:

With a Flux component --- Most Flux components look up messages for a given field name automatically and render them with consistent styling:

<flux:input label="New Task" wire:model="newTask" />
<flux:error name="newTask" />

With the plain Blade @error directive --- works identically whether the errors came from a Livewire component or a traditional controller + Form Request, and gives you full control over the markup:

<input type="text" wire:model="newTask">

@error('newTask')
    <span class="text-red-600 text-sm">{{ $message }}</span>
@enderror

Both read from the same error bag, so they are interchangeable --- <flux:error> is simply a pre-styled convenience wrapper around the same @error/$errors mechanism every Laravel view already has access to. Reach for @error (or $errors->first('field')) whenever you're not using Flux, or need custom markup that the Flux component doesn't support.

Alpine.js under the hood

Livewire ships with Alpine.js bundled, so Alpine is available on Livewire pages without installing a second copy. Livewire and Alpine integrate closely, but they are distinct systems: Livewire has its own component snapshots, request lifecycle, hydration/dehydration, wire:* directive handling, and DOM morphing runtime. Alpine provides lightweight client-side state and interactivity and can communicate with Livewire through $wire and related integration APIs.

This matters practically because Alpine remains available for purely client-side interactivity that doesn't need a server round-trip --- toggling a dropdown, animating a transition, tracking whether a tooltip is open. Reaching for Alpine instead of a Livewire property/method avoids an unnecessary network request for state that the server never needs to know about:

<div x-data="{ open: false }">
    <button @click="open = !open">Toggle</button>

    <div x-show="open" x-transition>
        Purely client-side panel — no server round-trip on toggle.
    </div>
</div>

Alpine and Livewire also talk to each other directly. The magic @this gives Alpine access to the Livewire component instance, so you can call Livewire methods, read/write Livewire properties, or listen for Livewire-dispatched browser events from plain Alpine markup:

<div x-data="{ shown: false }"
     x-init="$wire.on('task-created', () => { shown = true; setTimeout(() => shown = false, 2000) })"
     x-show="shown"
>
    Task created!
</div>

<button @click="$wire.addTask()">Add via Alpine</button>

A practical rule of thumb:

Needs server data/logic (DB, auth, business rules)?
    → Livewire property / method

Purely visual/ephemeral client state (open/closed, hover, local timers)?
    → Alpine x-data / x-show / x-transition

Keeping this boundary clear avoids two common mistakes: routing trivial UI toggles through the server (unnecessary Livewire round-trips), and pushing real business logic into Alpine (which has no access to the database, authorization, or validation on the server).

Routing to Livewire components

Modern Livewire versions support mapping a URL directly to a full-page component, without needing an intermediate controller:

// routes/web.php
Route::livewire('/tasks', 'TaskList')->name('tasks.index');

Route::livewire('/settings/profile', 'pages::settings.profile')
    ->middleware(['auth'])
    ->name('settings.profile');

Custom component namespaces can be configured (e.g. in config/livewire.php) so full-page components can be organized under resources/views/pages, resources/views/layouts, etc., instead of everything living in one flat resources/views/livewire folder:

'component_namespaces' => [
    'layouts' => resource_path('views/layouts'),
    'pages' => resource_path('views/pages'),
],

'component_layout' => 'components.layouts.app',

A full-page Livewire component (like a controller action) is wrapped in the configured layout and rendered as a complete HTML response, while a Livewire component embedded inside another Blade view (<livewire:task-list />) behaves like a regular nested component.

Where Livewire fits architecturally

Route::livewire(...)  ─────────────►  Livewire component
                                            │
                                            ├── mount() / lifecycle hooks
                                            ├── public properties (wire:model)
                                            ├── #[Computed] derived state
                                            ├── public methods (wire:click)
                                            │        │
                                            │        ├──► Policy (authorize)
                                            │        ├──► Validation rules
                                            │        └──► Service / Action / Model
                                            │
                                            └── render() ──► Blade view (often Flux components)

Livewire components therefore blend responsibilities that would otherwise be split across a Controller, a Form Request, and a View --- which is powerful for interactive UI, but means the same discipline (keep business logic in Services/Actions/Models, not in the component itself) still matters to avoid bloated "god components".


4. Application/business logic

These concepts organize the actual work your application performs.

Concept What it is Typical role Laravel-specific?
Action A class representing one application operation/use case. Performs one specific task such as CreateInvoice. Generic pattern
Service A class containing reusable business/application logic. Coordinates more substantial workflows or capabilities. Generic pattern
Repository An abstraction around data retrieval/persistence. Separates application logic from data-access implementation. Generic pattern
Reporter An abstraction for observing an operation's progress/results. Separates business logic from progress/metric/error reporting. Generic pattern
Helper Usually a globally accessible function or utility. Small reusable stateless operations. Generic pattern
Concern Reusable behavior mixed into classes, usually using PHP traits. Shares implementation across related classes. Generic pattern; Laravel uses the term heavily
Scope Reusable query constraint, particularly in Eloquent. Encapsulates common database query logic. Laravel/Eloquent-specific usage

These are where Laravel applications often differ architecturally because Laravel does not require repositories, services, actions, or DTOs.


Actions

An Action represents one use case.

For example:

CreateOrder
CancelSubscription
ApproveInvoice
RegisterUser
RefundPayment

Example:

class CreateOrder
{
    public function handle(Customer $customer, OrderData $data): Order
    {
        // perform use case
    }
}

Think:

Action = verb

An action answers:

What operation is the application performing?

Advantages:

small
focused
easy to test
easy to reuse
clear responsibility

Actions are not a Laravel feature. They are an architectural convention.


Services

A Service usually represents a broader application capability.

Examples:

BillingService
PricingService
InventoryService
PaymentService
TaxService

Example:

class PricingService
{
    public function calculate(Order $order): Money
    {
        // pricing logic
    }
}

A practical distinction is:

Action
    ↓
one application use case

CreateOrder
CancelOrder
RefundOrder

versus:

Service
    ↓
reusable capability/domain logic

PricingService
TaxService
InventoryService

There is no hard universal rule here.


Repositories

A repository abstracts data access.

Without repository:

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

With repository:

$user = $users->findByEmail($email);

Interface:

interface UserRepository
{
    public function findByEmail(string $email): ?User;
}

Implementation:

class EloquentUserRepository implements UserRepository
{
    public function findByEmail(string $email): ?User
    {
        return User::where('email', $email)->first();
    }
}

Conceptually:

Application
    │
    ▼
Repository interface
    │
    ▼
Eloquent / SQL / API / Elasticsearch

Repositories are generic architecture, not built into Laravel.

Also, Eloquent itself already provides a rich data-access abstraction, so adding repositories everywhere can sometimes produce unnecessary indirection. They are most valuable when you genuinely need to isolate persistence mechanisms or complex query logic.


Reporter

The Reporter pattern separates the responsibility of observing an operation and reporting its progress, results, metrics, warnings, or errors from the operation's core business logic. The business logic stays focused on what it does; the reporter decides what happens with information about how it went.

Without a reporter, progress/logging concerns tend to leak directly into the business logic:

class ImportProducts
{
    public function handle(iterable $rows): void
    {
        $count = 0;

        foreach ($rows as $row) {
            try {
                Product::create($row);
                $count++;

                // reporting concerns entangled with business logic
                echo "Imported {$count}\n";
                Log::info("Imported product {$row['sku']}");
            } catch (Throwable $e) {
                Log::error("Failed to import {$row['sku']}: {$e->getMessage()}");
            }
        }
    }
}

That makes the action hard to reuse (console output tied to a CLI context) and hard to test (assertions must intercept echo/log calls).

With a Reporter, the action only calls a small, injected interface, and remains unaware of how progress is surfaced:

interface ImportReporter
{
    public function progress(int $processed, int $total): void;

    public function warning(string $message): void;

    public function error(string $message, ?Throwable $exception = null): void;

    public function finished(ImportSummary $summary): void;
}
final readonly class ImportSummary
{
    public function __construct(
        public int $imported,
        public int $skipped,
        public int $failed,
    ) {}
}
class ImportProducts
{
    public function __construct(
        private ImportReporter $reporter,
    ) {}

    public function handle(iterable $rows): ImportSummary
    {
        $rows = collect($rows);
        $imported = 0;
        $failed = 0;

        foreach ($rows as $i => $row) {
            try {
                Product::create($row);
                $imported++;
            } catch (Throwable $e) {
                $failed++;
                $this->reporter->error("Row {$i}: {$e->getMessage()}", $e);
            }

            $this->reporter->progress($i + 1, $rows->count());
        }

        $summary = new ImportSummary($imported, 0, $failed);
        $this->reporter->finished($summary);

        return $summary;
    }
}

Different reporters plug into the same action for different contexts:

// Console command: render a progress bar
class ConsoleImportReporter implements ImportReporter
{
    public function __construct(private OutputStyle $output) {}

    public function progress(int $processed, int $total): void
    {
        $this->output->write("\rImporting {$processed}/{$total}");
    }

    public function warning(string $message): void
    {
        $this->output->warn($message);
    }

    public function error(string $message, ?Throwable $exception = null): void
    {
        $this->output->error($message);
    }

    public function finished(ImportSummary $summary): void
    {
        $this->output->newLine();
        $this->output->success("Imported {$summary->imported}, failed {$summary->failed}");
    }
}

For a Livewire-driven UI, there are two quite different ways a reporter can get progress information onto the screen, depending on where the long-running work actually runs:

  • In-request streaming, when the operation runs synchronously inside a Livewire action (e.g. wire:click="import" that runs to completion within that one HTTP request). Livewire 3/4's $this->stream() lets the component push partial output to the browser while the request is still executing, without waiting for the final response. This is ideal for progress bars/logs during a single-request operation, but the reporter must be constructed with (or resolved on) the Livewire component itself, since stream() is a method on the component instance, not a static/global call.
// Livewire component: stream progress from a synchronous action
class ImportProductsComponent extends Component
{
    public function import(ImportProducts $importer): void
    {
        $importer->handle(
            rows: $this->parsedRows(),
            reporter: new LivewireStreamImportReporter($this),
        );
    }
}
class LivewireStreamImportReporter implements ImportReporter
{
    public function __construct(private Component $component) {}

    public function progress(int $processed, int $total): void
    {
        // Pushed to the browser immediately, before the request finishes.
        $this->component->stream(
            to: 'import-progress',
            content: "Imported {$processed}/{$total}",
            replace: true,
        );
    }

    public function warning(string $message): void
    {
        Log::warning($message);
    }

    public function error(string $message, ?Throwable $exception = null): void
    {
        Log::error($message, ['exception' => $exception]);
    }

    public function finished(ImportSummary $summary): void
    {
        $this->component->stream(to: 'import-progress', content: 'Done.');
    }
}
  • Cross-request broadcasting, when the operation runs elsewhere entirely — a queued job, a scheduled command, another user's request — and the browser needs to be notified independently of any particular HTTP request/response cycle. That requires a real Laravel broadcast event (implementing ShouldBroadcast, delivered over a websocket connection via Reverb/Pusher) and a Livewire component that listens for it, either with Laravel Echo JavaScript directly, or declaratively via Livewire's #[On('echo:channel,EventName')] attribute. This is the only option once the reporting and the page rendering happen in different requests/processes.
// Queued job (or any code that isn't the current Livewire request):
// broadcast progress so any listening browser tab can pick it up.
class BroadcastingImportReporter implements ImportReporter
{
    public function __construct(private string $importId) {}

    public function progress(int $processed, int $total): void
    {
        // ShouldBroadcast event -> pushed over the websocket connection
        // (Reverb/Pusher), independent of any Livewire request lifecycle.
        ImportProgressUpdated::dispatch($this->importId, $processed, $total);
    }

    public function warning(string $message): void
    {
        Log::warning($message);
    }

    public function error(string $message, ?Throwable $exception = null): void
    {
        Log::error($message, ['exception' => $exception]);
    }

    public function finished(ImportSummary $summary): void
    {
        ImportFinished::dispatch($this->importId, $summary);
    }
}
// The Livewire component receiving the broadcast event, in a
// completely different request from the one that ran the import.
#[On('echo:imports.{importId},ImportProgressUpdated')]
public function onProgress(int $processed, int $total): void
{
    $this->processed = $processed;
    $this->total = $total;
}

It is easy to conflate these two mechanisms because both end up updating a Livewire component in the browser, but they are not interchangeable: stream() only works while that specific Livewire request is still open, whereas a broadcast event works from anywhere (including after the original request has long since finished) but requires the broadcasting infrastructure (Reverb/Pusher + Echo) to be configured. Livewire's own dispatch()/#[On] event system (component events, not broadcast events) is a third, unrelated mechanism again — it lets sibling components on the same page talk to each other during the same request/response cycle, with no websocket involved at all.

// Tests: capture everything in memory, assert against it
class FakeImportReporter implements ImportReporter
{
    public array $warnings = [];
    public array $errors = [];
    public ?ImportSummary $summary = null;

    public function progress(int $processed, int $total): void {}

    public function warning(string $message): void
    {
        $this->warnings[] = $message;
    }

    public function error(string $message, ?Throwable $exception = null): void
    {
        $this->errors[] = $message;
    }

    public function finished(ImportSummary $summary): void
    {
        $this->summary = $summary;
    }
}

Conceptually:

Business logic (ImportProducts)
        │
        ▼
  Reporter interface
        │
   ┌────┴────┬────────────────┬───────────────┬────────────┐
   ▼         ▼                ▼               ▼            ▼
Console   Livewire         Broadcast        Log-only     Fake/null
progress  ->stream()       event            reporter    (tests, CLI
bar       (same request,   (ShouldBroadcast,             silent mode)
          in-page push)    cross-request via
                           Reverb/Pusher + Echo)

This is closely related to, but distinct from, the Observer pattern: an Observer typically reacts to discrete domain events (OrderPlaced, UserRegistered) that other parts of the system may also react to, whereas a Reporter is usually injected into a single operation specifically to narrate its own execution (progress, partial results, warnings) back to whoever invoked it. A Reporter can be implemented on top of Laravel events/observers, but it doesn't have to be — it can just as easily be plain method calls, as shown above.

A "null object" reporter (one whose methods do nothing) is a common default, so callers that don't care about progress reporting aren't forced to pass one:

class NullImportReporter implements ImportReporter
{
    public function progress(int $processed, int $total): void {}
    public function warning(string $message): void {}
    public function error(string $message, ?Throwable $exception = null): void {}
    public function finished(ImportSummary $summary): void {}
}

The Reporter pattern is generic architecture, not a Laravel feature. It is especially useful for:

long-running imports/exports
data migrations
batch/bulk operations
queued jobs that need to surface progress to a UI
CLI commands with progress bars
any operation whose caller cares about *how* it went, not just its
    final return value

Helpers

Helpers are simple reusable functions.

function money(int $cents): string
{
    return number_format($cents / 100, 2);
}

Good helpers are usually:

small
stateless
pure or nearly pure
generic

Avoid turning helpers into hidden dependency containers such as:

function create_order(...)
{
    DB::transaction(...);
    Mail::send(...);
    PaymentGateway::charge(...);
}

That is application/service logic, not really helper logic.

Laravel itself provides many helper functions such as config(), route(), collect(), etc.


Concerns

In Laravel projects, "Concern" usually means a reusable PHP trait.

Example:

trait HasUuid
{
    protected static function bootHasUuid(): void
    {
        static::creating(function ($model) {
            $model->uuid ??= Str::uuid();
        });
    }
}

Used as:

class Order extends Model
{
    use HasUuid;
}

Concerns are useful for small reusable behavior across related classes.

Examples:

HasUuid
HasSlug
LogsActivity
InteractsWithMedia

But excessive traits can make behavior difficult to trace.

The pattern itself is generic PHP; Laravel merely uses it extensively.


5. Contracts, dependency injection, and framework integration

These concepts control how components depend on one another.

Concept Role Laravel-specific?
Service Container Resolves classes and automatically injects their dependencies. Laravel-specific
Contract Defines an interface/API that implementations must follow. Generic concept; Laravel heavily uses it
Service Provider Registers and bootstraps services into Laravel. Laravel-specific
Facade Static-looking proxy to a service resolved from Laravel's container. Laravel-specific implementation
Cache Stores expensive-to-compute values for fast repeated retrieval. Laravel-specific facade over pluggable stores
Session Persists small pieces of data across requests for one user. Laravel-specific facade over pluggable stores
Storage Abstracts file storage across local/cloud "disks". Laravel-specific
Package Reusable library/module installable into Laravel/PHP projects. Generic package concept with Laravel-specific integration
Macro / Mixin A way to add new methods to an existing class at runtime. Laravel-specific mechanism (Macroable trait)

Laravel's service container performs dependency injection and service providers are the main location for registering bindings and bootstrapping application services. (Laravel)


Service Container

The service container is Laravel's dependency injection mechanism: a registry that knows how to build classes and automatically supply their constructor dependencies.

class OrderController
{
    public function __construct(
        private PaymentGateway $gateway,
    ) {}
}

When Laravel resolves OrderController, it inspects the constructor, sees it needs a PaymentGateway, and resolves that too --- recursively, for any class the container knows how to build. Concrete classes are resolved automatically; interfaces need an explicit binding (usually registered in a Service Provider) telling the container which implementation to use:

$this->app->bind(PaymentGateway::class, StripePaymentGateway::class);

You can also resolve something manually when you're outside of automatic injection (e.g. in a script or closure):

$gateway = app(PaymentGateway::class);

Conceptually:

Contract           → the interface being depended on
Service Provider    → where bindings from Contract → implementation are registered
Service Container   → the mechanism that reads those bindings and performs the injection

Contract

A contract is an interface.

Example:

interface PaymentGateway
{
    public function charge(Money $amount): PaymentResult;
}

Implementations:

StripePaymentGateway
AdyenPaymentGateway
FakePaymentGateway

Business logic depends on:

PaymentGateway

rather than:

StripePaymentGateway

This produces:

CheckoutService
      │
      ▼
PaymentGateway
   /       \
Stripe    Fake

Laravel itself has many interfaces under:

Illuminate\Contracts

But the concept is normal object-oriented programming.


Service Provider

Service providers bootstrap Laravel components.

For example:

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

Now Laravel can inject:

public function __construct(PaymentGateway $gateway)

and automatically provide:

StripePaymentGateway

Service providers may also register:

container bindings
event listeners
routes
configuration
package components

Laravel describes providers as the central place for application bootstrapping. (Laravel)


Facades

Laravel facades provide static-looking access to services stored in the service container. (Laravel)

Example:

Cache::get('users');

looks static, but internally the facade resolves a service from Laravel's container.

Conceptually:

Cache::get()
    ↓
Facade
    ↓
Service Container
    ↓
Cache Manager

Common examples:

Cache
DB
Auth
Gate
Log
Storage
Notification

They offer concise syntax, although constructor dependency injection can sometimes make dependencies more explicit.


Cache

The Cache facade stores values that are expensive to compute or fetch, so subsequent requests can read them instead of repeating the work.

$value = Cache::remember('top-orders', now()->addMinutes(10), function () {
    return Order::orderByDesc('total')->limit(10)->get();
});

Laravel supports multiple cache stores (file, database, Redis, Memcached, array/null for testing) behind the same facade, configured per-application in config/cache.php.


Session

The Session facade (and the session() helper) persists small pieces of data for one user across multiple requests --- flashed validation errors, a shopping cart ID, "remember this tab was active", etc.

session(['locale' => 'de']);
$locale = session('locale', 'en');

Unlike the Cache (shared, keyed data for the whole app), a session is scoped to a single visitor and typically backed by cookies plus a server-side store (file, database, Redis).


Storage

The Storage facade abstracts file storage across configurable disks --- local disk, S3, or any other filesystem driver --- behind one consistent API.

Storage::disk('s3')->put('avatars/1.jpg', $contents);
Storage::disk('local')->exists('reports/2024.csv');

Application code can therefore stay agnostic of where files physically live; switching from local storage to S3 in production is normally a configuration change in config/filesystems.php, not a code change.


Packages

Packages are reusable components distributed through Composer.

Examples include packages providing:

permissions
payments
media handling
debugging
API clients
admin panels

A Laravel package may contain:

service providers
configuration
routes
controllers
views
migrations
commands
facades

Laravel provides dedicated integration mechanisms for package developers. (Laravel)


Macros and Mixins

Macros and mixins let you add new methods to an existing class at runtime, without modifying its source or extending it. Laravel enables this via the Illuminate\Support\Traits\Macroable trait, which many core classes use --- Str, Collection, Request, Response, Router, the query Builder, and others.

Macro

A macro registers a single closure as a new "method" on a macroable class:

use Illuminate\Support\Str;

Str::macro('shout', function (string $value): string {
    return strtoupper($value) . '!';
});

used exactly like a real method, anywhere in the app:

Str::shout('hello'); // "HELLO!"

Macros are normally registered once, in a service provider's boot() method:

class AppServiceProvider extends ServiceProvider
{
    public function boot(): void
    {
        Collection::macro('toUpperTitles', function () {
            return $this->pluck('title')->map(fn ($t) => strtoupper($t));
        });
    }
}

Because the closure is bound to the calling instance, $this inside the macro refers to the object the macro was called on (e.g. the specific Collection instance).

Mixin

A mixin is the same idea, but for adding several methods at once by extracting them into their own class. Each public method on the mixin class becomes an available method on the target class:

class StringMixin
{
    public function shout(): Closure
    {
        return fn (string $value): string => strtoupper($value) . '!';
    }

    public function slugifyUpper(): Closure
    {
        return fn (string $value): string => strtoupper(Str::slug($value));
    }
}

registered with:

Str::mixin(new StringMixin());

which is equivalent to calling Str::macro() once per public method on the mixin --- useful when you have a cohesive set of related additions rather than a single one-off method.

When (not) to reach for macros

Macros/mixins are convenient for:

small, generic, broadly useful helper methods
extending framework/package classes you don't own
package authors offering optional extra behavior

They are usually a poor fit for:

business-specific logic (prefer a Service/Action)
anything that needs its own dependencies injected
behavior that should be easy to discover via IDE autocompletion/static analysis

Because macros are registered dynamically, static analysis tools and "go to definition" often cannot find them without extra IDE helper packages --- which is the main tradeoff against simply writing a normal helper function or a small wrapper class.


6. Authorization, validation, and business rules

These answer different forms of "is this allowed or valid?"

Concept Question it answers Laravel-specific?
Policy May this user perform this action? Laravel-specific implementation of authorization pattern
Form Request Is incoming request data valid? Laravel-specific
Rule Does this particular value satisfy a validation requirement? Laravel-specific validation abstraction
Enum Which finite values/states are valid? PHP language feature / generic domain pattern

Authorization ("may this user do this?") is distinct from authentication ("who is this user?"). Authentication is a core Laravel concern built around guards, user providers, sessions/cookies, middleware, hashing, and the Auth API. Packages such as Fortify can implement common authentication workflows on top of that foundation; they do not replace Laravel's authentication system.


Policy

A policy organizes authorization around a resource/model.

Example:

class OrderPolicy
{
    public function update(User $user, Order $order): bool
    {
        return $order->user_id === $user->id;
    }
}

It answers:

Is this user allowed to perform this operation on this resource?

Example:

Admin → update any order
Customer → update their own order
Guest → update nothing

Laravel's authorization system provides Gates and Policies as its primary authorization mechanisms. (Laravel)

Do not confuse:

validation:
"is quantity >= 1?"

with:

authorization:
"may this user modify this order?"

Gate

A Gate is a simpler, closure-based alternative to a Policy --- useful for authorization checks that aren't tied to a specific Eloquent model.

Gate::define('access-admin-panel', function (User $user) {
    return $user->is_admin;
});
if (Gate::allows('access-admin-panel')) {
    // ...
}
@can('access-admin-panel')
    <a href="/admin">Admin</a>
@endcan

Think:

Gate    → "may this user do X?" (no specific resource involved)
Policy  → "may this user do X to this specific resource?" (e.g. this Order)

Laravel actually implements Policies on top of Gates internally --- a Policy is essentially a Gate scoped to a model class, with its methods auto-discovered by convention.


Rules

Rules encapsulate reusable validation logic.

For example:

class ValidVatNumber implements ValidationRule
{
    public function validate(
        string $attribute,
        mixed $value,
        Closure $fail
    ): void {
        // validation
    }
}

Then:

'vat_number' => [
    'required',
    new ValidVatNumber(),
]

Use a Rule when standard validation rules such as:

required
email
integer
max
exists
unique

are insufficient.


Enums

Enums represent a finite set of possible values.

enum OrderStatus: string
{
    case Pending = 'pending';
    case Paid = 'paid';
    case Cancelled = 'cancelled';
}

Instead of:

if ($status === 'paidd') // typo

you use:

OrderStatus::Paid

Enums can expose additional behavior:

public function getLabel(): ?string
{
    return match ($this) {
        self::Pending => __('Pending'),
        self::Paid => __('Paid'),
        self::Cancelled => __('Cancelled'),
    
    };
}
public function getIcon(): ?string
{
    return match ($this) {
        self::Pending => '...',
        self::Paid => '...',
        self::Cancelled => '...',
    
    };
}

Eloquent models can cast attributes to enums:

protected $casts = [
    'status' => OrderStatus::class,
];
    {{ $order->status->getLabel() }}
    {{ $order->status->getIcon() }}

Enums are PHP language/domain-design concepts, not specifically Laravel.

Laravel integrates nicely with PHP enums through things like casts and validation, but Laravel did not invent them.


Localization

Localization translates user-facing strings instead of hard-coding them in one language.

__('messages.welcome');          // looks up lang/en/messages.php (or lang/en.json)
__('Hello :name', ['name' => $user->name]);
<h1>{{ __('Welcome back!') }}</h1>

Laravel supports both short-key files (lang/en/messages.php, returning an array) and translation-string-as-key JSON files (lang/de.json, mapping the literal English string to its translation) --- this project uses the latter (lang/de.json). The active locale is read from app.locale / App::setLocale(), and enums frequently call __() inside a getLabel()-style method (see Enums) to keep display labels translatable.


Authentication: guards and providers

Laravel's authentication system answers who the current user is and how their identity is maintained.

Two foundational concepts are:

Guard
    = how authentication state is maintained for a request
      (for example, a session-backed web guard)

User provider
    = how the authenticated user is retrieved
      (commonly an Eloquent model)

Typical application code uses the Auth facade, auth() helper, or the authenticated request:

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

if (Auth::check()) {
    // authenticated
}

Routes that require an authenticated user normally use the auth middleware:

Route::middleware('auth')->group(function () {
    Route::get('/account', AccountController::class);
});

Authentication should not be confused with authorization:

Authentication  → who is this user?
Authorization   → may this user perform this action?

Password hashing, password reset, email verification, and session regeneration are related security concerns handled by Laravel's authentication infrastructure and optional authentication packages.

Fortify

Fortify is a first-party, front-end-agnostic package that implements common authentication workflows and routes such as login, registration, password reset, email verification, and optional two-factor authentication.

Fortify builds on Laravel's underlying authentication services. It is optional and ships without a required frontend, so it can be paired with Blade, Livewire/Flux, Inertia, or another UI.

Sanctum

Sanctum provides lightweight authentication for SPAs, mobile applications, and simple API tokens.

Its two common modes are:

First-party SPA
    → cookie/session-based authentication with CSRF protection

API/mobile client
    → personal access token sent with the request

Sanctum is often the appropriate choice when a Laravel application needs authenticated API requests without the complexity of a full OAuth2 server.


7. Event-driven and asynchronous processing

These components let parts of your application react to something without tightly coupling everything together.

Concept Role Laravel-specific?
Event Announces that something happened. Generic pattern; Laravel implementation
Listener Reacts to an event. Generic pattern; Laravel implementation
Job Represents work to perform, often asynchronously. Generic queue concept; Laravel implementation
Notification Represents a user-facing notification across channels. Laravel-specific abstraction
Mailable Represents a single email's envelope and content. Laravel-specific abstraction
Observer Groups model lifecycle event handlers. Generic Observer pattern; Laravel/Eloquent implementation

Laravel explicitly describes its events system as an implementation of the Observer pattern. (Laravel)


Event

An event says:

Something happened.

For example:

OrderPlaced
PaymentReceived
UserRegistered
SubscriptionCancelled

Example:

event(new OrderPlaced($order));

An event should normally describe a fact, rather than command something to happen.

Good:

OrderPlaced

Less ideal:

SendOrderConfirmationEmail

because the latter describes an action rather than an occurrence.


Listener

A listener reacts to an event.

OrderPlaced
      │
      ├── SendConfirmationEmail
      ├── ReserveInventory
      └── UpdateAnalytics

This allows the order-creation code to avoid knowing about every side effect.

Laravel stores event and listener classes separately and dispatches listeners when corresponding events occur. (Laravel)


Job

A Job represents executable work.

Example:

GenerateInvoicePdf
ResizeUploadedImage
ImportLargeCsv
SyncCustomerWithCRM

Jobs are frequently dispatched to a queue:

GenerateInvoicePdf::dispatch($invoice);

Conceptually:

Web Request
    │
    ├── save order
    │
    └── queue expensive task
              │
              ▼
          Queue Worker
              │
              ▼
             Job

This keeps slow work outside the HTTP request.

Laravel's queue system supports multiple queue backends and dependency injection into queued jobs. (Laravel)

Queue workers, retries, failures, chains, and batches

Dispatching a job is only half of queue processing. A queue worker continuously pulls queued jobs and executes them:

php artisan queue:work

Production queue workers are normally supervised by a process manager or a Laravel-oriented deployment/runtime system.

Important queue concepts include:

tries / retryUntil   → how many times or how long a failed job may retry
backoff              → delay between retries
timeout              → maximum execution time
failed jobs          → jobs that exhausted their retry policy
unique jobs          → prevent duplicate queued work
chains               → run jobs sequentially
batches               → coordinate groups of jobs

Queued jobs must be designed with retries in mind. Where possible, make them idempotent so that running the same job more than once does not corrupt state or duplicate external effects.

Laravel Horizon is an optional first-party dashboard and worker-management layer for Redis-backed queues.


Notifications

Notifications model communications sent to users.

Example:

$user->notify(
    new InvoicePaid($invoice)
);

One notification might support several channels:

email
database
SMS
Slack
broadcast/websocket

Conceptually:

InvoicePaidNotification
      │
      ├── Email
      ├── Database
      └── Slack

This differs from an event.

PaymentReceived
    = something happened

PaymentReceiptNotification
    = communicate something to a recipient

Mailable

A Mailable is a class representing a single email's envelope (subject, recipients) and content (view + data).

class ContactFormSubmitted extends Mailable
{
    use Queueable, SerializesModels;

    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly string $messageText,
    ) {}

    public function envelope(): Envelope
    {
        return new Envelope(subject: 'New contact form message');
    }

    public function content(): Content
    {
        return new Content(
            view: 'emails.contact-form-submitted',
            with: ['name' => $this->name, 'email' => $this->email],
        );
    }
}

Sent directly via the Mail facade:

Mail::to($recipient)->send(
    new ContactFormSubmitted($name, $email, $messageText)
);

Think:

Mailable
    = defines one specific email's content/envelope, sent via Mail::to(...)->send(...)

Notification
    = a user-facing message that may go out over several channels (mail, database, Slack...),
      and can itself use a Mailable-like `toMail()` method for its email channel

Use a Mailable directly when email is the only delivery channel and there's no need for the notification abstraction's multi-channel dispatch or database storage.


Observer

An Eloquent observer groups model lifecycle handlers.

For example:

class UserObserver
{
    public function created(User $user): void
    {
    }

    public function deleted(User $user): void
    {
    }
}

It reacts to Eloquent events such as:

creating
created
updating
updated
deleting
deleted

Use observers when the behavior is tightly associated with model lifecycle events.

Be cautious with significant hidden business logic in observers because:

$user->save();

may unexpectedly trigger many unrelated effects.


8. Database

These concepts manage database structure, querying, and sample data.

Concept Role Laravel-specific?
Eloquent ORM tying models, query builder, relationships, casts, and scopes together. Laravel-specific implementation of the ActiveRecord pattern
Scopes Reusable, named query fragments. Laravel/Eloquent-specific usage
Relationships Declares how models are connected (hasMany, belongsTo, ...). Laravel/Eloquent-specific implementation
Eager Loading Pre-loads relationships to avoid the N+1 query problem. Laravel/Eloquent-specific abstraction
Accessors and Casts Transform attribute values on read/write or by declarative type conversion. Laravel/Eloquent-specific implementation
Query Builder Fluent, chainable SQL construction underlying Eloquent. Laravel-specific implementation of the query builder pattern
Pagination Splits large result sets into pages. Laravel-specific implementation
Database Transactions Groups database writes so they succeed or roll back atomically. Laravel implementation of generic transaction pattern
Mass Assignment Controls which attributes may be set in bulk via $fillable/$guarded. Laravel-specific abstraction
Soft Deletes Marks records as deleted without removing the row. Laravel-specific implementation
Migration Defines database schema changes. Laravel implementation of generic migration pattern
Seeder Inserts predefined/sample data. Laravel-specific abstraction
Factory Generates model instances/test records. Laravel-specific implementation of factory pattern

Eloquent

Eloquent is Laravel's built-in ORM (Object-Relational Mapper): it lets you interact with database tables using PHP classes (Models) instead of writing raw SQL. A single Model class combines several responsibilities that, together, make up "Eloquent":

Model             → one class per table (e.g. Order)
Query Builder     → fluent, chainable query construction (where, orderBy, ...)
Relationships     → hasMany, belongsTo, belongsToMany, ...
Casts / Accessors → transform raw column values into richer PHP types
Scopes            → reusable, named query fragments
Collections       → results are returned as Illuminate\Support\Collection instances

Basic usage:

class Order extends Model
{
    protected $casts = ['total' => 'decimal:2'];

    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }
}
$order = Order::find(1);
$order->total;
$order->customer;                      // relationship, lazy-loaded

Order::where('status', 'paid')
    ->orderByDesc('created_at')
    ->get();

Order::create(['total' => 42, 'customer_id' => 1]);

Each Model corresponds to a database table by convention (Orderorders), and each row is hydrated into an instance of that class, giving you an object-oriented API over the underlying SQL.

Eloquent builds directly on top of Laravel's Query Builder — every Eloquent query eventually compiles down to the same fluent SQL construction, just with model hydration, relationships, casts, and scopes layered on top:

Query Builder  → generic fluent SQL construction (DB::table(...))
Eloquent       → Query Builder + Model hydration + Relationships + Casts + Scopes

The concepts that make up Eloquent are each explained in their own dedicated section elsewhere in this document: Model, Relationships, Accessors and Casts, Scopes, and Query Builder. This section exists mainly as the entry point that ties them together and explains how they relate to database structure (Migrations) and test/sample data (Seeders, Factories) covered below.


Scopes

Scopes encapsulate reusable Eloquent query logic.

Instead of repeatedly writing:

Order::where('status', 'paid')
    ->whereNull('cancelled_at')
    ->get();

you could define:

public function scopeActive($query)
{
    return $query
        ->where('status', 'paid')
        ->whereNull('cancelled_at');
}

then:

Order::active()->get();

Think:

Scope = reusable query fragment

Relationships

Relationships describe how Eloquent models are connected.

class Order extends Model
{
    public function customer(): BelongsTo
    {
        return $this->belongsTo(Customer::class);
    }

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

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

Common relationship types:

hasOne / hasMany        → one row owns one/many related rows
belongsTo               → this row belongs to one related row
belongsToMany           → many-to-many, usually via a pivot table
hasManyThrough          → access a distant relation through an intermediate model
morphTo / morphMany     → polymorphic relationships (one relation, several model types)

Relationships are accessed like properties (lazy-loaded on first access) or eagerly loaded to avoid the N+1 query problem:

$order->customer;                 // lazy-loaded
Order::with('customer', 'items')->get();   // eager-loaded

Eager Loading

Eager loading pre-fetches a model's relationships in the same handful of queries as the parent query, instead of running one extra query per row the first time each relationship is accessed (the N+1 query problem):

// N+1: 1 query for orders, then 1 query per order for its customer
foreach (Order::all() as $order) {
    echo $order->customer->name;
}

// Eager loading: 1 query for orders + 1 query for all their customers
foreach (Order::with('customer')->get() as $order) {
    echo $order->customer->name;
}
with('relation')            → eager load a relationship
with('relation.nested')     → eager load a relationship's own relationship
with(['relation' => fn ($q) => $q->where(...)])   → constrain what's loaded
load('relation')            → eager load onto an already-fetched collection/model
loadCount('relation')       → load only the related row count, not the rows
withCount('relation')       → same, applied to the query before it runs

Laravel ships with a preventLazyLoading() guard for local/testing environments, which throws when code accesses a relationship that wasn't eager-loaded --- a useful way to catch N+1 queries before they reach production.

Model::preventLazyLoading(! app()->isProduction());

Accessors and Casts

An accessor/mutator transforms a model attribute when it's read or written, using the Attribute class:

use Illuminate\Database\Eloquent\Casts\Attribute;

protected function fullName(): Attribute
{
    return Attribute::make(
        get: fn () => "{$this->first_name} {$this->last_name}",
        set: fn (string $value) => [
            'first_name' => Str::before($value, ' '),
            'last_name' => Str::after($value, ' '),
        ],
    );
}

A cast converts a raw database value into a richer PHP type automatically, without a dedicated accessor:

protected $casts = [
    'settings' => 'array',
    'is_active' => 'boolean',
    'status' => OrderStatus::class,   // enum cast, see Enums
    'total' => MoneyCast::class,      // custom cast class
];

Think:

Accessor/Mutator = custom transformation logic for one attribute

Cast = declarative type conversion (built-in or a reusable custom Cast class)

Both keep transformation logic on the model itself rather than scattering json_decode()/(bool) casts throughout the codebase wherever the attribute is used.


Query Builder

The Query Builder is Illuminate's fluent API for constructing SQL queries without writing raw SQL, and it underlies Eloquent itself.

DB::table('orders')
    ->where('status', 'paid')
    ->orderBy('created_at', 'desc')
    ->limit(10)
    ->get();

Eloquent models expose the same fluent methods (where, orderBy, join, ...) on top of the query builder, and a Scope is simply a named, reusable fragment of query-builder calls:

Query Builder  → generic fluent SQL construction (DB::table(...) or Model::query())
Scope          → a reusable, named slice of query-builder calls (Order::active())
Eloquent       → query builder + model hydration + relationships

Reach for the query builder directly (DB::table(...)) for one-off queries against tables with no corresponding model, or for performance-sensitive queries where you don't need model hydration.


Pagination

Pagination splits a large result set into pages, instead of loading (and rendering) every row at once.

$orders = Order::where('status', 'paid')
    ->orderByDesc('created_at')
    ->paginate(15);

->paginate() runs a second count() query so it knows the total number of pages, and returns a LengthAwarePaginator --- which behaves like a collection of the current page's items, but also carries the page metadata (current page, total, per-page, ...).

In a Blade view, that paginator renders its own links out of the box:

@foreach ($orders as $order)
    <p>{{ $order->id }}</p>
@endforeach

{{ $orders->links() }}

->links() outputs the "Previous / 1 2 3 / Next" pagination control, styled to match the configured pagination view (Tailwind by default), and automatically preserves any other query-string parameters (e.g. ?status=paid&page=2) when a link is clicked.

A lighter alternative is ->simplePaginate(), which only fetches "is there a next page?" without the extra count() query --- useful when you only need Previous/Next links and don't need to display the total number of pages.

paginate()        → Previous/Next + page numbers, one extra COUNT query
simplePaginate()  → Previous/Next only, no COUNT query (cheaper)
cursorPaginate()  → Previous/Next using a cursor instead of an offset (efficient for very large tables)

Livewire components paginate the same way --- a component using the WithPagination trait exposes ->paginate() results and wire:click on the rendered links updates the page without a full reload.


Database Transactions

A transaction makes a group of database writes atomic: either all of them succeed or all of them are rolled back.

DB::transaction(function () use ($orderData) {
    $order = Order::create($orderData);

    Inventory::reserveFor($order);
    Payment::recordFor($order);
});

Transactions are important when a business operation changes several rows or tables that must remain consistent. Validation does not provide atomicity; a transaction protects the persistence boundary when failures occur midway through the operation.

Be careful with external side effects such as sending email or calling a payment API from inside a long-running database transaction. Where appropriate, dispatch events/jobs after commit.


Mass Assignment

Eloquent supports assigning many attributes at once:

Order::create($request->validated());
$order->fill($data);

Because input arrays can contain fields the caller should not be allowed to set, models define which attributes may be mass assigned using $fillable or $guarded.

class Order extends Model
{
    protected $fillable = [
        'customer_id',
        'shipping_address',
    ];
}

Mass-assignment protection is not a substitute for validation or authorization. These are separate boundaries:

Validation          → is the input structurally valid?
Authorization       → may this user perform the operation?
Mass assignment     → which model attributes may be assigned in bulk?

Soft Deletes

The SoftDeletes trait marks a record as deleted by setting deleted_at instead of immediately removing the row.

use Illuminate\Database\Eloquent\SoftDeletes;

class Order extends Model
{
    use SoftDeletes;
}

Queries normally exclude soft-deleted rows. You can explicitly include or select them:

Order::withTrashed()->find($id);
Order::onlyTrashed()->get();

$order->restore();
$order->forceDelete();

Use soft deletes when restoration or historical retention is part of the application's requirements; do not treat them as a universal replacement for real deletion.


Migration

A migration changes database structure.

Schema::create('orders', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id');
    $table->decimal('total', 10, 2);
    $table->timestamps();
});

Think:

Git commits → source code history

Migrations → database schema history

Laravel itself describes migrations as effectively version control for your database schema. (Laravel)


Seeder

Seeders populate a database with known data.

Example:

class RoleSeeder extends Seeder
{
    public function run(): void
    {
        Role::create(['name' => 'admin']);
        Role::create(['name' => 'customer']);
    }
}

Typical uses:

default roles
countries
permissions
development data
demo environments

Think:

Migration → create table

Seeder → populate table

Factory

Factories generate model data.

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

The factory defines how to generate a model's attributes, often using Faker for realistic random data.

public function definition(): array
{
    return [
        'name' => fake()->firstName() . ' ' . fake()->lastName(),
        'email' => fake()->email(),
        'active' => fake()->boolean(85), // chance of 85% to be true
        'uuid' => (string) Str::uuid(),
    ];
}

They are particularly useful for tests:

$user = User::factory()->create();

Laravel factories define default attributes for Eloquent models and are designed primarily for testing and database seeding. (Laravel)

Difference:

Factory
    ↓
"give me realistic/random objects"

Seeder
    ↓
"put this dataset into the database"

Seeders often use factories.


9. Application configuration, security, and external services

Configuration and environment

Laravel application configuration lives in config/*.php. Environment variables are normally read by those configuration files:

// config/services.php
'example' => [
    'key' => env('EXAMPLE_API_KEY'),
],

Application code should normally read configuration through config() rather than calling env() throughout the codebase:

$key = config('services.example.key');

Production deployments commonly cache configuration:

php artisan config:cache

Once configuration is cached, application code should rely on configuration values rather than direct environment lookups.


Exception Handling and Logging

Exceptions represent failures that interrupt normal execution. Laravel's exception pipeline determines how exceptions are reported and how they are rendered into HTTP responses.

Use exceptions for exceptional failure paths, and use logging to record operational information:

Log::info('Order created', ['order_id' => $order->id]);
Log::error('Payment gateway failed', ['exception' => $e]);

Do not expose sensitive exception details to end users in production. Domain-specific exceptions can make application failure modes clearer than returning ambiguous booleans or null.


CSRF Protection

Cross-Site Request Forgery protection prevents another site from causing a user's browser to submit an authenticated state-changing request without the application's consent.

Traditional Blade forms include a CSRF token:

<form method="POST" action="/orders">
    @csrf
    ...
</form>

Laravel's web middleware validates the token for state-changing web requests. CSRF protection is especially important to understand alongside session authentication: the browser automatically sends authentication cookies, so the application needs a separate mechanism to verify that a state-changing request originated from the intended frontend.


Rate Limiting

Laravel's rate limiter restricts how often an action may occur during a time window. HTTP routes commonly use throttling middleware, while application code can use the RateLimiter abstraction directly.

Typical uses include:

login attempts
password-reset requests
API endpoints
expensive operations
verification-code sending

Rate limiting is not authorization: an authorized user may still be throttled because they are making too many requests.


HTTP Client

Laravel's HTTP client provides a fluent API for calling external HTTP services:

$response = Http::timeout(5)
    ->retry(3, 200)
    ->get('https://example.test/api/orders');

$response->throw();

$data = $response->json();

Important concerns include timeouts, retries, error handling, authentication headers, and testing with Http::fake().

External HTTP calls should usually live behind an application service or gateway abstraction when they represent an important external dependency.


10. CLI and scheduled/application operations

Commands

Commands are CLI entry points, usually Artisan commands.

Example:

php artisan invoices:cleanup

Class:

class CleanupInvoices extends Command
{
    protected $signature = 'invoices:cleanup';

    public function handle(): int
    {
        // ...
    }
}

They are useful for:

maintenance
imports
data repair
scheduled tasks
administrative operations
deployment tasks
developer tooling

A command should generally invoke application logic, rather than contain all that logic itself.

For example:

Command
   ↓
CleanupExpiredInvoices action
   ↓
application/domain logic

This allows the same operation to be triggered from HTTP, queues, tests, or CLI.


Task Scheduling

Task scheduling defines when Commands (or closures) should run automatically, replacing a pile of manually configured cron entries with one entry that calls Laravel's scheduler:

// routes/console.php (or a dedicated schedule file)
Schedule::command('invoices:cleanup')->dailyAt('01:00');
Schedule::call(fn () => Report::generate())->weekly();

A single cron entry runs the scheduler every minute, and Laravel decides which of the defined tasks are actually due:

* * * * * php artisan schedule:run >> /dev/null 2>&1

Think:

Command
    = what to run (php artisan invoices:cleanup)

Task Scheduling
    = when to run it (dailyAt, hourly, weekly, cron expressions, ...)

11. Data objects and domain modelling

These objects deliberately represent data rather than framework infrastructure.

Concept Main purpose Laravel-specific?
DTO Transport structured data between layers. Generic
Value Object Model a domain value with rules/behavior. Generic
Response Object Represent the result of an operation/API call. Generic

DTO --- Data Transfer Object

A DTO groups related data.

Instead of:

createOrder(
    $userId,
    $address,
    $currency,
    $coupon,
    $items,
    $note
);

you might have:

final readonly class CreateOrderData
{
    public function __construct(
        public int $userId,
        public AddressData $address,
        public Currency $currency,
        public array $items,
        public ?string $coupon,
    ) {}
}

Then:

$action->handle($data);

The primary purpose is:

transport immutable structured data between boundaries

Examples:

Request → DTO → Action

API response → DTO → Service

Job → DTO → Handler

DTOs normally contain little or no business behavior.

They are not Laravel-specific.


Value Object

A Value Object represents a meaningful domain concept.

Examples:

Money
EmailAddress
PhoneNumber
Coordinates
Percentage
DateRange
VatNumber

Example:

final readonly class Money
{
    public function __construct(
        public int $cents,
        public Currency $currency,
    ) {
        if ($cents < 0) {
            throw new InvalidArgumentException();
        }
    }
}

The important distinction from a DTO is:

DTO
    = package/transport data

Value Object
    = model a domain concept

For instance:

new Money(1000, Currency::EUR)

is not simply a bag of fields. It can enforce invariants and implement meaningful operations:

$money->add($other);
$money->multiply(2);
$money->isGreaterThan($other);

Value Objects are a generic Domain-Driven Design concept.


Response Object

A response object represents the result of an operation.

For example, a payment API might return:

final readonly class PaymentResult
{
    public function __construct(
        public bool $success,
        public ?string $transactionId,
        public ?string $failureReason,
    ) {}
}

Then:

$result = $gateway->charge($payment);

if ($result->success) {
    // do something with $result->transactionId
} else {
    // handle $result->failureReason
    // log, retry, etc.
}

This is useful when returning:

success/failure information
metadata
external IDs
warnings
status information

without returning unstructured arrays such as:

[
    'success' => true,
    'id' => 'abc',
    'error' => null,
]

A response object typically exposes static methods for success/failure construction:

public static function success(string $transactionId): self
{
    return new self(true, $transactionId, null);
}

public static function failure(string $failureReason): self
{
    return new self(false, null, $failureReason);
}
return PaymentResult::success('abc');
return PaymentResult::failure('insufficient funds');

A response object is generic architecture, not Laravel-specific.

Do not confuse it with Laravel's HTTP:

Illuminate\Http\Response

which represents an actual HTTP response.


12. Miscellaneous concepts

Collections

Laravel's Collection is a fluent wrapper around arrays and iterable data. Eloquent queries returning multiple models normally return an Eloquent\Collection, which extends Laravel's base collection.

$totals = Order::query()
    ->where('status', 'paid')
    ->get()
    ->pluck('total')
    ->filter()
    ->sum();

Common collection methods include map, filter, reduce, groupBy, keyBy, pluck, first, and sum.

A Collection is an in-memory data structure. Do not confuse collection operations with query-builder operations: filtering in SQL before get() is usually preferable to loading a huge result set and filtering it in PHP.


Broadcasting and Reverb

Broadcasting sends server-side events to connected clients in real time, commonly over WebSockets.

Laravel event
    ↓
broadcast
    ↓
WebSocket server / broadcaster
    ↓
browser client

Laravel's broadcasting layer can work with different broadcasters. Reverb is Laravel's first-party WebSocket server and integrates with Laravel Echo on the client.

Broadcasting is useful for chat, live dashboards, presence indicators, progress updates, and other interfaces where the server needs to push state changes to the browser without polling.


Processes

Laravel's Process facade provides an expressive, fluent API for invoking external command-line processes from PHP, wrapping Symfony's Process component.

use Illuminate\Support\Facades\Process;

$result = Process::run('ls -la');

$output = $result->output();
$exitCode = $result->exitCode();
$ok = $result->successful();

Processes can also be run with a timeout, piped together, run concurrently ("pools"), or run asynchronously in the background:

Process::timeout(120)->run('php artisan queue:work --once');

$process = Process::start('php artisan queue:listen');

while ($process->running()) {
    // ...
}

Process invocation is a Laravel-specific abstraction around shelling out to the operating system — useful for calling external tools (image processors, build scripts, other CLI programs) without writing raw exec()/shell_exec() calls. During testing, Process::fake() lets you record expectations and return fake output without actually running any command. (Laravel)


Concurrency

The Concurrency facade lets you run multiple PHP closures at the same time, instead of sequentially, and get back all of their results once they've all finished.

use Illuminate\Support\Facades\Concurrency;

[$serverUptime, $orderCount] = Concurrency::run([
    fn () => Http::get('https://example.com/uptime')->json('uptime'),
    fn () => DB::table('orders')->count(),
]);

Under the hood, each closure is executed in its own PHP process (spawned via the Process facade), so this is best suited for independent, CPU/IO-bound tasks rather than things that need to share state or a database transaction. There's also a defer method to run the closures after the HTTP response has already been sent to the browser:

Concurrency::defer([
    fn () => Log::info('Order processed'),
    fn () => Metrics::increment('orders.processed'),
]);

Concurrency is a Laravel-specific convenience layer for parallelizing otherwise-sequential PHP work; it does not replace queued jobs for long-running or unreliable work, since it's still bound to the lifetime of the current request/command. (Laravel)


Context

The Context facade captures key/value data and attaches it to every log entry written during the current request, job, or command — without having to manually pass that data down through every layer of the application.

use Illuminate\Support\Facades\Context;

Context::add('request_id', (string) Str::uuid());
Context::add('user_id', $request->user()?->id);

Log::info('Order placed');
// Log entry automatically includes request_id and user_id

Context is especially useful for correlating log lines from a single request/job across multiple classes, queued jobs dispatched from that request, and even across HTTP calls to other services (Laravel can automatically add the current context as headers to outgoing Http:: requests, and read it back on the receiving end). Context data set before a job is dispatched is also captured and made available again when the job runs.

Context is a Laravel-specific abstraction for structured, ambient logging metadata; it is not a general-purpose request-scoped container for arbitrary application state. (Laravel)


13. Testing

Tests

Tests automatically verify application behavior.

Laravel applications normally distinguish between:

Unit tests

and:

Feature tests

Laravel ships with testing infrastructure and integrations for Pest and PHPUnit. (Laravel)

Unit test

Tests an isolated class or small piece of logic.

test('money can be added', function () {
    $a = new Money(500, Currency::EUR);
    $b = new Money(300, Currency::EUR);

    expect($a->add($b)->cents)->toBe(800);
});

Usually:

fast
few dependencies
no HTTP
often no database

Feature test

Tests application behavior through several layers.

test('customer can create order', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->post('/orders', [
            // ...
        ])
        ->assertCreated();
});

A feature test might exercise:

route
middleware
request validation
controller
action/service
database
resource

Testing itself is generic; Laravel provides extensive testing utilities.


14. Craft CMS 6 concepts on top of Laravel

Craft 6 status: Craft CMS 6.x is currently unreleased/alpha documentation. The concepts in this chapter reflect the public 6.x documentation available in August 2026 and may change before the stable release.

Note: This chapter for now is 100% AI-generated. It is intended to provide a high-level overview of Craft 6 concepts and their relationship to Laravel. It may contain inaccuracies or omissions, and should not be relied upon as authoritative documentation.

This chapter is intended to be reviewed and rewritten by developers familiar with Laravel, with the goal of explaining Craft 6’s architecture and how it builds on Laravel.

Craft 6 is a Laravel application, so most ordinary Laravel concepts in this document remain directly relevant. Craft adds a CMS/domain layer on top of Laravel: elements, fields, project config, control-panel conventions, plugin infrastructure, and compatibility abstractions for concepts that existed in Craft 5/Yii.

A useful mental model is:

Laravel
├── HTTP routing / middleware / requests
├── Service container / dependency injection
├── Service providers
├── Eloquent / database
├── Validation
├── Events / listeners
├── Queues / commands
├── Filesystem disks
└── Blade + general application infrastructure
        │
        ▼
Craft CMS 6
├── Elements + element queries
├── Fields + field layouts
├── Components / data objects
├── Project Config
├── Control Panel
├── Craft users + permissions
├── Twig/site rendering
├── Craft services + registries
├── Plugins
└── Craft-specific content and extension APIs

The important consequence is that Craft concepts should not be translated mechanically into Laravel concepts. Some are direct Laravel concepts, some are Craft-specific abstractions built on Laravel, and some deliberately coexist with a similarly named Laravel concept.


Craft Model vs Eloquent Model vs Component

This is one of the most important terminology changes in Craft 6.

Craft 6 adopts Laravel's meaning of Model: a model is primarily an Eloquent persistence object representing database data.

Many classes that would have been Craft 5 craft\db\Record classes now become Eloquent models, commonly extending:

CraftCms\Cms\Shared\BaseModel

Craft's BaseModel adds Craft-oriented persistence conventions such as standardized dateCreated, dateUpdated, and dateDeleted columns. UIDs can be added through:

CraftCms\Cms\Shared\Concerns\HasUid

Craft core intentionally keeps these database models narrow. They describe persisted database data rather than serving simultaneously as validation objects, event emitters, service objects, and general-purpose domain containers.

Conceptually:

Craft 5
Model   → validation/data object
Record  → database persistence

Craft 6
Eloquent Model / BaseModel
        → database persistence

Component / ordinary PHP object
        → configurable/validatable data object when needed

This distinction is a Craft architectural convention, not a restriction imposed by Eloquent. Plugin authors may still put additional behavior or validation on Eloquent models when appropriate.

Relation to generic Laravel concepts

Craft 6 concept Closest Laravel concept Important difference
BaseModel Eloquent Model Craft adds CMS persistence conventions such as Craft timestamps and optional UIDs.
Craft 5 Record Eloquent Model Most former records migrate toward Eloquent persistence models.
Craft 5 Model DTO / validatable object / Component It no longer maps cleanly to the Laravel word "Model."
Element No direct equivalent An Element is a Craft CMS content/domain abstraction, not merely an Eloquent model.

The naming distinction matters whenever someone says "model" in a Craft 6 codebase: by default, read it in the Laravel/Eloquent sense unless the context clearly refers to a Craft Component or another data object.


Components and Data Objects

Craft 6 retains a Component abstraction for objects that need some of the conveniences historically associated with Craft/Yii models.

Most classes that previously extended craft\base\Model can instead extend:

CraftCms\Cms\Component\Component

A Component can provide features such as:

constructor-based configuration / mass assignment
automatic typecasting
validation
array-style access
macro-like extensibility

Craft also allows these capabilities to be adopted individually instead of requiring inheritance from Component.

For validation:

CraftCms\Cms\Validation\Contracts\Validatable
CraftCms\Cms\Validation\Concerns\Validates

Reusable validation rulesets can be represented with:

#[CraftCms\Cms\Validation\Ruleset(...)]

Arbitrary objects can be configured/typecast with:

CraftCms\Cms\Support\Typecast::configure(...)

Relation to DTOs and Value Objects

A Craft Component is not synonymous with a DTO.

DTO
    → generic architectural concept
    → carries structured data

Craft Component
    → Craft convenience abstraction
    → configurable object that can opt into validation,
      typecasting, array access, macros, etc.

If an object only transports already-valid data, an ordinary PHP DTO may be enough. Craft's own 6.x architecture explicitly takes advantage of Laravel's input validation to eliminate some small data-only model classes altogether.

Use a Component when the Craft-specific behavior is useful; do not extend it merely because an object contains data.


Elements

Elements are one of Craft's central domain concepts and have no direct Laravel equivalent.

Entries, assets, users, categories, and other CMS-managed objects are element types. Elements provide a common content abstraction above the persistence layer.

Conceptually:

Eloquent Model
    → database row / persistence model

Craft Element
    → CMS-managed content object
      with Craft lifecycle, fields, sites,
      permissions, revisions/drafts, etc.

An Element should therefore not be mentally reduced to "Craft's version of an Eloquent model." Craft uses Laravel/Eloquent underneath the application, but Elements exist at a higher CMS/domain layer.

This distinction becomes particularly important when writing application logic:

Need raw application persistence?
    → Eloquent model

Need a Craft entry, asset, user, or other CMS object?
    → Element

Need to find CMS content?
    → Element Query

Element Queries

An Element Query is Craft's content-query abstraction for locating elements.

It plays a role superficially similar to an Eloquent query builder:

Eloquent:
Order::query()
    ->where(...)
    ->get();

Craft:
Entry::find()
    ->section(...)
    ->site(...)
    ->all();

But the abstractions are not interchangeable.

An Eloquent query primarily describes database predicates against models/tables. An Element Query understands Craft-specific content semantics such as element types, sites, statuses, fields, drafts/revisions, and other CMS state.

Think:

Eloquent Query Builder
    = query persisted application records

Element Query
    = query Craft CMS content objects

Use Eloquent when working with your own persistence models. Use Element Queries when working with Craft-managed content.


Fields and Field Layouts

Fields are Craft concepts, not Laravel concepts.

A field defines configurable content attached to Elements. Field layouts determine which fields and UI elements appear for a particular content-editing context.

Examples include text fields, relational fields, asset fields, Matrix-like structured content fields, and plugin-defined field types.

A field type has two distinct validation concerns in Craft 6:

getRules()
    → validates the field type's own settings

getElementRules()
    → validates values stored on Elements using that field

This maps imperfectly to Laravel validation:

Laravel validation rule
    → validates an input/value

Craft field
    → defines a CMS content capability,
      its settings, storage/normalization behavior,
      UI, and element-level validation

Field types may be registered declaratively by plugins or through Craft's field-type registration mechanisms. Field layout elements have their own registration events.

Do not model a Craft Field as an Eloquent column mentally. A field is a CMS schema/configuration object whose content may participate in Craft's broader content storage system.


Project Config

Project Config is Craft-specific and has no direct Laravel equivalent.

Laravel configuration normally means:

.env
config/*.php
config()

Craft Project Config serves a different purpose: it tracks CMS schema and administrative configuration so that structural changes can be version-controlled and propagated between environments.

Typical Project Config state includes things such as:

fields and field layouts
sections / entry types
sites
asset volumes
user groups
plugin settings/schema
other control-panel-managed system configuration

The distinction is:

Laravel config
    → runtime/application configuration

Craft Project Config
    → version-controlled CMS schema and administrative state

Database content
    → entries, users, assets, and other content/state

This matters for plugin/application architecture. If something represents developer/admin-controlled project structure, it may belong in Project Config rather than being treated as ordinary mutable database content.

Project Config is conceptually closer to declarative application schema/configuration than to Laravel's config() repository.


Craft Services

Craft 6 substantially aligns services with Laravel's service container.

A Craft service is generally just an ordinary PHP class:

#[Illuminate\Container\Attributes\Singleton]
class Manager
{
    public function generate(Template $template): Report
    {
        // ...
    }
}

It can be resolved through dependency injection:

public function handle(Manager $manager)
{
    // ...
}

or explicitly:

$manager = app(Manager::class);

Craft may also expose a Laravel Facade for a service.

Relation to the generic Service concept

This maps closely to the Service section earlier in this document:

Generic Service
    → reusable application capability

Laravel container
    → resolves and injects it

Craft service
    → ordinary service class using Laravel DI,
      sometimes marked #[Singleton]

The major Craft 5 → Craft 6 change is the disappearance of the central service-locator architecture as the normal access pattern.

Prefer:

public function __construct(
    private Reports $reports,
) {}

over repeatedly reaching through a global Craft application object.

Services marked #[Singleton] should be resolved through the container rather than instantiated manually with new.


Craft Actions and Atomized Operations

Craft 6's internal refactoring uses small invokable/action-like classes for focused operations.

This maps directly to the generic Action pattern described earlier:

Craft/Laravel Action
    → one focused operation

Service
    → broader capability/API

Controller / Command / Job
    → transport/execution entry point that can invoke the action

A small operation can therefore be reused from:

HTTP controller
Artisan command
queue job
another service

This is Laravel architecture rather than a special Craft base class. Craft's adoption of it is significant because Craft 6 intentionally has shallower inheritance trees and more composition/dependency injection.


Plugins

A Craft plugin remains a distributable Craft extension, but in Craft 6 its base class is also a Laravel Service Provider.

Plugins extend:

CraftCms\Cms\Plugin\Plugin

and are Composer packages discoverable by Laravel/Craft.

Conceptually:

Composer package
        │
        ▼
Laravel Service Provider
        │
        + Craft plugin metadata/lifecycle
        + Craft feature registration
        + optional CP/settings integration
        ▼
Craft Plugin

This means the generic Laravel concepts of Package, Service Provider, Service Container, Routes, Commands, and Events all apply directly to Craft plugin development.

Plugin initialization follows Laravel's provider lifecycle:

application creation
    ↓
provider discovery
    ↓
provider registration
    ↓
provider booting
    ↓
routing / normal application execution

Craft adds plugin-specific lifecycle hooks and declarative concerns, but plugin code should respect Laravel's distinction between register-time container configuration and boot-time application integration.


Project Code vs Plugins vs Former Modules

Craft 5 had a strong distinction between plugins and project-specific modules.

In Craft 6, project-specific application code should generally live in the normal:

App\

namespace.

The Laravel equivalent of the old project module entry point is normally a Service Provider registered in:

bootstrap/providers.php

Use:

App\...
    → project-specific application code

Craft plugin
    → reusable/distributable Craft extension

Composer/Laravel package
    → reusable PHP/Laravel capability that does not need Craft's plugin lifecycle

"Module" is therefore no longer a particularly useful architectural category in a new Craft 6 application.


Plugin Concerns and Declarative Registration

Craft plugins provide concerns (traits) that declaratively register common plugin capabilities.

Examples include capabilities for:

routes
commands
listeners/events
configuration
settings
Craft component types

This relates to Laravel/PHP concepts as:

PHP Trait / Concern
    → reusable implementation

Craft Plugin Concern
    → reusable plugin bootstrapping +
      declarative registration convention

Prefer the plugin's declarative property/trait mechanism when Craft explicitly provides one rather than manually reproducing its registration logic in bootPlugin().


Registries

Registries are a Craft 6-specific extension concept.

A registry manages a validated collection of related Craft component types. Registries replace some Craft 5 patterns where plugins listened for "register component types" events.

Conceptually:

Registry
    ├── knows the required contract
    ├── contains registered types
    ├── validates additions
    ├── can register/remove types
    └── may provide built-in/default types

A registry is not the Laravel service container.

Service Container
    → resolves object dependencies

Craft Registry
    → catalogs implementations/types belonging
      to a particular Craft extension point

Laravel's container may inject the registry into your plugin:

public function boot(FieldTypes $fieldTypes): void
{
    $fieldTypes->register(MyField::class);
}

So the relationship is:

Laravel container
    ↓ injects
Craft Registry
    ↓ contains
registered Craft component types

Where Craft offers a plugin property backed by a registry, declarative registration is generally preferable.


Craft Events and Laravel Events

Craft 6 uses Laravel's event system rather than the old Yii event architecture.

Craft events are distinct event classes:

event(new DeliveryConfirmed());

Listeners can be normal Laravel listeners, and project-level listeners can use Laravel's discovery/conventions.

This maps directly to the earlier generic concepts:

Craft Event
    = Laravel event carrying Craft-domain information

Craft Listener
    = Laravel listener reacting to that event

A significant conceptual difference from Craft 5 is that listeners no longer need to be organized around a particular event sender. You listen for the event class.

Plugins can also listen to ordinary Laravel events, which means the event bus is a shared integration layer between Laravel infrastructure and Craft domain behavior.


Craft Controllers and Routes

Craft 6 controllers are ordinary PHP classes invoked through Laravel routing. There is no required Craft base controller.

A plugin defines routes with Laravel's Route facade, typically in Craft-provided route files such as:

routes/web.php
routes/cp.php
routes/actions.php

Their roles differ:

web.php
    → normal front-end routes with Craft web middleware

cp.php
    → Control Panel routes with CP middleware

actions.php
    → compatibility-oriented action-path routing

actions.php exists primarily for compatibility with Craft's historical action URLs; new code should generally prefer normal Laravel routing where practical.

This maps almost directly to the generic Laravel HTTP stack:

Route
    ↓
Craft/Laravel middleware
    ↓
Request / Form Request
    ↓
Controller
    ↓
Service / Action
    ↓
Response

Craft-specific behavior is mainly supplied by route groups, middleware, permissions, template helpers, and the surrounding CMS environment.


Craft Validation

Craft 6 increasingly follows Laravel's principle of validating data at the boundary.

For HTTP input, ordinary Laravel validation applies:

$data = $request->validate([
    'name' => ['required', 'max:64'],
]);

or use a Laravel FormRequest.

Craft Components can additionally opt into object-level validation when an object itself needs to be validatable.

The distinction is:

Form Request / Request validation
    → validate incoming HTTP data

Craft Component validation
    → validate a configurable/data object

Field element rules
    → validate content stored for a Craft field

Eloquent model
    → persistence object; Craft core does not
      assume it is the primary validation boundary

This is one of the places where understanding Laravel's validation model prevents carrying Craft 5/Yii patterns unnecessarily into Craft 6.


Craft Users, Authentication, and Permissions

Craft users remain Elements, while authentication is integrated with Laravel.

The current Craft user can be obtained through Laravel's authentication facade:

$user = Auth::user();

Craft then layers its CMS permission system and user-element behavior on top of Laravel authentication/authorization infrastructure.

Conceptually:

Laravel Authentication
    → who is authenticated?

Craft User Element
    → CMS representation of that user

Laravel Gate / Policy + Craft permissions
    → what may that user do?

Do not treat Craft's user permission strings and Laravel authentication guards as the same concept. Authentication establishes identity; Craft permissions and Laravel authorization mechanisms decide capabilities.


Craft Templates: Twig and Blade

Craft 6 can participate in both Craft's established Twig rendering environment and Laravel's Blade rendering environment.

For Craft templates:

return template('activity/_session-report', [
    'report' => $report,
]);

For Blade, Laravel's normal:

return view('reports.show', [
    'report' => $report,
]);

remains available.

Conceptually:

Twig
    → Craft's CMS/site + control-panel templating ecosystem

Blade
    → Laravel's native templating system

Craft's Twig environment also exposes selected Laravel helpers/facades. Do not assume, however, that Twig and Blade components or extension APIs are interchangeable.

When rendering templates derived from untrusted or lower-privilege input, Craft provides sandboxed Twig rendering APIs. That is a Craft-specific security facility above Laravel's general view layer.


Craft Filesystems and Laravel Disks

Craft 6 filesystems wrap Laravel's filesystem disks.

Laravel provides:

Storage facade
disk configuration
Flysystem abstraction

Craft adds CMS-facing filesystem configuration that can participate in Project Config and be selected by asset volumes.

Conceptually:

Laravel Disk
    → actual filesystem/storage adapter

Craft Filesystem
    → Craft-configurable wrapper that produces
      Laravel disk configuration

Craft Asset Volume
    → CMS asset organization using a filesystem/disk

Application code that only needs generic file storage can use Laravel disks directly. Craft asset management should use Craft's asset/filesystem abstractions so that CMS configuration and asset semantics are preserved.


Craft Commands and Queues

Craft 6 console commands are Laravel Artisan commands.

Old Yii console-controller thinking should be replaced with:

one Artisan command
    → one command class/signature
    → dependencies injected by Laravel

Plugins can register command classes declaratively.

Likewise, Craft operations can use Laravel's queue system and Jobs. A focused Craft action/service can therefore be invoked synchronously or wrapped/deferred as queue work.

The generic sections on:

Commands
Jobs
Queues
Dependency Injection
Task Scheduling

apply directly to Craft 6.


Craft Configuration vs Laravel Configuration vs Project Config

These three sources of configuration should be kept conceptually separate.

Mechanism Purpose Typical examples
Laravel configuration Runtime application/infrastructure settings config/*.php, config(), environment-derived values
Craft/plugin configuration Runtime settings exposed by Craft/plugin code, often built on Laravel config plugin defaults, environment-specific plugin behavior
Craft Project Config Version-controlled CMS schema/admin configuration fields, sections, entry types, sites, plugin schema/settings

A plugin may combine defaults, published config, environment-aware values, and a settings object. The important design question is what kind of state this is, not merely where a value can technically be stored.


The Yii Adapter

Craft 6 provides an optional craftcms/yii2-adapter compatibility package for migrating Craft 5 plugins and code.

Its architectural role is:

Legacy Craft/Yii API
    ↓
Yii adapter
    ↓
Craft 6 / Laravel APIs

The adapter should be understood as a migration compatibility layer, not as the architecture to target for new Craft 6 code.

For new code, prefer Laravel/Craft 6 concepts directly:

Yii service locator       → Laravel dependency injection
Yii events                → Laravel events/listeners
Yii controller routing    → Laravel routes/controllers
Yii console controllers   → Artisan commands
craft\db\Record           → Eloquent model
craft\base\Model          → Component/DTO/validated input as appropriate

Craft 6 Concept Mapping

The following table is the compact reference for translating between Craft-specific and generic Laravel concepts.

Craft 6 concept Generic Laravel / PHP concept Relationship
Element Domain/content object; no direct Laravel equivalent Craft CMS abstraction above persistence.
Element Query Query Builder-like abstraction Queries Craft content semantics rather than ordinary Eloquent rows.
Field No direct Laravel equivalent Defines configurable CMS content behavior and validation.
Field Layout No direct Laravel equivalent Defines the editing/content UI schema for Elements.
BaseModel Eloquent Model Craft-oriented base for persisted records.
Component DTO/configurable object + optional validation Craft convenience abstraction for non-Eloquent objects.
Craft Service Service + Service Container Ordinary injectable PHP service, often a singleton.
Craft Action Action pattern Focused operation reusable across transports.
Plugin Composer package + Laravel Service Provider Adds Craft discovery, lifecycle, metadata, and CMS extension points.
Former Module Application code + Service Provider Project-specific extension code normally belongs under App\.
Plugin Concern PHP Trait / Concern Declarative Craft plugin registration/bootstrapping.
Registry Typed catalog/registry pattern Holds valid implementations for a Craft extension point; not a DI container.
Craft Event Laravel Event Domain-specific event on Laravel's event system.
Craft Listener Laravel Listener Handles Craft or Laravel events.
Craft Controller Laravel Controller/invokable class Uses Laravel routes, DI, requests, middleware, and responses.
Craft Route Laravel Route Craft supplies route groups/middleware for web, CP, and compatibility actions.
Craft validation Laravel Validator/Form Request + optional object validation Boundary validation is increasingly preferred.
Craft User Authenticatable user + Craft Element Laravel handles auth infrastructure; Craft adds CMS user semantics.
Craft permission Gate/Policy authorization Craft permission checks integrate with Laravel authorization infrastructure.
Twig template View Craft's primary CMS templating environment; Blade is also available.
Craft Filesystem Laravel filesystem Disk configuration Craft wrapper integrates storage with CMS/project configuration.
Asset Volume No direct Laravel equivalent CMS-level asset organization backed by a Craft filesystem/Laravel disk.
Project Config No direct Laravel equivalent Version-controlled CMS schema/admin state.
Craft Command Artisan Command Direct Laravel concept in Craft 6.
Craft queued work Job / Queue Direct Laravel queue infrastructure.
Yii Adapter Compatibility/adapter layer Bridges legacy Craft/Yii APIs to Craft 6/Laravel during migration.

Practical architectural rule for Craft 6 applications

When deciding where new code belongs, ask what kind of object you are actually modeling:

Persisting ordinary application data?
    → Eloquent Model

Representing Craft-managed content?
    → Element

Finding Craft-managed content?
    → Element Query

Defining editor-configurable content behavior?
    → Field / Field Layout

Transporting simple data?
    → ordinary DTO / value object

Need Craft-style configurable + validatable object behavior?
    → Component

Performing one application operation?
    → Action

Providing a broader reusable capability?
    → Service resolved through Laravel's container

Handling HTTP?
    → Laravel Route + Request/Form Request + Controller

Reacting to something that happened?
    → Laravel/Craft Event + Listener

Doing deferred work?
    → Laravel Job / Queue

Adding project-specific behavior?
    → App\ code + Service Provider

Shipping reusable Craft functionality?
    → Craft Plugin

Storing runtime/environment configuration?
    → Laravel/Craft config

Tracking CMS schema/admin state across environments?
    → Project Config

The overarching rule is: use Laravel's native infrastructure unless the problem is specifically a Craft CMS domain problem. Craft 6's architecture deliberately makes that boundary clearer than earlier Craft versions did.

15. The concepts people most commonly confuse

Several pairs sound similar but have substantially different purposes.

Request vs DTO

Form Request
    ↓
HTTP-specific input + validation

DTO
    ↓
framework-independent structured application data

For example:

StoreOrderRequest
       ↓
   OrderData
       ↓
 CreateOrder

Your Action therefore doesn't need to know that the data originally came from HTTP.


DTO vs Value Object

DTO
= carries data

Value Object
= represents a domain concept and protects its validity

Example:

CreateInvoiceData     ← DTO

Money                 ← Value Object
EmailAddress          ← Value Object
DateRange             ← Value Object

Service vs Action

A useful convention is:

Action
= one use case

Service
= broader reusable capability

Example:

CreateInvoice         Action
CancelInvoice         Action

TaxCalculator         Service
CurrencyConverter     Service

Neither convention is imposed by Laravel.


Event vs Job

Event
= "something happened"

Job
= "do this work"

Example:

OrderPlaced           Event

GenerateInvoice       Job

An event listener might dispatch a job:

OrderPlaced
     ↓
GenerateInvoice listener
     ↓
GenerateInvoicePdf job
     ↓
queue worker

Policy vs Middleware

Middleware
= may this request enter this part of the application?

Policy
= may this user perform this operation on this resource?

For example:

auth middleware

checks:

Is somebody logged in?

while:

OrderPolicy::update()

checks:

May this specific logged-in user update this specific order?


Rule vs business rule

Laravel validation rules mostly validate input:

valid email
minimum length
field exists
number > 0

Business rules are broader:

an order cannot be cancelled after shipment
a customer cannot exceed their credit limit
inventory cannot become negative

Those usually belong in:

domain objects
services
actions
models

rather than Form Requests.


Observer vs Listener

Both react to events, but their scope differs:

Observer
    ↓
specifically groups Eloquent model lifecycle behavior

Listener
    ↓
reacts to application events

Example:

UserObserver::created()

vs.

UserRegistered → SendWelcomeEmail listener

For important business workflows, explicit application events are often easier to understand than hiding everything behind model observers.


Macro/Mixin vs Helper vs Service

Helper function
= a standalone, framework-agnostic utility function

Macro / Mixin
= a new method bolted onto an existing (usually framework) class

Service
= a class holding reusable business/application logic

Example:

function money(int $cents): string {}     ← Helper

Str::macro('money', ...)                  ← Macro (feels like it "belongs" to Str)

PricingService::calculate()               ← Service (has dependencies, business rules)

Reach for a macro only when you want the new behavior to feel like a natural extension of a class you don't own (e.g. Str::, Collection::); reach for a Service once real dependencies or business rules are involved.


View Composer vs passing data from a Controller

Controller passing data
= "this specific action needs this specific data"

View Composer
= "this view always needs this data, regardless of which controller rendered it"

Example:

return view('orders.show', ['order' => $order]);   ← controller-specific data

View::composer('layouts.navigation', NavigationComposer::class);
                                                     ← data every render of this view needs

If only one controller ever needs a piece of data, pass it explicitly. Reach for a view composer once several unrelated controllers would otherwise have to duplicate the same "fetch this and pass it to the view" logic.


16. Putting everything together

A reasonably structured Laravel application might look like this:

HTTP
 │
 ▼
Route
 │
 ▼
Middleware
 │
 ▼
Form Request
 │
 │ validated input
 ▼
DTO
 │
 ▼
Controller
 │
 ▼
Action
 │
 ├──────────────► Policy
 │
 ▼
Service
 │
 ├──────────────► Value Objects
 │
 ├──────────────► Repository Contract
 │                        │
 │                        ▼
 │                 Eloquent Repository
 │                        │
 │                        ▼
 │                      Model
 │                        │
 │                      Scope
 │
 ├──────────────► Event
 │                    │
 │                    ▼
 │                 Listener
 │                    │
 │                    ▼
 │                   Job
 │
 └──────────────► Notification

Controller
    │
    ├──► Resource ──► JSON Response
    │
    └──► View ──────► HTML Response

Meanwhile, the application's supporting infrastructure looks more like:

Service Provider
      │
      ├── Contract → Implementation bindings
      ├── package registration
      └── application bootstrapping

Database
      │
      ├── Migrations → schema
      ├── Seeders    → predefined data
      └── Factories  → generated/test data

Console
      │
      └── Commands → Actions / Services

Testing
      │
      ├── Unit tests
      └── Feature tests

That distinction is useful when navigating a Laravel codebase: controllers, requests, middleware and resources deal primarily with the transport layer; actions/services represent application behavior; models/repositories deal with persistence and domain data; policies/rules enforce constraints; events/jobs/listeners handle decoupled or asynchronous work; and service providers/contracts/facades wire infrastructure together. Laravel supplies many of those mechanisms directly, while Actions, Services, Repositories, DTOs and Value Objects are architectural choices you add when they improve clarity. (Laravel)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment