PHP to Perl#

You know PHP. You reach for $arr['key'] without thinking, foreach ($items as $item) is muscle memory, and you join strings with . because of course you do. Perl and PHP are close cousins: both are sigil-using, pragmatic, get-the-job-done languages, and PHP borrowed heavily from Perl in its early days. This guide maps your PHP habits onto Perl so you can be productive in pperl in an afternoon.

Two shocks land in the first five minutes, so meet them now:

  • The $ sigil is not the whole story. In PHP every variable is $x, always. In Perl the sigil changes with how you access the value: @a is the whole array but $a[0] is one element; %h is the whole hash but $h{k} is one value. The dollar you already know marks a single scalar; @ and % are new and section one is about nothing else.

  • PHP’s one array becomes Perl’s two types. A PHP array is an ordered map that does duty as both list and dictionary. Perl splits that into @array (integer-indexed) and %hash (string-keyed), and you choose up front which one you have. This split drives the Arrays and Hashes sections.

Every example here runs on pperl, a Rust reimplementation of Perl 5.42. Each Perl snippet was executed on pperl; the # ... comments show its real output. Start your scripts with:

use v5.42;

That one line turns on strict (no accidental globals), warnings, the say built-in (a print with a trailing newline, like PHP’s echo plus PHP_EOL), subroutine signatures, and the modern feature set used throughout this guide.

How to read this#

Variables and sigils#

You already use $, so the first half of Perl’s sigils is free. The new idea is that the sigil marks the access, not the variable. The whole array is @a; one element is $a[0]. The whole hash is %h; one value is $h{k}. PHP keeps $ on everything and varies the brackets ($a[0], $h['k']); Perl varies the sigil instead. my declares a lexically scoped variable, the equivalent of an ordinary local binding.

$name = "Ada";
$langs = ["PHP", "Perl"];
$ages = ["Ada" => 36, "Rasmus" => 56];
use v5.42;
my $name  = "Ada";
my @langs = ("PHP", "Perl");
my %ages  = (Ada => 36, Rasmus => 56);
say $name;            # Ada
say $langs[0];        # PHP
say $ages{Ada};       # 36
say scalar @langs;    # 2

The gotcha: when you reach into an aggregate for one element, the sigil follows the value you get back, not the container. A single element of @langs is a scalar, so it is $langs[0], not @langs[0]; one value from %ages is $ages{Ada}. Read the leading sigil as “what shape am I getting” - $ for one thing, @ for a list, % for the whole map - and the brackets as “from where.”

Strings#

Concatenation is ., exactly as in PHP. Double quotes interpolate variables, single quotes do not - the same split PHP makes between "..." and '...'. There is no sprintf-into-the-string syntax to learn because the default double-quoted string already interpolates.

$name = "Ada";
$greeting = "Hello, $name";
$literal  = 'Hello, $name';
$joined   = "AB" . "CD";
$shout    = str_repeat("AB", 3);
use v5.42;
my $name = "Ada";
my $greeting = "Hello, $name";   # Hello, Ada
my $literal  = 'Hello, $name';   # Hello, $name  (no interpolation)
my $joined   = "AB" . "CD";      # ABCD
my $shout    = "AB" x 3;         # ABABAB

The gotcha: string repetition is the x operator, not a function - "AB" x 3, not str_repeat. Interpolating an array or hash element needs no curly-brace escape: "First: $langs[0]" and "Name: $h{name}" both work as written, where PHP would make you write "{$arr['k']}". To interpolate the result of an expression, wrap it in @{[ ... ]}: "sum: @{[ 2 + 3 ]}" prints sum: 5.

Numbers#

Perl has one numeric type at the surface; you do not declare int vs float. The thing that will actually bite you is operators, so read the gotcha twice.

echo 7 / 2;           // 3.5
echo intdiv(7, 2);    // 3
echo 2 ** 10;         // 1024
echo 10 % 3;          // 1
use v5.42;
say 7 / 2;          # 3.5
say int(7 / 2);     # 3
say 2 ** 10;        # 1024
say 10 % 3;         # 1

The gotcha: Perl has two operator families and you pick by operand type, not by the variable. Numbers use ==, !=, <, >, <=>; strings use eq, ne, lt, gt, cmp. And + is always numeric - there is no operator overloading the way PHP’s + does array union. Concatenate with ., add with +, and never cross them:

