Skip to content

01: Mapping Concepts: Rails vs Laravel

Mapping Concepts: Rails vs Laravel

If you’ve worked with Ruby on Rails, you already understand web frameworks—you just need to see how those concepts translate to Laravel. Laravel’s creator, Taylor Otwell, openly acknowledges Rails as a major inspiration. The result? Two frameworks that share remarkably similar philosophies and patterns.

This chapter shows you Rails code you know, then demonstrates the Laravel equivalent. You’ll discover that Laravel isn’t fundamentally different from Rails—it’s the same concepts with different syntax and conventions.

By the end of this chapter, you’ll have a comprehensive mental map that lets you confidently translate your Rails knowledge into Laravel development.

Before starting this chapter, you should have:

  • Completion of Chapter 00: Introduction or equivalent understanding
  • Basic familiarity with Ruby on Rails (routing, models, controllers, migrations)
  • PHP 8.4+ installed (for running Laravel examples)
  • Estimated Time: ~45-60 minutes

Verify your setup:

Terminal window
php --version # Should show PHP 8.4 or higher

By the end of this chapter, you will have:

  • A comprehensive mental map of Rails-to-Laravel concept translations
  • Understanding of how routing, models, controllers, and views compare
  • Knowledge of migration syntax differences
  • Familiarity with Laravel’s command-line tools (artisan)
  • A reference guide for common Rails patterns in Laravel
  • Map Rails routing patterns to Laravel routing syntax
  • Compare ActiveRecord with Eloquent ORM
  • Understand controller differences between Rails and Laravel
  • Translate ERB templates to Blade syntax
  • Learn migration command and syntax differences
  • Compare command-line tools: rails vs artisan
  • Understand validation and authentication approaches

If you want to see the big picture first, here’s a side-by-side comparison of a simple Rails and Laravel feature:

Rails Route + Controller:

config/routes.rb
resources :posts
# app/controllers/posts_controller.rb
def index
@posts = Post.where(published: true).order(created_at: :desc)
end

Laravel Route + Controller:

routes/web.php
Route::resource('posts', PostController::class);
# app/Http/Controllers/PostController.php
public function index()
{
$posts = Post::where('published', true)->orderBy('created_at', 'desc')->get();
return view('posts.index', compact('posts'));
}

Notice the similarities? The patterns are nearly identical—just different syntax. Let’s dive into the details.

Both Rails and Laravel embrace:

  • Convention over configuration - Sensible defaults that “just work”
  • MVC architecture - Separation of concerns
  • Active Record pattern - Models that map to database tables
  • RESTful routing - Resource-oriented URLs
  • Developer happiness - Focus on productivity and joy
  • Batteries included - Full-stack frameworks with everything you need

::: tip As a Rails developer, you’re already 70% of the way to understanding Laravel. The remaining 30% is mostly syntax and PHP-specific patterns. :::

Here’s a high-level comparison to orient you:

Rails ConceptLaravel EquivalentNotes
rails new applaravel new appCreate new application
rails serverphp artisan serveStart development server
rails consolephp artisan tinkerInteractive REPL
rails generatephp artisan make:Code generation
ActiveRecordEloquent ORMNearly identical Active Record pattern
routes.rbroutes/web.phpRoute definitions
ControllersControllersSame concept, different namespace
ERB templatesBlade templatesSimilar syntax, different directives
db/migrate/database/migrations/Migration files
rake db:migrateartisan migrateRun migrations
has_manyhasMany()Relationships (camelCase in Laravel)
validatesValidation rulesDifferent syntax, same concept
DeviseBreeze/JetstreamAuthentication scaffolding
RSpec/MinitestPHPUnit/PestTesting frameworks
Bundler (Gemfile)ComposerDependency management

Learn how Rails routing patterns translate directly to Laravel’s routing syntax.

  1. Compare basic routing syntax

In Rails, you define routes in config/routes.rb:

config/routes.rb
Rails.application.routes.draw do
# Simple route
get 'welcome', to: 'pages#welcome'
# RESTful resource routes
resources :posts
# Nested resources
resources :posts do
resources :comments
end
# Root route
root 'pages#home'
end

In Laravel, routes are defined in routes/web.php:

routes/web.php
<?php
use App\Http\Controllers\PageController;
use App\Http\Controllers\PostController;
use App\Http\Controllers\CommentController;
// Simple route
Route::get('/welcome', [PageController::class, 'welcome']);
// RESTful resource routes
Route::resource('posts', PostController::class);
// Nested resources
Route::resource('posts.comments', CommentController::class);
// Root route
Route::get('/', [PageController::class, 'home']);
  1. Understand route helpers

