Course outline

Laravel / Basic: request and data foundations / Lesson 6

Migrations, Eloquent models, and safe query foundations

Connect schema rules, model attributes, and parameterized queries using an owned in-memory SQLite fixture. Distinguish migration intent, model convenience, validation, and durable constraints.

About 4 min readContent reviewed
On this page

Lesson goals

  • Define schema and model responsibilities separately.
  • Use bound values and selected writable fields.
  • State database-driver and migration limitations.

A migration changes a shared data contract

Migrations describe versioned schema changes, but running one is a real data operation with compatibility and rollback consequences. Plan additions, backfills, indexes, defaults, and constraints according to existing data and application versions. A model validation rule is not a substitute for a durable uniqueness or foreign-key constraint. DDL and transaction behavior vary by engine, so do not infer production migration safety from an in-memory example. The examples create a fresh private schema through the framework schema builder; they do not run the project migration history or touch an existing connection.

Keep values separate from query structure

The query builder binds ordinary values rather than concatenating them into SQL. A quote inside a title remains data in the local fixture. Placeholders do not authorize the requested row and cannot safely stand for arbitrary column names or sort directions; select structural choices from an application-owned allowlist. Raw expressions need separate review because the builder cannot sanitize arbitrary SQL you inject. The first example verifies a stored title and a selected row count. It does not establish concurrency correctness, another database engine’s collation, or a production query plan.

php Bound data in an owned SQLite schema
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$db = new Illuminate\Database\Capsule\Manager();
$db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'foreign_key_constraints' => true]);
$db->setAsGlobal();
$db->bootEloquent();
$db->schema()->create('courses', function (Illuminate\Database\Schema\Blueprint $table) {
    $table->id(); $table->string('title'); $table->integer('seats');
});
$db->table('courses')->insert(['title' => "Ada's PHP", 'seats' => 2]);
$query = $db->table('courses')->where('title', "Ada's PHP");
echo $query->getBindings()[0], PHP_EOL;
echo $query->count(), PHP_EOL;
Expected output
Ada's PHP
1

Capsule uses only sqlite :memory:. No configured connection, migration history, or network database is loaded.

A model describes persistence behavior, not every business rule

Eloquent maps model instances to records and offers casts, relationships, events, and query scopes. A cast can normalize a loaded representation, but it does not authorize a write or validate an entire request. Define writable attributes deliberately and pass selected validated data into create or fill. Mass-assignment protection does not prevent direct property assignment and is not a complete authorization system. Keep internal notes, tenant identifiers, prices, and role flags under explicit server ownership. Decide timestamps, table naming, key behavior, and serialization rules instead of relying on conventions that do not match the schema.

Match a small model to its declared table

The local model uses an explicit table, a two-field writable list, no timestamps, and an integer cast for seats. It creates one synthetic record and reads the resulting count. Each choice is visible because a miniature fixture lacks the rest of an application’s conventions. The example is not a recommendation to disable timestamps in real systems; that depends on the domain. Do not use this create call directly with all request input. A production use case still needs validation, permission, transaction rules, and a public representation that omits private fields.

php An explicit model/schema pair in memory
<?php
declare(strict_types=1);
require __DIR__ . '/vendor/autoload.php';
$db = new Illuminate\Database\Capsule\Manager();
$db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', 'foreign_key_constraints' => true]);
$db->setAsGlobal();
$db->bootEloquent();
$db->schema()->create('courses', function (Illuminate\Database\Schema\Blueprint $table) {
    $table->id(); $table->string('title'); $table->integer('seats');
});
final class CourseRecord extends Illuminate\Database\Eloquent\Model {
    protected $table = 'courses';
    public $timestamps = false;
    protected $fillable = ['title', 'seats'];
    protected $casts = ['seats' => 'integer'];
}
$course = CourseRecord::create(['title' => 'PDO', 'seats' => 2]);
echo $course->title, ' ', $course->seats, ' ', CourseRecord::count(), PHP_EOL;
Expected output
PDO 2 1

The result proves this isolated model can persist and read one row. It does not execute application observers or policies.

Verify schema, model, and business effects independently

Test duplicate keys and invalid references against the intended database constraint, then test how the application translates that rejection. Check no-row versus null-field outcomes and type assumptions after retrieval. For migrations, use representative synthetic prior data and verify transformed values and compatibility, not merely successful command exit. Rollback may not undo an external payment or every engine-specific DDL effect. Keep Eloquent relationship loading and query counts visible as the dataset grows. A passing SQLite fixture is a useful starting point, but target-engine behavior and application access rules require additional controlled evidence.

Common errors and fixes

All request fields are passed to create

Why it happens
Mass-assignment convenience replaces a writable-data policy.
How to fix it
Select validated fields and keep server-owned attributes explicit.
Prevent it next time
Test unexpected sensitive fields and direct-assignment paths.

A bound value is assumed to protect dynamic column names

Why it happens
Data parameters and SQL structure are conflated.
How to fix it
Allowlist structural choices and bind values separately.
Prevent it next time
Review every raw expression and user-selected sort field.

An in-memory migration pass is called production safety

Why it happens
Existing data and engine-specific DDL behavior are omitted.
How to fix it
Test target-engine changes with representative synthetic prior data.
Prevent it next time
Record rollback, compatibility, and untested limitations.

Check your understanding

1. Does a seats cast validate a negative incoming quantity?

Show worked solution

No. A cast changes representation according to its rules; domain range validation is separate. Reject invalid input before a reservation effect and preserve durable constraints where appropriate. Test the returned type, accepted range, and absence of mutation after rejection.

2. What does the SQLite model example leave untested?

Show worked solution

It omits the real application bootstrap, policies, observers, configured database engine, concurrency, and production migration data. Add focused integration checks for the actual risk rather than claiming a complete persistence guarantee from one successful create call.

Key takeaways

  • Schema constraints, model conveniences, validation, and access rules are distinct.
  • Use owned fixtures and disclose driver and application boundaries.

Official references