Молчание предупреждений "Декларация... должна быть совместима" в PHP 7

После обновления до PHP 7 логи почти забиты такими ошибками:

PHP Warning: Declaration of Example::do($a, $b, $c) should be compatible with ParentOfExample::do($c = null) in Example.php on line 22548

Как мне замолчать эти и только эти ошибки в PHP 7?

  • До PHP 7 они были E_STRICT тип предупреждений, с которыми можно легко справиться. Теперь это просто старые предупреждения. Поскольку я действительно хочу знать о других предупреждениях, я не могу просто отключить все предупреждения.

  • У меня нет умственных способностей переписать эти устаревшие API, даже не упоминая все программное обеспечение, которое их использует. Угадай, за это тоже никто не заплатит. И я не занимаюсь их разработкой, поэтому я не виноват. (Юнит-тесты? Не в моде десять лет назад.)

  • Я хотел бы избежать любых хитростей с func_get_args и похоже как можно больше.

  • Не очень я хочу перейти на PHP 5.

  • Я все еще хочу знать о других ошибках и предупреждениях.

Есть ли чистый и хороший способ сделать это?

8 ответов

Решение

1. Обходной путь

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

if (PHP_MAJOR_VERSION >= 7) {
    set_error_handler(function ($errno, $errstr) {
       return strpos($errstr, 'Declaration of') === 0;
    }, E_WARNING);
}

Этот обработчик ошибок возвращает true для предупреждений, начинающихся с Declaration of который в основном говорит PHP о предупреждении. Вот почему PHP не будет сообщать об этом предупреждении в другом месте.

Кроме того, этот код будет работать только на PHP 7 или выше.


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

if (PHP_MAJOR_VERSION >= 7) {
    set_error_handler(function ($errno, $errstr, $file) {
        return strpos($file, 'path/to/legacy/library') !== false &&
            strpos($errstr, 'Declaration of') === 0;
    }, E_WARNING);
}

2. Правильное решение

