Привет! Меня зовут Александр Володин.

Я PHP backend developer из компании Skyeng.
Опыт разработки более 8 лет.

С выходом PHP 8 мне захотелось скорее использовать все новые фичи релиза, поэтому я взял свой рабочий проект и... поправил весь код руками. Сначала это было интересно, затем монотонно, а к середине рефакторинга превратилось в наказание. Ох, PHP 8, ты классный, но второй такой рефакторинг я не потяну. И тогда я задался вопросом: есть ли такой инструмент, который автоматически переводил бы код на новую версию синтаксиса? Так я познакомился с Rector.

В статье поделюсь, как этот инструмент автоматического рефакторинга помогает обуздать легаси и автоматизировать обновление проектов и пакетов, чтобы процесс проходил эффективнее и малой кровью. 

Содержание статьи:

  1. Знакомство с Rector

  2. Rector и обновление пакетов

  3. Rector и архитектурный рефакторинг

  4. Итоги и полезные материалы

Знакомство с Rector

Rector — это инструмент автоматического рефакторинга, одной из главных функций которого является перевод кода на новые версии PHP и популярных фреймворков. 

Мой первый опыт использования Rector был просто фантастический. Я взял проект на PHP 7.3 со следующими входными данными:

  • Кол-во PHP-файлов: 608;

  • Кол-во строк PHP-кода: 28 976.

С помощью Rector я отрефакторил его под PHP 8, изменив более 7 тысяч строк кода! При этом на всё, включая настройку Rector, ушло полтора часа. Если бы я правил вручную, то это точно заняло бы кучу времени.

Rector разработал Томаш Вотруба. Он был на конференции PHP Russia в 2019 году, где в интерактивном режиме показал, как Rector улучшает качество кода. Вот его доклад.

У Rector есть неплохая документация и даже целая книга «The Power of Automated Refactoring» под авторством Томаша Вотруба и Матиаса Нобака. Ещё Томаш ведёт блог про Rector, где рассказывает про его новые фичи и практики применения, а также личный блог про обновления и различные технологии в целом. За этим очень интересно следить.

Rector — это по сути обёртка над PHP-Parser, которая использует PHPStan для анализа типов. Анализ руководствуется правилами (rules) — единицы рефакторинга, которые изменяют конкретную часть кода. Например, ужесточают типизацию:

Rector имеет более 400 правил рефакторинга, что очень много. Было бы неудобно искать из них нужные и добавлять по одному в конфиг. Поэтому схожие по смыслу правила объединяют в наборы (sets). Например, набор правил TYPE_DECLARATION улучшает строгую типизацию в коде:

return static function (RectorConfig $rectorConfig): void {
	$rectorConfig->sets([
		SetList::TYPE_DECLARATION,
	]);
};

Я разделяю все правила на три категории:

  1. Апгрейд/даунгрейд. Эти правила переводят код на новые версию PHP или фреймворка. Даунгрейд же возможен под более старые версии PHP.

  2. Качество кода. Эти правила помогают оптимизировать логику кода, удалить мертвый код или, например, улучшить типизацию. Я советую использовать их как часть CI.

  3. Настраиваемые правила. Эти правила нельзя бездумно закинуть в конфиг. Они настраиваются под определенный контекст и о них я расскажу позже — в части «Rector и обновление пакетов».

Место Rector среди других инструментов

Сам создатель Rector привёл хорошую классификацию всех инструментов. Он разделил их на те, которые репортят проблемы, и те, которые их фиксят, а также те, которые занимаются код-стайлом и логикой:

Из классификации видно, что главная цель Rector — фикс проблем в логике.

В чём фишка Rector?

Может возникнуть вопрос: «А как же PHP CS Fixer, у которого тоже есть правила рефакторинга код-стайла для разных версий PHP? Почему-бы не развивать его и добавить такие-же правила как у Rector?». Да, правила там есть, но они достаточно примитивные. Например, в новой версии PHP позволено ставить запятую в конце списка аргументов. PHP CS Fixer это поправить может, но это его максимум. Он не поможет перевести код на более сложные новые фичи, такие как Атрибуты.

Это связано с тем, что такие код-стайл-фиксеры (как PHP CS Fixer) представляют исходный код в виде последовательности токенов. Rector же работает с абстрактным синтаксическим деревом (AST).

