Open-Source Wikis

/

Laravel

/

Background

laravel/laravel

Background

Why some things in the skeleton look the way they do. Most of the answers are "because the framework changed and the skeleton followed," but a few decisions are genuinely opinionated.

Why bootstrap/app.php is so important now

Pre–Laravel 11, the application's HTTP middleware, console kernel, and exception handler were three separate classes (App\Http\Kernel, App\Console\Kernel, App\Exceptions\Handler). Adding a global middleware meant editing app/Http/Kernel.php. Customizing exception rendering meant editing app/Exceptions/Handler.php. New contributors to a Laravel app had to learn the existence of these classes, what each one was for, and how to navigate between them.

Laravel 11 (March 2024) collapsed all three into closures inside a single bootstrap/app.php. The skeleton's job changed: it stopped shipping kernel/handler classes and started shipping a fluent configuration call. That's why bootstrap/app.php looks like:

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(...)
    ->withMiddleware(function (Middleware $middleware) { /* ... */ })
    ->withExceptions(function (Exceptions $exceptions) { /* ... */ })
    ->create();

This is the whole surface for what would have been three files of overrides. It's terser, but it does require knowing which method on Middleware / Exceptions corresponds to what you used to override.

Why app/ is so empty

Same era, same logic. The "slim skeleton" rework deleted app/Http/Kernel.php, app/Console/Kernel.php, app/Exceptions/Handler.php, the bundled middleware classes (Authenticate, EncryptCookies, PreventRequestsDuringMaintenance, RedirectIfAuthenticated, TrimStrings, TrustHosts, TrustProxies, ValidateSignature, VerifyCsrfToken), and the four extra service providers (AuthServiceProvider, BroadcastServiceProvider, EventServiceProvider, RouteServiceProvider). The framework provides defaults for all of those internally; the skeleton no longer ships override stubs.

The result is app/ containing 64 lines of PHP across two files. New contributors don't need to read framework code unless they're customizing it. New developers learning Laravel encounter less ceremony.

Why migrations have weird timestamps

Migration files in database/migrations/ use the format 0001_01_01_000000_create_users_table.php. That's not a real timestamp — it's a sentinel that sorts before any plausible real timestamp. The reason: the framework wants its baseline migrations to always run first, even if you generate a new migration on January 2 of year 1 (which you won't). Laravel 11+ shipped this normalization for the bundled migrations.

When you run php artisan make:migration create_posts_table, the new file gets a real current timestamp like 2026_04_30_153022_create_posts_table.php. Mixed timestamps work fine — they all sort lexicographically.

Why three migrations bundle nine tables

The skeleton's three migrations create:

  • users, password_reset_tokens, sessions (one file)
  • cache, cache_locks (one file)
  • jobs, job_batches, failed_jobs (one file)

Grouping by purpose rather than table makes the rollback story cleaner: one rollback drops a related set of tables atomically. It also matches how the corresponding subsystems are documented in the framework — sessions and password resets are part of "auth"; cache_locks is a cache concern; job_batches is a queueing concern.

In v12.11.2 (December 2025), PR #6736 ("Update jobs/cache migrations") and PR #6739 ("Remove failed jobs indexes") consolidated these files into the shapes you see today. Older skeletons had finer-grained migrations.

Why the welcome view inlines a stylesheet

resources/views/welcome.blade.php has a ~10 KB minified Tailwind dump as a fallback <style> block. The view checks for public/build/manifest.json (production build) or public/hot (Vite dev server) before falling back to the inline styles.

This is the welcome-experience tradeoff: a fresh composer create-project laravel/laravel should look reasonable in a browser even when the developer hasn't run npm install yet. Without the fallback, the page would render unstyled until they figured out the frontend pipeline. The cost is ~10 KB of bytes in source control, which the team has decided is acceptable.

Why the queue defaults to database

.env.example sets QUEUE_CONNECTION=database. Other Laravel docs you may have read promote redis or sqs as production-grade options, so why does the skeleton point at the database?

Two reasons:

  1. No external dependencies. A fresh clone with default config runs against SQLite-only. Queues "just work" with no extra services to install.
  2. Switching is one env var. When you outgrow database-backed queues, change QUEUE_CONNECTION and you're on Redis or SQS. The drivers are interchangeable.

The same logic explains why CACHE_STORE=database and SESSION_DRIVER=database are the defaults. SQLite + database-driven everything = a dev environment that needs zero infrastructure.

Why package.json ships zero dependencies

devDependencies has five entries (Vite, Tailwind, the Laravel plugin, concurrently, @tailwindcss/vite). dependencies is missing entirely. That's deliberate.

Spring 2026 saw a supply-chain incident affecting the JavaScript ecosystem broadly. The team responded with three commits:

  1. Pin axios to a specific patch version.
  2. Remove axios from the default skeleton.
  3. Enable npm audit and ignore-scripts in .npmrc.

The reasoning: most Laravel applications never call axios directly — they use the framework's HTTP facade for server-side requests, and pages fetch via fetch or a JS framework's bundled HTTP client. Shipping axios by default added a transitive dep without serving most users. Removing it shrunk the attack surface to zero runtime dependencies.

Why CI tests three PHP versions but doesn't run Pint

.github/workflows/tests.yml runs php artisan test on PHP 8.3, 8.4, and 8.5 — the version range supported by composer.json's ^8.3. The matrix exists because each PHP minor introduces deprecations that can break a working app (e.g. PHP 8.5's PDO::MYSQL_ATTR_* deprecation, handled by a runtime PHP_VERSION_ID branch in config/database.php).

Pint isn't in CI because:

  • The skeleton is small enough that style violations are noticed in code review.
  • Style runs are personal-preference; some contributors run them, some don't.
  • The Laravel team has chosen to keep CI runs cheap and focused.

If you fork this skeleton and use it as a base for your own project, adding Pint to CI is a one-line change: vendor/bin/pint --test as another step in tests.yml.

Why the inspire command is still here

routes/console.php ships exactly one Artisan command:

Artisan::command('inspire', function () {
    $this->comment(Inspiring::quote());
})->purpose('Display an inspiring quote');

It's a smoke test. Running php artisan inspire and getting a quote back proves the framework boots, the container resolves, and the console application runs end-to-end. The fact that it's an "inspirational quote" is the punchline — the team likes the easter-egg energy.

Illuminate\Foundation\Inspiring::quote() has been part of the framework since Laravel 5. The skeleton has always shipped the inspire route (in some form).

Why .env.example is committed but .env isn't

The two files have different roles:

  • .env.example documents every environment variable the skeleton knows about, with safe defaults. It's a template.
  • .env holds the real values for this checkout (your local DB credentials, your dev APP_KEY, etc.). It's secrets-adjacent.

.gitignore excludes .env and .env.backup. The composer setup and post-root-package-install scripts copy .env.example to .env if .env doesn't already exist.

Production deploys typically don't use either — they read environment variables from the deployment platform (Forge, Vapor, Kubernetes secrets, AWS Parameter Store) directly. The .env file is a local-dev convenience.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Background – Laravel wiki | Factory