# 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: ```perl 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](#variables-and-sigils) - the `$`/`@`/`%` shift. - [Strings](#strings) - interpolation and `.` concatenation. - [Numbers](#numbers) - `==` vs `eq`, the classic PHP trap. - [Arrays and lists](#arrays-and-lists) - half of your `array`. - [Hashes](#hashes) - the other half of your `array`. - [Context](#context) - the concept PHP has no word for. Read this one. - [Truthiness](#truthiness) - what counts as false, with surprises. - [Control flow](#control-flow) - `if`, loops, the postfix forms. - [Subroutines](#subroutines) - `function` becomes `sub`. - [References](#references) - nested data and closures. - [Regular expressions](#regular-expressions) - regex as syntax, not `preg_*`. - [File and stream I/O](#file-and-stream-io) - `fopen`, line reads, slurp. - [Modules and packages](#modules-and-packages) - `require`/`use` becomes `use`. - [Objects](#objects) - the modern `class` syntax. - [Error handling](#error-handling) - `try`/`catch`, the same idea. - [Idioms](#idioms) - the daily one-liners. - [Gotchas coming from PHP](#gotchas-coming-from-php) - scan before you write. ## 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. ```php $name = "Ada"; $langs = ["PHP", "Perl"]; $ages = ["Ada" => 36, "Rasmus" => 56]; ``` ```perl 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. ```php $name = "Ada"; $greeting = "Hello, $name"; $literal = 'Hello, $name'; $joined = "AB" . "CD"; $shout = str_repeat("AB", 3); ``` ```perl 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. ```php echo 7 / 2; // 3.5 echo intdiv(7, 2); // 3 echo 2 ** 10; // 1024 echo 10 % 3; // 1 ``` ```perl 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: ```perl 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. ```php $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] ``` ```perl 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. ```php $squares = array_map(fn($x) => $x * $x, $nums); $evens = array_filter($nums, fn($x) => $x % 2 == 0); rsort($nums); // descending, in place ``` ```perl 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}`. ```php $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"; } ``` ```perl 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. ```perl 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: ```perl 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. ```perl use v5.42; for my $v (0, 1, "", "0", "00", "0.0", "false") { say "'$v' is ", ($v ? "true" : "false"); } ``` This prints, exactly: ```text '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 `""`): ```perl 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. ```php if ($n < 0) { $sign = "neg"; } elseif ($n == 0) { $sign = "zero"; } else { $sign = "pos"; } foreach ($nums as $x) { echo $x; } while ($running) { step(); } ``` ```perl 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: ```perl 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. ```php function greet($name, $greeting = "Hello") { return "$greeting, $name!"; } function total(...$nums) { return array_sum($nums); } ``` ```perl 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: ```perl 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](#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 `->`. ```php $data = ["name" => "Ada", "langs" => ["Perl", "PHP"]]; echo $data["langs"][1]; // PHP $data["langs"][] = "Ruby"; ``` ```perl 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: ```perl 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 `%+`. ```php 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"); ``` ```perl 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 =~ /(?\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`: ```perl 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`. ```php $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); ``` ```perl 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()`: ```perl 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. ```php require 'vendor/autoload.php'; use App\Util\ListHelper; $sum = ListHelper::sum(range(1, 10)); ``` ```perl 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). ```php 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) ``` ```perl 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`: ```perl 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](../oop/index). ## 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`): ```php function risky($n) { if ($n < 0) { throw new Exception("negative!"); } return sqrt($n); } try { risky(-4); } catch (Exception $e) { echo "caught: " . $e->getMessage() . "\n"; } ``` ```perl 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: ```perl 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): ```perl 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): ```perl 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: ```bash pperl -lane '$s += $F[-1]; END { print $s }' data.txt ``` The gotcha: read the [One-Liners guide](../oneliner/index) 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](#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.