Компания
199,70
рейтинг
5 августа 2014 в 12:00

Разработка → Badoo PHP Code Formatter. Теперь в open source!

Несколько лет назад компания Badoo начала значительно расти по числу сотрудников, с 20 до 100 и более. Это потребовало серьезной перестройки многих процессов, касающихся разработки. Одна из проблем, с которой мы столкнулись, — как заставить всех разработчиков следовать единому стандарту кодирования, чтобы весь наш код выглядел единообразно и был легко поддерживаемым?

Для решения этой задачи мы решили внедрить инструмент для форматирования кода, который умел бы следующее:

  1. выводить сообщения о несоответствии стандарту форматирования в виде списка, не трогая сам файл;
  2. автоматически исправлять все найденные проблемы с форматированием;
  3. уметь форматировать только часть файла (нам не нужно переформатировать репозиторий сразу целиком, чтобы не потерять историю).

Мы рассматривали два проекта, которые можно было бы взять за основу для написания такого инструмента — PHP Beautifier и PHP Code Sniffer. Первый умел форматировать код, но не умел печатать диагностику, а второй — наоборот, умел печатать диагностику, но не умел форматировать файлы. К сожалению, оба этих проекта, по нашей оценке, были не слишком пригодны для того, чтобы добавить в них недостающую нам функциональность, поэтому была написана новая утилита — phpcf (PHP Code Formatter). Уже в течение двух лет она работает как git pre-receive hook, настроенный на отклонение (!) изменений, которые не оформлены по нашему стандарту кодирования.

Наконец настало время открыть исходные тексты нашей утилиты для широкой публики: github.com/badoo/phpcf

Функциональность


Форматер был создан для того, чтобы в основном менять пробельные символы: переносы строк, отступы, пробелы вокруг операторов, и т.д. Таким образом, phpcf не заменяет другие схожие утилиты, такие как вышеупомянутый PHP Code Sniffer и PHP Coding Standards Fixer от Фабьена Потенцьера. Он дополняет их, выполняя «грязную работу» по правильной расстановке пробелов и переносов строк в файле. Важно отметить, что наша утилита учитывает изначальное форматирование в файле и меняет только те пробелы, которые не соответствуют выбранному стандарту (в отличие от некоторых других решений, которые сначала удаляют все пробельные токены, а потом начинают форматирование).

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

Пример использования (команда “phpcf apply <filename>” форматирует указанный файл, а “phpcf check <filename>” проверяет форматирование и возвращает ненулевой exit-код в случае наличия неотформатированных фрагментов):

$ cat minifier.php
<?php
$tokens=token_get_all(file_get_contents($argv[1]));$contents='';foreach($tokens as $tok){if($tok[0]===T_WHITESPACE||$tok[0]===T_COMMENT)continue;if($tok[0]===T_AS||$tok[0]===T_ELSE)$contents.=' '.$tok[1].' '; else $contents.=is_array($tok)?$tok[1]:$tok;}echo$contents."\n";

$ phpcf apply minifier.php
minifier.php formatted successfully

$ cat minifier.php
<?php
$tokens = token_get_all(file_get_contents($argv[1]));
$contents = '';
foreach ($tokens as $tok) {
    if ($tok[0] === T_WHITESPACE || $tok[0] === T_COMMENT) continue;
    if ($tok[0] === T_AS || $tok[0] === T_ELSE) $contents .= ' ' . $tok[1] . ' ';
    else $contents .= is_array($tok) ? $tok[1] : $tok;
}
echo $contents . "\n";

$ phpcf check minifier.php; echo $?
minifier.php does not need formatting
0


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

$ cat zebra.php 
<?php
echo "White "."strip".PHP_EOL;
echo "Black "."strip".PHP_EOL; // not formatted
echo "Arse".PHP_EOL;

$ phpcf apply zebra.php:1-2,4
zebra.php formatted successfully

$ cat zebra.php 
<?php
echo "White " . "strip" . PHP_EOL;
echo "Black "."strip".PHP_EOL; // not formatted
echo "Arse" . PHP_EOL;

$ phpcf check zebra.php
zebra.php issues:
        Expected one space before binary operators (= < > * . etc)   on line 3 column 14
        Expected one space after binary operators (= < > * . etc)   on line 3 column 15
        ...

$ echo $?
1


Даже несмотря на то, что утилита написана на PHP, форматирование большинства файлов проходит за доли секунды. Но у нас большой репозиторий и много кода, так что мы написали расширение, которое, будучи подключенным, увеличивает производительность работы в сотню раз: весь наш репозиторий в 2 миллиона строк форматируется за 8 секунд на «ноутбучном» Core i7. Для использования расширения требуется его собрать из директории “ext/”, установить, включить “enable_dl = On” в php.ini или прописать его как extension.

