Home > Article > Backend Development > Best Practices for Exceptions in PHP 5.3_PHP Tutorial
Every new feature added to the PHP runtime creates an exponential random number, in this way developers can use and even abuse this new feature. However, it wasn't until some good and bad use cases emerged that developers came to a consensus. As these new cases emerge, we can finally discern what is the best or worst approach.
Exception handling is indeed not a new feature in PHP by any means. But in this article, we will discuss two new features based on exception handling in PHP 5.3. The first is nested exceptions and the second is a set of new exception types that are extended by SPL (a core extension of the current PHP running mechanism). For these two new features, best practices can be found in this book and are worthy of your detailed study.
Special note: some of these features already exist in PHP versions earlier than 5.3, or at least can be implemented in versions earlier than 5.3. When this article mentions PHP 5.3, it is not strictly responsible. In the sense of PHP runtime version. Instead, it means that the code base and project are using PHP 5.3 as the minimum version, but also all the best practices that emerge in the new development stage. This development stage highlights the specific "2.0" attempts by several projects like Zend Framework, Symfony, Doctrine and PEAR.
Background
PHP 5.2 has only one exception class, Exception. According to Zend Framework/PEAR development standards, this class is the base class for all exception classes in your library. If you create a library named MyCompany, according to Zend Framework / PEAR standards, all code files in the library will start with MyCompany_. If you want to create your own exception base class for the library: MyCompany_Exception, then use this class to inherit Exception, and then the component (component) inherits and throws this exception class. For example, if you have a component MyCompany_Foo, you can create an exception base class MyCompany_Foo_Exception for use inside the component. These exceptions can be caught by code that catches MyCompany_Foo_Exception, MyCompany_Exception or Exception. For other code in the library that uses this component, this is a three-level exception (or more, depending on how many subclasses MyCompany_Foo_Exception has), and they can handle these exceptions as they see fit.
In php5, the basic exception class already supports nesting features. What is nesting? Nesting is the ability to catch a specific exception, or a new exception object created with reference to the original exception. This will allow the caller attribute to be reflected on the two exception classes that appear in the overhead library of the more public types, and of course on the exception class with the original exception behavior.
Why are these features useful? Often it's most efficient code to throw exceptions of your own type by using other code. This code may be code from a third-party code library that provides some more adaptable functions encapsulated using the adapter pattern, or simple code that utilizes some PHP extensions to throw exceptions.
For example, in the component Zend_Db, it uses the adapter pattern to encapsulate specific PHP extensions to create a database abstraction layer. In an adapter, Zend_Db encapsulates PDO, and PDO will throw its own exception PDOException, Zend_Db needs to catch these PDO-specific exceptions and have them re-thrown as a predictable and known type Zend_Db_Exception. This gives the developer the guarantee that Zend_Db will always throw exceptions of type Zend_Db_Exception (and therefore can be caught ), and they can also access the PDOException that was originally thrown when needed.
The following example shows how a fictional database adapter might implement embedded exceptions:
<ol class="dp-c"><li class="alt"><span><span class="keyword">class</span><span> MyCompany_Database </span></span></li><li><span>{ </span></li><li class="alt"><span> <span class="comment">/**</span> </span></li><li><span><span class="comment"> * @var PDO object setup during construction</span> </span></li><li class="alt"><span><span class="comment"> */</span><span> </span></span></li><li><span> <span class="keyword">protected</span><span> </span><span class="vars">$_pdoResource</span><span> = null; </span></span></li><li class="alt"><span> </span></li><li><span> <span class="comment">/**</span> </span></li><li class="alt"><span><span class="comment"> * @throws MyCompany_Database_Exception</span> </span></li><li><span><span class="comment"> * @return int</span> </span></li><li class="alt"><span><span class="comment"> */</span><span> </span></span></li><li><span> <span class="keyword">public</span><span> </span><span class="keyword">function</span><span> executeQuery(</span><span class="vars">$sql</span><span>) </span></span></li><li class="alt"><span> { </span></li><li><span> try { </span></li><li class="alt"><span> <span class="vars">$numRows</span><span> = </span><span class="vars">$this</span><span>->_pdoResource-></span><span class="func">exec</span><span>(</span><span class="vars">$sql</span><span>); </span></span></li><li><span> } catch (PDOException <span class="vars">$e</span><span>) { </span></span></li><li class="alt"><span> <span class="keyword">throw</span><span> </span><span class="keyword">new</span><span> MyCompany_Database_Exception(</span><span class="string">'Query was unexecutable'</span><span>, null, </span><span class="vars">$e</span><span>); </span></span></li><li><span> } </span></li><li class="alt"><span> <span class="keyword">return</span><span> </span><span class="vars">$numRows</span><span>; </span></span></li><li><span> } </span></li><li class="alt"><span> </span></li><li><span>} </span></li></ol>
In order to use embedded exceptions, you have to call the getPrevious() method of the caught exception:
<ol class="dp-c"><li class="alt"><span><span class="comment">// $sql and $connectionParameters assumed</span><span> </span></span></li><li><span>try { </span></li><li class="alt"><span> <span class="vars">$db</span><span> = </span><span class="keyword">new</span><span> MyCompany_Database(</span><span class="string">'PDO'</span><span>, </span><span class="vars">$connectionParams</span><span>); </span></span></li><li><span> <span class="vars">$db</span><span>->executeQuery(</span><span class="vars">$sql</span><span>); </span></span></li><li class="alt"><span>} catch (MyCompany_Database_Exception <span class="vars">$e</span><span>) { </span></span></li><li><span> <span class="func">echo</span><span> </span><span class="string">'General Error: '</span><span> . </span><span class="vars">$e</span><span>->getMessage() . </span><span class="string">"\n"</span><span>; </span></span></li><li class="alt"><span> <span class="vars">$pdoException</span><span> = </span><span class="vars">$e</span><span>->getPrevious(); </span></span></li><li><span> <span class="func">echo</span><span> </span><span class="string">'PDO Specific error: '</span><span> . </span><span class="vars">$pdoException</span><span>->getMessage() . </span><span class="string">"\n"</span><span>; </span></span></li><li class="alt"><span>} </span></li></ol>
Most recently implemented PHP extensions have OO (object-oriented) interfaces. Therefore, these APIs tend to throw exceptions rather than terminate with an error. Extensions in PHP that can throw exceptions include PDO, DOM, Mysqli, Phar, Soap and SQLite.
New feature: New core exception type
In PHP 5.3 development we are showing off some interesting new exception types. These exceptions have existed in PHP 5.2. They are implemented in the SPL extension and are listed in the manual here). Since these new exception types are part of PHP core and part of SPL, they can be used by anyone running code with PHP 5.3 and above. Although it may not seem that important when writing application layer code, using these new exception types becomes more important when we write or use code libraries
那么为什么新异常是普通类型?以前,开发者试图通过在异常消息提醒中放入更多的内容来赋予异常更多的含义。虽然这样做是可行的,但是它有几个缺点。一是你无法捕获基于消息的异常。这可是一个问题,如果你知道一组代码是同样的异常类型与不同的提示消息对应不同异常情况下,处理起来的难度将相当的大。例如,一个认证类,在对$auth->authenticate();;它抛出异常的相同类型的假设是异常),但不同的消息对应两个具体的故障:产生故障原因是认证服务器不能达到但是相同的异常类型却提示失败的验证消息不同。在这种情况下注意,使用异常可能不是处理认证响应最好的方式),这将需要用字符串来解析消息从而处理这两种不同的情况。
这个问题的解决办法显然是通过某种方式对异常进行编码,这样就可以在需要辨别如何对这种异常环境做出反应的时候能够更加容易的查询到。第一个反应库是使用异常基类的$code属性。另一个是通过创建可以被抛出且能描述自身行为的子类或者新的异常类。这两种方法具有相同的明显的缺点。两者都没有呈现出想这样的最好的例子。两者都不被认为是一个标准,因此每个试图复制这两种解决方案的项目都会有小的变化,这就迫使使用这需要回到文档以了解所创建的库中已经有的具体解决方案。现在通过使用SPL的新的类型方法,也称作php标准库;开发者就可以以同样的方式在他们的项目中,并且复用这些项目的新的最佳的方法已经出现。
第二个缺点是使用详细信息的做法使得理解这些异常情况对那些非英语或英语能力有限的开发者来说十分困难。这可能会使的开发者在试图理解异常信息的含义的过程十分的缓慢。许多开发者也会写关于异常的文章,因为还未出现一个统一的整合过的标准所要有同这些开发者数量相同的不同的版本来描述异常消息所描述的情况。
所以我如何去使用它们,就用这些让人无语的密密麻麻的细节描述?
现在在SPL中有总共13个新的异常类型。其中两个可被视为基类:逻辑异常和运行时异常;两种都继承php异常类。其余的方法在逻辑上可以被拆分为3组:动态调用组,逻辑组和运行时组。
动态调用组包含异常 BadFunctionCallException和BadMethodCallException,BadMethodCallException是BadFunctionCallExceptionLogicException的子类)的子类,这意味着这些异常可以被其直接类型译者注:就是异常自身的类型,大家都知道异常有很多种)、LogicException,或者Exception抓到译者注:就是catch)你应该在什么时候使用这些?通常,你应该在由一个无法处理的__call()方法产生的情况,或者回调无法不是一个有效的函数简单说,当某些东西并非is_callable())时使用。
例如:
<ol class="dp-c"><li class="alt"><span><span class="comment">// OO variant</span><span> </span></span></li><li><span><span class="keyword">class</span><span> Foo </span></span></li><li class="alt"><span>{ </span></li><li><span> <span class="keyword">public</span><span> </span><span class="keyword">function</span><span> __call(</span><span class="vars">$method</span><span>, </span><span class="vars">$args</span><span>) </span></span></li><li class="alt"><span> { </span></li><li><span> <span class="keyword">switch</span><span> (</span><span class="vars">$method</span><span>) { </span></span></li><li class="alt"><span> <span class="keyword">case</span><span> </span><span class="string">'doBar'</span><span>: </span><span class="comment">/* ... */</span><span> </span><span class="keyword">break</span><span>; </span></span></li><li><span> <span class="keyword">default</span><span>: </span></span></li><li class="alt"><span> <span class="keyword">throw</span><span> </span><span class="keyword">new</span><span> BadMethodCallException(</span><span class="string">'Method '</span><span> . </span><span class="vars">$method</span><span> . </span><span class="string">' is not callable by this object'</span><span>); </span></span></li><li><span> } </span></li><li class="alt"><span> } </span></li><li><span> </span></li><li class="alt"><span>} </span></li><li><span> </span></li><li class="alt"><span><span class="comment">// procedural variant</span><span> </span></span></li><li><span><span class="keyword">function</span><span> foo(</span><span class="vars">$bar</span><span>, </span><span class="vars">$baz</span><span>) { </span></span></li><li class="alt"><span> <span class="vars">$func</span><span> = </span><span class="string">'do'</span><span> . </span><span class="vars">$baz</span><span>; </span></span></li><li><span> <span class="keyword">if</span><span> (!</span><span class="func">is_callable</span><span>(</span><span class="vars">$func</span><span>)) { </span></span></li><li class="alt"><span> <span class="keyword">throw</span><span> </span><span class="keyword">new</span><span> BadFunctionCallException(</span><span class="string">'Function '</span><span> . </span><span class="vars">$func</span><span> . </span><span class="string">' is not callable'</span><span>); </span></span></li><li><span> } </span></li><li class="alt"><span>} </span></li></ol>
一个直接的例子,在__call时call_user_func()。这组异常在开发各种API动态方法的调用、函数调用时非常有用,例如这是一个可以被SOAP和XML-RPC客户端/服务端能够发送和解释的请求。
第二组是逻辑logic )组。这组由DomainException、InvalidArgumentException、LengthException、OutOfRangeException组成。这些异常也是LogicException的子类,当然也是PHP的Exception的子类。在有状态不定,或者错误的方法/函数的参数时使用这些异常。为了更好地理解这一点,我们先看看最后一组异常
最后一组是运行时runtime )组。它由OutOfBoundsException、OverflowException、RangeException、UnderflowException、UnexpectedValueExceptio组成。这些异常也是RuntimeException的子类,当然也是PHP的Exception的子类。在“运行时”runtime)的函数、方法发生异常时,这些异常运行时组)会被调用
逻辑组和运行时组如何一起工作?如果你看看对象的剖析,通常是发生的是两者之一。首先,对象将跟踪并改变状态。这意味着对象通常是不做任何事情。它可能会传递结构给它,它可能会通过setter和getter设置一些东西译者注:例如$this->foo='foo'),或者,它可能会引用其他对象。第二,当对象不跟踪或改变状态,这代表正在操作——做它该做的事。这是对象的运行时runtime)。例如,在对象的一生中,它可能被创建,设置一些东西,那么它可能会被setFoo($foo),setBar($bar)。在这些时候,任何类型的LogicException应该被提高。此外,当对象内的方法被带参数调用时,例如$object->doSomething($someVariation);在前几行检查$someVariation变量时,可能抛出一个LogicException。完成检查$someVariation后,它继续做它该做的doSomething(),这时被认为是它的“运行时”runtime),在这段代码中,可能抛出RuntimeExcpetions异常。
要理解得更好,我们来看看这个概念在代码中的运用:
<ol class="dp-c"><li class="alt"><span><span class="keyword">class</span><span> Foo </span></span></li><li><span>{ </span></li><li class="alt"><span> <span class="keyword">protected</span><span> </span><span class="vars">$number</span><span> = 0; </span></span></li><li><span> <span class="keyword">protected</span><span> </span><span class="vars">$bar</span><span> = null; </span></span></li><li class="alt"><span> </span></li><li><span> <span class="keyword">public</span><span> </span><span class="keyword">function</span><span> __construct(</span><span class="vars">$options</span><span>) </span></span></li><li class="alt"><span> { </span></li><li><span> <span class="comment">/** 本方法抛出LogicException异常 **/</span><span> </span></span></li><li class="alt"><span> } </span></li><li><span> </span></li><li class="alt"><span> <span class="keyword">public</span><span> </span><span class="keyword">function</span><span> setNumber(</span><span class="vars">$number</span><span>) </span></span></li><li><span> { </span></li><li class="alt"><span> <span class="comment">/** 本方法抛出LogicException异常 **/</span><span> </span></span></li><li><span> } </span></li><li class="alt"><span> </span></li><li><span> <span class="keyword">public</span><span> </span><span class="keyword">function</span><span> setBar(Bar </span><span class="vars">$bar</span><span>) </span></span></li><li class="alt"><span> { </span></li><li><span> <span class="comment">/** 本方法抛出LogicException异常 **/</span><span> </span></span></li><li class="alt"><span> } </span></li><li><span> </span></li><li class="alt"><span> <span class="keyword">public</span><span> </span><span class="keyword">function</span><span> doSomething(</span><span class="vars">$differentNumber</span><span>) </span></span></li><li><span> { </span></li><li class="alt"><span> <span class="keyword">if</span><span> (</span><span class="vars">$differentNumber</span><span> != </span><span class="vars">$expectedCondition</span><span>) { </span></span></li><li><span> <span class="comment">/** 在这里,抛出LogicException异常 **/</span><span> </span></span></li><li class="alt"><span> } </span></li><li><span> </span></li><li class="alt"><span> <span class="comment">/**</span> </span></li><li><span><span class="comment"> * 在这里,本方法抛出RuntimeException异常</span> </span></li><li class="alt"><span><span class="comment"> */</span><span> </span></span></li><li><span> } </span></li><li class="alt"><span> </span></li><li><span>} </span></li></ol>
现在理解了这一概念,那么,对代码库的使用者来说,这是做什么的呢?使用者可以随时确定对象的异常状态,他们可以用异常的具体的类型来捕获(catch)异常,例如InvalidArgumentException或LengthException,至少也是LogicException。通过这种级别的精度调整,和类型的多样,他们可以用LogicException捕获最小的异常,但也可以通过实际的异常类型获得更好的理解。同样的概念也适用于运行时的异常,可以抛出更多的特定类型的异常,并且不论是特定或非特定类型的异常,都可以被捕获catch)。它可以给使用者提供更详细的情况和精确度。
下面是一个关于SPL异常的表,您可能会有兴趣
类库代码中的最佳实践
PHP 5.3 带来了新的异常类型, 同时也带给我们新的最佳实践. 除了将某些特定的异常(如: InvalidArgumentException, RuntimeException)标准化外, 捕捉组件级的异常, 也很重要. 关于这方面, ZF2 wiki 和 PEAR2 wiki 上面有深入的探讨.
简而言之, 除了上面提到的各种最佳实践, 我们还应该用 Marker Interface 来创建一个组件级的异常基类. 通过创建组件级的 Marker Interface, 用在组件内部的异常既能继承 SPL 的异常类型, 也能在运行时被各种代码捕捉. 我们来看下列代码:
<ol class="dp-c"><li class="alt"><span><span class="comment">// usage of bracket syntax for brevity</span><span> </span></span></li><li><span>namespace MyCompany\Component { </span></li><li class="alt"><span> </span></li><li><span> <span class="keyword">interface</span><span> Exception </span></span></li><li class="alt"><span> {} </span></li><li><span> </span></li><li class="alt"><span> <span class="keyword">class</span><span> UnexpectedValueException </span></span></li><li><span> <span class="keyword">extends</span><span> \UnexpectedValueException </span></span></li><li class="alt"><span> <span class="keyword">implements</span><span> Exception </span></span></li><li><span> {} </span></li><li class="alt"><span> </span></li><li><span> <span class="keyword">class</span><span> Component </span></span></li><li class="alt"><span> { </span></li><li><span> <span class="keyword">public</span><span> </span><span class="keyword">static</span><span> </span><span class="keyword">function</span><span> doSomething() </span></span></li><li class="alt"><span> { </span></li><li><span> <span class="keyword">if</span><span> (</span><span class="vars">$somethingExceptionalHappens</span><span>) { </span></span></li><li class="alt"><span> <span class="keyword">throw</span><span> </span><span class="keyword">new</span><span> UnexpectedValueException(</span><span class="string">'Something bad happened'</span><span>); </span></span></li><li><span> } </span></li><li class="alt"><span> } </span></li><li><span> } </span></li><li class="alt"><span> </span></li><li><span>} </span></li></ol>
如果调用上面代码中的 MyCompany\Component\Component::doSomething() 函数, doSomething() 抛出的异常可以当作下列异常类型捕捉: PHP 的 Exception, SPL 的 UnexpectedValueException, SPL 的 RuntimeException, 该组件的MyCompany\Component\UnexpectedValueException, 或该组件的 MyCompany\Component\Exception. 这为捕捉你的类库组件中的异常提供了极大的便利. 此外, 通过分析异常的类型, 我们也能看出某个异常的含义.
总结
总而言之,本文旨在教大家, 创建和抛出异常的最佳标准做法, 即: 应该多关注异常的类型, 少纠结异常的错误消息。如果你有什么看法, 欢迎在这里留言, 或在 PHP 文档网页, 亦或是上面给出链接的ZF2 wiki 留言。