Что касается фактического исправления чужого унаследованного кода, существует ряд случаев, когда это можно сделать между простым и управляемым. В примерах ниже класса B это подкласс A, Обратите внимание, что вы не обязательно удалите любые нарушения LSP, следуя этим примерам.

  1. Некоторые случаи довольно просты. Если в подклассе отсутствует аргумент по умолчанию, просто добавьте его и продолжайте. Например, в этом случае:

    Declaration of B::foo() should be compatible with A::foo($bar = null)
    

    Вы бы сделали:

    - public function foo()
    + public function foo($bar = null)
    
  2. Если в подкласс добавлены дополнительные ограничения, удалите их из определения, перемещаясь внутри тела функции.

    Declaration of B::add(Baz $baz) should be compatible with A::add($n)
    

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

    - public function add(Baz $baz)
    + public function add($baz)
      {
    +     assert($baz instanceof Baz);
    

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

    - protected function setValue(Baz $baz)
    + /**
    +  * @param Baz $baz
    +  */
    + protected function setValue($baz)
      {
    +     /** @var $baz Baz */
    
  3. Если у вашего подкласса меньше аргументов, чем у суперкласса, и вы можете сделать их необязательными в суперклассе, просто добавьте заполнители в подкласс. Заданная строка ошибки:

    Declaration of B::foo($param = '') should be compatible with A::foo($x = 40, $y = '')
    

    Вы бы сделали:

    - public function foo($param = '')
    + public function foo($param = '', $_ = null)
    
  4. Если вы видите какие-то аргументы, требуемые в подклассе, возьмите дело в свои руки.

    - protected function foo($bar)
    + protected function foo($bar = null)
      {
    +     if (empty($bar['key'])) {
    +         throw new Exception("Invalid argument");
    +     }
    
  5. Иногда может быть проще изменить метод суперкласса, чтобы полностью исключить необязательный аргумент, возвращаясь к func_get_args магия. Не забудьте документировать недостающий аргумент.

      /**
    +  * @param callable $bar
       */
    - public function getFoo($bar = false)
    + public function getFoo()
      {
    +     if (func_num_args() && $bar = func_get_arg(0)) {
    +         // go on with $bar
    

    Конечно, это может стать очень утомительным, если вам нужно удалить более одного аргумента.

  6. Все становится намного интереснее, если у вас есть серьезные нарушения принципа замещения. Если у вас нет набранных аргументов, то это легко. Просто сделайте все дополнительные аргументы необязательными, а затем проверьте их наличие. Данная ошибка:

    Declaration of B::save($key, $value) should be compatible with A::save($foo = NULL)
    

    Вы бы сделали:

    - public function save($key, $value)
    + public function save($key = null, $value = null)
      {
    +     if (func_num_args() < 2) {
    +         throw new Exception("Required argument missing");
    +     }
    

    Обратите внимание, что мы не могли использовать func_get_args() здесь, потому что он не учитывает стандартные (не переданные) аргументы. Нам осталось только func_num_args(),

  7. Если у вас есть целые иерархии классов с расходящимся интерфейсом, возможно, будет проще расходиться еще дальше. Переименуйте функцию с конфликтующим определением в каждом классе. Затем добавьте прокси-функцию в одном родительском посреднике для этих классов:

    function save($arg = null) // conforms to the parent
    {
        $args = func_get_args();
        return $this->saveExtra(...$args); // diverged interface
    }
    

    Таким образом, LSP все равно будет нарушен, хотя и без предупреждения, но вы сохраните все проверки типов, которые вы имеете в подклассах.

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

//"Warning: Declaration of B::foo($arg1) should be compatible with A::foo()"
class B extends A {
    function foo($arg1) {}
}

class A {
    function foo() {}
}

Это не будет:

class B extends A {
    function foo($arg1 = null) {}
}

class A {
    function foo() {}
}

Если вы должны заставить ошибку замолчать, вы можете объявить класс внутри заглушенного, немедленно вызванного выражения функции:

<?php

// unsilenced
class Fooable {
    public function foo($a, $b, $c) {}
}

// silenced
@(function () {
    class ExtendedFooable extends Fooable {
        public function foo($d) {}
    }
})();

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


Если вам необходимо поддерживать совместимость с PHP 5, имейте в виду, что приведенный выше код работает только в PHP 7, поскольку PHP 5 не имел единого синтаксиса для выражений. Чтобы заставить его работать с PHP 5, вам нужно присвоить функцию переменной перед ее вызовом (или сделать ее именованной функцией):

$_ = function () {
    class ExtendedFooable extends Fooable {
        public function foo($d) {}
    }
};
@$_();
unset($_);

PHP 7 удаляет E_STRICT уровень ошибки. Информацию об этом можно найти в заметках о совместимости с PHP7. Вы также можете прочитать документ с предложением, где он обсуждался во время разработки PHP 7.

Простой факт заключается в следующем: E_STRICT Уведомления были введены несколько версий назад, в попытке уведомить разработчиков о том, что они используют дурную практику, но изначально без попыток форсировать какие-либо изменения. Однако последние версии, и в частности PHP 7, стали более строгими в этом отношении.

Ошибка, которую вы испытываете, является классическим случаем:

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

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

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

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

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

У вас есть возможность вернуться к PHP 5.6. Я знаю, что вы сказали, что не хотите этого делать, но в качестве краткосрочного и среднесрочного решения это облегчит вам задачу. Честно говоря, я думаю, что это может быть вашим лучшим вариантом.

Я согласен: пример в первом посте - плохая практика. А что если у вас есть этот пример:

class AnimalData {
    public $shout;
}

class BirdData extends AnimalData {
    public $wingNumber;
}

class DogData extends AnimalData {
    public $legNumber;
}

class AnimalManager {
    public static function displayProperties(AnimalData $animal) {
        var_dump($animal->shout);
    }
}

class BirdManager extends AnimalManager {
    public static function displayProperties(BirdData $bird) {
        self::displayProperties($bird);
        var_dump($bird->wingNumber);
    }
}

class DogManager extends AnimalManager {
    public static function displayProperties(DogData $dog) {
        self::displayProperties($dog);
        var_dump($dog->legNumber);
    }
}

Я считаю, что это допустимая структура кода, тем не менее, это вызовет предупреждение в моих журналах, потому что "displayProperties" не имеют одинаковые параметры. Более того, я не могу сделать их необязательными, добавив после них " = null"...

Правильно ли я думаю, что это предупреждение неправильно в этом конкретном примере, пожалуйста?

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

  1. изменить имя функции в подклассе (чтобы оно больше не перекрывало родительскую функцию)
  2. измените параметры родительской функции, но сделайте дополнительные параметры необязательными (например, функция func($var1, $var2=null) - это может быть самым простым и требующим меньшего количества изменений кода. Но это может не стоить изменять в родитель, если он использовал так много других мест. Так что я пошел с #1 в моем случае.

  3. Если это возможно, вместо передачи дополнительных параметров в функции подкласса используйте global для извлечения дополнительных параметров. Это не идеальное кодирование; но возможный лейкопластырь в любом случае.

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

public function __call($name, $args)
{
    if($name == 'do') {
        // do things with the unknown # of args
    } else {
        throw new \Exception("Unknown method $name", 500);
    }
}

Я просто столкнулся с этой проблемой и пошел по этому маршруту

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

          $namespace = 'default';
    if (func_num_args() > 2) {
        $namespace = func_get_arg(2);
    }

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

Я обнаружил эту ситуацию в каком-то старом коде Joomla (v1.5), где JSession::set добавил параметр $namespace, но имеет JObject в качестве базового класса, где JObject::set не имеет такого параметра.

Другие вопросы по тегам