Хотелось бы еще раз подчеркнуть, что phpcf прежде всего меняет пробельные символы и умеет делать лишь простейшие преобразования над кодом: например, заменять короткий открывающий тег на длинный или убирать последний закрывающий тег из файла. Помимо этого, phpcf умеет автоматически исправлять кириллицу в названиях функций на английские символы. Также не трогаются выражения, выровненные вручную с помощью пробелов. Это происходит из-за архитектуры — форматер работает как конечный автомат с правилами, которые задает пользователь, а не как набор «захардкоженных» замен (форматер поставляется с «конфигом по умолчанию», соответствующим нашим правилам форматирования). Поэтому, если вы хотите автоматическую замену “var” на “public” или похожих вещей, рекомендуем обратить внимание на PHP-CS-Fixer — он мало внимания уделяет пробельным символам (в отличие от phpcf), но зато умеет переписывать токены.

Поддержка версий PHP


Изначально наш форматер работал на версии PHP 5.3 и поддерживал только ее. В данный момент мы полностью поддерживаем синтаксис PHP 5.4 и 5.5, и для работы форматера требуется PHP версии не ниже 5.4. Если вы хотите форматировать код, предназначенный для более ранних версий PHP, то вы можете это делать, но непосредственно phpcf должен запускаться с помощью PHP 5.4+.

Хотелось бы отдельно отметить, что phpcf не умеет форматировать «недописанные» файлы, содержащие, к примеру, несбалансированные скобки: в этом случае будет выдано сообщение об ошибке и файл просто не будет отформатирован. При этом в некоторых случаях вы можете форматировать «невалидный» с точки зрения интерпретатора PHP код, потому что сам по себе форматер не делает проверки синтаксиса файла.

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

Поддержка интеграции с git


Когда вы скачаете нашу утилиту, вы скорее всего заметите, что есть не только действия “check”, “preview” и “apply”, но и такие же действия с суффиксом “-git”. В Badoo мы в качестве системы контроля версий используем Git, и по умолчанию проверяются и форматируются только измененные строки. Чтобы не заставлять разработчиков вспоминать номера измененных строк, мы сделали “*-git” команды, которые работают следующим образом:

  1. Посмотреть «незакоммиченные» и добавленные в индекс изменения.
  2. Посмотреть изменения, которые произведены в текущей ветке, но при этом отсутствуют в origin/master и origin/<текущая-ветка> (соответствующие ветки обновляются при git push / git pull), или, другими словами, еще не отправлены в репозиторий.
  3. Применить форматирование только к найденным в (1) и (2) строкам.

Мы используем разработку в feature branches, и при этом в ветке master у нас находится production-код, поэтому “-git” команды заточены под этот flow и определяют измененные строки по приведённому выше алгоритму.

Пример использования:
(master) $ git checkout -b some_feature
(some_feature) $ vim test.php # меняем test.php
(some_feature) $ phpcf apply-git # при запуске без аргументов, форматируются все измененные файлы
test.php formatted successfully



Использование классов phpcf напрямую


Помимо использования phpcf как утилиты, также есть возможность использовать классы phpcf напрямую, в том числе с подключением расширения. Эта возможность бывает полезна для разных задач, например, для создания веб-сервиса по форматированию PHP-файлов. Для своих нужд мы ее используем в процессе ревью кода: при просмотре изменений, сделанных в ветке, мы не умеем не показывать изменения, которые связаны исключительно с форматированием (для этого полностью форматируются две версии файла, старая и новая, после чего считается diff между ними).

Пример использования классов phpcf напрямую:
<?php
// Подключение констант и автолоадера
require_once ПУТЬ_К_PHPCF_SRC . '/src/init.php';
// Создание опций форматирования
$Options = new \Phpcf\Options();
// Опциональные настройки (для всех существуют дефолтные значения)
$Options->setTabSequence('   '); // Ваши 3-4 пробела или Tab
$Options->setMaxLineLength(130); // 120 по умолчанию
$Options->setCustomStyle('style'); // путь к директории с вашими стилями
$Options->toggleCyrillicFilter(true|false); // переключение фильтра кириллических символов
$Options->usePure(true); // принудительное  использование версии без extension
$Formatter = new \Phpcf\Formatter($Options);

// Форматирование файла
$Formatter->formatFile('file.php'); // весь файл
$Formatter->formatFile('file.php:1-40,65'); // диапазон строк

// Форматирование строки
$Formatter->format('<?php phpinfo()'); // вся строка с кодом
$Formatter->format($code, [1, 2, 10]); // номера строк для форматирования

