`trigger_error` против` throw Exception` в контексте магических методов PHP

13

У меня спор с коллегой по поводу правильного использования (если есть) trigger_errorв контексте магических методов . Во-первых, я думаю, что этого trigger_errorследует избегать, за исключением одного этого случая.

Скажем, у нас есть класс с одним методом foo()

class A {
    public function foo() {
        echo 'bar';
    }
}

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

class B {
    public function __call($method, $args) {
        switch (strtolower($method)) {
        case 'foo':
            echo 'bar';
            break;
        }
    }
}

$a = new A;
$b = new B;

$a->foo(); //bar
$b->foo(); //bar

Оба класса одинаковы в том, как они отвечают, foo()но различаются при вызове недопустимого метода.

$a->doesntexist(); //Error
$b->doesntexist(); //Does nothing

Мой аргумент заключается в том, что магические методы должны вызывать trigger_errorпри обнаружении неизвестного метода

class B {
    public function __call($method, $args) {
        switch (strtolower($method)) {
        case 'foo':
            echo 'bar';
            break;
        default:
            $class = get_class($this);
            $trace = debug_backtrace();
            $file = $trace[0]['file'];
            $line = $trace[0]['line'];
            trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);
            break;
        }
    }
}

Так что оба класса ведут себя (почти) одинаково

$a->badMethod(); //Call to undefined method A::badMethod() in [..] on line 28
$b->badMethod(); //Call to undefined method B::badMethod() in [..] on line 32

Мой вариант использования - реализация ActiveRecord. Я использую, __callчтобы поймать и обработать методы, которые по сути делают то же самое, но имеют модификаторы, такие как Distinctили Ignore, например,

selectDistinct()
selectDistinctColumn($column, ..)
selectAll()
selectOne()
select()

или

insert()
replace()
insertIgnore()
replaceIgnore()

Такие методы , как where(), from(), groupBy()и т.д. жестко закодированы.

Мой аргумент высвечивается, когда вы случайно звоните insret(). Если моя реализация активной записи жестко закодирует все методы, это будет ошибкой.

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

chriso
источник

Ответы:

7

Возьмите две реализации одного и того же интерфейса ActiveRecord ( select(), where()и т.д.)

class ActiveRecord1 {
    //Hardcodes all methods
}

class ActiveRecord2 {
    //Uses __call to handle some methods, hardcodes the rest
}

Если вы вызываете недопустимый метод в первом классе, например ActiveRecord1::insret(), поведение PHP по умолчанию вызывает ошибку . Недопустимый вызов функции / метода не является условием, которое разумное приложение хотело бы перехватить и обработать. Конечно, вы можете поймать его на таких языках, как Ruby или Python, где ошибка является исключением, но другие (JavaScript / любой статический язык / другое?) Потерпят неудачу.

Вернемся к PHP - если оба класса реализуют один и тот же интерфейс, почему они не должны демонстрировать одинаковое поведение?

Если __callили __callStaticобнаружен неправильный метод, они должны вызвать ошибку, чтобы имитировать поведение языка по умолчанию

$class = get_class($this);
$trace = debug_backtrace();
$file = $trace[0]['file'];
$line = $trace[0]['line'];
trigger_error("Call to undefined method $class::$method() in $file on line $line", E_USER_ERROR);

Я не спорю, следует ли использовать ошибки над исключениями (они не должны использоваться на 100%), однако я считаю, что магические методы PHP являются исключением - каламбуром :) к этому правилу в контексте языка