Если я захочу после оператора «=» поставить пробел, то это легко сделать, когда всё представлено в виде последовательности токенов. Но когда это AST, то можно анализировать и изменять целые ветки логики приложения, не обращая внимания на пробелы, переносы и прочие нюансы код-стайла.

Тем не менее, у Rector есть аналоги, которые похожи на него по целям и принципу работы, но он всё равно превосходит их во всём на голову:

Аналоги Rector и их сравнение

Я нашёл 2 других инструмента, которые способны провести апгрейд кода:

  1. Phabelio/Phabel

  2. Slevomat/coding-standard

Сравнение по GitHub

Когда я выбираю между несколькими инструментами, то сначала смотрю, насколько они популярны в GitHub.

Тут мы видим, что Phabel не очень популярен, скорее аутсайдер, чего не скажешь о Rector и Slevomat Coding Standard. Они идут прямо впритык, но у Rector больше звёзд, активности и релизов в месяц. Также у него достаточно быстро фиксятся проблемы (issues на github), которые закидывает сообщество. Это свидетельствует о том, что продукт активно поддерживается и развивается.

Сравнение по возможности апгрейда до PHP 8

Нельзя сравнивать только по популярности. Нужно сравнить ещё по возможностям, но абсолютно все их сравнить сложно. Поэтому я решил сравнить, как эти инструменты справятся с переходом на PHP 8 (актуальной по сей день проблемой для многих проектов):

Phabel и Slevomat Coding Standard не покрывают половину фич PHP 8. Rector же покрывает все возможности, кроме Nullsafe оператора.

Также у Rector самый широкий диапазон поддержки версий PHP — у него есть правила для перехода начиная с версии 5.2, заканчивая последней на текущий момент — 8.2. У аналогов они начинаются только с 7-ой версии.

И это не говоря о том, что у Rector ещё есть правила для обновления под новые версии фреймворков, чего совсем нет у аналогов.

Таким образом: Rector — лучший инструмент для апгрейда кода.

Как начать rector’ить?

Чаще всего нельзя просто взять и отрефакторить код с помощью Rector. Поэтому расскажу про банальные шаги, которые нужно сделать перед этим процессом.

1. Улучшить покрытие тестами

Тесты позволят вам быстро проверить результат рефакторинга Rector и провести отладку. Ручная проверка займет гораздо больше времени и это более монотонный процесс, чем даже сам рефакторинг.

Поэтому мой совет: перед тем, как заниматься автоматическим изменением кода, лучше сделать автоматическую проверку.

2. Настроить статический анализ

Статический анализ — это уже культура ведения кода. Но кроме этого он нужен Rector, чтобы эффективнее проводить рефакторинг.

Я рекомендую использовать Psalm 6-8 уровня и ниже или PHPStan 3-4 уровня и выше. Но если вы только начинаете внедрять это в свой проект, лучше использовать PHPStan, так как Rector при анализе ориентируется на него.

3. Задать код-стайл

Rector не заботится о банальном код-стайле. Он занимается более «высокими» вещами. Для контролирования код-стайла рекомендую выбрать один из этих инструментов и запускать его после работы Rector:

- PHP CS Fixer;
- Slevomat Coding Standard;
- Easy Coding Standard.

Новичкам советую начинать с Easy Coding Standard.

4. Настроить Rector

Теперь нужно настроить конфиг rector.php, чтобы он делал то, что надо, а что не надо — не делал:

<?php // rector.php

use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
    // здесь будем настраивать
}

4.1 Указать, что рефакторить, а что пропустить.

В первую очередь нужно указать, какие директории в проекте рефакторить, а какие — скипнуть.

<?php // rector.php
...
    $rectorConfig->paths([
        __DIR__.'/src',
        __DIR__.'/tests',
    ]);
    $rectorConfig->skip([
        __DIR__.'/**/_generated/*',
    ]);
...

4.2 Применить параллельную обработку.

Можно значительно повысить скорость работы Rector благодаря конфигурации parallel.

<?php // rector.php
...
    $rectorConfig->parallel(seconds: 360);
...

4.3 Настроить импорт имён.

Когда Rector отрефакторит код, по умолчанию он выведет всё в виде fully-qualified class names (FQCN). Это сделает чтение кода неудобным, поэтому сразу рекомендую задать конфиги так, чтобы пространства имён импортировались.

<?php // rector.php
...
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses(false);
...

Можно задать опцию APPLY_AUTO_IMPORT_NAMES_ON_CHANGED_FILES_ONLY , чтобы этот импорт происходил только в файлах, которые изменил рефакторинг, а не абсолютно во всем проекте.

