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.
- 1. MVC and the HTTP request/response layer
- 2. UI components: Blade, Livewire, and Flux
- 3. Livewire in depth
- 4. Application/business logic
- 5. Contracts, dependency injection, and framework integration
- 6. Authorization, validation, and business rules
- 7. Event-driven and asynchronous processing
- 8. Database
- 9. Application configuration, security, and external services
- 10. CLI and scheduled/application operations
- 11. Data objects and domain modelling
- 12. Miscellaneous concepts
- 13. Testing
- 14. Craft CMS 6 concepts on top of Laravel
- 15. The concepts people most commonly confuse
- 16. Putting everything together
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 | |
| 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 |
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
Laravel models normally extend:
Illuminate\Database\Eloquent\ModelExample:
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.
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.
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 define which code handles which URL.
Route::post('/orders', [OrderController::class, 'store']);They answer:
When
POST /ordershappens, what handles it?
Routes can also attach middleware:
Route::middleware('auth')->group(function () {
Route::post('/orders', [OrderController::class, 'store']);
});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).
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>
@enderrorThis same @error directive works identically for Livewire components
--- see Displaying validation errors in
the Livewire chapter for the Flux-based alternative.
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)
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');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.
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 |
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 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.
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 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 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.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.
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)
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.
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
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 eventLivewire 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;
}
}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.
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)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.
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.
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>
@enderrorBoth 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.
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).
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.
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".
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.
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.
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.
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.
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, sincestream()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 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.
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.
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)
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
A contract is an interface.
Example:
interface PaymentGateway
{
public function charge(Money $amount): PaymentResult;
}Implementations:
StripePaymentGateway
AdyenPaymentGateway
FakePaymentGatewayBusiness logic depends on:
PaymentGatewayrather than:
StripePaymentGatewayThis produces:
CheckoutService
│
▼
PaymentGateway
/ \
Stripe Fake
Laravel itself has many interfaces under:
Illuminate\ContractsBut the concept is normal object-oriented programming.
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:
StripePaymentGatewayService providers may also register:
container bindings
event listeners
routes
configuration
package components
Laravel describes providers as the central place for application bootstrapping. (Laravel)
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.
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.
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).
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 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 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.
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).
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.
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.
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.
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?"
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>
@endcanThink:
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 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 represent a finite set of possible values.
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Cancelled = 'cancelled';
}Instead of:
if ($status === 'paidd') // typoyou use:
OrderStatus::PaidEnums 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 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.
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 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 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.
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)
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.
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)
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)
Dispatching a job is only half of queue processing. A queue worker continuously pulls queued jobs and executes them:
php artisan queue:workProduction 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 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
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.
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.
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 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 (Order → orders), 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 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 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-loadedEager 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());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.
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 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.
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.
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?
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.
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)
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
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.
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:cacheOnce configuration is cached, application code should rely on configuration values rather than direct environment lookups.
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.
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.
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.
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.
Commands are CLI entry points, usually Artisan commands.
Example:
php artisan invoices:cleanupClass:
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 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, ...)
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 |
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.
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.
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\Responsewhich represents an actual HTTP response.
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 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.
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)
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)
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_idContext 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)
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)
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
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.
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.
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\BaseModelCraft's BaseModel adds Craft-oriented persistence conventions such as standardized dateCreated, dateUpdated, and dateDeleted columns. UIDs can be added through:
CraftCms\Cms\Shared\Concerns\HasUidCraft 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.
| 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.
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\ComponentA 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\ValidatesReusable validation rulesets can be represented with:
#[CraftCms\Cms\Validation\Ruleset(...)]Arbitrary objects can be configured/typecast with:
CraftCms\Cms\Support\Typecast::configure(...)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 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
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 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 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 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.
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 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.
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\Pluginand 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.
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.
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 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 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 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 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 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 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 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 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.
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.
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
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. |
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.
Several pairs sound similar but have substantially different purposes.
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
= carries data
Value Object
= represents a domain concept and protects its validity
Example:
CreateInvoiceData ← DTO
Money ← Value Object
EmailAddress ← Value Object
DateRange ← Value Object
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
= "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
Middleware
= may this request enter this part of the application?
Policy
= may this user perform this operation on this resource?
For example:
auth middlewarechecks:
Is somebody logged in?
while:
OrderPolicy::update()checks:
May this specific logged-in user update this specific order?
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.
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.
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.
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.
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)