laravel/framework
Laravel is a web application framework with expressive, elegant syntax, offering routing, dependency injection, sessions, caching, migrations, queues, and broadcasting for building robust PHP applications.
Laravel is a full-stack framework, not a library you bolt onto an existing app. laravel/framework is the core package that arrives transitively — you scaffold a project from the application skeleton and let Composer pull the framework in:
composer create-project laravel/laravel my-app
cd my-app
php artisan about # confirmation of PHP, drivers, cache, DB, versions
php artisan serve
Laravel 13 requires a modern PHP runtime (8.3+) — verify with php artisan about before debugging anything else.
Where to look first
| Path | What lives there |
|---|---|
routes/web.php, routes/api.php, routes/console.php |
Entry points; read these to understand the app's surface area |
app/Http/Controllers, app/Http/Requests |
Request handling and validation |
app/Models |
Eloquent models, relationships, casts |
config/*.php |
The only place env() should be called |
database/migrations, database/factories, database/seeders |
Schema and test data |
tests/Feature, tests/Unit |
Pest or PHPUnit suites; run php artisan test |
bootstrap/app.php |
The 11+ style single-file wiring for middleware, exceptions, routing |
app/Providers/AppServiceProvider.php |
Container bindings, model strictness, global config |
First use case — a single resource end-to-end, which exercises routing, DI, validation, Eloquent, Blade/JSON and testing in one pass:
php artisan make:model Post -mfsc --requests --resource
php artisan migrate
// routes/web.php
Route::resource('posts', PostController::class);
// app/Http/Controllers/PostController.php
public function store(StorePostRequest $request)
{
$post = Post::create($request->validated());
return redirect()->route('posts.show', $post);
}
Then write the feature test before adding a second resource — this is the fastest way to learn the framework's feedback loop:
it('creates a post', function () {
$this->post(route('posts.store'), ['title' => 'Hello'])
->assertRedirect();
expect(Post::count())->toBe(1);
});
Thin controllers, fat domain. Keep controllers to transport concerns (validate → delegate → respond). Push multi-step logic into action classes or services resolved from the container, so it stays unit-testable and reusable from jobs and commands.
// app/Providers/AppServiceProvider.php
$this->app->bind(PaymentGateway::class, StripeGateway::class);
Constructor-inject the interface; swap the binding in tests. Remember the container also resolves jobs, listeners, commands and middleware — any of them can take dependencies.
Form Requests for validation + authorization. Move authorize() and rules() out of controllers. Validation rules should be explicit and strict — recent releases tightened loose-comparison behaviour in in_array, contains/doesnt_contain and dotted/asterisk input keys, so prefer typed arrays, enums in Rule::in(), and bail where the first failure is sufficient.
Eloquent as a query builder, not a row loader. Eager-load deliberately and constrain eager loads at the point of use:
$posts = Post::query()
->with(['author:id,name', 'comments' => fn ($q) => $q->latest()->limit(5)])
->whereBinary('slug', $slug)
->latest()
->paginate(20);
For concurrency-sensitive flows, wrap reads in transactions and use the newer refreshForUpdate() (13.27+) rather than lockForUpdate() on a stale instance. Use inOrderOf() (accepts enums) for deterministic ordering, and whereBinary() when case-sensitivity matters on MySQL/MariaDB.
Queue everything that isn't a response. Jobs are the natural home for slow I/O, notifications and fan-out:
#[TryAgain(3, backoff: [10, 60, 300])]
class SendInvoice implements ShouldQueue
{
use InteractsWithQueue, Queueable;
public function handle(): void { /* ... */ }
}
Combine with WithoutOverlapping, job middleware, and unique jobs. Recent releases added a JobReleased event on the worker, totalXSize queue inspection methods, and fixed uniqueness releasing locks the job never acquired — useful if you're building queue dashboards or alerting.
Events and listeners for decoupling. Fire a domain event from the model/action, then queue listeners. Listen to worker lifecycle events (JobReleased, JobProcessing) for observability instead of sprinkling logging through handle().
HTTP and process abstraction over raw cURL. Http::withToken(...)->retry(3, 100)->get(...) plus Http::fake() in tests. Since 13.27, quietly() no longer makes throw() unreachable on process results, and image creation from URLs throws on unsuccessful responses — don't assume silent failure.
Testing patterns that pay for themselves:
Http::fake(['api.example.com/*' => Http::response(['ok' => true])]);
Queue::fake();
Event::fake([InvoiceSent::class]);
Storage::fake('local');
$this->travelTo(now()->setDate(2026, 1, 1));
Use RefreshDatabase for anything touching the DB, factories for state, and Pest datasets for matrixed cases. For queues, prefer Queue::fake() assertions (assertPushed) over executing jobs unless you're specifically testing the job.
Artisan as a first-class interface. php artisan make:* for scaffolding (controllers, requests, policies, tests, casts), scheduled tasks in routes/console.php, and custom commands for ops chores. php artisan pail for live logs, php artisan telescope/horizon when installed for inspection.
composer require laravel/framework directly. Use the laravel/laravel skeleton and let the framework version float within your constraint. Requiring it explicitly is how you end up with a mismatched skeleton.vendor/. Everything in the framework is overridable: bind an interface, extend a class, or use an extension point (Builder::macro, custom casts, custom validation rules, Model::resolveRelationUsing). If you genuinely need a core change, contribute upstream.env() outside config/ returns null once config:cache runs. This is the single most common "works locally, breaks in prod" bug. Only call env() inside config/*.php, and add new keys to .env.example at the same time.Model::preventLazyLoading(! app()->isProduction()) and Model::preventSilentlyDiscardingAttributes() in local/staging. For API surface control, be aware maxRelationshipDepth limits nested includes — including the behaviour when it's set to zero, which was fixed to also limit nested includes.Arr::dot-style expansion, retest on upgrade.orWhereKey() was added, reverted (13.26.1) and re-introduced without breaking Builder subclasses (13.27.0). Same caution applies to refreshForUpdate(), whereBinary(), the Cloud facade and the read-through filesystem disk — all recent additions.APP_DEBUG=false in production.Auth::user(), request()) leaks unless you use scoped() bindings or reset in middleware. Duplicate scoped registrations are now prevented, but the scoping decision is still yours.RouteRegistrar action isn't callable — fixed in 13.27, but if you generate URLs dynamically, assert route() resolves in tests rather than assuming.changelog/ per minor, run php artisan test plus composer outdated in CI, and apply Rector rules for framework-driven refactors. Laravel's release cadence is fast; skipping several minors turns a 10-minute upgrade into an afternoon.dump()/dd() → Log::debug() → php artisan pail → DB::listen() for query inspection → Telescope/Horizon for request and job traces. Reach for a debugger before adding echo in a controller.How can I help you explore Laravel packages today?