PONY λ M2 Modula-2

Ruby.CodeCompared.To/PHP

An interactive executable cheatsheet for Rubyists learning PHP

Ruby 4.0 PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM)
Variables & Types
Hello, World
puts "Hello, World!"
<?php echo "Hello, World!\n";
PHP uses echo (or print) for output. Unlike Ruby's puts, echo does not append a newline — you must include \n explicitly. The opening tag is required for every PHP script; without it, PHP outputs the file content literally as HTML.
Variable Declaration
name = "Alice" age = 30 active = true puts "#{name} is #{age}"
<?php $name = "Alice"; $age = 30; $active = true; echo "$name is $age\n";
Every PHP variable starts with a dollar sign ($). There is no declaration keyword — assignment creates the variable. Statements end with a semicolon. PHP is dynamically typed like Ruby, but the $ prefix is mandatory and applies to every variable, every time it appears.
Basic Types
text = "hello" count = 42 ratio = 3.14 active = true nothing = nil p text.class # String p count.class # Integer p ratio.class # Float p active.class # TrueClass p nothing.nil? # true
<?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($active) . "\n"; // boolean echo gettype($nothing) . "\n"; // NULL echo is_null($nothing) ? "null\n" : "not null\n";
PHP's scalar types are string, integer, double (always used for floats), boolean, and NULL. The gettype() function returns the type name as a string. PHP also provides is_string(), is_int(), is_float(), is_bool(), and is_null() for type-checking predicates.
Type Casting
puts Integer("42") # 42 puts "42".to_i # 42 puts Float("3.14") # 3.14 puts 42.to_s # "42" puts !!0 # false (0 is truthy in Ruby!) puts !!nil # false
<?php echo (int) "42" . "\n"; // 42 echo (int) "3.99" . "\n"; // 3 (truncates, not rounds) echo (float) "3.14" . "\n"; // 3.14 echo (string) 42 . "\n"; // 42 echo (bool) 0 ? "true\n" : "false\n"; // false (0 is falsy in PHP) echo (bool) 1 ? "true\n" : "false\n"; // true echo (bool) "" ? "true\n" : "false\n"; // false
PHP uses (type) cast operators. Note that 0 is falsy in PHP — unlike Ruby, where only nil and false are falsy and 0 is truthy. Other falsy values in PHP: "", "0", [], 0.0, and null. This difference in truthiness rules is one of the most common sources of bugs for Ruby developers new to PHP.
Constants
PI = 3.14159 VERSION = "1.0" puts PI puts VERSION
<?php define('PI', 3.14159); const VERSION = "1.0"; // class-scoped or top-level echo PI . "\n"; echo VERSION . "\n";
PHP has two ways to define constants: define() (a function, evaluated at runtime) and the const keyword (evaluated at compile time, preferred inside classes). Constants are not prefixed with $ — they are the only PHP identifiers without the dollar sign. By convention PHP constants use ALL_CAPS. Ruby uses ALL_CAPS for constants and the runtime warns when they are reassigned.
Null Coalescing ??
config = nil timeout = config || 30 puts timeout # 30 settings = { retries: 3 } retries = settings[:retries] || 5 puts retries # 3
<?php $config = null; $timeout = $config ?? 30; echo $timeout . "\n"; // 30 $settings = ['retries' => 3]; $retries = $settings['retries'] ?? 5; echo $retries . "\n"; // 3 // Chaining: try each in sequence until non-null $result = null ?? null ?? "found"; echo $result . "\n"; // found
The ?? operator returns its left operand if it is not null, otherwise the right operand. It is equivalent to Ruby's || for nil checks, but more precise: ?? only checks for null, while Ruby's || also short-circuits on false. There is also ??= for null-coalescing assignment: $value ??= "default".
Nullsafe Operator ?->
class Address attr_reader :city def initialize(city) = @city = city end class User attr_reader :address def initialize(address = nil) = @address = address end user = User.new(Address.new("New York")) guest = User.new puts user&.address&.city # New York puts (guest&.address&.city || "(no city)") # (no city)
<?php class Address { public function __construct( public readonly string $city = '', ) {} } class User { public function __construct( public readonly ?Address $address = null, ) {} } $user = new User(new Address("New York")); $guest = new User(); echo $user?->address?->city . "\n"; // New York echo $guest?->address?->city ?? "(no city)"; // (no city)
The nullsafe operator ?-> (PHP 8.0) short-circuits the entire chain if any step returns null, avoiding a null-pointer error. It is equivalent to Ruby's safe navigation operator &.. Combine it with ?? at the end to provide a default when the chain produces null.
Array Destructuring
first, second, third = [10, 20, 30] puts first # 10 puts second # 20 # Swap variables alpha = "a" beta = "b" alpha, beta = beta, alpha puts alpha # b coordinates = [3, 7] x, y = coordinates puts "#{x}, #{y}"
<?php [$first, $second, $third] = [10, 20, 30]; echo $first . "\n"; // 10 echo $second . "\n"; // 20 // Swap variables $alpha = "a"; $beta = "b"; [$alpha, $beta] = [$beta, $alpha]; echo $alpha . "\n"; // b // Skip elements with empty slots [, $middle, ] = [1, 2, 3]; echo $middle . "\n"; // 2
PHP 7.1+ supports short [$a, $b] = $array list destructuring, equivalent to the older list($a, $b) = $array syntax. Skip elements by leaving empty commas. Associative array destructuring uses string keys: ['name' => $name, 'age' => $age] = $person. Ruby's parallel assignment works the same way.
Strings
Single vs Double Quotes
name = "Alice" puts "Hello, #{name}!" # Hello, Alice! (interpolates) puts 'Hello, #{name}!' # Hello, #{name}! (no interpolation)
<?php $name = "Alice"; echo "Hello, {$name}!\n"; // Hello, Alice! (interpolates) echo 'Hello, {$name}!' . "\n"; // Hello, {$name}! (no interpolation) $count = 42; echo "Count is: $count\n"; // Count is: 42 echo 'Count is: $count' . "\n"; // Count is: $count
PHP double-quoted strings interpolate $variable and {$variable}; single-quoted strings are always literal. This is the opposite of Ruby, where all string literals interpolate with #{}. Use {$variable} (brace before dollar sign) for complex expressions like array access inside strings. The deprecated ${variable} syntax (dollar before brace) was removed in PHP 8.2.
String Concatenation
first = "Hello" last = "World" greeting = first + ", " + last + "!" puts greeting # String multiplication puts "ha" * 3 # hahaha puts "-" * 20
<?php $first = "Hello"; $last = "World"; $greeting = $first . ", " . $last . "!"; echo $greeting . "\n"; // Concatenation assignment $result = "Go"; $result .= " home"; echo $result . "\n"; // Go home // str_repeat for repetition echo str_repeat("ha", 3) . "\n"; // hahaha echo str_repeat("-", 20) . "\n";
PHP uses the dot . operator for string concatenation, not + (which performs numeric addition). The .= operator appends in place. There is no string repetition operator in PHP — use str_repeat($string, $times), which is equivalent to Ruby's * operator on strings.
String Functions
text = " Hello, World! " puts text.strip # "Hello, World!" puts text.length # 20 (with spaces) puts text.upcase # " HELLO, WORLD! " puts text.downcase # " hello, world! " puts text.reverse # " !dlroW ,olleH " puts text.include?("World") # true
<?php $text = " Hello, World! "; echo trim($text) . "\n"; // "Hello, World!" echo strlen($text) . "\n"; // 20 (with spaces) echo strtoupper($text) . "\n"; // " HELLO, WORLD! " echo strtolower($text) . "\n"; // " hello, world! " echo strrev($text) . "\n"; // " !dlroW ,olleH " echo str_contains($text, "World") ? "yes\n" : "no\n"; // yes
PHP string functions are global functions, not methods. The function name is often the verb, with the string as the first argument — the reverse of Ruby's method syntax ($string.method() vs function($string)). strlen() counts bytes, not Unicode characters — use mb_strlen() for multibyte-safe character counting.
String Search (PHP 8.0)
url = "https://example.com/page" puts url.include?("example") # true puts url.start_with?("https") # true puts url.end_with?(".com/page") # true puts url.index("example") # 8 (position)
<?php $url = "https://example.com/page"; echo str_contains($url, "example") ? "yes\n" : "no\n"; // yes echo str_starts_with($url, "https") ? "yes\n" : "no\n"; // yes echo str_ends_with($url, ".com/page") ? "yes\n" : "no\n"; // yes echo strpos($url, "example") . "\n"; // 8 (position)
str_contains(), str_starts_with(), and str_ends_with() were added in PHP 8.0. Before PHP 8.0, the idiom for checking substring presence was strpos($haystack, $needle) !== false — both the !== false and strpos are easy to get wrong. These new functions match Ruby's include?, start_with?, and end_with? exactly.
Split & Join
sentence = "one,two,three" parts = sentence.split(",") puts parts.inspect # ["one", "two", "three"] puts parts.join(" | ") # one | two | three
<?php $sentence = "one,two,three"; $parts = explode(",", $sentence); print_r($parts); echo implode(" | ", $parts) . "\n"; // one | two | three // str_split for character arrays $chars = str_split("hello"); echo implode("-", $chars) . "\n"; // h-e-l-l-o
PHP uses explode($delimiter, $string) to split (note the argument order is opposite to Ruby's split) and implode($glue, $array) to join. The alias join() also works for implode(). str_split($string) splits into individual characters, like Ruby's String#chars.
Heredoc & Nowdoc
name = "Alice" message = <<~TEXT Hello, #{name}! Welcome aboard. TEXT puts message
<?php $name = "Alice"; // Heredoc (interpolates like double-quoted string) $message = <<<EOT Hello, {$name}! Welcome aboard. EOT; echo $message . "\n"; // Nowdoc (no interpolation, like single-quoted string) $raw = <<<'EOT' Hello, {$name}! EOT; echo $raw . "\n";
PHP's heredoc (<<) interpolates variables like a double-quoted string. The nowdoc variant (<<<'EOT' ... EOT) treats content as a literal like a single-quoted string — the equivalent of Ruby's <<~'TEXT'. PHP 7.3+ allows the closing marker to be indented, stripping leading whitespace exactly like Ruby's squiggly heredoc (<<~).
Formatted Strings (sprintf)
name = "Alice" score = 98.5 rank = 1 puts format("%-10s %6.1f #%d", name, score, rank) puts "Score: %.2f" % score
<?php $name = "Alice"; $score = 98.5; $rank = 1; echo sprintf("%-10s %6.1f #%d\n", $name, $score, $rank); echo sprintf("Score: %.2f\n", $score); // printf prints directly (like Ruby's printf) printf("Player: %s (rank %d)\n", $name, $rank);
PHP's sprintf() returns a formatted string; printf() prints directly. The format specifiers are nearly identical to Ruby's Kernel#sprintf and format(): %s for strings, %d for integers, %f for floats, %-10s for left-aligned in a 10-character field.
Arrays & Maps
Indexed Arrays
numbers = [1, 2, 3, 4, 5] puts numbers.length # 5 puts numbers[0] # 1 puts numbers[-1] # 5 (last element) puts numbers.first # 1 puts numbers.last # 5
<?php $numbers = [1, 2, 3, 4, 5]; echo count($numbers) . "\n"; // 5 echo $numbers[0] . "\n"; // 1 echo $numbers[count($numbers) - 1] . "\n"; // 5 (last element) echo reset($numbers) . "\n"; // 1 (first) echo end($numbers) . "\n"; // 5 (last, but moves internal pointer)
PHP arrays are zero-indexed like Ruby. count() returns the number of elements (Ruby uses .length or .size). PHP has no [-1] negative indexing — use array_key_last() or end() to access the last element. PHP 8.1 added array_is_list() to check whether an array is a pure indexed list.
Associative Arrays (Hashes)
person = { name: "Alice", age: 30, city: "NYC" } puts person[:name] # Alice puts person[:age] # 30 person[:email] = "alice@example.com" puts person.keys.inspect puts person.values.inspect
<?php $person = ['name' => 'Alice', 'age' => 30, 'city' => 'NYC']; echo $person['name'] . "\n"; // Alice echo $person['age'] . "\n"; // 30 $person['email'] = 'alice@example.com'; echo implode(', ', array_keys($person)) . "\n"; echo implode(', ', array_values($person)) . "\n";
PHP has a single array type for both indexed arrays and associative arrays (key-value maps). Ruby uses separate Array and Hash classes. PHP's array_keys() and array_values() correspond to Ruby's Hash#keys and Hash#values. Array keys can be either integers or strings; other types are cast.
Push, Pop, Shift, Unshift
queue = [1, 2, 3] queue.push(4) puts queue.last # 4 popped = queue.pop puts popped # 4 queue.unshift(0) puts queue.first # 0 shifted = queue.shift puts shifted # 0
<?php $queue = [1, 2, 3]; $queue[] = 4; // push (or array_push($queue, 4)) echo end($queue) . "\n"; // 4 $popped = array_pop($queue); echo $popped . "\n"; // 4 array_unshift($queue, 0); echo reset($queue) . "\n"; // 0 $shifted = array_shift($queue); echo $shifted . "\n"; // 0
PHP uses $array[] = $value or array_push() to append, array_pop() to remove from the end, array_unshift() to prepend, and array_shift() to remove from the front. These correspond exactly to Ruby's push, pop, unshift, and shift methods.
array_map
numbers = [1, 2, 3, 4, 5] doubled = numbers.map { |number| number * 2 } puts doubled.inspect # [2, 4, 6, 8, 10] words = ["hello", "world"] upcased = words.map(&:upcase) puts upcased.inspect # ["HELLO", "WORLD"]
<?php $numbers = [1, 2, 3, 4, 5]; $doubled = array_map(fn($number) => $number * 2, $numbers); echo implode(', ', $doubled) . "\n"; // 2, 4, 6, 8, 10 $words = ["hello", "world"]; $upcased = array_map('strtoupper', $words); echo implode(', ', $upcased) . "\n"; // HELLO, WORLD
PHP's array_map($callback, $array) takes the callback first and the array second — the reverse of Ruby's array.map { block }. Pass a function name as a string to use a built-in as the callback. array_map() preserves array keys; use array_values() to reindex if needed.
array_filter
numbers = [1, 2, 3, 4, 5, 6] evens = numbers.select { |number| number.even? } puts evens.inspect # [2, 4, 6] words = ["", "hello", nil, "world", false] truthy = words.select { |word| word } puts truthy.inspect # ["hello", "world"]
<?php $numbers = [1, 2, 3, 4, 5, 6]; $evens = array_filter($numbers, fn($number) => $number % 2 === 0); echo implode(', ', $evens) . "\n"; // 2, 4, 6 // Without callback: removes falsy values (0, "", null, false) $mixed = ["", "hello", null, "world", false, 0]; $truthy = array_filter($mixed); echo implode(', ', $truthy) . "\n"; // hello, world
PHP's array_filter() corresponds to Ruby's select or filter. Without a callback argument, it removes all falsy values (false, null, 0, "", "0", []). Note that 0 is falsy in PHP but truthy in Ruby. array_filter() preserves keys — wrap in array_values() to reindex.
array_reduce
numbers = [1, 2, 3, 4, 5] total = numbers.reduce(0) { |sum, number| sum + number } puts total # 15 product = numbers.reduce(1, :*) puts product # 120
<?php $numbers = [1, 2, 3, 4, 5]; $total = array_reduce($numbers, fn($sum, $number) => $sum + $number, 0); echo $total . "\n"; // 15 $product = array_reduce($numbers, fn($product, $number) => $product * $number, 1); echo $product . "\n"; // 120
PHP's array_reduce($array, $callback, $initial) corresponds to Ruby's reduce or inject. The argument order differs: PHP takes the array first, then callback, then initial value. The callback receives the accumulator as the first argument and the current element second — the same order as Ruby's block parameters.
Sorting
numbers = [3, 1, 4, 1, 5, 9, 2] puts numbers.sort.inspect # [1, 1, 2, 3, 4, 5, 9] puts numbers.sort.reverse.inspect # [9, 5, 4, 3, 2, 1, 1] puts numbers.sort_by { |n| -n }.inspect # [9, 5, 4, 3, 2, 1, 1] words = ["banana", "apple", "cherry"] puts words.sort.inspect
<?php $numbers = [3, 1, 4, 1, 5, 9, 2]; sort($numbers); // sorts in-place, reindexes echo implode(', ', $numbers) . "\n"; // 1, 1, 2, 3, 4, 5, 9 rsort($numbers); // reverse sort in-place echo implode(', ', $numbers) . "\n"; // 9, 5, 4, 3, 2, 1, 1 $words = ["banana", "apple", "cherry"]; sort($words); echo implode(', ', $words) . "\n"; // apple, banana, cherry usort($words, fn($a, $b) => strlen($a) <=> strlen($b)); echo implode(', ', $words) . "\n"; // apple, banana, cherry
PHP's sort() sorts arrays in place — unlike Ruby's sort, which returns a new array. Use rsort() for reverse, asort()/arsort() to sort while preserving keys, ksort()/krsort() to sort by key. usort() accepts a custom comparator like Ruby's sort_by, with the spaceship operator <=> as the standard return convention.
Spread & Merge
first = [1, 2, 3] second = [4, 5, 6] merged = [*first, *second] puts merged.inspect # [1, 2, 3, 4, 5, 6] combined = [0, *first, 10] puts combined.inspect # [0, 1, 2, 3, 10]
<?php $first = [1, 2, 3]; $second = [4, 5, 6]; // Spread operator in array literals (PHP 8.1+: string keys too) $merged = [...$first, ...$second]; echo implode(', ', $merged) . "\n"; // 1, 2, 3, 4, 5, 6 $combined = [0, ...$first, 10]; echo implode(', ', $combined) . "\n"; // 0, 1, 2, 3, 10 // array_merge also works (and handles associative arrays) $merged2 = array_merge($first, $second); echo implode(', ', $merged2) . "\n"; // 1, 2, 3, 4, 5, 6
PHP 7.4+ supports the spread operator ...$array inside array literals, identical to Ruby's splat *array. PHP 8.1 extended it to work with string-keyed arrays. array_merge() also concatenates arrays and is the traditional alternative. With associative arrays, array_merge() gives later keys priority over earlier ones when keys collide.
array_find / array_any / array_all (PHP 8.4)
products = [ { name: 'Apple', price: 1.20, organic: true }, { name: 'Banana', price: 0.50, organic: false }, { name: 'Cherry', price: 3.99, organic: true }, ] # Enumerable#find: first match affordable = products.find { |product| product[:price] < 1.00 } puts affordable[:name] # Banana # find_index: the matching key expensive_index = products.find_index { |product| product[:price] > 3.00 } puts expensive_index # 2 # any?: true if at least one matches puts products.any? { |product| product[:organic] } # true # all?: true if every element matches puts products.all? { |product| product[:organic] } # false
<?php $products = [ ['name' => 'Apple', 'price' => 1.20, 'organic' => true ], ['name' => 'Banana', 'price' => 0.50, 'organic' => false], ['name' => 'Cherry', 'price' => 3.99, 'organic' => true ], ]; // array_find: first match (like Ruby's Enumerable#find) $affordable = array_find($products, fn($product) => $product['price'] < 1.00); echo $affordable['name'] . "\n"; // Banana // array_find_key: returns the key, not the value (like Ruby's find_index) $expensiveIndex = array_find_key($products, fn($product) => $product['price'] > 3.00); echo $expensiveIndex . "\n"; // 2 // array_any: true if at least one matches (like Ruby's any?) $hasOrganic = array_any($products, fn($product) => $product['organic']); echo ($hasOrganic ? 'true' : 'false') . "\n"; // true // array_all: true if every element matches (like Ruby's all?) $allOrganic = array_all($products, fn($product) => $product['organic']); echo ($allOrganic ? 'true' : 'false') . "\n"; // false
PHP 8.4 adds four array-search functions that directly mirror Ruby's Enumerable: array_find() → Ruby's find; array_find_key() → Ruby's find_index; array_any() → Ruby's any?; array_all() → Ruby's all?. Before PHP 8.4, achieving these patterns required array_filter() combined with reset() or verbose custom loops. Note: this example requires PHP 8.4 or later — switch to PHP 8.5.5 (WASM) using the ▼ menu to run it.
array_first / array_last (PHP 8.5)
scores = [88, 92, 74, 96, 81] puts scores.first # 88 puts scores.last # 81 # first/last on an empty array returns nil (no exception) puts [].first.inspect # nil puts [].last.inspect # nil # Hashes: first/last return [key, value] pairs config = { timeout: 30, retries: 3, max_size: 1024 } puts config.first.inspect # [:timeout, 30] puts config.to_a.last.inspect # [:max_size, 1024]
<?php $scores = [88, 92, 74, 96, 81]; echo array_first($scores) . "\n"; // 88 echo array_last($scores) . "\n"; // 81 // Returns null for empty arrays (no exception) var_dump(array_first([])); // NULL var_dump(array_last([])); // NULL // Associative arrays: returns values (not key-value pairs like Ruby) $config = ['timeout' => 30, 'retries' => 3, 'maxSize' => 1024]; echo array_first($config) . "\n"; // 30 (the value, not the key) echo array_last($config) . "\n"; // 1024 // Before PHP 8.5, you needed array_key_first/last: $first = $scores !== [] ? $scores[array_key_first($scores)] : null; echo $first . "\n"; // 88
PHP 8.5 adds array_first() and array_last() to retrieve the first or last value of an array, returning null for empty arrays. Ruby's Array#first and Array#last work the same way. One important difference: on associative arrays (hashes), PHP's functions return the value for the first/last key, while Ruby's Hash#first and Hash#last return a [key, value] pair. Note: this example requires PHP 8.5 — switch to PHP 8.5.5 (WASM) using the ▼ menu to run it.
Control Flow
if / elseif / else
score = 85 if score >= 90 puts "A" elsif score >= 80 puts "B" elsif score >= 70 puts "C" else puts "F" end
<?php $score = 85; if ($score >= 90) { echo "A\n"; } elseif ($score >= 80) { echo "B\n"; } elseif ($score >= 70) { echo "C\n"; } else { echo "F\n"; }
PHP uses elseif (one word, like Ruby's elsif) and requires parentheses around conditions and curly braces around the body. Ruby's keyword-terminated blocks (if/elsif/else/end) become brace-delimited blocks in PHP. PHP also supports else if (two words) as an alias.
match Expression (PHP 8.0)
status = 2 result = case status in 1 then "active" in 2 then "inactive" in 3 then "pending" else "unknown" end puts result # inactive
<?php $status = 2; $result = match($status) { 1 => "active", 2 => "inactive", 3 => "pending", default => "unknown", }; echo $result . "\n"; // inactive // Multiple conditions → one arm $code = 404; $category = match(true) { $code >= 500 => "server error", $code >= 400 => "client error", $code >= 300 => "redirect", default => "success", }; echo $category . "\n"; // client error
PHP 8.0's match is an expression (returns a value), uses strict comparison (===), and throws an UnhandledMatchError if no arm matches and there is no default. It is like Ruby's case/in pattern matching in that it is an expression, but without the structural deconstruction. The older switch statement used loose == and fell through by default.
for / while / do-while
# C-style for is uncommon in Ruby; use times/upto/each 3.times { |index| puts index } count = 0 while count < 3 puts count count += 1 end
<?php // C-style for loop for ($index = 0; $index < 3; $index++) { echo $index . "\n"; } // while $count = 0; while ($count < 3) { echo $count . "\n"; $count++; } // do-while (always executes at least once) $count = 5; do { echo "ran: $count\n"; $count++; } while ($count < 3);
PHP has C-style for, while, and do-while loops. Ruby rarely uses C-style for — it is considered un-idiomatic, and each, times, and upto are preferred. PHP's do-while has no direct Ruby equivalent; it guarantees at least one execution by checking the condition at the end.
foreach
fruits = ["apple", "banana", "cherry"] fruits.each { |fruit| puts fruit } scores = { alice: 95, bob: 87, carol: 92 } scores.each { |name, score| puts "#{name}: #{score}" }
<?php $fruits = ["apple", "banana", "cherry"]; foreach ($fruits as $fruit) { echo $fruit . "\n"; } $scores = ['alice' => 95, 'bob' => 87, 'carol' => 92]; foreach ($scores as $name => $score) { echo "$name: $score\n"; }
PHP's foreach is the idiomatic loop over any array. The $value form iterates values only; the $key => $value form also exposes each key. This maps directly to Ruby's each { |value| } and each { |key, value| }. PHP's foreach operates on a copy of the array by default; use foreach ($array as &$value) for in-place modification.
break / continue
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] numbers.each do |number| next if number % 2 == 0 # skip evens break if number > 7 # stop after 7 puts number end
<?php $numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; foreach ($numbers as $number) { if ($number % 2 === 0) continue; // skip evens (Ruby: next) if ($number > 7) break; // stop at 7 (Ruby: break) echo $number . "\n"; }
PHP's break corresponds to Ruby's break; PHP's continue corresponds to Ruby's next. Both break and continue accept an integer argument in PHP to break/continue an outer loop: break 2 exits the two innermost loops. Ruby achieves nested-loop control with throw/catch or by restructuring the code.
Functions
Function Definition
def greet(name) "Hello, #{name}!" end puts greet("Alice") # Hello, Alice!
<?php function greet(string $name): string { return "Hello, {$name}!"; } echo greet("Alice") . "\n"; // Hello, Alice!
PHP functions use the function keyword and support optional type hints on parameters and return values. Type hints are enforced at runtime in PHP 8 strict mode. Unlike Ruby methods, PHP functions are not automatically associated with classes — they live in the global namespace or must be called with a class prefix.
Default Parameters
def create_user(name, role: "viewer", active: true) "#{name} (#{role}, active=#{active})" end puts create_user("Alice") puts create_user("Bob", role: "admin")
<?php function createUser( string $name, string $role = "viewer", bool $active = true, ): string { return "$name ($role, active=" . ($active ? 'true' : 'false') . ")"; } echo createUser("Alice") . "\n"; echo createUser("Bob", role: "admin") . "\n";
PHP supports default parameter values like Ruby's keyword arguments, but default arguments must appear after required ones. PHP 8.0 added named arguments so callers can pass any parameter by name, skipping others that have defaults — a feature Ruby has had as keyword arguments for much longer.
Named Arguments (PHP 8.0)
def create_button(label:, color: "blue", disabled: false) "#{label} (#{color}#{disabled ? ', disabled' : ''})" end puts create_button(label: "Save", disabled: true) puts create_button(color: "red", label: "Delete")
<?php function createButton( string $label, string $color = "blue", bool $disabled = false, ): string { $suffix = $disabled ? ', disabled' : ''; return "$label ($color$suffix)"; } echo createButton(label: "Save", disabled: true) . "\n"; echo createButton(color: "red", label: "Delete") . "\n";
PHP 8.0 named arguments let you pass any argument by name, in any order, skipping parameters with defaults. This matches Ruby's keyword arguments syntax (key: value). Named arguments also work with built-in PHP functions: array_slice(array: $items, offset: 1, length: 3). In PHP, positional and named arguments can be mixed in one call, but all named arguments must come after positional ones.
Variadic Functions
def sum(*numbers) numbers.sum end def tag(element, *classes, **attributes) attrs = attributes.map { |key, val| " #{key}='#{val}'" }.join "<#{element} class='#{classes.join(' ')}'#{attrs}>" end puts sum(1, 2, 3, 4) # 10 puts tag("div", "card", "active", id: "main")
<?php function sum(int ...$numbers): int { return array_sum($numbers); } echo sum(1, 2, 3, 4) . "\n"; // 10 function tag(string $element, string ...$classes): string { $classList = implode(' ', $classes); return "<{$element} class='{$classList}'>"; } echo tag("div", "card", "active") . "\n";
PHP variadic functions use the ...$param spread syntax; the variadic parameter collects remaining positional arguments into an array. PHP 8.0+ supports type hints on the variadic parameter itself (int ...$numbers). PHP has no built-in equivalent of Ruby's double-splat **kwargs for variadic keyword arguments.
Generators (yield)
def fibonacci Enumerator.new do |yielder| a, b = 0, 1 loop do yielder.yield a a, b = b, a + b end end end fibonacci.take(7).each { |number| print "#{number} " } puts
<?php function fibonacci(): Generator { [$a, $b] = [0, 1]; while (true) { yield $a; [$a, $b] = [$b, $a + $b]; } } $generator = fibonacci(); for ($index = 0; $index < 7; $index++) { echo $generator->current() . " "; $generator->next(); } echo "\n";
PHP generators use yield inside a function that returns a Generator object. They are lazy — values are computed only when requested. This is similar to Ruby's Enumerator or lazy enumerators. PHP generators also support yield $key => $value for key-value pairs, and yield from to delegate to another generator.
Closures
Anonymous Functions
double = ->(number) { number * 2 } add = ->(a, b) { a + b } puts double.call(5) # 10 puts add.call(3, 4) # 7 puts double.(6) # 12 (shorthand)
<?php $double = function(int $number): int { return $number * 2; }; $add = function(int $a, int $b): int { return $a + $b; }; echo $double(5) . "\n"; // 10 echo $add(3, 4) . "\n"; // 7 echo $double(6) . "\n"; // 12
PHP anonymous functions (closures) are created with the function keyword without a name. They are first-class values assigned to variables and called with $variable($args). Like Ruby lambdas (->), they support type hints. Unlike Ruby's closures and lambdas, PHP anonymous functions do not automatically capture outer-scope variables — this requires an explicit use clause.
Capturing Variables with use
multiplier = 3 # Ruby lambdas/procs capture outer variables automatically: triple = ->(number) { number * multiplier } puts triple.call(5) # 15 multiplier = 10 # Captures the binding — sees the updated value: puts triple.call(5) # 50
<?php $multiplier = 3; // Must explicitly list captured vars in 'use': $triple = function(int $number) use ($multiplier): int { return $number * $multiplier; }; echo $triple(5) . "\n"; // 15 $multiplier = 10; // Captured by VALUE at definition time — sees 3, not 10: echo $triple(5) . "\n"; // 15 (not 50!) // Capture by REFERENCE with &: $tripleRef = function(int $number) use (&$multiplier): int { return $number * $multiplier; }; echo $tripleRef(5) . "\n"; // 50 (sees updated $multiplier)
PHP anonymous functions require an explicit use ($variable) clause to access outer variables. By default, the variable is captured by value at definition time — so later changes to the outer variable are not visible inside the closure. Use use (&$variable) to capture by reference. Ruby closures (blocks, procs, lambdas) always capture by reference and see outer-scope updates automatically.
Arrow Functions (PHP 7.4)
multiplier = 3 # Proc/lambda that captures outer scope: triple = ->(number) { number * multiplier } puts triple.call(5) # 15 # Shorter stabby lambda syntax: double = ->(n) { n * 2 } puts [1, 2, 3].map(&double).inspect # [2, 4, 6]
<?php $multiplier = 3; // Arrow function: captures outer scope AUTOMATICALLY (no 'use' needed) $triple = fn(int $number): int => $number * $multiplier; echo $triple(5) . "\n"; // 15 $multiplier = 10; // Arrow functions capture by value at definition — still 3, not 10: echo $triple(5) . "\n"; // 15 // Arrow functions as callbacks — clean one-liners: $numbers = [1, 2, 3, 4, 5]; $doubled = array_map(fn($number) => $number * 2, $numbers); echo implode(', ', $doubled) . "\n"; // 2, 4, 6, 8, 10
PHP 7.4 arrow functions (fn($param) => $expression) automatically capture outer variables by value — no use clause needed. They are limited to a single expression (no curly brace body). Like Ruby's stabby lambda (->(n) { n * 2 }), they are ideal for short callbacks. For multi-statement logic, use a regular anonymous function with use.
First-Class Callables (PHP 8.1)
# Ruby methods as values via method() or Symbol#to_proc: numbers = ["3", "1", "4", "1", "5"] parsed = numbers.map(&method(:Integer)) puts parsed.sort.inspect # [1, 1, 3, 4, 5] words = ["hello", "WORLD"] puts words.map(&:downcase).inspect # ["hello", "world"]
<?php // PHP 8.1: use Closure::fromCallable or the ... syntax $numbers = ["3", "1", "4", "1", "5"]; $parseInt = Closure::fromCallable('intval'); $parsed = array_map($parseInt, $numbers); sort($parsed); echo implode(', ', $parsed) . "\n"; // 1, 1, 3, 4, 5 // PHP 8.1 short syntax: strlen(...) creates a closure $toLower = strtolower(...); $words = array_map($toLower, ["HELLO", "WORLD"]); echo implode(', ', $words) . "\n"; // hello, world
PHP 8.1 introduced the strlen(...) first-class callable syntax — appending (...) to a function name creates a Closure object from it. This is equivalent to Ruby's method(:strlen) and &:method_name Symbol-to-proc shorthand. Before PHP 8.1, Closure::fromCallable('function_name') was the standard way to convert a function name to a closure.
Pipe Operator |> (PHP 8.5)
# Ruby uses .then (alias: yield_self) to pipe values left-to-right: result = " Hello World " .then { |string| string.strip } .then { |string| string.gsub(' ', '-') } .then { |string| string.downcase } puts result # hello-world # Or with Ruby's native method chaining when methods exist directly: puts " Hello World ".strip.gsub(' ', '-').downcase # hello-world words = 'the quick brown fox' puts words.split.count # 4
<?php // Without pipe: nested calls read right-to-left (inside-out) $result1 = strtolower(str_replace(' ', '-', trim(' Hello World '))); echo $result1 . "\n"; // hello-world // With pipe operator: left-to-right, reads top-to-bottom like Ruby's .then // Arrow functions on the right of |> must be wrapped in parentheses: $result2 = ' Hello World ' |> trim(...) |> (fn($string) => str_replace(' ', '-', $string)) |> strtolower(...); echo $result2 . "\n"; // hello-world // The pipe passes the left value as the first argument. // Use fn() when argument order differs (explode needs the separator first): $wordCount = 'the quick brown fox' |> (fn($string) => explode(' ', $string)) |> count(...); echo $wordCount . "\n"; // 4
PHP 8.5's pipe operator |> passes the value on the left as the first argument to the callable on the right, enabling left-to-right function chains that read in execution order. Ruby achieves the same with .then (also known as yield_self) or natural method chaining. Two syntax rules to remember: first-class callables (trim(...)) work directly; arrow functions must be wrapped in parentheses — (fn($x) => ...), not bare fn($x) => .... Note: this example requires PHP 8.5 — switch to PHP 8.5.5 (WASM) using the ▼ menu to run it.
Classes & OOP
Basic Class
class Animal attr_reader :name, :sound def initialize(name, sound) @name = name @sound = sound end def speak "#{@name} says #{@sound}!" end end cat = Animal.new("Cat", "meow") puts cat.name # Cat puts cat.speak # Cat says meow!
<?php class Animal { public string $name; public string $sound; public function __construct(string $name, string $sound) { $this->name = $name; $this->sound = $sound; } public function speak(): string { return "{$this->name} says {$this->sound}!"; } } $cat = new Animal("Cat", "meow"); echo $cat->name . "\n"; // Cat echo $cat->speak() . "\n"; // Cat says meow!
PHP classes use $this->property to access instance variables; Ruby uses @instance_variable. Properties must be declared at the class level in PHP (optionally with a type). The constructor is always named __construct. Methods are called with -> (object) or :: (static/class). PHP requires new to instantiate; Ruby calls .new.
Constructor Promotion (PHP 8.0)
class Point attr_reader :x, :y def initialize(x, y) @x = x @y = y end def distance_to(other) Math.sqrt((other.x - x)**2 + (other.y - y)**2) end end point = Point.new(3.0, 4.0) puts "#{point.x}, #{point.y}"
<?php class Point { // Constructor promotion: declare + assign in one line public function __construct( public readonly float $x, public readonly float $y, ) {} public function distanceTo(Point $other): float { return sqrt(($other->x - $this->x) ** 2 + ($other->y - $this->y) ** 2); } } $point = new Point(3.0, 4.0); $origin = new Point(0.0, 0.0); echo "$point->x, $point->y\n"; // 3, 4 echo $origin->distanceTo($point) . "\n"; // 5
PHP 8.0 constructor promotion allows declaring and assigning properties directly in the constructor parameters using a visibility modifier (public, protected, or private). This eliminates the boilerplate of separately declaring the property, then assigning $this->property = $param in the body. The promoted properties are also implicitly created as regular class properties.
Readonly Properties (PHP 8.1)
Point = Data.define(:x, :y) # Ruby 3.2+ immutable value object origin = Point.new(x: 0, y: 0) puts "#{origin.x}, #{origin.y}" # 0, 0 begin origin.instance_variable_set(:@x, 99) # raises FrozenError rescue FrozenError => error puts "Frozen: #{error.message}" end
<?php class Temperature { public function __construct( public readonly float $celsius, ) {} public function toFahrenheit(): float { return $this->celsius * 9.0 / 5.0 + 32.0; } } $temp = new Temperature(100.0); echo $temp->celsius . "\n"; // 100 echo $temp->toFahrenheit() . "\n"; // 212 try { $temp->celsius = 0.0; // Error: Cannot modify readonly property } catch (Error $error) { echo "Error: " . $error->getMessage() . "\n"; }
PHP 8.1 readonly properties can only be set once, in the constructor. Any later write attempt throws an Error. This is similar to Ruby's Data.define value objects (Ruby 3.2+) or using attr_reader with no setter. PHP 8.2 added readonly class to make all properties of a class readonly automatically.
Inheritance
class Shape def area raise NotImplementedError end def describe "I am a #{self.class.name} with area #{area.round(2)}" end end class Circle < Shape def initialize(radius) @radius = radius end def area Math::PI * @radius ** 2 end end circle = Circle.new(5) puts circle.describe
<?php abstract class Shape { abstract public function area(): float; public function describe(): string { $name = (new ReflectionClass($this))->getShortName(); return "I am a $name with area " . round($this->area(), 2); } } class Circle extends Shape { public function __construct( private readonly float $radius, ) {} public function area(): float { return M_PI * $this->radius ** 2; } } $circle = new Circle(5.0); echo $circle->describe() . "\n";
PHP uses extends for class inheritance and abstract for classes and methods that must be overridden. The parent::method() syntax calls a parent class method (Ruby uses super). PHP supports single inheritance only — use interfaces or traits for multiple-inheritance patterns. The final keyword prevents a class or method from being overridden.
Static Methods & Properties
class Counter @@count = 0 def self.increment = @@count += 1 def self.reset = @@count = 0 def self.value = @@count end Counter.increment Counter.increment puts Counter.value # 2 Counter.reset puts Counter.value # 0
<?php class Counter { private static int $count = 0; public static function increment(): void { self::$count++; } public static function reset(): void { self::$count = 0; } public static function value(): int { return self::$count; } } Counter::increment(); Counter::increment(); echo Counter::value() . "\n"; // 2 Counter::reset(); echo Counter::value() . "\n"; // 0
PHP static methods and properties belong to the class, not an instance. They are accessed with ClassName::method() or self::method() inside the class — equivalent to Ruby's class methods defined with def self.method. PHP's static:: is a late-static-binding variant that resolves to the called class in inheritance chains.
Magic Methods
class Config def initialize @data = {} end def [](key) = @data[key] def []=(key, value) @data[key] = value end def to_s = @data.to_s def inspect = "#<Config #{@data.inspect}>" end config = Config.new config[:timeout] = 30 puts config[:timeout] # 30 puts config
<?php class Config { private array $data = []; public function __get(string $key): mixed { return $this->data[$key] ?? null; } public function __set(string $key, mixed $value): void { $this->data[$key] = $value; } public function __toString(): string { return json_encode($this->data); } } $config = new Config(); $config->timeout = 30; echo $config->timeout . "\n"; // 30 echo $config . "\n"; // {"timeout":30}
PHP magic methods are prefixed with __: __construct (constructor), __get/__set (property access), __toString (string conversion), __invoke (call instance as function), __clone (deep clone). Ruby has analogous hooks: method_missing, to_s, and call for callable objects.
Property Hooks (PHP 8.4)
class Product attr_reader :name # Custom getter: computed from internal state def display_price "$#{'%.2f' % @price}" end # Custom setter: validates before storing def price=(value) raise ArgumentError, "Price cannot be negative" if value < 0 @price = value end def price = @price def initialize(name, initial_price) @name = name self.price = initial_price end end product = Product.new('Widget', 9.99) puts product.name # Widget puts product.price # 9.99 puts product.display_price # $9.99 product.price = 14.99 puts product.display_price # $14.99
<?php class Product { // Computed get-only property: no backing store, derived from $this->price public string $displayPrice { get => '$' . number_format($this->price, 2); } // Property with a validation hook on set public float $price { set(float $value) { if ($value < 0) { throw new \ValueError("Price cannot be negative"); } $this->price = $value; // writes to the backing store } } public function __construct(public string $name, float $initialPrice) { $this->price = $initialPrice; } } $product = new Product('Widget', 9.99); echo $product->name . "\n"; // Widget echo $product->price . "\n"; // 9.99 echo $product->displayPrice . "\n"; // $9.99 $product->price = 14.99; echo $product->displayPrice . "\n"; // $14.99
PHP 8.4 property hooks let you attach get and set logic directly to a property declaration, eliminating boilerplate getter/setter methods. A get-only hook creates a computed property with no backing store; a set-only hook adds validation while PHP auto-generates the backing store. Ruby achieves the same with explicit def name / def name= methods or attr_reader/attr_writer, but the property-level syntax is unique to PHP 8.4. Note: this example requires PHP 8.4 or later — switch to PHP 8.5.5 (WASM) using the ▼ menu to run it.
Asymmetric Visibility (PHP 8.4)
class BankAccount # attr_reader exposes both for reading; no external setter attr_reader :account_number, :balance def initialize(account_number) @account_number = account_number @balance = 0.0 end def deposit(amount) raise ArgumentError, "Amount must be positive" unless amount > 0 @balance += amount end def withdraw(amount) raise "Insufficient funds" if amount > @balance @balance -= amount end end account = BankAccount.new('ACC-12345') account.deposit(1000.00) account.withdraw(250.00) puts account.account_number # ACC-12345 (readable) puts account.balance # 750.0 (readable, not writable externally)
<?php class BankAccount { // public private(set): readable by anyone, writable only inside this class public private(set) string $accountNumber; // public protected(set): readable by anyone, writable by this class + subclasses public protected(set) float $balance = 0.0; public function __construct(string $accountNumber) { $this->accountNumber = $accountNumber; } public function deposit(float $amount): void { if ($amount <= 0) throw new \ValueError("Amount must be positive"); $this->balance += $amount; } public function withdraw(float $amount): void { if ($amount > $this->balance) throw new \ValueError("Insufficient funds"); $this->balance -= $amount; } } $account = new BankAccount('ACC-12345'); $account->deposit(1000.00); $account->withdraw(250.00); echo $account->accountNumber . "\n"; // ACC-12345 (readable) echo $account->balance . "\n"; // 750 (readable) // These would throw a fatal error: // $account->balance = 99999; // Cannot modify protected(set) from outside // $account->accountNumber = 'hacked'; // Cannot modify private(set) from outside
PHP 8.4 asymmetric visibility gives each property two separate access levels: one for reading and one for writing. public private(set) means anyone can read the property, but only code inside the declaring class can write it. public protected(set) extends write access to subclasses. Ruby achieves the same encapsulation with attr_reader (read-only from outside) combined with @instance_variable assignment internally. Note: this example requires PHP 8.4 or later — switch to PHP 8.5.5 (WASM) using the ▼ menu to run it.
Final Properties (PHP 8.5)
class ApiResponse attr_reader :status_code, :body def initialize(status_code, body: '') @status_code = status_code @body = body end end class NotFoundResponse < ApiResponse # Ruby: parent's attr_reader is inherited; subclass can't remove it, # but CAN shadow it with a different method or ivar (no hard enforcement) def initialize(body: 'Not Found') super(404, body: body) end def error? = status_code >= 400 end response = ApiResponse.new(200, body: 'OK') puts response.status_code # 200 not_found = NotFoundResponse.new(body: 'Resource not found') puts not_found.status_code # 404 puts not_found.body # Resource not found puts not_found.error? # true
<?php class ApiResponse { public function __construct( // final: subclasses cannot re-declare or override this property final public readonly int $statusCode, public string $body = '', ) {} } class NotFoundResponse extends ApiResponse { public function __construct(string $body = 'Not Found') { parent::__construct(404, body: $body); } // This would be a Fatal error: // final public readonly int $statusCode; // Cannot redeclare final property public function isError(): bool { return $this->statusCode >= 400; } } $response = new ApiResponse(200, body: 'OK'); echo $response->statusCode . "\n"; // 200 $notFound = new NotFoundResponse('Resource not found'); echo $notFound->statusCode . "\n"; // 404 echo $notFound->body . "\n"; // Resource not found echo ($notFound->isError() ? 'error' : 'ok') . "\n"; // error
PHP 8.5 adds the final modifier to constructor-promoted properties. A final property cannot be re-declared in any subclass — any attempt throws a fatal compile-time error. This reinforces contracts in class hierarchies, especially for readonly properties that act as immutable identifiers (HTTP status codes, UUIDs, event types). Ruby has no equivalent enforcement mechanism; by convention a parent's attr_reader property is simply not overridden in subclasses, but the language does not prevent it. Note: this example requires PHP 8.5 — switch to PHP 8.5.5 (WASM) using the ▼ menu to run it.
Traits & Interfaces
Traits (like Ruby Modules)
module Greetable def greet "Hello, I am #{name}" end end module Farewell def farewell "Goodbye from #{name}" end end class Person include Greetable include Farewell attr_reader :name def initialize(name) = @name = name end alice = Person.new("Alice") puts alice.greet # Hello, I am Alice puts alice.farewell # Goodbye from Alice
<?php trait Greetable { public function greet(): string { return "Hello, I am {$this->name}"; } } trait Farewell { public function farewell(): string { return "Goodbye from {$this->name}"; } } class Person { use Greetable, Farewell; public function __construct( public readonly string $name, ) {} } $alice = new Person("Alice"); echo $alice->greet() . "\n"; // Hello, I am Alice echo $alice->farewell() . "\n"; // Goodbye from Alice
PHP traits are the closest equivalent to Ruby modules used as mixins. A class can use multiple traits; a trait can include properties and methods that are "copied into" the using class. Unlike Ruby modules, PHP traits cannot be used as types — they exist only for code reuse, not for polymorphism. Interfaces serve the polymorphism role in PHP.
Interfaces
module Serializable def serialize raise NotImplementedError, "#{self.class} must implement serialize" end end class User include Serializable def initialize(name) = @name = name def serialize { name: @name }.to_json end end
<?php interface Exportable { public function toJson(): string; } class User implements Exportable { public function __construct( private readonly string $name, ) {} public function toJson(): string { return json_encode(['name' => $this->name]); } } $user = new User("Alice"); echo $user->toJson() . "\n"; // {"name":"Alice"} echo ($user instanceof Exportable) ? "is Exportable\n" : "no\n";
PHP interfaces define a contract: a list of method signatures that implementing classes must provide. A class can implement multiple interfaces. PHP interfaces serve the role of Ruby modules used with include for duck-typing contracts, but with compile-time enforcement via type checking. The instanceof operator checks interface implementation like Ruby's is_a?.
Abstract Class vs Interface
# Ruby uses modules for both interface contracts and shared implementation module Printable def print_info puts "Name: #{name}" # calls subclass #name method end end class Document include Printable attr_reader :name def initialize(name) = @name = name end Document.new("README").print_info # Name: README
<?php // Interface: pure contract, no implementation interface Printable { public function getName(): string; } // Abstract class: partial implementation (like Ruby module + some methods) abstract class Document implements Printable { abstract public function getName(): string; public function printInfo(): void { echo "Name: " . $this->getName() . "\n"; } } class Report extends Document { public function __construct( private readonly string $title, ) {} public function getName(): string { return $this->title; } } (new Report("Annual Report"))->printInfo(); // Name: Annual Report
In PHP, use an interface when you only need a type contract; use an abstract class when you want to share implementation between subclasses. A class can implement multiple interfaces but extend only one abstract class. Ruby modules blur this distinction — they can do both at once, acting as both contract and mixin.
Error Handling
try / catch / finally
begin result = 10 / 0 rescue ZeroDivisionError => error puts "Caught: #{error.message}" ensure puts "Always runs" end
<?php try { $result = intdiv(10, 0); } catch (DivisionByZeroError $error) { echo "Caught: " . $error->getMessage() . "\n"; } finally { echo "Always runs\n"; }
PHP's try/catch/finally maps directly to Ruby's begin/rescue/ensure. PHP exceptions are objects extending the Throwable interface, with two main branches: Exception (user-land) and Error (PHP engine errors like type errors, division by zero, and readonly violations). Always catch the specific type, not the generic base class.
Custom Exceptions
class InsufficientFundsError < StandardError attr_reader :amount def initialize(amount) @amount = amount super("Insufficient funds: need #{amount} more") end end def withdraw(balance, amount) raise InsufficientFundsError.new(amount - balance) if amount > balance balance - amount end begin withdraw(50, 100) rescue InsufficientFundsError => error puts error.message puts "Short by: #{error.amount}" end
<?php class InsufficientFundsException extends RuntimeException { public function __construct( public readonly float $shortfall, ) { parent::__construct("Insufficient funds: need {$shortfall} more"); } } function withdraw(float $balance, float $amount): float { if ($amount > $balance) { throw new InsufficientFundsException($amount - $balance); } return $balance - $amount; } try { withdraw(50.0, 100.0); } catch (InsufficientFundsException $error) { echo $error->getMessage() . "\n"; echo "Short by: " . $error->shortfall . "\n"; }
PHP custom exceptions extend \Exception or a subclass like \RuntimeException. Multiple catch blocks can catch different exception types, and PHP 8.0 supports union catches: catch (TypeErrorException | InvalidArgumentException $e). This is like Ruby's multiple rescue clauses.
Exception Hierarchy
# Ruby's exception tree # Exception # StandardError (most user exceptions) # RuntimeError (default from raise "message") # TypeError, ArgumentError, etc. # ScriptError # SignalException # SystemExit begin raise "something failed" rescue RuntimeError => error puts error.class # RuntimeError puts error.message # something failed end
<?php // PHP Throwable tree: // Throwable // Error (PHP engine errors — do NOT catch blindly) // TypeError, ValueError, ArithmeticError, ParseError, etc. // Exception (user-land — extend for custom exceptions) // RuntimeException, LogicException, InvalidArgumentException, etc. function divide(int $a, int $b): float { if ($b === 0) throw new InvalidArgumentException("Cannot divide by zero"); return $a / $b; } try { echo divide(10, 2) . "\n"; // 5 echo divide(10, 0) . "\n"; // throws } catch (InvalidArgumentException $error) { echo "Invalid: " . $error->getMessage() . "\n"; } catch (Throwable $error) { echo "Unexpected: " . $error->getMessage() . "\n"; }
PHP distinguishes Error (engine-level: type violations, readonly writes, syntax errors) from Exception (application-level). Catching \Throwable catches both — use sparingly and only in top-level error handlers. Ruby's StandardError is roughly equivalent to Exception; catching Exception in Ruby (like Throwable in PHP) is an antipattern.
Multiple & Union Catches
def parse_input(input) raise ArgumentError, "empty input" if input.empty? raise TypeError, "not a string" unless input.is_a?(String) Integer(input) rescue ArgumentError, TypeError => error puts "Input error: #{error.message}" 0 rescue ArgumentError => error puts "Range error: #{error.message}" 0 end puts parse_input("") # Input error: empty input puts parse_input("42") # 42
<?php function parseInput(string $input): int { if ($input === '') throw new InvalidArgumentException("empty input"); if (!is_numeric($input)) throw new TypeError("not a number"); return (int) $input; } try { echo parseInput("") . "\n"; } catch (InvalidArgumentException | TypeError $error) { // PHP 8.0 union catch — handles either type echo "Input error: " . $error->getMessage() . "\n"; } try { echo parseInput("abc") . "\n"; } catch (InvalidArgumentException | TypeError $error) { echo "Input error: " . $error->getMessage() . "\n"; } try { echo parseInput("42") . "\n"; // 42 } catch (Throwable $error) { echo "Unexpected: " . $error->getMessage() . "\n"; }
PHP 8.0 union catches (catch (TypeA | TypeB $e)) let a single catch block handle multiple exception types. This is equivalent to Ruby's rescue TypeA, TypeB => e. Multiple separate catch blocks are also valid and are checked in order, like Ruby's multiple rescue clauses.
Type System
Type Declarations
# Ruby: optional type annotations via Sorbet/RBS or just duck typing # No built-in enforcement at runtime def add(a, b) a + b # works for anything that defines + end puts add(1, 2) # 3 puts add(1.5, 2.5) # 4.0 puts add("ab", "cd") # abcd
<?php // PHP: type hints enforced at runtime function add(int|float $a, int|float $b): int|float { return $a + $b; } echo add(1, 2) . "\n"; // 3 echo add(1.5, 2.5) . "\n"; // 4 // add("ab", "cd") would throw TypeError // void return type function logMessage(string $message): void { echo "[LOG] $message\n"; // cannot return a value } logMessage("Application started");
PHP type declarations are enforced at runtime (in strict mode, also at call time). Parameter types, return types, and property types are all supported. PHP's type system is structural (like Ruby's duck typing at the surface) but enforced at runtime. Strict mode can be enabled per file with declare(strict_types=1); at the top.
Nullable Types
def find_user(id) return nil if id <= 0 { id: id, name: "User #{id}" } end user = find_user(5) puts user&.dig(:name) || "(not found)" # User 5 missing = find_user(0) puts missing&.dig(:name) || "(not found)" # (not found)
<?php function findUser(int $id): ?array { if ($id <= 0) return null; return ['id' => $id, 'name' => "User {$id}"]; } $user = findUser(5); $missing = findUser(0); echo ($user ? $user['name'] : "(not found)") . "\n"; // User 5 echo ($missing ? $missing['name'] : "(not found)") . "\n"; // (not found)
The ?Type syntax declares that a value can be either the given type or null. ?string means string|null. This is equivalent to Ruby methods that can return either a value or nil. PHP 8.0's ?-> nullsafe operator pairs with nullable types to safely chain through potentially-null values.
Union Types (PHP 8.0)
# Ruby: no built-in union types; use duck typing or Sorbet def format_value(value) case value in Integer then "int: #{value}" in Float then "float: #{value}" in String then "str: #{value}" else "unknown" end end puts format_value(42) # int: 42 puts format_value(3.14) # float: 3.14 puts format_value("hi") # str: hi
<?php function formatValue(int|float|string $value): string { return match(true) { is_int($value) => "int: $value", is_float($value) => "float: $value", is_string($value)=> "str: $value", }; } echo formatValue(42) . "\n"; // int: 42 echo formatValue(3.14) . "\n"; // float: 3.14 echo formatValue("hi") . "\n"; // str: hi
PHP 8.0 union types (int|string) allow a parameter or return value to be one of several types. PHP 8.2 added true, false, and null as standalone types in unions. The special type mixed means any type (equivalent to no declaration). PHP 8.1 added intersection types (TypeA&TypeB) for values that must satisfy multiple type constraints simultaneously.
Intersection Types (PHP 8.1)
# Ruby: no intersection types; combine with include and respond_to? module Countable def count = raise NotImplementedError end module Serializable def serialize = raise NotImplementedError end def process(collection) # Duck-type: caller must ensure collection responds to both puts collection.count puts collection.serialize end
<?php interface Sizeable { public function size(): int; } interface Exportable { public function toText(): string; } // Intersection type: argument must implement BOTH interfaces function process(Sizeable&Exportable $collection): void { echo $collection->size() . "\n"; echo $collection->toText() . "\n"; } class NumberList implements Sizeable, Exportable { private array $items; public function __construct(int ...$items) { $this->items = $items; } public function size(): int { return count($this->items); } public function toText(): string { return implode(',', $this->items); } } process(new NumberList(1, 2, 3, 4, 5)); // 5 then 1,2,3,4,5
PHP 8.1 intersection types (TypeA&TypeB) require a value to satisfy multiple interface or class constraints simultaneously. Intersection types only work with class/interface types, not scalars. This is stricter than Ruby's duck typing, but serves the same role as requiring an object to respond to multiple method contracts.
Enums (PHP 8.1)
Pure Enums
# Ruby: no built-in enum; common idiom uses symbols or constants module Status ACTIVE = :active INACTIVE = :inactive PENDING = :pending end current = Status::ACTIVE puts current # active puts current == :active # true
<?php enum Status { case Active; case Inactive; case Pending; } $current = Status::Active; echo $current->name . "\n"; // Active echo ($current === Status::Active) ? "is active\n" : "not active\n"; // is active // Enums work in match expressions $label = match($current) { Status::Active => "Online", Status::Inactive => "Offline", Status::Pending => "Waiting", }; echo $label . "\n"; // Online
PHP 8.1 enums are first-class types. Pure enums have no associated value — each case is a singleton object. Enum cases are compared with strict equality (===). Enums can implement interfaces, have methods, and have constants. They work naturally in match expressions.
Backed Enums (int / string)
# Ruby: hash-backed enums (common pattern) module Color RED = { name: :red, value: "red" } GREEN = { name: :green, value: "green" } BLUE = { name: :blue, value: "blue" } end color = Color::RED puts color[:value] # red
<?php enum Color: string { case Red = 'red'; case Green = 'green'; case Blue = 'blue'; } $color = Color::Red; echo $color->value . "\n"; // red echo $color->name . "\n"; // Red // Create from raw value $fromValue = Color::from('green'); echo $fromValue->name . "\n"; // Green // tryFrom returns null if not found (instead of throwing) $unknown = Color::tryFrom('purple'); echo ($unknown ?? "(not found)") . "\n"; // (not found)
PHP backed enums associate each case with a scalar value: either int or string. The ->value property retrieves the backing value. Color::from() creates an enum case from its value (throws ValueError if not found); Color::tryFrom() returns null instead. Color::cases() returns all cases as an array.
Enum Methods
module Suit HEARTS = :hearts DIAMONDS = :diamonds CLUBS = :clubs SPADES = :spades RED_SUITS = [HEARTS, DIAMONDS].freeze def self.red?(suit) = RED_SUITS.include?(suit) end puts Suit.red?(:hearts) # true puts Suit.red?(:spades) # false
<?php enum Suit: string { case Hearts = 'hearts'; case Diamonds = 'diamonds'; case Clubs = 'clubs'; case Spades = 'spades'; public function isRed(): bool { return match($this) { Suit::Hearts, Suit::Diamonds => true, default => false, }; } public function label(): string { return ucfirst($this->value); } } echo Suit::Hearts->isRed() ? "red\n" : "black\n"; // red echo Suit::Spades->isRed() ? "red\n" : "black\n"; // black echo Suit::Clubs->label() . "\n"; // Clubs
PHP enums can have regular and static methods. Instance methods receive $this as the specific enum case, enabling per-case behavior via match($this). Enums can implement interfaces and define constants with const. This makes PHP enums much more powerful than Ruby's typical symbol-based or module-constant patterns.
⚠ Gotchas for Rubyists
Loose == vs Strict ===
# Ruby's == does NOT coerce types p 0 == nil # false (nil is not 0) p 1 == "1" # false (no string-to-int coercion) p nil == false # false (nil and false are not equal) p [] == false # false (empty array is truthy in Ruby)
<?php // PHP loose == coerces types (like JavaScript ==) var_dump(0 == null); // bool(true) — 0 equals null! var_dump(1 == "1"); // bool(true) — int coerced to string var_dump(null == false); // bool(true) — both falsy var_dump([] == false); // bool(true) — empty array is falsy echo "---\n"; // PHP strict === requires same type AND value var_dump(0 === null); // bool(false) var_dump(1 === "1"); // bool(false) — int != string var_dump(null === false); // bool(false)
PHP's == operator performs type coercion and produces surprising results: 0 == null is true, and an empty array equals false. Always use === in PHP for type-safe comparisons — it requires both value and type to match. Ruby's == does not coerce types, so nil == 0 and nil == false are both false. This is one of the most common PHP bugs for developers coming from Ruby.
Zero is Falsy
# Ruby: only nil and false are falsy puts !!0 # true (0 is truthy in Ruby!) puts !!"" # true (empty string is truthy in Ruby!) puts !![] # true (empty array is truthy in Ruby!) puts !!nil # false puts !!false # false
<?php // PHP: many values are falsy var_dump((bool) 0); // bool(false) — 0 is FALSY in PHP var_dump((bool) 0.0); // bool(false) var_dump((bool) ""); // bool(false) — empty string is FALSY var_dump((bool) "0"); // bool(false) — the string "0" is FALSY! var_dump((bool) []); // bool(false) — empty array is FALSY var_dump((bool) null); // bool(false) echo "---\n"; var_dump((bool) 1); // bool(true) var_dump((bool) "0.0"); // bool(true) — only "0" is falsy, not "0.0"
PHP's falsy values are false, null, 0, 0.0, "", "0", and []. In Ruby, only nil and false are falsy — everything else, including 0, "", and [], is truthy. This difference causes real bugs when porting Ruby conditionals to PHP: if $count in PHP fails when $count is zero, whereas if count in Ruby always succeeds for an integer.
Variable Scope in Closures
threshold = 5 # Ruby blocks capture outer scope automatically: evens_above = [1, 2, 6, 7, 8].select { |n| n > threshold && n.even? } puts evens_above.inspect # [6, 8] # Ruby lambdas also capture automatically: above_threshold = ->(number) { number > threshold } puts above_threshold.call(8) # true
<?php $threshold = 5; // PHP anonymous functions need 'use' to capture outer scope: $aboveThreshold = function(int $number) use ($threshold): bool { return $number > $threshold; }; echo $aboveThreshold(8) ? "yes\n" : "no\n"; // yes // Arrow functions capture automatically (but only expressions, no {}): $evensAbove = array_filter( [1, 2, 6, 7, 8], fn($number) => $number > $threshold && $number % 2 === 0 ); echo implode(', ', $evensAbove) . "\n"; // 6, 8
PHP anonymous functions (function() { }) do not capture outer-scope variables automatically — you must list them explicitly in use ($variable). PHP arrow functions (fn() =>) DO capture outer scope automatically, but are limited to a single expression. Ruby blocks and lambdas always capture the outer binding automatically, with no special syntax required.
echo Does Not Add a Newline
# puts always adds a trailing newline: print "Hello" # no newline print "World" # no newline puts # just a newline puts "Done" # Done + newline
<?php // echo does NOT add a newline (unlike Ruby's puts): echo "Hello"; echo "World"; // Output so far: HelloWorld (no space, no newline!) echo "\n"; // explicit newline echo "---\n"; // Two common patterns for newlines: echo "Line one\n"; // explicit echo "Line two" . PHP_EOL; // PHP_EOL constant (\n on Unix, \r\n on Windows)
PHP's echo and print never add a trailing newline — you must include \n explicitly or use the PHP_EOL constant. Ruby's puts always appends a newline; print does not. This is a constant source of confusion for Rubyists: forgetting \n in PHP causes consecutive outputs to appear on the same line with no separator.
Arrays Are Copied by Value
# Ruby arrays are objects, passed by reference: original = [1, 2, 3] reference = original reference.push(4) puts original.inspect # [1, 2, 3, 4] — original was mutated! # To copy, use .dup: copy = original.dup copy.push(5) puts original.inspect # [1, 2, 3, 4] — original unchanged
<?php // PHP arrays are VALUE types — assignment copies the array: $original = [1, 2, 3]; $copy = $original; // This is a copy, not a reference $copy[] = 4; echo implode(', ', $original) . "\n"; // 1, 2, 3 — original unchanged! echo implode(', ', $copy) . "\n"; // 1, 2, 3, 4 // To share an array, use a reference: $reference = &$original; $reference[] = 99; echo implode(', ', $original) . "\n"; // 1, 2, 3, 99 — original changed
PHP arrays are value types with copy-on-write semantics — assignment creates an independent copy. In Ruby, arrays are objects and assignment copies the reference, so both variables point to the same array. This is a significant difference: Ruby's arr2 = arr1 shares the array; PHP's $arr2 = $arr1 copies it. To share a PHP array, use $arr2 = &$arr1 (reference).
© 2026 Brandon Zylstra  •  A project of GlobeChatter  •  Ruby runs via ruby.wasm  •  PHP runs via Judge0 CE or PHP WASM