All notes
Web & Framework3 min readUpdated August 7, 2026

Building Systems with Laravel

A map of Laravel's parts and the order a request travels through them, so logic ends up where it belongs rather than wherever it happens to work.

LaravelPHPMVCEloquentMigrationArtisan

Laravel organises an application around MVC: the Model handles data, the View handles presentation, the Controller connects them. The real value is not the pattern itself but the convention that every kind of thing has a predetermined home. A developer opening a Laravel project knows where to look without asking.

The journey of one request

  1. 01public/index.php receives everything — it is the only file exposed publicly.
  2. 02bootstrap/app.php boots the framework and registers middleware and error handling. Since Laravel 11 this file replaces Http/Kernel.php and Console/Kernel.php.
  3. 03The router matches the URL against definitions in routes/web.php or routes/api.php.
  4. 04Middleware runs in sequence — authentication, rate limiting, CSRF verification. A request can be stopped here before it touches any logic.
  5. 05The controller receives an already-clean request, calls models, and assembles the result.
  6. 06A response goes back — a View for a page, JSON for an API.

The folders you touch most

LocationContents
routes/URL definitions; web.php is session-based, api.php stateless
app/Models/Eloquent classes, one model per table
app/Http/Controllers/The bridge between a request and the logic
app/Http/Requests/Validation rules kept out of the controller
app/Http/Middleware/Filters that run before the controller
database/migrations/The history of schema changes
database/seeders/Starting data for development and testing
resources/views/Blade templates
.envCredentials and per-environment configuration

Migrations: schema that follows the code

A migration expresses a schema change as code that goes into Git. As a result the database on a colleague's laptop, on staging, and in production are all built from the same sequence of commands. That removes an exhausting class of problem: it works on my machine but not on the server, because a column was never created.

A migration, a model, and the relationship between them
// database/migrations/xxxx_create_visitors_table.php
Schema::create('visitors', function (Blueprint $table) {
    $table->id();
    $table->foreignId('host_id')->constrained('employees');
    $table->string('name');
    $table->string('purpose');
    $table->timestamp('checked_in_at')->nullable();
    $table->timestamps();
});

// app/Models/Visitor.php
class Visitor extends Model
{
    protected $fillable = ['host_id', 'name', 'purpose', 'checked_in_at'];

    public function host()
    {
        return $this->belongsTo(Employee::class, 'host_id');
    }
}

The N+1 trap in Eloquent

Eloquent loads relations lazily: a relation is fetched the first time it is touched. Listing 100 visitors with their host names therefore produces one query for the visitors plus 100 separate queries for the hosts. On a small test dataset this is invisible, and it becomes a real problem only once the system is in use.

// N+1: one query per visitor to fetch the host
$visitors = Visitor::all();
foreach ($visitors as $v) { echo $v->host->name; }

// Eager loading: two queries no matter how many visitors
$visitors = Visitor::with('host')->get();

More notes