PHP ランタイムに追加されるすべての新機能は指数関数的な乱数を作成するため、開発者はこの新機能を使用したり、悪用したりする可能性があります。ただし、開発者が合意に達したのは、良いユースケースと悪いユースケースがいくつか出現してからでした。こうした新たなケースが出現するにつれて、私たちは最終的に何が最良のアプローチか、何が最悪のアプローチかを識別できるようになります。
例外処理は、決して PHP の新しい機能ではありません。ただし、この記事では、PHP 5.3 の例外処理に基づく 2 つの新機能について説明します。 1 つ目はネストされた例外で、2 つ目は SPL (現在の PHP 実行メカニズムのコア拡張機能) によって拡張された一連の新しい例外タイプです。これら 2 つの新機能については、本書にベスト プラクティスが記載されており、詳細に検討する価値があります。
特別な注意: これらの機能の一部は、PHP 5.3 より前のバージョンにすでに存在しているか、少なくとも 5.3 より前のバージョンで実装できます。この記事で言及されている PHP 5.3 は、厳密な意味では PHP ランタイム バージョンではありません。これは、コード ベースとプロジェクトが最小バージョンとして PHP 5.3 を使用していることを意味しますが、この開発段階では、Zend Framework、Symfony などのプロジェクトによるいくつかの特定の「2.0」の試みに焦点を当てています。教義と PEAR。
背景
PHP 5.2 には、Exception という例外クラスが 1 つだけあります。 Zend Framework/PEAR 開発標準によれば、このクラスはライブラリ内のすべての例外クラスの基本クラスです。 MyCompany という名前のライブラリを作成すると、Zend Framework / PEAR 標準に従って、ライブラリ内のすべてのコード ファイルは MyCompany_ で始まります。ライブラリ MyCompany_Exception に独自の例外基本クラスを作成する場合は、このクラスを使用して Exception を継承し、コンポーネント (コンポーネント) がこの例外クラスを継承してスローします。たとえば、コンポーネント MyCompany_Foo がある場合、コンポーネント内で使用する例外基本クラス MyCompany_Foo_Exception を作成できます。これらの例外は、MyCompany_Foo_Exception、MyCompany_Exception、または Exception をキャッチするコードによってキャッチできます。 このコンポーネントを使用するライブラリ内の他のコードの場合、これは 3 レベルの例外 (MyCompany_Foo_Exception のサブクラスの数によってはそれ以上) であり、これらの例外を必要に応じて処理できます。
php5 では、基本例外クラスはすでにネスト機能をサポートしています。ネスティングとは何ですか?ネストとは、特定の例外、または元の例外を参照して作成された新しい例外オブジェクトをキャッチする機能です。これにより、呼び出し元属性を、よりパブリックな型のオーバーヘッド ライブラリに表示される 2 つの例外クラスに反映できるようになり、もちろん元の例外動作を持つ例外クラスにも反映できるようになります。
これらの機能が役立つのはなぜですか?多くの場合、他のコードを使用して独自のタイプの例外をスローするのが最も効率的なコードです。このコードは、アダプター パターンを使用してカプセル化された適応性の高い機能を提供するサードパーティ コード ライブラリのコード、または例外をスローするためにいくつかの PHP 拡張機能を利用する単純なコードである可能性があります。
たとえば、コンポーネント Zend_Db では、アダプター パターンを使用して特定の PHP 拡張機能をカプセル化し、データベース抽象化レイヤーを作成します。アダプターでは、Zend_Db が PDO をカプセル化し、PDO が独自の例外 PDOException をスローします。これは、Zend_Db がこれらの PDO をキャッチする必要があります。これにより、開発者は Zend_Db が常に Zend_Db_Exception 型の例外をスローする (したがってキャッチできる) ことが保証され、最初にスローされた PDOException にもアクセスできます。必要なときに。次の例は、架空のデータベース アダプターが埋め込み例外を実装する方法を示しています。 リーリー
埋め込み例外を使用するには、キャッチした例外の getPrevious() メソッドを呼び出す必要があります。 リーリー
最近実装された PHP 拡張機能のほとんどは OO (オブジェクト指向) インターフェイスを備えているため、これらの API はエラーで終了するのではなく、例外をスローする傾向があります。例外をスローできる PHP の拡張機能には、PDO、DOM、Mysqli、Phar、Soap、SQLite などがあります。
新機能: 新しいコア例外タイプ
PHP 5.3 開発では、いくつかの興味深い新しい例外タイプを紹介しています。これらの例外は PHP 5.2 に存在していました。これらの新しい例外タイプは SPL 拡張機能に実装されており、こちらのマニュアルにリストされています)。これらの新しい例外タイプは PHP コアの一部であり、SPL の一部であるため、PHP 5.3 以降でコードを実行している人なら誰でも使用できます。アプリケーション層のコードを作成するときはそれほど重要ではないように思えるかもしれませんが、コード ライブラリを作成または使用するときは、これらの新しい例外タイプの使用がより重要になります
那么为什么新异常是普通类型?以前,开发者试图通过在异常消息提醒中放入更多的内容来赋予异常更多的含义。虽然这样做是可行的,但是它有几个缺点。一是你无法捕获基于消息的异常。这可是一个问题,如果你知道一组代码是同样的异常类型与不同的提示消息对应不同异常情况下,处理起来的难度将相当的大。例如,一个认证类,在对$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 留言。