Upgrades to Newer PHP and Libraries

The upgrading section decides on upgrading code to a newer PHP and to new library versions: what a newer PHP can write better, what it deprecated and what to replace it with, and what the libraries your project uses have renamed or removed.

The decisions of the section are grouped by what they are about: syntax covers new syntax (?->, match, the pipe operator, promoted properties, readonly), functions new functions (str_contains(), array_any(), clamp()), classes attributes and interfaces that PHP added (#[\Override], Stringable), phpdoc annotations that an attribute or a keyword replaced, php what PHP deprecated, api replacement maps for the API of libraries and of your own code, and declarations uses of what your own code marked @deprecated. The value required turns on the newer form (“write the newer form wherever you find the older one”), and the value forbidden turns on replacing a deprecated form (“the deprecated form must not be here”).

upgrading:
	functions:
		substringFunctions: required
	syntax:
		nullsafe: required
		pipe: required
function describe(?User $user, string $url): string
{
	if (strpos($url, 'https://') === 0) {  // The `strpos()` comparison must be written with `str_starts_with()`.
		$scheme = 'secure';
	}
	$name = $user === null ? null : $user->getName();  // The ternary testing for null must be written with `?->`.
	return trim(strtolower(strip_tags($name)));  // The nested calls must be written with the pipe operator.
}
function describe(?User $user, string $url): string
{
	if (str_starts_with($url, 'https://')) {
		$scheme = 'secure';
	}
	$name = $user?->getName();
	return $name |> strip_tags(...) |> strtolower(...) |> trim(...);
}

Each decision that writes newer PHP code knows which PHP version introduced it, and it applies only when the target PHP version of the project allows it. The pipe operator from the example gets into the code only when the composer.json of the project allows nothing older than PHP 8.5; until then, dresscode config prints no effect: php next to the decision. Deprecations, on the other hand, are always reported, because code written for an older PHP will one day run on a version that has deprecated it.

You do not have to write the decisions of the section one by one. The modernizations set turns on everything that writes a newer form, and the deprecations set everything that fixes deprecated code. You add both on top of the standard in the use key, and as DressCode gains new decisions, the sets bring them in without any change to your configuration. Upgrading to a Newer PHP shows what upgrading to a newer PHP looks like in practice, and Code Upgrade shows how to upgrade libraries.

PHP Migration Guides

DressCode carries what PHP deprecated, and how the same thing is written today, as the data of the package php. This data has the same format as the upgrade data of libraries in the upgrade packages: a deprecated function or constant is reported, utf8_encode() is rewritten to mb_convert_encoding(), and a call that does nothing on the target version, such as curl_close(), disappears. A single requirement, upgrading.php.deprecatedApi, turns on the whole guide, and everything the data finds is reported under it, so this is also the name you use in --only, severity, fixRisky or in a dresscode:ignore comment. To exclude a single item of the data, set it to keep in the map that the item belongs to:

upgrading:
	php:
		deprecatedApi: forbidden
	api:
		replacedCalls:
			'curl_close($handle)': keep

No map can name an argument value that PHP deprecated, such as E_USER_ERROR passed to trigger_error() or an integer passed to a ctype_*() function. A separate decision, upgrading.php.deprecatedArgument, reports such a value. The nette standard and the deprecations set turn on both decisions.

API Replacement Maps

The keys under upgrading.api are replacement maps: a class for a class, a member for a member, a call for a template, a forbidden function with advice on what to write instead. A project writes them when it renames its own code, and upgrade packages carry them for whole ecosystems. The decision upgrading.packages: yes, which both sets, deprecations and modernizations, turn on, adds the package data as a layer below the maps of the project. The value keep takes an item the project does not want out of the maps (Acme\Mail\SmtpTransport: keep), and an exclamation mark after a map key (replacedClasses!:) replaces the maps of lower layers and the package data with what the project writes. The page Replacement Maps describes the grammar of the maps.

upgrading.functions.array_is_list

array_is_list() for a hand-written test of a list.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.1 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.nullArrayKey

[null => 1] is ['' => 1], which it becomes.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.spreadForArrayMerge

[...$a, ...$b] for array_merge($a, $b).

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.1 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.dynamicClassConstant

X::{$name} for constant(X::class . '::' . $name).

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.3 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.constructorReturnValue

A return with a value in a constructor or destructor, deprecated by PHP 8.6.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.sleepAndWakeup

__sleep() and __wakeup(), which PHP 8.5 soft-deprecated.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep · covers modern_serialization_methods

upgrading.classes.Override

#[\Override] on a member overriding an inherited one.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.3 · needs types · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.promotedProperties

A constructor parameter only assigned to a property.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep · covers SlevomatCodingStandard.Classes.RequireConstructorPropertyPromotion

upgrading.syntax.readonlyClasses

A class whose every property is readonly.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.2 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.phpdoc.readonly

readonly for @readonly.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.1 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep · covers phpdoc_readonly_class_comment_to_keyword

upgrading.classes.Stringable

A class with __toString() declares it.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.functions.arraySearchFunctions

array_any(), array_all(), array_find() and array_find_key() for a foreach or an array_filter() that only asks what they answer.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.4 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.match

match for a switch whose every case returns or assigns one value.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.returnInFinally

A return inside finally, which PHP 8.6 deprecated.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.caseSemicolon

case 1; and default; are case 1: and default:.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs forbidden, psr12 forbidden, nette forbidden, symfony forbidden, laravel forbidden · covers switch_case_semicolon_to_colon, PSR2.ControlStructures.SwitchDeclaration

upgrading.syntax.throwExpression

$a ?? throw new E() for an if throwing where the assigned value is null.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.cloneWith

clone($o, [...]) for a clone followed by assignments to the copy.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.5 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.backticks

The backtick operator PHP 8.5 deprecated, written as a shell_exec() call.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette forbidden, symfony forbidden, laravel keep · covers backtick_to_shell_exec

upgrading.syntax.nullsafe

$a?->b for $a !== null ? $a->b : null and $a === null ? null : $a->b.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep · covers SlevomatCodingStandard.ControlStructures.RequireNullSafeObjectOperator

upgrading.syntax.pipe

Calls nested one in another, each handing its result to the next, written with the pipe operator of PHP 8.5 in the order they run: $x |> a(...) |> b(...) for b(a($x)).

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.5 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.pipeMinCalls

The calls a nest has at least for the pipe operator to be written.

  • a number from 2

Default 3 · PHP 8.5 · standards: perCs 3, psr12 3, nette 3, symfony 3, laravel 3

upgrading.functions.arrayFirstLast

array_first(), array_last() for reset(), end() read as an item.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.5 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.firstClassCallables

foo(...) for Closure::fromCallable(), a forwarding closure and 'self::foo'; from PHP 8.6 a partial application.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.1 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.functions.clamp

clamp($x, $lo, $hi) for max($lo, min($x, $hi)).

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.6 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.csvEscapeArgument

The escape argument of fgetcsv() and kin written out, since PHP 8.4 deprecated relying on its default.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.functions.get_debug_type

get_debug_type($x) for is_object($x) ? get_class($x) : gettype($x).

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.functions.json_validate

json_validate($s) for json_decode($s) !== null && json_last_error() === JSON_ERROR_NONE.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.3 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.syntax.newInInitializer

Clock $clock = new SystemClock for a null default the body swaps for the object.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.1 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.functions.substringFunctions

str_contains(), str_starts_with(), str_ends_with() for strpos() !== false, substr() ===.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.0 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep · covers modernize_strpos

upgrading.functions.roundingMode

RoundingMode::HalfAwayFromZero for PHP_ROUND_HALF_UP.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.4 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.classes.SensitiveParameter

#[\SensitiveParameter] of PHP 8.2 on a parameter holding a secret, which keeps its value out of stack traces.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.2 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.classes.SensitiveParameterNames

The names of the parameters holding a secret, whatever their letter case and underscores.

  • a list of names

Default [password, passphrase, secret, apiKey, privateKey] · PHP 8.2 · standards: perCs [password, passphrase, secret, apiKey, privateKey], psr12 [password, passphrase, secret, apiKey, privateKey], nette [password, passphrase, secret, apiKey, privateKey], symfony [password, passphrase, secret, apiKey, privateKey], laravel [password, passphrase, secret, apiKey, privateKey]

upgrading.php.dollarBraceInterpolation

${name} is {$name}.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette forbidden, symfony forbidden, laravel keep · covers simple_to_complex_string_variable

upgrading.syntax.octalPrefix

0o17 for 017.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.1 · standards: perCs keep, psr12 keep, nette required, symfony keep, laravel keep · covers octal_notation

upgrading:
	syntax:
		octalPrefix: required
$mode = 0755;  // The octal number `0755` must be written `0o755`.
$zero = 0;
$flags = 0x1F;
$mode = 0o755;
$zero = 0;
$flags = 0x1F;

upgrading.php.reservedNames

Names PHP 8.6 reserved, in declarations and imports.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.phpdoc.deprecated

#[\Deprecated] for @deprecated.

  • required: always there
  • keep: nothing is enforced, the code stays as it is

Default keep · PHP 8.4 · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.implicitNullable

A parameter type made nullable by its default null alone, int $x = null, deprecated by PHP 8.4 and written ?int $x = null.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette forbidden, symfony forbidden, laravel forbidden · covers nullable_type_declaration_for_default_null_value, SlevomatCodingStandard.TypeHints.NullableTypeForNullDefaultValue

upgrading.packages

Whether the code follows the upgrading files of the installed packages, their maps laid under those the project writes and the rules of the plugin of a package turned on where its files say.

  • yes or no

Default no · standards: perCs no, psr12 no, nette no, symfony no, laravel no

upgrading.api.replacedClasses

A class written instead of another one, both fully qualified (Acme\Old\Mailer: Acme\Mail\Mailer).

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.replacedFunctions

A function written instead of another one (acme_send: Acme\Mail\send).

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.deprecatedApi

A function, a constant or a member PHP deprecated, written as what replaces it as the upgrading data of PHP say, mb_convert_encoding() for utf8_encode(), or reported.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette forbidden, symfony keep, laravel keep · covers Generic.PHP.DeprecatedFunctions

upgrading.api.replacedMembers

A constant, a method or a property written instead of another one of the class (Acme\Mail\Mailer::send(): sendMessage()).

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · needs types · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.replacedCalls

A call written as the template says (Acme\Mail\Mailer::send($to, $body): send(new Message($to, $body))).

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.forbiddenClasses

A class that may not be used, with what to do instead (Acme\Legacy\Db: "use the repository").

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.forbiddenFunctions

A function that may not be called, with what to do instead.

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep · covers Generic.PHP.ForbiddenFunctions, Generic.PHP.SAPIUsage, Squiz.PHP.DiscouragedFunctions

upgrading.api.forbiddenConstants

A global constant that may not be used, with what to do instead (E_STRICT: "it no longer does anything").

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.forbiddenMembers

A constant, a method or a property that may not be used, with what to do instead.

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · needs types · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.attributeForAnnotation

An attribute written for an annotation (@ORM\Entity: ORM\Entity).

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.api.attributeForMember

An attribute written for a member the class declares by convention.

  • a map in the grammar of replacement maps, an entry is withdrawn with the value keep
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.declarations.deprecatedClass

A class whose declaration is @deprecated, replaced by the class the deprecation names, reported where it names none.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · needs types · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.declarations.deprecatedMember

A constant, method or property whose declaration is @deprecated, replaced by the member the deprecation names, reported where it names none.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · needs types · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

The replacement can be a member of the same class, and for a static call also a public static method of another class; DressCode writes it the way the file can reach it and adds an import where needed. The replacement must accept the call the same way as the original method; parameter names matter only where the call passes arguments by name. But the annotation is true only for the installed version. Where the deprecated class or its replacement belongs to a package installed in a newer version than the lowest version the composer.json of the project allows, DressCode rewrites nothing, because the replacement may not exist in the older version, and it does not report the deprecated class or member of such a package either. To install the lowest versions into vendor, run composer update --prefer-lowest. For a package that the project does not require itself, or requires without a lower bound, the deprecated use is only reported. The fix is risky, like every replacement based on an annotation: the annotation says what to write, not that it behaves the same, so the fix is made only with fixRisky or --fix-risky. A method called on an object whose replacement is in another class is only reported, never rewritten.

typeAnalysis: phpstan

upgrading:
	declarations:
		deprecatedMember: forbidden
use Acme\Text\Text;

$valid = Text::check($s);  // Static method `Acme\Text\Text::check()` is deprecated: use Acme\Text\Encoding::isValid().
use Acme\Text\Encoding;
use Acme\Text\Text;

$valid = Encoding::isValid($s);

upgrading.declarations.deprecatedFunction

A function whose declaration is @deprecated, replaced by the function the deprecation names, reported where it names none.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · needs types · standards: perCs keep, psr12 keep, nette keep, symfony keep, laravel keep

upgrading.php.deprecatedArgument

An argument whose value PHP deprecated is reported, E_USER_ERROR given to trigger_error() or an integer to a ctype_*() function.

  • forbidden: never there
  • keep: nothing is enforced, the code stays as it is

Default keep · standards: perCs keep, psr12 keep, nette forbidden, symfony keep, laravel keep