Очистка PHP-приложения с помощью PHPStan

NOTES 18.04.22 18.04.22 116
Бесплатные курсына главную сниппетов

За то время, что я работаю PHP-разработчиком, способ написания и доставки кода сильно изменился. В ранних приложениях Symfony и Zend Framework группы PHP-FIG не существовало, а стандарты кодирования определялись на усмотрение того, кто их писал. На протяжении тех лет, когда мы наблюдали широкое распространение стандартов PSR, надежные инструменты статического анализа были несколько разрозненными. Так было до настоящего времени, пока не вышла версия 1.0 PHPStan. Давайте отметим это событие, ознакомившись с некоторыми ее возможностями!

Компилируемые языки — ваш превентивный уничтожитель ошибок

Одно из больших преимуществ использования скомпилированного языка, такого как Java или C#, заключается в том, что процесс компиляции завершится неудачей, если ваш код не является типобезопасным, обеспечивая соблюдение стандартов. Поскольку PHP является интерпретируемым языком, у нас нет такой роскоши.

Интерпретируемый как компилируемый: CI + инструментарий

Благодаря огромному количеству инструментов DevOps, доступных в современной веб-разработке и статическом анализе, нам в действительности предоставляются те же самые механизмы, но с помощью различных средств. Поскольку это так, то и я не смогу сильно настаивать на использовании чего-то похожего на ту среду, которую буду описывать. Итак, зачем вам нужен этот инструментарий? Давайте рассмотрим пример.

Сценарий

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

"Помогите! Моё PHP-приложение создано кем-то другим, и нужно, чтобы кто-либо помог мне спасти его и взял на себя обслуживание, потому что необходимо создать функции X/Y/Z, а функционал A/B/C даже не работает нормально!".

Взятие на себя чужой кодовой базы/проекта — это всегда сплошная лотерея. Если вы беретесь за осуществление проекта, потому что ему нужны новые функции, а он уже погряз в технических долгах, знайте, что прежде чем приступать к чему-то еще, сначала придется разобраться с этим. Хуже того, многие из этих проектов (по моему опыту) обычно поступают с полным отсутствием тестов для самодокументирования кода. Рассмотрим классический пример, который встречается то и дело:

$someData = \MyNamespace\MyORM\MyRepository::findAllBySomething(SOMETHING);

    foreach ($someData as $myEntity) {
    $myEntity->doTheThing();
    }

Вы не писали этот класс сущности или метод репозитория. У них нет никаких тайпхинтов, потому что изначально это было написано на PHP5.3, или разработчик их не использовал. Хорошо, если ваш ORM возвращает массив тех же сущностей, но одна ошибка, один нулевой результат в возвращаемом значении findAllBySomething() и doTheThing() выбросит фатальную ошибку.

Пришло время применить анализатор PHPStan.

Легко сказать "используйте PHPStan", однако если вы используете унаследованное приложение или приложение с большими техническими долгами, то вам нужна стратегия, а не выбрасывать все подряд, чтобы посмотреть, что получится. Во-первых, вы должны познакомиться с Уровнями Правил.

Уровни Правил

PHPStan структурирован так, чтобы запускаться с заданными уровнями правил, пронумерованными от 0 до 9:

  1. базовые проверки, неизвестные классы, неизвестные функции, неизвестные методы, вызываемые на $this, неправильное количество аргументов, передаваемых этим методам и функциям, всегда неопределенные переменные.

  2. возможно неопределенные переменные, неизвестные магические методы и свойства на классах с __call и __get.

  3. неизвестные методы проверяются на всех выражениях (не только на $this), валидация PHPDocs.

  4. типы возвращаемых значений, типы, присваиваемые свойствам.

  5. базовая проверка мертвого кода — всегда ложные instanceof и другие проверки типов, мертвые ветви else, недостижимый код после возврата; и т.д.

  6. проверка типов аргументов, передаваемых методам и функциям.

  7. сообщать об отсутствующих тайпхинтах.

  8. сообщать о частично неправильных типах объединения — если вы вызываете метод, который существует только для некоторых типов в объединении, уровень 7 начинает сообщать об этом; другие возможные неправильные ситуации.

  9. сообщать о вызове методов и доступе к свойствам на nullable типах.

  10. будьте строги к mixed типу — единственная разрешенная операция, которую вы можете сделать с ним, это передать его другому mixed.

Вот почему ваша стратегия очень важна. Если у вас есть унаследованный проект, написанный кем-то другим, и вы запускаете программу выполнения задач PHPStan на уровне 9, то можете быть ошеломлены результатами, которые она выдает. Все сломано! Для рефакторинга я бы предложил следующее:

Пайплайн

В мире DevOps существует слишком большое количество вариантов инструментария для решения ваших проблем. Для данного примера я предлагаю только один подход, но он является наименее сложным по сравнению с другими доступными вариантами. После того, как вы определились со стратегией, пришло время настроить свой пайплайн так, чтобы не коммитить новый код, который сначала не прошел через PHPStan.

Защитные барьеры: локальный в сравнении с серверным

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

Локальный

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

Чтобы установить PHPStan, выполните следующее:

$composer require --dev phpstan/phpstan

Мы добавляем --dev, поскольку в продакшне он нам не нужен (теоретически!).

