root/fException.php

Revision 633, 19.4 kB (checked in by wbond, 8 months ago)

Fixed some API documentation in fException

LineHide Line Numbers
1 <?php
2 /**
3  * An exception that allows for easy l10n, printing, tracing and hooking
4  *
5  * @copyright  Copyright (c) 2007-2009 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fException
11  *
12  * @version    1.0.0b8
13  * @changes    1.0.0b8  Added a missing line of backtrace to ::formatTrace() [wb, 2009-06-28]
14  * @changes    1.0.0b7  Updated ::__construct() to no longer require a message, like the Exception class, and allow for non-integer codes [wb, 2009-06-26]
15  * @changes    1.0.0b6  Fixed ::splitMessage() so that the original message is returned if no list items are found, added ::reorderMessage() [wb, 2009-06-02]
16  * @changes    1.0.0b5  Added ::splitMessage() to replace fCRUD::removeListItems() and fCRUD::reorderListItems() [wb, 2009-05-08]
17  * @changes    1.0.0b4  Added a check to ::__construct() to ensure that the `$code` parameter is numeric [wb, 2009-05-04]
18  * @changes    1.0.0b3  Fixed a bug with ::printMessage() messing up some HTML messages [wb, 2009-03-27]
19  * @changes    1.0.0b2  ::compose() more robustly handles `$components` passed as an array, ::__construct() now detects stray `%` characters [wb, 2009-02-05]
20  * @changes    1.0.0b   The initial implementation [wb, 2007-06-14]
21  */
22 abstract class fException extends Exception
23 {
24     /**
25     * Callbacks for when exceptions are created
26     *
27     * @var array
28     */
29     static private $callbacks = array();
30    
31    
32     /**
33     * Composes text using fText if loaded
34     *
35     * @param  string  $message    The message to compose
36     * @param  mixed   $component  A string or number to insert into the message
37     * @param  mixed   ...
38     * @return string  The composed and possible translated message
39     */
40     static protected function compose($message)
41     {
42         $components = array_slice(func_get_args(), 1);
43        
44         // Handles components passed as an array
45         if (sizeof($components) == 1 && is_array($components[0])) {
46             $components = $components[0];   
47         }
48        
49         // If fText is loaded, use it
50         if (class_exists('fText', FALSE)) {
51             return call_user_func_array(
52                 array('fText', 'compose'),
53                 array($message, $components)
54             );
55            
56         } else {
57             return vsprintf($message, $components);
58         }
59     }
60    
61    
62     /**
63     * Creates a string representation of any variable using predefined strings for booleans, `NULL` and empty strings
64     *
65     * The string output format of this method is very similar to the output of
66     * [http://php.net/print_r print_r()] except that the following values
67     * are represented as special strings:
68     *   
69     *  - `TRUE`: `'{true}'`
70     *  - `FALSE`: `'{false}'`
71     *  - `NULL`: `'{null}'`
72     *  - `''`: `'{empty_string}'`
73     *
74     * @param  mixed $data  The value to dump
75     * @return string  The string representation of the value
76     */
77     static protected function dump($data)
78     {
79         if (is_bool($data)) {
80             return ($data) ? '{true}' : '{false}';
81        
82         } elseif (is_null($data)) {
83             return '{null}';
84        
85         } elseif ($data === '') {
86             return '{empty_string}';
87        
88         } elseif (is_array($data) || is_object($data)) {
89            
90             ob_start();
91             var_dump($data);
92             $output = ob_get_contents();
93             ob_end_clean();
94            
95             // Make the var dump more like a print_r
96             $output = preg_replace('#=>\n(  )+(?=[a-zA-Z]|&)#m', ' => ', $output);
97             $output = str_replace('string(0) ""', '{empty_string}', $output);
98             $output = preg_replace('#=> (&)?NULL#', '=> \1{null}', $output);
99             $output = preg_replace('#=> (&)?bool\((false|true)\)#', '=> \1{\2}', $output);
100             $output = preg_replace('#string\(\d+\) "#', '', $output);
101             $output = preg_replace('#"(\n(  )*)(?=\[|\})#', '\1', $output);
102             $output = preg_replace('#(?:float|int)\((-?\d+(?:.\d+)?)\)#', '\1', $output);
103             $output = preg_replace('#((?:  )+)\["(.*?)"\]#', '\1[\2]', $output);
104             $output = preg_replace('#(?:&)?array\(\d+\) \{\n((?:  )*)((?:  )(?=\[)|(?=\}))#', "Array\n\\1(\n\\1\\2", $output);
105             $output = preg_replace('/object\((\w+)\)#\d+ \(\d+\) {\n((?:  )*)((?:  )(?=\[)|(?=\}))/', "\\1 Object\n\\2(\n\\2\\3", $output);
106             $output = preg_replace('#^((?:  )+)}(?=\n|$)#m', "\\1)\n", $output);
107             $output = substr($output, 0, -2) . ')';
108            
109             // Fix indenting issues with the var dump output
110             $output_lines = explode("\n", $output);
111             $new_output = array();
112             $stack = 0;
113             foreach ($output_lines as $line) {
114                 if (preg_match('#^((?:  )*)([^ ])#', $line, $match)) {
115                     $spaces = strlen($match[1]);
116                     if ($spaces && $match[2] == '(') {
117                         $stack += 1;
118                     }
119                     $new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
120                     if ($spaces && $match[2] == ')') {
121                         $stack -= 1;
122                     }
123                 } else {
124                     $new_output[] = str_pad('', ($spaces)+(4*$stack)) . $line;
125                 }
126             }
127            
128             return join("\n", $new_output);
129            
130         } else {
131             return (string) $data;
132         }
133     }
134    
135    
136     /**
137     * Adds a callback for when certain types of exceptions are created
138     *
139     * The callback will be called when any exception of this class, or any
140     * child class, specified is tossed. A single parameter will be passed
141     * to the callback, which will be the exception object.
142     *
143     * @param  callback $callback        The callback
144     * @param  string   $exception_type  The type of exception to call the callback for
145     * @return void
146     */
147     static public function registerCallback($callback, $exception_type=NULL)
148     {
149         if ($exception_type === NULL) {
150             $exception_type = 'fException';   
151         }
152        
153         if (!isset(self::$callbacks[$exception_type])) {
154             self::$callbacks[$exception_type] = array();
155         }
156        
157         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
158             $callback = explode('::', $callback);   
159         }
160        
161         self::$callbacks[$exception_type][] = $callback;
162     }
163    
164    
165     /**
166     * Compares the message matching strings by longest first so that the longest matches are made first
167     *
168     * @param  string $a  The first string to compare
169     * @param  string $b  The second string to compare
170     * @return integer  `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
171     */
172     static private function sortMatchingArray($a, $b)
173     {
174         return -1 * strnatcmp(strlen($a), strlen($b));
175     }
176    
177    
178     /**
179     * Sets the message for the exception, allowing for string interpolation and internationalization
180     *
181     * The `$message` can contain any number of formatting placeholders for
182     * string and number interpolation via [http://php.net/sprintf `sprintf()`].
183     * Any `%` signs that do not appear to be part of a valid formatting
184     * placeholder will be automatically escaped with a second `%`.
185     *
186     * The following aspects of valid `sprintf()` formatting codes are not
187     * accepted since they are redundant and restrict the non-formatting use of
188     * the `%` sign in exception messages:
189     *  - `% 2d`: Using a literal space as a padding character - a space will be used if no padding character is specified
190     *  - `%'.d`: Providing a padding character but no width - no padding will be applied without a width
191     *
192     * @param  string $message    The message for the exception. This accepts a subset of [http://php.net/sprintf `sprintf()`] strings - see method description for more details.
193     * @param  mixed  $component  A string or number to insert into the message
194     * @param  mixed  ...
195     * @param  mixed  $code       The exception code to set
196     * @return fException
197     */
198     public function __construct($message='')
199     {
200         $args          = array_slice(func_get_args(), 1);
201         $required_args = preg_match_all(
202             '/
203                 (?<!%)                       # Ensure this is not an escaped %
204                 %(                           # The leading %
205                   (?:\d+\$)?                 # Position
206                   \+?                        # Sign specifier
207                   (?:(?:0|\'.)?-?\d+|-?)     # Padding, alignment and width or just alignment
208                   (?:\.\d+)?                 # Precision
209                   [bcdeufFosxX]              # Type
210                 )/x',
211             $message,
212             $matches
213         );
214        
215         // Handle %s that weren't properly escaped
216         $formats    = $matches[1];
217         $delimeters = ($formats) ? array_fill(0, sizeof($formats), '#') : array();
218         $lookahead  = join(
219             '|',
220             array_map(
221                 'preg_quote',
222                 $formats,
223                 $delimeters
224             )
225         );
226         $lookahead  = ($lookahead) ? '|' . $lookahead : '';
227         $message    = preg_replace('#(?<!%)%(?!%' . $lookahead . ')#', '%%', $message);   
228        
229         // If we have an extra argument, it is the exception code
230         $code = NULL;
231         if ($required_args == sizeof($args) - 1) {
232             $code = array_pop($args);       
233         }
234        
235         if (sizeof($args) != $required_args) {
236             $message = self::compose(
237                 '%1$d components were passed to the %2$s constructor, while %3$d were specified in the message',
238                 sizeof($args),
239                 get_class($this),
240                 $required_args
241             );
242             throw new Exception($message);   
243         }
244        
245         $args = array_map(array('fException', 'dump'), $args);
246        
247         parent::__construct(self::compose($message, $args));
248         $this->code = $code;
249        
250         foreach (self::$callbacks as $class => $callbacks) {
251             foreach ($callbacks as $callback) {
252                 if ($this instanceof $class) {
253                     call_user_func($callback, $this);
254                 }
255             }
256         }       
257     }
258    
259    
260     /**
261     * All requests that hit this method should be requests for callbacks
262     *
263     * @internal
264      *
265     * @param  string $method  The method to create a callback for
266     * @return callback  The callback for the method requested
267     */
268     public function __get($method)
269     {
270         return array($this, $method);       
271     }
272    
273    
274     /**
275     * Gets the backtrace to currently called exception
276     *
277     * @return string  A nicely formatted backtrace to this exception
278     */
279     public function formatTrace()
280     {
281         $doc_root  = realpath($_SERVER['DOCUMENT_ROOT']);
282         $doc_root .= (substr($doc_root, -1) != DIRECTORY_SEPARATOR) ? DIRECTORY_SEPARATOR : '';
283        
284         $backtrace = explode("\n", $this->getTraceAsString());
285         array_unshift($backtrace, $this->file . '(' . $this->line . ')');
286         $backtrace = preg_replace('/^#\d+\s+/', '', $backtrace);
287         $backtrace = str_replace($doc_root, '{doc_root}' . DIRECTORY_SEPARATOR, $backtrace);
288         $backtrace = array_diff($backtrace, array('{main}'));
289         $backtrace = array_reverse($backtrace);
290        
291         return join("\n", $backtrace);
292     }
293    
294    
295     /**
296     * Returns the CSS class name for printing information about the exception
297     *
298     * @return void
299     */
300     protected function getCSSClass()
301     {
302         $string = preg_replace('#^f#', '', get_class($this));
303        
304         do {
305             $old_string = $string;
306             $string = preg_replace('/([a-zA-Z])([0-9])/', '\1_\2', $string);
307             $string = preg_replace('/([a-z0-9A-Z])([A-Z])/', '\1_\2', $string);
308         } while ($old_string != $string);
309        
310         return strtolower($string);
311     }
312    
313    
314     /**
315     * Prepares content for output into HTML
316     *
317     * @return string  The prepared content
318     */
319     protected function prepare($content)
320     {
321         // See if the message has newline characters but not br tags, extracted from fHTML to reduce dependencies
322         static $inline_tags_minus_br = '<a><abbr><acronym><b><big><button><cite><code><del><dfn><em><font><i><img><input><ins><kbd><label><q><s><samp><select><small><span><strike><strong><sub><sup><textarea><tt><u><var>';
323         $content_with_newlines = (strip_tags($content, $inline_tags_minus_br)) ? $content : nl2br($content);
324        
325         // Check to see if we have any block-level html, extracted from fHTML to reduce dependencies
326         $inline_tags = $inline_tags_minus_br . '<br>';
327         $no_block_html = strip_tags($content, $inline_tags) == $content;
328        
329         // This code ensures the output is properly encoded for display in (X)HTML, extracted from fHTML to reduce dependencies
330         $reg_exp = "/<\s*\/?\s*[\w:]+(?:\s+[\w:]+(?:\s*=\s*(?:\"[^\"]*?\"|'[^']*?'|[^'\">\s]+))?)*\s*\/?\s*>|&(?:#\d+|\w+);|<\!--.*?-->/";
331         preg_match_all($reg_exp, $content, $html_matches, PREG_SET_ORDER);
332         $text_matches = preg_split($reg_exp, $content_with_newlines);
333        
334         foreach($text_matches as $key => $value) {
335             $value = htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
336         }
337        
338         for ($i = 0; $i < sizeof($html_matches); $i++) {
339             $text_matches[$i] .= $html_matches[$i][0];
340         }
341        
342         $content_with_newlines = implode($text_matches);
343        
344         $output  = ($no_block_html) ? '<p>' : '';
345         $output .= $content_with_newlines;
346         $output .= ($no_block_html) ? '</p>' : '';
347        
348         return $output;
349     }
350    
351    
352     /**
353     * Prints the message inside of a div with the class being 'exception %THIS_EXCEPTION_CLASS_NAME%'
354     *
355     * @return void
356     */
357     public function printMessage()
358     {
359         echo '<div class="exception ' . $this->getCSSClass() . '">';
360         echo $this->prepare($this->message);
361         echo '</div>';
362     }
363    
364    
365     /**
366     * Prints the backtrace to currently called exception inside of a pre tag with the class being 'exception %THIS_EXCEPTION_CLASS_NAME% trace'
367     *
368     * @return void
369     */
370     public function printTrace()
371     {
372         echo '<pre class="exception ' . $this->getCSSClass() . ' trace">';
373         echo $this->formatTrace();
374         echo '</pre>';
375     }
376    
377    
378     /**
379     * Reorders list items in the message based on simple string matching
380     *
381     * @param  string $match  This should be a string to match to one of the list items - whatever the order this is in the parameter list will be the order of the list item in the adjusted message
382     * @param  string ...
383     * @return fException  The exception object, to allow for method chaining
384     */
385     public function reorderMessage($match)
386     {
387         // If we can't find a list, don't bother continuing
388         if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $message_parts)) {
389             return $this;
390         }
391        
392         $matching_array = func_get_args();
393         // This ensures that we match on the longest string first
394         uasort($matching_array, array('self', 'sortMatchingArray'));
395        
396         $beginning     = $message_parts[1];
397         $list_contents = $message_parts[2];
398         $ending        = $message_parts[3];
399        
400         preg_match_all('#<li(.*?)</li>#i', $list_contents, $list_items, PREG_SET_ORDER);
401        
402         $ordered_items = array_fill(0, sizeof($matching_array), array());
403         $other_items   = array();
404        
405         foreach ($list_items as $list_item) {
406             foreach ($matching_array as $num => $match_string) {
407                 if (strpos($list_item[1], $match_string) !== FALSE) {
408                     $ordered_items[$num][] = $list_item[0];
409                     continue 2;
410                 }
411             }
412            
413             $other_items[] = $list_item[0];
414         }
415        
416         $final_list = array();
417         foreach ($ordered_items as $ordered_item) {
418             $final_list = array_merge($final_list, $ordered_item);
419         }
420         $final_list = array_merge($final_list, $other_items);
421        
422         $this->message = $beginning . join("\n", $final_list) . $ending;
423        
424         return $this;
425     }
426    
427    
428     /**
429     * Allows the message to be overwriten
430     *
431     * @param  string $new_message  The new message for the exception
432     * @return void
433     */
434     public function setMessage($new_message)
435     {
436         $this->message = $new_message;
437     }
438    
439    
440     /**
441     * Splits an exception with an HTML list into multiple strings each containing part of the original message
442     *
443     * This method should be called with two or more parameters of arrays of
444     * string to match. If any of the provided strings are matching in a list
445     * item in the exception message, a new copy of the message will be created
446     * containing just the matching list items.
447     *
448     * Here is an exception message to be split:
449     *
450     * {{{
451     * #!html
452     * <p>The following problems were found:</p>
453     * <ul>
454     *     <li>First Name: Please enter a value</li>
455     *     <li>Last Name: Please enter a value</li>
456     *     <li>Email: Please enter a value</li>
457     *     <li>Address: Please enter a value</li>
458     *     <li>City: Please enter a value</li>
459     *     <li>State: Please enter a value</li>
460     *     <li>Zip Code: Please enter a value</li>
461     * </ul>
462     * }}}
463     *
464     * The following PHP would split the exception into two messages:
465     *
466     * {{{
467     * #!php
468     * list ($name_exception, $address_exception) = $exception->splitMessage(
469     *     array('First Name', 'Last Name', 'Email'),
470     *     array('Address', 'City', 'State', 'Zip Code')
471     * );
472     * }}}
473     *
474     * The resulting messages would be:
475     *
476     * {{{
477     * #!html
478     * <p>The following problems were found:</p>
479     * <ul>
480     *     <li>First Name: Please enter a value</li>
481     *     <li>Last Name: Please enter a value</li>
482     *     <li>Email: Please enter a value</li>
483     * </ul>
484     * }}}
485     *
486     * and
487     *
488     * {{{
489     * #!html
490     * <p>The following problems were found:</p>
491     * <ul>
492     *     <li>Address: Please enter a value</li>
493     *     <li>City: Please enter a value</li>
494     *     <li>State: Please enter a value</li>
495     *     <li>Zip Code: Please enter a value</li>
496     * </ul>
497     * }}}
498     *
499     * If no list items match the strings in a parameter, the result will be
500     * an empty string, allowing for simple display:
501     *
502     * {{{
503     * #!php
504     * fHTML::show($name_exception, 'error');
505     * }}}
506     *
507     * An empty string is returned when none of the list items matched the
508     * strings in the parameter. If no list items are found, the first value in
509     * the returned array will be the existing message and all other array
510     * values will be an empty string.
511     *
512     * @param  array $list_item_matches  An array of strings to filter the list items by, list items will be ordered in the same order as this array
513     * @param  array ...
514     * @return array  This will contain an array of strings corresponding to the parameters passed - see method description for details
515     */
516     public function splitMessage($list_item_matches)
517     {
518         $class = get_class($this);
519        
520         $matching_arrays = func_get_args();
521        
522         if (!preg_match('#^(.*<(?:ul|ol)[^>]*?>)(.*?)(</(?:ul|ol)>.*)$#isD', $this->message, $matches)) {
523             return array_merge(array($this->message), array_fill(0, sizeof($matching_arrays)-1, ''));
524         }
525        
526         $beginning_html  = $matches[1];
527         $list_items_html = $matches[2];
528         $ending_html     = $matches[3];
529        
530         preg_match_all('#<li(.*?)</li>#i', $list_items_html, $list_items, PREG_SET_ORDER);
531        
532         $output = array();
533        
534         foreach ($matching_arrays as $matching_array) {
535            
536             // This ensures that we match on the longest string first
537             uasort($matching_array, array('self', 'sortMatchingArray'));
538            
539             // We may match more than one list item per matching string, so we need a multi-dimensional array to hold them
540             $matched_list_items = array_fill(0, sizeof($matching_array), array());
541             $found              = FALSE;
542            
543             foreach ($list_items as $list_item) {
544                 foreach ($matching_array as $match_num => $matching_string) {
545                     if (strpos($list_item[1], $matching_string) !== FALSE) {
546                         $matched_list_items[$match_num][] = $list_item[0];
547                         $found = TRUE;
548                         continue 2;
549                     }
550                 }
551             }
552            
553             if (!$found) {
554                 $output[] = '';
555                 continue;
556             }
557            
558             // This merges all of the multi-dimensional arrays back to one so we can do a simple join
559             $merged_list_items = array();
560             foreach ($matched_list_items as $match_num => $matched_items) {
561                 $merged_list_items = array_merge($merged_list_items, $matched_items);
562             }
563            
564             $output[] = $beginning_html . join("\n", $merged_list_items) . $ending_html;
565         }
566        
567         return $output;
568     }
569 }
570  
571  
572  
573 /**
574  * Copyright (c) 2007-2009 Will Bond <will@flourishlib.com>
575  *
576  * Permission is hereby granted, free of charge, to any person obtaining a copy
577  * of this software and associated documentation files (the "Software"), to deal
578  * in the Software without restriction, including without limitation the rights
579  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
580  * copies of the Software, and to permit persons to whom the Software is
581  * furnished to do so, subject to the following conditions:
582  *
583  * The above copyright notice and this permission notice shall be included in
584  * all copies or substantial portions of the Software.
585  *
586  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
587  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
588  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
589  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
590  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
591  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
592  * THE SOFTWARE.
593  */