Hello World & Running
Hello, World
<?php
echo "Hello, World!\n"; puts "Hello, World!" Ruby has no
<?php opening tag — every file is Ruby from the first byte, and top-level code runs the moment the file loads. puts writes its argument followed by a newline, so unlike PHP's echo you do not append \n yourself. There are no semicolons; a newline ends a statement.Running a Script
<?php
// Save as hello.php and run from a terminal:
// php hello.php
$name = "World";
echo "Hello, {$name}!\n"; # Save as hello.rb and run from a terminal:
# ruby hello.rb
name = "World"
puts "Hello, #{name}!" Both languages are interpreted and run a file directly — no compile step. Ruby comments use
# rather than //. Ruby does not run inside a web server by default the way PHP historically did; it is a general-purpose scripting language, and web frameworks such as Rails or Sinatra sit on top of it explicitly.Output: echo vs puts/print/p
<?php
echo "no newline";
echo "added newline\n";
print "print returns 1\n";
var_dump([1, 2, 3]);
printf("%s is %d\n", "age", 30); print "no newline"
puts "added newline"
puts "puts returns nil"
p [1, 2, 3] # inspect form
printf("%s is %d\n", "age", 30) Ruby splits PHP's
echo into two: print writes exactly what you give it, while puts adds a trailing newline (and one per array element). Ruby's p is the analogue of var_dump for quick debugging — it prints the inspect form and returns its argument, so you can wrap any expression in p without changing it.Variables & Types
Variable Assignment
<?php
$name = "Alice";
$age = 30;
$active = true;
echo "{$name} is {$age}\n"; name = "Alice"
age = 30
active = true
puts "#{name} is #{age}" Ruby variables have no
$ sigil — the bare name is the variable. The sigil-free style means a name like name reads as a plain word, but it also means Ruby uses other rules (capitalization) to distinguish constants and the leading character (@, @@, $) to mark instance, class, and global variables.Basic Types
<?php
$text = "hello";
$count = 42;
$ratio = 3.14;
$active = true;
$nothing = null;
echo gettype($text) . "\n"; // string
echo gettype($count) . "\n"; // integer
echo gettype($ratio) . "\n"; // double
echo gettype($nothing) . "\n"; // NULL text = "hello"
count = 42
ratio = 3.14
active = true
nothing = nil
puts text.class # String
puts count.class # Integer
puts ratio.class # Float
puts active.class # TrueClass
puts nothing.class # NilClass In Ruby every value is an object, so you ask the value itself for its type with
.class rather than calling a global gettype(). PHP's NULL is Ruby's nil, and notice that even true is a full object (an instance of TrueClass), not a primitive.Everything Is an Object
<?php
// In PHP, scalars are primitives — you call global functions on them.
echo strlen("hello") . "\n"; // 5
echo abs(-7) . "\n"; // 7
echo strtoupper("hi") . "\n"; // HI
// 42 has no methods; (-7)->abs() is a syntax error. # In Ruby, even integers and nil are objects with methods.
puts "hello".length # 5
puts(-7.abs) # 7
puts "hi".upcase # HI
puts 42.even? # false
puts nil.to_a.inspect # [] This is the deepest difference: PHP scalars are primitives acted on by global functions (
strlen, abs), while in Ruby 5, "hello", and even nil are objects that respond to messages. There is no global function namespace to memorize — you discover behavior by asking an object what it can do.Type Conversion
<?php
$number = (int) "42";
$text = (string) 99;
$float = (float) "3.5";
echo $number + 8 . "\n"; // 50
echo $text . "!\n"; // 99!
echo $float * 2 . "\n"; // 7 number = "42".to_i
text = 99.to_s
float = "3.5".to_f
puts number + 8 # 50
puts text + "!" # 99!
puts float * 2 # 7.0 Ruby uses explicit
to_i, to_s, and to_f methods instead of cast syntax like (int). Crucially, Ruby never converts implicitly: "42" + 8 raises a TypeError rather than silently coercing, so you convert deliberately. This is the opposite of PHP's eager juggling and eliminates a whole class of surprises.Constants
<?php
const MAX_USERS = 100;
define("APP_NAME", "Demo");
echo MAX_USERS . "\n";
echo APP_NAME . "\n"; MAX_USERS = 100
APP_NAME = "Demo"
puts MAX_USERS
puts APP_NAME Ruby has no
const keyword or define() function: any identifier that begins with a capital letter is a constant. Reassigning one only prints a warning rather than raising, so the convention is enforced socially, not strictly — but the capitalization rule means class and module names are constants too.Strings
String Interpolation
<?php
$user = "Alice";
$age = 30;
echo "Name: $user\n";
echo "Next year: " . ($age + 1) . "\n";
echo "Name: {$user}, age {$age}\n"; user = "Alice"
age = 30
puts "Name: #{user}"
puts "Next year: #{age + 1}"
puts "Name: #{user}, age #{age}" Ruby interpolates with
#{ ... }, and any expression fits inside — there is no difference between a bare variable and a computed value, so you never drop out to concatenation the way PHP does with . ($age + 1) .. Interpolation works only in double-quoted strings; single-quoted strings are literal in both languages.String Operations
<?php
$text = "Hello, World";
echo strlen($text) . "\n"; // 12
echo strtoupper($text) . "\n"; // HELLO, WORLD
echo str_replace("World", "Ruby", $text) . "\n";
echo substr($text, 0, 5) . "\n"; // Hello
echo strpos($text, "World") . "\n"; // 7 text = "Hello, World"
puts text.length # 12
puts text.upcase # HELLO, WORLD
puts text.sub("World", "Ruby")
puts text[0, 5] # Hello
puts text.index("World") # 7 Every PHP string function becomes a method you call on the string. The names are shorter and chainable:
text.upcase.reverse reads left to right, whereas strrev(strtoupper($text)) reads inside-out. Note sub replaces the first match and gsub replaces all — PHP's str_replace always replaces all.Split & Join
<?php
$csv = "apple,banana,cherry";
$fruits = explode(",", $csv);
print_r($fruits);
$joined = implode(" | ", $fruits);
echo $joined . "\n"; csv = "apple,banana,cherry"
fruits = csv.split(",")
p fruits
joined = fruits.join(" | ")
puts joined Ruby's
split and join replace explode and implode, and the argument order feels more natural — the separator is the argument to a method on the thing being split or joined. With no argument, split divides on whitespace, a common convenience PHP lacks.Multi-line Strings
<?php
$name = "Alice";
$message = <<<TEXT
Dear {$name},
Welcome aboard.
Regards
TEXT;
echo $message . "\n"; name = "Alice"
message = <<~TEXT
Dear #{name},
Welcome aboard.
Regards
TEXT
puts message Both languages have heredocs. Ruby's squiggly heredoc
<<~ strips the leading indentation of the least-indented line, so you can indent the heredoc to match surrounding code and still get a flush-left result — handy for keeping templates readable inside a method body.Arrays & Hashes
Lists: The Array Split
<?php
// PHP has ONE array type — an ordered map.
$fruits = ["apple", "banana", "cherry"];
echo $fruits[0] . "\n";
echo count($fruits) . "\n";
$fruits[] = "date"; // append
print_r($fruits); # Ruby splits PHP's array into two types.
# A plain list is an Array (integer-indexed only):
fruits = ["apple", "banana", "cherry"]
puts fruits[0]
puts fruits.length
fruits << "date" # append
p fruits This is the single biggest mental shift. PHP's array is one type that is both a list and a dictionary; Ruby separates them into
Array (integer-indexed, contiguous) and Hash (key-value). The append operator is << rather than [], and count/length/size are interchangeable.Maps: Hashes
<?php
$ages = [
"Alice" => 30,
"Bob" => 25,
];
echo $ages["Alice"] . "\n";
$ages["Carol"] = 35;
foreach ($ages as $name => $age) {
echo "{$name}: {$age}\n";
} ages = {
"Alice" => 30,
"Bob" => 25,
}
puts ages["Alice"]
ages["Carol"] = 35
ages.each do |name, age|
puts "#{name}: #{age}"
end A Ruby
Hash uses the same => "hash rocket" arrow that PHP borrowed, but it is a distinct type from Array and wraps in { }. Iteration is a method call (each) taking a block, rather than a foreach statement — the block parameters |name, age| destructure each pair automatically.Symbol Keys
<?php
// PHP keys are strings or ints.
$person = [
"name" => "Alice",
"role" => "admin",
];
echo $person["name"] . "\n"; # Ruby hashes usually use symbol keys.
person = {
name: "Alice",
role: "admin",
}
puts person[:name]
puts person[:role] Idiomatic Ruby keys hashes with symbols (
:name) rather than strings. A symbol is an immutable, interned identifier — cheaper to compare and store than a string, and the name: shorthand makes hash literals read like named arguments. Symbols have no PHP equivalent; think of them as lightweight, reusable string constants.Map, Filter, Reduce
<?php
$numbers = [1, 2, 3, 4, 5];
$doubled = array_map(fn($n) => $n * 2, $numbers);
$evens = array_filter($numbers, fn($n) => $n % 2 === 0);
$total = array_reduce($numbers, fn($carry, $n) => $carry + $n, 0);
print_r($doubled);
print_r(array_values($evens));
echo $total . "\n"; numbers = [1, 2, 3, 4, 5]
doubled = numbers.map { |number| number * 2 }
evens = numbers.select { |number| number.even? }
total = numbers.reduce(0) { |sum, number| sum + number }
p doubled
p evens
puts total Ruby's
map, select, and reduce are methods on the array, so they chain naturally and put the collection first — no more remembering whether the callback or the array comes first in array_map vs array_filter. The { |x| ... } block is Ruby's closure syntax, covered in the Closures section.Destructuring
<?php
$point = [3, 4];
[$x, $y] = $point;
echo "{$x}, {$y}\n";
$first = ["Alice", "admin", 30];
[$name, , $age] = $first; // skip the middle
echo "{$name} ({$age})\n"; point = [3, 4]
x, y = point
puts "#{x}, #{y}"
first = ["Alice", "admin", 30]
name, _, age = first # skip the middle
puts "#{name} (#{age})" Ruby destructures on the left of
= without brackets — x, y = point just works. The splat * captures the rest (first, *others = list), and a bare _ conventionally names an ignored slot. This parallel assignment also powers the multiple-return-value idiom shown later.Ranges
<?php
$numbers = range(1, 5);
print_r($numbers);
$letters = range("a", "e");
echo implode("", $letters) . "\n"; // abcde numbers = (1..5).to_a
p numbers
letters = ("a".."e").to_a
puts letters.join # abcde Ruby's
Range is a first-class lazy object written 1..5 (inclusive) or 1...5 (exclusive of the end), not just a function that materializes an array. You can iterate a range directly, use it in a case, or call to_a to expand it — far more flexible than PHP's eager range().Control Flow
Conditionals
<?php
$age = 20;
if ($age >= 65) {
echo "senior\n";
} elseif ($age >= 18) {
echo "adult\n";
} else {
echo "minor\n";
} age = 20
if age >= 65
puts "senior"
elsif age >= 18
puts "adult"
else
puts "minor"
end Ruby drops the parentheses around the condition and the braces around the body, closing the whole block with
end. The keyword is elsif (one word, no second e) rather than PHP's elseif. There is no colon-style alternative syntax; end is the one and only block terminator.Statement Modifiers
<?php
$balance = -5;
if ($balance < 0) {
echo "Overdrawn!\n";
}
// PHP has no trailing-if; you always write the block. balance = -5
puts "Overdrawn!" if balance < 0
# Read it as a sentence: do X if Y.
puts "OK" unless balance < 0 Ruby lets a condition trail the statement it guards:
puts "..." if balance < 0. This statement modifier form has no PHP equivalent and is idiomatic for short guards. Ruby also adds unless (the negation of if), so you rarely write if (!condition).match vs case
<?php
$status = 2;
$label = match ($status) {
1 => "active",
2, 3 => "pending",
default => "unknown",
};
echo $label . "\n"; status = 2
label = case status
when 1 then "active"
when 2, 3 then "pending"
else "unknown"
end
puts label Ruby's
case is the counterpart to PHP 8's match: it is an expression that returns a value, has no fall-through, and accepts several values per branch (when 2, 3). But case compares with the === operator, so a when can be a class, a range, or a regex — not just an equal value, as the next example shows.case With Ranges & Types
<?php
$score = 82;
// PHP match only tests equality, so ranges need explicit comparisons:
$grade = match (true) {
$score >= 90 => "A",
$score >= 80 => "B",
$score >= 70 => "C",
default => "F",
};
echo $grade . "\n"; score = 82
grade = case score
when 90.. then "A"
when 80.. then "B"
when 70.. then "C"
else "F"
end
puts grade Because Ruby's
when uses ===, a range like 80.. matches any score it covers — no match (true) trick needed. The same mechanism lets you switch on when String, when Integer, or when /regex/, making case dramatically more expressive than PHP's equality-only match.Loops
<?php
for ($i = 0; $i < 3; $i++) {
echo $i . "\n";
}
foreach (["a", "b", "c"] as $letter) {
echo $letter . "\n";
} 3.times do |index|
puts index
end
["a", "b", "c"].each do |letter|
puts letter
end Ruby almost never uses a C-style
for loop. You send an iteration message to an object: 3.times, (1..10).each, or array.each. The block receives each value. This object-sends-its-own-iteration model is why explicit index counters are rare in idiomatic Ruby.Truthiness
<?php
// In PHP, 0, "", "0", [], and null are all falsy.
$values = [0, "", "0", [], null, "hello"];
foreach ($values as $value) {
echo (($value) ? "truthy" : "falsy") . "\n";
} # In Ruby, ONLY nil and false are falsy. Everything else is truthy.
[0, "", "0", [], nil, "hello"].each do |value|
puts value ? "truthy" : "falsy"
end
# => truthy, truthy, truthy, truthy, falsy, truthy Memorize this one difference and you avoid a category of bugs: in Ruby only
nil and false are falsy. 0, "", "0", and [] are all truthy, unlike PHP. So if $count to test "is it nonzero" is a PHP habit that breaks in Ruby — write if count > 0 instead.Functions & Methods
Defining Functions
<?php
function greet(string $name): string {
return "Hello, {$name}";
}
echo greet("Alice") . "\n"; def greet(name)
"Hello, #{name}"
end
puts greet("Alice") Ruby uses
def ... end and has no type annotations on parameters or return. The biggest surprise: the last expression is returned automatically, so an explicit return is usually omitted. return exists for early exits, but a one-expression method needs none.Default & Named Arguments
<?php
function connect(string $host, int $port = 5432, bool $ssl = false): string {
return "{$host}:{$port} ssl=" . ($ssl ? "on" : "off");
}
echo connect("db.local") . "\n";
echo connect("db.local", ssl: true) . "\n"; def connect(host, port: 5432, ssl: false)
"#{host}:#{port} ssl=#{ssl ? 'on' : 'off'}"
end
puts connect("db.local")
puts connect("db.local", ssl: true) Ruby distinguishes positional parameters from keyword parameters with a trailing colon (
port:). PHP 8 lets you pass any argument by name; Ruby requires the method to opt in by declaring the parameter as a keyword. The payoff is the same readable call site, connect("db.local", ssl: true), with order-independence guaranteed.Variadic Arguments
<?php
function sum(int ...$numbers): int {
return array_sum($numbers);
}
echo sum(1, 2, 3, 4) . "\n"; // 10
$values = [5, 6, 7];
echo sum(...$values) . "\n"; // 18 def sum(*numbers)
numbers.sum
end
puts sum(1, 2, 3, 4) # 10
values = [5, 6, 7]
puts sum(*values) # 18 Ruby's splat operator is a single
* rather than PHP's ..., used both to collect arguments in a definition (*numbers) and to spread an array at a call site (sum(*values)). A double splat **options does the same for keyword arguments, mirroring PHP's ...$named spread of associative arrays.Multiple Return Values
<?php
function minMax(array $numbers): array {
return [min($numbers), max($numbers)];
}
[$low, $high] = minMax([3, 1, 4, 1, 5]);
echo "{$low} to {$high}\n"; def min_max(numbers)
[numbers.min, numbers.max]
end
low, high = min_max([3, 1, 4, 1, 5])
puts "#{low} to #{high}" Ruby has no special tuple type either, but returning an array and destructuring it on assignment (
low, high = ...) is so seamless it feels like true multiple return. Note the method name min_max uses snake_case — the universal Ruby convention for methods and variables, versus PHP's common camelCase.? and ! Method Names
<?php
// PHP uses naming conventions like isEmpty()/hasRole().
function isEven(int $n): bool {
return $n % 2 === 0;
}
echo (isEven(4) ? "yes" : "no") . "\n"; # Ruby allows ? and ! as the last character of a method name.
def even?(number)
number % 2 == 0
end
puts even?(4) ? "yes" : "no"
puts [3, 1, 2].sort!.inspect # ! = mutates in place Ruby permits
? and ! as the final character of a method name, a piece of syntax PHP lacks. By convention ? marks a predicate that returns a boolean (even?, empty?, nil?) and ! marks a "dangerous" variant that mutates the receiver or raises (sort!, save!). The names document intent.Closures & Blocks
Blocks
<?php
$numbers = [1, 2, 3];
// PHP passes a closure as an ordinary argument:
array_walk($numbers, function ($n) {
echo $n * 10 . "\n";
}); numbers = [1, 2, 3]
# Ruby passes a block — special syntax attached to the call:
numbers.each do |number|
puts number * 10
end Ruby's signature feature is the block: an anonymous function written with
do...end (or { }) and passed after the method's parentheses, not inside them. It is not an argument in the list — it is a dedicated slot every method can receive. This is why each, map, and times read so cleanly.Brace vs do/end
<?php
$numbers = [1, 2, 3, 4];
$result = array_map(fn($n) => $n * $n, $numbers);
print_r($result); numbers = [1, 2, 3, 4]
# One line: braces.
result = numbers.map { |number| number * number }
p result
# Multi-line: do/end.
numbers.each do |number|
puts number
end A block has two interchangeable forms. The convention:
{ } for a single-line block that returns a value (like map), and do...end for multi-line blocks run for their side effects (like each). PHP's arrow function fn($n) => ... is closest to the brace form, but blocks are not objects unless you ask for them.Variable Capture
<?php
$factor = 3;
// PHP requires an explicit "use" clause to capture by value:
$scale = function ($n) use ($factor) {
return $n * $factor;
};
echo $scale(10) . "\n"; // 30 factor = 3
# Ruby blocks close over surrounding variables automatically:
scale = ->(number) { number * factor }
puts scale.call(10) # 30
puts scale.(10) # 30 — shorthand Ruby closures capture surrounding variables automatically — there is no
use ($factor) clause to maintain. The ->( ) { } syntax (a "stabby lambda") creates a callable Proc object; invoke it with .call, the shorthand .( ), or [ ]. Closing over the live variable also means changes are visible, unlike PHP's capture-by-value default.yield
<?php
// To run caller-supplied code, PHP takes a callable parameter.
function repeat(int $times, callable $task): void {
for ($i = 1; $i <= $times; $i++) {
$task($i);
}
}
repeat(3, fn($i) => print("step {$i}\n")); # Ruby methods can yield to an implicit block — no parameter needed.
def repeat(times)
(1..times).each { |index| yield index }
end
repeat(3) do |index|
puts "step #{index}"
end Any Ruby method can accept a block implicitly and invoke it with the
yield keyword — you do not declare a callable parameter. This makes "take a block and call it" the lightest pattern in the language, and block_given? lets a method behave differently when no block is passed.Method References
<?php
$names = ["alice", "bob"];
// PHP 8.1 first-class callable syntax:
$upper = array_map(strtoupper(...), $names);
print_r($upper); names = ["alice", "bob"]
# Ruby: turn a method into a block with &:symbol
upper = names.map(&:upcase)
p upper
# Or grab a Method object explicitly:
rounder = [1.4, 2.6].map(&3.method(:+))
p rounder # [4.4, 5.6] Ruby's
&:upcase is the everyday counterpart to PHP 8.1's strtoupper(...): the & converts a symbol naming a method into a block that calls that method on each element. It is the most common way to write point-free transformations like map(&:to_i) or select(&:even?).Classes & OOP
Defining a Class
<?php
class Greeter {
public function __construct(private string $name) {}
public function greet(): string {
return "Hi, {$this->name}";
}
}
$greeter = new Greeter("Alice");
echo $greeter->greet() . "\n"; class Greeter
def initialize(name)
@name = name
end
def greet
"Hi, #{@name}"
end
end
greeter = Greeter.new("Alice")
puts greeter.greet The constructor is named
initialize, instance variables carry an @ sigil (@name), and you call methods with a dot (greeter.greet) rather than ->. You instantiate with Greeter.new — new is a method on the class object, not a keyword. There is no public keyword; methods are public by default.Properties & Accessors
<?php
class Point {
public function __construct(
public int $x,
public int $y,
) {}
}
$point = new Point(3, 4);
echo $point->x . "\n";
$point->y = 10;
echo $point->y . "\n"; class Point
attr_accessor :x, :y
def initialize(x, y)
@x = x
@y = y
end
end
point = Point.new(3, 4)
puts point.x
point.y = 10
puts point.y Ruby instance variables are always private — there is no public field. To expose one you generate accessor methods with
attr_accessor (read + write), attr_reader (read only), or attr_writer. So point.x = 10 is never a direct field poke; it always calls an x= method, which means you can later swap in validation without changing callers.Inheritance
<?php
class Animal {
public function __construct(protected string $name) {}
public function speak(): string { return "..."; }
}
class Dog extends Animal {
public function speak(): string {
return "{$this->name} says Woof";
}
}
echo (new Dog("Rex"))->speak() . "\n"; class Animal
def initialize(name)
@name = name
end
def speak = "..."
end
class Dog < Animal
def speak
"#{@name} says Woof"
end
end
puts Dog.new("Rex").speak Ruby spells
extends as < (class Dog < Animal) and uses super to call the parent — bare super even forwards the same arguments automatically. The example also shows Ruby 4.0's one-line method syntax def speak = "...", a concise form for single-expression methods.Class Methods & Constants
<?php
class MathUtil {
const PI = 3.14159;
public static function square(int $n): int {
return $n * $n;
}
}
echo MathUtil::square(5) . "\n";
echo MathUtil::PI . "\n"; class MathUtil
PI = 3.14159
def self.square(number)
number * number
end
end
puts MathUtil.square(5)
puts MathUtil::PI A class method is defined with
def self.square and called with a dot, just like an instance method — Ruby does not use :: to call methods (that is for constants and nested classes). Inside the class, self refers to the class object itself, since classes are ordinary objects in Ruby too.String Representation
<?php
class Money {
public function __construct(private int $cents) {}
public function __toString(): string {
return "$" . number_format($this->cents / 100, 2);
}
}
echo new Money(1599) . "\n"; // $15.99 class Money
def initialize(cents)
@cents = cents
end
def to_s
format("$%.2f", @cents / 100.0)
end
end
puts Money.new(1599) # $15.99 Ruby's
to_s is the analogue of __toString and is called automatically by puts and interpolation. A companion method, inspect, provides the debugging representation that p prints — splitting the "human" and "developer" views into two hooks rather than PHP's single one.Value Objects: Struct
<?php
// PHP: a small data class, usually with readonly promotion.
final class Coordinate {
public function __construct(
public readonly float $latitude,
public readonly float $longitude,
) {}
}
$here = new Coordinate(51.5, -0.1);
echo "{$here->latitude}, {$here->longitude}\n"; # Ruby: Struct builds a whole value class in one line.
Coordinate = Struct.new(:latitude, :longitude) do
def to_s = "#{latitude}, #{longitude}"
end
here = Coordinate.new(51.5, -0.1)
puts here.latitude
puts here Struct.new generates a class with accessors, a constructor, ==, and more from a list of attribute names — perfect for the lightweight data holders you would write as a small readonly class in PHP. The optional block adds methods. For an immutable variant, Ruby 3.2+ offers Data.define.Traits & Modules
Traits → Mixins
<?php
trait Greetable {
public function greet(): string {
return "Hello from {$this->name()}";
}
}
class Robot {
use Greetable;
public function name(): string { return "R2"; }
}
echo (new Robot())->greet() . "\n"; module Greetable
def greet
"Hello from #{name}"
end
end
class Robot
include Greetable
def name = "R2"
end
puts Robot.new.greet Ruby's
module mixed in with include is the direct counterpart to a PHP trait used with use. Both inject shared methods into a class. The difference: a Ruby module is a real object that also forms a node in the method-lookup chain, so super works across mixins — they compose, rather than being flattened in at compile time.Namespaces
<?php
namespace Billing;
class Invoice {
public function total(): int { return 100; }
}
// Used elsewhere as \Billing\Invoice
$invoice = new Invoice();
echo $invoice->total() . "\n"; module Billing
class Invoice
def total = 100
end
end
invoice = Billing::Invoice.new
puts invoice.total A Ruby
module doubles as a namespace: nest classes inside it and reference them with :: (Billing::Invoice). Unlike PHP's namespace declaration, which applies to a whole file, Ruby modules are ordinary module ... end blocks you can reopen, nest, and assign — the same construct used for mixins.Comparable & Enumerable
<?php
// PHP: implement comparison by hand for sorting.
class Version {
public function __construct(public int $value) {}
}
$list = [new Version(3), new Version(1), new Version(2)];
usort($list, fn($a, $b) => $a->value <=> $b->value);
echo implode(",", array_map(fn($v) => $v->value, $list)) . "\n"; class Version
include Comparable
attr_reader :value
def initialize(value) = @value = value
def <=>(other) = value <=> other.value
end
list = [Version.new(3), Version.new(1), Version.new(2)]
puts list.sort.map(&:value).join(",") # 1,2,3 Define one method —
<=> (the spaceship, which PHP also has) — and include Comparable hands you <, <=, ==, between?, clamp, and sortability for free. Enumerable works the same way: define each and you inherit map, select, min, and dozens more. This "define one method, get many" pattern has no PHP analogue.Error Handling
Exceptions
<?php
function parsePort(string $text): int {
if (!ctype_digit($text)) {
throw new InvalidArgumentException("bad port: {$text}");
}
return (int) $text;
}
try {
echo parsePort("abc") . "\n";
} catch (InvalidArgumentException $error) {
echo "Error: " . $error->getMessage() . "\n";
} finally {
echo "done\n";
} def parse_port(text)
raise ArgumentError, "bad port: #{text}" unless text.match?(/\A\d+\z/)
text.to_i
end
begin
puts parse_port("abc")
rescue ArgumentError => error
puts "Error: #{error.message}"
ensure
puts "done"
end Ruby spells
throw as raise, catch as rescue, and finally as ensure. (Ruby does have throw/catch, but those are a non-error control-flow mechanism — do not reach for them here.) The error message comes from .message rather than ->getMessage(), and the rescued exception is bound with => error.Custom Exceptions
<?php
class PaymentError extends \RuntimeException {}
try {
throw new PaymentError("card declined");
} catch (PaymentError $error) {
echo "Caught: {$error->getMessage()}\n";
} class PaymentError < StandardError; end
begin
raise PaymentError, "card declined"
rescue PaymentError => error
puts "Caught: #{error.message}"
end Custom exceptions subclass
StandardError, not the very top Exception — a bare rescue catches StandardError and its descendants, deliberately leaving system-level signals like Interrupt alone. This mirrors PHP's distinction between Exception and Error, but the rule of thumb "inherit from StandardError" is firmer in Ruby.Inline Rescue & retry
<?php
// PHP has no expression-level catch; you wrap in a full try block.
$port = 5432;
try {
$port = (int) "not-a-number";
if ($port === 0) throw new Exception("zero");
} catch (Exception $error) {
$port = 5432;
}
echo $port . "\n"; # Ruby: rescue as a one-line expression modifier.
port = Integer("not-a-number") rescue 5432
puts port # 5432
# And rescue blocks can 'retry' from the top.
attempts = 0
begin
attempts += 1
raise "flaky" if attempts < 3
puts "ok after #{attempts}"
rescue
retry if attempts < 3
end Two Ruby tools have no PHP equivalent. The inline
expr rescue fallback modifier evaluates to the fallback when expr raises — a compact default-on-error idiom. And inside a begin block, retry jumps back to the top to attempt the operation again, ideal for transient failures without a manual loop.Enums & Symbols
Enums
<?php
enum Status: string {
case Active = "active";
case Pending = "pending";
case Closed = "closed";
}
$status = Status::Pending;
echo $status->value . "\n"; // pending
echo Status::from("active")->name . "\n"; // Active # Ruby has no enum keyword; a module of constants is the idiom.
module Status
ACTIVE = "active"
PENDING = "pending"
CLOSED = "closed"
ALL = [ACTIVE, PENDING, CLOSED].freeze
end
puts Status::PENDING
puts Status::ALL.include?("active") This is a place where PHP is genuinely ahead: PHP 8.1 has a real
enum type with cases, backing values, and ::from(), whereas Ruby has no native enum. The common substitutes are a module of frozen constants (shown here) or symbols. Libraries and Rails add richer enum behavior, but the language core leaves it to convention.Symbols as Lightweight Enums
<?php
// PHP would use enum cases or string constants here.
function describe(string $role): string {
return match ($role) {
"admin" => "full access",
"editor" => "can edit",
default => "read only",
};
}
echo describe("editor") . "\n"; def describe(role)
case role
when :admin then "full access"
when :editor then "can edit"
else "read only"
end
end
puts describe(:editor) In everyday Ruby, a symbol like
:admin fills the role of a lightweight enum value. Symbols are interned, compare by identity, and read cleanly in case branches and as hash keys — so where PHP reaches for an enum case or a string constant, idiomatic Ruby often just passes a symbol.Behavior on Enum Values
<?php
enum Suit: string {
case Hearts = "H";
case Spades = "S";
public function color(): string {
return match ($this) {
Suit::Hearts => "red",
Suit::Spades => "black",
};
}
}
echo Suit::Hearts->color() . "\n"; module Suit
COLORS = { hearts: "red", spades: "black" }.freeze
def self.color(suit)
COLORS.fetch(suit)
end
end
puts Suit.color(:hearts) PHP enums can carry methods directly on each case — a clean piece of object-orientation Ruby cannot match without a full class. To attach behavior to symbol-style values in Ruby, you typically pair them with a lookup hash or a module method, as here, or graduate to a proper class when the behavior grows.
Pattern Matching
Structural Matching
<?php
// PHP has no structural pattern matching; you inspect by hand.
$response = ["status" => "ok", "data" => [1, 2, 3]];
if (($response["status"] ?? null) === "ok") {
$data = $response["data"];
echo "got " . count($data) . " items\n";
} response = { status: "ok", data: [1, 2, 3] }
case response
in { status: "ok", data: [_, *rest] }
puts "got #{rest.length + 1} items"
in { status: "error", message: }
puts "error: #{message}"
end Ruby's
case/in (pattern matching, since 3.0) destructures and tests shape in one step — something PHP has no equivalent for. The pattern { status: "ok", data: [_, *rest] } matches a hash with that exact status whose data is an array, binding the tail to rest. A bare message: binds that key to a local variable of the same name.Type & Guard Patterns
<?php
function classify($value): string {
if (is_int($value) && $value > 0) return "positive int";
if (is_string($value)) return "string of " . strlen($value);
if (is_array($value)) return "array of " . count($value);
return "other";
}
echo classify("hello") . "\n"; def classify(value)
case value
in Integer => number if number > 0
"positive int"
in String => text
"string of #{text.length}"
in Array => items
"array of #{items.length}"
else
"other"
end
end
puts classify("hello") Patterns can match by class (
Integer => number binds and type-checks at once) and add a guard with a trailing if. This folds PHP's chain of is_int/is_string checks plus binding into a single readable structure, with the matched value already destructured into a named local.Standard Library
Nil Handling
<?php
$config = ["timeout" => 30];
$timeout = $config["timeout"] ?? 60;
$retries = $config["retries"] ?? 3;
echo "{$timeout}, {$retries}\n"; // 30, 3 config = { timeout: 30 }
timeout = config[:timeout] || 60
retries = config.fetch(:retries, 3)
puts "#{timeout}, #{retries}" # 30, 3 Ruby's
|| covers most of PHP's ??, but watch the truthiness difference: || falls through on any falsy value, and since nil/false are the only falsy ones, 0 survives (good) but false would not. For exact "is the key present" semantics like ??, use Hash#fetch with a default — it only supplies the default when the key is genuinely absent.Safe Navigation
<?php
class Account { public ?Profile $profile = null; }
class Profile { public string $city = "Paris"; }
$account = new Account();
$city = $account->profile?->city ?? "unknown";
echo $city . "\n"; // unknown Account = Struct.new(:profile)
Profile = Struct.new(:city)
account = Account.new(nil)
city = account.profile&.city || "unknown"
puts city # unknown Ruby's safe-navigation operator
&. is the twin of PHP's nullsafe ?->: it calls the method only when the receiver is non-nil, otherwise short-circuits to nil. Combine it with || for a fallback, exactly as you combine ?-> with ?? in PHP.JSON
<?php
$payload = ["name" => "Alice", "roles" => ["admin", "user"]];
$json = json_encode($payload);
echo $json . "\n";
$parsed = json_decode($json, true);
echo $parsed["name"] . "\n"; require "json"
payload = { name: "Alice", roles: ["admin", "user"] }
json = payload.to_json
puts json
parsed = JSON.parse(json)
puts parsed["name"] After
require "json", every object gains to_json and you parse with JSON.parse. By default JSON.parse returns string keys (like json_decode($json, true)); pass symbolize_names: true to get symbol keys instead. Ruby groups such utilities into requirable standard-library modules rather than PHP's always-loaded global functions.Sorting
<?php
$words = ["banana", "apple", "cherry"];
sort($words); // mutates in place
print_r($words);
$people = [["age" => 30], ["age" => 25]];
usort($people, fn($a, $b) => $a["age"] <=> $b["age"]);
echo $people[0]["age"] . "\n"; words = ["banana", "apple", "cherry"]
p words.sort # returns a new array
people = [{ age: 30 }, { age: 25 }]
youngest_first = people.sort_by { |person| person[:age] }
puts youngest_first.first[:age] Ruby's
sort returns a new array and leaves the original untouched (the mutating version is sort!) — the opposite of PHP's in-place sort. For sorting by a derived key, sort_by { |x| ... } is clearer and faster than writing a spaceship comparator, since it computes each key once.Method Chaining
<?php
$numbers = [5, 3, 8, 1, 9, 2];
// PHP: nested function calls, read inside-out.
$result = array_slice(
array_values(
array_filter($numbers, fn($n) => $n > 2)
),
0,
3
);
print_r($result); numbers = [5, 3, 8, 1, 9, 2]
# Ruby: a left-to-right pipeline of method calls.
result = numbers
.select { |number| number > 2 }
.sort
.first(3)
p result # [3, 5, 8] Because every collection operation returns a collection, Ruby code reads as a top-to-bottom pipeline —
select then sort then first(3) — instead of PHP's inside-out nesting of array_slice(array_values(array_filter(...))). This fluent style is one of the most visible day-to-day differences when reading Ruby.⚠ Gotchas for PHP Devs
Arrays Are References
<?php
// PHP arrays are COPIED on assignment (value semantics).
$original = [1, 2, 3];
$copy = $original;
$copy[] = 4;
print_r($original); // still [1, 2, 3]
print_r($copy); // [1, 2, 3, 4] # Ruby arrays are SHARED on assignment (reference semantics).
original = [1, 2, 3]
copy = original
copy << 4
p original # [1, 2, 3, 4] ← changed too!
p duplicate = original.dup # use .dup for a real copy A genuine trap. PHP copies arrays on assignment, so
$copy is independent. Ruby variables hold references: copy = original makes both names point at the same array, so mutating one mutates both. When you need an independent copy, call .dup (shallow) or Marshal/deep_dup for nested structures.Equality
<?php
// PHP: == is loose (juggling), === is strict.
var_dump(0 == "a"); // false in PHP 8 (was true in PHP 7!)
var_dump("1" == 1); // true — loose
var_dump("1" === 1); // false — strict
var_dump(null == false); // true # Ruby: == compares VALUE with no type juggling at all.
p "1" == 1 # false — different types, never coerced
p 1 == 1.0 # true — numeric tower compares across Integer/Float
p nil == false # false — distinct objects
p 1.equal?(1) # true — equal? is identity (object sameness) Ruby has no loose
==: it compares values without coercion, so the PHP juggling surprises ("1" == 1, null == false) simply do not happen. Ruby's three levels are == (value), eql? (value and type, used by hashes), and equal? (object identity). There is no operator that behaves like PHP's loose ==.Integer Division
<?php
echo 7 / 2 . "\n"; // 3.5 — PHP promotes to float
echo intdiv(7, 2) . "\n"; // 3 — explicit integer division
echo 7 % 3 . "\n"; // 1 puts 7 / 2 # 3 — Integer / Integer stays Integer (floors)
puts 7.0 / 2 # 3.5 — make one a Float to get a Float
puts 7 % 3 # 1
puts(-7 / 2) # -4 — floors toward negative infinity Beware:
7 / 2 is 3 in Ruby, not 3.5 — dividing two integers yields an integer, the reverse of PHP's automatic float promotion. Make an operand a float (7.0 / 2) to get a real quotient. Ruby also floors toward negative infinity, so -7 / 2 is -4, where PHP's intdiv truncates toward zero to give -3.Optional Parentheses
<?php
// PHP: calling a method always needs parentheses.
$text = " hello ";
echo strlen(trim($text)) . "\n"; // 5
// trim($text) without () would be an error. text = " hello "
# Ruby: parentheses on calls are optional.
puts text.strip.length # 5
puts "hi".upcase # HI — no ()
# This means a "property" access is really a method call:
puts [1, 2, 3].length # length is a method, not a field Ruby makes parentheses optional on method calls, so
array.length looks like a field access but is a real method call (there are no public fields, remember). This uniformity is powerful but can surprise: puts foo calls foo if it is a method. When passing arguments, parentheses are still wise for clarity even though the language permits omitting them.Frozen String Literals
<?php
// PHP strings are mutable values; building them up is routine.
$message = "Hello";
$message .= ", World";
$message[0] = "J";
echo $message . "\n"; // Jello, World # Ruby 4.0 freezes string LITERALS by default.
message = "Hello"
message += ", World" # OK: reassigns to a new String
puts message
buffer = String.new("Hello") # explicitly mutable
buffer << ", World" # OK: << mutates a non-frozen String
puts buffer In Ruby 4.0 string literals are frozen (immutable), so the in-place tricks PHP allows —
$message[0] = "J" or .= appending to a literal — raise a FrozenError. Reassignment with += is fine because it builds a new string. When you truly need a mutable buffer, create one with String.new and append with <<.Significant Newlines
<?php
// PHP ignores newlines; semicolons end statements.
$total = 1 +
2 +
3;
echo $total . "\n"; // 6 # Ruby ends a statement at a newline — no semicolons.
total = 1 +
2 +
3 # OK: trailing + signals "continued"
puts total # 6
# But breaking BEFORE the operator would end the line early:
sum = 1
.succ # leading-dot continuation also works
puts sum # 2 Ruby treats a newline as a statement terminator, so you cannot break a line wherever you like. A line that ends with a binary operator (or an open bracket, or a comma) is understood as continuing; a "complete-looking" line is not. The safe place to break a method chain is before the dot, putting
.method at the start of the next line.