PHP 7.3: small wins that add up
PHP 7.3 shipped December 6th. No single killer feature. It’s a collection of quality-of-life improvements that individually feel minor but together make daily work noticeably less annoying.
Flexible heredoc and nowdoc
Until 7.3, the closing marker of a heredoc had to be at column zero. That forced awkward de-indentation in otherwise well-formatted code:
// before
$html = <<<HTML
<div>
<p>Hello</p>
</div>
HTML; // had to be at column 0, ugly
// after
$html = <<<HTML
<div>
<p>Hello</p>
</div>
HTML;
The closing marker can now be indented to match the surrounding code, and that indentation is stripped from the content. This looks cosmetic. It’s not. Heredocs in nested contexts (class methods, conditionals) were visually jarring before. Now they fit.
array_key_first() and array_key_last()
This existed forever as a workaround:
$first = array_keys($array)[0];
7.3 adds the obvious helpers:
$first = array_key_first($array);
$last = array_key_last($array);
And is_countable() to safely check before calling count() on something that might not implement Countable. Functions that should have existed years ago.
PCRE2
The regular expression engine was migrated from PCRE to PCRE2. Mostly invisible for existing patterns, but PCRE2 is actively maintained and handles edge cases better. The main practical impact: some patterns that previously produced undefined behavior now throw errors. That’s the correct behavior, even if it surprises you on the first upgrade.
Trailing commas in function calls
7.2 allowed trailing commas in grouped namespace imports. 7.3 extends this to function and method calls:
$result = array_merge(
$defaults,
$overrides,
$extras, // no more removing this comma before the closing paren
);
This matters most with multiline calls. Adding or removing an argument no longer means touching the adjacent line. Diffs stay honest, rebases get a little less painful.
Reference assignments in array destructuring
Array destructuring gained the ability to capture references instead of copies:
$data = ['Alice', 42];
[&$name, $age] = $data;
$name = 'Bob';
var_dump($data[0]); // string(3) "Bob"
Nested references work too:
[$a, [&$b]] = [1, [2]];
More niche than trailing commas, but the right tool when you need to alias deep into a structure without a pile of intermediate assignments.
instanceof with literals is now legal
Previously, using instanceof with a literal on the left side was a parse error. 7.3 makes it valid:
var_dump(null instanceof stdClass); // bool(false)
It always evaluates to false, which is exactly correct. The benefit is that code that conditionally constructs a value and then checks its type no longer needs to extract the value into a variable first. Useful in generated code and test helpers.
json_decode() and json_encode() can throw now
Before 7.3, JSON errors were silent unless you remembered to check json_last_error(). Easy to forget, easy to get wrong:
$data = json_decode($response);
if (json_last_error() !== JSON_ERROR_NONE) {
// most people forgot this part
}
7.3 adds JSON_THROW_ON_ERROR:
$data = json_decode($response, true, 512, JSON_THROW_ON_ERROR);
// throws JsonException on malformed input
JsonException extends RuntimeException. Catch it specifically or let it propagate. Should have worked this way from day one.
setcookie() with an options array
The old setcookie() signature is a relic: seven positional arguments, most of which you leave as defaults just to reach the one you actually want. 7.3 adds an alternative form that takes an associative array:
setcookie('session', $token, [
'expires' => time() + 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
The samesite option is the real reason this was added — the old positional signature had no slot for it. session_set_cookie_params() received the same treatment, and a new session.cookie_samesite ini directive covers the default.
hrtime() for benchmarking that actually measures time
microtime() reads wall clock time. Fine for most cases. But it’s affected by NTP adjustments, and its resolution is implementation-dependent. hrtime() reads the monotonic high-resolution clock:
$start = hrtime(true); // nanoseconds as integer
doWork();
$elapsed = hrtime(true) - $start;
echo $elapsed / 1e6 . " ms\n";
Without the true argument it returns [seconds, nanoseconds] as a two-element array. Use this for microbenchmarks, or anywhere clock drift would silently corrupt your measurement.
gc_status() — looking inside the garbage collector
PHP’s cyclic garbage collector runs when a buffer of potential cycles fills up. Until 7.3 you had no easy way to see what it was actually doing. gc_status() exposes the internal state:
$status = gc_status();
// [
// 'runs' => 3,
// 'collected' => 127,
// 'threshold' => 10001,
// 'roots' => 42,
// ]
Not something most app code needs. Useful when you’re trying to figure out why memory keeps climbing under specific workloads.
CompileError joins the exception hierarchy
Parse errors have been catchable as ParseError since PHP 7.0. 7.3 introduces CompileError as the parent class for compile-time failures, with ParseError becoming a subclass of it:
Error
└── CompileError
└── ParseError
In practice, code that catches ParseError still works. The new class just gives future compile-time errors (that aren’t parse errors) a proper home in the hierarchy.
bcscale() as a getter
The BC Math scale was always settable via bcscale($n). Getting the current scale required tracking it yourself. 7.3 makes bcscale() work without arguments:
bcscale(4);
echo bcscale(); // 4
Minor. Worth knowing if you’re writing library code that needs to respect or restore whoever called it’s scale setting.
The continue inside switch warning
This one is a correctness fix that looks like a deprecation. In PHP, continue inside a switch has always behaved like break — it exits the switch, not the enclosing loop. Developers coming from other languages often write this expecting to skip to the next loop iteration:
foreach ($items as $item) {
switch ($item->type) {
case 'skip':
continue; // WRONG: exits the switch, not the foreach
}
}
7.3 adds a warning for this pattern. The fix is continue 2 to explicitly target the enclosing loop. The behavior hasn’t changed. The silence has.
Deprecations
Case-insensitive constants declared via define() are deprecated:
define('MY_CONST', 42, true); // third argument deprecated
Passing a non-string needle to strpos(), strstr(), and related functions is deprecated. In PHP 8 these will interpret the needle as a string, not an ASCII codepoint. If you’re passing integers to these functions intentionally, chr($n) is the explicit form.
fgetss() is deprecated — it was fgets() with HTML/PHP tags stripped. Use fgets() and strip tags explicitly if you need them stripped. The string.strip_tags stream filter goes with it.
7.3 is the kind of release you appreciate in hindsight. Nothing individually dramatic, but after six months with it the heredoc fix alone has paid back the upgrade cost in readability. Sometimes boring is exactly right.