Молчание предупреждений "Декларация... должна быть совместима" в 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, следуя этим примерам.
Некоторые случаи довольно просты. Если в подклассе отсутствует аргумент по умолчанию, просто добавьте его и продолжайте. Например, в этом случае:
Declaration of B::foo() should be compatible with A::foo($bar = null)
Вы бы сделали:
- public function foo() + public function foo($bar = null)
Если в подкласс добавлены дополнительные ограничения, удалите их из определения, перемещаясь внутри тела функции.
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 */
Если у вашего подкласса меньше аргументов, чем у суперкласса, и вы можете сделать их необязательными в суперклассе, просто добавьте заполнители в подкласс. Заданная строка ошибки:
Declaration of B::foo($param = '') should be compatible with A::foo($x = 40, $y = '')
Вы бы сделали:
- public function foo($param = '') + public function foo($param = '', $_ = null)
Если вы видите какие-то аргументы, требуемые в подклассе, возьмите дело в свои руки.
- protected function foo($bar) + protected function foo($bar = null) { + if (empty($bar['key'])) { + throw new Exception("Invalid argument"); + }
Иногда может быть проще изменить метод суперкласса, чтобы полностью исключить необязательный аргумент, возвращаясь к
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
Конечно, это может стать очень утомительным, если вам нужно удалить более одного аргумента.
Все становится намного интереснее, если у вас есть серьезные нарушения принципа замещения. Если у вас нет набранных аргументов, то это легко. Просто сделайте все дополнительные аргументы необязательными, а затем проверьте их наличие. Данная ошибка:
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()
,Если у вас есть целые иерархии классов с расходящимся интерфейсом, возможно, будет проще расходиться еще дальше. Переименуйте функцию с конфликтующим определением в каждом классе. Затем добавьте прокси-функцию в одном родительском посреднике для этих классов:
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"...
Правильно ли я думаю, что это предупреждение неправильно в этом конкретном примере, пожалуйста?
У меня тоже была эта проблема. У меня есть класс, который переопределяет функцию родительского класса, но переопределение имеет различное количество параметров. Я могу придумать несколько простых способов обхода, но они требуют незначительного изменения кода.
- изменить имя функции в подклассе (чтобы оно больше не перекрывало родительскую функцию)
измените параметры родительской функции, но сделайте дополнительные параметры необязательными (например, функция func($var1, $var2=null) - это может быть самым простым и требующим меньшего количества изменений кода. Но это может не стоить изменять в родитель, если он использовал так много других мест. Так что я пошел с #1 в моем случае.
Если это возможно, вместо передачи дополнительных параметров в функции подкласса используйте 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 не имеет такого параметра.