use v5.42;
say "12" . "3";     # 123   (string concat)
say "12" + "3";     # 15    (numeric add)
say "10" == "10.0" ? "same" : "diff";   # same  (numeric compare)
say "10" eq "10.0" ? "same" : "diff";   # diff  (string compare)

PHP retired the loose == mess in modern versions; Perl never had it, but it solves the same problem by making you say which comparison you mean. "10" == "10.0" is true (numeric), "10" eq "10.0" is false (string). Using == on strings, or eq on numbers, is the single most common PHP-to-Perl bug.

Arrays and lists#

Here is where PHP’s one array splits in two. When you used an array as a numerically indexed list - $items[0], array_push, foreach over values - that is a Perl array, declared with @. The verbs are renamed but the operations are the same, and negative indices work as you expect.

$nums = [1, 2, 3, 4, 5];
$nums[] = 6;
$last = array_pop($nums);
echo count($nums);             // 5
print_r(array_slice($nums, 1, 2)); // [2, 3]
use v5.42;
my @nums = (1, 2, 3, 4, 5);
push @nums, 6;
my $last = pop @nums;       # 6
say scalar @nums;           # 5
say "@nums[1,2]";           # 2 3   (a slice)

Your array_map and array_filter become map and grep. map transforms (PHP’s array_map); grep filters (PHP’s array_filter). $_ is the current element, Perl’s implicit loop variable, the way the callback’s parameter is in PHP - except you do not name it.

$squares = array_map(fn($x) => $x * $x, $nums);
$evens   = array_filter($nums, fn($x) => $x % 2 == 0);
rsort($nums);   // descending, in place
use v5.42;
my @nums    = (1, 2, 3, 4, 5);
my @squares = map  { $_ * $_ }    @nums;  # 1 4 9 16 25
my @evens   = grep { $_ % 2 == 0 } @nums; # 2 4
my @desc    = sort { $b <=> $a }  @nums;  # 5 4 3 2 1

The gotcha: sort compares as strings by default, so sort (10, 9, 100) gives 10 100 9. For numbers you must pass a comparator: sort { $a <=> $b } @nums. $a and $b are the two elements being compared, and <=> is the numeric three-way operator (cmp is its string sibling). Note also that grep keeps elements, returning a new list, where PHP’s array_filter preserves keys - Perl arrays have no keys to preserve, so the result is simply re-indexed.

Hashes#

The other half of PHP’s array - the part you used with string keys, $config['host'], array_key_exists, foreach ($h as $k => $v) - is a Perl hash, declared with % and accessed one value at a time with $h{key}.

$h = ["apple" => 3, "pear" => 5, "plum" => 2];
echo $h["apple"];                 // 3
$keys = array_keys($h);
sort($keys);
echo array_key_exists("pear", $h) ? "yes" : "no";  // yes
unset($h["plum"]);
foreach ($h as $k => $v) {
    echo "$k => $v\n";
}
use v5.42;
my %h = (apple => 3, pear => 5, plum => 2);
say $h{apple};                          # 3
say join ",", sort keys %h;             # apple,pear,plum
say exists $h{pear} ? "yes" : "no";     # yes
delete $h{plum};
for my $k (sort keys %h) {
    say "$k => $h{$k}";                 # apple => 3 ... pear => 5
}

The gotcha: membership is exists $h{key} (the key is present) or defined $h{key} (the key is present and its value is not undef) - between them they cover PHP’s array_key_exists and isset. The fat comma => is the idiomatic key/value separator and auto-quotes the bareword on its left, so apple => 3 needs no quotes around apple. A Perl hash has no insertion order (unlike a PHP array, which keeps it); iterate sort keys %h when you want a stable order. To walk key and value together, PHP’s foreach ($h as $k => $v) becomes either for my $k (keys %h) or, when you want both at once, while (my ($k, $v) = each %h).

Context#

This is the chapter to read twice. PHP has no equivalent, and almost every “Perl is weird” story traces back to it.

Every expression in Perl runs in either list context or scalar context, decided by where it appears, not by what it is. The same array behaves differently in each. Assigning an array to a scalar gives its length; assigning it to a list (a parenthesised set of variables) copies elements.

use v5.42;
my @list = (10, 20, 30);

my $count = @list;        # scalar context -> 3 (the length)
say $count;               # 3

my ($first) = @list;      # list context -> first element copied
say $first;               # 10

my ($x, $y) = @list;      # 10 and 20
say "$x $y";              # 10 20

