Inspector.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\analysis;
  9. use Exception;
  10. use ReflectionClass;
  11. use ReflectionProperty;
  12. use ReflectionException;
  13. use SplFileObject;
  14. use lithium\core\Libraries;
  15. /**
  16. * General source code inspector.
  17. *
  18. * This inspector provides a simple interface to the PHP Reflection API that
  19. * can be used to gather information about any PHP source file for purposes of
  20. * test metrics or static analysis.
  21. */
  22. class Inspector extends \lithium\core\StaticObject {
  23. /**
  24. * classes used
  25. *
  26. * @var array
  27. */
  28. protected static $_classes = array(
  29. 'collection' => 'lithium\util\Collection'
  30. );
  31. /**
  32. * Maps reflect method names to result array keys.
  33. *
  34. * @var array
  35. */
  36. protected static $_methodMap = array(
  37. 'name' => 'getName',
  38. 'start' => 'getStartLine',
  39. 'end' => 'getEndLine',
  40. 'file' => 'getFileName',
  41. 'comment' => 'getDocComment',
  42. 'namespace' => 'getNamespaceName',
  43. 'shortName' => 'getShortName'
  44. );
  45. /**
  46. * Will determine if a method can be called.
  47. *
  48. * @param string|object $class Class to inspect.
  49. * @param string $method Method name.
  50. * @param bool $internal Interal call or not.
  51. * @return bool
  52. */
  53. public static function isCallable($object, $method, $internal = false) {
  54. $methodExists = method_exists($object, $method);
  55. $callable = function($object, $method) {
  56. return is_callable(array($object, $method));
  57. };
  58. return $internal ? $methodExists : $methodExists && $callable($object, $method);
  59. }
  60. /**
  61. * Determines if a given $identifier is a class property, a class method, a class itself,
  62. * or a namespace identifier.
  63. *
  64. * @param string $identifier The identifier to be analyzed
  65. * @return string Identifier type. One of `property`, `method`, `class` or `namespace`.
  66. */
  67. public static function type($identifier) {
  68. $identifier = ltrim($identifier, '\\');
  69. if (strpos($identifier, '::')) {
  70. return (strpos($identifier, '$') !== false) ? 'property' : 'method';
  71. }
  72. if (is_readable(Libraries::path($identifier))) {
  73. if (class_exists($identifier) && in_array($identifier, get_declared_classes())) {
  74. return 'class';
  75. }
  76. }
  77. return 'namespace';
  78. }
  79. /**
  80. * Detailed source code identifier analysis
  81. *
  82. * Analyzes a passed $identifier for more detailed information such
  83. * as method/property modifiers (e.g. `public`, `private`, `abstract`)
  84. *
  85. * @param string $identifier The identifier to be analyzed
  86. * @param array $info Optionally restrict or expand the default information
  87. * returned from the `info` method. By default, the information returned
  88. * is the same as the array keys contained in the `$_methodMap` property of
  89. * Inspector.
  90. * @return array An array of the parsed meta-data information of the given identifier.
  91. */
  92. public static function info($identifier, $info = array()) {
  93. $info = $info ?: array_keys(static::$_methodMap);
  94. $type = static::type($identifier);
  95. $result = array();
  96. $class = null;
  97. if ($type === 'method' || $type === 'property') {
  98. list($class, $identifier) = explode('::', $identifier);
  99. try {
  100. $classInspector = new ReflectionClass($class);
  101. } catch (Exception $e) {
  102. return null;
  103. }
  104. if ($type === 'property') {
  105. $identifier = substr($identifier, 1);
  106. $accessor = 'getProperty';
  107. } else {
  108. $identifier = str_replace('()', '', $identifier);
  109. $accessor = 'getMethod';
  110. }
  111. try {
  112. $inspector = $classInspector->{$accessor}($identifier);
  113. } catch (Exception $e) {
  114. return null;
  115. }
  116. $result['modifiers'] = static::_modifiers($inspector);
  117. } elseif ($type === 'class') {
  118. $inspector = new ReflectionClass($identifier);
  119. } else {
  120. return null;
  121. }
  122. foreach ($info as $key) {
  123. if (!isset(static::$_methodMap[$key])) {
  124. continue;
  125. }
  126. if (method_exists($inspector, static::$_methodMap[$key])) {
  127. $setAccess = (
  128. ($type === 'method' || $type === 'property') &&
  129. array_intersect($result['modifiers'], array('private', 'protected')) != array()
  130. && method_exists($inspector, 'setAccessible')
  131. );
  132. if ($setAccess) {
  133. $inspector->setAccessible(true);
  134. }
  135. $result[$key] = $inspector->{static::$_methodMap[$key]}();
  136. if ($setAccess) {
  137. $inspector->setAccessible(false);
  138. $setAccess = false;
  139. }
  140. }
  141. }
  142. if ($type === 'property' && !$classInspector->isAbstract()) {
  143. $inspector->setAccessible(true);
  144. try {
  145. $result['value'] = $inspector->getValue(static::_class($class));
  146. } catch (Exception $e) {
  147. return null;
  148. }
  149. }
  150. if (isset($result['start']) && isset($result['end'])) {
  151. $result['length'] = $result['end'] - $result['start'];
  152. }
  153. if (isset($result['comment'])) {
  154. $result += Docblock::comment($result['comment']);
  155. }
  156. return $result;
  157. }
  158. /**
  159. * Gets the executable lines of a class, by examining the start and end lines of each method.
  160. *
  161. * @param mixed $class Class name as a string or object instance.
  162. * @param array $options Set of options:
  163. * - `'self'` _boolean_: If `true` (default), only returns lines of methods defined in
  164. * `$class`, excluding methods from inherited classes.
  165. * - `'methods'` _array_: An arbitrary list of methods to search, as a string (single
  166. * method name) or array of method names.
  167. * - `'filter'` _boolean_: If `true`, filters out lines containing only whitespace or
  168. * braces. Note: for some reason, the Zend engine does not report `switch` and `try`
  169. * statements as executable lines, as well as parts of multi-line assignment
  170. * statements, so they are filtered out as well.
  171. * @return array Returns an array of the executable line numbers of the class.
  172. */
  173. public static function executable($class, array $options = array()) {
  174. $defaults = array(
  175. 'self' => true,
  176. 'filter' => true,
  177. 'methods' => array(),
  178. 'empty' => array(' ', "\t", '}', ')', ';'),
  179. 'pattern' => null,
  180. 'blockOpeners' => array('switch (', 'try {', '} else {', 'do {', '} while')
  181. );
  182. $options += $defaults;
  183. if (empty($options['pattern']) && $options['filter']) {
  184. $pattern = str_replace(' ', '\s*', join('|', array_map(
  185. function($str) { return preg_quote($str, '/'); },
  186. $options['blockOpeners']
  187. )));
  188. $pattern = join('|', array(
  189. "({$pattern})",
  190. "\\$(.+)\($",
  191. "\s*['\"]\w+['\"]\s*=>\s*.+[\{\(]$",
  192. "\s*['\"]\w+['\"]\s*=>\s*['\"]*.+['\"]*\s*"
  193. ));
  194. $options['pattern'] = "/^({$pattern})/";
  195. }
  196. if (!$class instanceof ReflectionClass) {
  197. $class = new ReflectionClass(is_object($class) ? get_class($class) : $class);
  198. }
  199. $options += array('group' => false);
  200. $result = array_filter(static::methods($class, 'ranges', $options));
  201. if ($options['filter'] && $class->getFileName() && $result) {
  202. $lines = static::lines($class->getFileName(), $result);
  203. $start = key($lines);
  204. $code = implode("\n", $lines);
  205. $tokens = token_get_all('<' . '?php' . $code);
  206. $tmp = array();
  207. foreach ($tokens as $token) {
  208. if (is_array($token)) {
  209. if (!in_array($token[0], array(T_COMMENT, T_DOC_COMMENT, T_WHITESPACE))) {
  210. $tmp[] = $token[2];
  211. }
  212. }
  213. }
  214. $filteredLines = array_values(array_map(
  215. function($ln) use ($start) { return $ln + $start - 1; },
  216. array_unique($tmp))
  217. );
  218. $lines = array_intersect_key($lines, array_flip($filteredLines));
  219. $result = array_keys(array_filter($lines, function($line) use ($options) {
  220. $line = trim($line);
  221. $empty = preg_match($options['pattern'], $line);
  222. return $empty ? false : (str_replace($options['empty'], '', $line) !== '');
  223. }));
  224. }
  225. return $result;
  226. }
  227. /**
  228. * Returns various information on the methods of an object, in different formats.
  229. *
  230. * @param mixed $class A string class name or an object instance, from which to get methods.
  231. * @param string $format The type and format of data to return. Available options are:
  232. * - `null`: Returns a `Collection` object containing a `ReflectionMethod` instance
  233. * for each method.
  234. * - `'extents'`: Returns a two-dimensional array with method names as keys, and
  235. * an array with starting and ending line numbers as values.
  236. * - `'ranges'`: Returns a two-dimensional array where each key is a method name,
  237. * and each value is an array of line numbers which are contained in the method.
  238. * @param array $options
  239. * @return mixed array|null|object
  240. */
  241. public static function methods($class, $format = null, array $options = array()) {
  242. $defaults = array('methods' => array(), 'group' => true, 'self' => true);
  243. $options += $defaults;
  244. if (!(is_object($class) && $class instanceof ReflectionClass)) {
  245. try {
  246. $class = new ReflectionClass($class);
  247. } catch (ReflectionException $e) {
  248. return null;
  249. }
  250. }
  251. $options += array('names' => $options['methods']);
  252. $methods = static::_items($class, 'getMethods', $options);
  253. $result = array();
  254. switch ($format) {
  255. case null:
  256. return $methods;
  257. case 'extents':
  258. if ($methods->getName() == array()) {
  259. return array();
  260. }
  261. $extents = function($start, $end) { return array($start, $end); };
  262. $result = array_combine($methods->getName(), array_map(
  263. $extents, $methods->getStartLine(), $methods->getEndLine()
  264. ));
  265. break;
  266. case 'ranges':
  267. $ranges = function($lines) {
  268. list($start, $end) = $lines;
  269. return ($end <= $start + 1) ? array() : range($start + 1, $end - 1);
  270. };
  271. $result = array_map($ranges, static::methods(
  272. $class, 'extents', array('group' => true) + $options
  273. ));
  274. break;
  275. }
  276. if ($options['group']) {
  277. return $result;
  278. }
  279. $tmp = $result;
  280. $result = array();
  281. array_map(function($ln) use (&$result) { $result = array_merge($result, $ln); }, $tmp);
  282. return $result;
  283. }
  284. /**
  285. * Returns various information on the properties of an object.
  286. *
  287. * @param mixed $class A string class name or an object instance, from which to get methods.
  288. * @param array $options Set of options:
  289. * -'self': If true (default), only returns properties defined in `$class`,
  290. * excluding properties from inherited classes.
  291. * @return mixed object lithium\analysis\Inspector._items.map|null
  292. */
  293. public static function properties($class, array $options = array()) {
  294. $defaults = array('properties' => array(), 'self' => true);
  295. $options += $defaults;
  296. if (!(is_object($class) && $class instanceof ReflectionClass)) {
  297. try {
  298. $class = new ReflectionClass($class);
  299. } catch (ReflectionException $e) {
  300. return null;
  301. }
  302. }
  303. $options += array('names' => $options['properties']);
  304. return static::_items($class, 'getProperties', $options)->map(function($item) {
  305. $class = __CLASS__;
  306. $modifiers = array_values($class::invokeMethod('_modifiers', array($item)));
  307. $setAccess = (
  308. array_intersect($modifiers, array('private', 'protected')) != array()
  309. );
  310. if ($setAccess) {
  311. $item->setAccessible(true);
  312. }
  313. $result = compact('modifiers') + array(
  314. 'docComment' => $item->getDocComment(),
  315. 'name' => $item->getName(),
  316. 'value' => $item->getValue($item->getDeclaringClass())
  317. );
  318. if ($setAccess) {
  319. $item->setAccessible(false);
  320. }
  321. return $result;
  322. }, array('collect' => false));
  323. }
  324. /**
  325. * Returns an array of lines from a file, class, or arbitrary string, where $data is the data
  326. * to read the lines from and $lines is an array of line numbers specifying which lines should
  327. * be read.
  328. *
  329. * @param string $data If `$data` contains newlines, it will be read from directly, and have
  330. * its own lines returned. If `$data` is a physical file path, that file will be
  331. * read and have its lines returned. If `$data` is a class name, it will be
  332. * converted into a physical file path and read.
  333. * @param array $lines The array of lines to read. If a given line is not present in the data,
  334. * it will be silently ignored.
  335. * @return array Returns an array where the keys are matching `$lines`, and the values are the
  336. * corresponding line numbers in `$data`.
  337. * @todo Add an $options parameter with a 'context' flag, to pull in n lines of context.
  338. */
  339. public static function lines($data, $lines) {
  340. $c = array();
  341. if (strpos($data, PHP_EOL) !== false) {
  342. $c = explode(PHP_EOL, PHP_EOL . $data);
  343. } else {
  344. if (!file_exists($data)) {
  345. $data = Libraries::path($data);
  346. if (!file_exists($data)) {
  347. return null;
  348. }
  349. }
  350. $file = new SplFileObject($data);
  351. foreach ($file as $current) {
  352. $c[$file->key() + 1] = rtrim($file->current());
  353. }
  354. }
  355. if (!count($c) || !count($lines)) {
  356. return null;
  357. }
  358. return array_intersect_key($c, array_combine($lines, array_fill(0, count($lines), null)));
  359. }
  360. /**
  361. * Gets the full inheritance list for the given class.
  362. *
  363. * @param string $class Class whose inheritance chain will be returned
  364. * @param array $options Option consists of:
  365. * - `'autoLoad'` _boolean_: Whether or not to call `__autoload` by default. Defaults
  366. * to `true`.
  367. * @return array An array of the name of the parent classes of the passed `$class` parameter,
  368. * or `false` on error.
  369. * @link http://php.net/manual/en/function.class-parents.php PHP Manual: `class_parents()`.
  370. */
  371. public static function parents($class, array $options = array()) {
  372. $defaults = array('autoLoad' => false);
  373. $options += $defaults;
  374. $class = is_object($class) ? get_class($class) : $class;
  375. if (!class_exists($class, $options['autoLoad'])) {
  376. return false;
  377. }
  378. return class_parents($class);
  379. }
  380. /**
  381. * Gets an array of classes and their corresponding definition files, or examines a file and
  382. * returns the classes it defines.
  383. *
  384. * @param array $options
  385. * @return array Associative of classes and their corresponding definition files
  386. * @todo Document valid options
  387. */
  388. public static function classes(array $options = array()) {
  389. $defaults = array('group' => 'classes', 'file' => null);
  390. $options += $defaults;
  391. $list = get_declared_classes();
  392. $files = get_included_files();
  393. $classes = array();
  394. if ($file = $options['file']) {
  395. $loaded = static::_instance('collection', array('data' => array_map(
  396. function($class) { return new ReflectionClass($class); }, $list
  397. )));
  398. $classFiles = $loaded->getFileName();
  399. if (in_array($file, $files) && !in_array($file, $classFiles)) {
  400. return array();
  401. }
  402. if (!in_array($file, $classFiles)) {
  403. include $file;
  404. $list = array_diff(get_declared_classes(), $list);
  405. } else {
  406. $filter = function($class) use ($file) { return $class->getFileName() === $file; };
  407. $list = $loaded->find($filter)->map(function ($class) {
  408. return $class->getName() ?: $class->name;
  409. }, array('collect' => false));
  410. }
  411. }
  412. foreach ($list as $class) {
  413. $inspector = new ReflectionClass($class);
  414. if ($options['group'] === 'classes') {
  415. $inspector->getFileName() ? $classes[$class] = $inspector->getFileName() : null;
  416. } elseif ($options['group'] === 'files') {
  417. $classes[$inspector->getFileName()][] = $inspector;
  418. }
  419. }
  420. return $classes;
  421. }
  422. /**
  423. * Gets the static and dynamic dependencies for a class or group of classes.
  424. *
  425. * @param mixed $classes Either a string specifying a class, or a numerically indexed array
  426. * of classes
  427. * @param array $options
  428. * @return array An array of the static and dynamic class dependencies
  429. * @todo Document valid options
  430. */
  431. public static function dependencies($classes, array $options = array()) {
  432. $defaults = array('type' => null);
  433. $options += $defaults;
  434. $static = $dynamic = array();
  435. $trim = function($c) { return trim(trim($c, '\\')); };
  436. $join = function ($i) { return join('', $i); };
  437. foreach ((array) $classes as $class) {
  438. $data = explode("\n", file_get_contents(Libraries::path($class)));
  439. $data = "<?php \n" . join("\n", preg_grep('/^\s*use /', $data)) . "\n ?>";
  440. $classes = array_map($join, Parser::find($data, 'use *;', array(
  441. 'return' => 'content',
  442. 'lineBreaks' => true,
  443. 'startOfLine' => true,
  444. 'capture' => array('T_STRING', 'T_NS_SEPARATOR')
  445. )));
  446. if ($classes) {
  447. $static = array_unique(array_merge($static, array_map($trim, $classes)));
  448. }
  449. $classes = static::info($class . '::$_classes', array('value'));
  450. if (isset($classes['value'])) {
  451. $dynamic = array_merge($dynamic, array_map($trim, array_values($classes['value'])));
  452. }
  453. }
  454. if (empty($options['type'])) {
  455. return array_unique(array_merge($static, $dynamic));
  456. }
  457. $type = $options['type'];
  458. return isset(${$type}) ? ${$type} : null;
  459. }
  460. /**
  461. * Returns an instance of the given class without directly instantiating it. Inspired by the
  462. * work of Sebastian Bergmann on the PHP Object Freezer project.
  463. *
  464. * @link http://sebastian-bergmann.de/archives/831-Freezing-and-Thawing-PHP-Objects.html
  465. * Freezing and Thawing PHP Objects
  466. * @param string $class The name of the class to return an instance of.
  467. * @return object Returns an instance of the object given by `$class` without calling that
  468. * class' constructor.
  469. */
  470. protected static function _class($class) {
  471. if (!class_exists($class)) {
  472. throw new RuntimeException(sprintf('Class `%s` could not be found.', $class));
  473. }
  474. return unserialize(sprintf('O:%d:"%s":0:{}', strlen($class), $class));
  475. }
  476. /**
  477. * Helper method to get an array of `ReflectionMethod` or `ReflectionProperty` objects, wrapped
  478. * in a `Collection` object, and filtered based on a set of options.
  479. *
  480. * @param ReflectionClass $class A reflection class instance from which to fetch.
  481. * @param string $method A getter method to call on the `ReflectionClass` instance, which will
  482. * return an array of items, i.e. `'getProperties'` or `'getMethods'`.
  483. * @param array $options The options used to filter the resulting method list.
  484. * @return object Returns a `Collection` object instance containing the results of the items
  485. * returned from the call to the method specified in `$method`, after being passed
  486. * through the filters specified in `$options`.
  487. */
  488. protected static function _items($class, $method, $options) {
  489. $defaults = array('names' => array(), 'self' => true, 'public' => true);
  490. $options += $defaults;
  491. $params = array(
  492. 'getProperties' => ReflectionProperty::IS_PUBLIC | (
  493. $options['public'] ? 0 : ReflectionProperty::IS_PROTECTED
  494. )
  495. );
  496. $data = isset($params[$method]) ? $class->{$method}($params[$method]) : $class->{$method}();
  497. if (!empty($options['names'])) {
  498. $data = array_filter($data, function($item) use ($options) {
  499. return in_array($item->getName(), (array) $options['names']);
  500. });
  501. }
  502. if ($options['self']) {
  503. $data = array_filter($data, function($item) use ($class) {
  504. return ($item->getDeclaringClass()->getName() === $class->getName());
  505. });
  506. }
  507. if ($options['public']) {
  508. $data = array_filter($data, function($item) { return $item->isPublic(); });
  509. }
  510. return static::_instance('collection', compact('data'));
  511. }
  512. /**
  513. * Helper method to determine if a class applies to a list of modifiers.
  514. *
  515. * @param string $inspector ReflectionClass instance.
  516. * @param array|string $list List of modifiers to test.
  517. * @return boolean Test result.
  518. */
  519. protected static function _modifiers($inspector, $list = array()) {
  520. $list = $list ?: array('public', 'private', 'protected', 'abstract', 'final', 'static');
  521. return array_filter($list, function($modifier) use ($inspector) {
  522. $method = 'is' . ucfirst($modifier);
  523. return (method_exists($inspector, $method) && $inspector->{$method}());
  524. });
  525. }
  526. }
  527. ?>