Тишина "Декларация... должна быть совместимой" в 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 типа предупреждений с которыми можно было легко справиться. Теперь они просто старые предупреждения. Поскольку я do хочу знать о других предупреждениях, я не могу полностью отключить все предупреждения.

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

  • Я хотел бы избегать любых обманщиков с func_get_args и, насколько это возможно, как можно больше.

  • На самом деле я не хочу перейти на PHP 5.

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

Есть ли чистый и хороший способ выполнить это?

Ответ 1

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 все равно будет нарушен, хотя и без предупреждения, но вы сохраните все проверки типов, которые вы имеете в подклассах.

Ответ 2

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

<?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($_);

Ответ 3

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

//"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() {}
}

Ответ 4

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. Я знаю, что вы сказали, что не хотите этого делать, но в краткосрочной перспективе это облегчит вам жизнь. Честно говоря, я думаю, это может быть вашим лучшим вариантом.

Ответ 5

Я согласен: пример в первом сообщении - плохая практика. Теперь, если у вас есть этот пример:

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"...

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

Ответ 6

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

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

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