這篇文章主要介紹PHP異常處理的方法,有興趣的朋友參考下,希望對大家有幫助。
每一個新的功能加入PHP運行時會創建一個指數隨機數,透過這樣的方式開發者可以使用和甚至濫用這個新特性。然而,直到一些好的和壞的使用情況陸續出現開發者才達成了共識。當這些新案例不斷浮現,我們終於可以分辨出什麼是最好或最壞的做法。
異常處理在PHP中的確無論如何都不算是新的特徵。但在本文中,我們將討論在PHP 5.3中基於異常處理的兩個新的特點。第一個是嵌套異常第二是一套SPL(現在的PHP運作機制的一個核心擴展)的擴展的新的異常類型。這兩個新特性,這本書裡都能找到最佳實務值得各位去詳細研究。
特別要注意:這些特性中的一些已經存在於低於5.3的PHP版本之中,或者至少能夠在低於5.3的版本之中被實現. 而當本文提到PHP 5.3, 並不是嚴責意義上的PHP 運行時版本. 相反,它意味著代碼庫和項目是採用PHP 5.3 作為最低版本的,但同時也是在新的發展階段出現的所有最佳實踐. 這個發展階段凸顯的是特定的幾個像Zend Framework, Symfony, Doctrine 以及PEAR 這樣的專案所進行的「2.0」嘗試.
背景
#PHP 5.2 只有一個異常類Exception。依照 Zend Framework / PEAR 的開發標準, 這個類別是你的函式庫中所有異常類別的基底類別。如果你建立一個名叫 MyCompany 的函式庫,按 Zend Framework / PEAR 的標準, 庫中所有的程式碼檔案都會以 MyCompany_ 開頭。要是你想為庫建立自己的異常基底類別: MyCompany_Exception, 那就用該類別繼承 Exception,然後再由元件(component )繼承和拋出該異常類別。例如你有一個元件 MyCompany_Foo,你可以為它建立一個用在該元件內部的異常基底類別 MyCompany_Foo_Exception。這些異常能被捕捉 MyCompany_Foo_Exception,MyCompany_Exception 或 Exception 的程式碼抓到。對於庫中其他用到該元件的程式碼來說,這是個三層的異常(或更多,取決於 MyCompany_Foo_Exception 的子類別有幾層 ), 他們可以根據自己的需求處理這些異常。
在php5中,基本異常類別已經支援嵌套的特性了。什麼是嵌套呢?巢狀是一種能力可以去捕捉特殊異常,或是捕捉參照原始異常而創建的一個新的異常物件。這將允許caller屬性在更公開的類型的開銷庫中出現的兩種異常類別上得到體現,當然也會在具有原始異常行為的異常類別上體現。
為什麼這些特性很有用?通常,透過使用其他程式碼來拋出自己的類型的異常是最有效的程式碼。這些程式碼可能是使用適配器模式封裝的提供一些適應性更強的函數的第三方程式碼庫的程式碼,或利用一些PHP擴充功能來拋出異常的簡單程式碼。
例如,在元件Zend_Db 中, 它使用了適配器模式來封裝特定的PHP 擴展,來創建一個資料庫抽象層. 在一個適配器中, Zend_Db 封裝了PDO, 而PDO 會拋出它自己的異常PDOException, Zend_Db 需要捕獲這些特定於PDO 的異常,並讓它們以可預期且類型已知的Zend_Db_Exception 重新被拋出. 這樣就給了開發者保證, Zend_Db 將總是拋出Zend_Db_Exception 類型的異常(因此可以被捕獲), 而他們同時也可以在需要的時候訪問到最開始被拋出的PDOException.
下面的示例展示了一個虛構的數據庫適配器可能如何去實現嵌入式的異常:
class MyCompany_Database { /** * @var PDO object setup during construction */ protected $_pdoResource = null; /** * @throws MyCompany_Database_Exception * @return int */ public function executeQuery($sql) { try { $numRows = $this->_pdoResource->exec($sql); } catch (PDOException $e) { throw new MyCompany_Database_Exception('Query was unexecutable', null, $e); } return $numRows; } }
為了使用嵌入式的異常,你就得呼叫被捕獲異常的getPrevious()方法:
// $sql and $connectionParameters assumed try { $db = new MyCompany_Database('PDO', $connectionParams); $db->executeQuery($sql); } catch (MyCompany_Database_Exception $e) { echo 'General Error: ' . $e->getMessage() . "\n"; $pdoException = $e->getPrevious(); echo 'PDO Specific error: ' . $pdoException->getMessage() . "\n"; }
大多數最近被實現的PHP擴展都擁有OO(物件導向)介面. 因此,這些API傾向於拋出異常,而不是發生錯誤終止。 PHP中能夠拋出異常的擴展,稍微列舉出幾個就包括有PDO, DOM, Mysqli, Phar, Soap 以及SQLite.
新特性:新核心異常類型
#在PHP 5.3開發中,我們展示了一些有趣的新異常類型。這些異常在PHP 5.2.x中已經存在,但最近還沒到「重新評估」異常的最佳實踐,現在他們會顯得更加引人注目。他們在SPL擴充中得以應用,並在手冊中列出(這裡)由於這些新的異常類型是PHP核心的一部分,也是SPL的一部分,它們可以被任何用PHP 5.3(及以上)運行程式碼的人使用。雖然在編寫應用程式層的程式碼時,看起來不那麼重要,但在我們寫或使用程式碼庫時,使用這些新異常類型變得更加重要
那么为什么新异常是普通类型?以前,开发者试图通过在异常消息提醒中放入更多的内容来赋予异常更多的含义。虽然这样做是可行的,但是它有几个缺点。一是你无法捕获基于消息的异常。这可是一个问题,如果你知道一组代码是同样的异常类型与不同的提示消息对应不同异常情况下,处理起来的难度将相当的大。例如,一个认证类,在对$auth->authenticate();;它抛出异常的相同类型的(假设是异常),但不同的消息对应两个具体的故障:产生故障原因是认证服务器不能达到但是相同的异常类型却提示失败的验证消息不同。在这种情况下(注意,使用异常可能不是处理认证响应最好的方式),这将需要用字符串来解析消息从而处理这两种不同的情况。
这个问题的解决办法显然是通过某种方式对异常进行编码,这样就可以在需要辨别如何对这种异常环境做出反应的时候能够更加容易的查询到。第一个反应库是使用异常基类的$code属性。另一个是通过创建可以被抛出且能描述自身行为的子类或者新的异常类。这两种方法具有相同的明显的缺点。两者都没有呈现出想这样的最好的例子。两者都不被认为是一个标准,因此每个试图复制这两种解决方案的项目都会有小的变化,这就迫使使用这需要回到文档以了解所创建的库中已经有的具体解决方案。现在通过使用SPL的新的类型方法,也称作php标准库;开发者就可以以同样的方式在他们的项目中,并且复用这些项目的新的最佳的方法已经出现。
第二个缺点是使用详细信息的做法使得理解这些异常情况对那些非英语或英语能力有限的开发者来说十分困难。这可能会使的开发者在试图理解异常信息的含义的过程十分的缓慢。许多开发者也会写关于异常的文章,因为还未出现一个统一的整合过的标准所要有同这些开发者数量相同的不同的版本来描述异常消息所描述的情况。
所以我如何去使用它们,就用这些让人无语的密密麻麻的细节描述?
现在在SPL中有总共13个新的异常类型。其中两个可被视为基类:逻辑异常和运行时异常;两种都继承php异常类。其余的方法在逻辑上可以被拆分为3组:动态调用组,逻辑组和运行时组。
动态调用组包含异常 BadFunctionCallException和BadMethodCallException,BadMethodCallException是BadFunctionCallException(LogicException的子类)的子类,这意味着这些异常可以被其直接类型(译者注:就是异常自身的类型,大家都知道异常有很多种)、LogicException,或者Exception抓到(译者注:就是catch)你应该在什么时候使用这些?通常,你应该在由一个无法处理的__call()方法产生的情况,或者回调无法不是一个有效的函数(简单说,当某些东西并非is_callable())时使用。
例如:
// OO variant class Foo { public function __call($method, $args) { switch ($method) { case 'doBar': /* ... */ break; default: throw new BadMethodCallException('Method ' . $method . ' is not callable by this object'); } } } // procedural variant function foo($bar, $baz) { $func = 'do' . $baz; if (!is_callable($func)) { throw new BadFunctionCallException('Function ' . $func . ' is not callable'); } }
一个直接的例子,在__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异常。
要理解得更好,我们来看看这个概念在代码中的运用:
class Foo { protected $number = 0; protected $bar = null; public function __construct($options) { /** 本方法抛出LogicException异常 **/ } public function setNumber($number) { /** 本方法抛出LogicException异常 **/ } public function setBar(Bar $bar) { /** 本方法抛出LogicException异常 **/ } public function doSomething($differentNumber) { if ($differentNumber != $expectedCondition) { /** 在这里,抛出LogicException异常 **/ } /** * 在这里,本方法抛出RuntimeException异常 */ } }
现在理解了这一概念,那么,对代码库的使用者来说,这是做什么的呢?使用者可以随时确定对象的异常状态,他们可以用异常的具体的类型来捕获(catch)异常,例如InvalidArgumentException或LengthException,至少也是LogicException。通过这种级别的精度调整,和类型的多样,他们可以用LogicException捕获最小的异常,但也可以通过实际的异常类型获得更好的理解。同样的概念也适用于运行时的异常,可以抛出更多的特定类型的异常,并且不论是特定或非特定类型的异常,都可以被捕获(catch)。它可以给使用者提供更详细的情况和精确度。
下面是一个关于SPL异常的表,您可能会有兴趣
类库代码中的最佳实践
PHP 5.3 带来了新的异常类型, 同时也带给我们新的最佳实践. 除了将某些特定的异常(如: InvalidArgumentException, RuntimeException)标准化外, 捕捉组件级的异常, 也很重要. 关于这方面, ZF2 wiki 和 PEAR2 wiki 上面有深入的探讨.
简而言之, 除了上面提到的各种最佳实践, 我们还应该用 Marker Interface 来创建一个组件级的异常基类. 通过创建组件级的 Marker Interface, 用在组件内部的异常既能继承 SPL 的异常类型, 也能在运行时被各种代码捕捉. 我们来看下列代码:
// usage of bracket syntax for brevity namespace MyCompany\Component { interface Exception {} class UnexpectedValueException extends \UnexpectedValueException implements Exception {} class Component { public static function doSomething() { if ($somethingExceptionalHappens) { throw new UnexpectedValueException('Something bad happened'); } } } }
如果调用上面代码中的 MyCompany\Component\Component::doSomething() 函数, doSomething() 抛出的异常可以当作下列异常类型捕捉: PHP 的 Exception, SPL 的 UnexpectedValueException, SPL 的 RuntimeException, 该组件的MyCompany\Component\UnexpectedValueException, 或该组件的 MyCompany\Component\Exception. 这为捕捉你的类库组件中的异常提供了极大的便利. 此外, 通过分析异常的类型, 我们也能看出某个异常的含义.
总结:以上就是本篇文的全部内容,希望能对大家的学习有所帮助。
相关推荐:
以上是PHP中異常處理的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!