PageRenderTime 44ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 1ms

/analysis/Inspector.php

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