适用于初学者的PHP。 错误处理

图片

只有不做任何事的人不会犯错,我们就是一个例子-我们坐着不倦地工作,请阅读《哈伯书》 :)

在本文中,我将介绍有关PHP错误以及如何抑制错误的故事。

失误


错误系列中的各种


在驯服错误之前,我建议研究每个物种,并分别注意最聪明的代表。

为了防止单个错误被忽略,您需要使用error_reporting()函数并使用display_errors伪指令启用对所有错误的显示,以跟踪所有错误:

<?php error_reporting(E_ALL); ini_set('display_errors', 1); 

致命错误


最可怕的错误类型是致命的,它们可能在编译期间以及在解析器或PHP脚本的工作期间发生,而脚本被中断。

E_PARSE

当您犯了严重的语法错误,并且PHP解释器无法理解您想要的内容时,例如,如果您没有关闭花括号或括号,则会出现此错误:

 <?php /** * Parse error: syntax error, unexpected end of file */ { 

或者他们用一种难以理解的语言写着:

 <?php /** * Parse error: syntax error, unexpected '...' (T_STRING) */     

也会出现多余的括号,但不是很重要的圆形或卷曲:

 <?php /** * Parse error: syntax error, unexpected '}' */ } 

我注意到一个要点-将不会执行解析错误所在的文件的代码,因此,如果您尝试在发生解析器错误的同一文件中打开错误显示,则将无法正常工作:

 <?php //     error_reporting(E_ALL); ini_set('display_errors', 1); // ..     

E_ERROR

当PHP理解了您想要的内容后,就会出现此错误,但是由于多种原因,该错误无法解决。 此错误还会中断脚本的执行,并且代码将在错误出现之前起作用:

找不到插件:

 /** * Fatal error: require_once(): Failed opening required 'not-exists.php' * (include_path='.:/usr/share/php:/usr/share/pear') */ require_once 'not-exists.php'; 

引发了异常(什么样的野兽,我稍后再告诉您),但未处理:

 /** * Fatal error: Uncaught exception 'Exception' */ throw new Exception(); 

尝试调用不存在的类方法时:

 /** * Fatal error: Call to undefined method stdClass::notExists() */ $stdClass = new stdClass(); $stdClass->notExists(); 

缺少可用内存(超过memory_limit指令中规定的数量)或其他类似内容:

 /** * Fatal Error: Allowed Memory Size */ $arr = array(); while (true) { $arr[] = str_pad(' ', 1024); } 

读取或下载大文件时非常常见,因此请注意内存消耗问题
递归函数调用。 在此示例中,它在第256次迭代时结束,因为它是在xdebug设置中这样编写 (是的,仅当启用xdebug扩展名时,此错误才能以这种形式出现):

 /** * Fatal error: Maximum function nesting level of '256' reached, aborting! */ function deep() { deep(); } deep(); 

不致命


该视图不会中断脚本的执行,但通常是测试人员找到它们。 此类错误对新手开发人员造成了最大的麻烦。

E_WARNING

当您使用include连接文件时,通常会发生这种情况,但它不会出现在服务器上,或者您在输入文件路径时出错:

 /** * Warning: include_once(): Failed opening 'not-exists.php' for inclusion */ include_once 'not-exists.php'; 

如果在调用函数时使用了错误的参数类型,则会发生这种情况:

 /** * Warning: join(): Invalid arguments passed */ join('string', 'string'); 

它们很多,列出所有内容都没有意义...

E_NOTICE

这些是最常见的错误,此外,还有风扇会关闭错误输出并将其整天铆牢。 有许多琐碎的错误。

访问未定义的变量时:

 /** * Notice: Undefined variable: a */ echo $a; 

访问不存在的数组元素时:

 /** * Notice: Undefined index: a */ $b = []; $b['a']; 

当访问不存在的常量时:

 /** * Notice: Use of undefined constant UNKNOWN_CONSTANT - assumed 'UNKNOWN_CONSTANT' */ echo UNKNOWN_CONSTANT; 

不转换数据类型时:

 /** * Notice: Array to string conversion */ echo array(); 

为避免此类错误,请小心,如果IDE告诉您一些信息,请不要忽略它:

PHPStorm中的PHP E_NOTICE

E_STRICT

