<?php
/**
* Copyright (c) 2011, Laurent Laville <pear@laurent-laville.org>
*
* Credits to Sebastian Bergmann on base concept from phpunit/PHP_Token_Stream
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of the authors nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* PHP version 5
*
* @category PHP
* @package PHP_Reflect
* @author Laurent Laville <pear@laurent-laville.org>
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* @version SVN: $Id$
* @link http://php5.laurent-laville.org/reflect/
*/
/**
* PHP_Reflect adds the ability to reverse-engineer
* classes, interfaces, functions, constants and more.
*
* @category PHP
* @package PHP_Reflect
* @author Laurent Laville <pear@laurent-laville.org>
* @license http://www.opensource.org/licenses/bsd-license.php BSD
* @version Release: 1.0.0
* @link http://php5.laurent-laville.org/reflect/
*/
class PHP_Reflect implements ArrayAccess
{
/**
* Support for interface ArrayAccess
* @var array
* @link http://www.php.net/manual/en/class.arrayaccess.php
*/
private $_container;
/**
* @var array
*/
protected static $customTokens = array(
'(' => 'T_OPEN_BRACKET',
')' => 'T_CLOSE_BRACKET',
'[' => 'T_OPEN_SQUARE',
']' => 'T_CLOSE_SQUARE',
'{' => 'T_OPEN_CURLY',
'}' => 'T_CLOSE_CURLY',
';' => 'T_SEMICOLON',
'.' => 'T_DOT',
',' => 'T_COMMA',
'=' => 'T_EQUAL',
'<' => 'T_LT',
'>' => 'T_GT',
'+' => 'T_PLUS',
'-' => 'T_MINUS',
'*' => 'T_MULT',
'/' => 'T_DIV',
'?' => 'T_QUESTION_MARK',
'!' => 'T_EXCLAMATION_MARK',
':' => 'T_COLON',
'"' => 'T_DOUBLE_QUOTES',
'@' => 'T_AT',
'&' => 'T_AMPERSAND',
'%' => 'T_PERCENT',
'|' => 'T_PIPE',
'$' => 'T_DOLLAR',
'^' => 'T_CARET',
'~' => 'T_TILDE',
'`' => 'T_BACKTICK'
);
/**
* @var array
*/
protected $tokens = array();
/**
* @var array
*/
protected $parserToken;
/**
* @var array
*/
public $options;
/**
* @var string
*/
protected $filename;
/**
* @var array
*/
protected $linesOfCode = array('loc' => 0, 'cloc' => 0, 'ncloc' => 0);
/**
* Class constructor
*
* @param array $options (OPTIONAL) Configure options
*
* @throws RuntimeException
*/
public function __construct($options = NULL)
{
$defaultOptions = array(
// default containers to store results from parsing
'containers' => array(
'namespace' => 'namespaces',
'interface' => 'interfaces',
'class' => 'classes',
'function' => 'functions',
'require_once' => 'includes',
'require' => 'includes',
'include_once' => 'includes',
'include' => 'includes',
'variable' => 'globals'
),
// properties for each component to provide on final result
'properties' => array(
'namespace' => array(
'file', 'startEndLines', 'docblock'
),
'interface' => array(
'file', 'startEndLines', 'docblock', 'namespace',
'keywords', 'parent', 'methods'
),
'class' => array(
'file', 'startEndLines', 'docblock', 'namespace',
'keywords', 'parent', 'methods', 'interfaces', 'package'
),
'function' => array(
'file', 'startEndLines', 'docblock', 'namespace',
'keywords', 'signature', 'ccn'
),
'require_once' => array(
'file', 'startEndLines', 'docblock', 'namespace',
),
'require' => array(
'file', 'startEndLines', 'docblock', 'namespace',
),
'include_once' => array(
'file', 'startEndLines', 'docblock', 'namespace',
),
'include' => array(
'file', 'startEndLines', 'docblock', 'namespace',
),
'variable' => array(
'file', 'startEndLines', 'docblock', 'namespace',
),
),
);
$this->options = $defaultOptions;
if (NULL !== $options) {
if (is_array($options)) {
foreach ($options as $key => $values) {
$this->options[$key] = array_merge(
$defaultOptions[$key], $values
);
}
} else {
throw new RuntimeException('Invalid options');
}
}
// default parsers for interfaces, classes, functions, includes
$this->parserToken = array(
'T_NAMESPACE' => array(
'PHP_Reflect_Token_NAMESPACE', array($this, 'parseToken')
),
'T_INTERFACE' => array(
'PHP_Reflect_Token_INTERFACE', array($this, 'parseToken')
),
'T_CLASS' => array(
'PHP_Reflect_Token_CLASS', array($this, 'parseToken')
),
'T_FUNCTION' => array(
'PHP_Reflect_Token_FUNCTION', array($this, 'parseToken')
),
'T_REQUIRE_ONCE' => array(
'PHP_Reflect_Token_REQUIRE_ONCE', array($this, 'parseToken')
),
'T_REQUIRE' => array(
'PHP_Reflect_Token_REQUIRE', array($this, 'parseToken')
),
'T_INCLUDE_ONCE' => array(
'PHP_Reflect_Token_INCLUDE_ONCE', array($this, 'parseToken')
),
'T_INCLUDE' => array(
'PHP_Reflect_Token_INCLUDE', array($this, 'parseToken')
),
'T_VARIABLE' => array(
'PHP_Reflect_Token_VARIABLE', array($this, 'parseToken')
),
);
}
/**
* Connect additionnal tokens for parsing
*
* @param string $tokenName Token name T_ prefixed
* @param string $tokenClass Token class corresponding
* @param mixed $callback Function to connect to token for parsing
*
* @return void
* @throws RuntimeException
*/
public function connect($tokenName, $tokenClass, $callback)
{
if (!class_exists($tokenClass, TRUE)) {
throw new RuntimeException(
"Invalid token name provided. " .
"Given '" . (string)$tokenName . "'"
);
}
if (!is_callable($callback)) {
throw new RuntimeException(
"Cannot connect to function provided"
);
}
$this->parserToken[$tokenName] = array($tokenClass, $callback);
}
/**
* Scans the source for sequences of characters and converts them into a
* stream of tokens.
*
* @param string $sourceCode Filename or raw php code line
*
* @return array
* @throws RuntimeException
*/
public function scan($sourceCode)
{
if (is_file($sourceCode)) {
$this->filename = $sourceCode;
$sourceCode = file_get_contents($sourceCode);
} elseif (!is_string($sourceCode)) {
throw new RuntimeException('sourceCode wrong parameter');
}
$line = 1;
$this->tokens = token_get_all($sourceCode);
foreach ($this->tokens as $id => $token) {
if (is_array($token)) {
$text = $token[1];
$tokenName = token_name($token[0]);
} else {
$text = $token;
$tokenName = self::$customTokens[$token];
$this->tokens[$id] = array(1 => $text);
}
$this->tokens[$id][2] = $line;
$this->tokens[$id][0] = $tokenName;
$lines = substr_count($text, "\n");
$line += $lines;
if ('T_HALT_COMPILER' == $tokenName) {
break;
} elseif ($tokenName == 'T_COMMENT'
|| $tokenName == 'T_DOC_COMMENT'
) {
$this->linesOfCode['cloc'] += $lines + 1;
}
}
$this->linesOfCode['loc'] = substr_count($sourceCode, "\n");
$this->linesOfCode['ncloc']
= $this->linesOfCode['loc'] - $this->linesOfCode['cloc'];
$this->parse();
return $this->tokens;
}
/**
* Magic methods to get informations on parsing results about
* includes, interfaces, classes, functions, constants
*
* @param string $name Method name invoked
* @param array $args Method arguments provided
*
* @return array
* @throws RuntimeException
*/
public function __call($name, $args)
{
$methods = array_map(
'ucfirst',
array_unique(array_values($this->options['containers']))
);
$pattern = '/get' .
'(?>(' . implode('|', $methods) . '))/';
if (preg_match($pattern, $name, $matches)) {
$container = strtolower($matches[1]{0}) . substr($matches[1], 1);
if ($container == 'namespaces') {
return $this->offsetGet($container);
} else {
$namespace = (isset($args[0]) && is_string($args[0]))
? $args[0] : false;
if ($namespace === FALSE) {
// get data from all namespaces
return $this->offsetGet($container);
} else {
// get data from specified namespace
if ($this->offsetExists(array($container => $namespace))) {
return $this->offsetGet(array($container => $namespace));
} else {
return array();
}
}
}
} else {
throw new RuntimeException(
"Invalid method. Given '$name'"
);
}
}
/**
* Gets the names of all files that have been included
* using include(), include_once(), require() or require_once().
*
* Parameter $categorize set to TRUE causing this function to return a
* multi-dimensional array with categories in the keys of the first dimension
* and constants and their values in the second dimension.
*
* Parameter $category allow to filter following specific inclusion type
*
* @param bool $categorize OPTIONAL
* @param string $category OPTIONAL Either 'require_once', 'require',
* 'include_once', 'include'
* @param string $namespace OPTIONAL Default is global namespace
*
* @return array
*/
public function getIncludes($categorize = FALSE, $category = NULL,
$namespace = FALSE)
{
if ($namespace === FALSE) {
// global namespace
$ns = '\\';
} else {
$ns = $namespace;
}
$includes = $this->offsetGet(array('includes' => $ns));
foreach (array('require_once', 'require', 'include_once', 'include')
as $key) {
if (!isset($includes[$key])) {
$includes[$key] = array();
}
}
if (isset($includes[$category])) {
$includes = $includes[$category];
} elseif ($categorize === FALSE) {
$includes = array_merge(
$includes['require_once'],
$includes['require'],
$includes['include_once'],
$includes['include']
);
}
return $includes;
}
/**
* Gets global variables defined in source scanned
*
* Parameter $categorize set to TRUE causing this function to return a
* multi-dimensional array with categories in the keys of the first dimension
* and constants and their values in the second dimension.
*
* Parameter $category allow to filter following specific global type
*
* @param bool $categorize OPTIONAL
* @param string $category OPTIONAL
* @param string $namespace OPTIONAL Default is global namespace
*
* @return array
*/
public function getGlobals($categorize = FALSE, $category = NULL,
$namespace = FALSE)
{
static $glob = array(
'global',
'$GLOBALS',
'$HTTP_SERVER_VARS',
'$_SERVER',
'$HTTP_GET_VARS',
'$_GET',
'$HTTP_POST_VARS',
'$HTTP_POST_FILES',
'$_POST',
'$HTTP_COOKIE_VARS',
'$_COOKIE',
'$HTTP_SESSION_VARS',
'$_SESSION',
'$HTTP_ENV_VARS',
'$_ENV',
);
if ($namespace === FALSE) {
// global namespace
$ns = '\\';
} else {
$ns = $namespace;
}
$globals = $this->offsetGet(array('globals' => $ns));
foreach ($glob as $key) {
if (!isset($globals[$key])) {
$globals[$key] = array();
}
}
if (isset($globals[$category])) {
$globals = $globals[$category];
} elseif ($categorize === FALSE) {
$globals = array_merge(
$globals['global'],
$globals['$GLOBALS'],
$globals['$HTTP_SERVER_VARS'],
$globals['$_SERVER'],
$globals['$HTTP_GET_VARS'],
$globals['$_GET'],
$globals['$HTTP_POST_VARS'],
$globals['$HTTP_POST_FILES'],
$globals['$_POST'],
$globals['$HTTP_COOKIE_VARS'],
$globals['$_COOKIE'],
$globals['$HTTP_SESSION_VARS'],
$globals['$_SESSION'],
$globals['$HTTP_ENV_VARS'],
$globals['$_ENV']
);
}
ksort($globals);
return $globals;
}
/**
* Returns number of lines (code, comment, total) in source code parsed
*
* @return array
*/
public function getLinesOfCode()
{
return $this->linesOfCode;
}
/**
* Main Parser
*
* @return void
*/
protected function parse()
{
$namespace = FALSE;
$namespaceEndLine = FALSE;
$class = FALSE;
$classEndLine = FALSE;
$interface = FALSE;
$interfaceEndLine = FALSE;
foreach ($this->tokens as $id => $token) {
if ('T_HALT_COMPILER' == $token[0]) {
break;
}
$tokenName = $token[0];
$text = $token[1];
$line = $token[2];
$context = array(
'namespace' => $namespace,
'class' => $class,
'interface' => $interface,
'context' => strtolower(str_replace('T_', '', $tokenName))
);
switch ($tokenName) {
case 'T_CLOSE_CURLY':
if ($namespaceEndLine !== FALSE
&& $namespaceEndLine == $line
) {
$namespace = FALSE;
$namespaceEndLine = FALSE;
}
if ($classEndLine !== FALSE
&& $classEndLine == $line
) {
$class = FALSE;
$classEndLine = FALSE;
}
if ($interfaceEndLine !== FALSE
&& $interfaceEndLine == $line
) {
$interface = FALSE;
$interfaceEndLine = FALSE;
}
break;
default:
if (isset($this->parserToken[$tokenName])) {
$tokenClass = $this->parserToken[$tokenName][0];
$token = new $tokenClass($text, $line, $id, $this->tokens);
call_user_func_array(
$this->parserToken[$tokenName][1],
array(&$this, $context, $token)
);
}
break;
}
if ($tokenName == 'T_NAMESPACE') {
$namespace = $token->getName();
$namespaceEndLine = $token->getEndLine();
} elseif ($tokenName == 'T_INTERFACE') {
$interface = $token->getName();
$interfaceEndLine = $token->getEndLine();
} elseif ($tokenName == 'T_CLASS') {
$class = $token->getName();
$classEndLine = $token->getEndLine();
}
}
}
/**
* Default parser for tokens
* T_NAMESPACE, T_INTERFACE, T_CLASS, T_FUNCTION,
* T_REQUIRE_ONCE, T_REQUIRE, T_INCLUDE_ONCE, T_INCLUDE,
* T_VARIABLE
*
* @return void
*/
protected function parseToken()
{
static $globals = array(
'global',
'$GLOBALS',
'$HTTP_SERVER_VARS',
'$_SERVER',
'$HTTP_GET_VARS',
'$_GET',
'$HTTP_POST_VARS',
'$HTTP_POST_FILES',
'$_POST',
'$HTTP_COOKIE_VARS',
'$_COOKIE',
'$HTTP_SESSION_VARS',
'$_SESSION',
'$HTTP_ENV_VARS',
'$_ENV'
);
list($subject, $context, $token) = func_get_args();
extract($context);
$container = $subject->options['containers'][$context];
if ($container === NULL) {
return;
}
$tmp = array();
$name = $token->getName();
if ($name === NULL) {
return;
}
$inc = in_array(
$context, array('require_once', 'require', 'include_once', 'include')
);
if (method_exists($token, 'getType')) {
$type = $token->getType();
$glob = in_array($type, $globals);
} else {
$glob = FALSE;
}
if (isset($subject->options['properties'][$context])) {
$properties = $subject->options['properties'][$context];
} else {
$properties = array();
}
switch ($context) {
case 'namespace':
case 'interface':
case 'class':
case 'function':
case 'require_once':
case 'require':
case 'include_once':
case 'include':
case 'variable':
if (in_array('startEndLines', $properties)) {
$tmp['startLine'] = $token->getLine();
$tmp['endLine'] = $token->getEndLine();
}
if (in_array('file', $properties)) {
$tmp['file'] = $subject->filename;
}
if (in_array('namespace', $properties)) {
$tmp['namespace'] = (($namespace === FALSE) ? '' : $namespace);
}
break;
}
foreach ($properties as $property) {
$method = 'get' . ucfirst($property);
if (method_exists($token, $method)) {
$tmp[$property] = $token->{$method}();
}
}
if ($namespace === FALSE) {
// global namespace
$ns = '\\';
} else {
$ns = $namespace;
}
if ($context == 'function') {
$properties = $subject->options['properties'];
if ($class === FALSE && $interface === FALSE) {
// update user functions
$_ns = $subject->offsetGet(array($container => $ns));
$_ns[$name] = $tmp;
$subject->offsetSet(array($container => $ns), $_ns);
} elseif ($interface === FALSE) {
if (!in_array('methods', $properties['class'])) {
return;
}
$container = $subject->options['containers']['class'];
if ($container !== NULL) {
// update class methods
if (isset($tmp['namespace'])) {
unset($tmp['namespace']);
}
$_ns = $subject->offsetGet(array($container => $ns));
$_ns[$class]['methods'][$name] = $tmp;
$subject->offsetSet(array($container => $ns), $_ns);
}
} else {
if (!in_array('methods', $properties['interface'])) {
return;
}
$container = $subject->options['containers']['interface'];
if ($container !== NULL) {
// update interface methods
if (isset($tmp['namespace'])) {
unset($tmp['namespace']);
}
if ($namespace === FALSE) {
// global namespace
$ns = '\\';
} else {
$ns = $namespace;
}
$_ns = $subject->offsetGet(array($container => $ns));
$_ns[$interface]['methods'][$name] = $tmp;
$subject->offsetSet(array($container => $ns), $_ns);
}
}
} elseif ($inc === TRUE || $glob === TRUE) {
// update includes or globals
$_ns = $subject->offsetGet(array($container => $ns));
$_ns[$type][$name] = $tmp;
$subject->offsetSet(array($container => $ns), $_ns);
} elseif ($context == 'namespace') {
$subject->offsetSet(array($container => $name), $tmp);
} else {
$_ns = $subject->offsetGet(array($container => $ns));
$_ns[$name] = $tmp;
$subject->offsetSet(array($container => $ns), $_ns);
}
}
/**
* Whether or not an offset exists
*
* @param mixed $offset An offset to check for
*
* @return bool
*/
public function offsetExists($offset)
{
if (is_array($offset)) {
list ($container, $namespace) = each($offset);
return isset($this->_container[$container][$namespace]);
} else {
return isset($this->_container[$offset]);
}
}
/**
* Returns the value at specified offset, or null if offset does not exists
*
* @param mixed $offset The offset to retrieve
*
* @return mixed
*/
public function offsetGet($offset)
{
if (is_array($offset)) {
list ($container, $namespace) = each($offset);
if (isset($this->_container[$container][$namespace])) {
return $this->_container[$container][$namespace];
}
} else {
if (isset($this->_container[$offset])) {
return $this->_container[$offset];
}
}
}
/**
* Assigns a value to the specified offset
*
* @param mixed $offset The offset to assign the value to
* @param mixed $value The value to set
*
* @return void
*/
public function offsetSet($offset, $value)
{
if (is_null($offset)) {
$this->_container[] = $value;
} elseif (is_array($offset)) {
list ($container, $namespace) = each($offset);
$this->_container[$container][$namespace] = $value;
} else {
$this->_container[$offset] = $value;
}
}
/**
* Unsets an offset
*
* @param mixed $offset The offset to unset
*
* @return void
*/
public function offsetUnset($offset)
{
if (is_array($offset)) {
list ($container, $namespace) = each($offset);
unset($this->_container[$container][$namespace]);
} else {
unset($this->_container[$offset]);
}
}
}