Выдача исключения из пользовательского обработчика ошибок в PHP - PullRequest
1 голос
/ 05 марта 2019

Я работал с потоками ресурсов, а точнее с fopen () . Эта функция выдает предупреждение при сбое, в дополнение к возвращению false вместо ресурса. Нежелательные предупреждения являются проблемой, я решил их подавить.

Мне пришло в голову две возможности: использовать оператор подавления ошибок @ или set_error_handler (). И, сказав, что у @ были не очень хорошие показатели и часто создавало больше проблем, чем решало, я провел быстрый тест, чтобы увидеть, как set_error_handler () справился с этим.

А вот и проблемный код:

<?php 

error_reporting(E_ALL);

function errorHandler(int $errorNumber, string $errorMessage)
{
    throw new \Exception();
}

$previousHandler = set_error_handler("errorHandler");

$operations = 10000;

for($i = 0; $i < $operations; $i++) {
    try{
        $inexistant[0];
    } catch (\Exception $e) {}
}

set_error_handler($previousHandler);

echo 'ok';

Выполнение этого простого кода приведет к сбою сервера apache с таким сообщением:

[mpm_winnt:notice] [pid 6000:tid 244] AH00428: Parent: child process 3904 exited with status 3221225725 -- Restarting.

После поиска это сообщение означает, что на сервере произошла ошибка нарушения доступа, главным образом в случае достижения предела размера стека. Это, однако, не должно иметь место, так как этот код не должен увеличивать размер стека (и фактически он не увеличивает фреймы стека PHP).

Я также проверил, важно ли было время, но даже при перерыве в 3 мс между каждой итерацией сбой происходит после более или менее одинакового количества итераций. Это число составляет около 700, но колеблется очень незначительно, иногда работает нормально на 704, а иногда нет.

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

Поскольку я хотел бы правильно распространить сообщение об ошибке, способ с использованием set_error_handler () был бы наиболее разборчивым, но я знаю, что могу использовать error_get_last () и оператор @ для достижения того же самого цель с гораздо большим количеством кода (поскольку в реальном проекте есть несколько функций, таких как fopen (), вызываемых одна за другой).

Итак, вот вопросы: это ошибка PHP? Есть ли способ обойти эту проблему, сохраняя чистый код?

Спасибо.

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

Редактировать: я забыл поставить версии, с которыми я тестировал:

  • Windows 7, Apache 2.4.38, PHP 7.3.2 через XAMPP
  • Windows 7, Apache 2.4.29, PHP 7.2.2 через XAMPP
  • Ubuntu Server 18.04, Apache / 2.4.29 (Ubuntu), PHP 7.2.15-0ubuntu0.18.04.1

Ответы [ 2 ]

0 голосов
/ 06 марта 2019

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

Во-первых, вот сообщение об ошибке: https://bugs.php.net/bug.php?id=77693.

Это переполнение стека, вызвавшее захват контекста исключения, который в этом случае включает функцию, вызывающую обработчик ошибок, так как он в своем поведении захватывает родительский контекст. Этот родительский контекст включает исключение previoulsy перехватывается, которое добавляется в контекст нового перехватываемого исключения и повторяется ad vitam aeternam до сбоя.

Поскольку причина четко определена, решение простое: просто добавьте unset () в конец блока catch, например:

$operations = 10000;

for($i = 0; $i < $operations; $i++) {
    try{
        $inexistant[0];
    } catch (\Exception $e) {
        unset($e);
    }
}

Тогда проблем больше нет.

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

function throwLastError() {
    $context = error_get_last();
    error_clear_last();
    throw new ErrorException($context["message"],
                             0,
                             $context["type"],
                             $context["file"],
                             $context["line"]);
}

// Wrong call to fopen
if (!@fopen("", "a"))
    throwLastError();

Оба ответа имеют примерно одинаковую производительность, метод @ работает на 10% медленнее, когда все параметры ошибок используются в обоих.

0 голосов
/ 05 марта 2019

Зачем переживать все эти хлопоты? Было бы намного лучше просто обработать возвращаемое значение false, так как состояние документа: Returns a file pointer resource on success, or FALSE on error. Поэтому должно быть достаточно просто проверить, является ли возвращаемое значение fopen ложным, а затем продолжить работу на основе этого (или, если необходимо, выдать свою ошибку).

...