Руководство по миграции PHP
Основные Breaking Changes
PHP 7.x -> 8.0
Это самый сложный переход с точки зрения обратной совместимости.
<?php
// 1. Изменение поведения сравнений (самое опасное!)
// PHP 7: 0 == "foo" => true (нестрогое сравнение приводит строку к 0)
// PHP 8: 0 == "foo" => false (если строка не числовая, сравнение по строке)
// Было:
var_dump(0 == 'foo'); // PHP 7: true | PHP 8: false
var_dump(0 == ''); // PHP 7: true | PHP 8: false
var_dump(0 == 'not a number'); // PHP 7: true | PHP 8: false
var_dump(0 == null); // PHP 7: true | PHP 8: true (не изменилось)
// 2. match строже чем switch
// switch использует ==, match использует ===
$val = '0';
$result = match ($val) {
0 => 'integer zero',
'0' => 'string zero', // Сработает в PHP 8 (строгое сравнение)
};
// 3. Ошибки вместо предупреждений
// PHP 7: Warning для undefined variable
// PHP 8: Warning для undefined variable (без изменений)
// Но: много E_WARNING стали TypeError/ValueError
// strlen(null) — Warning в PHP 7, TypeError в PHP 8
// array_merge(null) — Warning в PHP 7, TypeError в PHP 8
// sort(null) — Warning в PHP 7, TypeError в PHP 8
Критические изменения PHP 7.x -> 8.0
<?php
// 4. @ оператор не подавляет фатальные ошибки
// PHP 7: @ подавлял почти всё
// PHP 8: @ не подавляет E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR
// 5. Изменение приоритета конкатенации
// PHP 7: echo "Sum: " . 1 + 2; => "Sum: 12" (конкатенация как +)
// PHP 8: echo "Sum: " . 1 + 2; => Deprecated, используйте скобки
echo "Sum: " . (1 + 2); // Правильно в обеих версиях
// 6. Именованные аргументы при вызове internal functions
// PHP 8 добавил named arguments — нужно учитывать имена параметров
// 7. Reflection API изменения
// ReflectionParameter::getClass() — Deprecated
// Используйте: ReflectionParameter::getType()
PHP 8.0 -> 8.1
<?php
// 1. Intersection types не допускают дубликаты
// Countable&Countable — Fatal error
// 2. Readonly свойства нельзя unset
// unset($obj->readonlyProp); // Error
// 3. Fiber — новые исключения
// FiberError при неправильном использовании
// 4. Внутренние функции возвращают правильные типы
// Раньше внутренние функции могли вернуть не тот тип
// Теперь строже
// 5. Serialization changes
// __serialize()/__unserialize() — рекомендуемые вместо Serializable interface
PHP 8.1 -> 8.2
<?php
// 1. Dynamic properties deprecated — САМОЕ ВАЖНОЕ
class User
{
public string $name;
}
$user = new User();
$user->undeclared = 'value'; // Deprecated в 8.2!
// Решение: добавить #[AllowDynamicProperties] или объявить свойства
// 2. Partially supported callables deprecated
// "self::method" как callable — Deprecated
// "parent::method" как callable — Deprecated
// "static::method" как callable — Deprecated
// Используйте: self::method(...) или Closure::fromCallable()
// 3. Неявное преобразование float в int — Deprecated
$arr = [];
$arr[1.5] = 'value'; // Deprecated: ключ будет 1
// 4. ${var} в строках — Deprecated
$name = 'world';
echo "Hello ${name}"; // Deprecated
echo "Hello {$name}"; // OK
echo "Hello $name"; // OK
PHP 8.2 -> 8.3
<?php
// 1. Изменения в DateTimeInterface
// DateTime::createFromInterface() — deprecated предупреждения
// 2. Улучшенные range() проверки
range('a', 0); // ValueError в PHP 8.3 (было бы неожиданный результат)
range('', 'z'); // ValueError
range(1, 2, 0); // ValueError (шаг 0)
// 3. unserialize() строже с классами
// E_WARNING при ошибках unserialize -> ValueError
// 4. Изменения в assert()
// assert() с string аргументом — Deprecated (уже давно, но строже)
PHP 8.3 -> 8.4
<?php
// 1. Неявный nullable deprecated
function test(string $param = null): void {}
// PHP 8.4: Deprecated implicit nullable
// Правильно:
function test(?string $param = null): void {}
// 2. E_STRICT полностью удалён
// 3. Изменения в DOM API
// Новые классы Dom\* рядом со старыми DOMDocument
// Рекомендуется миграция на Dom\HTMLDocument
// 4. array_merge() с одним аргументом — без изменений
// Но стоит проверить код, использующий ...spread с ассоциативными массивами
PHP 8.4 -> 8.5
<?php
// 1. Неканонические касты deprecated
$x = (integer) $val; // Deprecated — используйте (int)
$x = (boolean) $val; // Deprecated — используйте (bool)
$x = (double) $val; // Deprecated — используйте (float)
// 2. Backticks deprecated (используйте shell_exec())
$output = `ls -la`; // Deprecated
$output = shell_exec('ls -la'); // OK
// 3. Переобъявление констант deprecated
define('FOO', 1);
define('FOO', 2); // Deprecated в PHP 8.5
// 4. curl_close() / curl_share_close() — no-op (deprecated)
// Ресурсы освобождаются автоматически с PHP 8.0
// 5. xml_parser_free() — no-op (deprecated)
// Ресурсы освобождаются автоматически с PHP 8.0
// 6. socket_set_timeout() deprecated
// Используйте: stream_set_timeout()
// 7. MHASH_* constants deprecated
// Используйте HASH_* константы
// 8. Новые возможности для принятия:
// - Pipe operator |>
// - clone() с модификацией свойств
// - URI extension (Uri\Rfc3986\Uri)
// - #[\NoDiscard] attribute
// - array_first() / array_last()
// - Closures в константных выражениях
Таблица совместимости
Ключевые функции по версиям
| Функция |
7.0 |
7.1 |
7.2 |
7.3 |
7.4 |
8.0 |
8.1 |
8.2 |
8.3 |
8.4 |
8.5 |
| Скалярные type hints |
+ |
|
|
|
|
|
|
|
|
|
|
Nullable types ? |
|
+ |
|
|
|
|
|
|
|
|
|
| Typed properties |
|
|
|
|
+ |
|
|
|
|
|
|
| Union types |
|
|
|
|
|
+ |
|
|
|
|
|
| Intersection types |
|
|
|
|
|
|
+ |
|
|
|
|
| DNF types |
|
|
|
|
|
|
|
+ |
|
|
|
| Enums |
|
|
|
|
|
|
+ |
|
|
|
|
| Readonly properties |
|
|
|
|
|
|
+ |
|
|
|
|
| Readonly classes |
|
|
|
|
|
|
|
+ |
|
|
|
| Property hooks |
|
|
|
|
|
|
|
|
|
+ |
|
| Asymmetric visibility |
|
|
|
|
|
|
|
|
|
+ |
|
| Typed constants |
|
|
|
|
|
|
|
|
+ |
|
|
| Named arguments |
|
|
|
|
|
+ |
|
|
|
|
|
| Match expression |
|
|
|
|
|
+ |
|
|
|
|
|
| Arrow functions |
|
|
|
|
+ |
|
|
|
|
|
|
#[Attributes] |
|
|
|
|
|
+ |
|
|
|
|
|
| Fibers |
|
|
|
|
|
|
+ |
|
|
|
|
| JIT |
|
|
|
|
|
+ |
|
|
|
|
|
Pipe operator |> |
|
|
|
|
|
|
|
|
|
|
+ |
| Clone with |
|
|
|
|
|
|
|
|
|
|
+ |
| URI extension |
|
|
|
|
|
|
|
|
|
|
+ |
#[\NoDiscard] |
|
|
|
|
|
|
|
|
|
|
+ |
array_first/last |
|
|
|
|
|
|
|
|
|
|
+ |
Deprecated -> Removed
| Функция |
Deprecated |
Removed |
Замена |
each() |
7.2 |
8.0 |
foreach |
create_function() |
7.2 |
8.0 |
Анонимные функции |
mbstring.func_overload |
7.2 |
8.0 |
mb_* функции явно |
| Dynamic properties |
8.2 |
9.0 |
#[AllowDynamicProperties] или объявление свойств |
${var} в строках |
8.2 |
9.0 |
{$var} |
utf8_encode/decode |
8.2 |
9.0 |
mb_convert_encoding() |
| Implicit nullable |
8.4 |
9.0 |
Явный ?type |
Serializable interface |
8.1 |
9.0 |
__serialize()/__unserialize() |
Инструменты миграции
Rector — автоматический рефакторинг
<?php
// rector.php — конфигурация
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;
use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
return RectorConfig::configure()
->withPaths([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->withPhpSets(php84: true) // Обновить до PHP 8.4
->withSets([
SetList::CODE_QUALITY,
SetList::DEAD_CODE,
SetList::EARLY_RETURN,
SetList::TYPE_DECLARATION,
])
->withRules([
AddVoidReturnTypeWhereNoReturnRector::class,
])
->withSkip([
__DIR__ . '/src/Legacy', // Пропустить legacy-код
]);
Запуск Rector
# Предпросмотр изменений (dry run)
vendor/bin/rector process --dry-run
# Применение изменений
vendor/bin/rector process
# Только конкретный набор правил
vendor/bin/rector process --set php84
Полезные правила Rector
<?php
// Автоматическое добавление типов
// До:
function sum($a, $b) { return $a + $b; }
// После:
function sum(int $a, int $b): int { return $a + $b; }
// Замена array() на []
// До:
$data = array(1, 2, 3);
// После:
$data = [1, 2, 3];
// Замена get_class() на ::class
// До:
$class = get_class($object);
// После:
$class = $object::class;
// Замена switch на match
// До:
switch ($status) {
case 'active': $label = 'Active'; break;
case 'inactive': $label = 'Inactive'; break;
default: $label = 'Unknown';
}
// После:
$label = match ($status) {
'active' => 'Active',
'inactive' => 'Inactive',
default => 'Unknown',
};
// Добавление readonly
// До:
class User {
private int $id;
public function __construct(int $id) { $this->id = $id; }
public function getId(): int { return $this->id; }
}
// После:
class User {
public function __construct(private readonly int $id) {}
public function getId(): int { return $this->id; }
}
PHPStan — статический анализ
<?php
// phpstan.neon
// includes:
// - vendor/phpstan/phpstan/conf/bleedingEdge.neon
//
// parameters:
// phpVersion: 80400 # Целевая версия PHP
// level: 9 # Максимальный уровень
// paths:
// - src
// - tests
// excludePaths:
// - src/Legacy
// treatPhpDocTypesAsCertain: false
// PHPStan находит проблемы совместимости:
// - Вызов deprecated функций
// - Неправильные типы параметров
// - Missing return types
// - Несовместимые типы свойств
# Запуск анализа
vendor/bin/phpstan analyse
# Конкретный уровень
vendor/bin/phpstan analyse -l 6
# Генерация baseline (для legacy-проектов)
vendor/bin/phpstan analyse --generate-baseline
PHP_CodeSniffer
# Проверка стандартов
vendor/bin/phpcs --standard=PSR12 src/
# Автоисправление
vendor/bin/phpcbf --standard=PSR12 src/
Стратегия миграции
Пошаговый план
1. Подготовка
├── Обновить зависимости (composer update)
├── Проверить совместимость пакетов
├── Настроить CI с целевой версией PHP
└── Создать baseline PHPStan
2. Автоматизация
├── Настроить Rector для целевой версии
├── Запустить rector --dry-run
├── Просмотреть и применить изменения
└── Запустить тесты
3. Ручная проверка
├── Проверить breaking changes из changelog
├── Поискать deprecated функции
├── Обновить конфигурацию (php.ini)
└── Проверить расширения
4. Тестирование
├── Unit-тесты
├── Integration-тесты
├── Нагрузочное тестирование
└── Staging environment
5. Развёртывание
├── Canary deployment
├── Мониторинг ошибок
└── Rollback план
Composer: проверка совместимости
{
"require": {
"php": "^8.4"
},
"config": {
"platform": {
"php": "8.4.0"
}
}
}
# Проверка совместимости пакетов
composer why-not php 8.4
# Обновление с проверкой
composer update --with-all-dependencies
Чеклист миграции
PHP 7.4 -> 8.0
- Проверить нестрогие сравнения (
==) с 0 и строками
- Заменить
switch на match где уместно
- Удалить
@ перед критичными операциями
- Проверить именования параметров (стали частью API)
- Обновить Reflection API вызовы
- Заменить
get_class($obj) на $obj::class
PHP 8.0 -> 8.1
- Рассмотреть Enums для статусов и типов
- Добавить
readonly к immutable свойствам
- Заменить
Closure::fromCallable() на func(...)
- Проверить
Serializable interface -> __serialize()
PHP 8.1 -> 8.2
- Найти и исправить dynamic properties
- Заменить
${var} на {$var} в строках
- Проверить partially supported callables
- Рассмотреть
readonly class для DTO
PHP 8.2 -> 8.3
- Добавить типы к константам классов
- Добавить
#[\Override] к переопределённым методам
- Заменить
json_decode() + проверку на json_validate() где нужна только валидация
- Проверить
range() вызовы
PHP 8.3 -> 8.4
- Исправить implicit nullable (
string $x = null -> ?string $x = null)
- Рассмотреть property hooks для геттеров/сеттеров
- Рассмотреть asymmetric visibility для DTO
- Заменить старый DOM API на
Dom\HTMLDocument
PHP 8.4 -> 8.5
- Заменить
(integer) → (int), (boolean) → (bool), (double) → (float)
- Заменить backticks
`cmd` на shell_exec('cmd')
- Убрать повторные
define() одной константы
- Удалить вызовы
curl_close(), xml_parser_free() (no-op)
- Заменить
socket_set_timeout() на stream_set_timeout()
- Заменить
MHASH_* константы на HASH_*
- Рассмотреть pipe operator
|> для цепочек преобразований
- Рассмотреть
clone() с аргументами для readonly классов
- Рассмотреть
Uri\Rfc3986\Uri вместо parse_url()
- Добавить
#[\NoDiscard] к важным return values
- Заменить
$arr[array_key_first($arr)] на array_first($arr)
Типичные проблемы при миграции
Проблема 1: Dynamic properties в ORM
<?php
// Проблема: Doctrine entities с magic properties
// Решение: Добавить #[AllowDynamicProperties] или объявить свойства
// До:
class OldEntity {}
$entity = new OldEntity();
$entity->name = 'Test'; // Deprecated
// После:
class NewEntity
{
public ?string $name = null;
public ?int $age = null;
}
Проблема 2: Изменение сравнений
<?php
// Проблема: switch case 0 ловил строки
// До (PHP 7):
switch ($input) {
case 0: echo 'zero or string'; break; // Ловил '', 'abc', etc.
}
// После (PHP 8):
switch ($input) {
case 0: echo 'only zero'; break;
case '': echo 'empty string'; break;
default: echo 'other string';
}
// Или лучше: match с ===
Проблема 3: Strict types для internal functions
<?php
// PHP 8 строже с типами внутренних функций
// strlen(null) — TypeError
// array_push($notArray, 1) — TypeError
// str_contains(null, 'x') — TypeError
// Решение: проверка типов перед вызовом
$length = $value !== null ? strlen($value) : 0;