// Все вышеуказанные функции форматирования возвращают объект \Phpcf\FormattingResult

$Result->getContent(); // строка с отформатированным кодом
$Result->wasFormatted(); // bool, был ли правлен код
$Result->getIssues(); // array, текстовое описание проблем форматирования кода
$Result->getError(); // \Exception|null ошибка при форматировании кода



Интеграция с IDE на примере PHPStorm


Если вы хотите уметь форматировать PHP-код с помощью нашего форматера и при этом пользуетесь PHPStorm, можно проделать следующие шаги:

1. git clone https://github.com/badoo/phpcf.git
2. В PHPStorm идем в настройки и находим секцию «External Tools».
3. Жмём «Add...» и заполняем поля:

Name: format whole file
Group: phpcf
Снимаем галочку «open console» (чтоб не надоедала)
Program: php
Parameters: путь_до_phpcf.git/phpcf apply $FilePath$
Working directory: любая

Name: format selection
Group: phpcf
Снимаем галочку «open console» (чтоб не надоедала)
Program: php
Parameters: путь_до_phpcf.git/phpcf apply $FilePath$:$SelectionStartLine$-$SelectionEndLine$
Working directory: любая


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

Помимо этого, можно таким же способом настроить работу “phpcf check-git --emacs”: в таком режиме phpcf будет печатать имена файлов с номерами строк в emacs-стиле, и благодаря этому можно переходить к указанной в выводе строке нажатием по ссылке.

Реализация


В процессе своей работы форматер проходит следующие этапы:

  1. Подготовка списка имен файлов и номеров строк, которые требуется отформатировать.
  2. Получение списка токенов для файла с помощью вызова token_get_all (prepareTokens).
  3. Преобразование токенов в единый формат с возможностью выполнения хуков, позволяющих заменять одни токены на другие.
  4. Вызов метода process(), который проходит по всем токенам с использованием конечного автомата Phpcf\Impl\Fsm и составляет массив действий по форматированию (exec).
  5. Вызов метода exec(), в котором сформированный массив действий обрабатывается и превращается в конечную строку.


Больше подробностей

Описание класса Phpcf\Impl\Fsm — конечного автомата для разбора токенов


Класс Phpcf\Impl\Fsm представляет из себя конечный автомат, в котором состояние представлено в виде стека (массива). Верхний элемент стека используется для правил перехода между состояниями:

