cvs: peardoc /en/chapters standards.xml

From: Date: Sun, 17 Dec 2006 10:53:04 +0000
Subject: cvs: peardoc /en/chapters standards.xml
Groups: php.pear.doc 
Request: Send a blank email to pear-doc+get-9109@lists.php.net to get a copy of this message
mj Sun Dec 17 10:53:04 2006 UTC Modified files: /peardoc/en/chapters standards.xml Log: * Added exception handling guidelines for PHP 5 code.

http://cvs.php.net/viewvc.cgi/peardoc/en/chapters/standards.xml?r1=1.46&r2=1.47&diff_format=u Index: peardoc/en/chapters/standards.xml diff -u peardoc/en/chapters/standards.xml:1.46 peardoc/en/chapters/standards.xml:1.47 --- peardoc/en/chapters/standards.xml:1.46 Sun Dec 17 10:15:59 2006 +++ peardoc/en/chapters/standards.xml Sun Dec 17 10:53:04 2006 @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="iso-8859-1" ?> -<!-- $Revision: 1.46 $ --> +<!-- $Revision: 1.47 $ --> <chapter id="standards"> <title>Coding Standards</title> @@ -22,7 +22,7 @@ mode hook that will set up Emacs (ensure that it is called when you are editing PHP files): <programlisting role="elisp"> - <![CDATA[ + <![CDATA[s (defun php-mode-hook () (setq tab-width 4 c-basic-offset 4 @@ -849,6 +849,503 @@ </para> </sect1> + <sect1 id="standars.errors"> + <title>Errr Handling Guidelines</title> + + <abstract> + <para> + This part of the Coding Standards describes how errors are + handled in PEAR packages that are developed for PHP 5 and 6. It + uses Exceptions, introduced in PHP 5.0 with Zend Engine 2, as + the error handling mechanism. + </para> + </abstract> + + <sect2> + <title>Definition of an error</title> + + <para> + An error is defined as an unexpected, invalid program state from + which it is impossible to recover. For the sake of definition, + recovery scope is defined as the method scope. Incomplete + recovery is considered a recovery. + </para> + + <example> + <title>One pretty straightforward example for an error</title> + <programlisting role="php"> + <![CDATA[ +/* + * Connect to Specified Database + * + * @throws Example_Datasource_Exception when it can't connect + * to specified DSN. + */ +function connectDB($dsn) { + $this->db =& DB::connect($dsn); + if (DB::isError($this->db)) { + throw new Example_Datasource_Exception( + "Unable to connect to $dsn:" . $this->db->getMessage() + ); + } +} + ]]> + </programlisting> + </example> + + <para> + In this example the objective of the method is to connect to the + given DSN. Since it can't do anything but ask PEAR DB to do it, + whenever DB returns an error, the only option is to bail out and + launch the exception. + </para> + + <example> + <title>Error handling with recovery</title> + + <programlisting role="php"> + <![CDATA[ +/* + * Connect to one of the possible databases + * + * @throws Example_Datasource_Exception when it can't connect to + * any of the configured databases. + * + * @throws Example_Config_Exception when it can't find databases + * in the configuration. + */ + +function connect(Config $conf) { + $dsns =& $conf->searchPath(array('config', 'db')); + if ($dsns === FALSE) throw new Example_Config_Exception( + 'Unable to find config/db section in configuration.' + ); + + $dsns =& $dsns->toArray(); + + foreach($dsns as $dsn) { + try { + $this->connectDB($dsn); + return; + } catch (Example_Datasource_Exception e) { + // Some warning/logging code recording the failure + // to connect to one of the databases + } + } + throw new Example_Datasource_Exception( + 'Unable to connect to any of the configured databases' + ); +} + ]]> + </programlisting> + </example> + + <para> + This second example shows an exception being caught and + recovered from. Altough the lower level + <function>connectDB</function> method is unable to do anything + but throw an error when one database connection fails, the upper + level <function>connect</function> method knows the object can + go by with any one of the configured databases. Since the error + was recovered from, the exception is silenced at this level and + not rethrown. + </para> + + <example> + <title>Incomplete recovery</title> + + <programlisting role="php"> + <![CDATA[ +/* + * loadConfig parses the provided configuration. If the configuration + * is invalid, it will set the configuration to the default config. + * + */ +function loadConfig(Config $conf) { + try { + $this->config = $conf->parse(); + } catch (Config_Parse_Exception e) { + // Warn/Log code goes here + // Perform incomplete recovery + $this->config = $this->defaultConfig; + } +} + ]]> + </programlisting> + </example> + + <para> + The recovery produces side effects, so it is considered + incomplete. However, the program may proceed, so the exception + is considered handled, and must not be rethrown. As in the + previous example, when silencing the exception, logging or + warning should occur. + </para> + </sect2> + + <sect2> + <title>Error Signaling in PHP 5 PEAR packages</title> + + <para> + Error conditions in PEAR packages written for PHP 5 must be + signaled using exceptions. Usage of return codes or return + <classname>PEAR_Error</classname> objects is deprecated in favor + of exceptions. Naturally, packages providing compatibility with + PHP 4 do not fall under these coding guidelines, and may thus + use the error handling mechanisms defined in the PHP 4 PEAR + coding guidelines. + </para> + <para> + An exception should be thrown whenever an error condition is + met, according to the definition provided in the previous + section. The thrown exception should contain enough information + to debug the error and quickly identify the error cause. Note + that, during production runs, no exception should reach the + end-user, so there is no need for concern about technical + complexity in the exception error messages. + </para> + <para> + The basic <classname>PEAR_Exception</classname> contains a + textual error, describing the program state that led to the + throw and, optionally, a wrapped lower level exception, + containing more info on the lower level causes of the error. + </para> + <para> + The kind of information to be included in the exception is + dependent on the error condition. From the point of view of + exception throwing, there are three classes of error conditions: + </para> + + <orderedlist> + <listitem> + <simpara> + Errors detected during precondition checks + </simpara> + </listitem> + <listitem> + <simpara> + Lower level library errors signaled via error return codes + or error return objects + </simpara> + </listitem> + <listitem> + <simpara> + Uncorrectable lower library exceptions + </simpara> + </listitem> + </orderedlist> + + <para> + Errors detected during precondition checks should contain a + description of the failed check. If possible, the description + should contain the violating value. Naturally, no wrapped + exception can be included, as there isn't a lower level cause of + the error. Example: + </para> + + <example> + <title /> + <programlisting role="php"> + <![CDATA[ +function divide($x, $y) { + if ($y == 0) { + throw new Example_Aritmetic_Exception('Division by zero'); + } +} + ]]> + </programlisting> + </example> + + <para> + Errors signaled via return codes by lower level libraries, if + unrecoverable, should be turned into exceptions. The error + description should try to convey all information contained in + the original error. One example, is the connect method + previously presented: + </para> + + <example> + <title /> + <programlisting role="php"> + <![CDATA[ +/* + * Connect to Specified Database + * + * @throws Example_Datasource_Exception when it can't connect to specified DSN. + */ +function connectDB($dsn) { + $this->db =& DB::connect($dsn); + if (DB::isError($this->db)) { + throw new Example_Datasource_Exception( + "Unable to connect to $dsn:" . $this->db->getMessage() + ); + } +} + ]]> + </programlisting> + </example> + + <para> + Lower library exceptions, if they can't be corrected, should + either be rethrown or bubbled up. When rethrowing, the original + exception must be wrapped inside the one being thrown. When + letting the exception bubble up, the exception just isn't + handled and will continue up the call stack in search of a + handler. + </para> + + <example> + <title>Rethrowing an exception</title> + + <programlisting role="php"> + <![CDATA[ +function preTaxPrice($retailPrice, $taxRate) { + try { + return $this->divide($retailPrice, 1 + $taxRate); + } catch (Example_Aritmetic_Exception e) { + throw new Example_Tax_Exception('Invalid tax rate.', e); + } +} + ]]> + </programlisting> + </example> + + <example> + <title>Letting exceptions bubble up</title> + + <programlisting role="php"> + <![CDATA[ +function preTaxPrice($retailPrice, $taxRate) { + return $this->divide($retailPrice, 1 + $taxRate); +} + ]]> + </programlisting> + </example> + + <para> + The case between rethrowing or bubbling up is one of software + architecture: Exceptions should be bubbled up, except in these + two cases: + </para> + + <orderedlist> + <listitem> + <simpara> + The original exception is from another package. Letting it + bubble up would cause implementation details to be exposed, + violating layer abstraction, conducing to poor design. + </simpara> + </listitem> + <listitem> + <simpara> + The current method can add useful debugging information to + the received error before rethrowing. + </simpara> + </listitem> + </orderedlist> + </sect2> + + <sect2> + <title>Exceptions and normal program flow</title> + + <para> + Exceptions should never be used as normal program flow. If + removing all exception handling logic (try-catch statements) + from the program, the remaining code should represent the + &quot;One True Path&quot; -- the flow that would be executed in + the absence of errors. + </para> + <para> + This requirement is equivalent to requiring that exceptions be + thrown only on error conditions, and never in normal program + states. + </para> + <para> + One example of a method that wrongly uses the bubble up + capability of exceptions to return a result from a deep + recursion: + </para> + + <example> + <title /> + <programlisting role="php"> + <![CDATA[ +/** + * Recursively search a tree for string. + * @throws ResultException + */ +public function search(TreeNode $node, $data) { + if ($node->data === $data) { + throw new ResultException( $node ); + } else { + search( $node->leftChild, $data ); + search( $node->rightChild, $data ); + } +} + ]]> + </programlisting> + </example> + + <para> + In the example the ResultException is simply using the + &quot;eject!&quot; qualities of exception handling to jump out + of deeply nested recursion. When actually used to signify an + error this is a very powerful feature, but in the example above + this is simply lazy development. + </para> + </sect2> + + <sect2> + <title>Exception class hierarchies</title> + + <para> + All of PEAR packages exceptions must be descendant from + <classname>PEAR_Exception</classname>. <classname>PEAR_Exception</classname> + provides exception wrapping abilities, absent from the top level + PHP Exception class, and needed to comply with the previous + section requirements. + </para> + <para> + Aditionally, each PEAR package must provide a top level + exception, named &lt;Package_Name&gt;_Exception. It is + considered best practice that the package never throws + exceptions that aren't descendant from its top level exception. + </para> + </sect2> + + <sect2> + <title>Documenting Exceptions</title> + + <para> + Because PHP, unlike Java, does not require you to explicitly + state which exceptions a method throws in the method signature, + it is critical that exceptions be thoroughly documented in your + method headers. + </para> + + <example> + <title> + Exceptions should be documented using the + <literal>@throws</literal> phpdoc keyword + </title> + + <programlisting role="php"> + <![CDATA[ +/** + * This method searches for aliens. + * + * @return array Array of Aliens objects. + * @throws AntennaBrokenException If the impedence readings indicate + * that the antenna is broken. + * + * @throws AntennaInUseException If another process is using the + * antenna already. + */ +public function findAliens($color = 'green'); + ]]> + </programlisting> + </example> + + <para> + In many cases middle layers of an application will rewrap any + lower-level exceptions into more meaningful application + exceptions. This also needs to be made clear: + </para> + + <example> + <title /> + <programlisting role="php"> + <![CDATA[ +/** + * Load session objects into shared memory. + * + * @throws LoadingException Any lower-level IOException will be wrapped + * and re-thrown as a LoadingException. + */ +public function loadSessionObjects(); + ]]> + </programlisting> + </example> + + <para> + In other cases your method may simply be a conduit through which + lower level exceptions can pass freely. As challenging as it may + be, your method should also document which exceptions it is + <emphasis>not</emphasis> catching. + </para> + + <example> + <title /> + <programlisting role="php"> + <![CDATA[ +/** + * Performs a batch of database queries (atomically, not in transaction). + * @throws SQLException Low-level SQL errors will bubble up through this method. + */ +public function batchExecute(); + ]]> + </programlisting> + </example> + </sect2> + + <sect2> + <title>Exceptions as part of the API</title> + + <para> + Exceptions play a critical role in the API of your + library. Developers using your library + <emphasis>depend</emphasis> on accurate descriptions of where + and why exceptions might be thrown from your + package. Documentation is critical. Also maintaining the types + of messages that are thrown is also an important requirement for + maintaining backwards-compatibility. + </para> + <para> + Because Exceptions are critical to the API of your package, you + must ensure that you don't break backwards compatibility (BC) by + making changes to exceptions. + </para> + <para> + Things that break BC include: + </para> + + <itemizedlist> + <listitem> + <simpara> + Any change to which methods throw exceptions. + </simpara> + </listitem> + <listitem> + <simpara> + A change whereby a method throws an exception higher in the + inheritance tree. For example, if you changed your method to + throw a <classname>PEAR_Exception</classname> rather than a + <classname>PEAR_IOException</classname>, you would be + breaking backwards compatibility. + </simpara> + </listitem> + </itemizedlist> + + <para> + Things that do not break BC: + </para> + + <itemizedlist> + <listitem> + <simpara> + Throwing a subclass of the original exception. For example, + changing a method to throw + <classname>PEAR_IOException</classname> when before it had + been throwing <classname>PEAR_Exception</classname> would + not break BC (provided that + <classname>PEAR_IOException</classname> extends + <classname>PEAR_Exception</classname>). + </simpara> + </listitem> + </itemizedlist> + </sect2> + </sect1> + <sect1 id="standards.bestpractices"> <title>Best practices</title> <para>
« previous php.pear.doc (#9109) next »