$rectorConfig->parameters()->set(Option::APPLY_AUTO_IMPORT_NAMES_ON_CHANGED_FILES_ONLY, true);

Но я обычно сразу привожу весь код к единому стилю и не устанавливаю эту опцию.

4.4 Добавить правила апгрейдов.

Рекомендуют сразу добавлять константы, которые позволяют перевести вас с любой версии PHP (хоть с 7.1 или 5.3) сразу на 8. То же самое сделать и для Symfony.

<?php // rector.php
...
    $rectorConfig->sets([
        LevelSetList::UP_TO_PHP_80,
        SymfonyLevelSetList::UP_TO_SYMFONY_54,
    ]);
...

Конфиг теперь выглядит так:

rector.php
<?php declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Core\Configuration\Option;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Symfony\Set\SymfonyLevelSetList;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__.'/src',
        __DIR__.'/tests',
    ]);
    $rectorConfig->skip([
        __DIR__.'/**/_generated/*',
    ]);

    $rectorConfig->parallel(seconds: 360);
    
    $rectorConfig->importNames();
    $rectorConfig->importShortClasses(false);
    $rectorConfig->parameters()->set(Option::APPLY_AUTO_IMPORT_NAMES_ON_CHANGED_FILES_ONLY, true);

    $rectorConfig->sets([
        LevelSetList::UP_TO_PHP_80,
        SymfonyLevelSetList::UP_TO_SYMFONY_54,
    ]);
} 

А вот ссылка на мой реальный рабочий конфиг.

5. Теперь можно rector’ить!

vendor/bin/rector process --clear-cache

Рекомендую запускать команду с --clear-cache, чтобы избежать ситуаций, когда кэш сформировался не полностью (например вы прервали предыдущий запуск Rector). Из-за этого он может обработать не все файлы.

Также команду можно выполнить в режиме --dry-run и посмотреть в консоли все предложения по рефакторингу.

Итоги этих работ

Может показаться, что предварительные работы отнимут немало времени. В каких-то заброшенных проектах — да. Но вы это сделаете один раз, а Rector будете запускать постоянно, потому что периодически будут выходить новые версии PHP, Symfony и других библиотек. Сам Rector развивается и в нем появляются новые правила оптимизации кода.

В конечном итоге Rector сэкономит вам много времени, сделает эту работу качественно и будет держать код на высоком уровне.

Rector и обновление пакетов

Надеюсь, я убедил вас использовать Rector для улучшения кода. Но обновлять в проектах, кроме нашего кода, приходится ещё сторонние пакеты. И тут сложностей не меньше.

У меня был печальный опыт поддержки пакета. Я смотрел на код, который просил архитектурной переделки, но был уже плотно интегрирован в исходный код других проектов из разных команд. Я проводил небольшие рефакторинги, но это всегда была непростая история как для меня, так и для тех, кто затем обновлял мой пакет у себя проекте.

И тогда я заинтересовался, как этот процесс можно упростить. 

Проблемы, которые я выделил

Со стороны пользователя, который обновляет пакеты, всегда надо:

  • Изучить особенности новой версии (changelog) перед обновлением, чтобы проанализировать, сколько работы надо проводить.

  • Поправить deprecated, внести новые требования версии.

  • Отладить работу. Хотя в результате всё равно на что-то можно наткнуться и придётся дебажить.

Со стороны мейнтейнера важно:

  • Поддерживать обратную совместимость.

  • Растягивать выход мажорных релизов. Потому что нельзя каждую версию делать мажорной, иначе пользователи с ума сойдут каждый раз обновлять.

  • Консультировать или помогать по переходу на новые версии.

Поиск решения