Это довольно интересная функция PHPStan. Ваша базовая линия устанавливает "нулевой уровень" вашего приложения, так что все текущие ошибки, существующие в рамках выбранного вами Уровня Правил, игнорируются до тех пор, пока вы не решите их устранить, но в то же время может быть установлен уровень правил для любых новых изменений. Разумный подход, описанный в стратегии, заключается в том, чтобы установить базовую линию на Уровне Правила 6:

Чтобы создать базовую линию, выполните следующие действия:

vendor/bin/phpstan analyse --level 6 \  --configuration phpstan.neon \  src/ tests/ --generate-baseline

Сейчас у вас есть конфигурация базовой линии, установленная в указанном файле (phpstan.neon), которая сохраняет подробный обзор ошибок по каждому файлу.

Теперь вы хотите, чтобы PHPStan предотвращал коммиты в вашем репозитории до того, как они будут выгружены в ваш источник. Для этого мы используем Git hooks.

Мне потребовались годы, чтобы понять, что git на самом деле устанавливает хуки в стандартной комплектации в новый git-репозиторий при git init. Вы можете прочитать подробнее о git hooks здесь. Мы собираемся отредактировать pre-commit хук. Если вы не применяли никаких хуков ранее в вашем проекте, то можете активировать хук pre-commit, переименовав его — запустите его из корня вашего проекта:

mv ./.git/hooks/pre-commit.sample ./.git/hooks/pre-commit

Теперь откройте файл, удалите его содержимое и скопируйте следующее:

#!/bin/sh  
    #  

    exec < /dev/tty

    if git rev-parse --verify HEAD >/dev/null 2>&1
    then
    against=HEAD
    else
    # Initial commit: diff against an empty tree object  
    against=4b825dc642cb6eb9a060e54bf8d69288fbee4904
    fi

    gitDiffFiles=$(git diff --name-only --diff-filter=d $against | grep \.php)

    if [ "$gitDiffFiles" != "" ]; then
    analysisResult=$(vendor/bin/phpstan analyse $gitDiffFiles)

    if [ "$analysisResult" = "" ]; then
    echo 'PHPStan pass'
    else
    echo "$analysisResult"
    exit 1;
    fi
    fi

    # Redirect output to stderr.  
    exec 1>&2

    # If there are whitespace errors, print the offending file names and fail.  
    exec git diff-index --check --cached $against --

Теперь, когда вы включили pre-commit, PHPStan будет запускаться перед каждым коммитом и анализировать базовую линию на предмет новых файлов, которые были изменены в git-коммите. Больше никакого дурно пахнущего закоммиченного кода!

Возможно, вы захотите настроить триггер командной строки при переходе на более высокий уровень, поэтому, когда он должен измениться (или вы захотите включить другие функции PHPStan), измените аргументы строки analysisResult=$(vendor/bin/phpstan analyse $gitDiffFiles).

Серверный

Чем больше защиты вы сможете организовать для своего кода, тем лучше. Запуск PHPStan на стороне сервера после перехода к вашему коду в рамках Непрерывной Интеграции является обязательным условием. Для этого примера мы будем использовать Github Actions, но имейте в виду, что можно настроить этот процесс с тем же уровнем функциональности в CircleCI, Bitbucket Pipelines, Gitlab CI/CD или Jenkins. Вот пример рабочего процесса actions, настроенного на Github, при сборке кода с помощью контейнера Ubuntu:

---
    name: build

    on: [ push, pull_request ]

    jobs:
    build:
    runs-on: ubuntu-latest
    strategy:
    matrix:
    name: Build example
    steps:
    - name: Checkout
    uses: actions/checkout@v2

    - name: Setup PHP
    uses: shivammathur/setup-php@v2
    with:
    php-version: 8.0
    extensions: json, mbstring
    coverage: pcov
    env:
    COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    - name: Run PHPStan
    run: vendor/bin/phpstan analyse .

Команда в разделе “Выполнить PHPStan” может быть настроена в соответствии с вашими требованиями таким же образом, как вы можете настроить команду при локальном запуске PHPStan. Я написал данный рабочий процесс для запуска PHPStan на уровне по умолчанию для всех файлов в проекте (этот рабочий процесс еще не запустил composer, так что у него не будет ненужного и неэффективного шага по его запуску в папке vendor), поэтому здесь я бы рекомендовал иметь для извлечения конфигурацию, которая устанавливает Уровень Правил для всего проекта.

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

И последнее, но не менее важное: статический анализ в сравнении с тестами

Заявляю об этом громко, особенно для тех, кто на задней парте: PHPStan и любой другой инструмент статического анализа не является заменой тестам! Я бы сказал так: набор тестов и PHPStan дополняют друг друга в оценке качества вашего кода.

Ошибочно полагать, будто набор тестов вам практически не нужен. Самое существенное здесь то, что статический анализ не может проверить вашу доменную логику. Хотя такое утверждение может показаться очевидным, однако это может сбить с толку, поскольку PHPStan способен устранить необходимость в определенных тестах. Примером может служить тест instanceOf, который утверждает, что создаваемый класс является конечным результатом процесса. PHPStan исключает данное требование, поскольку он обеспечивает анализ, необходимый для устранения этой потенциальной ошибки, но он не знает заранее о логике вашего домена — это то, что вам действительно нужно протестировать.

И помните, что существуют альтернативы!

 

 

на главную сниппетов
Курсы