Как обнаружить динамически объявленные поля в объектах с помощью codeniffer в PHP

После рефакторинга у нас было что-то вроде этого в одном из наших классов:

class FooBar
{
    // $foo was $bla before
    private $foo;

    public function setBlubbOnArrayOnlyOnce($value)
    {
        // $this->bla was forgotten during refactoring. Must be $this->foo
        if(!isset($this->bla['blubb'])) {
             $this->foo['blubb'] = $value;
        }
    }
}

Так что, в конце концов, $this->foo['blubb'] устанавливалось всегда, а не только один раз. Это происходит из-за магических методов PHP. Мы не хотим, чтобы был возможен динамический доступ к полям, поэтому я решил просто добавить правило codeniffer. Но я ничего не нашел и спросил почему.

PHPStorm показывает поле, объявленное динамически, уведомление там, но я хочу, чтобы это автоматически терпело неудачу с codeniffer (или чем-то подобным) во время нашего цикла развертывания.

У кого-нибудь есть идеи по этому поводу? Есть хорошее правило? Должен ли я написать свой собственный и как? Или было бы плохой практикой отключить его?

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


person Kasihasi    schedule 13.08.2015    source источник
comment
Можете ли вы искать неопределенные переменные, так как $this-›bla не будет объявлено? Возможно, вам придется расширить код в PHPCodeSniffer.   -  person Steven Scott    schedule 13.08.2015
comment
Я пытаюсь, но я надеялся на очевидный и простой способ сделать это.   -  person Kasihasi    schedule 14.08.2015
comment
Вы спрашивали на Squizlabs (squizlabs.com) или на их GitHub (github.com/squizlabs/PHP_CodeSniffer), так как Грег Шервуд в прошлом довольно быстро отвечал на вопросы.   -  person Steven Scott    schedule 14.08.2015
comment
Вероятно, это не так просто, поскольку, как правило, $this->bla может быть определен в родительском классе. Codesniffer работает на уровне файла/токена, и если вы будете следовать стандартам кодирования PSR, он не будет знать структуру родительского класса (поскольку он находится в отдельном файле).   -  person weirdan    schedule 08.01.2016
comment
@Weirdan Возможно, это правда. Но было бы хорошо, если бы неопределенные свойства можно было обнаружить в простых классах, не расширяющих какой-либо другой класс.   -  person maxhb    schedule 10.01.2016
comment
Почему бы вам не объявить private $foo = []; как массив в классе?   -  person Ravi    schedule 12.01.2016
comment
Вы пробовали property_exists? php.net/manual/en/function.property-exists.php   -  person Ravi    schedule 12.01.2016


Ответы (3)


Это не проблема codesniffer или phpstorm. И вы не можете решить эту проблему с помощью codeniffer или IDE. IDE, codeniffer, phpdocumentor и т. д. -- это "статический" анализ. А для динамического анализа вы можете использовать, например. phpunit.

Если вы хотите проверить наличие свойства, вы должны использовать функцию property_exists().

class X
{
    public function __get($name)
    {
        $this->{$name} = null;
        return $this->{$name};
    }
}

$x = new X();
var_dump(property_exists($x, 'foo')); // false
var_dump($x->foo); // NULL
var_dump(property_exists($x, 'foo')); // true

Или, может быть, вы можете использовать отражение для свойства http://php.net/manual/en/class.reflectionproperty.php

Если вы хотите проверить «isset», вы должны знать:

var_dump(isset($x), $x); // false + NULL with notice
$x = null;
var_dump(isset($x), $x); // false + NULL
unset($x);
var_dump(isset($x), $x); // false + NULL without notice

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

Но вы всегда должны сначала проверить наличие свойства. В противном случае вы можете иметь неопределенное поведение вашего кода.

person Deep    schedule 10.01.2016
comment
Речь идет не об обнаружении того, определен ли $this->bar[''anyStringValue (возможно, это даже невозможно при статическом анализе), речь идет об обнаружении того, что $this-›bar не определен (больше). - person maxhb; 10.01.2016
comment
@maxhb Мой ответ в том числе и об этом. - person Deep; 10.01.2016

После рефакторинга

Было бы хорошо предотвратить это в первую очередь.

Вы можете обнаружить такие ошибки рефакторинга, только запуская тесты после каждого шага рефакторинга. Эта ошибка также будет всплывать, потому что foo['blubb'] установлено определенное значение, и это должно вызвать нежелательный эффект в другом тесте — не только в тесте на логику сеттера.

Мы используем тесты, но иногда что-то упускаешь...

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

Эти две строки не были зелеными в вашем отчете о покрытии:

   if(!isset($this->bla['blubb'])) {
       $this->foo['blubb'] = $value;

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

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


Теперь, когда уже слишком поздно, вы хотите, чтобы другой инструмент перехватил ошибку, хорошо:

Инструмент должен будет проанализировать файл PHP и его родителей (из-за области видимости переменной) и найти $this->bla без предварительного объявления переменной public|private|protected (свойства класса). Это не будет указывать точный тип ошибки, просто доступ к bla был осуществлен без объявления.

Это можно реализовать как правило CodeSniffer.

Вы также можете указать http://phpmd.org/ или https://scrutinizer-ci.com/ попробуйте. И, если вы используете PHP7: https://github.com/etsy/phan

tl;tr

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

Динамический анализатор должен отслеживать все, здесь $this-> обращается и будет учитывать контекст: !isset(x). Оценка контекста может найти множество распространенных ошибок кодирования. В конце вы можете построить отчет: говоря, что $this-›bla был доступен только 1 раз, и это указывает либо на то, что

  • было введено динамически объявленное свойство, но никогда не использовалось повторно, с предложением удалить его или объявить как свойство класса.
  • ИЛИ с добавленной оценкой контекста: что и когда эта переменная была доступна изнутри isset() - что был получен доступ к несуществующему ключу необъявленного свойства, без предварительного set() и т. д.
person Jens A. Koch    schedule 13.01.2016

Сейчас, в 2017 году, вы ищете инструмент PHPStan. Я связываю краткое введение, которое я написал для новых пользователей.

Он делает именно то, что вам нужно!

person Tomas Votruba    schedule 21.05.2017