company_banner

Избавляемся от рутины со своим плагином для PhpStorm

    Привет, Хабр! Я тружусь в команде Антиспама, и, как и у большинства бэкенд-разработчиков Badoo, большая часть времени у меня уходит на работу с PHP-кодом.

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

    Так появился плагин Badoo для PhpStorm, который мы сегодня активно используем. За несколько лет его возможности серьёзно расширились, мы его развиваем, и в этой статье я расскажу на примере наших кейсов, как адаптировать IDE под свои задачи и инструменты, и докажу, что это не так сложно, как кажется.



    Все знают, что JetBrains разрабатывает коммерческие продукты, однако на тот момент для меня стало открытием, что IDEA — это open-source-проект. По сути, это платформа для построения IDE, а все среды JetBrains являются наборами плагинов, специализирующихся на решении задач определённого языка программирования. Отсюда пришло осознание, что весь функционал, который реализует PhpStorm, можно заточить под собственные нужды. К слову, Eclipse использует аналогичный подход.

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

    Не буду заострять внимание на том, как писать плагины: на Хабре уже были статьи об этом, на сайте есть QuickStart, да и в целом достаточно скачать шаблон тут (в нашем случае подойдёт Simple). Если вы считаете, что тема раскрыта не до конца, скажите об этом в комментариях — я напишу более подробную статью об этом.

    Итак, давайте рассмотрим на примерах, для каких кейсов и как мы дорабатывали PhpStorm.

    Валидация кастомного кода


    В PHP есть функции типа printf, когда первым аргументом передаётся шаблон, а дальше мы передаём аргументы, которые будут подставлены в этот шаблон. Однако стандартные инспекции PhpStorm проверяют корректность только в случае встроенных в PHP функций. Поэтому мы добавили аналогичные проверки наших функций логирования.

    Например, у нас есть свой Logger, у которого есть методы infof, errorf и т. д. Мы пишем плагин, который будет валидировать, что количество аргументов соответствует количеству подстановок в шаблоне.

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

    Создаём класс-наследник LocalInspectionTool:

    class LoggerFormatInspection : LocalInspectionTool() {
       override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean, session: LocalInspectionToolSession): PsiElementVisitor {
           if (session.file !is PhpFile) {
               // Нас не интересуют не-PHP-код
               return super.buildVisitor(holder, isOnTheFly, session)
           }
           return LoggerFormatVisitor(holder, isOnTheFly)
       }
    }


    Основная работа будет происходить в классе LoggerFormatVisitor. По сути, он пробегается по всем элементам исходного кода (PsiElement, PSI — Program Structure Interface) и вызывает для них обработчик. Метод buildVisitor будет запускаться для каждого файла в проекте.

    class LoggerFormatVisitor(private val holder: ProblemsHolder, onTheFly: Boolean) : PsiElementVisitor() {
       override fun visitElement(element: PsiElement) {
           if (!isLoggerFormatFunction(element)) {
               return // Пропускаем всё, что не похоже на функцию логгера
           }
    
           checkMethodReference(element as MethodReference) //
       }
    }

    Первый метод (isLoggerFormatFunction()) убеждается, что текущий элемент — это вызов функции, и проверяет сигнатуру метода: переменная-логгер должна быть наследником класса \Logger\Logger, а имя метода должно быть одним из тех, что поддерживают параметры prinf-like функций (у нас таких шесть: debugf, infof, noticef, warningf, errorf, infof).

    Второй метод делает непосредственно работу: проверяет параметры и подсвечивает ошибки.

    Давайте его и разберём.

    private fun checkMethodReference(element : MethodReference) {
       // Сигнатура printf — следующая:
       // Первый (нулевой) аргумент — формат-строка; если таковой нет, то выходим из функции в расчёте на то, что программист ещё не дописал её (хотя это решение спорно)
       // Начиная со второго идут аргументы формата, их-то мы и будем проверять на соответствие
       val printfArgument = 0
       val firstPossibleArgument = printfArgument + 1
       if (element.parameters.size <= printfArgument) {
           return
       }
    
       val formatLine = element.parameters[printfArgument] as? StringLiteralExpression // Если первый аргумент не строка, выходим
               ?: return
    
       val expectingParameters = getExpectingParameters(formatLine)
       val arguments = element.parameters.slice(IntRange(firstPossibleArgument, element.parameters.size - 1))
    

    Мы нашли первый аргумент — формат-строку и распарсили его (я опущу код парсинга, его можно найти в финальном исходнике). expectingParameters теперь содержит описание параметров, которые ожидаются в последующих аргументах. А в arguments как раз находятся аргументы, которые мы будем проверять.

    Остаётся написать код проверки!

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

    if (expectingParameters.isEmpty() && arguments.isNotEmpty()) {
       holder.registerProblem(arguments.first(), "No format item found in first parameter but call has more than one argument", ProblemHighlightType.WARNING)
       ++ problems
    }

    В редакторе будет подсвечен первый неописанный аргумент с критичностью warning.
    Число ожидаемых аргументов может не соответствовать числу переданных (в обе стороны).

    if (expectingParameters.isNotEmpty()) {
       var expectingIndex = 0
       for (i in arguments.indices) {
           if (expectingParameters.size <= expectingIndex) {
               // Если аргументов больше, чем в формат-строке, подсвечиваем лишние аргументы
               holder.registerProblem(
                       arguments[i],
                       "Format line expecting only ${expectingParameters.size} parameters",
                       ProblemHighlightType.WARNING
               )
               ++ problems
               continue
           }
           ++ expectingIndex
       }
    
       if (arguments.size < expectingParameters.size) {
           for (i in arguments.size until expectingParameters.size) {
               val item = expectingParameters[i]
               // Если нашли неиспользованный плейсхолдер, помечаем это как warning
               holder.registerProblem(
                       formatLine,
                       "Unused format item",
                       ProblemHighlightType.WARNING,
                       TextRange(item.rangeStart + 1, item.rangeEnd + 2)
               )
               ++ problems
           }
       }
    }

    Первый аргумент в holder.registerProblem() — это элемент PSI, который мы считаем ошибочным. Параметры кажутся очевидными, поэтому не буду на них останавливаться.

    Внимательный читатель заметит, что во втором случае есть ещё какой-то TextRange с непонятными манипуляциями числами. Структура item содержит в себе место в тексте формат-строки, где мы обнаружили плейсхолдер. Однако элемент formatLine, помимо содержимого строки, содержит обрамляющие её кавычки. Чтобы найти местоположение плейсхолдеров, мы добавляем единицу, чтобы скорректировать смещение. Как вы уже поняли, аргумент TextRange нужен, чтобы подсвечивать не целиком формат-строку, а лишь ту её часть, где располагается плейсхолдер.

    И последнее: если ошибка всё-таки обнаружена, покажем это более явно, подсветив имя функции.

    if (problems > 0) {
       val elementStart = element.textOffset
       val nameNodeTextRange = element.nameNode!!.textRange
    
       val nameTextRange = TextRange(nameNodeTextRange.startOffset - elementStart, nameNodeTextRange.endOffset - elementStart)
       holder.registerProblem(element, nameTextRange, "Invalid format function usage")
    }

    И снова манипуляции со смещениями. Суть их в том, что нам надо указать смещение внутри элемента MethodReference (обращение к методу). Он, в свою очередь, состоит из объекта, оператора «стрелочка», имени вызываемого метода и аргументов. Для большей аккуратности мы маркируем только имя функции. Также можно заметить, что тут не указана критичность ошибки — она будет взята из настройки инспекции (настройки PhpStorm -> Editor -> Inspections).
    На этом всё, фича готова.



    Полностью исходник инспекции можно посмотреть тут.

    Далее я расскажу о некоторых интересных возможностях, которые мы реализовали, но не буду подробно останавливаться на их реализации.

    Работа с базами данных


    По историческим причинам у нас есть своя обёртка для работы с базой, которая на вход принимает шаблон с SQL-запросом и массив с именованными подстановками. Это ещё один пример использования инспекций: мы валидируем, что в массиве с параметрами присутствуют все необходимые подстановки и нет лишних.



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

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



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

    Кроме того, у нас имеется более сотни MySQL-серверов. Мы собираем данные о базах и таблицах на всех машинах и можем проверить, правильный ли коннект используется в запросе (этот функционал в данный момент находится в разработке).

    Генерация boilerplate-кода


    Во многих фреймворках есть функционал, который пишется по какому-то шаблону, и во многих местах проекта надо писать примерно одинаковый код. Пример — модели в Yii.

    В нашем фреймворке такое тоже присутствует. Но все мы знаем, что писать одно и то же — скучно. Поэтому мы научили PhpStorm делать это за нас.

    Например, если в фасаде модуля определить функцию, PhpStorm предложит Quick Fix для создания соответствующего класса.



    Он заполнит тело функции, создаст необходимый класс-обработчик запроса и выполнит необходимые действия для его регистрации в системе.

    public function onAcceptRequest(\Framework\Request $request, \User $user): void
    {
       $command = new Commands\OnAcceptRequest($request, $user);
       return $command->run();
    }

    Пример класса:

    <?php
    namespace Modules\Chaos\Commands;
    
    class OnAcceptRequest extends \Modules\core\AbstractCommand
    {
       private $request;
    
       private $user;
    
       public function __construct(\Framework\Request $request, \User $user)
       {
           $this->request = $request;
           $this->user = $user;
       }
    
       protected function execute()
       {
           // TODO implement me
       }
    }
    

    Поддержка SoftMocks


    Ещё одной особенностью нашего фреймворка является активное использование SoftMocks в тестах. Например:

    \Badoo\SoftMocks::redefineMethod(\User\ProfilePhoto::class, ‘getAll’, $arguments, $code)

    В этом фрагменте кода мы перехватываем вызовы метода \User\ProfilePhoto::getAll(), чтобы избежать хождения в базу из тестов. Это не является вызовом метода с точки зрения синтаксиса языка, однако мы знаем эту особенность фреймворка, поэтому научили нашу среду распознавать эту строку как обращение к соответствующему методу (что включает в себя навигацию, отображение в Find Usages и автодополнение второго параметра).

    Отладка тестов (удалённая и с SoftMocks!)


    Помимо написания тестов, в обязанности разработчика часто входит их отладка. Иногда она производится на удалённом сервере. Задача усложняется тем, что SoftMocks переписывают исходный текст и интерпретатор PHP выполняет не тот код, который отображается в IDE (хотя и равнозначный). А так как путь к исполняемому файлу определяется динамически, стандартный Path Mapping в PhpStorm оказывается бессилен и не может соотнести путь на удалённом сервере с путём в открытом проекте.

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

    1. Установить Xdebug proxy.
    2. Настроить тестовый фреймворк PhpStorm для запуска на удалённой машине (подробнее об этом можно почитать тут).
    3. Настроить Path Mapping для проекта.
    4. Молиться, чтобы всё было выполнено без ошибок.

    Можно также забить на интерактивную отладку и дебажить с помощью print (вполне действенный способ).

    Учитывая востребованность данной процедуры, мы научили IDE делать всё самостоятельно: поддерживать динамическое отображение путей, подключение по SSH, запуск тестов и собственно отладку.

    Подтягиваем статистику


    Мы внимательно следим за производительностью нашего кода. Так, у нас на серверах активно используется Liveprof (ранее мы рассказывали об этом). Он отслеживает изменение производительности кода и позволяет отследить момент, когда что-то пошло не так.

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



    Отображаем легаси


    На последнем PHP Meetup мы рассказывали про сбор мёртвого кода.

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



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



    Заключение


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

    Наш плагин использует лишь небольшую часть возможностей API IDEA, и в статье приведены далеко не все кейсы. Если тема вам интересна, напишите об этом в комментариях — и мы будем продолжать писать статьи про наш плагин и держать вас в курсе его развития.

    Несколько вещей, которые мы узнали, работая с IDEA:

    1. Расширять IDEA/PhpStorm гораздо проще, чем может показаться на первый взгляд. При разработке вас будет поддерживать одна из передовых сред разработки (IntelliJ IDEA).
    2. Архитектура IDEA построена таким образом, что для добавления нового функционала не требуется ничего менять: просто реализуем класс соответствующего интерфейса — и готово!
    3. В любой непонятной ситуации вам окажут поддержку разработчики JetBrains и сообщество в течение пары дней. На самом деле, я не видел, чтобы где-нибудь ещё разработчики платформы так активно поддерживали сообщество.
    4. Небольшая ложка дёгтя — это документация, которая, к сожалению, покрывает только небольшую часть возможностей платформы. Однако это компенсируется активным сообществом и умением гуглить (очень помогает поиск в гитхабе по коду).
    5. Ещё одна ложка дёгтя (хотя это можно рассматривать и как преимущество) — это очень быстрое развитие платформы. Видно, что разработчики стараются сохранять обратную совместимость до последнего, но всё же обновлять код, заменяя вызовы deprecated-методов и классов, приходится регулярно. Чтобы это не влияло на конечных пользователей, мы всегда собираем код с версией EAP, чтобы при выходе новой версии быть к ней готовыми.


    Если вам интересно начать использовать наш плагин в своей работе, присоединяйтесь к нашей команде, мы активно ищем людей.

    Ссылки


    IntelliJ Platform UI Guidelines
    IntelliJ Platform SDK DevGuide
    IntelliJ IDEA Open API and Plugin Development (community)
    Intellij IDEA Community sources

    Пример плагина, упоминавшегося в статье: github.com/badoo/idea-printf-checker-plugin-example

    UPD: тест завершён, спасибо всем участникам! Результаты пока в обработке. Возможно, вы ещё не слышали, что мы расширяем лондонскую часть команды. До 1 марта можно пройти тест, по результатам которого лучших участников мы пригласим на собеседование в Москве. Успешно пройденное собеседование — оффер в Лондон в тот же день. Билеты до места проведения интервью и релокация — за счёт компании.
    Badoo
    Big Dating

    Comments 12

      –1

      осваивай kotlin
      @
      пиши на php

        +2

        Чем больше скила, тем лучше.

        +1

        Подскажите не было ли необходимости работать с директориями? Например шторм стабильно отмечает папки внутри вендора Excluded, а хотелось бы с некоторыми так не делать.

          0
          Таковые можно добавить в Settings > directories проекта и они будут индексироваться.
            0

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

              +1
              Добрый день!
              К сожалению, таким заниматься не довелось. Насколько я понял, список этих директорий генерит плагин com.jetbrains.php (то есть сам PhpStorm), а по нему документации ещё меньше, для собственно IDEA.
              я бы попробовал идти в следующих направлениях:

              1. посмотреть на реализации Extension Point «directoryIndexExcludePolicy» / com.intellij.openapi.roots.impl.DirectoryIndexExcludePolicy (и тех, что рядом)

              2. посмотреть на Extension Point com.jetbrains.php.libraryRoot / com.jetbrains.php.config.library.PhpLibraryRoot — это уже ближе именно к Шторму.

              3. задать вопрос в комьюнити Idea
          0
          Мы собираем данные о базах и таблицах на всех машинах и можем проверить, правильный ли коннект используется в запросе (этот функционал в данный момент находится в разработке).
          У нас был похожий функционал и одна из самых полезных и востребованных функций была возможность автоматически найти и показать комментарии из/добавить ссылку на wiki для таблицы, процедуры, webapi вызова и т.п.

            0
            Спасибо за комментарий! Я даже не подумал, что действительно, можно прям из запроса делать ссылку на реестр БД. Обязательно внесу это в план. А почему функционал «был», кстати?
            Может у вас было ещё что-то интересное?
              +2
              А почему функционал «был», кстати?
              Это было на другом проекте.
              Может у вас было ещё что-то интересное?
              Было очень много чего интересного. Мы использовали Visual Studio с набором собственных плагинов, но важна сама концепция и Intelij IDEA имеет очень развитую систему плагинов, так что всё что сделано в VS, можно сделать в IDEA.

              Например очень полезная связка плагинов — в браузер ставился плагин который мониторил коммуникацию с сайтами которые мы разрабатывали и по querystring/formdata получал id разных сущностей. После этого передавал эту информацию в плагин на VS которые автоматически запрашивал данные из базы по этим id и выводил для разработчика, включая зависимые данные. т.е. разработчки навигирует по dev сайту и сразу видит всю относящееся к текущей странице без единого клика мышки, очень сильно помогало.

              Пример — зашли на страницу dev.test.com/product/JK390 нам в плагине показывается два грида один информация из базы для таблицы Products по id JK390 и 10 последних записей из таблицы OrderItems для этого продукта. На страницу dev.test.com/orders/KKN93012-33, показывается гриды для таблицы Orders для заказа KKN93012-33 и из таблицы OrderItems для этого заказа.

              Ещё был плагин который по тому где находится курсор, определял если это имя таблицы или хранимой процедуры, позволял сделать запрос (с фильтром/параметрами из истории) и по результатам выполнения применить один из T4 шаблонов для автогенерации кода. Чтобы сгенерировать вызов или создать класс на основе возвращённого dataset.

            +2
            Очень и очень интересная статья! Огромное вам спасибо, что делитесь знаниями!
            Понравилась идея сбора статистики профайлинга и отображения прямо в IDE, это фантастика!
            Сам использую плагин «Navigate From Literal», позволяет резолвить пути к файлам внутри строчек и прыгать в них (путь к шаблону, например).
              0
              Спасибо! Про Navigate From Literal не знал, спасибо — полезная штука!
              0
              (случайно отправил)

              Only users with full accounts can post comments. Log in, please.