这些错误将教您正确编写代码,以免感到羞耻,特别是因为IDE立即向您显示这些错误。 例如,如果您将非静态方法称为静态方法,则代码可以正常工作,但是在某种程度上是错误的,并且如果将来更改类方法,则可能会发生严重错误,并且对$this的吸引力出现了:

 /** * Strict standards: Non-static method Strict::test() should not be called statically */ class Strict { public function test() { echo "Test"; } } Strict::test(); 


此类错误与PHP 5.6版本有关,几乎所有错误都已删除。
7场比赛。 在相关的RFC中阅读更多内容。 如果有人知道这些错误仍然存​​在,请在注释中写


E_DEPRECATED

因此,如果您使用过时的功能(即标记为已弃用的功能,并且它们不会在下一个主要版本中使用),PHP将会发誓:

 /** * Deprecated: Function split() is deprecated */ //  ,   PHP 7.0 //    PHP 5.3 split(',', 'a,b'); 

在我的编辑器中,类似的功能将被删除:

PHPStorm中的PHP E_DEPRECATED

自订


代码开发人员自己“繁殖”的这种类型,很长时间以来我都没有看到过,并且不建议您滥用它们:

  • E_USER_ERROR严重错误
  • E_USER_WARNING不是严重错误
  • E_USER_NOTICE不是错误的消息