Я обратился к опыту обновления Symfony-проектов. Если отбросить нюансы разных версий, можно выделить три важных этапа:

  1. Symfony Flex — обновление проекта (структуру, конфиги, индекс и kernel PHP) с помощью рецептов symfony/flex.

  2. Rector — обновление кода проекта с помощью наборов правил Rector. То есть мы с помощью Rector и набора правил SymfonyLevelSetList::UP_TO_SYMFONY_54, обновляем код, адаптируем его под новую версию Symfony.

  3. Composer — обновление самих пакетов Symfony. На этом этапе мы накатываем новые Symfony-bundles в готовый проект: composer update symfony/*.

Здесь интересует второй этап. Как Rector адаптирует код? Я решил тоже написать свои наборы правил. 

Пишем свои наборы правил

Наборы правил Rector для обновления Symfony-проектов хранятся в отдельном репозитории. Для популярного опенсорс-пакета это хорошее решение. Но для небольшого бандла, который используется только в рамках организации это будет неудобно и расточительно, поэтому свои наборы правил я буду хранить прямо в пакете .

1. Задаём структуру для наборов правил в нашем пакете:


Я создал директорию utils/rector и в ней две важных папки: config, которая хранит сами наборы для перехода на новые версии, и src для класса с константами.

2. Создаём константы для наборов правил

<?php // utils/rector/src/Set/CmsSetList.php

declare(strict_types=1);

namespace Skyeng\CmsBundle\Utils\Rector\Set;

use Rector\Set\Contract\SetListInterface;

class CmsSetList implements SetListInterface
{
    public const VER_32 = __DIR__.'/../../config/sets/ver_32.php';
    public const VER_33 = __DIR__.'/../../config/sets/ver_33.php';
    public const VER_34 = __DIR__.'/../../config/sets/ver_34.php';
    public const VER_40 = __DIR__.'/../../config/sets/ver_40.php';

    public const UP_TO_LAST_VER = __DIR__.'/../../config/sets/up_to_last_ver.php';
}

Здесь константы, которые ссылаются на наборы правил для каждой версии. Это такой юзер интерфейс Rector, чтобы потом было удобно подключать их в конфиге проекта.

Также я добавил константу UP_TO_LAST_VER, которая заключает в себе все предыдущие наборы правил, тем самым позволяя нам обновляться с версии 3.2 сразу на 4.0. 

3. Пишем набор правил

Предположим, в новой версии пакета 4.0, мы переместили пачку сервисов в другое место, тогда у нас изменится namespace. Но проект, который использует наш пакет, будет искать эти сервисы по старому namespace, из-за чего возникнет критическая ошибка. Для того, чтобы в проекте namespace изменился на новый, используем правило RenameNamespaceRector.

Аналогичные действия мы можем сделать для методов, названия которых мы изменили или просто хотим, чтобы в проекте перешли на новую реализацию. Для этого используем RenameMethodRector.

В итоге, набор правил для перехода на версию 4.0, будет выглядеть следующим образом:

<?php // utils/rector/config/sets/ver_40.php

declare(strict_types=1);

use ...;

return static function (RectorConfig $rectorConfig): void {

    // меняет неймспейсы
    $rectorConfig->ruleWithConfiguration(RenameNamespaceRector::class, [
        'Skyeng\CmsBundle\UI\EasyAdminField' => 'Skyeng\CmsBundle\Infrastructure\EasyAdmin\Field',
    ]);

    // меняет все вызовы метода findByValue на findByName
    $rectorConfig->ruleWithConfiguration(RenameMethodRector::class, [
        new MethodCallRename(FieldRepository::class, 'findByValue', 'findByName'),
        new MethodCallRename(FieldRepositoryInterface::class, 'findByValue', 'findByName'),
    ]);
};

И это далеко не всё, что мы можем сделать с кодом.

Возможности встроенных настраиваемых правил:

  • Удаление: интерфейсов,  классов, трейтов, аргументов функций и методов.

  • Переименование: неймспейсов, классов, интерфейсов, констант, свойств, функций и методов.

  • Трансформация: замена вызова одних функций или методов на другие, замена одних сервисов на другие, изменение аргументов.

Все их можно найти в полном списке правил.

Если возможностей недостаточно, то можно легко написать свои правила.

Как создать свои правила в Rector

Наше правило должно реализовывать интерфейс PhpRectorInterface с двумя методами: 

  1. getNodeTypes — дает список классов узлов, которые поддерживает правило;

  2. refactor — делает всю остальную работу по дополнительной проверке и изменению узла (Node).

<?php declare (strict_types=1);

namespace Rector\Core\Contract\Rector;

use PhpParser\Node;

interface PhpRectorInterface
{
    /**
    * @return array<class-string<Node>>
    */
    public function getNodeTypes(): array;

    /**
     * @return Node|Node[]|null
     */
    public function refactor(Node $node);
}

Но лучше не реализовывать этот интерфейс напрямую, а наследоваться отAbstractRector, так как там куча вспомогательных сервисов для анализа и манипуяции над узлами.

