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.