chriso
источник
1
Извините, но я не покупаю это. Почему мы должны быть овцами, потому что исключения не существовали в PHP более десяти лет назад, когда классы были впервые реализованы в PHP 4.something?
Мэтью Шарли
@ Матфея за последовательность. Я не спорю об исключениях и ошибках (нет споров) или о том, что у PHP неудача в дизайне (нет), я утверждаю, что в этом уникальном случае для согласованности лучше всего подражать поведению языка
Крис
Последовательность не всегда хорошая вещь. Последовательный, но плохой дизайн - все еще плохой дизайн, который трудно использовать в конце дня.
Мэтью Шарли
@ Правда, но IMO, вызывающая ошибку при неверном вызове метода, не плохой дизайн 1) во многих (большинстве?) Языках он встроен, и 2) я не могу вспомнить ни одного случая, когда бы вы хотели чтобы поймать недопустимый вызов метода и обрабатывать его?
Крис
@ Chriso: Вселенная достаточно велика, и есть случаи, когда ни вы, ни я никогда не мечтали бы о том, чтобы это произошло. Вы используете __call()для динамической маршрутизации, действительно ли это так неразумно ожидать, что где-то на пути, кто-то может захотеть обработать случай, когда это не удается? Во всяком случае, это происходит по кругу, так что это будет мой последний комментарий. Делайте что хотите, в конце дня это сводится к суждению: Лучшая поддержка против последовательности. Оба метода приведут к одинаковому воздействию на приложение в случае отсутствия специальной обработки.
Мэтью Шарли
3

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

Преимущества исключений:

  • Их можно поймать. Это огромное преимущество, и оно должно быть единственным, которое вам нужно. Люди могут попробовать что-то другое, если они ожидают, что что-то может пойти не так. Ошибки не дают вам этой возможности. Даже установка собственного обработчика ошибок не удерживает свечу от простого перехвата исключения. Что касается ваших комментариев на ваш вопрос, то, что «разумное» приложение полностью зависит от контекста приложения. Люди не могут поймать исключения, если они думают, что это никогда не произойдет в их случае. Не давать людям выбор - Плохая вещь ™.
  • Следы стека. Если что-то идет не так, вы знаете, где и в каком контексте возникла проблема. Вы когда-нибудь пытались отследить, откуда исходит ошибка от некоторых основных методов? Если вы вызываете функцию со слишком малым количеством параметров, вы получаете бесполезную ошибку, которая выделяет начало метода, который вы вызываете, и полностью исключает, откуда он вызывается.
  • Ясность. Объедините два вышеупомянутых кода, и вы получите более понятный код. Если вы пытаетесь использовать собственный обработчик ошибок для обработки ошибок (например, для генерации трассировки стека для ошибок), вся ваша обработка ошибок находится в одной функции, а не там, где ошибки на самом деле генерируются.

Решение ваших проблем, вызов метода, который не существует, может быть допустимой возможностью . Это полностью зависит от контекста кода, который вы пишете, но в некоторых случаях это может произойти. С учетом вашего конкретного случая использования некоторые серверы баз данных могут предоставлять некоторые функции, которые другие не могут. Использование try/ catchи исключений в __call()сравнении с функцией для проверки возможностей - это совершенно другой аргумент.

Единственный вариант использования, который я могу придумать для использования, trigger_error- для E_USER_WARNINGили ниже. Запуск, E_USER_ERRORхотя это всегда ошибка на мой взгляд.