Правила в Rector применяются следующим образом:

  • Rector парсит код в AST. 

  • Проходится по каждому узлу.

  • В каждом узле Rector перебирает все запущенные правила, вызывая у них метод getNodeTypes, тем самым сравнивая класс текущего узла с теми классами, что поддерживает правило. Если они совпадают, то узел обрабатывается в методе refactor.

Почти у всех правил алгоритм работы метода refactor одинаковый:

  • Сначала проверяется то, что логически это тот самый узел, который нужно менять (например, проверяется родитель узла, его атрибуты, данные, и прочий контекст)

  • Если узел не проходит эти проверки, то возвращается null, что говорит о том, что узел не был изменен.

  • Если узел прошел все проверки, то он изменяется и возвращается как результат работы метода refactor.

Совет: Правила должны быть идемпотентными, то есть их выполнение над одним и тем же кодом, должно приводить к одному и тому же результату.

Свои правила я храню в папке «/utils/rector/src» рядом с константами:

Также у  Rector есть своя обёртка над PHPunit, которая позволяет удобно писать unit-тесты для своих правил. Подробнее об этом в документации.

Как теперь выглядит процесс обновления 

Со стороны мейнтейнера

Чек-лист релиза пакета выглядит так:

  1. Внести правки в код пакета.

  2. Написать набор правил для перехода на новую версию: CmsSetRule::VER_40.

  3. Выпустить релиз новой версии 4.0.

Со стороны пользователя пакета или бандла 

Нужно выполнить три шага:

  1. Обновить пакет: composer update skyeng/cms-bundle;

  2. Добавить в конфиг rector.php набор правил: CmsSetRule::VER_40;

  3. Запустить Rector: vendor/bin/rector process.

Эти три шага позволят пользователю без лишних страданий обновлять пакет в проекте и не беспокоиться, что в коде что-то отвалилось и не работает.

Но если интересно, то и этот процесс можно ещё сократить:

Как еще упростить процесс с Composer Scripts

Для дальнейшей оптимизации мы используем Composer Scripts и подписываемся на события обновления пакетов.

# composer.json
{
    ...
    "scripts": {
        "post-package-update": [
            "App\\Infrastructure\\Composer\\EventHandler::postPackageUpdate"
        ],
    }
    ...
}

Пишем EventHandler для Composer со следующей логикой:

<?php // src/Infrastructure/Composer/EventHandler.php

declare(strict_types=1);

namespace App\Infrastructure\Composer;

use Composer\Installer\PackageEvent;
use Composer\Util\ProcessExecutor;

class EventHandler
{
    public static function postPackageUpdate(PackageEvent $event): void
    {
        // 1. Проверяем, что обновился нужный пакет.
      
        if ($event->getOperation()->getTargetPackage()->getName() !== 'skyeng/cms-bundle') {
            return;
        }

        // 2. Спрашиваем пользователя, хочет ли он, чтобы Rector
        // обновил код. Не все люди любят, чтобы код автоматически 
        // изменялся, поэтому я добавил эту возможность.
        
        if ('y' !== $event->getIO()->ask('Execute Rector script? [y,n]', 'y')) {
            return;
        }

        // 3. Запускаем Rector с особым конфигом.
        
        (new ProcessExecutor($event->getIO()))->execute(
            'vendor/bin/rector process --config=cms-bundle-rector.php --clear-cache'
        );
    }
}

Создаем особый конфиг cms-bundle-rector.php , содержащий в наборах правил только одну константу CmsSetList::UP_TO_LAST_VER - которая всегда переводит на последнюю версию пакета.

<?php // cms-bundle-rector.php

declare(strict_types=1);

use App\Utils\Rector\CommonRectorConfig;
use Rector\Config\RectorConfig;
use Skyeng\CmsBundle\Utils\Rector\Set\CmsSetList;

return static function (RectorConfig rectorConfig){
    $rectorConfig->sets([
        CmsSetList::UP_TO_LAST_VER,
    ]);
};

Готово. Теперь весь процесс обновления состоит из вызова одной команды:

composer update skyeng/cms-bundle

То есть она одновременно и обновляет пакет и адаптирует код проекта под новую версию пакета!

Преимущества подхода

Для пользователя:

  • Не нужно погружаться в детали релиза, изучать, какие изменились или стали deprecated интерфейсы, сервисы, константы и так далее.

  • За счёт автоматизации ускоряется переход на новую версию.