另外,值得一提的是E_USER_DEPRECATED仍然经常使用此类型来提醒程序员方法或函数已过时,现在是不使用而重写代码的时候了。 trigger_error()函数用于创建此错误和类似错误:

 /** * @deprecated Deprecated since version 1.2, to be removed in 2.0 */ function generateToken() { trigger_error('Function `generateToken` is deprecated, use class `Token` instead', E_USER_DEPRECATED); // ... // code ... // ... } 

既然您已经熟悉了大多数类型的错误,现在该对display_errors指令的操作进行简短的解释了:

  • 如果display_errors = on ,则在出现错误的情况下,浏览器将接收带有错误文本和代码200的html
  • 如果display_errors = off ,则对于致命错误,响应代码将为500,并且不会将结果返回给用户;对于其他错误,该代码将无法正常工作,但不会告诉任何人


驯服


有3个函数可用于处理PHP中的错误:


现在,有关使用set_error_handler()错误处理的一些详细信息,作为该函数的参数,该函数接受将被分配用于处理错误的任务的函数的名称以及将要监视的错误类型 。 错误处理程序也可以是类方法或匿名函数,主要是它采用以下参数列表:

  • $errno第一个参数包含错误类型的整数
  • $errstr第二个参数包含错误消息
  • $errfile可选的第三个参数,包含发生错误的文件的名称
  • $errline可选的第四个参数包含发生错误的行号
  • $errcontext可选的第五个参数包含发生错误的作用域中存在的所有变量的数组

如果处理程序返回true ,那么该错误将被视为已处理并且脚本将继续执行,否则,将调用记录该错误的标准处理程序,并根据其类型继续执行脚本或完成该脚本。 这是一个示例处理程序:

 <?php //    ,  E_NOTICE error_reporting(E_ALL & ~E_NOTICE); ini_set('display_errors', 1); //    function myHandler($level, $message, $file, $line, $context) { //         switch ($level) { case E_WARNING: $type = 'Warning'; break; case E_NOTICE: $type = 'Notice'; break; default; //   E_WARNING   E_NOTICE //      //      PHP return false; } //    echo "<h2>$type: $message</h2>"; echo "<p><strong>File</strong>: $file:$line</p>"; echo "<p><strong>Context</strong>: $". join(', $', array_keys($context))."</p>"; // ,    ,      return true; } //   ,         set_error_handler('myHandler', E_ALL); 

尽管我非常想为每种类型的错误注册我自己的处理程序,但是您将不能分配多个函数来处理错误,但是不能-编写一个处理程序,并直接在其中描述每种类型的整个显示逻辑
上面编写的处理程序存在一个重要问题-它不会捕获致命错误,并且由于此类错误,用户只能看到空白页,甚至更糟的是错误消息,而不是站点。 为了防止出现这种情况,您应该使用register_shutdown_function ()函数,并使用它来注册一个始终在脚本末尾执行的函数:

 function shutdown() { echo '    '; } register_shutdown_function('shutdown'); 

此功能将始终有效!

但是回到错误,要跟踪错误代码中错误的出现,我们使用error_get_last()函数,借助它的帮助,您可以获得有关最后检测到的错误的信息,并且由于致命错误会中断代码的执行,因此它们始终会扮演“最后”的角色:

 function shutdown() { $error = error_get_last(); if ( //       is_array($error) && //       in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR]) ) { //    (       ) while (ob_get_level()) { ob_end_clean(); } //    echo "    ,  "; } } register_shutdown_function('shutdown'); 

我想提请注意一个事实,即该代码甚至发生在错误处理中,您甚至可能遇到过,但是自PHP的第7版以来,它就失去了相关性。 我将在稍后讲述替换的内容。
工作任务
在发生错误的文件的源代码输出中补充致命错误处理程序,并在输出代码中添加语法高亮显示。

关于暴食


让我们做一个简单的测试,找出最琐碎的错误消耗了多少宝贵的资源:

 /** *      */ //     $time= microtime(true); define('AAA', 'AAA'); $arr = []; for ($i = 0; $i < 10000; $i++) { $arr[AAA] = $i; } printf('%f seconds <br/>', microtime(true) - $time); 

运行此脚本的结果是,我得到了以下结果:

 0.002867 seconds 

现在在循环中添加错误:

 /** *     */ //     $time= microtime(true); $arr = []; for ($i = 0; $i < 10000; $i++) { $arr[BBB] = $i; //   ,      } printf('%f seconds <br/>', microtime(true) - $time); 

预期结果会更糟,并达到一个数量级(甚至两个数量级!):

 0.263645 seconds 

结论很明显-代码中的错误导致脚本过于繁琐-因此请在应用程序开发和测试过程中打开所有错误的显示!
测试是在各种版本的PHP上进行的,各地的差异都是数十倍,因此,这是修复代码中所有错误的另一个原因。

那只狗埋在哪里


PHP有一个特殊的符号“ @”-一个错误抑制运算符,用于不编写错误处理,而是在这种情况下依赖于PHP的正确行为:

 <?php echo @UNKNOWN_CONSTANT; 

在这种情况下,仍将调用set_error_handler()指定的错误处理程序,并且可以通过在处理程序中调用error_reporting()函数来跟踪对错误进行抑制的事实,在这种情况下它将返回0
如果您以这种方式抑制错误,则与仅隐藏它们相比(参见上面的比较测试),这将减少处理器的负载,但是无论如何, 抑制错误是有害的
工作任务
检查使用@错误抑制如何影响前面的循环示例。

例外情况


在PHP4时代,没有例外,一切都变得更加复杂,开发人员竭尽所能地与错误搏斗,这不是一场为了生而死的战斗...您可以在Exceptional Code一文中深入探讨这个令人着迷的对峙故事 第一部分 我现在应该阅读吗? 我无法给出确切的答案,我只想指出,这将帮助您理解语言的发展,并揭示异常的所有魅力。
异常是PHP中的异常事件,与错误不同,它们不仅指出问题,而且要求程序员采取其他措施来处理每种特定情况。

例如,如果出现错误(没有写访问权,没有磁盘空间),生成了相应类型的异常并在异常处理程序中做出了决定,则脚本应将一些数据保存到缓存文件中-保存到其他位置或通知用户有关问题。

异常是Exception类或它的许多后代之一的对象,它包含错误文本,状态,并且还可能包含指向另一个已成为其根本原因的异常的链接。 PHP中的异常模型与其他编程语言中使用的异常模型相似。 可以使用throw运算符引发异常(如他们所说的“ throw”),并且您可以捕捉(“ catch”) catch语句。 引发异常的代码必须由try块包围,以便捕获异常。 每个try块必须至少具有一个匹配的catchfinally块:

 try { //      if (random_int(0, 1)) { throw new Exception("One"); } echo "Zero" } catch (Exception $e) { //      echo $e->getMessage(); } 

在这种情况下,值得使用异常:

  • 如果在一个方法/函数的框架内有多个操作可能会失败
  • 如果您的框架或库声明其使用

为了说明第一种情况,让我们以一个已经有人表达过的将数据写入文件的功能示例为例-许多因素可能会阻止我们,并且为了在上面的代码中明确说明问题出在哪里,您需要创建并抛出异常:

 $directory = __DIR__ . DIRECTORY_SEPARATOR . 'logs'; //     if (!is_dir($directory)) { throw new Exception('Directory `logs` is not exists'); } //         if (!is_writable($directory)) { throw new Exception('Directory `logs` is not writable'); } //  -   ,      if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Ym-d') . '.log', 'a+')) { throw new Exception('System can\'t create log file'); } fputs($file, date("[H:i:s]") . " done\n"); fclose($file); 

因此,我们将捕获以下异常:

 try { //      // ... } catch (Exception $e) { //    echo " : ". $e->getMessage(); } 

这个例子展示了一个非常简单的异常处理场景,当我们以一种方式处理任何异常时。 但是通常,各种异常需要不同的处理方式,然后应使用异常代码并在应用程序中设置异常的层次结构:

 //    class FileSystemException extends Exception {} //     class DirectoryException extends FileSystemException { //   const DIRECTORY_NOT_EXISTS = 1; const DIRECTORY_NOT_WRITABLE = 2; } //     class FileException extends FileSystemException {} 

现在,如果使用这些异常,则可以获取以下代码:

 try { //      if (!is_dir($directory)) { throw new DirectoryException('Directory `logs` is not exists', DirectoryException::DIRECTORY_NOT_EXISTS); } if (!is_writable($directory)) { throw new DirectoryException('Directory `logs` is not writable', DirectoryException::DIRECTORY_NOT_WRITABLE); } if (!$file = @fopen($directory . DIRECTORY_SEPARATOR . date('Ym-d') . '.log', 'a+')) { throw new FileException('System can\'t open log file'); } fputs($file, date("[H:i:s]") . " done\n"); fclose($file); } catch (DirectoryException $e) { echo "   : ". $e->getMessage(); } catch (FileException $e) { echo "   : ". $e->getMessage(); } catch (FileSystemException $e) { echo "  : ". $e->getMessage(); } catch (Exception $e) { echo " : ". $e->getMessage(); } 

重要的是要记住,异常主要是一个例外事件,换句话说就是规则的异常。 您不需要使用它们来处理明显的错误,例如,验证用户输入(尽管这不是那么简单)。 在这种情况下,应将异常处理程序写在能够处理异常处理程序的位置。 例如,用于处理由于文件不可访问而导致的异常的处理程序应该位于负责选择文件的方法或调用该文件的方法中,以便它可以选择另一个文件或另一个目录。

那么,如果您没有捕获到异常,将会发生什么? 您将收到“致命错误:未捕获的异常...”。 不愉快

为了避免这种情况,您应该使用set_exception_handler()函数,并为抛出在try-catch块之外并且尚未处理的异常设置处理程序。 调用此类处理程序后,脚本执行将停止:

 //     //     set_exception_handler(function($exception) { /** @var Exception $exception */ echo $exception->getMessage(), "<br/>\n"; echo $exception->getFile(), ':', $exception->getLine(), "<br/>\n"; echo $exception->getTraceAsString(), "<br/>\n"; }); 

我还将向您介绍使用finally块进行的构造-无论是否引发异常,都将执行此块:

 try { //      } catch (Exception $e) { //      //     } finally { // ,       } 

为了理解这给了我们什么,我将给出以下使用finally块的示例:

 try { // -    //     $handler = mysqli_connect('localhost', 'root', '', 'test'); try { //        // ... throw new Exception('DB error'); } catch (Exception $e) { //  ,     //     ,    throw new Exception('Catch exception', 0, $e); } finally { // ,      //      finally mysqli_close($handler); } //     ,       echo "Ok"; } catch (Exception $e) { //  ,    echo $e->getMessage(); echo "<br/>"; //      echo $e->getPrevious()->getMessage(); } 

即 请记住-即使您在catch块上方抛出异常,也会执行finally块(实际上,这就是他的意图)。

对于恰到好处的入门信息文章,谁渴望获得更多详细信息,您可以在文章Exceptional code ;)中找到它们。

工作任务
编写您的异常处理程序,并在其中显示发生错误的文件的文本输出,并使用语法突出显示所有这些内容,同时不要忘记以可读的形式显示跟踪。 作为参考,请看它在whoops上有多酷。

PHP7-一切都不尽如人意


因此,现在您已经了解了上面的所有信息,现在我将向您介绍PHP7中的创新,即我将讨论在现代PHP项目中工作时遇到的问题。之前,我告诉过您,并举例说明了您需要构建哪个拐杖才能捕获关键错误,因此-在PHP7中,他们决定修复它,但是?和往常一样?绑定到代码的向后兼容性,并获得了虽然是通用的解决方案,但远非理想。现在关于更改的要点:

  1. 当类型的E_ERROR致命错误或致命错误发生并可能处理E_RECOVERABLE_ERRORPHP时引发异常
  2. 这些异常不会继承Exception(请记住,我谈到过向后兼容性,这全都是出于她的缘故)
  3. 这些异常继承Error
  4. Exception和Error类均实现Throwable接口
  5. 您不能在代码中实现throwable接口

该界面Throwable几乎完全重复了我们Exception

 interface Throwable { public function getMessage(): string; public function getCode(): int; public function getFile(): string; public function getLine(): int; public function getTrace(): array; public function getTraceAsString(): string; public function getPrevious(): Throwable; public function __toString(): string; } 

有困难吗?现在以示例为例,以较高和稍作现代化的示例为例:

 try { // ,     include 'e_parse_include.php'; } catch (Error $e) { var_dump($e); } 

结果,我们发现了错误并打印:

 object(ParseError)#1 (7) { ["message":protected] => string(48) "syntax error, unexpected '' (T_STRING)" ["string":"Error":private] => string(0) "" ["code":protected] => int(0) ["file":protected] => string(49) "/www/education/error/e_parse_include.php" ["line":protected] => int(4) ["trace":"Error":private] => array(0) { } ["previous":"Error":private] => NULL } 

如您所见,他们捕获了ParseError异常,该异常ErrorThrowable在Jack所建房屋中实现接口的异常的后继者还有许多其他例外,但我不会折磨-为了清楚起见,我将给出例外的层次结构:

 interface Throwable |- Exception implements Throwable | |- ErrorException extends Exception | |- ... extends Exception | `- ... extends Exception `- Error implements Throwable |- TypeError extends Error |- ParseError extends Error |- ArithmeticError extends Error | `- DivisionByZeroError extends ArithmeticError `- AssertionError extends Error 

还有更多细节:

TypeError-当函数参数的类型与传入的类型不匹配时发生错误:

 try { (function(int $one, int $two) { return; })('one', 'two'); } catch (TypeError $e) { echo $e->getMessage(); } 

ArithmeticError-可能在数学运算期间发生,例如,当计算结果超过为整数分配的限制时:

 try { 1 << -1; } catch (ArithmeticError $e) { echo $e->getMessage(); } 

DivisionByZeroError-除以零错误:

 try { 1 / 0; } catch (ArithmeticError $e) { echo $e->getMessage(); } 

AssertionError-assert()中指定的条件不满足时出现的罕见野兽

 ini_set('zend.assertions', 1); ini_set('assert.exception', 1); try { assert(1 === 0); } catch (AssertionError $e) { echo $e->getMessage(); } 

在设置生产服务器指令zend.assertionsassert.exception切断,这是正确的
您可以在官方手册中找到与SPL例外相同层次的预定义例外的完整列表

工作任务
为PHP7编写一个通用错误处理程序,它将捕获所有可能的异常。

在编写本节时,使用了PHP 7中的Throwable Exceptions and Errors中的材料

均匀度


-有错误,异常,但是所有这些都可以以某种方式提升到堆上吗?

是的,这很容易,我们只有set_error_handler()一个人,没有人会禁止我们在此处理程序中抛出异常:

 //     function errorHandler($severity, $message, $file = null, $line = null) { //  ,       @ if (error_reporting() === 0) { return false; } throw new \ErrorException($message, 0, $severity, $file, $line); } //   -  set_error_handler('errorHandler', E_ALL); 

但是PHP7的这种方法是多余的,现在它可以处理所有事情Throwable

 try { /** ... **/ } catch (\Throwable $e) { //      echo $e->getMessage(); } 

侦错


有时,要调试代码,您需要在特定阶段跟踪变量或对象发生了什么,为此,有一个函数debug_backtrace()debug_print_backtrace()将以相反的顺序返回对函数/方法的调用历史记录:

 <?php function example() { echo '<pre>'; debug_print_backtrace(); echo '</pre>'; } class ExampleClass { public static function method () { example(); } } ExampleClass::method(); 

作为函数执行的结果,debug_print_backtrace()将显示导致我们到达这一点的调用列表:

 #0 example() called at [/www/education/error/backtrace.php:10] #1 ExampleClass::method() called at [/www/education/error/backtrace.php:14] 

您可以使用php_check_syntax()函数或命令来检查代码中的语法错误php -l [ ],但是我还没有看到它们的用法。

断言


我还想谈谈PHP中的assert()这样的奇特野兽实际上,这可以看作是合同编程方法的模仿,然后我将告诉您我从未使用过它的方法:)
该函数assert()在从5.6版过渡到7.0版的过程中改变了其行为,而在7.2版中,所有功能都变得更加强大,因此请仔细阅读changelogs和PHP;)
第一种情况是您需要直接在代码中编写TODO,以免忘记实现给定的功能:

 //  asserts  php.ini // zend.assertions=1 assert(false, "Remove it!"); 

通过执行此代码,我们得到E_WARNING

 Warning: assert(): Remove it! failed 

PHP7可以切换到异常模式,并且不会出现错误,而是会始终显示异常AssertionError

 //    «» ini_set('assert.exception', 1); assert(false, "Remove it!"); 

结果,我们期望出现异常AssertionError

如有必要,可以抛出一个任意异常:

 assert(false, new Exception("Remove it!")); 

我建议使用标签@TODO,现代IDE可以很好地与它们一起使用,并且尽管与它们“打分”的诱惑很大,但您无需花费额外的精力和资源来使用它们。
第二个用例是创建某种TDD,但是请记住,这只是一个相似之处。虽然,如果您努力尝试,可能会得到有趣的结果,这将有助于测试代码:

 // callback-      function backlog($script, $line, $code, $message) { echo $message; } //  callback- assert_options(ASSERT_CALLBACK, 'backlog'); //    assert_options(ASSERT_WARNING, false); //      assert(sqr(4) === 16, 'When I send integer, function should return square of it'); // ,   function sqr($a) { return; //    } 

第三种选择是一种合同编程,当您描述了使用库的规则时,但是您想确保自己被正确理解,并且在这种情况下,请立即告诉开发人员一个错误(我什至不确定我是否正确理解了它,而是一个示例。该代码是相当有效的):

 /** *        * * [ * 'host' => 'localhost', * 'port' => 3306, * 'name' => 'dbname', * 'user' => 'root', * 'pass' => '' * ] * * @param $settings */ function setupDb ($settings) { //   assert(isset($settings['host']), 'Db `host` is required'); assert(isset($settings['port']) && is_int($settings['port']), 'Db `port` is required, should be integer'); assert(isset($settings['name']), 'Db `name` is required, should be integer'); //    // ... } setupDb(['host' => 'localhost']); 


如果您对合同感兴趣,那么专门为您提供PhpDeal框架的链接


切勿使用它assert()来检查输入参数,因为实际上它会assert()解释第一个参数(表现为eval()),并且这会引起PHP注入。是的,这是正确的行为,因为如果您禁用断言,那么所有传递的参数都将被忽略,并且如果您按照上述示例中的操作进行操作,则将执行代码,并且执行的布尔结果将在禁用的断言中传递。哦,它在PHP 7.2中发生了变化:)


如果您有使用的现场经验assert()-与我分享,我将不胜感激。是的,这是关于该主题的另一篇有趣的读物-PHP断言,末尾有相同的问题:)

总结


我将为您写这篇文章的结论:

  • 消除错误-它们不应出现在您的代码中
  • 使用例外-与他们一起工作需要妥善组织,并且会有快乐
  • 断言-很好地了解了它们

聚苯乙烯


转贴了一系列的文章«PHP初学者“的:


如果您对文章的材料或形式有意见,请在评论中描述要点,我们将使材料变得更好。

感谢Maxim Slesarenko撰写本文的帮助。

Source: https://habr.com/ru/post/zh-CN440744/


All Articles