Rails:

post_path(@post) # => /posts/1
posts_path # => /posts
edit_post_path(@post) # => /posts/1/edit

Laravel:

example-route-helpers.php
<?php
declare(strict_types=1);
// Generate URL from named route
route('posts.show', $post); // => /posts/1
route('posts.index'); // => /posts
route('posts.edit', $post); // => /posts/1/edit

You should understand that:

  • Rails uses symbol-based routing ('controller#action')
  • Laravel uses class-based routing ([Controller::class, 'method'])
  • Both support RESTful resource routing
  • Route helpers work similarly but use different syntax

Both frameworks follow RESTful conventions, automatically generating standard CRUD routes. Laravel’s Route::resource() creates the same seven routes as Rails’ resources, following identical HTTP verb and URL patterns.

::: tip Laravel Route Naming Laravel automatically names resource routes using the pattern resource.action. For example, Route::resource('posts', PostController::class) creates named routes like posts.index, posts.show, posts.store, etc. :::

  • Error: “Target class does not exist” — Ensure you’ve imported the controller class with use App\Http\Controllers\ControllerName; at the top of your routes file.

  • Routes not working — Run php artisan route:list to see all registered routes (equivalent to rails routes in Rails).

Step 2: Comparing Models and ORM (~15 min)

Section titled “Step 2: Comparing Models and ORM (~15 min)”

Understand how ActiveRecord and Eloquent provide nearly identical functionality with different syntax.

  1. Compare model relationships

Rails Model:

app/models/post.rb
class Post < ApplicationRecord
# Relationships
belongs_to :user
has_many :comments, dependent: :destroy
has_many :tags, through: :post_tags
# Scopes
scope :published, -> { where(published: true) }
scope :recent, -> { order(created_at: :desc).limit(10) }
# Callbacks
before_save :generate_slug
private
def generate_slug
self.slug = title.parameterize
end
end

Laravel Model:

app/Models/Post.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Post extends Model
{
protected $fillable = ['title', 'body', 'slug', 'published', 'user_id'];
// Relationships
public function user()
{
return $this->belongsTo(User::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function tags()
{
return $this->belongsToMany(Tag::class, 'post_tags');
}
// Scopes
public function scopePublished($query)
{
return $query->where('published', true);
}
public function scopeRecent($query)
{
return $query->orderBy('created_at', 'desc')->limit(10);
}
// Model events
protected static function booted()
{
static::saving(function ($post) {
$post->slug = \Str::slug($post->title);
});
}
}
  1. Compare query building

Rails:

# Find by ID
post = Post.find(1)
# Where clauses
posts = Post.where(published: true)
.order('created_at DESC')
.limit(10)
# Eager loading
posts = Post.includes(:comments, :user).all

Laravel:

example-queries.php
<?php
declare(strict_types=1);
use App\Models\Post;
// Find by ID
$post = Post::find(1);
// Where clauses
$posts = Post::where('published', true)
->orderBy('created_at', 'desc')
->limit(10)
->get();
// Eager loading
$posts = Post::with('comments', 'user')->get();

You should see that:

  • Relationship methods use camelCase in Laravel (hasMany vs has_many)
  • Scopes are methods prefixed with scope in Laravel
  • Query building is nearly identical between frameworks
  • Laravel requires explicit ->get() to execute queries

Both frameworks implement the Active Record pattern where models directly correspond to database tables. The query builders use method chaining to construct SQL, making the APIs feel nearly identical despite being different languages.

::: tip Mass Assignment Laravel requires you to define fillable or guarded properties for mass assignment protection, whereas Rails uses strong parameters in controllers with params.require().permit(). :::

Learn how Rails controller actions map to Laravel controller methods.

  1. Compare a typical RESTful controller

Rails:

app/controllers/posts_controller.rb
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
before_action :set_post, only: [:show, :edit, :update, :destroy]
def index
@posts = Post.published.recent.page(params[:page])
end
def show
end
def create
@post = current_user.posts.build(post_params)
if @post.save
redirect_to @post, notice: 'Post created successfully.'
else
render :new, status: :unprocessable_entity
end
end
private
def set_post
@post = Post.find(params[:id])
end
def post_params
params.require(:post).permit(:title, :body, :published)
end
end

Laravel:

app/Http/Controllers/PostController.php
<?php
namespace App\Http\Controllers;
use App\Models\Post;
use App\Http\Requests\StorePostRequest;
class PostController extends Controller
{
public function __construct()
{
$this->middleware('auth')->except(['index', 'show']);
}
public function index()
{
$posts = Post::published()->recent()->paginate(15);
return view('posts.index', compact('posts'));
}
public function show(Post $post)
{
// Route model binding automatically finds the post
return view('posts.show', compact('post'));
}
public function store(StorePostRequest $request)
{
$post = $request->user()->posts()->create($request->validated());
return redirect()
->route('posts.show', $post)
->with('success', 'Post created successfully.');
}
}

Key differences you should notice:

  • Laravel uses middleware() instead of before_action
  • Laravel has route model binding (no need for set_post)
  • Validation happens in Form Request classes, not controllers
  • Flash messages use ->with() instead of notice:

Both frameworks follow the MVC pattern with controllers handling HTTP requests. Laravel’s dependency injection automatically resolves method parameters, enabling features like route model binding where Post $post automatically queries the database.

::: tip Route Model Binding In Laravel, type-hinting a model in your controller method (Post $post) automatically queries the database using the route parameter. This eliminates Rails’ need for before_action :set_post filters. If the record isn’t found, Laravel automatically returns a 404 response. :::

  • Error: “Too few arguments to function” — Ensure your route parameter name matches the variable name in the controller method. For example, if your route is posts/{post}, your method parameter should be Post $post, not Post $item.

  • Error: “Class ‘App\Http\Controllers\Post’ not found” — You’re missing the use App\Models\Post; import statement at the top of your controller file.

  • Middleware not working — Verify the middleware is registered in app/Http/Kernel.php and that you’re using the correct middleware alias in your controller.

Translate ERB template syntax to Blade template syntax.

  1. Compare template syntax

Rails (ERB):

app/views/posts/index.html.erb
<h1>All Posts</h1>
<% if @posts.any? %>
<ul>
<% @posts.each do |post| %>
<li>
<%= link_to post.title, post_path(post) %>
<% if post.published? %>
<span class="badge">Published</span>
<% end %>
</li>
<% end %>
</ul>
<% else %>
<p>No posts found.</p>
<% end %>

Laravel (Blade):

{{-- resources/views/posts/index.blade.php --}}
<h1>All Posts</h1>
@if($posts->count() > 0)
<ul>
@foreach($posts as $post)
<li>
<a href="{{ route('posts.show', $post) }}">{{ $post->title }}</a>
@if($post->published)
<span class="badge">Published</span>
@endif
</li>
@endforeach
</ul>
@else
<p>No posts found.</p>
@endif
  1. Compare common directives
FeatureRails (ERB)Laravel (Blade)
Output (escaped)Uses equals syntaxUses double-brace syntax
ConditionalsTag-based if/else/end@if / @else / @endif
LoopsBlock iteration with .each@foreach / @endforeach
CommentsHash-prefixed tagsDouble-dash curly syntax
Partialsrender helper@include directive

Key Similarities:

  • Both escape output by default for XSS protection
  • Both support layouts and content sections
  • Both compile templates for performance
  • Both provide raw/unescaped output options

You should understand that:

  • Blade uses at-sign directives (like @if, @foreach) instead of ERB tag syntax
  • Both frameworks escape output by default for security
  • Blade syntax is cleaner and more readable
  • Variable access uses dollar-sign prefix in Blade

Both templating engines compile to native code (Ruby/PHP) for performance. They provide the same security features (auto-escaping), layout inheritance, and partials—just with different syntax.

Learn how Rails migrations translate to Laravel migrations.

  1. Compare migration syntax

Rails:

db/migrate/20240101000000_create_posts.rb
class CreatePosts < ActiveRecord::Migration[7.0]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body
t.string :slug, index: { unique: true }
t.boolean :published, default: false
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end

Laravel:

database/migrations/2024_01_01_000000_create_posts_table.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()
{
Schema::create('posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->text('body')->nullable();
$table->string('slug')->unique();
$table->boolean('published')->default(false);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('posts');
}
};
  1. Compare migration commands
TaskRailsLaravel
Create migrationrails g migration AddXToYartisan make:migration add_x_to_y
Run migrationsrake db:migrateartisan migrate
Rollbackrake db:rollbackartisan migrate:rollback
Reset databaserake db:resetartisan migrate:fresh

You should see that:

  • Laravel uses up() and down() methods explicitly
  • Column modifiers chain in Laravel (->nullable()->default())
  • Foreign keys are more explicit in Laravel
  • Commands are nearly identical

Both frameworks provide a DSL for database schema management, allowing version control of database structure. The migration files track changes over time and can be rolled back, making database evolution safe and repeatable.

Understand the key difference between Rails’ model-level validation and Laravel’s request-level validation.

  1. Compare validation philosophies

Rails (Model Validation):

app/models/post.rb
class Post < ApplicationRecord
validates :title, presence: true, length: { minimum: 5, maximum: 200 }
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :slug, uniqueness: true
validates :category, inclusion: { in: %w[tech business lifestyle] }
end

Laravel (Request Validation):

app/Http/Requests/StorePostRequest.php
<?php
declare(strict_types=1);
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StorePostRequest extends FormRequest
{
public function authorize(): bool
{
return true; // or check user permissions
}
public function rules(): array
{
return [
'title' => 'required|min:5|max:200',
'email' => 'required|email',
'slug' => 'required|unique:posts,slug',
'category' => 'in:tech,business,lifestyle',
];
}
public function messages(): array
{
return [
'title.required' => 'Please provide a post title',
'title.min' => 'Title must be at least 5 characters',
];
}
}

You should understand that:

  • Rails validates at the model level before saving to database
  • Laravel validates at the HTTP request level before reaching controllers
  • Laravel’s approach separates validation logic from models
  • Both provide similar validation rules with different syntax

Laravel’s request validation approach decouples validation from models, allowing different validation rules for the same model depending on the context (create vs update, public API vs admin panel). Rails keeps validation closer to the model for consistency.

::: tip Validation Benefits Laravel Form Requests also handle authorization logic, allowing you to check if the user has permission to perform the action in the same class that validates the input. :::

Step 7: Command-Line Tools Comparison (~5 min)

Section titled “Step 7: Command-Line Tools Comparison (~5 min)”

Map Rails CLI commands to Laravel Artisan equivalents.

Rails CLI:

Terminal window
# Generate scaffolding
rails generate scaffold Post title:string body:text
rails generate model Post title:string
rails generate controller Posts index show
# Database operations
rails db:migrate
rails db:seed
rails db:rollback
# Console and server
rails console
rails server

Laravel Artisan:

Terminal window
# Generate scaffolding
php artisan make:model Post -mcr # Model + Migration + Controller + Resource
php artisan make:controller PostController --resource
php artisan make:migration create_posts_table
# Database operations
php artisan migrate
php artisan db:seed
php artisan migrate:rollback
# Console and server
php artisan tinker
php artisan serve

You should see that Artisan provides the same code generation and database management capabilities as Rails, just with php artisan instead of rails.

Both frameworks embrace code generation to boost productivity. Artisan’s make: commands parallel Rails’ generate commands, creating boilerplate files that follow framework conventions.

You’ve completed a comprehensive mapping of Rails to Laravel concepts! Here’s what you’ve accomplished:

Mapped routing patterns - Translated Rails routes to Laravel syntax with route model binding
Compared ORMs - Understood ActiveRecord vs Eloquent similarities and query building
Learned controller differences - Saw how Rails controllers map to Laravel with dependency injection
Translated templates - Converted ERB to Blade syntax with cleaner directives
Compared migrations - Understood database schema differences and chaining modifiers
Understood validation - Compared model-level vs request-level validation approaches
Learned command-line tools - Mapped rails commands to artisan equivalents

As a Rails developer, you now understand that Laravel isn’t fundamentally different—it’s the same concepts with different syntax and some architectural improvements. The main differences are:

  • Syntax: PHP vs Ruby, but patterns remain familiar
  • Architecture: Laravel’s service container and dependency injection
  • Validation: Request-level in Laravel vs model-level in Rails
  • Routing: Class-based controllers vs string-based in Rails
  • Model Binding: Automatic in Laravel vs explicit before_action in Rails

You’re now ready to dive deeper into modern PHP features that make Laravel possible, and see how PHP 8.4 brings many features Rails developers already know and love.

Exercise 1: Translate a Rails Model to Laravel

Section titled “Exercise 1: Translate a Rails Model to Laravel”

Goal: Practice translating Rails ActiveRecord patterns to Laravel Eloquent

Create a Laravel model equivalent to this Rails model:

class Post < ApplicationRecord
belongs_to :user
has_many :comments
validates :title, presence: true
scope :published, -> { where(published: true) }
end

Your Laravel model should include:

  • The same relationships (belongsTo, hasMany)
  • A published scope method
  • Proper namespace and type hints

Validation: Test your implementation:

$post = Post::factory()->create(['published' => true]);
// Test relationship
$this->assertInstanceOf(User::class, $post->user);
// Test scope
$published = Post::published()->get();

Expected result: A working Laravel model that mirrors the Rails model’s functionality.

::: tip Continue Learning Move on to Chapter 02: Modern PHP: What’s Changed to learn about modern PHP 8.4 features. :::