Laravel MVC Architecture: A Beginner's Guide with Simple Examples
Learn the Model-View-Controller pattern and how Laravel implements it.
Migrations are like version control for your database. They allow you to define and share your database schema in code, making it easy to create, modify, and rollback database tables across different environments.
Think of migrations as:
Create a New Migration
# Basic migration
php artisan make:migration create_users_table
# Create migration with model
php artisan make:model User -m
# Create specific table migration
php artisan make:migration create_posts_table
# Create migration for modifying existing table
php artisan make:migration add_category_id_to_posts_table
# Create pivot table migration
php artisan make:migration create_post_tag_table
When you run php artisan make:migration create_users_table, Laravel creates a file like:
<?php
// database/migrations/2024_01_15_000000_create_users_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id(); // Auto-incrementing BIGINT primary key
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps(); // created_at and updated_at
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};
php artisan migratephp artisan migrate:rollbackpublic function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('content');
$table->foreignId('user_id')->constrained();
$table->timestamps();
});
}
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title', 200);
$table->string('slug')->unique();
$table->text('excerpt')->nullable();
$table->longText('content');
$table->string('featured_image')->nullable();
$table->boolean('is_published')->default(false);
$table->timestamp('published_at')->nullable();
$table->integer('views_count')->default(0);
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('category_id')->constrained();
$table->timestamps();
$table->softDeletes(); // Adds deleted_at column
// Indexes for better performance
$table->index(['is_published', 'published_at']);
$table->index('slug');
$table->index('user_id');
});
}
Schema::create('examples', function (Blueprint $table) {
// Primary Key
$table->id(); // BIGINT auto-increment
$table->uuid('uuid')->primary(); // UUID primary key
// Strings and Text
$table->string('name', 100); // VARCHAR with length
$table->text('description'); // TEXT
$table->longText('content'); // LONGTEXT
$table->char('code', 10); // CHAR with fixed length
// Numbers
$table->integer('votes'); // INTEGER
$table->tinyInteger('status'); // TINYINT
$table->unsignedInteger('user_count'); // UNSIGNED INTEGER
$table->bigInteger('views'); // BIGINT
$table->decimal('price', 8, 2); // DECIMAL(8,2)
$table->double('rating', 3, 1); // DOUBLE(3,1)
$table->float('amount'); // FLOAT
// Date and Time
$table->date('birth_date'); // DATE
$table->dateTime('published_at'); // DATETIME
$table->time('start_time'); // TIME
$table->timestamp('email_verified_at'); // TIMESTAMP
$table->timestamps(); // created_at & updated_at
$table->softDeletes(); // deleted_at
// Boolean
$table->boolean('is_active'); // BOOLEAN/TINYINT(1)
// JSON
$table->json('meta'); // JSON column
$table->jsonb('settings'); // JSONB (PostgreSQL)
// Special Types
$table->enum('difficulty', ['easy', 'medium', 'hard']);
$table->ipAddress('visitor_ip');
$table->macAddress('device_mac');
$table->uuid('public_id');
$table->year('release_year');
});
Schema::create('products', function (Blueprint $table) {
$table->id();
// Nullable
$table->string('description')->nullable();
// Default values
$table->integer('stock')->default(0);
$table->boolean('in_stock')->default(true);
$table->string('currency')->default('USD');
// Unique constraints
$table->string('sku')->unique();
$table->string('slug')->unique();
// Indexes
$table->string('name')->index();
$table->decimal('price')->index();
// Multiple column unique
$table->unique(['store_id', 'product_code']);
// Fulltext index (MySQL)
$table->text('content')->fulltext();
// Spatial index (PostgreSQL/MySQL)
$table->point('location')->spatialIndex();
// Comment
$table->string('status')->comment('Product status: active, inactive, draft');
});
public function up(): void
{
Schema::create('comments', function (Blueprint $table) {
$table->id();
$table->text('body');
// Simple foreign key
$table->foreignId('user_id')->constrained();
// With custom table name
$table->foreignId('author_id')->constrained('users');
// With on delete cascade
$table->foreignId('post_id')
->constrained()
->onDelete('cascade');
// With on delete set null
$table->foreignId('parent_id')
->nullable()
->constrained('comments')
->onDelete('set null');
$table->timestamps();
});
}
$table->unsignedBigInteger('category_id');
$table->foreign('category_id')
->references('id')
->on('categories')
->onDelete('cascade');
php artisan make:migration add_featured_image_to_posts_table
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->string('featured_image')->after('title')->nullable();
$table->integer('reading_time')->after('content')->default(1);
$table->boolean('is_featured')->after('is_published')->default(false);
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn(['featured_image', 'reading_time', 'is_featured']);
});
}
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
// Change column type
$table->string('name', 100)->change();
// Make column nullable
$table->string('phone')->nullable()->change();
// Add default value
$table->string('timezone')->default('UTC')->change();
});
}
// Note: Requires doctrine/dbal package for column modifications
// composer require doctrine/dbal
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->renameColumn('is_published', 'published');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->renameColumn('published', 'is_published');
});
}
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('old_column');
$table->dropColumn(['column1', 'column2', 'column3']);
});
}
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
// Single column index
$table->index('slug');
// Unique index
$table->unique('email');
// Composite index
$table->index(['user_id', 'created_at']);
// Full-text index
$table->fullText('content');
// Spatial index
$table->spatialIndex('location');
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropIndex(['slug']);
$table->dropUnique(['email']);
$table->dropIndex(['user_id', 'created_at']);
$table->dropFullText(['content']);
$table->dropSpatialIndex(['location']);
});
}
php artisan make:migration create_post_tag_table
public function up(): void
{
Schema::create('post_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('post_id')->constrained()->onDelete('cascade');
$table->foreignId('tag_id')->constrained()->onDelete('cascade');
$table->integer('order')->default(0);
$table->timestamps();
// Prevent duplicate relationships
$table->unique(['post_id', 'tag_id']);
});
}
public function down(): void
{
Schema::dropIfExists('post_tag');
}
# Run all pending migrations
php artisan migrate
# Show migration status
php artisan migrate:status
# Rollback last migration batch
php artisan migrate:rollback
# Rollback specific number of migrations
php artisan migrate:rollback --step=3
# Rollback all migrations
php artisan migrate:reset
# Rollback and re-run all migrations
php artisan migrate:refresh
# Refresh and seed the database
php artisan migrate:refresh --seed
# Run migrations for production
php artisan migrate --force
# Run specific migration group
php artisan migrate --path=/database/migrations/2024_01_15_000000_create_users_table.php
# Run migrations from specific folder
php artisan migrate --path=/database/migrations/custom/
<?php
// database/migrations/2024_01_15_000001_create_ecommerce_tables.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
{
// Categories table
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->string('image')->nullable();
$table->boolean('is_active')->default(true);
$table->integer('order')->default(0);
$table->foreignId('parent_id')->nullable()->constrained('categories');
$table->timestamps();
$table->softDeletes();
$table->index(['is_active', 'order']);
});
// Products table
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->text('short_description')->nullable();
$table->decimal('price', 10, 2);
$table->decimal('sale_price', 10, 2)->nullable();
$table->string('sku')->unique();
$table->integer('stock_quantity')->default(0);
$table->boolean('in_stock')->default(true);
$table->boolean('is_featured')->default(false);
$table->boolean('is_active')->default(true);
$table->json('images')->nullable();
$table->json('specifications')->nullable();
$table->foreignId('category_id')->constrained();
$table->foreignId('brand_id')->constrained();
$table->timestamps();
$table->softDeletes();
$table->index(['is_active', 'is_featured']);
$table->index(['category_id', 'is_active']);
$table->fullText(['name', 'description']);
});
// Orders table
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->string('order_number')->unique();
$table->foreignId('user_id')->constrained();
$table->decimal('subtotal', 10, 2);
$table->decimal('tax', 10, 2)->default(0);
$table->decimal('shipping', 10, 2)->default(0);
$table->decimal('total', 10, 2);
$table->string('status')->default('pending'); // pending, processing, completed, cancelled
$table->text('notes')->nullable();
$table->json('billing_address');
$table->json('shipping_address');
$table->timestamp('paid_at')->nullable();
$table->timestamp('shipped_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'created_at']);
$table->index('status');
});
}
public function down(): void
{
Schema::dropIfExists('order_items');
Schema::dropIfExists('orders');
Schema::dropIfExists('products');
Schema::dropIfExists('categories');
Schema::dropIfExists('brands');
}
};
# ✅ GOOD
php artisan make:migration create_posts_table
php artisan make:migration add_published_at_to_posts_table
php artisan make:migration create_post_tag_pivot_table
# ❌ BAD
php artisan make:migration update_table
php artisan make:migration add_columns
// ✅ GOOD: One migration per logical change
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->boolean('is_featured')->default(false);
});
}
// ❌ BAD: Multiple unrelated changes
public function up(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->boolean('is_featured')->default(false);
});
Schema::table('users', function (Blueprint $table) {
$table->string('phone')->nullable();
});
}
public function down(): void
{
Schema::table('posts', function (Blueprint $table) {
$table->dropColumn('is_featured');
});
}
// ✅ GOOD
$table->foreignId('user_id')->constrained()->onDelete('cascade');
// ❌ BAD
$table->integer('user_id');
// Add indexes for frequently queried columns
$table->index('email');
$table->index(['status', 'created_at']);
$table->unique('slug');
# Reset and re-run
php artisan migrate:reset
php artisan migrate
# Or manually delete the migration from migrations table
// Ensure tables are created in correct order
// Users table should exist before posts table
// Or use separate migrations
public function up(): void
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->unsignedBigInteger('user_id'); // Add without constraint first
$table->timestamps();
});
// Add foreign key in separate migration
Schema::table('posts', function (Blueprint $table) {
$table->foreign('user_id')->references('id')->on('users');
});
}
composer require doctrine/dbal
Migrations are version control for your database schema. They allow you to define database changes in code and apply them consistently across different environments.
Use php artisan make:migration migration_name. The migration file will be created in the database/migrations directory.
The up() method applies the migration changes, while down() reverses them. This allows for rollbacks and ensures database changes are reversible.
Use $table->foreignId('user_id')->constrained() or manually with $table->foreign('user_id')->references('id')->on('users').
timestamps() adds created_at and updated_at columns, while softDeletes() adds a deleted_at column for soft deletion functionality.
Use php artisan migrate:rollback to rollback the last batch, or php artisan migrate:rollback --step=3 to rollback specific number of migrations.
# Create migrations
php artisan make:migration create_table_name
php artisan make:migration add_column_to_table
php artisan make:model ModelName -m
# Run migrations
php artisan migrate
php artisan migrate:status
php artisan migrate --force
# Rollback migrations
php artisan migrate:rollback
php artisan migrate:rollback --step=3
php artisan migrate:reset
php artisan migrate:refresh
php artisan migrate:refresh --seed
Now you're equipped to manage your database schema like a pro with Laravel migrations! In our next post, we'll explore Laravel Eloquent ORM: What is it and Why is it a Game-Changer? to learn how to interact with your database using elegant, expressive syntax.