Мэтью Шарли
источник
Спасибо за ответ :) - 1) Я согласен с тем, что исключения почти всегда должны использоваться при ошибках - все ваши аргументы являются действительными точками. Однако .. Я думаю, что в этом контексте ваш аргумент не удаётся ..
Крис
2
« Вызов метода, который не существует, может быть допустимой возможностью », - я совершенно не согласен. На каждом языке (который я когда-либо использовал) вызов функции / метода, который не существует, приведет к ошибке. Это не условие, которое должно быть поймано и обработано. Статические языки не позволят вам скомпилировать с неверным вызовом метода, а динамические языки потерпят неудачу, как только они достигнут вызова.
Крис
Если вы прочитаете мое второе редактирование, вы увидите мой вариант использования. Допустим, у вас есть две реализации одного класса, одна с использованием __call, а другая с жестко закодированными методами. Не обращая внимания на детали реализации, почему оба класса должны вести себя по-разному, когда они реализуют один и тот же интерфейс? PHP вызовет ошибку, если вы вызовете неверный метод с классом, который имеет жестко закодированные методы. Использование trigger_errorв контексте __call или __callStatic имитирует поведение языка по умолчанию
chriso
@chriso: динамические языки, не являющиеся PHP, не будут работать, за исключением того, что их можно поймать . Например, Ruby выдает, NoMethodErrorкоторый вы можете поймать, если захотите. По моему личному мнению, ошибки - это огромная ошибка в PHP. То, что ядро ​​использует неработающий метод для сообщения об ошибках, не означает, что ваш собственный код должен это делать.
Мэтью Шарли
@ chriso Мне нравится верить, что единственная причина, по которой ядро ​​все еще использует ошибки, заключается в обратной совместимости. Надеемся, что PHP6 станет еще одним шагом вперед, как PHP5, и вообще избавится от ошибок. В конце дня и ошибки, и исключения приводят к одному и тому же результату: немедленное прекращение выполнения кода. За исключением вы можете диагностировать, почему и где гораздо проще, хотя.
Мэтью Шарли
3

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

function errorToExceptionHandler($errNo, $errStr, $errFile, $errLine, $errContext)
{
if (error_reporting() == 0) return;
throw new ErrorException($errStr, 0, $errNo, $errFile, $errLine);
}
set_error_handler('errorToExceptionHandler');

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

Wayne
источник
1
Если вы используете это, вам нужно проверить тип ошибки, чтобы вы не превратили E_NOTICEs в исключения. Это было бы плохо.
Мэтью Шарли
1
Нет, превращать E_NOTICES в исключения - это хорошо! Все уведомления должны рассматриваться как ошибки; Это хорошая практика, независимо от того, конвертируете ли вы их в исключения или нет.
Уэйн
2
Обычно я соглашусь с вами, но как только вы начнете использовать какой-либо сторонний код, это быстро может обернуться лицом к лицу.
Мэтью Шарли
Почти все сторонние библиотеки E_STRICT | E_ALL безопасно. Если я использую код, которого нет, я пойду и исправлю его. Я работал так годами без проблем.
Уэйн
Вы, очевидно, раньше не использовали Dual. Ядро действительно такое хорошее, но многие, многие сторонние модули - нет
Мэтью Шарли,
0

ИМО, это вполне допустимый вариант использования для trigger_error:

function handleError($errno, $errstring, $errfile, $errline, $errcontext) {
    if (error_reporting() & $errno) {
        // only process when included in error_reporting
        return handleException(new \Exception($errstring, $errno));
    }
    return true;
}

function handleException($exception){
    // Here, you do whatever you want with the generated
    // exceptions. You can store them in a file or database,
    // output them in a debug section of your page or do
    // pretty much anything else with it, as if it's a
    // normal variable

    switch ($code) {
        case E_ERROR:
        case E_CORE_ERROR:
        case E_USER_ERROR:
            // Make sure script exits here
            exit(1);
        default:
            // Let script continue
            return true;
    }
}

// Set error handler to your custom handler
set_error_handler('handleError');
// Set exception handler to your custom handler
set_exception_handler('handleException');


// ---------------------------------- //

// Generate warning
trigger_error('This went wrong, but we can continue', E_USER_WARNING);

// Generate fatal error :
trigger_error('This went horrible wrong', E_USER_ERROR);

Используя эту стратегию, вы получаете $errcontextпараметр, если делаете это $exception->getTrace()внутри функции handleException. Это очень полезно для определенных целей отладки.

К сожалению, это работает, только если вы используете trigger_errorнапрямую из своего контекста, что означает, что вы не можете использовать функцию / метод-обертку для псевдонима trigger_errorфункции (поэтому вы не можете сделать что-то подобное, function debug($code, $message) { return trigger_error($message, $code); }если хотите, чтобы данные контекста были в вашей трассировке).

Я искал лучшую альтернативу, но пока не нашел.

Джон Слегерс
источник