PageRenderTime 98ms CodeModel.GetById 26ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fException.php

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