The parentheses on the left are the whole difference. my $x = @list asks “how many?” and gets 3 - this is where count($arr) lives, just spelled by context instead of a function. my ($x) = @list asks “give me the elements” and gets 10. A PHP developer reads both as “assign the array,” which is exactly the trap.

A subroutine can ask which context it was called in, with wantarray, and return different things accordingly:

use v5.42;
sub items {
    return wantarray ? (1, 2, 3) : "three items";
}
my @got = items();        # list context
my $msg = items();        # scalar context
say "@got";               # 1 2 3
say $msg;                 # three items

Built-ins use this too. scalar @array forces scalar context to get a count - that is the explicit count() you were looking for. A regex match with /g returns the list of matches in list context and a true/false in scalar context.

The gotcha: there is no single answer to “what does this array do” - it depends on the surrounding expression. When something returns a number you expected to be a list (or vice versa), context is the first suspect. Reach for scalar to force a count and parentheses on the left to force element copying.

Truthiness#

Perl’s false values are a short, specific list: the number 0, the empty string "", the string "0", and undef (Perl’s null). Everything else is true. This is close to PHP’s rules but not identical, and the differences are quiet.

use v5.42;
for my $v (0, 1, "", "0", "00", "0.0", "false") {
    say "'$v' is ", ($v ? "true" : "false");
}

This prints, exactly:

'0' is false
'1' is true
'' is false
'0' is false
'00' is true
'0.0' is true
'false' is true

The gotcha: "00" and "0.0" are true in Perl, even though PHP treats "0" and only "0" as the falsy string too - Perl agrees on "0" but not on its look-alikes. The string "false" is true (it is a non-empty, non-"0" string), the same as in PHP. And unlike PHP’s empty-array-is-falsy rule, an empty Perl array is not itself a false value: in boolean context an array yields its length, so if (@list) is true exactly when the list is non-empty, which is the test you want. For “use this if defined, else a default,” use defined-or //, the equivalent of PHP’s ??, which falls back only on undef (not on 0 or ""):

use v5.42;
my %cfg = (host => "localhost");
my $port = $cfg{port} // 8080;   # key absent -> undef -> 8080
say $port;                       # 8080

Control flow#

The keywords are familiar; the spelling differs in two small ways. elseif is elsif (one e), and conditions need parentheses while blocks need braces - there is no :/endif alternate syntax.

if ($n < 0) {
    $sign = "neg";
} elseif ($n == 0) {
    $sign = "zero";
} else {
    $sign = "pos";
}

foreach ($nums as $x) {
    echo $x;
}

while ($running) {
    step();
}
use v5.42;
my $sign;
if    ($n < 0)  { $sign = "neg"  }
elsif ($n == 0) { $sign = "zero" }
else            { $sign = "pos"  }

for my $x (@nums) { say $x }

while ($running) { step() }

Perl adds two things PHP does not have. Postfix modifiers let a simple statement carry its own condition or loop, which reads naturally for one-liners:

use v5.42;
say $_ for @nums;                 # loop, one statement
say "negative" if $n < 0;         # guarded statement
$sum += $_ for @nums;             # accumulate

And unless/until are the negated forms of if/while, for when the positive phrasing reads awkwardly: say "ok" unless $error.

The gotcha: the ternary is ?/:, the same as PHP’s, and chains the same way: my $sign = $n < 0 ? "neg" : $n == 0 ? "zero" : "pos". Perl has no switch/match statement in core; a chained ternary or a dispatch hash ($handler{$key}->()) is the idiom, and a for-on-a- single-value block is the common substitute for a switch.

Subroutines#

PHP’s function becomes Perl’s sub. Modern Perl has signatures that look much like PHP’s parameter lists, including default values.

function greet($name, $greeting = "Hello") {
    return "$greeting, $name!";
}

function total(...$nums) {
    return array_sum($nums);
}
use v5.42;
sub greet ($name, $greeting = "Hello") {
    return "$greeting, $name!";
}
say greet("Ada");           # Hello, Ada!
say greet("Rasmus", "Hi");  # Hi, Rasmus!

sub total (@nums) {
    my $sum = 0;
    $sum += $_ for @nums;
    return $sum;
}
say total(1, 2, 3, 4);      # 10

The variadic ...$nums becomes a trailing array parameter @nums, which gathers all remaining arguments. PHP’s named arguments (or the common $opts array convention) map to a hash parameter, since key => value pairs flatten into a list that a %opts signature gathers up:

use v5.42;
sub connect_to (%opts) {
    return "host=$opts{host} port=$opts{port}";
}
say connect_to(host => "localhost", port => 8080);
# host=localhost port=8080

The gotcha: a sub returns the value of its last expression even without return, and the return is context-sensitive (see Context). If you return @list, the caller gets the list in list context but the count in scalar context. Be deliberate about what you return when it might be used either way.

References#

PHP arrays nest directly: an array can hold another array and you just keep subscripting. Perl arrays and hashes do not nest directly; you nest them through references, scalars that point at an aggregate. This is the same machinery as PHP’s &$x references and its object handles, but in Perl you reach for it whenever you build nested data. Take a reference with \, follow it with ->.

$data = ["name" => "Ada", "langs" => ["Perl", "PHP"]];
echo $data["langs"][1];      // PHP
$data["langs"][] = "Ruby";
use v5.42;
my $data = { name => "Ada", langs => ["Perl", "PHP"] };
say $data->{name};                  # Ada
say $data->{langs}[1];              # PHP
push $data->{langs}->@*, "Ruby";    # postfix deref to push
say "@{$data->{langs}}";           # Perl PHP Ruby

{ ... } builds an anonymous hash reference and [ ... ] an anonymous array reference - the literal building blocks of nested data, matching the [...] and [k => v] literals you already write in PHP. The -> arrow follows a reference one step; between two subscripts it is optional, so $data->{langs}[1] and $data->{langs}->[1] are the same. The arrow is also exactly the method-call arrow you know from PHP OO - same symbol, both for “reach inside” and “call a method.”

Anonymous subs are closures, exactly like PHP’s fn and function() use (...), but with no use clause needed - they capture lexicals automatically:

use v5.42;
my $double = sub ($n) { $n * 2 };
say $double->(21);          # 42

The gotcha: the postfix deref forms (->@*, ->%*, ->$*) are the modern, readable way to dereference a whole aggregate, and "@{ ... }" is how you interpolate a dereferenced array into a string. The older @$ref and ${$ref}{key} forms still work and appear in existing code, but reach for the arrow and postfix forms in new code.

Regular expressions#

In PHP regex is the preg_* function family with pattern strings like '/\d+/'. In Perl it is syntax: =~ binds a pattern to a string, m// matches, s/// substitutes - no delimiters-inside-a-string, no function call. Capture groups land in $1, $2, and named captures in %+.

if (preg_match('/(\d{4})-(\d{2})-(\d{2})/', $s, $m)) {
    [$_, $year, $month, $day] = $m;
}
$t = preg_replace('/,/', ';', "a,b,c");
$parts = explode(",", "one,two,three");
use v5.42;
my $s = "2026-06-20";
if ($s =~ /(\d{4})-(\d{2})-(\d{2})/) {
    say "$1 $2 $3";                 # 2026 06 20
}
if ($s =~ /(?<year>\d{4})/) {
    say $+{year};                   # 2026
}
(my $t = "a,b,c") =~ s/,/;/g;       # a;b;c
my @parts = split /,/, "one,two,three";   # one two three

A /g match in list context returns all captures, the direct analogue of preg_match_all:

use v5.42;
my @nums = "x1y2z3" =~ /(\d)/g;     # 1 2 3
say "@nums";

The gotcha: s/// modifies its target in place and returns the number of substitutions, not a new string - unlike preg_replace, which returns a fresh string. That is why the example copies first with (my $t = "a,b,c") and then substitutes on the copy - otherwise you would be editing a string literal. To get a new string without touching the original (the preg_replace reflex), use the /r flag: my $t = $s =~ s/,/;/gr.

File and stream I/O#

The idiom is the three-argument open with a lexical filehandle, the counterpart of PHP’s fopen into a $handle. The angle-bracket operator <$fh> reads a line, the same role as fgets.

$wh = fopen("/tmp/sample.txt", "w");
for ($i = 1; $i <= 3; $i++) {
    fwrite($wh, "line $i\n");
}
fclose($wh);

$rh = fopen("/tmp/sample.txt", "r");
while (($line = fgets($rh)) !== false) {
    echo "got: " . rtrim($line) . "\n";
}
fclose($rh);
use v5.42;
my $path = "/tmp/sample.txt";

open my $wh, '>', $path or die "cannot write: $!";
print {$wh} "line $_\n" for 1 .. 3;
close $wh;

