/src/PieCrust/Data/DataFormatter.php

https://bitbucket.org/ndj/piecrust · PHP · 317 lines · 256 code · 33 blank · 28 comment · 38 complexity · 2ddfb0c7cfb62c953d16db4aa10e7e57 MD5 · raw file

  1. <?php
  2. namespace PieCrust\Data;
  3. use \ArrayAccess;
  4. use \ReflectionClass;
  5. use \ReflectionMethod;
  6. use \ReflectionProperty;
  7. /**
  8. * A class that formats rendering data to display in a debug window.
  9. */
  10. class DataFormatter
  11. {
  12. const MAX_VALUE_LENGTH = 150;
  13. protected $indent;
  14. public function __construct()
  15. {
  16. $this->indent = 0;
  17. }
  18. public function format($data)
  19. {
  20. $this->indent++;
  21. $treatAsObject = false;
  22. if (is_object($data))
  23. {
  24. $class = get_class($data);
  25. $r = new ReflectionClass($class);
  26. $docComment = $r->getDocComment();
  27. if (preg_match('/@formatObject/', $docComment))
  28. {
  29. $treatAsObject = true;
  30. }
  31. }
  32. if (is_array($data) || (!$treatAsObject and is_object($data) and $data instanceof ArrayAccess))
  33. {
  34. $this->formatArray($data);
  35. }
  36. else if ($treatAsObject || is_object($data))
  37. {
  38. $this->formatReflection($data);
  39. }
  40. else
  41. {
  42. if (is_null($data))
  43. {
  44. $strData = "<null>";
  45. }
  46. else if (is_bool($data))
  47. {
  48. $strData = ($data ? 'true' : 'false');
  49. }
  50. else
  51. {
  52. $strData = (string)$data;
  53. }
  54. if (filter_var($strData, FILTER_VALIDATE_URL) !== false)
  55. {
  56. // If the value is an URL, turn it into a link.
  57. echo '<a href="' . $strData . '" style="color: #afa;">' . htmlspecialchars($strData) . '</a>';
  58. }
  59. else
  60. {
  61. // Truncate the value if it's too long.
  62. if (strlen($strData) > self::MAX_VALUE_LENGTH)
  63. $strData = substr($strData, 0, self::MAX_VALUE_LENGTH - 5) . '[...]';
  64. echo htmlspecialchars($strData);
  65. }
  66. }
  67. $this->indent--;
  68. }
  69. protected function getIndent()
  70. {
  71. return str_repeat(' ', $this->indent);
  72. }
  73. protected function formatArray($data)
  74. {
  75. $includedObjectMethods = array();
  76. $includedObjectProperties = array();
  77. if (is_object($data))
  78. {
  79. // If this is an object that passes for an array, take some time to
  80. // look for some documentation string we may need to display.
  81. $class = get_class($data);
  82. $r = new ReflectionClass($class);
  83. $docComment = $r->getDocComment();
  84. $classParams = $this->getReflectionFormattingParameters($docComment);
  85. if ($classParams['documentation'])
  86. {
  87. echo $this->getIndent() . '<span style="' . DataStyles::CSS_DOC . '">' . $classParams['documentation'] . '</span>' . PHP_EOL;
  88. }
  89. // See if there are some methods that are explicitely included in
  90. // addition to its array contents.
  91. foreach ($r->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
  92. {
  93. $docComment = $method->getDocComment();
  94. $params = $this->getReflectionFormattingParameters($docComment);
  95. if ($params['include'])
  96. $includedObjectMethods[] = $method;
  97. }
  98. foreach ($r->getProperties(ReflectionProperty::IS_PUBLIC) as $property)
  99. {
  100. $docComment = $property->getDocComment();
  101. $params = $this->getReflectionFormattingParameters($docComment);
  102. if ($params['include'])
  103. $includedObjectProperties[] = $property;
  104. }
  105. }
  106. // Render the data!
  107. echo PHP_EOL . $this->getIndent() . '<div style="' . DataStyles::CSS_DATABLOCK . '">' . PHP_EOL;
  108. $this->indent++;
  109. // Render any included object methods or properties.
  110. if (count($includedObjectMethods) > 0 or
  111. count($includedObjectProperties) > 0)
  112. {
  113. foreach ($includedObjectMethods as $method)
  114. {
  115. $docComment = $method->getDocComment();
  116. $params = $this->getReflectionFormattingParameters($docComment);
  117. $this->formatObjectMethod($data, $method, $params);
  118. }
  119. foreach ($includedObjectProperties as $property)
  120. {
  121. $docComment = $property->getDocComment();
  122. $params = $this->getReflectionFormattingParameters($docComment);
  123. $this->formatObjectProperty($data, $property, $params);
  124. }
  125. }
  126. // Render the formatted array contents.
  127. foreach ($data as $key => $value)
  128. {
  129. echo $this->getIndent() . '<div style="' . DataStyles::CSS_VALUE . '">' . $key;
  130. if ($value !== null)
  131. {
  132. echo ' : ';
  133. $this->format($value);
  134. }
  135. echo $this->getIndent() . '</div>' . PHP_EOL;
  136. }
  137. $this->indent--;
  138. echo $this->getIndent() . '</div>' . PHP_EOL;
  139. }
  140. protected function formatReflection($data)
  141. {
  142. $class = get_class($data);
  143. $r = new ReflectionClass($class);
  144. // Display an optional documentation string.
  145. $docComment = $r->getDocComment();
  146. $classParams = $this->getReflectionFormattingParameters($docComment);
  147. if ($classParams['documentation'])
  148. {
  149. echo $this->getIndent() . '<span style="' . DataStyles::CSS_DOC . '">' . $classParams['documentation'] . '</span>' . PHP_EOL;
  150. }
  151. // Start inspecting the object's methods and properties, and format
  152. // them along the way.
  153. echo PHP_EOL . $this->getIndent() . '<div style="' . DataStyles::CSS_DATABLOCK . '">' . PHP_EOL;
  154. $this->indent++;
  155. foreach ($r->getMethods(ReflectionMethod::IS_PUBLIC) as $method)
  156. {
  157. // Filter special methods out.
  158. if ($method->isConstructor() or
  159. $method->isDestructor())
  160. continue;
  161. // Filter ignored or non-explicitely-included methods.
  162. $docComment = $method->getDocComment();
  163. $params = $this->getReflectionFormattingParameters($docComment);
  164. if ($params['ignore'])
  165. continue;
  166. if ($classParams['explicitInclude'] and !$params['include'])
  167. continue;
  168. $this->formatObjectMethod($data, $method, $params);
  169. }
  170. foreach ($r->getProperties(ReflectionProperty::IS_PUBLIC) as $property)
  171. {
  172. $docComment = $property->getDocComment();
  173. $params = $this->getReflectionFormattingParameters($docComment);
  174. if ($params['ignore'])
  175. continue;
  176. if ($classParams['explicitInclude'] and !$params['include'])
  177. continue;
  178. $this->formatObjectProperty($data, $property, $params);
  179. }
  180. $this->indent--;
  181. echo $this->getIndent() . '</div>' . PHP_EOL;
  182. }
  183. protected function formatObjectMethod($data, $method, $params)
  184. {
  185. // If this method can be called without arguments, and there's no
  186. // '@noCall' annotation on it, get the value so we can display it.
  187. // Otherwise, display only the method's signature.
  188. $value = null;
  189. $argCount = $method->getNumberOfRequiredParameters();
  190. if ($argCount == 0)
  191. {
  192. $name = strtolower($method->getName());
  193. if(!$params['noCall'])
  194. {
  195. $value = $method->invoke($data);
  196. }
  197. }
  198. else
  199. {
  200. $name = strtolower($method->getName()) . '(';
  201. $args = $method->getParameters();
  202. $firstArg = true;
  203. foreach ($args as $a)
  204. {
  205. if (!$firstArg)
  206. $name .= ', ';
  207. $firstArg = false;
  208. $name .= $a->getName();
  209. }
  210. $name .= ')';
  211. }
  212. echo $this->getIndent() . '<div style="' . DataStyles::CSS_VALUE . '">' . $name;
  213. if ($params['documentation'])
  214. {
  215. echo ' <span style="' . DataStyles::CSS_DOC . '">&ndash; ' . $params['documentation'] . '</span>' . PHP_EOL;
  216. }
  217. if ($value !== null)
  218. {
  219. echo ' : ' . PHP_EOL;
  220. $this->format($value);
  221. }
  222. echo $this->getIndent() . '</div>' . PHP_EOL;
  223. }
  224. protected function formatObjectProperty($data, $property, $params)
  225. {
  226. $name = $property->getName();
  227. // Only get the value of this property if there's no '@noCall'
  228. // annotation on it.
  229. $value = null;
  230. if (!$params['noCall'])
  231. {
  232. $value = $property->getValue($data);
  233. }
  234. echo $this->getIndent() . '<div style="' . DataStyles::CSS_VALUE . '">' . $name;
  235. if ($params['documentation'])
  236. {
  237. echo ' <span style="' . DataStyles::CSS_DOC . '">' . $params['documentation'] . '</span>' . PHP_EOL;
  238. }
  239. if ($value !== null)
  240. {
  241. echo ' : ' . PHP_EOL;
  242. $this->format($value);
  243. }
  244. echo $this->getIndent() . '</div>' . PHP_EOL;
  245. }
  246. protected function getReflectionFormattingParameters($docComments)
  247. {
  248. $params = array(
  249. 'ignore' => false,
  250. 'include' => false,
  251. 'documentation' => null,
  252. 'explicitInclude' => false,
  253. 'noCall' => false
  254. );
  255. if (preg_match('/@ignore/', $docComments))
  256. {
  257. // Don't include this property/method.
  258. $params['ignore'] = true;
  259. }
  260. if (preg_match('/@include/', $docComments))
  261. {
  262. // Do include this property/method (for use with '@explicitInclude').
  263. $params['include'] = true;
  264. }
  265. if (preg_match('/@explicitInclude/', $docComments))
  266. {
  267. // Declare that only properties/methods with '@include' should be
  268. // formatted.
  269. $params['explicitInclude'] = true;
  270. }
  271. if (preg_match('/@noCall/', $docComments))
  272. {
  273. // Don't call into a method, or don't get a property's value.
  274. $params['noCall'] = true;
  275. }
  276. $m = array();
  277. if (preg_match('/@documentation\s+(.*)/m', $docComments, $m))
  278. {
  279. // A documentation string to display.
  280. $params['documentation'] = $m[1];
  281. }
  282. return $params;
  283. }
  284. }