PONY λ M2 Modula-2

Perl.CodeCompared.To/PHP

An interactive executable cheatsheet comparing Perl and PHP

Perl 5.40 PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM)
Hello World & Running
Hello, World
use v5.38; say "Hello, World!";
<?php declare(strict_types=1); echo "Hello, World!\n";
PHP requires the <?php opening tag before any PHP code — without it, the file is served as literal HTML. echo is the direct equivalent of Perl's print: it does not append a trailing newline, so "\n" must be included explicitly, unlike Perl's say. declare(strict_types=1); is PHP's closest analog to Perl's use v5.38; preamble — both opt a file into stricter, more modern behavior.
Running a program
# Run directly (no compile step): # perl hello.pl # # Check syntax only: # perl -c hello.pl # # One-liner: # perl -e 'print "Hello!\n"'
<?php // Run directly (no compile step): // php hello.php // // Check syntax only: // php -l hello.php // // One-liner: // php -r 'echo "Hello!\n";' // // Built-in development server: // php -S localhost:8000
Both languages are interpreted with no separate compile step, so php hello.php and perl hello.pl run identically. PHP additionally ships a built-in development web server (php -S) with no external module needed — a Perl programmer would typically reach for plackup or a CPAN-distributed web framework to get the same convenience.
Comments
use v5.38; # Single-line comment — the only inline comment syntax # Perl has no multi-line comment syntax. # Block documentation uses Pod: =begin comment ... =end comment my $name = "Alice"; # inline comment say $name;
<?php declare(strict_types=1); // Single-line comment (C++ style) # Single-line comment (shell style — also valid) /* * Multi-line comment * block */ $name = "Alice"; // inline comment echo $name . "\n";
PHP supports three comment styles: // and # for single-line comments (both valid — // is by far the more common convention), and C-style /* ... */ for real multi-line block comments. Perl's # is the only inline form, and Perl has no true block-comment syntax at all — the =begin/=end Pod markers are a workaround borrowed from documentation tooling, not a first-class comment construct.
echo vs print vs var_dump
use v5.38; print "no newline"; print "\n"; say "with newline"; # say appends a newline automatically say 42; # works with numbers too
<?php declare(strict_types=1); echo "no newline"; echo "\n"; echo "with newline\n"; // echo needs an explicit \n echo 42 . "\n"; // works with numbers too, via string concatenation var_dump("hello"); // shows type and value: string(5) "hello" var_dump([1, 2, 3]); // shows structure, like Perl's Data::Dumper
PHP's echo is close to Perl's print — neither appends a newline automatically, unlike Perl's say. echo accepts multiple comma-separated arguments (echo "a", "b";) but does not implicitly stringify numbers the way string interpolation does, so numeric values are usually concatenated with .. PHP's var_dump plays the same debugging role as Ruby's p or Perl's Data::Dumper: it shows the type and full structure of a value, not just its printed form.
Variables & Sigils
One sigil for everything vs sigil-per-type
use v5.38; my $name = "Alice"; my @colors = ("red", "green", "blue"); my %person = (name => "Alice", age => 30); say $name; say "@colors"; say $person{name};
<?php declare(strict_types=1); $name = "Alice"; $colors = ["red", "green", "blue"]; $person = ["name" => "Alice", "age" => 30]; echo $name . "\n"; echo implode(", ", $colors) . "\n"; echo $person["name"] . "\n";
PHP flattens Perl's three sigils ($ for scalars, @ for arrays, % for hashes) into one: every variable, regardless of what it holds, is prefixed with $. This is a genuine simplification of the sigil system, but it comes at a cost a Perl programmer will notice immediately: PHP has no separate array and hash sigil-driven type, because it has no separate array and hash type at all — see the Arrays section for what that unification actually means.
my declaration vs plain assignment
use v5.38; my $count = 0; { my $count = 10; # shadows the outer $count in this block say $count; } say $count;
<?php declare(strict_types=1); $count = 0; if (true) { $count = 10; // reassigns the SAME variable — no new scope here echo $count . "\n"; } echo $count . "\n";
Perl's my creates a genuinely new lexical variable scoped to the enclosing block, so an inner my $count shadows an outer one. PHP has no declaration keyword for local variables at all — a bare assignment creates or reassigns a variable in the enclosing function scope, and if/while/for blocks do not introduce a new scope, exactly like Ruby. Only function bodies (and, separately, closures via use) create new variable scopes in PHP.
use v5.38 vs declare(strict_types=1)
use v5.38; use strict; use warnings; # use v5.38 implies strict and warnings automatically. my $count = "5" + 3; # Perl coerces the string "5" to a number say $count; # 8
<?php declare(strict_types=1); function addOne(int $number): int { return $number + 1; } // With strict_types, PHP will NOT coerce a float or numeric string // into an int parameter — this line would throw a TypeError: // echo addOne("5") . "\n"; echo addOne(5) . "\n"; // 6
Perl's use v5.38 preamble automatically enables strict (mandatory variable declarations) and warnings (diagnostics for questionable constructs), but it does not add compile-time type checking — Perl remains fully dynamically typed with automatic numeric-string coercion. PHP's declare(strict_types=1); is orthogonal: it only affects type-hinted function signatures, forbidding PHP's usual implicit type coercion (an int parameter will reject a passed string rather than silently converting it) for calls within that file. Without it, PHP coerces arguments the way Perl coerces values everywhere.
Dynamic typing with optional hints
use v5.38; my $value = 42; say ref(\$value) ? "reference" : "plain scalar"; $value = "hello"; # scalars hold any single value freely $value = [1, 2, 3]; say ref($value); # "ARRAY" — now it's an array reference
<?php declare(strict_types=1); $value = 42; echo gettype($value) . "\n"; // integer $value = "hello"; echo gettype($value) . "\n"; // string $value = [1, 2, 3]; echo gettype($value) . "\n"; // array
Both languages let a variable hold a value of any type at any time by default — neither requires a type declaration for a plain variable. PHP additionally supports optional type hints on function parameters, return values, and (since PHP 7.4) typed properties, giving a Perl programmer an opt-in static-checking layer that Perl has no built-in equivalent for (Perl relies on CPAN modules like Type::Tiny for anything similar).
Constants
use v5.38; use constant MAX_RETRIES => 3; use constant API_URL => "https://example.com"; say MAX_RETRIES; say API_URL;
<?php declare(strict_types=1); define("MAX_RETRIES", 3); const API_URL = "https://example.com"; // preferred: compile-time constant echo MAX_RETRIES . "\n"; echo API_URL . "\n";
PHP has two ways to define a top-level constant: the define() function, evaluated at runtime (closer to Perl's use constant, which is also evaluated when the pragma is processed), and the const keyword, evaluated at compile time and preferred for simple scalar values. Unlike every other PHP variable, a constant defined either way is referenced with no $ sigil at all — a rare case where PHP drops its otherwise-universal sigil rule, landing closer to how Perl's use constant names read as bareword function calls.
Truthiness — PHP keeps the "0" trap
use v5.38; # false: undef, 0, "", "0" # true: everything else — including "00" and " " for my $value (undef, 0, "", "0", "00", " ") { my $label = defined($value) ? "'$value'" : "undef"; say "$label is " . ($value ? "truthy" : "falsy"); }
<?php declare(strict_types=1); // Falsy in PHP: null, false, 0, 0.0, "", "0", [] $values = [null, 0, "", "0", "00", " ", []]; foreach ($values as $value) { $label = is_null($value) ? "null" : (is_array($value) ? "[]" : "'$value'"); echo $label . " is " . ($value ? "truthy" : "falsy") . "\n"; } // "00" and " " are TRUTHY in both Perl and PHP — only the exact string "0" is falsy
PHP's truthiness rules track Perl's almost exactly — this is one of the strongest resemblances between the two languages' semantics, and a pleasant surprise for a Perl programmer used to other languages diverging here. Both treat 0, the empty string "", and the single-character string "0" as falsy, while "00" and a lone space " " are truthy in both. PHP additionally treats an empty array [] as falsy (Perl has no unified array/hash type to compare against directly, but an empty Perl array in boolean context is also falsy) and null plays the role Perl's undef plays.
List destructuring
use v5.38; my ($first, $second, $third) = (1, 2, 3); say "$first $second $third"; # Swap without a temp variable ($first, $second) = ($second, $first); say "$first $second"; # Destructure a list my ($head, @tail) = (10, 20, 30, 40); say $head; say "@tail";
<?php declare(strict_types=1); [$first, $second, $third] = [1, 2, 3]; echo "$first $second $third\n"; // Swap without a temp variable [$first, $second] = [$second, $first]; echo "$first $second\n"; // Destructure with named keys from an associative array ["name" => $name, "age" => $age] = ["name" => "Alice", "age" => 30]; echo "$name is $age\n";
PHP's list destructuring, written with either list(...) or the shorthand [...] syntax on the left-hand side, mirrors Perl's list-assignment destructuring, including the temp-free swap idiom both languages share. PHP has no direct equivalent to Perl's my ($head, @tail) = (...) "soak up the rest" pattern — there is no splat-style remainder capture in list destructuring, so grabbing "the first element and everything else" needs array_shift plus the remaining array rather than a single destructuring assignment.
Strings
String interpolation
use v5.38; my $name = "Alice"; my $age = 30; say "Hello, $name! You are $age years old."; say "Next year: @{[ $age + 1 ]}"; # expression interpolation trick
<?php declare(strict_types=1); $name = "Alice"; $age = 30; echo "Hello, $name! You are $age years old.\n"; // Curly-brace form for expressions and complex access echo "Next year: {$age}\n"; $person = ["name" => "Alice"]; echo "Hello, {$person['name']}!\n"; // curly braces needed for array access
PHP double-quoted strings interpolate a bare $name exactly like Perl, and the same single-vs-double-quote distinction applies: single-quoted PHP strings interpolate nothing, just like Perl's single-quoted strings. Where the two diverge is arbitrary-expression interpolation — Perl's awkward @{[ expression ]} trick has no PHP equivalent at all, because PHP interpolation only ever substitutes a variable's value (optionally wrapped in {} for array/property access), never evaluates an arbitrary expression like $age + 1 inline.
String functions as standalone functions
use v5.38; my $text = " Hello, World! "; $text =~ s/^\s+|\s+$//g; # trim: no built-in method, use regex say $text; say length($text); say uc($text); say lc($text); say reverse($text); say index($text, "World") >= 0 ? "found" : "not found";
<?php declare(strict_types=1); $text = trim(" Hello, World! "); echo $text . "\n"; echo strlen($text) . "\n"; echo strtoupper($text) . "\n"; echo strtolower($text) . "\n"; echo strrev($text) . "\n"; echo (str_contains($text, "World") ? "found" : "not found") . "\n";
Like Perl, PHP's string operations are mostly standalone functions that take the string as an argument (strlen($text), strtoupper($text)) rather than methods called on a string object — this is a genuine similarity, since neither language treats a string as an object with a method table by default. Unlike Perl, PHP does provide a built-in trim() function directly, with no regex substitution needed, and str_contains() (PHP 8.0+) replaces the older, more awkward strpos($text, "World") !== false idiom for a simple substring check.
Heredocs and nowdocs
use v5.38; my $name = "Alice"; my $message = <<~LETTER; Dear $name, Welcome to the team! Regards, HR LETTER print $message;
<?php declare(strict_types=1); $name = "Alice"; $message = <<<LETTER Dear $name, Welcome to the team! Regards, HR LETTER; echo $message; // Nowdoc — single-quote-style, no interpolation $literal = <<<'RAW' No $interpolation happens here. RAW; echo $literal . "\n";
PHP's heredoc (<<<LABEL) is essentially Perl's squiggly heredoc (<<~LABEL) — both strip the closing marker's indentation from every line so the source can be indented naturally, and both interpolate variables by default. PHP additionally names the non-interpolating variant a "nowdoc" (<<<'LABEL', with the label single-quoted), which maps directly onto Perl's single-quoted heredoc terminator (<<~'LABEL') for suppressing interpolation.
sprintf-style formatting
use v5.38; printf("%-10s: %6.2f\n", "Price", 9.99); say sprintf("Hex: 0x%08X", 255); say sprintf("Count: %05d", 42);
<?php declare(strict_types=1); printf("%-10s: %6.2f\n", "Price", 9.99); echo sprintf("Hex: 0x%08X", 255) . "\n"; echo sprintf("Count: %05d", 42) . "\n";
PHP's sprintf and printf take the exact same format-specifier syntax as Perl's — both languages inherited this format-string mini-language directly from C, so field widths, precision, padding, and hex/octal formatting all transfer with zero changes needed. This is one of the most portable pieces of syntax between the two languages.
Splitting and joining strings
use v5.38; my $csv = "apple,banana,cherry"; my @fruits = split(/,/, $csv); say "@fruits"; say join(" | ", @fruits); my @words = split ' ', " the quick fox "; # special-cased whitespace split say scalar(@words);
<?php declare(strict_types=1); $csv = "apple,banana,cherry"; $fruits = explode(",", $csv); echo implode(", ", $fruits) . "\n"; echo implode(" | ", $fruits) . "\n"; $words = preg_split('/\s+/', " the quick fox ", -1, PREG_SPLIT_NO_EMPTY); echo count($words) . "\n";
PHP's explode/implode pair map directly onto Perl's split/join, but explode only splits on a literal separator string, not a regex pattern — Perl's split(/,/, ...) happens to use a trivial regex, but PHP requires preg_split for anything beyond a fixed delimiter. Perl's special-cased split ' ', $string form (splitting on runs of whitespace and discarding leading empty fields) has no single-function PHP equivalent; preg_split with PREG_SPLIT_NO_EMPTY is the closest match.
Numbers
int and float vs Perl's single scalar
use v5.38; my $integer = 42; my $float = 3.14; say ref(\$integer); # "SCALAR" — Perl doesn't distinguish int from float at the type level say $integer + $float; say 2**64; # Perl promotes to floating point beyond native integer range
<?php declare(strict_types=1); $integer = 42; $float = 3.14; echo gettype($integer) . "\n"; // integer echo gettype($float) . "\n"; // double echo ($integer + $float) . "\n"; echo (2 ** 62) . "\n"; // exact — fits in a 64-bit int echo gettype(2 ** 64) . "\n"; // "double" — silently promotes to float on overflow
PHP distinguishes integer and double (its name for float) as genuinely separate internal types, unlike Perl's single undifferentiated scalar. Both languages, however, silently promote an integer computation to a floating-point value once it overflows the native integer range — PHP's 64-bit integer overflows to a double just as Perl's scalar does, so neither offers Ruby's arbitrary-precision integer arithmetic without an external library (PHP's GMP or BCMath extensions, Perl's Math::BigInt).
Numeric functions as standalone calls
use v5.38; my $number = 42; say $number % 2 == 0 ? "even" : "odd"; say $number > 0 ? "positive" : "not positive"; say abs(-42); for my $count (0..2) { say "tick"; }
<?php declare(strict_types=1); $number = 42; echo ($number % 2 === 0 ? "even" : "odd") . "\n"; echo ($number > 0 ? "positive" : "not positive") . "\n"; echo abs(-42) . "\n"; for ($count = 0; $count < 3; $count++) { echo "tick\n"; }
Both languages check parity and sign with inline comparison expressions rather than dedicated predicate methods, and both provide abs() as a standalone function — this is a close match. PHP's C-style for loop (for ($count = 0; $count < 3; $count++)) is the idiomatic counting loop, mirroring Perl's own C-style for more closely than Ruby's range-based 3.times; PHP does support ranges via range(0, 2) combined with foreach as an alternative.
Division and modulo
use v5.38; say 10 / 3; # 3.33333... — Perl's / is always floating-point division say int(10 / 3); # 3 — truncate toward zero for integer division say 10 % 3; # 1 say -7 % 3; # 2 in Perl — result takes the sign of the right operand
<?php declare(strict_types=1); echo (10 / 3) . "\n"; // 3.3333333333333 — PHP's / is always floating-point too echo intdiv(10, 3) . "\n"; // 3 — dedicated integer-division function echo (10 % 3) . "\n"; // 1 echo (-7 % 3) . "\n"; // -1 in PHP — result takes the sign of the LEFT operand
PHP's / operator behaves exactly like Perl's — always floating-point division regardless of operand types — but PHP provides a dedicated intdiv() function for truncating integer division rather than Perl's int(10 / 3) workaround. The two languages disagree on modulo sign convention with a negative operand: Perl's % takes the sign of the right operand (-7 % 3 is 2), while PHP's % takes the sign of the left operand, C-style (-7 % 3 is -1) — a genuine behavioral trap worth checking carefully when porting arithmetic.
Numeric conversions
use v5.38; my $parsed = "42" + 0; # Perl coerces strings to numbers automatically say $parsed; say "$parsed"; # back to string via interpolation say "3.14" + 0.0; say hex("FF"); # 255 say sprintf("%x", 255); # "ff"
<?php declare(strict_types=1); $parsed = (int) "42"; echo $parsed . "\n"; echo (string) $parsed . "\n"; echo (float) "3.14" . "\n"; echo hexdec("FF") . "\n"; // 255 echo dechex(255) . "\n"; // "ff"
PHP supports the same implicit numeric-string coercion Perl does when strict_types is off ("42" + 0 works in both), but explicit cast operators — (int), (float), (string) — are the idiomatic and, under declare(strict_types=1);, often required way to convert. PHP's hexdec()/dechex() pair replaces Perl's hex()/sprintf("%x", ...) for base-16 conversion, with matching functions for octal (octdec/decoct) and binary (bindec/decbin) that Perl handles through oct() and sprintf format specifiers instead.
Arrays
Perl's separate @array vs PHP's ordered map
use v5.38; my @numbers = (1, 2, 3, 4, 5); my @fruits = ("apple", "banana", "cherry"); say $numbers[0]; # element access uses $, not @ say scalar(@fruits); # count say "@fruits"; # interpolates space-joined
<?php declare(strict_types=1); $numbers = [1, 2, 3, 4, 5]; $fruits = ["apple", "banana", "cherry"]; echo $numbers[0] . "\n"; echo count($fruits) . "\n"; echo implode(" ", $fruits) . "\n";
This is the single biggest structural difference between the languages. Perl keeps arrays (@array, integer-indexed, ordered) and hashes (%hash, string-keyed, unordered) as two genuinely distinct data types with distinct sigils. PHP has exactly one array type — internally an ordered hash map — that serves both roles: a PHP array with sequential integer keys starting at 0 behaves like Perl's @array, while the same type with string keys behaves like Perl's %hash, and it can even freely mix both kinds of keys in one array. There is no PHP equivalent to Perl needing a reference to nest an array inside a hash — it is already the same type.
Negative indexing and slicing
use v5.38; my @numbers = (10, 20, 30, 40, 50); say $numbers[0]; # first: 10 say $numbers[-1]; # last: 50 say $numbers[-2]; # second-to-last: 40 my @middle = @numbers[1..3]; say "@middle";
<?php declare(strict_types=1); $numbers = [10, 20, 30, 40, 50]; echo $numbers[0] . "\n"; // first: 10 echo end($numbers) . "\n"; // last: 50 (no negative indexing!) echo array_slice($numbers, -2, 1)[0] . "\n"; // second-to-last: 40 echo implode(" ", array_slice($numbers, 1, 3)) . "\n"; // middle: 20 30 40
This is a real gap for a Perl programmer: PHP arrays do not support negative indexing ($numbers[-1] looks for a literal key of -1, which does not exist here, rather than "the last element"). Getting the last element needs end($numbers) or array_slice($numbers, -1)[0], and general slicing uses array_slice($array, $offset, $length) — a start-and-length form like Perl's @array[$start, $length] alternative, not Perl's inclusive-range slice syntax.
push, pop, shift, unshift
use v5.38; my @items = (2, 3); push @items, 4; # add to end unshift @items, 1; # add to front say "@items"; # 1 2 3 4 my $last = pop @items; # remove from end my $first = shift @items; # remove from front say $last; say $first; say "@items";
<?php declare(strict_types=1); $items = [2, 3]; array_push($items, 4); // add to end — also: $items[] = 4; array_unshift($items, 1); // add to front echo implode(" ", $items) . "\n"; // 1 2 3 4 $last = array_pop($items); // remove from end $first = array_shift($items); // remove from front echo $last . "\n"; echo $first . "\n"; echo implode(" ", $items) . "\n";
PHP's array_push, array_pop, array_shift, and array_unshift are named almost identically to Perl's built-in functions of the same purpose and behave the same way. PHP also offers the terser $items[] = 4; append syntax as an idiomatic alternative to array_push — pushing a single element this way is the more common style and avoids a function-call for the simplest case, something Perl has no equivalent shorthand for.
map and grep vs array_map, array_filter
use v5.38; my @numbers = (1, 2, 3, 4, 5, 6); my @doubled = map { $_ * 2 } @numbers; my @evens = grep { $_ % 2 == 0 } @numbers; my @odds = grep { $_ % 2 != 0 } @numbers; say "@doubled"; say "@evens"; say "@odds";
<?php declare(strict_types=1); $numbers = [1, 2, 3, 4, 5, 6]; $doubled = array_map(fn($number) => $number * 2, $numbers); $evens = array_filter($numbers, fn($number) => $number % 2 === 0); $odds = array_filter($numbers, fn($number) => $number % 2 !== 0); echo implode(" ", $doubled) . "\n"; echo implode(" ", array_values($evens)) . "\n"; echo implode(" ", array_values($odds)) . "\n";
PHP's array_map and array_filter correspond directly to Perl's map and grep — both transform and filter with a callback, using PHP's arrow-function syntax where Perl uses an implicit-$_ block. A subtle gotcha: unlike Perl's grep, PHP's array_filter preserves the original keys of surviving elements rather than renumbering them, so the result often needs array_values() to get a clean sequential array back — a direct consequence of PHP's single unified array type not distinguishing "list" from "map" the way Perl's array and hash types do.
List::Util reduce vs array_reduce
use v5.38; use List::Util qw(reduce sum0); my @numbers = (1, 2, 3, 4, 5); my $total = sum0(@numbers); my $product = reduce { $a * $b } @numbers; say $total; say $product;
<?php declare(strict_types=1); $numbers = [1, 2, 3, 4, 5]; $total = array_sum($numbers); $product = array_reduce($numbers, fn($carry, $number) => $carry * $number, 1); echo $total . "\n"; // 15 echo $product . "\n"; // 120
Perl needs the CPAN-distributed core module List::Util to get reduce and sum0 — these are not built into arrays and use the implicit $a/$b package variables rather than named parameters. PHP's array_sum and array_reduce are always-available built-in functions requiring no import, and array_reduce takes an explicit initial value as its third argument (matching the accumulator idiom, just as a named-parameter callback rather than Perl's special variables).
sort and uniq vs sort() and array_unique
use v5.38; use List::Util qw(uniq); my @numbers = (3, 1, 4, 1, 5, 9); my @sorted = sort { $a <=> $b } @numbers; say "@sorted"; say join(" ", reverse @sorted); say join(" ", uniq(@numbers));
<?php declare(strict_types=1); $numbers = [3, 1, 4, 1, 5, 9]; sort($numbers); // sorts and reindexes in place echo implode(" ", $numbers) . "\n"; rsort($numbers); // sorts descending in place echo implode(" ", $numbers) . "\n"; $unique = array_unique([3, 1, 4, 1, 5, 9]); echo implode(" ", array_values($unique)) . "\n";
Both languages default to comparing numbers incorrectly as strings unless told otherwise: Perl's sort needs an explicit { $a <=> $b } comparator for numeric order, and PHP's sort() actually does the right thing for a purely numeric array by default (it uses "smart" comparison), though an explicit SORT_NUMERIC flag or a comparator callback via usort is safer for mixed data. A sharp difference: PHP's sort()/rsort() mutate the array in place and return a boolean, unlike Perl's sort, which always returns a new list leaving the original untouched.
Hashes vs Associative Arrays
%hash becomes an associative array
use v5.38; my %scores = ( alice => 95, bob => 87, carol => 91, ); say $scores{alice}; # element access uses $, not % say scalar(keys %scores); # count
<?php declare(strict_types=1); $scores = [ "alice" => 95, "bob" => 87, "carol" => 91, ]; echo $scores["alice"] . "\n"; echo count($scores) . "\n";
A PHP associative array is not a separate type from a PHP indexed array — it is the exact same underlying ordered hash map, just populated with string keys instead of sequential integers. This means a Perl programmer's %scores and @fruits map onto the same PHP type, using the same [] literal and [key] access syntax; PHP's fat-arrow => for key-value pairs directly echoes Perl's own fat-comma => hash literal syntax, which PHP borrowed the symbol from.
Accessing, setting, and checking existence
use v5.38; my %config = (timeout => 30, retries => 3); $config{timeout} = 60; # set / update say $config{timeout}; say exists $config{missing} ? "yes" : "no"; # existence check say $config{missing} // "default"; # defined-or with a default
<?php declare(strict_types=1); $config = ["timeout" => 30, "retries" => 3]; $config["timeout"] = 60; // set / update echo $config["timeout"] . "\n"; echo (array_key_exists("missing", $config) ? "yes" : "no") . "\n"; // existence check echo ($config["missing"] ?? "default") . "\n"; // null coalescing default
Perl's exists keyword and PHP's array_key_exists() function serve the identical purpose of checking key presence without triggering a warning. PHP's ?? null-coalescing operator (added in PHP 7.0) is a near-exact match for Perl's // defined-or operator — both supply a fallback only when the left side is missing or explicitly undef/null, unlike ||, which would also trigger on any other falsy value.
Iterating over key-value pairs
use v5.38; my %scores = (alice => 95, bob => 87, carol => 91); for my $name (sort keys %scores) { say "$name: $scores{$name}"; } say join(", ", keys %scores); say join(", ", values %scores);
<?php declare(strict_types=1); $scores = ["alice" => 95, "bob" => 87, "carol" => 91]; foreach ($scores as $name => $score) { echo "$name: $score\n"; } echo implode(", ", array_keys($scores)) . "\n"; echo implode(", ", array_values($scores)) . "\n";
PHP's foreach ($array as $key => $value) yields both the key and value in one step, exactly matching the ergonomics a Perl programmer would get from writing a helper to zip keys and values together — Perl's idiomatic loop instead iterates keys %hash alone and looks up each value by key inside the loop body. array_keys() and array_values() map directly onto Perl's keys and values functions.
Merging and default values
use v5.38; my %word_count; my @words = ("apple", "banana", "apple", "cherry", "apple"); $word_count{$_}++ for @words; # autovivifies each key, starting at undef+1 for my $word (sort keys %word_count) { say "$word: $word_count{$word}"; } my %defaults = (timeout => 30, retries => 3, debug => 0); my %overrides = (timeout => 60); my %merged = (%defaults, %overrides); # later keys win say $merged{timeout};
<?php declare(strict_types=1); $wordCount = []; $words = ["apple", "banana", "apple", "cherry", "apple"]; foreach ($words as $word) { $wordCount[$word] = ($wordCount[$word] ?? 0) + 1; // no autovivification — must default explicitly } ksort($wordCount); foreach ($wordCount as $word => $count) { echo "$word: $count\n"; } $defaults = ["timeout" => 30, "retries" => 3, "debug" => false]; $overrides = ["timeout" => 60]; $merged = array_merge($defaults, $overrides); // later keys win echo $merged["timeout"] . "\n";
Perl's autovivification lets $word_count{$_}++ spring a missing key into existence and treat undef as 0 for the increment. PHP does not autovivify a scalar the way Perl does here — incrementing a genuinely missing key raises a warning under strict settings, so the explicit ?? 0 default is idiomatic PHP rather than optional. PHP does autovivify nested array structures on assignment ($data["a"]["b"] = 1; creates both levels), which is closer to Perl's nested-hash autovivification. array_merge mirrors Perl's list-flattening merge idiom, with later keys winning in both.
Context Sensitivity
Scalar vs list context — a Perl-only concept
use v5.38; my @animals = ("cat", "dog", "bird"); my $count = @animals; # scalar context: array evaluates to its length say $count; # 3 my @copy = @animals; # list context: array evaluates to all its elements say "@copy"; my $last_result = (1, 2, 3); # scalar context on a list: LAST element, not count! say $last_result; # 3 — a classic Perl surprise
<?php declare(strict_types=1); $animals = ["cat", "dog", "bird"]; $count = count($animals); // explicit function call — no ambiguity echo $count . "\n"; $copy = $animals; // always the array itself, no context switch echo implode(" ", $copy) . "\n"; // There is no equivalent surprise: PHP has no context sensitivity at all $literalList = [1, 2, 3]; $lastElement = end($literalList); // end() needs a real variable, not a literal echo $lastElement . "\n";
This is the single biggest conceptual difference between the two languages, exactly as it is for Ruby. In Perl, the same expression evaluates differently depending on whether it appears in scalar or list context — assigning an array to a scalar silently gives its length, while assigning a literal list (1, 2, 3) to a scalar gives its last element, not its count. PHP, like PHP's C-family relatives, has no context sensitivity whatsoever: an expression always evaluates to exactly one thing, and a count always requires an explicit count() call. A Perl programmer moving to PHP loses this expressiveness but also every context-related surprise it can cause.
wantarray — no PHP equivalent
use v5.38; sub context_aware { if (wantarray()) { return (1, 2, 3); # caller wants a list } else { return "scalar result"; # caller wants a scalar } } my @list_result = context_aware(); my $scalar_result = context_aware(); say "@list_result"; say $scalar_result;
<?php declare(strict_types=1); // PHP functions always return the same value regardless of call site. // There is no equivalent to wantarray — a function cannot inspect how // its return value will be used. function contextAware(): array { return [1, 2, 3]; } $listResult = contextAware(); $firstOnly = contextAware()[0]; echo implode(" ", $listResult) . "\n"; echo $firstOnly . "\n";
Perl's wantarray lets a subroutine inspect the context it was called in and return a different value depending on whether the caller wants a list, a scalar, or nothing at all (void context). PHP has no equivalent mechanism — a function always returns the same value regardless of how the caller intends to use it, exactly like Ruby. The caller is responsible for extracting what it needs (contextAware()[0] for the first element), rather than the callee adapting its return shape.
Forcing a count — always explicit
use v5.38; my @items = (10, 20, 30, 40); say scalar(@items); # explicit count via the scalar() function say 0 + @items; # numeric context also forces scalar say "There are " . @items . " items"; # string concatenation, too — forces scalar
<?php declare(strict_types=1); $items = [10, 20, 30, 40]; echo count($items) . "\n"; // always call count() explicitly echo "There are " . count($items) . " items\n"; // no implicit context conversion — count() is required
Perl offers several implicit ways to coerce an array into its count — numeric context, string concatenation, or the explicit scalar() function all work. PHP offers none of these shortcuts: concatenating an array directly with . triggers a warning ("Array to string conversion") rather than silently substituting a count, so count($items) must always be called out explicitly, in a string or anywhere else.
Control Flow
if / elsif vs if / elseif
use v5.38; my $temperature = 22; if ($temperature > 30) { say "hot"; } elsif ($temperature > 20) { say "warm"; } elsif ($temperature > 10) { say "cool"; } else { say "cold"; }
<?php declare(strict_types=1); $temperature = 22; if ($temperature > 30) { echo "hot\n"; } elseif ($temperature > 20) { echo "warm\n"; } elseif ($temperature > 10) { echo "cool\n"; } else { echo "cold\n"; }
PHP's middle branch is spelled elseif as one word (though the two-word else if also works when each branch is its own nested block) — a small spelling difference from Perl's elsif, which drops the "e". Everything else about the construct is identical: parenthesized conditions, curly-brace bodies, and the same short-circuit evaluation order.
Postfix conditionals — a genuine gap
use v5.38; my $authenticated = 0; unless ($authenticated) { say "Please log in"; } # Postfix form say "Please log in" unless $authenticated; say "Welcome!" if $authenticated;
<?php declare(strict_types=1); $authenticated = false; if (!$authenticated) { echo "Please log in\n"; } // PHP has no postfix if/unless and no unless keyword at all — // the condition must always come first, in a full if statement: if (!$authenticated) { echo "Please log in\n"; } if ($authenticated) { echo "Welcome!\n"; }
This is a genuine loss for a Perl programmer: PHP has neither an unless keyword nor a postfix statement-modifier form for if at all. Every conditional guard must be written as a full if (...) { ... } block, even for the simplest one-line guard clause that Perl or Ruby would express as statement unless condition;. This makes short validation-guard code measurably more verbose in PHP than in either Perl or Ruby.
for/foreach over ranges and arrays
use v5.38; for my $number (1..5) { say $number * $number; } foreach my $letter ("a".."e") { print "$letter "; } print "\n";
<?php declare(strict_types=1); foreach (range(1, 5) as $number) { echo ($number * $number) . "\n"; } foreach (range("a", "e") as $letter) { echo "$letter "; } echo "\n";
PHP's range() function generates the same kind of numeric or character sequence Perl's 1..5 and "a".."e" range operator produces, including character ranges — but in PHP it is a function call producing a real array up front, not a lazy first-class Range object the way Ruby has, nor Perl's built-in operator syntax. foreach is the idiomatic PHP iteration keyword and maps directly onto Perl's foreach (Perl's for and foreach are interchangeable keywords for this form; PHP reserves for strictly for the C-style three-clause loop).
while / do-while
use v5.38; my $counter = 0; while ($counter < 5) { say $counter; $counter++; } # Perl has no "until the condition is false" C-style do-while, # but does have a postfix "do { } while" form: $counter = 0; do { say "runs once: $counter"; $counter++; } while ($counter < 0);
<?php declare(strict_types=1); $counter = 0; while ($counter < 5) { echo $counter . "\n"; $counter++; } // do-while guarantees the body runs at least once $counter = 0; do { echo "runs once: $counter\n"; $counter++; } while ($counter < 0);
Both languages have an identical while loop and a do { ... } while (...) form that guarantees at least one execution before checking the condition — this is one of the more directly portable control-flow constructs between Perl and PHP, syntax and semantics both. PHP has no built-in until keyword (unlike Perl and Ruby), so "loop while false" always needs a negated while condition instead.
given/when workaround vs match expression
use v5.38; my $direction = "north"; for ($direction) { # topicalize $_ — the "poor man's given/when" since Perl deprecated given/when if (/^north$/) { say "Going up"; last; } if (/^south$/) { say "Going down"; last; } if (/^east$/ || /^west$/) { say "Going sideways"; last; } say "Unknown"; }
<?php declare(strict_types=1); $direction = "north"; $result = match ($direction) { "north" => "Going up", "south" => "Going down", "east", "west" => "Going sideways", // comma-separated alternatives default => "Unknown", }; echo $result . "\n";
Perl once had an experimental given/when construct but removed it due to design problems with smart matching, leaving Perl programmers to fake it with a for ($topic) { if (...) { ...; last } } loop. PHP 8.0's match expression is a clean, permanent replacement with real advantages over Perl's workaround: it is an expression that returns a value directly (no intermediate variable assignment inside the loop needed), it uses strict (===) comparison with no implicit type coercion, and an unmatched value with no default arm throws UnhandledMatchError rather than silently falling through.
Subroutines & Functions
sub vs function — named parameters vs @_
use v5.38; sub square { my ($number) = @_; # unpack the args array manually return $number * $number; } sub greet { my ($name) = @_; return "Hello, $name!"; } say square(7); say greet("Alice");
<?php declare(strict_types=1); function square(int $number): int { return $number * $number; } function greet(string $name): string { return "Hello, $name!"; } echo square(7) . "\n"; echo greet("Alice") . "\n";
Perl subroutines receive all arguments bundled into the single array @_ and must manually unpack them with my ($number) = @_; — there is no way to name parameters directly in the widely-used, portable sub syntax. PHP functions declare named parameters directly in the signature, optionally with type hints (int $number), so there is no unpacking step at all — matching the ergonomics Perl programmers will recognize as a real upgrade from manual @_ handling.
Default parameter values
use v5.38; sub greet { my ($name, $greeting) = @_; $greeting //= "Hello"; # defined-or assigns the default return "$greeting, $name!"; } say greet("Alice"); say greet("Bob", "Hi");
<?php declare(strict_types=1); function greet(string $name, string $greeting = "Hello"): string { return "$greeting, $name!"; } echo greet("Alice") . "\n"; echo greet("Bob", "Hi") . "\n";
Perl has no default-parameter syntax in a sub signature — the idiomatic approach is unpacking positionally from @_ and using //= to fill in a default afterward. PHP supports default values directly in the function signature (string $greeting = "Hello"), evaluated once at the point the function is declared for simple literal defaults — much closer to Ruby's ergonomics here than to Perl's.
Simulated keyword args vs real named arguments
use v5.38; sub create_user { my (%args) = @_; # unpack into a hash to simulate keyword args my $name = $args{name}; my $age = $args{age}; my $city = $args{city} // "Unknown"; say "$name, age $age, from $city"; } create_user(name => "Alice", age => 30, city => "Paris"); create_user(age => 25, name => "Bob"); # order doesn't matter
<?php declare(strict_types=1); function createUser(string $name, int $age, string $city = "Unknown"): void { echo "$name, age $age, from $city\n"; } createUser(name: "Alice", age: 30, city: "Paris"); createUser(age: 25, name: "Bob"); // order doesn't matter
Perl simulates keyword arguments by unpacking a flat key-value list into a hash inside the subroutine (my (%args) = @_;) — there is no way for the signature itself to declare which keywords exist or provide defaults. PHP 8.0's named arguments (name: "Alice") are a real language feature layered directly onto ordinary positional parameters: any parameter can be called by name in any order, required parameters with no default must be supplied, and defaults are declared in the signature — no manual hash unpacking needed, much like Ruby's keyword arguments.
Variadic arguments — @_ slicing vs ...$args
use v5.38; sub print_all { my (@items) = @_; say $_ for @items; } sub describe { my (%attributes) = @_; for my $key (sort keys %attributes) { say "$key: $attributes{$key}"; } } print_all(10, 20, 30); describe(name => "Alice", age => 30);
<?php declare(strict_types=1); function printAll(int ...$items): void { foreach ($items as $item) { echo $item . "\n"; } } function describe(string $name, int $age): void { echo "name: $name\n"; echo "age: $age\n"; } printAll(10, 20, 30); describe(name: "Alice", age: 30); // named arguments, not a variadic hash
PHP's ...$items splat parameter (the "spread"/"variadic" operator) collects any number of trailing positional arguments into a plain indexed array, directly matching Perl's my (@items) = @_; idiom for "gather everything into an array." PHP has no equivalent to Perl's hash-unpacking variadic pattern for arbitrary named arguments, though — real named arguments (shown in describe) cover that use case instead, requiring each parameter to be declared up front rather than accepted as an open-ended set of keys.
Return type declarations — a Perl gap
use v5.38; sub double_it { my ($number) = @_; return $number * 2; } sub triple_it { my ($number) = @_; return $number * 3; } say double_it(5); say triple_it(5);
<?php declare(strict_types=1); function doubleIt(int $number): int { return $number * 2; } function tripleIt(int $number): int { return $number * 3; } echo doubleIt(5) . "\n"; echo tripleIt(5) . "\n";
Perl has no return-type declaration syntax at all — a sub can return any value of any shape, and the caller has no compile-time guarantee about what comes back. PHP's : int return-type hint (available since PHP 7.0) is checked at call time under declare(strict_types=1);, so a function declared to return int that tries to return a string raises a TypeError — real type safety a Perl programmer would otherwise need a CPAN module like Function::Parameters or Type::Tiny to approximate.
Blocks & Closures
Anonymous subs vs anonymous functions
use v5.38; my @names = ("Alice", "Bob", "Carol"); # Anonymous sub passed explicitly as a code reference for my $name (@names) { (sub { say "Hello, $_[0]!" })->($name); } # More idiomatically, just use a for loop directly: say "Hello, $_!" for @names;
<?php declare(strict_types=1); $names = ["Alice", "Bob", "Carol"]; // Anonymous function passed explicitly, called immediately foreach ($names as $name) { (function (string $name) { echo "Hello, $name!\n"; })($name); } // More idiomatically, just use a foreach loop directly: foreach ($names as $name) { echo "Hello, $name!\n"; }
Perl's anonymous subroutine (sub { ... }) must be explicitly created and invoked with an arrow (->()), just like PHP's anonymous function, which is invoked by calling the variable it is stored in directly ($callback($name)) or, immediately, with a parenthesized call right after the definition. Neither language has Ruby's dedicated trailing-block syntax (each do |name| ... end) built into every iteration method — both PHP's array_map/foreach and Perl's map/for take an explicit callback or use dedicated loop syntax rather than a lightweight block-passing convention.
Explicit callbacks in both languages
use v5.38; sub repeat { my ($times, $callback) = @_; for my $count (0 .. $times - 1) { $callback->($count); } } repeat(3, sub { my ($count) = @_; say "Step $count"; });
<?php declare(strict_types=1); function repeatTimes(int $times, callable $callback): void { for ($count = 0; $count < $times; $count++) { $callback($count); } } repeatTimes(3, function (int $count) { echo "Step $count\n"; });
Both languages require a callback-taking function to accept the callable as an explicit parameter — neither has Ruby's implicit yield mechanism for attaching a block with no parameter declared in the signature. PHP's callable type hint documents the parameter's intent (any anonymous function, named function string, or invokable object satisfies it), while Perl typically just documents "expects a code reference" in a comment, since Perl's type system has no equivalent hint for code references.
Automatic closure capture vs explicit use
use v5.38; my $multiplier = 3; # Perl closures capture the outer $multiplier automatically: my $triple = sub { my ($number) = @_; return $number * $multiplier; }; say $triple->(5); # 15 $multiplier = 10; say $triple->(5); # 50 — sees the updated value, because Perl captures by reference
<?php declare(strict_types=1); $multiplier = 3; // PHP anonymous functions do NOT auto-capture outer variables — // an explicit use() clause is required: $triple = function (int $number) use ($multiplier): int { return $number * $multiplier; }; echo $triple(5) . "\n"; // 15 $multiplier = 10; // Captured by VALUE at definition time — still sees 3, not 10: echo $triple(5) . "\n"; // 15, not 50 // Capture by REFERENCE with &, to see later updates: $tripleRef = function (int $number) use (&$multiplier): int { return $number * $multiplier; }; echo $tripleRef(5) . "\n"; // 50 — sees the updated $multiplier
This is a real behavioral difference worth calling out for a Perl programmer: Perl closures capture every enclosing lexical variable automatically and always by reference, so a later change to $multiplier outside the closure is visible inside it. PHP anonymous functions capture nothing automatically — every outer variable a closure needs must be listed explicitly in a use (...) clause, and by default that capture is by value at definition time (a later outer change is invisible inside), unless the variable is prefixed with & in the use clause to opt into capture-by-reference, matching Perl's default behavior.
Arrow functions — automatic capture by value
use v5.38; my $doubler = sub { my ($number) = @_; return $number * 2; }; my $tripler = sub { my ($number) = @_; return $number * 3; }; say $doubler->(5); say $tripler->(5); # Storing code references in a hash my %operations = ( double => sub { $_[0] * 2 }, square => sub { $_[0] ** 2 }, ); say $operations{double}->(5);
<?php declare(strict_types=1); $doubler = fn(int $number): int => $number * 2; $tripler = fn(int $number): int => $number * 3; echo $doubler(5) . "\n"; echo $tripler(5) . "\n"; // Storing closures in an array $operations = [ "double" => fn($number) => $number * 2, "square" => fn($number) => $number ** 2, ]; echo $operations["double"](5) . "\n";
PHP 7.4's arrow function syntax (fn($number) => expression) is the closest PHP equivalent to Perl's single-expression anonymous sub, and — unlike PHP's longer function closure syntax — it does automatically capture outer variables by value, with no use clause needed, closer to Perl's automatic capture (though still by value, not by reference, so it still cannot see later updates to $multiplier the way a Perl closure would). Arrow functions are limited to a single expression body with an implicit return, much like Ruby's stabby lambda.
Closures capture lexical scope in both languages
use v5.38; sub make_counter { my $count = 0; return sub { return ++$count; }; } my $counter = make_counter(); say $counter->(); # 1 say $counter->(); # 2 say $counter->(); # 3
<?php declare(strict_types=1); function makeCounter(): callable { $count = 0; return function () use (&$count): int { return ++$count; }; } $counter = makeCounter(); echo $counter() . "\n"; // 1 echo $counter() . "\n"; // 2 echo $counter() . "\n"; // 3
Both languages support a true closure-factory pattern: a function returns an anonymous function that keeps its own private state across calls, and each call to the factory produces an independent closure with its own copy of that state. The key difference is explicitness: Perl's closure captures $count automatically by reference with no extra syntax, while PHP requires the by-reference use (&$count) clause spelled out — without the &, each call to the returned function would see a fresh copy of $count starting at 0 every time, since PHP's default capture is by value.
References
Explicit \ references vs implicit object references
use v5.38; my $name = "Alice"; my $ref = \$name; # backslash creates a reference say $$ref; # $$ dereferences say ref($ref); # "SCALAR" — what type of reference $$ref = "Bob"; # modify via reference say $name; # "Bob" — original is changed
<?php declare(strict_types=1); // PHP arrays and scalars are VALUES by default — assignment copies: $numbers = [1, 2, 3]; $copy = $numbers; $copy[] = 4; echo implode(",", $numbers) . "\n"; // 1,2,3 — original untouched, unlike Perl aliasing // PHP objects, by contrast, are always implicit references (like Ruby): class Counter { public int $value = 0; } $counter = new Counter(); $alias = $counter; // both variables point to the same object $alias->value = 5; echo $counter->value . "\n"; // 5 — original IS changed, because objects are references
This is a genuinely three-way contrast worth pausing on. Perl requires an explicit backslash to create any reference and an explicit sigil to dereference it ($$ref). Ruby has no distinction at all: every value except a few immediates is always an implicit reference, so assigning one variable to another always shares the underlying object. PHP sits in between: arrays and scalars are copied by value on assignment (no explicit reference needed, and no accidental aliasing — a real convenience over Perl's explicit-everything model for these types), while objects are always implicit references, exactly like Ruby. A Perl programmer needs to remember which PHP behavior applies to which kind of value.
PHP's explicit & — Perl-style, scalar-only
use v5.38; sub increment { my ($ref) = @_; # receives a reference explicitly $$ref++; } my $count = 5; increment(\$count); # must pass a reference explicitly say $count; # 6
<?php declare(strict_types=1); function increment(int &$number): void { // & means "pass by reference" $number++; } $count = 5; increment($count); // no explicit reference syntax needed at the call site echo $count . "\n"; // 6
PHP has one place where it genuinely resembles Perl's explicit-reference model rather than Ruby's implicit one: a function parameter can be declared by reference with &$parameter, letting the function mutate the caller's original scalar — something Ruby cannot do for a plain number or string at all (Ruby's integers are immutable and reassignment inside a method never affects the caller's variable). Where Perl requires the caller to pass an explicit \$count reference, PHP's reference parameter is transparent at the call site — the & lives only in the function signature, not in each call — but the effect (mutating a scalar the caller can see) is the same feature.
Anonymous refs become plain array literals
use v5.38; my $array_ref = [1, 2, 3]; # anonymous array reference my $hash_ref = {name => "Alice", age => 30}; # anonymous hash reference say $array_ref->[1]; say $hash_ref->{name}; push @$array_ref, 4; say "@$array_ref";
<?php declare(strict_types=1); $array = [1, 2, 3]; $hash = ["name" => "Alice", "age" => 30]; echo $array[1] . "\n"; echo $hash["name"] . "\n"; $array[] = 4; echo implode(" ", $array) . "\n";
Perl's [...] and {...} literals create anonymous references that must always be accessed through the arrow operator ($array_ref->[1]) or an explicit dereferencing sigil (@$array_ref). PHP's [...] literal creates the array value directly, with plain bracket access ($array[1]) and no arrow or dereferencing sigil ever needed — much closer to Ruby's uniform object model here than to Perl's reference ceremony, since a PHP array is a value in its own right rather than something that needs to be dereferenced through a pointer-like reference.
Regular Expressions
Native =~ vs wrapped preg_* functions
use v5.38; my $email = 'user@example.com'; if ($email =~ /\@.*\./) { say "Looks like an email"; } my $text = "The quick brown fox"; if ($text =~ /(\w+)\s+(\w+)$/) { say "Last two words: $1 $2"; }
<?php declare(strict_types=1); $email = "user@example.com"; if (preg_match('/\@.*\./', $email)) { echo "Looks like an email\n"; } $text = "The quick brown fox"; if (preg_match('/(\w+)\s+(\w+)$/', $text, $matches)) { echo "Last two words: {$matches[1]} {$matches[2]}\n"; }
Both languages' regex engines are PCRE-descended and share nearly identical pattern syntax, character classes, and quantifiers — a Perl programmer's pattern-writing intuition transfers directly. The calling convention differs, though: Perl builds regex matching into the language itself via the =~ binding operator and true regex literals, while PHP wraps the (literal, C-library) PCRE engine in ordinary functions — preg_match, preg_match_all, preg_replace — that take the pattern as a delimited string argument (commonly /pattern/, though any matching delimiter pair works) and write captured groups into a variable passed by reference rather than into $1/$2 globals.
s/// substitution vs preg_replace
use v5.38; my $text = "The cat sat on the mat"; (my $changed = $text) =~ s/cat/dog/; say $changed; my $message = "hello world hello"; (my $all = $message) =~ s/hello/hi/g; # /g = all occurrences say $all;
<?php declare(strict_types=1); $text = "The cat sat on the mat"; $changed = preg_replace('/cat/', 'dog', $text, 1); // limit 1 = first occurrence only echo $changed . "\n"; $message = "hello world hello"; $all = preg_replace('/hello/', 'hi', $message); // no limit = all occurrences (like Perl's /g) echo $all . "\n";
Perl's s/pattern/replacement/ needs the /g flag to replace every occurrence rather than just the first, and always returns a new copy in this non-destructive form (the destructive s/// without a copy mutates the original). PHP's preg_replace always returns a new string too, but flips the default: with no limit argument it replaces all occurrences (matching Perl's /g behavior by default), and passing an explicit limit of 1 is what restricts it to the first match only — the opposite default from Perl's bare s///.
Named captures — nearly identical syntax
use v5.38; my $date = "2026-04-22"; if ($date =~ /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/) { say "Year: $+{year}"; say "Month: $+{month}"; say "Day: $+{day}"; }
<?php declare(strict_types=1); $date = "2026-04-22"; if (preg_match('/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/', $date, $matches)) { echo "Year: {$matches['year']}\n"; echo "Month: {$matches['month']}\n"; echo "Day: {$matches['day']}\n"; }
Named captures use the identical PCRE (?<name>...) syntax in both languages, since both sit directly atop PCRE-compatible engines. The retrieval mechanism differs from Perl's special %+ hash: PHP's preg_match writes both the numbered and named captures into the same $matches array passed by reference, so $matches['year'] and $matches[1] refer to the same captured value side by side.
Global match — preg_match_all
use v5.38; my $text = 'The price is $10 and $25 and $100'; my @prices = ($text =~ /\$(\d+)/g); # list context collects all matches say join(", ", @prices); my $html = "<b>bold</b> and <i>italic</i>"; while ($html =~ /<(\w+)>/g) { say "Tag: $1"; }
<?php declare(strict_types=1); $text = 'The price is $10 and $25 and $100'; preg_match_all('/\$(\d+)/', $text, $matches); echo implode(", ", $matches[1]) . "\n"; $html = "<b>bold</b> and <i>italic</i>"; preg_match_all('/<(\w+)>/', $html, $tagMatches); foreach ($tagMatches[1] as $tag) { echo "Tag: $tag\n"; }
Perl relies on context sensitivity for "find every match": the same =~ /pattern/g returns all matches in list context, or advances an internal position pointer one match at a time in scalar context inside a while loop. PHP has one dedicated function instead — preg_match_all — which always populates every match into the $matches array in one call, with $matches[0] holding the full matches and $matches[1], $matches[2], and so on holding each capture group across all matches — no context-dependent behavior to reason about, matching PHP's consistent no-context-sensitivity story throughout.
Regex modifiers — same letters, same meaning
use v5.38; my $text = "Hello World"; say "case-insensitive" if $text =~ /hello/i; # /x allows whitespace and comments in the pattern my $date = "2026-04-22"; if ($date =~ / (\d{4}) # year - (\d{2}) # month - (\d{2}) # day /x) { say "Year: $1, Month: $2, Day: $3"; }
<?php declare(strict_types=1); $text = "Hello World"; if (preg_match('/hello/i', $text)) { echo "case-insensitive\n"; } // /x allows whitespace and comments in the pattern — same as Perl $date = "2026-04-22"; if (preg_match('/ (\d{4}) # year - (\d{2}) # month - (\d{2}) # day /x', $date, $matches)) { echo "Year: {$matches[1]}, Month: {$matches[2]}, Day: {$matches[3]}\n"; }
PHP uses the same PCRE modifier letters Perl does, in the same position after the closing delimiter — i for case-insensitive, m for multiline anchors, x for extended mode. This is unsurprising, since both languages' regex engines are literally built on (or, in PHP's case, directly wrap) the same PCRE library — a Perl programmer's regex modifier intuition transfers to PHP with no translation needed at all, more directly than almost any other piece of syntax covered in this cheatsheet.
OOP
package + bless vs native class
use v5.38; package Animal; sub new { my ($class, %args) = @_; return bless { name => $args{name}, sound => $args{sound} // "...", }, $class; } sub speak { my $self = shift; say "$self->{name} says $self->{sound}"; } package main; my $dog = Animal->new(name => "Rex", sound => "Woof"); $dog->speak;
<?php declare(strict_types=1); class Animal { public function __construct( private string $name, private string $sound = "...", ) {} public function speak(): void { echo "{$this->name} says {$this->sound}\n"; } } $dog = new Animal(name: "Rex", sound: "Woof"); $dog->speak();
This is where a Perl programmer will feel real envy. Perl OOP is built manually from two separate primitives: a package to hold the namespace, and bless to associate a plain hash reference with that package so it is recognizable as an object — every constructor is hand-written. PHP has a genuine native class keyword with constructor property promotion (PHP 8.0+): declaring a constructor parameter with a visibility modifier like private string $name both declares the property and assigns it from the argument in one line, collapsing what would be several lines of bless-and-assign Perl into a single parameter declaration.
Hand-written accessors vs property visibility
use v5.38; package Person; sub new { my ($class, %args) = @_; return bless { name => $args{name}, age => $args{age} }, $class; } sub name { $_[0]->{name} } # read-only getter sub age { # combined getter/setter my $self = shift; $self->{age} = shift if @_; return $self->{age}; } package main; my $person = Person->new(name => "Alice", age => 30); say $person->name; $person->age(31); say $person->age;
<?php declare(strict_types=1); class Person { public function __construct( public readonly string $name, // read-only after construction public int $age, // freely mutable — direct property access ) {} } $person = new Person(name: "Alice", age: 30); echo $person->name . "\n"; $person->age = 31; // direct property assignment, no setter method needed echo $person->age . "\n";
Perl has no built-in accessor generation at all — every getter and setter is hand-written, often using the combined pattern shown here where the same method acts as both getter and setter depending on whether an argument was passed. PHP's public properties need no accessor methods whatsoever: $person->name and $person->age = 31 read and write the property directly, and PHP 8.1's readonly modifier enforces Perl-style "settable once, in the constructor, then immutable" semantics with a single keyword — no hand-written guard logic needed. This is more direct than even Ruby's attr_accessor, which still generates method calls under the hood; PHP's public properties are true direct field access.
Inheritance — @ISA vs extends
use v5.38; package Animal; sub new { bless { name => $_[1] }, $_[0] } sub name { $_[0]->{name} } sub speak { say $_[0]->{name} . " makes a sound" } package Dog; our @ISA = ('Animal'); # declare inheritance sub speak { my $self = shift; say $self->name . " barks"; } package main; my $dog = Dog->new("Rex"); $dog->speak; say $dog->isa("Animal") ? "is an Animal" : "not an Animal";
<?php declare(strict_types=1); class Animal { public function __construct(protected string $name) {} public function speak(): string { return "{$this->name} makes a sound"; } } class Dog extends Animal { public function speak(): string { return "{$this->name} barks"; } } $dog = new Dog("Rex"); echo $dog->speak() . "\n"; echo ($dog instanceof Animal ? "is an Animal" : "not an Animal") . "\n";
Perl declares inheritance by populating the special array @ISA with parent package names (or, in modern Perl, the more readable use parent 'Animal'; pragma). PHP uses the extends keyword directly in the class definition — a single-inheritance declaration built into the syntax, closer to Ruby's < than to Perl's array-based mechanism. PHP's instanceof operator replaces Perl's isa method call for checking class ancestry, and both languages support calling the parent's overridden method — Perl's SUPER::method, PHP's parent::method().
use overload vs magic methods
use v5.38; # Perl has no polymorphism check beyond isa/can, no built-in # equality override protocol, and no operator overloading without # the separate "use overload" pragma: package Money; use overload '+' => \&add, '""' => \&stringify; sub new { bless { cents => $_[1] }, $_[0] } sub add { Money->new($_[0]->{cents} + $_[1]->{cents}) } sub stringify { sprintf('$%.2f', $_[0]->{cents} / 100) } package main; my $total = Money->new(150) + Money->new(250); say "$total";
<?php declare(strict_types=1); class Money { public function __construct(private int $cents) {} public function add(Money $other): Money { return new Money($this->cents + $other->cents); } public function __toString(): string { return sprintf('$%.2f', $this->cents / 100); } } $total = (new Money(150))->add(new Money(250)); echo $total . "\n"; // __toString is called implicitly here
PHP has no true operator-overloading protocol for a custom class — unlike Perl's use overload pragma, there is no way to make PHP's built-in + operator invoke a method on a custom object; addition between two Money objects must be an explicit method call like add() rather than $a + $b. Where the languages do align is stringification: PHP's __toString() magic method plays exactly the role Perl's '""' overload entry plays, and PHP needs no separate pragma to opt in — any class simply defining __toString() is automatically used wherever the object is treated as a string.
Error Handling
die/eval vs try/catch
use v5.38; eval { die "Something went wrong\n"; say "This won't print"; }; if ($@) { say "Caught: $@"; }
<?php declare(strict_types=1); try { throw new Exception("Something went wrong"); echo "This won't print\n"; } catch (Exception $error) { echo "Caught: {$error->getMessage()}\n"; }
die is Perl's throw; a bare eval { } block is the try-block equivalent of PHP's try/catch. PHP's error-handling ergonomics land close to Ruby's and are a real improvement over Perl's global-variable-based approach: the caught exception object is bound directly to a local variable at the point of the catch clause (catch (Exception $error)), with no equivalent of Perl's easy-to-forget or easy-to-accidentally-clear $@ global to check manually.
warn — different meanings in each language
use v5.38; warn "Something looks suspicious\n"; # prints to STDERR say "Execution continues"; my $value = -1; warn "Negative value: $value\n" if $value < 0; say "Processing: $value";
<?php declare(strict_types=1); // PHP's warn() is unrelated — trigger_error() is the closer analog: trigger_error("Something looks suspicious", E_USER_WARNING); echo "Execution continues\n"; $value = -1; if ($value < 0) { trigger_error("Negative value: $value", E_USER_WARNING); } echo "Processing: $value\n";
A naming trap for a Perl programmer: PHP has no warn() function that behaves like Perl's — PHP's error-reporting equivalent is trigger_error(), which raises a PHP-level warning (visible depending on the configured error-reporting level and handler) without halting execution, matching Perl's warn in spirit if not in name. PHP also has real E_WARNING/E_NOTICE diagnostic levels the runtime itself emits for things like undefined-array-key access — closer to Perl's own built-in warnings pragma output than to a user-triggered message.
Exception objects — bless vs Exception hierarchy
use v5.38; package AppError; sub new { my ($class, %args) = @_; return bless { message => $args{message}, code => $args{code} }, $class; } sub message { $_[0]->{message} } sub code { $_[0]->{code} } package main; eval { die AppError->new(message => "Not found", code => 404); }; if (my $error = $@) { if (ref($error) && $error->isa('AppError')) { say "Error " . $error->code . ": " . $error->message; } }
<?php declare(strict_types=1); class AppError extends Exception { public function __construct(string $message, private int $statusCode) { parent::__construct($message); } public function getStatusCode(): int { return $this->statusCode; } } try { throw new AppError("Not found", 404); } catch (AppError $error) { echo "Error {$error->getStatusCode()}: {$error->getMessage()}\n"; }
Perl can die with a blessed object instead of a plain string, but the caller must manually check ref($error) && $error->isa('AppError') to determine what was thrown. PHP has a dedicated exception hierarchy rooted at Throwable (with Exception and Error as the two main branches), and catch (AppError $error) filters by class directly in the catch clause — no manual instanceof check needed, and multiple catch blocks can handle different exception types in sequence, tried in order, exactly like Ruby's multiple rescue clauses.
Cleanup — local $@ save/restore vs finally
use v5.38; sub process_with_cleanup { say "Starting work"; eval { die "failure during work\n"; }; my $error = $@; # save before it can be clobbered say "Cleanup always runs"; die $error if $error; } eval { process_with_cleanup() }; say "Caught: $@" if $@;
<?php declare(strict_types=1); function processWithCleanup(): void { echo "Starting work\n"; try { throw new Exception("failure during work"); } finally { echo "Cleanup always runs\n"; } } try { processWithCleanup(); } catch (Exception $error) { echo "Caught: {$error->getMessage()}\n"; }
Perl has no dedicated "always run this" block keyword — guaranteed cleanup requires a nested eval with careful manual saving of $@ before a subsequent statement can overwrite it, or a CPAN module like Scope::Guard. PHP's finally clause is a first-class part of try/catch/finally, running whether the try block succeeded, threw, or even used return early — no manual state juggling required, matching Ruby's ensure almost exactly in both placement and guarantee.
File I/O
Writing a file — open/close vs file_put_contents
use v5.38; # Cannot actually run in this sandboxed environment (no filesystem access). open(my $filehandle, '>', 'output.txt') or die "Could not open output.txt: $!"; print $filehandle "Hello, file!\n"; close($filehandle); say "Wrote output.txt";
<?php declare(strict_types=1); // Cannot actually run in this sandboxed environment (no filesystem access). file_put_contents("output.txt", "Hello, file!\n"); echo "Wrote output.txt\n";
Perl's three-argument open takes a mode string ('>' for write, '>>' for append, '<' for read) and a lexical filehandle, and requires an explicit close when finished. PHP's file_put_contents is a one-line convenience function for the common "write this whole string to this path" case, directly analogous to Ruby's File.write; for finer control PHP's fopen/fwrite/fclose trio mirrors Perl's explicit-handle style closely, including the same requirement to close the handle manually. Both examples are marked norun because the sandboxed execution backends for both languages have no real filesystem.
Reading a file — line by line vs slurping
use v5.38; # Cannot actually run in this sandboxed environment (no filesystem access). open(my $filehandle, '<', 'input.txt') or die "Could not open input.txt: $!"; while (my $line = <$filehandle>) { chomp $line; say "Line: $line"; } close($filehandle); # Slurp the whole file at once open(my $slurp_handle, '<', 'input.txt') or die $!; local $/; # undef the input record separator to slurp my $contents = <$slurp_handle>; close($slurp_handle); say length($contents);
<?php declare(strict_types=1); // Cannot actually run in this sandboxed environment (no filesystem access). $lines = file("input.txt", FILE_IGNORE_NEW_LINES); foreach ($lines as $line) { echo "Line: $line\n"; } // Read the whole file at once $contents = file_get_contents("input.txt"); echo strlen($contents) . "\n";
Perl reads a file line by line with the diamond operator <$filehandle> inside a while loop, and slurping the whole file requires localizing $/ (the input record separator) to undef — an obscure special-variable trick. PHP has two direct, purpose-built functions instead: file(), which reads the whole file into an array of lines in one call (the FILE_IGNORE_NEW_LINES flag strips trailing newlines, similar to Perl's chomp), and file_get_contents() to slurp the entire contents into one string — no special-variable manipulation needed for either case, matching Ruby's File.foreach/File.read pair closely.
Special Variables
$_ the default topic vs no implicit topic
use v5.38; my @words = ("apple", "banana", "cherry"); for (@words) { # $_ is implicitly set to each element say uc; # uc() with no argument operates on $_ } say "found" if grep { /an/ } @words; # $_ implicit inside grep's block too
<?php declare(strict_types=1); $words = ["apple", "banana", "cherry"]; foreach ($words as $word) { // the loop variable must be named explicitly echo strtoupper($word) . "\n"; } $found = array_filter($words, fn($word) => str_contains($word, "an")); echo (count($found) > 0 ? "found" : "not found") . "\n";
Perl's $_ is a pervasive implicit topic variable: many built-in functions (uc, print, chomp) operate on it with no argument, and constructs like for, map, and grep set it automatically. PHP has no equivalent implicit variable at all — every foreach loop variable and every closure parameter must be named explicitly ($word), which is more verbose for quick one-off transformations but leaves no ambiguity about which value a function call is operating on, exactly matching Ruby's stance on the same trade-off.
@ARGV / %ENV vs $argv / $_ENV
use v5.38; # Command-line arguments say "Arguments: @ARGV"; say "First argument: $ARGV[0]" if @ARGV; # Environment variables say $ENV{HOME} // "no HOME set"; $ENV{MY_VAR} = "custom value"; # setting also works say $ENV{MY_VAR};
<?php declare(strict_types=1); // Command-line arguments (CLI SAPI only) echo "Arguments: " . implode(" ", array_slice($argv, 1)) . "\n"; echo "First argument: " . ($argv[1] ?? "none") . "\n"; // $argv[0] is the script name itself // Environment variables echo (getenv("HOME") ?: "no HOME set") . "\n"; putenv("MY_VAR=custom value"); // setting also works echo getenv("MY_VAR") . "\n";
Perl's @ARGV excludes the script name itself ($ARGV[0] is the first real argument), while PHP's $argv array includes the script name as element 0, so the first real argument is $argv[1] — a common off-by-one trap when porting command-line scripts. Environment access differs too: Perl's %ENV is a plain hash read and written with ordinary hash syntax, while PHP requires the getenv()/putenv() function pair (the $_ENV superglobal also exists but is populated only when the variables_order ini setting includes E, so getenv() is the more portable choice).
Process and script identity
use v5.38; say "Script name: $0"; say "Process ID: $$"; say "Perl version: $]" if 0; # would print numeric version if uncommented
<?php declare(strict_types=1); echo "Script name: " . basename(__FILE__) . "\n"; echo "Process ID: " . getmypid() . "\n"; echo "PHP version: " . PHP_VERSION . "\n";
Perl exposes process identity through terse punctuation variables: $0 for the script name, $$ for the process ID. PHP provides the same information through self-describing names instead: the magic constant __FILE__ for the current file's path, getmypid() for the process ID, and PHP_VERSION for the interpreter version — closer to Ruby's $PROGRAM_NAME/Process.pid/RUBY_VERSION naming style than to Perl's punctuation-heavy variables.
CPAN vs Composer
CPAN vs Composer/Packagist
# Not runnable — package manager commands, not code. # Install a module from CPAN: # cpan install JSON::PP # Or with the modern cpanm: # cpanm JSON::PP # # Use it in a script: use JSON::PP; my $json = encode_json({ name => "Alice", age => 30 }); print "$json\n";
<?php declare(strict_types=1); // Not runnable — package manager commands, not code. // Install a package from Packagist via Composer: // composer require monolog/monolog // // Composer generates an autoloader — require it once: // require __DIR__ . '/vendor/autoload.php'; // // JSON support is built into PHP core — no package needed: $json = json_encode(["name" => "Alice", "age" => 30]); echo "$json\n";
CPAN (the Comprehensive Perl Archive Network) predates Composer and Packagist by roughly fifteen years and pioneered the idea of a centralized, versioned package archive with a consistent module-distribution format. Composer plays the equivalent role for PHP, pulling packages from Packagist and — critically — generating a PSR-4 autoloader file that resolves class names to file paths automatically, whereas Perl programmers write explicit use lib statements or rely on the module's own use declaration to load each dependency by name. Like Ruby, PHP ships JSON encoding in its standard library with no external dependency needed, where Perl reaches for a CPAN module (JSON::PP or similar) even for this common task.