<?php
$fsm_context_rules = array(
    'CTX_SOMETHING' => array(        // правила переходов, когда вершина стека = CTX_SOMETHING
        'T_1' => 'CTX_OTHER_THING',  // для токена T_1 заменить вершину стека на CTX_OTHER_THING
        'T_2' => array('CTX_OTHER_THING'),  // для токена T_2 выполнить push(CTX_OTHER_THING)
        'T_3' => -2,                 // для токена T_3 выполнить pop() со стека 2 раза
        // выполнить переход N сейчас и выполнить переход M перед обработкой следующего токена
        // полезно, чтобы дать отдельный контекст для закрывающей скобки
        'T_4' => array(PHPCF_CTX_NOW => N, PHPCF_CTX_NEXT => M),
        // cнять часть элементов со стека и заменить новым
        'T_5' => array('REPLACE' => array(-2, array('CTX_OTHER_THING')),
        // если в текущем контексте встречается токен, который не перечислен в массиве, ничего не делается
    ),
);



В общем виде правила для конечного автомата выглядят следующим образом:

<?php
$fsm_context_rules = array(
    '<context_name>[ ... <context_name>]' => array(
        '<token_code>[ ... <token_code>]' => <context_rule>,
    ),
);
 
$context_rule = '<context_name>';        // переход в состояние <context_name> с заменой вершины стека
$context_rule = array('<context_name>'); // переход в состояние <context_name>, добавив <context_name> в стек
$context_rule = -N;                      // перейти на N уровней вверх, N — натуральное число
// выполнить переход, определенный в секции PHPCF_CTX_NOW, а перед обработкой следующего токена выполнить переход,
// определенный в PHPCF_CTX_NEXT (в debug будет выводится сообщение об этом как о "delayed rule")
$context_rule = array(PHPCF_CTX_NOW => <context_rule>, PHPCF_CTX_NEXT => <context_rule>);



Массив правил форматирования: $controls


Форматер принципиально устроен так, что он регулирует только содержимое пробельных токенов (за исключением хуков). Все правила форматирования определяются в массиве "$controls", который представлен в следующем виде:

<?php
$controls = array(
    '<token_code>[ ... <token_code>]' => array(    // коды токенов через пробел, для которых применить указанное правило форматирования
        ['<context>' => <formatting_rule>,]        // применить <formatting_rule> для указанного контекста
        PHPCF_KEY_ALL => <formatting_rule>,        // применить <formatting_rule>, если правило для конкретного контекста не было найдено
    ),
);
 
$formatting_rule = array(
    PHPCF_KEY_DESCR_LEFT => '<description>',       // описание того, что надо сделать с пробелами слева токена
    PHPCF_KEY_LEFT => PHPCF_EX_<action>,           // действие, которое нужно выполнить с пробелами слева от токена
    PHPCF_KEY_DESCR_RIGHT => '<description>',      // описание действия справа от токена
    PHPCF_KEY_RIGHT => PHPCF_EX_<action>,          // действие, которое надо выполнить справа
);
 
// пример:
 
'T_AS T_ELSEIF T_ELSE T_CATCH' => array(  // для токенов "as", "elseif", "else" и "catch"
    PHPCF_KEY_ALL => array(               // во всех контекстах
        PHPCF_KEY_DESCR_LEFT => 'One space before as, elseif, else, catch',
        PHPCF_KEY_LEFT => PHPCF_EX_SHRINK_SPACES_STRONG,                    // все пробелы слева превратить в один (больший приоритет)
        PHPCF_KEY_DESCR_RIGHT => 'One space after as, elseif, else, catch', // то же самое
        PHPCF_KEY_RIGHT => PHPCF_EX_SHRINK_SPACES_STRONG,                   // сделать справа
    )
),



В случае, если определены разные правила для одних и тех же пробелов, например в "$a = $b;" после "=" требуется поставить 1 пробел, а перед $b — убрать все пробелы, то порядок применения правил зависит от их приоритета. Приоритет операций описан в секции «PHPCF_EX-constants»: чем выше стоит правило, тем выше у него приоритет.

Хуки на токены


В свойстве $token_hook_names определены названия методов, которые должны вызываться, когда метод prepareTokens натыкается на этот токен. Хуки определяются в следующем виде:

<?php
namespace Phpcf\Impl;

class Pure implements \Phpcf\IFormatter
{
    /*
     * Массив $idx_tokens содержит в себе массив вида array(T_SOMETHING => 'T_SOMETHING'),
     * необходимый для составления обработанного вида токена
     *
     * Параметр $i_value представляет из себя один токен в виде, отдаваемом token_get_all
     *
     * Метод может сдвигать текущую итерируемую позицию вперед с помощью чтения $this->tokens с использованием each()
     *  пример такого метода: tokenHookStr
     *
     * Метод должен возвращать массив токенов, на которые нужно заменить
     * текущий итерируемый токен в уже преобразованном виде
     *
     * Пример минимального хука, который ничего не делает с токеном:
     */
    private function tokenHookDoNothing($idx_tokens, $i_value)
    {
        if (is_array($i_value)) {
            $this->current_line = $i_value[2];
            return array(
                array(
                    PHPCF_KEY_CODE => $idx_tokens[$i_value[0]],
                    PHPCF_KEY_TEXT => $i_value[1],
                    PHPCF_KEY_LINE => $this->current_line,
                )
            );
            // set correct current line for next token if it does not have line number
            $this->current_line += substr_count($i_value[1], "\n");
        }
 
        return array(
            array(
                PHPCF_KEY_CODE => $i_value,
                PHPCF_KEY_TEXT => $i_value,
                PHPCF_KEY_LINE => $this->current_line,
            )
        );
    }
}



Описание хуков для токенов


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

  • tokenHookHeredoc, tokenHookStr: по умолчанию PHP «токенизирует» текст внутри HEREDOC'ов, «двойных» и `косых` кавычек и выделяет там переменные. Поскольку форматер не должен трогать строки, содержимое объединяется в один токен;
  • tokenHookOpenBrace превращает токен "(" в "(_LONG" в том случае, если выражение в скобках является длинным (по умолчанию 120 символов) или в выражении есть перенос строки. Используется для того, чтобы различать «длинные» и «короткие» массивы, а также вызовы и определения функций;
  • tokenHookCheckUnary определяет, является ли оператор унарным (например "+", "-" и &). Служит для уменьшения количества переходов между контекстами в правилах;
  • tokenHookStatic отделяет вызовы вроде «static::HELLO» от использования в виде «public static function». Также служит для упрощения логики переходов между состояниями;
  • tokenHookClassdef определяет, что после ключевого слова идет перенос строки (например «const\n»), служит для корректного форматирования конструкций вида «const \n var1 = 1, \n var2 = 2;»;
  • tokenHookOpenTag проверяет, что открывающий тег является длинным, а также отделяет whitespace от открывающего тега (превращая "<?php \n" в два токена: "<?php" и "\n");
  • tokenHookCloseTag проверяет, что в конце файла нет закрывающего тега;
  • tokenHookIncrement определяет, с какой стороны от переменной находится оператор "++" или "–". Сделано для упрощения логики перехода между контекстами;
  • tokenHookWhiteSpace определяет, что выражения выровнены с помощью пробелов, и заменяет T_WHITESPACE на T_WHITESPACE_ALIGNED, который не трогается при форматировании;
  • tokenHookElse определяет, является ли else однострочным или содержит блоки. Сделано для упрощения логики;
  • tokenHookComment проверяет, что однострочные комментарии начинаются с "//", а также отделяет перенос строки от токена ("// something \n" превращается в "// something" и "\n");
  • tokenHookTString переименовывает T_STRING в T_FUNCTION_NAME, когда этот T_STRING — имя метода или функции. Сделано для упрощения логики перехода между контекстами;
  • tokenHookBinary служит для обработки ситуации с переносом оператора на следующую строку;
  • tokenHookComma превращает "," в ",_LONG" для запятых, которые можно перенести на новую строку в длинном массиве;
  • tokenHookFunction разделяет анонимные и неанонимные функции друг от друга.


Если хук меняет содержимое токена, то он должен проверить, имеет ли он право это делать ($can_change_tokens = !isset($this->lines) || isset($this->lines[$this->current_line])). Если не имеет, то хук не должен менять содержимое токенов, но может менять их количество и разделять токены на составные части. В качестве примера можно привести tokenHookOpenTag: он не проверяет содержимое открывающего тега, если пользователь запрашивал форматирование лишь части файла, но все равно отделяет whitespace от открывающего тега. Отделение whitespace требуется для того, чтобы правильно учитывалось количество строк (и indentation) после открывающего тега и однострочных комментариев.


Ссылки


PHP Beautifier — pear.php.net/package/PHP_Beautifier
PHP Code Sniffer — pear.php.net/package/PHP_CodeSniffer
PHP CS Fixer — github.com/fabpot/PHP-CS-Fixer

Наша утилита phpcf — github.com/badoo/phpcf

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

Юрий youROCK Насретдинов, PHP-разработчик Badoo
Александр alexkrash Крашенинников, PHP-разработчик Badoo

Автор: @youROCK
Badoo
рейтинг 199,70

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

  • +2
    Хорошая работа, и к какому стандарту пришли? Или свой придумали?
    github.com/php-fig/fig-standards/tree/master/accepted
    • +2
      Мы «придумали свой», но по факту он очень слабо отличается от PSR-*. В данный момент мы не предоставляем готового стиля для PSR-*, но мы работаем над этим.
      • +2
        Жаль, что не PSR, с удовольствием использовал бы. А есть какой-то документ, где кракто описаны отличия?
        • +3
          В данный момент такого документа нет, к сожалению. Из «значимых» отличий я могу вспомнить, к примеру, про то, что после type-cast операторов у нас убирается пробел:

          <?php
          $a = (int)$b; // phpcf "default" style
          $a = (int) $b; // PSR
          
          • +1
            Если нет документа который описывает различия, то может есть документ который описывает весь стиль кодирования?
            • +2
              Выложил: Стандарт форматирования Badoo (пока что только по-русски, в дальнейшем планируем также сделать перевод всего на английский)
              • 0
                Почему у вас запрещен импорт классов?
                • 0
                  Вы имеете в виду использование «use SomeClass as AnotherClass»?
                  • +2
                    Нет, я имею ввиду почему вас в принципе запрещен импорт классов?
                    К классу, находящемуся внутри неймспейса, следует обращаться как \Namescape\ClassName и никак иначе, за исключением случая, когда код находится внутри того же неймспейса, в этом случае можно опустить \Namespace.
                    • 0
                      На самом деле, phpcf это не проверяет, это наше внутреннее соглашение. Такое соглашение принято для того, чтобы облегчить grep по коду.
                      • +2
                        А что вы грепаете так часто, что пренебрегаете удобствами? Ну то есть usage-статистику если считать, можно и скриптик написать, а для чего еще?
                        Я почему так удивляюсь — потому что вот это на мой взгляд немного жесть)
                        • 0
                          Как мне кажется, вы сильно недооцениваете необходимость в использовании утилиты grep в проекте с парой миллионов строк ;).
                          • +1
                            Find usages в PhpStorm не пробовали? Говорят, лучше подходит для таких целей.
                            • +1
                              Для этого нужно ещё все тесты переписать на использования MyClass::$class вместо 'MyClass' в моках, например.
                              • 0
                                Абсолютно верно. Еще попадаются редкие места, помимо тестов, где классы тоже в виде строки. В основном это call_user_function.
                                Тем не менее почти всегда стараемся в новом коде избегать таких вещей.

                                Кстати важный момент насчет MyClass::$class — класс должен существовать, те в случае автолоада он сразу подгружается. Т.е. есть вдруг кому захочется прописать где-нибудь некий маппинг классов в массиве, то лучше уж использовать строки, чем подгружать их всех разом.
                                • 0
                                  Кстати важный момент насчет MyClass::$class — класс должен существовать, те в случае автолоада он сразу подгружается.

                                  Это неверное утверждение.
                                  • 0
                                    Я тоже могу кинуть фразу, что вы не правы и сделать крутое лицо.
                                    $a = Classname::$class; PHP Fatal error: Class 'Classname' not found in php shell code on line 1
                                    Возможно я где-то не прав. Приведите пример — это был бы более конструктивный комментарий.
                                  • 0
                                    Перечитал первоисточник(документацию) и понял в чем я не прав.
                                    Да вы правы, погорячился я. Хотя можно было с вашей стороны поправить нас с неправильным синтаксисом:
                                    ClassName::class действительно работает без подгрузки класса.
                                    • +1
                                      Я подумал, что это просто опечатка. $class — это ж просто обращение к статическому свойству, подобный magic был бы адом и ломал бы BC. А так реюзается уже имеющийся keyword.
      • 0
        Почему не настраиваемо? (тот же phpcs умеет принимать файлы с стандартами, что позволило его включить в PHPStorm в качестве штатного инспектора)
        • +2
          Точнее, в комментариях написано, что конфиг есть, но в гитхабе/в посте — мимо.
          • +1
            Стандарты кода описываются здесь github.com/badoo/phpcf/tree/master/phpcf-src/styles
            Например можно описать требуемые отличия от стандарта по умолчанию
            github.com/eelf/phpcf/commit/76c8c7205f4c316cb9f557ea960c8ed15a3a1eba#diff-d41d8cd98f00b204e9800998ecf8427e
            Работает так
            $ ~/phpcf/phpcf --style=compact apply classes/Db.php
            или так
            $ ~/phpcf/phpcf --style=minified apply classes/Db.php
            • 0
              Для публичного инструмента конфиг ужасен :)
              • +3
                Согласен, выглядит несколько пугающе, но при рассмотрении вариантов:

                1.) То же самое, в XML (плюсов с ходу не вижу, из минусов — потребить больше памяти, написать парсер конфига)
                2.) Инструкции, подобно тому, как PHP: lxr.php.net/xref/PHP_5_5/Zend/zend_language_parser.y (опять же, подсистему парсинга, но теперь уже — псевдокода конфига)
                3.) Сделать OOP-интерфейс для правил переходов
                $State ->when($ContextPredicate) ->then($Transition) ->done($ReverseTransition); // (а-ля Promises, не обещает быть меннее монстроузным)
                и для правил форматирования
                $ClassToken ->inContext($ClassDefinitionContext)->do($IndentIncreateAction) ->inContext($ClassReferenceContext)->do($IndentIncreateAction);
                4.) Текущая реализация (минимальный memory footprint, возможность программирования конфига на языке приложения)

                Последний вариант мне кажется наиболее оптимальным.
                • 0
                  yaml
  • +1
    добавление в packagist планируется?
    • 0
      Через небольшое количество времени мы планируем написать статью также на английском языке (не для Хабра), и мы постараемся учесть пожелания по формату распространения. Пока «формат распространения» у phpcf — это github, но идея добавить и в composer мне лично нравится.
  • –7
    Когда вожусь в чужом коде иногда очень хочется простой инструмент, который

    Из такого:
    <?php
    
    $result = a(b(c(d($data))));
    
    ?>
    

    делает примерно такое
    <?php
    
    $result4 = d($data);
    
    $result3 = c($result4);
    
    $result2 = b($result3);
    
    $result = a($result2);
    
    ?>
    


    А то названия функций или классы+методы очень длинные
    и это все в одну строку...


    • +1
      На самом деле, phpcf намеренно сделан таким, какой он есть, то есть, трогающим только пробелы, чтобы его можно было достаточно легко конфигурировать (то есть, не меняя исходный код, а изменяя конфиг). Мы «just for fun» писали простенькие утилиты по, к примеру, конвертации «старого синтаксиса массивов» в «новый», но более сложный анализ кода требует большой аккуратности, и пока что таких утилит у нас нет :).
      • +1
        Я только За простые утилиты.
        Это просто к теме читабельности кода. Возможно как в старом анекдоте, где из-за студента профессор с бородой не мог уснуть, вы также однажды решите что пришло время еще одной простой утилиты :)
  • 0
    Интересно, а существует ли плагин к PHPStorm (например), который анализирует выбранный проект на используемый стиль (думаю данных должно хватить по всем пунктам, если проект не мелкий), и сохраняет его как шаблон. А вот делаю Pull Request например куда-нибудь, так и хочется нажать хоткей форматирования, то все порушится)
    • 0
      Не уверен, существует ли, но на основе phpcf такой плагин сделать вполне реально :).
    • 0
      В Project Settings -> PHP -> Code Sniffer настройки PHPCS. Это самое близкое к требуемому.
      • 0
        Я говорил про Project Settings -> Code Style -> PHP, чтобы на основе загруженного проекта можно было сформировать правильный стиль, который используется в данном проекте и без проблем форматировать свой код не заботясь о «Code Style».
    • 0
      И еще Mess Detector там же. Щелкаете по директории — Inspect Code… и PHPStorm проверит по всем инспекциям, что у него есть (настроенные phpcs, phpmd, jshint, инспекции из Inspections, плюс разное — нераспознанные идешкой переменные, например).
  • –1
    Имхо неверно поставлена задача. Принимаете плохой код и правите его автоматически.
    А если просто не принимать плохой код, проверяя его через git с помощью PHP Code Sniffer?
    • +5
      Наш pre-receive хук проверяет плохое форматирование, используя phpcf. Т.е. производится форматирование измененных строк, и, если там код не отформатирован, коммит не проходит.
  • +2
    Не понял по тексту, а возможно ли этой утилите скормить в пайп просто кусок кода вместо имени файла и номеров строк начала/конца? А то, насколько я помню, в эклипсе можно получить выделенный кусок кода для внешней тулзы, а номера строк нельзя…
    • +1
      Действие «phpcf stdin» должно это делать. Помимо этого, вы можете воспользоваться функциональностью, описанной в секции «Использование классов phpcf напрямую»
      • 0
        Попробую, спасибо!
  • +2
    А что мешает в шторме импортировать удобный лично Вашей компании конфиг форматера(который более чем подробный в конфигурации) и пожамкать им в два клика целый проект?
    • +1
      «Несколько лет назад компания Badoo начала значительно расти по числу сотрудников, с 20 до 100 и более»

      Заставить под угрозой увольнения все сто человек выбросить любимый vim/emacs/netbeans? Это какой-то фашизм :)
    • +1
      Причин несколько, на самом деле. Помимо того, что не все используют PHPStorm, мы хотели поддерживать «отформатированность» кода с помощью git-хуков, настроенных на отклонение неотформатированных изменений, с возможностью легко исправить форматирование. Поэтому отдельная утилита представляется намного более удобным и гибким решением, чем использование форматирующей части одной из популярных IDE, тем более проприетарной :).
    • +1
      Как уже выше сказали, авто-форматирование в PhpStorm хорошая фича для небольших проектов с малым количеством задействованных разработчиков. Для большого проекта она бесполезна. Это всё нужно делать централизованно.
    • +2
      > пожамкать им в два клика целый проект?
      именно этого нам и хотелось избежать. Изначально (примерно 6-8 лет назад), у нас не было никакого стандарта. Точнее он был, но скорее как рекомендации и во многих местах он не был соблюден. Компания росла и в какой-то момент вопросы стандарта кодирования стали возникать всё чаще.
      Было 2 варианта — отформатировать разом весь репозиторий, или сделать так чтобы весь новый код был по стандарту, а старый код постепенно бы менялся/рефакторился и тем самым попал бы под новое форматирование.
      Полный реформат репозитория не понравился тем что сильно загрязнится история в git-е — много строк кода будут ссылаться на комит в котором был сделан реформат. Это неудобно потому что нам довольно часто приходится читать старый код, искать кто и почему написал ту или иную строку кода. Поэтому был выбран второй вариант
      • –3
        насколько я понимаю с вопросом хука запрещающего коммиты «не по-формату» прекрасно справляется phpcs
        вопрос автоформата в истории коммитов — его можно делать отдельным коммитом.

        Собственно что хотелось бы понять из всей этой предистории — почему решили писать что-то свое а не адаптировать существующее. Ну я просто не верю что шторм настолько проприетарен что нельзя на отдельном компьютере(например на машине с TeamCity или PhpCI) поднять его и делать автоформат по хукам, или даже написать свою java-оберточку с форматером и запускать именно ее.
        Если используете именно php как extension то почему не пойти с азов начиная с форка tokenizer'а или даже собственного bison-парсера?

        Сейчас не вдаваясь в подробности я вижу что-то вида «ну мы тут чтото удобное нам накодили на php и немножко на zend'е — оно правит пробелы и еще что-то полезное. Profit»
        Давайте лучше системный подход применим? Пойдем с понимания целей(в первую очередь) и задач?
        А извините, с форматированием Unix-style строк и sed прекрасно справляется!
        • +3
          1. В статье сказано почему нам не подошёл phpcs — он мог проверить формат, но не мог отформатировать код.
          2. Про автоформат.
          Вы настаиваете что автоформат всего репозитория в нашем случае было бы правильным решением? Почему?
          Очевидно что если бы мы решили его делать, то это был бы отдельный комит, но всё равно это сделало бы работу с репозиторием менее удобной — менее удобно было бы получать историю по всем строкам, задетым в момент форматирования. Баду репозиторий существует наверное более 8 лет, вопрос с форматированием кода начали решать где-то 3-4 года назад (могу ошибаться, точно не помню). То есть большАя часть кода в репозитории за первые 4-5 лет в git blame указывала бы на коммит автоформата, вместо того чтобы указывать на реальные «боевые» коммиты. С таким репозиторием неудобно работать.
          И не стоит думать что мы сразу кинулись писать php экстеншен, сначало конечно был простенький скриптик на чистом php.

          На остальные вопросы затрудняюсь ответить — я лишь пользователь phpcf, а не его разработчик.
        • +3
          Причина, по которой мы написали phpcf на PHP, а не стали «раздраконивать PHPStorm» очень простая — мы (100+ разработчиков) умеем писать и готовить PHP, и не умеем готовить Java :). Первую версию phpcf написал fisher за пару вечеров, а с любыми другими утилитами на Java так бы не получилось :), даже если там всё на самом деле просто — мы не Java-программисты
      • 0
        Полный реформат репозитория не понравился тем что сильно загрязнится история в git-е — много строк кода будут ссылаться на комит в котором был сделан реформат.

        Это ж гит, в нем если нельзя, но хочется — то можно. :) Сделать реформат с сохранением истории, скажем, можно с помощью filter-branch.
        • 0
          После filter-branch изменятся хеши всех коммитов, и придется _везде_ переклонировать репозитории :). А разработчиков у нас немало, плюс ещё деплой, teamcity, и прочие места, где есть репозиторий — это очень большой гемморой был бы для нас.
          • +1
            Понятно, что изменятся. :) Один раз, кмк, такое терпимо, это проще, чем, скажем, переезд с svn ;)
  • +1
    Попробовал интегрировать в PHP Storm но вот что выводит в консоле при попытке обработать файл:
    Internal formatter error: final state must be CTX_PHP or CTX_DEFAULT, got 'CTX_PHP / CTX_DEFAULT'

    Проверяю на ОС Win8

    А вот ошибка при попытке обработать выделенную область:
    Internal formatter error: final state must be CTX_PHP or CTX_DEFAULT, got 'CTX_PHP / CTX_PHP / CTX_PHP / CTX_PHP / CTX_GENERIC_BLOCK / CTX_PHP / CTX_PHP / CTX_GENERIC_BLOCK / CTX_GENERIC_BLOCK / CTX_PHP / CTX_DEFAULT'
    • +1
      Проблема форматирования связана с комбинированным использованием PHP + HTML в файле.
      По инциденту заведен Issue на github, спасибо за репорт.
    • +1
      Та-же ошибка при работе из cli 5.5.15 ubuntu
      • +2
        Если вы не используете смесь HTML + PHP (то есть, случай, по которому мы уже завели тикет на гитхабе), будьте добры пожалуйста привести полный текст ошибки и содержимое файла, на котором у вас происходит ошибка (можно сразу на github, или мне или alexkrash в личку)
        • +2
          php+html, комментарии писали одновременно.
  • –1
    А разве не проще настроить подобные простые замены в редакторе кода? Лично у меня это есть в VIM, а всякие там PHPStorm должны это всё уметь по-умолчанию. Или я не прав?
  • 0
    В PHPUnit есть известный bug с покрытием таких конструкций:
    $var = false;
    if($var == true)
        return false;
    if($var == true) return false;
    

    If в данном случае не отработает, однако Xdebug считает его покрытым. Как выход можно заключить операторы после if в фигурные скобки:
    $var = false;
    if($var == true) {
        return false;
    }
    if($var == true) { return false; }
    

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

    Вопрос вот в чем, как в badoo справляетесь с этой проблемой, если ваш форматтер как раз убирает фигурные скобки?
    • 0
      Все просто: наш форматтер их не убирает :)
    • 0
      А я вот немного не понял. Не поясните, что имеется в виду под «считает покрытым»? Не помечает к удалению второй if?
      • 0
        Нет, просто отмечает строку, что программа туда зашла, а на самом деле захода не было.

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

Самое читаемое Разработка