open my $rh, '<', $path or die "cannot read: $!";
while (my $line = <$rh>) {
    chomp $line;
    say "got: $line";       # got: line 1 ... got: line 3
}
close $rh;

To slurp every line at once, read the handle in list context - the moral equivalent of file():

use v5.42;
open my $fh, '<', "/tmp/sample.txt" or die $!;
my @lines = <$fh>;          # all lines, including newlines
say scalar @lines;          # 3

The gotcha: a line read with <$fh> keeps its trailing newline; call chomp to strip it (the rtrim reflex, but chomp removes only the record separator). And open returns false on failure rather than raising or warning, so the or die "...: $!" idiom is mandatory - $! is the system error message, Perl’s errno (PHP’s error_get_last()).

Modules and packages#

PHP’s require/include and Composer’s autoloader become Perl’s use. The namespace separator is ::, not \, and a module Foo::Bar lives at Foo/Bar.pm on the include path @INC (Perl’s include_path). A module’s import list is a selection of names to bring into your namespace.

require 'vendor/autoload.php';
use App\Util\ListHelper;
$sum = ListHelper::sum(range(1, 10));
use v5.42;
use List::Util qw(sum0 max first);
say sum0(1 .. 10);                       # 55
say max(3, 7, 2);                        # 7
say first { $_ % 2 == 0 } (1, 3, 4, 5);  # 4

PHP’s huge flat standard-function namespace (count, in_array, array_map, implode, sprintf, …) maps in Perl partly to built-in operators and partly to a few everyday modules. The common translations:

PHP

Perl

count($a)

scalar @a

in_array($x, $a)

grep { $_ eq $x } @a

array_map($f, $a)

map { ... } @a

array_filter($a, $f)

grep { ... } @a

implode(",", $a)

join ",", @a

explode(",", $s)

split /,/, $s

sprintf(...)

sprintf(...) (same)

isset($h[$k])

exists $h{$k} / defined $h{$k}

array_sum($a)

sum0 @a (List::Util)

List::Util, Scalar::Util, POSIX, File::Spec, and Data::Dumper are the everyday modules; most of what you would pull from Packagist ships in core or on CPAN, the Perl package archive.

The gotcha: use runs at compile time, not at the point in the file where it appears, so the order of use lines relative to your code does not matter the way require placement can in PHP. Importing is also opt-in per name: use List::Util qw(sum0 max) brings exactly sum0 and max into your namespace, nothing else - there is no global function table the way PHP exposes every built-in everywhere.

(Web request data - PHP’s $_GET, $_POST, $_SERVER superglobals - has no language-level analogue in Perl; that is the job of a CGI or PSGI layer, not the core language, and is out of scope for this guide.)

Objects#

For new code, Perl 5.42 has a first-class class syntax that will look familiar coming from PHP. Fields are declared with field, methods with method, and $self is available inside every method just as $this is in PHP (you do not list it as a parameter).

class Point {
    public function __construct(
        public int $x = 0,
        public int $y = 0,
    ) {}
    public function to_string(): string {
        return "({$this->x}, {$this->y})";
    }
    public function move(int $dx, int $dy): static {
        $this->x += $dx;
        $this->y += $dy;
        return $this;
    }
}

$p = new Point(x: 3, y: 4);
echo $p->to_string();        // (3, 4)
$p->move(1, 1);
echo $p->to_string();        // (4, 5)
use v5.42;
use feature 'class';

class Point {
    field $x :param = 0;
    field $y :param = 0;
    method to_string { return "($x, $y)" }
    method move ($dx, $dy) {
        $x += $dx;
        $y += $dy;
        return $self;
    }
}

my $p = Point->new(x => 3, y => 4);
say $p->to_string;          # (3, 4)
$p->move(1, 1);
say $p->to_string;          # (4, 5)

The constructor is generated for you, like PHP’s promoted constructor properties: :param marks a field that the auto-built new accepts as a named argument, and = 0 is its default. The method-call arrow -> is the same one you use in PHP. Inheritance is :isa, the counterpart of extends:

use v5.42;
use feature 'class';

class Point3D :isa(Point) {
    field $z :param = 0;
    method z { return $z }
}

my $q = Point3D->new(x => 1, y => 2, z => 3);
say $q->to_string, " z=", $q->z;   # (1, 2) z=3