Для мейнтейнеров:

  • Появляется возможность выпускать более радикальные релизы. Можно позволить себе более смелые правки, потому что процесс перехода автоматизирован. Нет никакой разницы, если правок много или мало.

  • Снижается необходимость обеспечивать обратную совместимость. Но это зависит от популярности пакета.

  • Уменьшается количество работы с комьюнити, так как они будут вам писать только по поводу багов и предложений, а не потому, что они что-то не так обновили.

Rector и архитектурный рефакторинг

Напоследок хочу рассказать, как Rector помог мне с архитектурным рефакторингом.

Когда в проекте отсутствует чёткая стандартизация и структура, всё превращается в беспорядок. Допустим, я хочу написать новый сервис в проекте, который выглядит как на картинке слева. Куда его пихать? А хочется, чтобы всё выглядело как на картинке справа. Здесь я чётко понимаю, куда и что девать.

Но проблема в том, что пока я буду адаптировать свой проект под такую структуру, в него будут вноситься изменения, катиться хотфиксы. Как результат, мы будем сталкиваться с кучей merge-конфликтов. Чтобы избежать их, надо стопорнуть всю команду и сказать, чтобы не трогали проект неделю или даже дольше. Но чаще всего, так делать нельзя.

Нужно ускорить эту задачу, и здесь снова поможет Rector. Весь процесс похож на написание наборов правил для обновления пакета. С помощью правил описываем:

  • Что и куда хотим переместить;

  • Что переименовать;

  • Что удалить. 

Как это сделать?

Сначала используем RenameNamespaceRector, RenameClassRector, то есть те самые настраиваемые правила. И создаём отдельный конфиг architect-rector.php:

Но у Rector есть одно ограничение: он не меняет физическое местоположение файлов. Но это не проблема, физические манипуляции можно описать с помощью утилиты rsync.

Для автоматизации всего процесса, я описываю последовательности команд в конфиге утилиты task:

После того, как всё это тестируется и отлаживается, наступает день рефакторинга:

  1. Оповещаем команду, чтобы сегодня не трогали проект.

  2. Подтягиваем последние правки из мастера.

  3. Выполняем команду: > task refactor , которая проводит автоматическую трансформацию старой структуры на новую.

  4. Тестируем: проверяем, что всё ОК.

  5. Релизим на проде в этот же день.

В результате:

  1. Сокращается время, при котором команда не может трогать проект, до 1 дня;

  2. Можно спокойно откатывать правки по рефакторингу, если что-то пошло не так;

  3. Растёт качество и надёжность, потому что мы убрали человеческий фактор. Всё рефакторит Rector. 

  4. Можно использовать готовую настройку в аналогичном проекте. У меня было 2 похожих проекта, где я потом это всё запустил.

Итоги

  • Rector отлично подходит для апгрейда кода.

  • Rector может служить как дополнительный анализатор/фиксер качества кода.

  • Rector способен упростить процесс обновления пакетов.

  • Также Rector может помочь при архитектурном рефакторинге.

И да, Rector останется актуальным в будущем. Язык PHP, различные фреймворки и пакеты будут развиваться — появятся новые версии и возможности. А с ними могут возникнуть и новые проблемы. Для решения этих проблем мы будем вырабатывать практики. А сами эти практики — автоматизировать с помощью таких инструментов автоматического рефакторинга как Rector.

Чем больше проблем будет покрыто автоматическим рефакторингом, тем проще мы будем избавляться от легаси, и тем больше времени уделять фичам и развитию наших проектов!

Спасибо вам, что дочитали до этого момента, надеюсь было полезно.
Благословляю вас на все будущие рефакторинги!)

Полезные материалы:

