PHPCS: PHP Code Sniffer руководство

PHP Code Sniffer (PHPCS) — это пакет для проверки синтаксиса, доступный на сайте PEAR. Он может проверять код на соответствие определенным правилам, охватывающим все: от пробелов и комментариев к документам до соглашений об именах переменных и многое другое. В этой статье мы рассмотрим начало работы с PHPCS, использование его для проверки синтаксиса наших файлов, а затем создание правил и определение стандартов.

Установка PHPCS

Существует два основных способа установки PHPCS: напрямую или через PEAR. Использование репозиториев PEAR рекомендуется и адаптируется ко всем различным платформам. Это также, вероятно, хорошо знакомо всем разработчикам PHP!

Альтернативой является использование метода, доступного для вашей системы. Например, в системе Ubuntu был пакет Aptitude под названием php-codesniffer, который установит для вас эту функциональность.

Чтобы использовать метод PEAR, вам просто нужно обновить репозиторий PEAR, а затем ввести:

pear install PHP_CodeSniffer

Теперь, когда пакет установлен, пришло время посмотреть, что он может для нас сделать.

Использование PHPCS

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

По умолчанию PHPCS поставляется с предустановленным рядом определений стандартов кодирования. Чтобы увидеть, какие определения доступны для проверки, используйте ключ -i:

phpcs -i
The installed coding standards are MySource, PEAR, Squiz, PHPCS and Zend

Здесь показаны некоторые стандарты кодирования по умолчанию, включая стандарт PHPCS, стандарт Zend (используемый Zend Framework и многими другими проектами) и широко известный стандарт PEAR.

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

Возьмите следующий пример кода:

_id);
return $ingredients;
}
}
?>

Для проверки кода этого класса на соответствие стандарту Zend мы используем следующий синтаксис:

phpcs --standard=Zend recipe.class.php

FILE: /home/lorna/phpcs/recipe.class.php
--------------------------------------------------------------------------------
FOUND 3 ERROR(S) AND 0 WARNING(S) AFFECTING 3 LINE(S)
--------------------------------------------------------------------------------
 10 | ERROR | Variable "prep_time" is not in valid camel caps format
 13 | ERROR | Spaces must be used to indent lines; tabs are not allowed
 17 | ERROR | A closing tag is not permitted at the end of a PHP file
--------------------------------------------------------------------------------

Однако стандарты Zend не требуют некоторых элементов, которые требуются в других стандартах. Например, стандарты PEAR ожидают, что имена классов и открывающие фигурные скобки будут находиться на новых строках, и это станет очевидным, если мы проверим тот же файл рецепта.class.php на соответствие вместо этого стандарт PEAR.

Мы используем тот же синтаксис, что и раньше, но меняем ключ --standard на PEAR:

FILE: /home/lorna/phpcs/recipe.class.php
--------------------------------------------------------------------------------
FOUND 8 ERROR(S) AND 0 WARNING(S) AFFECTING 5 LINE(S)
--------------------------------------------------------------------------------
2 | ERROR | Missing file doc comment
3 | ERROR | Class name must begin with a capital letter
3 | ERROR | Missing class doc comment
6 | ERROR | Protected member variable "_id" must not be prefixed with an
| | underscore
 12 | ERROR | Missing function doc comment
 12 | ERROR | Opening brace should be on a new line
 13 | ERROR | Line indented incorrectly; expected at least 8 spaces, found 1
 13 | ERROR | Spaces must be used to indent lines; tabs are not allowed
--------------------------------------------------------------------------------

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

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

Вот обновленный файл класса:

<?php
 
class Recipe
{
 
    protected $id;
 
    public $name;
 
    public $prep_time;
 
    function getIngredients()
    {
        $ingredients = Ingredients::fetchAllById($this->_id);
        return $ingredients;
    }
}
?>

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