The gotcha: the class feature is still marked experimental in Perl 5.42, and on pperl the “class is experimental” notices print to stderr even with no warnings 'experimental::class' in scope. They do not touch your program’s output, so a script that prints to stdout behaves exactly as shown above; just expect the notices on the error stream. The older object model - a package, a hash blessed into it with bless, and @ISA for inheritance - is what you will meet in existing code and most of CPAN; for it, for roles, and for migration advice, see the Object-oriented Programming guide.

Error handling#

PHP’s try/catch has a direct counterpart in modern Perl, and it reads almost identically. The raising side is die (PHP’s throw):

function risky($n) {
    if ($n < 0) {
        throw new Exception("negative!");
    }
    return sqrt($n);
}

try {
    risky(-4);
} catch (Exception $e) {
    echo "caught: " . $e->getMessage() . "\n";
}
use v5.42;
use feature 'try';

sub risky ($n) {
    die "negative!\n" if $n < 0;
    return sqrt $n;
}

try {
    risky(-4);
}
catch ($e) {
    print "caught: $e";     # caught: negative!
}

The older, still-common form wraps code in eval { } and inspects $@ (the caught error) afterward - the same pattern as PHP code written before exceptions were idiomatic:

use v5.42;
my $result = eval { risky(-1) };
if ($@) {
    print "caught: $@";     # caught: negative!
}

The gotcha: die with a string that ends in \n produces exactly that message; die with a string that does not end in a newline gets " at SCRIPT line N.\n" appended automatically. End your error strings with \n when you want a clean message and omit it when you want the file-and-line trace. Note also the two error variables: $@ is the last caught exception (PHP’s $e), while $! is the last system error (a failed open, the errno string).

Idioms#

A few patterns you will see daily and want in your fingers.

Sort a list by a computed key (the Schwartzian transform - PHP’s usort with a precomputed key, done in one expression: decorate, sort, undecorate):

use v5.42;
my @words = qw(apple fig pear);
my @by_length =
    map  { $_->[1] }
    sort { $a->[0] <=> $b->[0] }
    map  { [ length $_, $_ ] } @words;
say "@by_length";           # fig pear apple

Build a hash from a list, the array_combine/array_column analogue (here, word to its length):

use v5.42;
my @words = qw(apple pear fig);
my %len = map { $_ => length $_ } @words;
say "$_ => $len{$_}" for sort keys %len;
# apple => 5
# fig => 3
# pear => 4

And pperl is a command-line tool in the awk/sed niche - the same job you might reach for a quick PHP CLI script to do. The implicit-loop one-liner is the Perl developer’s everyday reflex:

pperl -lane '$s += $F[-1]; END { print $s }' data.txt

The gotcha: read the One-Liners guide for the switches (-n, -p, -a, -l, -e) and the dozens of recipes built on them; it is one of the highest-leverage things to learn coming from a PHP-and-shell background.

Gotchas coming from PHP#

The traps, collected for a final scan before you start writing:

  • Sigils change with usage. One element of @a is $a[0]; one value of %h is $h{k}. PHP keeps $ everywhere; Perl varies the sigil by what shape you are getting.

  • One PHP array is two Perl types. Decide up front: integer keys and order means @array; string keys means %hash. They are not interchangeable.

  • Two comparison families. ==/</<=> for numbers, eq/lt/ cmp for strings. Pick by operand type, not by variable. == on strings or eq on numbers is the classic bug.

  • . joins strings, + only adds numbers. Perl’s + never does array union or concatenation; mixing . and + corrupts your data silently or warns.

  • sort is string-sorted by default. Pass { $a <=> $b } for numeric order.

  • Hashes have no order. Unlike a PHP array, iteration order is not insertion order; sort keys %h when you need stability.

  • Context. my $x = @a is the count; my ($x) = @a is the first element. The parentheses are the whole difference. Re-read Context when a value comes back the wrong shape.

  • Truthiness surprises. "0" is false, but "00" and "0.0" are true. Only 0, "", "0", and undef are false. An empty array is not a false value - if (@a) tests non-empty.

  • s/// edits in place and returns a count, unlike preg_replace; use the /r flag to get a new string instead.

  • open does not raise. Use open ... or die "...: $!".

  • chomp your line reads. <$fh> keeps the trailing newline.

  • Web superglobals are not core. $_GET/$_POST belong to a CGI or PSGI layer, not the language.

  • Non-ASCII source needs use utf8;. Under use v5.42; a literal non-ASCII byte in your source is a compile error without it. Keep string data ASCII unless you specifically add use utf8;.

  • The class feature prints experimental notices to stderr on pperl. Correct output, noisy error stream.