Комментарии (13)


  1. FanatPHP
    00.00.0000 00:00
    +13

    Томаш очень крутой чувак. Взять хотя бы его недавний эксперимент по переезду с Симфони на Ларавель, просто по приколу. Жаль только, что он не привел никаких подробностей, только чеклист:


    ✅ regex Twig to Blade, syntax is 99 % similar
    ✅ 2 custom @rectorphp rules for controller class names, view() and routes
    ✅ copy configs from raw project
    ✅ tidy details


    А вам как раз спасибо за такой подробный рассказ!


    1. SerafimArts
      00.00.0000 00:00
      +11

      А вам как раз спасибо за такой подробный рассказ!

      Там ещё сам доклад огонь. По моему скромному мнению — один из лучших докладов на конференции.


      1. Alex-Volodin Автор
        00.00.0000 00:00
        +2

        спасибо Кирилл!)


    1. Alex-Volodin Автор
      00.00.0000 00:00
      +1

      спасибо)


  1. tommyangelo27
    00.00.0000 00:00
    +6

    Мы на проектах пришли к такой схеме:

    • Ректор запускается в пайплайне после пуша кода в битбакет

    • Запускаем в режиме --dry-run, чтобы только показывал ошибки, а не исправлял

    • Автор кода может просмотреть рекомендации Ректора, и исправить (вручную или тем же ректором), или, в некоторых случаях, пропустить изменения, по согласованию с членами команды

    Сделали так, потому что рефакторинг от Ректора в некоторых случаях ухудшает читаемость кода.


    1. Alex-Volodin Автор
      00.00.0000 00:00
      +3

      Да, тоже хорошее решение.

      Мне если какие-то правила начинают мешать или что-то не так рефакторить, то я скипаю сам файл, либо правило, либо правило в этом файле:

      <?php
          $rectorConfig->skip([
              CatchExceptionNameMatchingTypeRector::class,
              
              // или
      
              CatchExceptionNameMatchingTypeRector::class => [
                   __DIR__ . '/src/Some.php'
              ],
          ];
          
      


    1. SerafimArts
      00.00.0000 00:00
      +15

      Сделали так, потому что рефакторинг от Ректора в некоторых случаях ухудшает читаемость кода.

      +1. Особо проблем доставляют некоторые правила из CodeQuality и CodingStyle.


      Вот список, который пока что собираю, что отключаю (через ->skip([ ... ])) постоянно в каждом проекте, где есть ректор. Отдельно отмечу символами !!! то, что может привести к фаталам и падениям всего проекта после "ректоринга", обратите внимание.


      • !!! ClassPropertyAssignToConstructorPromotionRector — Ломает код, заменяя проперти на промоутед варианты (ломает доктрину, JMS или любые другие гидраторы с newInstanceWithoutConstructor()).
      • VarConstantCommentRector — Может заменить докблок, например list<string> на string[].
      • !!! ReadOnlyPropertyRector — Ломает доктрину, добавляя ридонли для проксей.
      • ChangeOrIfReturnToEarlyReturnRector — выражения if ($a || $b) разделяет на 2 разных.
      • !!! UnSpreadOperatorRector — Ломает код, заменяя __construct(Some ...$some) на __construct(array $some). Это вообще дичь какая-то, имхо.
      • CatchExceptionNameMatchingTypeRector — заменяет catch(Throwable $e) на catch(Throwable $throwable) (зачем? не понятно)
      • SplitDoubleAssignRector — Заменяет линейную инициализацию $from = $to = null; на два разных выражения.
      • !!! FinalizePublicClassConstantRector — Добавляет final к публичным константам, даже если они где-то переопределяются в будущем.
      • FlipTypeControlToUseExclusiveTypeRector — Заменяет строгую проверку $some === null на инвертированную !$some instanceof Example. Опять же, вообще не понятно зачем и кому такое нужно.
      • JoinStringConcatRector — Объединяет отформатированные строчки (как в примере чуть ниже) с конкатенацией в одну большую простынестроку, которую фиг прочитаешь:
        $message = 'Whoops, something went wrong. Please '
        . 'blah blah blah blah blah blah blah blah blah blah '
        . 'blah blah blah blah blah blah blah blah blah blah '
        . 'and delete your OS';
      • ReturnBinaryAndToEarlyReturnRector — заменяет return $some && $any; на 4 строки: if (!$some) { return false; } + return $any.

      Пока это всё, продолжаю вести наблюдение. Если у кого есть похожие кейсы, где ректор что-то ломает или ухудшает читаемость — просьба накидать тоже, по возможности, правил с описаниями. Добавлю к себе в коллекцию)


      P.S. Извините за фигово отформатированный коммент. Чёрт его знает как сделать его более читаемым.


      1. SerafimArts
        00.00.0000 00:00
        +5

        Вот ещё три правила, которые могут сломать код, которые я обнаружил за последние 2 дня:


        • !!! RenameClassRector — Иногда может сломать код, подсунув некорректный класс. В моём случае оно заменило все use статменты, которые ссылались PSR-16 CacheInterface на Symfony CacheInterface, в результате чего код обвалился (т.к. инстансы внутри были PSR, а не симфони).
        • !!! AddDefaultValueForUndefinedVariableRector — ломает енамы.

        В частности:


        private function caseToScalar(\BackedEnum $case): string|int
        {
            return $case->value;
        }
        
        // Заменило на
        private function caseToScalar(\BackedEnum $case): string|int
        {
            $case = null;
            return $case->value;
        }

        • !!! PrivatizeLocalGetterToPropertyRector — ломает код в случае использования каррирования/каста к анонимке.

        Может заменить код:


        $result = array_map($this->foo(...), $collection); // Первый аргумент анонимка
        
        // На некорректный
        
        $result = array_map($some->property, $collection); // Теперь это скалярный стринг

        • ChangeReadOnlyVariableWithDefaultValueToConstantRector — вот это на вкус и цвет.

        Есть код, вида:


        class SomeException
        
        // + куча методов, вида
        
        public static function fromSomeContext(string $value): self
        {
            $message = 'Something went wrong with value ' . $value;
            return new static($message, self::CODE_CONTEXT);
        }

        Это правило берёт константные строки ($message), которые используются только в этом методе и выносит их все в константы. В результате в классе 100500 констант. Вроде как правильно в теории, но фактически читаемость ухудшается.


        1. FanatPHP
          00.00.0000 00:00
          +2

          Может потянуть на полноценную статью! :)


          1. SerafimArts
            00.00.0000 00:00

            Да чёрт его знает.


            Я просто сейчас (месяц назад) на новом проекте воткнул его в качестве дополнения к кодстайлу. Такой себе phpcs на максималках. Вот сейчас и огребаю по дороге, когда сталкиваюсь с его проблемами.


            Ну и более того, тут в списке выше, помимо проблем вида "нельзя везде ставить readonly" или "нельзя везде поля заменять на promoted" есть очевидные баги, которые через пол года уже наверняка пофиксятся, поэтому материал устареет быстро.


            Так что пока что в виде комментариев вот так, чтобы те, кто статью прочитали — были более внимательными (у меня уже 16 правил отключено).


  1. shushu
    00.00.0000 00:00
    +1

    А как вы вообще апгрейд делаете?

    Из статьи понятно что вы апгрейдите код от версии 7, прямиком к версии 8.

    А с деплоем как? Или все на кубернетисах?

    Ведь задеплоив код который ожидает версию пхп 8, на машины с версией 7, понятное дело, работать не будет. Каков процесс?

    Лично я делал "переходную версию", которая поддерживает как версию 7, так и версию 8. В итоге процесс был такой:

    1. Деплой нового кода

    2. Апгрейд пхп версии на машинах. Обычно это половина всех нод за раз.

    После этого можно использовать фичи из пхп 8 в проекте

    Я пытался использовать ректор что бы хотя бы найти все места где код "не работал бы" в пхп 8, но не нашёл способа как это сделать.


    1. Alex-Volodin Автор
      00.00.0000 00:00
      +3

      Из статьи понятно что вы апгрейдите код от версии 7, прямиком к версии 8.

      А с деплоем как? Или все на кубернетисах?

      Да у нас кубер, до этого был сворм. У нас есть возможность менять образ PHP прямо в проекте и я делаю это вместе с апгрейдом кодом. Всё в рамках одного мердж-реквеста и одного деплоя.

      Если у вас версию PHP нужно менять отдельно, то делать "переходную версию" это единственное безопасное решение.

      Я пытался использовать ректор что бы хотя бы найти все места где код "не работал бы" в пхп 8, но не нашёл способа как это сделать.

      Попробуйте ещё использовать инструмент стат анализа PHPStan. Он может указать например какие методы не существуют.

      Ещё у Rector можно насильно указать в какой версии PHP делать рефакторинг. Например у меня локально на компьютере стоит PHP 8.1, но в контейнере в проекте текущая 7.4. Я хочу сделать в проекте 8.0 и чтобы бы код тоже был под 8.0. Для этого в конфиге rector.php указываю

      $rectorConfig->phpVersion(PhpVersion::PHP_80);

      и теперь я могу своей локальной пыхой (которая 8.1) rector'ить код под 8.0.

      И может таким образом он сможет показать какие участки кода нужно улучшить.


      1. shushu
        00.00.0000 00:00
        +2

        Спасибо за совет с PHPStan. Попробую.

        С ректором пробовал так, он меняет все подряд, оптимизируя код который бы и так работал бы с пхп8, но используя пхп8 фичи. И изменений было очень много. Поэтому решение не совсем подходило