3c3
< class recipe
---
> class Recipe
6c6
<     protected $_id;
---
>     protected $id;
12,13c12,14
<     function getIngredients() {
<   $ingredients = Ingredients::fetchAllById($this->_id);
---
>     function getIngredients()
>     {
>         $ingredients = Ingredients::fetchAllById($this->_id);

Теперь, если мы повторно проверим файл на соответствие стандартам PEAR, вы увидите, что только отсутствующие комментарии PHPDocumentor указаны как проблемы с файлом:

FILE: /home/lorna/phpcs/recipe.class.php
--------------------------------------------------------------------------------
FOUND 3 ERROR(S) AND 0 WARNING(S) AFFECTING 3 LINE(S)
--------------------------------------------------------------------------------
2 | ERROR | Missing file doc comment
3 | ERROR | Missing class doc comment
 12 | ERROR | Missing function doc comment
--------------------------------------------------------------------------------

Чтобы наш код прошел правильную проверку, мы можем добавить комментарии, ожидаемые PHPDocumentor . Существует несколько отличных руководств по работе с этим инструментом, но лучше всего начать с их домашней страницы http://www.phpdoc.org/ .

Вот класс с добавленными комментариями:

<?php
 
/**
 * Recipe class file
 *
 * PHP Version 5.2
 *
 * @category Recipe
 * @package  Recipe
 * @author   Lorna Jane Mitchell <lorna@ibuildings.com>
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
 * @link     http://example.com/recipes
 */
 
/**
 * Recipe class
 *
 * The class holding the root Recipe class definition
 *
 * @category Recipe
 * @package  Recipe
 * @author   Lorna Jane Mitchell <lorna@ibuildings.com>
 * @license  http://opensource.org/licenses/gpl-license.php GNU Public License
 * @link     http://example.com/recipes/recipe
 */
class Recipe
{
 
    protected $id;
 
    public $name;
 
    public $prep_time;
 
    /**
     * Get the ingredients
     *
     * This function calls a static fetching method against the Ingredient class
     * and returns everything matching this recipe ID
     *
     * @return array An array of Ingredient objects
     */
    function getIngredients()
    {
        $ingredients = Ingredient::fetchAllByRecipe($this->id);
        return $ingredients;
    }
}
?>

Принцип работы

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

Array
(
    [0] => Array
        (
            [0] => 367
            [1] => <?php
 
            [2] => 1
        )
 
    [1] => Array
        (
            [0] => 370
            [1] =>
 
            [2] => 2
        )
 
    [2] => Array
        (
            [0] => 333
            [1] => function
            [2] => 3
        )
 
    [3] => Array
        (
            [0] => 370
            [1] =>
            [2] => 3
        )
 
    [4] => Array
        (
            [0] => 307
            [1] => getIngredients
            [2] => 3
        )
 
    [5] => (
    [6] => )
    [7] => Array
        (
            [0] => 370
            [1] =>
            [2] => 3
        )
 
    [8] => {
    [9] => Array
        (
            [0] => 370
            [1] =>
 
            [2] => 3
        )
 
    [10] => Array
        (
            [0] => 309
            [1] => $ingredients
            [2] => 4
        )
... (truncated)

Вывод усекается из-за его огромного размера, даже если его проверять с помощью print_r, а не var_dump. Токенизация на самом деле — это функциональность, доступная нам в самом PHP — метод get_all_tokens(), который принимает строку. Функция file_get_contents использовалась для получения содержимого файла в качестве аргумента get_all_tokens, и то, что вы видите выше, является результатом этого print_r.

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

Создание правил

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

Обычно при установке Ubuntu они находятся в /usr/share/php/PHP/CodeSniffer/Standards. Все стандарты выходят из базового CodingStandard.php в этом каталоге.

Здесь определяются два простых метода: getIncludedSniffs() и getExcludedSniffs(). Это позволяет нам использовать существующие определения стандартов и просто добавлять и удалять отдельные стандарты, чтобы создать стандарт, который подойдет нам.

Эти «обнюхивания» представляют собой атомарные правила, охватывающие все: от длины строки до именования переменных и обнаружения «запахов кода», таких как недостижимый код или плохо отформатированные циклы. Каждый стандарт кодирования имеет свой собственный каталог «Sniffs», и все, что сюда включено, автоматически становится частью стандарта. Однако каждый стандарт может также использовать фрагменты других стандартов, и существует большой набор «стартовых» анализов, используемых в большинстве стандартов, которые включены в PHPCS в каталоге Generic.

Чтобы понять, что такое sniffs и как они наследуются, давайте взглянем на настоящий стандарт. Стандарт PEAR находится в каталоге PEAR, а файл класса — PEARCodingStandard.php (это расширение носит чисто методический характер!).

Класс выглядит так:

<?php
/**
 * PEAR Coding Standard.
 *
 * PHP version 5
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
 * @version   CVS: $Id: PEARCodingStandard.php,v 1.6 2007/08/02 23:18:31 squiz Exp $
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
 
if (class_exists('PHP_CodeSniffer_Standards_CodingStandard', true) === false) {
    throw new PHP_CodeSniffer_Exception('Class PHP_CodeSniffer_Standards_CodingStandard not found');
}
 
/**
 * PEAR Coding Standard.
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
 * @version   Release: 1.1.0
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
class PHP_CodeSniffer_Standards_PEAR_PEARCodingStandard extends PHP_CodeSniffer_Standards_CodingStandard
{
 
 
    /**
     * Return a list of external sniffs to include with this standard.
     *
     * The PEAR standard uses some generic sniffs.
     *
     * @return array
     */
    public function getIncludedSniffs()
    {
        return array(
                'Generic/Sniffs/Formatting/MultipleStatementAlignmentSniff.php',
                'Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php',
                'Generic/Sniffs/NamingConventions/UpperCaseConstantNameSniff.php',
                'Generic/Sniffs/PHP/LowerCaseConstantSniff.php',
                'Generic/Sniffs/PHP/DisallowShortOpenTagSniff.php',
                'Generic/Sniffs/WhiteSpace/DisallowTabIndentSniff.php',
               );
 
    }//end getIncludedSniffs()
 
 
}//end class
?>

Этот класс показывает, что в него включены некоторые фрагменты из каталога Generic, а также те, что специфичны для этого стандарта. Глядя на Sniffs for PEAR, мы видим, что у них есть следующее:

./Classes:
ClassDeclarationSniff.php
 
./Commenting:
ClassCommentSniff.php  FileCommentSniff.php  FunctionCommentSniff.php  InlineCommentSniff.php
 
./ControlStructures:
ControlSignatureSniff.php  InlineControlStructureSniff.php
 
./Files:
IncludingFileSniff.php  LineEndingsSniff.php  LineLengthSniff.php
 
./Functions:
FunctionCallArgumentSpacingSniff.php  FunctionCallSignatureSniff.php  ValidDefaultValueSniff.php
 
./NamingConventions:
ValidClassNameSniff.php  ValidFunctionNameSniff.php  ValidVariableNameSniff.php
 
./WhiteSpace:
ScopeClosingBraceSniff.php  ScopeIndentSniff.php

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

Функция Sniffs для стандарта PEAR

Класс PEARCodingStandard включает в себя анализатор Generic/Sniffs/Functions/OpeningFunctionBraceBsdAllmanSniff.php.

Более пристальный взгляд на этот каталог Generic/Sniffs/Functions показывает, что существует также стандарт под названием OpeningFunctionBraceKernighanRitchieSniff.php. Это небольшой кусочек истории информатики. Существуют две школы мысли о том, где должна располагаться открывающая скобка при объявлении функции.

Брайан Керниган и Деннис Ритчи (которые вместе изобрели Unix и C) выступали за то, чтобы оно располагалось в той же строке, что и объявление функции, тогда как в стиле BSD, поддерживаемом Эриком Оллманом (создателем sendmail), оно размещается на следующей строке. PEAR, как известно, использует стиль «на новой строке», поэтому OpeningFunctionBraceBsdAllmanSniff используется в стандарте PEAR.

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

<?php
/**
 * Generic_Sniffs_Methods_OpeningMethodBraceBsdAllmanSniff.
 *
 * PHP version 5
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
 * @version   CVS: $Id: OpeningFunctionBraceBsdAllmanSniff.php,v 1.8 2008/05/05 03:59:12 squiz Exp $
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
 
/**
 * Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff.
 *
 * Checks that the opening brace of a function is on the line after the
 * function declaration.
 *
 * @category  PHP
 * @package   PHP_CodeSniffer
 * @author    Greg Sherwood <gsherwood@squiz.net>
 * @author    Marc McIntyre <mmcintyre@squiz.net>
 * @copyright 2006 Squiz Pty Ltd (ABN 77 084 670 600)
 * @license   http://matrix.squiz.net/developer/tools/php_cs/licence BSD Licence
 * @version   Release: 1.1.0
 * @link      http://pear.php.net/package/PHP_CodeSniffer
 */
class Generic_Sniffs_Functions_OpeningFunctionBraceBsdAllmanSniff implements PHP_CodeSniffer_Sniff
{
 
 
    /**
     * Registers the tokens that this sniff wants to listen for.
     *
     * @return void
     */
    public function register()
    {
        return array(T_FUNCTION);
 
    }//end register()
 
 
    /**
     * Processes this test, when one of its tokens is encountered.
     *
     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
     * @param int                  $stackPtr  The position of the current token in the
     *                                        stack passed in $tokens.
     *
     * @return void
     */
    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
    {
        $tokens = $phpcsFile->getTokens();
 
        if (isset($tokens[$stackPtr]['scope_opener']) === false) {
            return;
        }
 
        $openingBrace = $tokens[$stackPtr]['scope_opener'];
 
        // The end of the function occurs at the end of the argument list. Its
        // like this because some people like to break long function declarations
        // over multiple lines.
        $functionLine = $tokens[$tokens[$stackPtr]['parenthesis_closer']]['line'];
        $braceLine    = $tokens[$openingBrace]['line'];
 
        $lineDifference = ($braceLine - $functionLine);
 
        if ($lineDifference === 0) {
            $error = 'Opening brace should be on a new line';
            $phpcsFile->addError($error, $openingBrace);
            return;
        }
 
        if ($lineDifference > 1) {
            $ender = 'line';
            if (($lineDifference - 1) !== 1) {
                $ender .= 's';
            }
 
            $error = 'Opening brace should be on the line after the declaration; found '.($lineDifference - 1).' blank '.$ender;
            $phpcsFile->addError($error, $openingBrace);
            return;
        }
 
        // We need to actually find the first piece of content on this line,
        // as if this is a method with tokens before it (public, static etc)
        // or an if with an else before it, then we need to start the scope
        // checking from there, rather than the current token.
        $lineStart = $stackPtr;
        while (($lineStart = $phpcsFile->findPrevious(array(T_WHITESPACE), ($lineStart - 1), null, false)) !== false) {
            if (strpos($tokens[$lineStart]['content'], $phpcsFile->eolChar) !== false) {
                break;
            }
        }
 
        // We found a new line, now go forward and find the first non-whitespace
        // token.
        $lineStart = $phpcsFile->findNext(array(T_WHITESPACE), $lineStart, null, true);
 
        // The opening brace is on the correct line, now it needs to be
        // checked to be correctly indented.
        $startColumn = $tokens[$lineStart]['column'];
        $braceIndent = $tokens[$openingBrace]['column'];
 
        if ($braceIndent !== $startColumn) {
            $error = 'Opening brace indented incorrectly; expected '.($startColumn - 1).' spaces, found '.($braceIndent - 1);
            $phpcsFile->addError($error, $openingBrace);
        }
 
    }//end process()
 
 
}//end class
 
?>

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

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

Раз уж мы заговорили о структуре функций, давайте также рассмотрим особенности PEAR для функций.

Они есть:

  • FunctionCallArgumentSpacingSniff.php
  • FunctionCallSignatureSniff.php
  • Валиддефаултувалуеснифф.php

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

Каждый из них применяет определенный элемент хорошей практики при объявлении функций, предупреждая вас, например, когда аргументы без значений по умолчанию помещаются после аргументов со значениями по умолчанию. Они также смотрят на расстояние вокруг скобок и аргументы в объявлениях функций (PEAR не допускает пробелов вокруг скобок, но требует их между аргументами после запятой).

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

Заключительные мысли о стандартах кодирования

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

Конечно, попытаться написать код в незнакомом стандарте без поддержки такого инструмента, как PHPCS, сложно, если это вообще возможно. Поскольку я работаю со многими различными базами кода, принадлежащими клиентам, проектам с открытым исходным кодом и собственным, я вижу много разных стандартов и мне приходится кодировать несколько стандартов, а иногда и все в течение одного дня!

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

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