Introduction
Laravel's Schema Builder offers fluent methods for every MySQL and PostgreSQL column type, plus modifiers like nullable, default, unique, and index that chain onto the column definition.
Key Concepts
- String / number / date types: Fluent methods like
string,integer,decimal,date,dateTime,timestamp,json,boolean. - Column modifier: A chainable method that alters the column's constraint —
nullable(),default(0),unique(),index(). - Primary key: The row identifier.
id()creates an unsigned BIGINT auto-increment;uuid()->primary()andulid()->primary()are alternatives. - Index: An optional database structure that speeds up queries on a column.
$table->index('column')or$table->unique(['a', 'b'])for composites. - Foreign key: A constraint linking this table's column to another table's primary key.
foreignId('user_id')->constrained()is the idiomatic shorthand.
Real World Context
Getting column types right up front saves migration churn later. Choosing decimal over float for money, json for settings, foreignId()->constrained() for relationships — these are the details that separate a solid schema from one you'll keep patching.
Deep Dive
Laravel's Schema Builder provides a fluent API for defining database columns. Let's explore all the column types and modifiers available.
Common Column Types
Strings and Text
php$table->char('code', 4); // Fixed-length string $table->string('name', 100); // VARCHAR(100), default 255 $table->text('description'); // TEXT $table->mediumText('content'); // MEDIUMTEXT $table->longText('body'); // LONGTEXT
Numbers
php$table->integer('quantity'); // INTEGER $table->tinyInteger('status'); // TINYINT $table->smallInteger('order'); // SMALLINT $table->mediumInteger('medium'); // MEDIUMINT $table->bigInteger('views'); // BIGINT $table->unsignedInteger('count'); // UNSIGNED INTEGER $table->unsignedBigInteger('user_id'); // For foreign keys $table->float('amount'); // FLOAT — use $table->float('amount', precision: 53) to override $table->double('latitude'); // DOUBLE $table->decimal('price', total: 10, places: 2); // DECIMAL(10, 2) - exact, use for money
Boolean
php$table->boolean('is_active'); // BOOLEAN (TINYINT(1))
Dates and Times
php$table->date('birth_date'); // DATE $table->time('alarm_time'); // TIME $table->dateTime('published_at'); // DATETIME $table->timestamp('verified_at'); // TIMESTAMP $table->timestamps(); // created_at & updated_at $table->timestampTz('created_at'); // TIMESTAMP with timezone $table->year('graduation_year'); // YEAR
JSON
php$table->json('settings'); // JSON $table->jsonb('settings'); // JSONB (PostgreSQL)
Binary
php$table->binary('data'); // BLOB
Special Types
php$table->id(); // Auto-incrementing BIGINT primary key $table->uuid('id'); // UUID column $table->ulid('id'); // ULID column $table->foreignId('user_id'); // UNSIGNED BIGINT (for foreign keys) $table->ipAddress('visitor_ip'); // IP address (VARCHAR(45)) $table->macAddress('device_mac'); // MAC address $table->enum('status', ['draft', 'published', 'archived']); $table->set('options', ['a', 'b', 'c']); // SET type
Column Modifiers
Modifiers change column behavior:
php$table->string('email')->nullable(); // Allow NULL $table->string('email')->nullable(false); // NOT NULL (default) $table->integer('votes')->default(0); // Default value $table->string('email')->unique(); // UNIQUE constraint $table->integer('order')->unsigned(); // UNSIGNED $table->string('title')->comment('Post title'); // Column comment $table->timestamp('added_at')->useCurrent(); // Default CURRENT_TIMESTAMP $table->timestamp('updated_at')->useCurrentOnUpdate(); // Update on change
Positioning (MySQL)
php$table->string('city')->after('address'); // After specific column $table->string('id')->first(); // First column
Invisible Columns (MySQL 8.0.23+)
php$table->string('secret')->invisible(); // Excluded from SELECT *
Primary Keys
php// Auto-incrementing primary key $table->id(); // id BIGINT UNSIGNED AUTO_INCREMENT $table->id('post_id'); // Custom name // Composite primary key $table->primary(['user_id', 'post_id']); // UUID primary key $table->uuid('id')->primary();
Indexes
php// Single column $table->string('email')->unique(); // Unique index $table->string('slug')->index(); // Regular index // Multiple columns $table->unique(['email', 'tenant_id']); // Composite unique $table->index(['status', 'created_at']); // Composite index // Named indexes $table->index('email', 'users_email_index'); // Full-text index $table->text('body'); $table->fullText('body'); // Spatial index $table->point('location'); $table->spatialIndex('location');
Foreign Keys
php// Method 1: Explicit foreign key $table->unsignedBigInteger('user_id'); $table->foreign('user_id') ->references('id') ->on('users') ->onDelete('cascade'); // Method 2: Shorthand (recommended) $table->foreignId('user_id')->constrained(); // With options $table->foreignId('user_id') ->constrained() // References users.id ->onUpdate('cascade') ->onDelete('cascade'); // Custom table/column $table->foreignId('author_id') ->constrained('users', 'id') ->nullOnDelete(); // Nullable foreign key $table->foreignId('category_id') ->nullable() ->constrained() ->nullOnDelete(); // Set to NULL when parent deleted
Complete Migration Example
php<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; return new class extends Migration { public function up(): void { Schema::create('posts', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->cascadeOnDelete(); $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete(); $table->string('title'); $table->string('slug')->unique(); $table->text('excerpt')->nullable(); $table->longText('body'); $table->string('featured_image')->nullable(); $table->json('metadata')->nullable(); $table->unsignedInteger('views')->default(0); $table->boolean('is_featured')->default(false); $table->timestamp('published_at')->nullable(); $table->timestamps(); $table->softDeletes(); // Indexes $table->index(['published_at', 'is_featured']); $table->fullText('body'); }); } public function down(): void { Schema::dropIfExists('posts'); } };
Common Pitfalls
- Using
floatfor money — Floating-point rounding errors corrupt prices and balances. Usedecimal(10, 2)instead. - Forgetting
->nullable()— Makes the column NOT NULL by default, which fails when your app writesnull. - Missing indexes on filtered columns — A
where('status', 'active')query on an unindexed column is a full table scan.
Best Practices
- Use
$table->id()for primary keys — It's the Laravel convention; don't fight it without a reason. - Always index foreign keys via
foreignId()->constrained()— One line, one FK constraint, one index. - Add composite indexes for common filter combinations —
$table->index(['status', 'created_at'])for a posts listing page.
Summary
- String, number, date, JSON, and boolean column types cover every SQL primitive.
- Modifiers (
nullable,default,unique,index,comment,after,first) chain onto column definitions. $table->id()is the canonical primary key;uuid()->primary()andulid()->primary()are alternatives.- Indexes (single and composite, unique and non-unique) speed up common queries.
foreignId()->constrained()is the one-line shorthand for foreign key columns plus constraints.