'lithium\util\Collection' ]; /** * Maps reflect method names to result array keys. * * @var array */ protected static $_methodMap = [ 'name' => 'getName', 'start' => 'getStartLine', 'end' => 'getEndLine', 'file' => 'getFileName', 'comment' => 'getDocComment', 'namespace' => 'getNamespaceName', 'shortName' => 'getShortName' ]; /** * Determines if a given method can be called on an object/class. * * @param string|object $object Class or instance to inspect. * @param string $method Name of the method. * @param boolean $internal Should be `true` if you want to check from inside the * class/object. When `false` will also check for public visibility, * defaults to `false`. * @return boolean Returns `true` if the method can be called, `false` otherwise. */ public static function isCallable($object, $method, $internal = false) { $methodExists = method_exists($object, $method); return $internal ? $methodExists : $methodExists && is_callable([$object, $method]); } /** * Determines if a given $identifier is a class property, a class method, a class itself, * or a namespace identifier. * * @param string $identifier The identifier to be analyzed * @return string Identifier type. One of `property`, `method`, `class` or `namespace`. */ public static function type($identifier) { $identifier = ltrim($identifier, '\\'); if (strpos($identifier, '::')) { return (strpos($identifier, '$') !== false) ? 'property' : 'method'; } if (is_readable(Libraries::path($identifier))) { if (class_exists($identifier) && in_array($identifier, get_declared_classes())) { return 'class'; } } return 'namespace'; } /** * Detailed source code identifier analysis. * * Analyzes a passed $identifier for more detailed information such * as method/property modifiers (e.g. `public`, `private`, `abstract`) * * @param string $identifier The identifier to be analyzed * @param array $info Optionally restrict or expand the default information * returned from the `info` method. By default, the information returned * is the same as the array keys contained in the `$_methodMap` property of * Inspector. * @return array An array of the parsed meta-data information of the given identifier. */ public static function info($identifier, $info = []) { $info = $info ?: array_keys(static::$_methodMap); $type = static::type($identifier); $result = []; $class = null; if ($type === 'method' || $type === 'property') { list($class, $identifier) = explode('::', $identifier); try { $classInspector = new ReflectionClass($class); } catch (Exception $e) { return null; } if ($type === 'property') { $identifier = substr($identifier, 1); $accessor = 'getProperty'; } else { $identifier = str_replace('()', '', $identifier); $accessor = 'getMethod'; } try { $inspector = $classInspector->{$accessor}($identifier); } catch (Exception $e) { return null; } $result['modifiers'] = static::_modifiers($inspector); } elseif ($type === 'class') { $inspector = new ReflectionClass($identifier); $classInspector = null; } else { return null; } foreach ($info as $key) { if (!isset(static::$_methodMap[$key])) { continue; } if (method_exists($inspector, static::$_methodMap[$key])) { $setAccess = ( ($type === 'method' || $type === 'property') && array_intersect($result['modifiers'], ['private', 'protected']) !== [] && method_exists($inspector, 'setAccessible') ); if ($setAccess) { $inspector->setAccessible(true); } $result[$key] = $inspector->{static::$_methodMap[$key]}(); if ($setAccess) { $inspector->setAccessible(false); } } } if ($type === 'property' && $classInspector && !$classInspector->isAbstract()) { $inspector->setAccessible(true); try { $result['value'] = $inspector->getValue(static::_class($class)); } catch (Exception $e) { return null; } } if (isset($result['start']) && isset($result['end'])) { $result['length'] = $result['end'] - $result['start']; } if (isset($result['comment'])) { $result += Docblock::comment($result['comment']); } return $result; } /** * Gets the executable lines of a class, by examining the start and end lines of each method. * * @param mixed $class Class name as a string or object instance. * @param array $options Set of options: * - `'self'` _boolean_: If `true` (default), only returns lines of methods defined in * `$class`, excluding methods from inherited classes. * - `'methods'` _array_: An arbitrary list of methods to search, as a string (single * method name) or array of method names. * - `'filter'` _boolean_: If `true`, filters out lines containing only whitespace or * braces. Note: for some reason, the Zend engine does not report `switch` and `try` * statements as executable lines, as well as parts of multi-line assignment * statements, so they are filtered out as well. * @return array Returns an array of the executable line numbers of the class. */ public static function executable($class, array $options = []) { $defaults = [ 'self' => true, 'filter' => true, 'methods' => [], 'empty' => [' ', "\t", '}', ')', ';'], 'pattern' => null, 'blockOpeners' => ['switch (', 'try {', '} else {', 'do {', '} while'] ]; $options += $defaults; if (empty($options['pattern']) && $options['filter']) { $pattern = str_replace(' ', '\s*', join('|', array_map( function($str) { return preg_quote($str, '/'); }, $options['blockOpeners'] ))); $pattern = join('|', [ "({$pattern})", "\\$(.+)\($", "\s*['\"]\w+['\"]\s*=>\s*.+[\{\(]$", "\s*['\"]\w+['\"]\s*=>\s*['\"]*.+['\"]*\s*" ]); $options['pattern'] = "/^({$pattern})/"; } if (!$class instanceof ReflectionClass) { $class = new ReflectionClass(is_object($class) ? get_class($class) : $class); } $options += ['group' => false]; $result = array_filter(static::methods($class, 'ranges', $options)); if ($options['filter'] && $class->getFileName() && $result) { $lines = static::lines($class->getFileName(), $result); $start = key($lines); $code = implode("\n", $lines); $tokens = token_get_all('<' . '?php' . $code); $tmp = []; foreach ($tokens as $token) { if (is_array($token)) { if (!in_array($token[0], [T_COMMENT, T_DOC_COMMENT, T_WHITESPACE])) { $tmp[] = $token[2]; } } } $filteredLines = array_values(array_map( function($ln) use ($start) { return $ln + $start - 1; }, array_unique($tmp)) ); $lines = array_intersect_key($lines, array_flip($filteredLines)); $result = array_keys(array_filter($lines, function($line) use ($options) { $line = trim($line); $empty = preg_match($options['pattern'], $line); return $empty ? false : (str_replace($options['empty'], '', $line) !== ''); })); } return $result; } /** * Returns various information on the methods of an object, in different formats. * * @param string|object $class A string class name for purely static classes or an object * instance, from which to get methods. * @param string $format The type and format of data to return. Available options are: * - `null`: Returns a `Collection` object containing a `ReflectionMethod` instance * for each method. * - `'extents'`: Returns a two-dimensional array with method names as keys, and * an array with starting and ending line numbers as values. * - `'ranges'`: Returns a two-dimensional array where each key is a method name, * and each value is an array of line numbers which are contained in the method. * @param array $options Set of options applied directly (check `_items()` for more options): * - `'methods'` _array_: An arbitrary list of methods to search, as a string (single * method name) or array of method names. * - `'group'`: If true (default) the array is grouped by context (ex.: method name), if * false the results are sequentially appended to an array. * -'self': If true (default), only returns properties defined in `$class`, * excluding properties from inherited classes. * @return mixed Return value depends on the $format given: * - `null` on failure. * - `lithium\util\Collection` if $format is `null` * - `array` if $format is either `'extends'` or `'ranges'`. */ public static function methods($class, $format = null, array $options = []) { $defaults = ['methods' => [], 'group' => true, 'self' => true]; $options += $defaults; if (!(is_object($class) && $class instanceof ReflectionClass)) { try { $class = new ReflectionClass($class); } catch (ReflectionException $e) { return null; } } $options += ['names' => $options['methods']]; $methods = static::_items($class, 'getMethods', $options); $result = []; switch ($format) { case null: return $methods; case 'extents': if ($methods->getName() === []) { return []; } $extents = function($start, $end) { return [$start, $end]; }; $result = array_combine($methods->getName(), array_map( $extents, $methods->getStartLine(), $methods->getEndLine() )); break; case 'ranges': $ranges = function($lines) { list($start, $end) = $lines; return ($end <= $start + 1) ? [] : range($start + 1, $end - 1); }; $result = array_map($ranges, static::methods( $class, 'extents', ['group' => true] + $options )); break; } if ($options['group']) { return $result; } $tmp = $result; $result = []; array_map(function($ln) use (&$result) { $result = array_merge($result, $ln); }, $tmp); return $result; } /** * Returns various information on the properties of an object. * * @param string|object $class A string class name for purely static classes or an object * instance, from which to get properties. * @param array $options Set of options applied directly (check `_items()` for more options): * - `'properties'`: array of properties to gather information from. * - `'self'`: If true (default), only returns properties defined in `$class`, * excluding properties from inherited classes. * @return mixed Returns an array with information about the properties from the class given in * $class or null on error. */ public static function properties($class, array $options = []) { $defaults = ['properties' => [], 'self' => true]; $options += $defaults; try { $reflClass = new ReflectionClass($class); } catch (ReflectionException $e) { return null; } $options += ['names' => $options['properties']]; return static::_items($reflClass, 'getProperties', $options)->map(function($item) use ($class) { $modifiers = array_values(static::_modifiers($item)); $setAccess = ( array_intersect($modifiers, ['private', 'protected']) !== [] ); if ($setAccess) { $item->setAccessible(true); } if (is_string($class)) { if (!$item->isStatic()) { $message = 'Must provide an object instance for non-static properties.'; throw new InvalidArgumentException($message); } $value = $item->getValue($item->getDeclaringClass()); } else { $value = $item->getValue($class); } $result = compact('modifiers', 'value') + [ 'docComment' => $item->getDocComment(), 'name' => $item->getName() ]; if ($setAccess) { $item->setAccessible(false); } return $result; }, ['collect' => false]); } /** * Returns an array of lines from a file, class, or arbitrary string, where $data is the data * to read the lines from and $lines is an array of line numbers specifying which lines should * be read. * * @param string $data If `$data` contains newlines, it will be read from directly, and have * its own lines returned. If `$data` is a physical file path, that file will be * read and have its lines returned. If `$data` is a class name, it will be * converted into a physical file path and read. * @param array $lines The array of lines to read. If a given line is not present in the data, * it will be silently ignored. * @return array Returns an array where the keys are matching `$lines`, and the values are the * corresponding line numbers in `$data`. * @todo Add an $options parameter with a 'context' flag, to pull in n lines of context. */ public static function lines($data, $lines) { $c = []; if (strpos($data, PHP_EOL) !== false) { $c = explode(PHP_EOL, PHP_EOL . $data); } else { if (!file_exists($data)) { $data = Libraries::path($data); if (!file_exists($data)) { return null; } } $file = new SplFileObject($data); foreach ($file as $current) { $c[$file->key() + 1] = rtrim($file->current()); } } if (!count($c) || !count($lines)) { return null; } return array_intersect_key($c, array_combine($lines, array_fill(0, count($lines), null))); } /** * Gets the full inheritance list for the given class. * * @param string $class Class whose inheritance chain will be returned * @param array $options Option consists of: * - `'autoLoad'` _boolean_: Whether or not to call `__autoload` by default. Defaults * to `true`. * @return array An array of the name of the parent classes of the passed `$class` parameter, * or `false` on error. * @link http://php.net/function.class-parents.php PHP Manual: `class_parents()`. */ public static function parents($class, array $options = []) { $defaults = ['autoLoad' => false]; $options += $defaults; $class = is_object($class) ? get_class($class) : $class; if (!class_exists($class, $options['autoLoad'])) { return false; } return class_parents($class); } /** * Gets an array of classes and their corresponding definition files, or examines a file and * returns the classes it defines. * * @param array $options Option consists of: * - `'group'`: Can be `classes` for grouping by class name or `files` for grouping by * filename. * - `'file': Valid file path for inspecting the containing classes. * @return array Associative of classes and their corresponding definition files */ public static function classes(array $options = []) { $defaults = ['group' => 'classes', 'file' => null]; $options += $defaults; $list = get_declared_classes(); $files = get_included_files(); $classes = []; if ($file = $options['file']) { $loaded = Libraries::instance(null, 'collection', ['data' => array_map( function($class) { return new ReflectionClass($class); }, $list )], static::$_classes); $classFiles = $loaded->getFileName(); if (in_array($file, $files) && !in_array($file, $classFiles)) { return []; } if (!in_array($file, $classFiles)) { include $file; $list = array_diff(get_declared_classes(), $list); } else { $filter = function($class) use ($file) { return $class->getFileName() === $file; }; $list = $loaded->find($filter)->map(function ($class) { return $class->getName() ?: $class->name; }, ['collect' => false]); } } foreach ($list as $class) { $inspector = new ReflectionClass($class); if ($options['group'] === 'classes') { $inspector->getFileName() ? $classes[$class] = $inspector->getFileName() : null; } elseif ($options['group'] === 'files') { $classes[$inspector->getFileName()][] = $inspector; } } return $classes; } /** * Gets the static and dynamic dependencies for a class or group of classes. * * @param mixed $classes Either a string specifying a class, or a numerically indexed array * of classes * @param array $options Option consists of: * - `'type'`: The type of dependency to check: `static` for static dependencies, * `dynamic`for dynamic dependencies or `null` for both merged in the same array. * Defaults to `null`. * @return array An array of the static and dynamic class dependencies or each if `type` is * defined in $options. */ public static function dependencies($classes, array $options = []) { $defaults = ['type' => null]; $options += $defaults; $static = $dynamic = []; $trim = function($c) { return trim(trim($c, '\\')); }; $join = function($i) { return join('', $i); }; foreach ((array) $classes as $class) { $data = explode("\n", file_get_contents(Libraries::path($class))); $data = ""; $classes = array_map($join, Parser::find($data, 'use *;', [ 'return' => 'content', 'lineBreaks' => true, 'startOfLine' => true, 'capture' => ['T_STRING', 'T_NS_SEPARATOR'] ])); if ($classes) { $static = array_unique(array_merge($static, array_map($trim, $classes))); } $classes = static::info($class . '::$_classes', ['value']); if (isset($classes['value'])) { $dynamic = array_merge($dynamic, array_map($trim, array_values($classes['value']))); } } if (empty($options['type'])) { return array_unique(array_merge($static, $dynamic)); } $type = $options['type']; return isset(${$type}) ? ${$type} : null; } /** * Returns an instance of the given class without directly instantiating it. Inspired by the * work of Sebastian Bergmann on the PHP Object Freezer project. * * @link http://sebastian-bergmann.de/archives/831-Freezing-and-Thawing-PHP-Objects.html * Freezing and Thawing PHP Objects * @param string $class The name of the class to return an instance of. * @return object Returns an instance of the object given by `$class` without calling that * class' constructor. */ protected static function _class($class) { if (!class_exists($class)) { throw new RuntimeException(sprintf('Class `%s` could not be found.', $class)); } return unserialize(sprintf('O:%d:"%s":0:{}', strlen($class), $class)); } /** * Helper method to get an array of `ReflectionMethod` or `ReflectionProperty` objects, wrapped * in a `Collection` object, and filtered based on a set of options. * * @param ReflectionClass $class A reflection class instance from which to fetch. * @param string $method A getter method to call on the `ReflectionClass` instance, which will * return an array of items, i.e. `'getProperties'` or `'getMethods'`. * @param array $options The options used to filter the resulting method list. * - `'names'`: array of properties for filtering the result. * - `'self'`: If true (default), only returns properties defined in `$class`, * excluding properties from inherited classes. * - `'public'`: If true (default) forces the property to be recognized as public. * @return object Returns a `Collection` object instance containing the results of the items * returned from the call to the method specified in `$method`, after being passed * through the filters specified in `$options`. */ protected static function _items($class, $method, $options) { $defaults = ['names' => [], 'self' => true, 'public' => true]; $options += $defaults; $params = [ 'getProperties' => ReflectionProperty::IS_PUBLIC | ( $options['public'] ? 0 : ReflectionProperty::IS_PROTECTED ) ]; $data = isset($params[$method]) ? $class->{$method}($params[$method]) : $class->{$method}(); if (!empty($options['names'])) { $data = array_filter($data, function($item) use ($options) { return in_array($item->getName(), (array) $options['names']); }); } if ($options['self']) { $data = array_filter($data, function($item) use ($class) { return ($item->getDeclaringClass()->getName() === $class->getName()); }); } if ($options['public']) { $data = array_filter($data, function($item) { return $item->isPublic(); }); } return Libraries::instance(null, 'collection', compact('data'), static::$_classes); } /** * Helper method to determine if a class applies to a list of modifiers. * * @param string $inspector ReflectionClass instance. * @param array|string $list List of modifiers to test. * @return boolean Test result. */ protected static function _modifiers($inspector, $list = []) { $list = $list ?: ['public', 'private', 'protected', 'abstract', 'final', 'static']; return array_filter($list, function($modifier) use ($inspector) { $method = 'is' . ucfirst($modifier); return (method_exists($inspector, $method) && $inspector->{$method}()); }); } /* Deprecated / BC */ /** * Calls a method on this object with the given parameters. Provides an OO wrapper for * `forward_static_call_array()`. * * @deprecated * @param string $method Name of the method to call. * @param array $params Parameter list to use when calling `$method`. * @return mixed Returns the result of the method call. */ public static function invokeMethod($method, $params = []) { $message = '`' . __METHOD__ . '()` has been deprecated.'; trigger_error($message, E_USER_DEPRECATED); return forward_static_call_array([get_called_class(), $method], $params); } /** * Returns an instance of a class with given `config`. The `name` could be a key from the * `classes` array, a fully namespaced class name, or an object. Typically this method is used * in `_init` to create the dependencies used in the current class. * * @deprecated * @param string|object $name A `$_classes` key or fully-namespaced class name. * @param array $options The configuration passed to the constructor. * @see lithium\core\Libraries::instance() * @return object An object instance of the given value in `$name`. */ protected static function _instance($name, array $options = []) { $message = '`' . __METHOD__ . '()` has been deprecated. '; $message .= 'Please use Libraries::instance(), with the 4th parameter instead.'; trigger_error($message, E_USER_DEPRECATED); return Libraries::instance(null, $name, $options, static::$_classes); } } ?>