PHP 7.3 is a modest release for syntax, but a few of its changes can affect code that ran quietly on PHP 7.2.
The most visible additions are quality-of-life features. Heredoc and nowdoc strings may now have an indented closing marker, and the indentation is removed from every line of the body. Trailing commas are allowed in function and method calls, which keeps argument lists tidy in version control. Array destructuring and list() can bind by reference, and instanceof accepts a literal as its left operand, always yielding false. A new CompileError class now sits above ParseError for a small set of compilation failures.
The riskier items are behavior changes. The flexible heredoc syntax means a body line that contains the closing label, even indented, now ends the string and can cause a syntax error. A continue that targets a switch now emits a warning, because PHP treats it like break. String-literal offsets such as $obj["123"] are passed to ArrayAccess::offsetGet() as strings instead of being converted to integers. Static properties can no longer be split from their parent by assigning a reference, and unpacking a Traversable with non-integer keys into arguments stopped working.
Two deprecations deserve early attention. Case-insensitive constants, declared by passing true to define() or used with a different case than declared, are deprecated. Passing a non-string needle to the strpos() family is also deprecated, because a future version will treat the needle as a string rather than an ASCII code point.
For an upgrade, run your test suite on PHP 7.3 with error_reporting set to E_ALL and display errors on, then search for these patterns: continue inside switch, heredoc bodies containing their own label, define() calls with a third argument, and numeric needles in string searches. Fix the warnings first, since each fix still works on PHP 7.2. Adopt the new syntax afterwards, once every environment you deploy to runs 7.3 or later.
NEW
Flexible heredoc and nowdoc closing markers
The closing marker of a heredoc or nowdoc may be indented, and that indentation is stripped from every line of the string body.
Before PHP 7.3 the closing marker had to start in the first column and could only be followed by a semicolon and a newline. That forced heredocs to break out of the indentation of surrounding code, which hurt readability inside methods and nested blocks. From PHP 7.3 the closing marker may be indented, and the same amount of indentation is removed from every line of the body. The marker also no longer has to be followed by a semicolon or newline.
The practical benefit is that you can keep templates, SQL and messages aligned with the code around them. Watch for two pitfalls. First, the stripped indentation is determined by the closing marker, so a body line indented less than the marker is a parse error. Second, this change alters how marker-like text inside a body is read, covered in the next change.
$name = 'Ada';
$text = <<<EOT
Hello, {$name}!
EOT;
echo $text, "\n";PHP 7.2 requires the closing marker at the start of the line; PHP 7.3 and later still accept this form. Shown for comparison; not executed on this site.
$name = 'Ada';
$text = <<<EOT
Hello, {$name}!
Indented line
Done
EOT;
echo $text, "\n";Hello, Ada! Indented line Done
CHANGED BEHAVIOR
Heredoc bodies that contain their own label
Because closing markers can be indented, a body line containing the ending label can now end the string early or cause a syntax error.
With flexible heredoc syntax, an indented occurrence of the ending label inside a doc string is no longer ordinary text. PHP 7.3 treats it as the end of the string, so whatever follows, including the real closing marker, may produce a syntax error. Code that worked on PHP 7.2 can therefore stop compiling after the upgrade, even though you never touched the heredoc.
The fix is simple and also works on PHP 7.2: pick an ending label that does not occur anywhere in the body. Descriptive labels such as SQL, TEXT or HTML_BLOCK are less likely to collide than short ones like FOO or EOT. When you scan an old code base, look for heredocs whose label is a common word and whose body may mention it, such as templates that embed example code or documentation.
$str = <<<FOO
abcdefg
FOO
FOO;PHP 7.2 accepts this because the indented FOO is plain text; PHP 7.3 and later treat it as the closing marker and report a syntax error. Shown for comparison; not executed on this site.
$str = <<<TEXT
abcdefg
FOO
TEXT;
echo $str, "\n";abcdefg FOO
CHANGED BEHAVIOR
continue targeting switch now warns
A continue statement whose target is a switch behaves like break in PHP, so PHP 7.3 emits a warning suggesting continue 2.
In PHP, switch counts as a looping structure for continue, and continue there is equivalent to break. Developers coming from other languages often expect it to skip to the next iteration of the surrounding loop, which is what continue 2 does in PHP. PHP 7.3 now emits a warning at compile time for such statements, pointing out the equivalence.
The warning does not change behavior, but it often exposes a real bug: code after the switch still runs when the author meant to skip it. Review every flagged line and decide which behavior you actually want. If you meant to skip the rest of the loop body, use continue 2. If you meant to leave the switch, write break so the intent is explicit. Because the warning appears at compile time, an error handler installed at runtime will not see it, so check your logs or run a lint pass.
foreach ([1, 2, 3] as $n) {
switch ($n) {
case 2:
continue;
}
echo "after switch $n\n";
}PHP 7.2 accepts this silently, and it acts like break; PHP 7.3 and later emit a warning. Shown for comparison; not executed on this site.
foreach ([1, 2, 3] as $n) {
switch ($n) {
case 2:
continue 2;
default:
echo "n=$n\n";
}
echo "after switch $n\n";
}n=1 after switch 1 n=3 after switch 3
NEW
Trailing commas in function and method calls
A trailing comma after the last argument of a function or method call is now allowed.
PHP 7.3 allows a trailing comma after the final argument in function and method calls. Arrays already allowed this, so multi-line calls can now follow the same style. The main benefit is cleaner diffs: adding or reordering the last argument touches one line instead of two, and you no longer risk a parse error from a forgotten comma.
The packet describes this for calls, so apply it to call sites and do not assume it works everywhere in PHP 7.3. A trailing comma does not add an empty argument; the call receives exactly the arguments you wrote. Code that uses it will fail to parse on PHP 7.2, so adopt it only after every environment you deploy to runs 7.3 or later, including CI images and developer machines.
function total(int ...$n) { return array_sum($n); }
echo total(
1,
2,
3
), "\n";PHP 7.2 requires the last argument to have no trailing comma; PHP 7.3 and later also accept this form. Shown for comparison; not executed on this site.
function total(int ...$n) { return array_sum($n); }
echo total(
1,
2,
3,
), "\n";6
NEW
Reference assignment in array destructuring
Array destructuring and list() can bind variables by reference using the & prefix, including inside nested patterns.
Destructuring in PHP 7.2 could only copy values out of an array. PHP 7.3 lets you mark targets with &, as in [&$a, [$b, &$c]] = $data, so those variables become references to the matching elements. The same works with list(). Mixed patterns are fine: unmarked targets still receive copies, as $b does here.
This replaces a series of manual $a = &$data[0] assignments and is handy when you want to update selected elements in place. The usual reference pitfalls apply. The source must be a variable that can be referenced, and references left behind in an array can surprise you later, for example when you copy the array or iterate it with foreach. Unset reference variables when you are done with them if the array outlives them.
$data = [1, [2, 3]];
$a = &$data[0];
$c = &$data[1][1];
$b = $data[1][0];
$a = 10;
$c = 30;This works in PHP 7.2 and in later versions, but needs one statement per target. Shown for comparison; not executed on this site.
$data = [1, [2, 3]];
[&$a, [$b, &$c]] = $data;
$a = 10;
$c = 30;
echo json_encode($data), "\n";
echo $b, "\n";[10,[2,30]] 2
CHANGED BEHAVIOR
Static properties stay shared after reference assignment
Assigning a reference to an inherited static property no longer separates it from the parent class's property.
Child classes share an inherited static property with the parent unless they redeclare it. Because of an implementation artifact, PHP 7.2 let you split them by assigning a reference to the child's static property, after which the two properties held different values. PHP 7.3 closes this loophole, so both names keep pointing at the same storage.
In the example, the child assignment makes the shared property a reference to $x. Setting $x = 1 then changes the value seen through both Test::$x and Test2::$x. If your code relied on the split, you probably wanted independent state per class. Redeclare the static property in the child class instead, which is the supported way to give it its own value, and does not depend on engine quirks.
class Test { public static $x = 0; }
class Test2 extends Test {}
Test2::$x = &$x;
$x = 1;
var_dump(Test::$x, Test2::$x);PHP 7.2 printed int(0) then int(1); PHP 7.3 and later print int(1) twice. Shown for comparison; not executed on this site.
class Test { public static $x = 0; }
class Test2 extends Test {}
Test2::$x = &$x;
$x = 1;
echo Test::$x, " ", Test2::$x, "\n";1 1
CHANGED BEHAVIOR
ArrayAccess receives integer-string literals as strings
For $obj["123"] on an ArrayAccess object, offsetGet() now receives the string "123" instead of the integer 123.
When an object implements ArrayAccess, a string literal that looks like an integer used to be converted to an integer before the offset method was called. PHP 7.3 stops doing that, so $obj["123"] calls offsetGet("123"), which matches what already happened for non-literal keys held in variables. Real arrays are not affected and still convert integral string keys to integers.
The risk is in classes that compared offsets strictly or used them as keys without normalizing. A lookup that worked for "123" literals now sees a different type. Normalize offsets inside your ArrayAccess methods, for example by casting numeric strings if your container treats 123 and "123" alike. The example class uses the #[\ReturnTypeWillChange] line, which PHP 7.3 reads as a plain comment but newer PHP versions use to silence a return-type deprecation.
// With a class implementing ArrayAccess:
// $obj["123"] reached offsetGet(123) in PHP 7.2
$value = $obj["123"];PHP 7.2 converted the literal to an integer before calling offsetGet(); PHP 7.3 and later pass the string. Shown for comparison; not executed on this site.
class Bag implements ArrayAccess {
public function offsetExists($offset): bool { return true; }
#[\ReturnTypeWillChange]
public function offsetGet($offset) { return gettype($offset); }
public function offsetSet($offset, $value): void {}
public function offsetUnset($offset): void {}
}
$bag = new Bag();
echo $bag["123"], "\n";
echo $bag[123], "\n";
$key = "123";
echo $bag[$key], "\n";string integer string
REMOVED
Argument unpacking rejects non-integer Traversable keys
Spreading a Traversable with non-integer keys into call arguments stopped working in PHP 7.3; it had only worked by accident.
Argument unpacking with ... accepts arrays and Traversable objects. In PHP 5.6 through 7.2, a generator that yielded non-integer keys, such as floats, was accepted by accident. PHP 7.3 no longer supports that case, so code that spread such a generator into a call fails after the upgrade.
The safe approach is to yield integer keys, or no explicit keys at all, when the iterator is meant to supply positional arguments. If your keys carry meaning, transform the data into a plain list first, for example by collecting values with iterator_to_array($gen, false). Later PHP releases changed unpacking again: PHP 8.1 allows string keys, which become named arguments, so do not depend on odd key types in any version.
function foo(...$args) { var_dump($args); }
function gen() { yield 1.23 => 123; }
foo(...gen());This worked in PHP 5.6 through 7.2 but is no longer supported from PHP 7.3. Shown for comparison; not executed on this site.
function add($a, $b, $c) { return $a + $b + $c; }
function numbers() {
yield 1;
yield 2;
yield 3;
}
echo add(...numbers()), "\n";6
DEPRECATED
Case-insensitive constants are deprecated
Passing true as the third argument to define(), or using a constant with a different case than declared, is deprecated.
PHP 7.3 deprecates declaring case-insensitive constants. Calling define() with true as its third argument now generates a deprecation, and using a constant with a case that differs from its declaration is deprecated as well. Case-insensitive constants were inconsistent with the rest of the language, where constants are normally case-sensitive.
The replacement is to declare constants as usual and refer to them with their exact spelling. Before you remove the third argument, search for call sites that rely on the loose matching, such as lowercase app_mode for a constant declared as APP_MODE. Those usages will need to be corrected at the same time, or they will start to fail. Later PHP releases removed case-insensitive constants entirely, so cleaning this up now also prepares you for newer versions.
define('APP_MODE', 'cli', true);
echo app_mode, "\n";PHP 7.2 accepts this silently; PHP 7.3 emits deprecations for the third argument and for the differing case. Shown for comparison; not executed on this site.
define('APP_MODE', 'cli');
echo APP_MODE, "\n";
const APP_NAME = 'Demo';
echo APP_NAME, "\n";cli Demo
DEPRECATED
Non-string needles in string searches are deprecated
Passing a non-string needle to strpos() and related search functions is deprecated because it was treated as an ASCII code point.
The string search functions, including strpos(), strrpos(), stripos(), strripos(), strstr(), strchr(), strrchr() and stristr(), historically treated an integer needle as the ASCII code of a single character. PHP 7.3 deprecates passing any non-string needle, because a future version will interpret it as a string instead. The same call could then silently mean something different.
Make your intent explicit. If you meant a character code, pass chr($code). If you meant the digits of a number, cast with (string) $value. Both forms behave the same in PHP 7.3 and in later versions, which makes the change safe to apply before upgrading. Look especially for needles coming from variables or user input, where the type is not obvious from the call site.
$haystack = 'Hello A';
echo strpos($haystack, 65), "\n";PHP 7.2 treats 65 as the character A without complaint; PHP 7.3 emits a deprecation, and later releases treat the needle as a string. Shown for comparison; not executed on this site.
$haystack = 'Hello A';
echo strpos($haystack, chr(65)), "\n";
echo strpos('order 66', (string) 66), "\n";6 6
NEW
instanceof accepts literals
A literal may now be the left operand of instanceof, and the result is always false.
Before PHP 7.3, putting a literal on the left of instanceof was a fatal error. Now it is accepted and always evaluates to false, because a scalar literal is never an object. This is mostly useful for generated code and macro-like constructs where the left operand may be a constant, and it removes a compile-time failure in those cases.
In ordinary application code you rarely write this on purpose, so treat a literal on the left as a sign of a bug, such as a quoted class name where you meant a variable holding an object. The quoted string "User" is not an instance of User. Because the answer is always false, static analysis tools may flag such code as dead logic.
class User {}
var_dump("User" instanceof User);PHP 7.2 stops with a fatal error because a constant is given; PHP 7.3 and later evaluate it to false. Shown for comparison; not executed on this site.
class User {}
var_dump("User" instanceof User);
var_dump((new User()) instanceof User);bool(false) bool(true)
NEW
CompileError as parent of ParseError
PHP 7.3 adds CompileError, which ParseError now extends, and a few compilation failures throw it instead of raising a fatal error.
PHP 7.3 introduces the CompileError exception. ParseError now inherits from it, so existing catch (ParseError $e) blocks keep working and a catch (CompileError $e) block sees both. A small number of compilation errors, currently only those that token_get_all() can raise in TOKEN_PARSE mode, now throw CompileError instead of ending the script with a fatal error.
For most applications nothing needs to change. The impact is on tools that parse PHP source, such as linters, code generators and static analyzers. They can now catch the exception and continue instead of dying. The packet notes that more errors may be converted in the future, so do not assume the current list is final. Catch CompileError when you want to handle any compile-time failure, and keep ParseError handlers where you only care about syntax errors.
try {
// code that parses source text
} catch (ParseError $e) {
// handle syntax errors only
}PHP 7.2 has no CompileError class; catching ParseError still works in PHP 7.3 and later. Shown for comparison; not executed on this site.
var_dump(is_subclass_of('ParseError', 'CompileError'));
$e = new ParseError('example');
var_dump($e instanceof CompileError);
var_dump($e instanceof Error);bool(true) bool(true) bool(true)