HardПрактика5 min

Руководство по миграции

Breaking changes, таблица совместимости, инструменты миграции: Rector, PHPStan

Руководство по миграции 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;