Skip to content

Instantly share code, notes, and snippets.

@thekid
Created July 8, 2011 08:19
Show Gist options
  • Save thekid/1071362 to your computer and use it in GitHub Desktop.
Save thekid/1071362 to your computer and use it in GitHub Desktop.
XP Framework: Performance suite for issue #14
<?php
/* This class is part of the XP framework
*
* $Id$
*/
uses('util.profiling.Timer');
/**
* Performance test
*
* @see https://github.com/xp-framework/xp-framework/issues/14
*/
class Issue14PerformanceTest extends Object {
protected static $inputs= array(
'#[@test]'
=> array('test' => NULL),
'#[@test(expect = "lang.FormatException")]'
=> array('test' => array('expect' => 'lang.FormatException')),
"#[@named('sybase')]"
=> array('named' => 'sybase'),
"#[@permission('rn=login, rt=config')]"
=> array('permission' => 'rn=login, rt=config'),
'#[@arg(name= "verbose", short="v")]'
=> array('arg' => array('name' => 'verbose', 'short' => 'v')),
'#[@arg(position= 0)]'
=> array('arg' => array('position' => 0)),
'#[@fromxml(xpath= "/root/element[position() = 3]/@id")]'
=> array('fromxml' => array('xpath' => '/root/element[position() = 3]/@id')),
'#[@webmethod, @restricted(role= "admin")]'
=> array('webmethod' => NULL, 'restricted' => array('role' => 'admin')),
'#[@restricted(roles = array("admin", "root"))]'
=> array('restricted' => array('roles' => array('admin', 'root'))),
'#[@require(permissions= array("rn=a", "rn=b"))]'
=> array('require' => array('permissions' => array('rn=a', 'rn=b'))),
'#[@overloaded(signatures= array(array("string"), array("string", "string")))]'
=> array('overloaded' => array('signatures' => array(array('string'), array('string', 'string')))),
);
/**
* Entry point
*
* @param string[] args
*/
public static function main(array $args) {
$max= isset($args[0]) ? (int)$args[0]: 100000;
$total= 0.0;
$count= 0;
$t= new Timer();
Console::writeLine(XPClass::forName('lang.XPClass')->getClassLoader());
Console::writeLine('==========================================');
foreach (self::$inputs as $input => $expected) {
Console::write('>> ', $input);
try {
$parsed= XPClass::parseAnnotations($input, 'Test');
} catch (Throwable $e) {
Console::$err->writeLine('*** Expected '.xp::stringOf($expected).' but was '.$e->compoundMessage());
continue;
}
if ($expected !== $parsed) {
Console::$err->writeLine('*** Expected '.xp::stringOf($expected).' but was '.xp::stringOf($parsed));
continue;
}
$t->start();
for ($i= 0; $i < $max; $i++) {
XPClass::parseAnnotations($input, 'Test');
}
$t->stop();
Console::writeLinef(': %d in %.3f seconds', $i, $t->elapsedTime());
$total+= $t->elapsedTime();
$count++;
}
Console::writeLine('==========================================');
Console::writeLinef('Total: %d tests(s) in %.3f seconds, avg. %.3f seconds', $count, $total, $total / $count);
}
}
?>
TIMES=100000
IMPL=complete regex scanner
all:
@echo "- $(MAKE) perf.[$(IMPL)]"
@echo " Tests the implementations' performance"
@echo
@echo "- $(MAKE) test.[$(IMPL)]"
@echo " Runs net.xp_framework.unittest.annotations.*"
@echo
@echo "- $(MAKE) shootout"
@echo " Compares different implementations performance-wise"
@echo
@echo "- $(MAKE) verify"
@echo " Compares different implementations functionality"
perf.%:
@echo '!$*' > class.pth ; echo -n "@$*: " ; xp Issue14PerformanceTest $(TIMES); rm class.pth
test.%:
@echo '!$*' > class.pth ; echo -n "@$*: " ; unittest net.xp_framework.unittest.annotations.* ; rm class.pth
shootout:
@for i in $(IMPL) ; do $(MAKE) perf.$$i ; echo "" ; done
verify:
@for i in $(IMPL) ; do $(MAKE) test.$$i ; echo "" ; done
<?php
/* This class is part of the XP framework
*
* $Id$
*/
uses(
'lang.Type',
'lang.reflect.Method',
'lang.reflect.Field',
'lang.reflect.Constructor',
'lang.reflect.Modifiers',
'lang.reflect.Package'
);
define('DETAIL_ARGUMENTS', 1);
define('DETAIL_RETURNS', 2);
define('DETAIL_THROWS', 3);
define('DETAIL_COMMENT', 4);
define('DETAIL_ANNOTATIONS', 5);
define('DETAIL_NAME', 6);
define('DETAIL_GENERIC', 7);
/**
* Represents classes. Every instance of an XP class has an method
* called getClass() which returns an instance of this class.
*
* Warning
* =======
* Do not construct this class publicly, instead use either the
* $o->getClass() syntax or the static method
* $class= XPClass::forName('fully.qualified.Name')
*
* Examples
* ========
* To retrieve the fully qualified name of a class, use this:
* <code>
* $o= new File('...');
* echo 'The class name for $o is '.$o->getClass()->getName();
* </code>
*
* Create an instance of a class:
* <code>
* $instance= XPClass::forName('util.Binford')->newInstance();
* </code>
*
* Invoke a method by its name:
* <code>
* try {
* $instance->getClass()->getMethod('connect')->invoke($instance);
* } catch (TargetInvocationException $e) {
* $e->getCause()->printStackTrace();
* }
* </code>
*
* @see xp://lang.Object#getClass
* @see xp://lang.XPClass#forName
* @test xp://net.xp_framework.unittest.reflection.ReflectionTest
* @test xp://net.xp_framework.unittest.reflection.ClassDetailsTest
* @test xp://net.xp_framework.unittest.reflection.IsInstanceTest
* @test xp://net.xp_framework.unittest.reflection.ClassCastingTest
* @purpose Reflection
*/
class XPClass extends Type {
protected $_class= NULL;
public $_reflect= NULL;
private static $DECLARING_CLASS_BUG= FALSE;
static function __static() {
self::$DECLARING_CLASS_BUG= version_compare(PHP_VERSION, '5.2.10', 'lt');
}
/**
* Constructor
*
* @param var ref either a class name, a ReflectionClass instance or an object
* @throws lang.IllegalStateException
*/
public function __construct($ref) {
if ($ref instanceof ReflectionClass) {
$this->_reflect= $ref;
$this->_class= $ref->getName();
} else if (is_object($ref)) {
$this->_reflect= new ReflectionClass($ref);
$this->_class= get_class($ref);
} else {
try {
$this->_reflect= new ReflectionClass((string)$ref);
} catch (ReflectionException $e) {
throw new IllegalStateException($e->getMessage());
}
$this->_class= $ref;
}
parent::__construct(xp::nameOf($this->_class));
}
/**
* Returns simple name
*
* @return string
*/
public function getSimpleName() {
return FALSE === ($p= strrpos(substr($this->name, 0, strcspn($this->name, '<')), '.'))
? $this->name // Already unqualified
: substr($this->name, $p+ 1) // Full name
;
}
/**
* Retrieves the package associated with this class
*
* @return lang.reflect.Package
*/
public function getPackage() {
return Package::forName(substr($this->name, 0, strrpos($this->name, '.')));
}
/**
* Creates a new instance of the class represented by this Class object.
* The class is instantiated as if by a new expression with an empty argument list.
*
* Example
* =======
* <code>
* try {
* $o= XPClass::forName($name)->newInstance();
* } catch (ClassNotFoundException $e) {
* // handle it!
* }
* </code>
*
* Example (passing arguments)
* ===========================
* <code>
* try {
* $o= XPClass::forName('peer.Socket')->newInstance('localhost', 6100);
* } catch (ClassNotFoundException $e) {
* // handle it!
* }
* </code>
*
* @param var* args
* @return lang.Object
* @throws lang.IllegalAccessException in case this class cannot be instantiated
*/
public function newInstance() {
if ($this->_reflect->isInterface()) {
throw new IllegalAccessException('Cannot instantiate interfaces ('.$this->name.')');
} else if ($this->_reflect->isAbstract()) {
throw new IllegalAccessException('Cannot instantiate abstract classes ('.$this->name.')');
}
try {
if (!$this->hasConstructor()) return $this->_reflect->newInstance();
$args= func_get_args();
return $this->_reflect->newInstanceArgs($args);
} catch (ReflectionException $e) {
throw new IllegalAccessException($e->getMessage());
}
}
/**
* Gets class methods for this class
*
* @return lang.reflect.Method[]
*/
public function getMethods() {
$list= array();
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2)) continue;
$list[]= new Method($this->_class, $m);
}
return $list;
}
/**
* Gets class methods declared by this class
*
* @return lang.reflect.Method[]
*/
public function getDeclaredMethods() {
$list= array();
if (self::$DECLARING_CLASS_BUG) {
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2) || $m->getDeclaringClass()->getName() !== $this->_reflect->name) continue;
$list[]= new Method($this->_class, $m);
}
} else {
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2) || $m->class !== $this->_reflect->name) continue;
$list[]= new Method($this->_class, $m);
}
}
return $list;
}
/**
* Gets a method by a specified name.
*
* @param string name
* @return lang.reflect.Method
* @see xp://lang.reflect.Method
* @throws lang.ElementNotFoundException
*/
public function getMethod($name) {
if ($this->hasMethod($name)) {
return new Method($this->_class, $this->_reflect->getMethod($name));
}
raise('lang.ElementNotFoundException', 'No such method "'.$name.'" in class '.$this->name);
}
/**
* Checks whether this class has a method named "$method" or not.
*
* Note
* ====
* Since in PHP, methods are case-insensitive, calling hasMethod('toString')
* will provide the same result as hasMethod('tostring')
*
* @param string method the method's name
* @return bool TRUE if method exists
*/
public function hasMethod($method) {
return ((0 === strncmp('__', $method, 2))
? FALSE
: $this->_reflect->hasMethod($method)
);
}
/**
* Retrieve if a constructor exists
*
* @return bool
*/
public function hasConstructor() {
return $this->_reflect->hasMethod('__construct');
}
/**
* Retrieves this class' constructor.
*
* @return lang.reflect.Constructor
* @see xp://lang.reflect.Constructor
* @throws lang.ElementNotFoundException
*/
public function getConstructor() {
if ($this->hasConstructor()) {
return new Constructor($this->_class, $this->_reflect->getMethod('__construct'));
}
raise('lang.ElementNotFoundException', 'No constructor in class '.$this->name);
}
/**
* Retrieve a list of all member variables
*
* @return lang.reflect.Field[] array of field objects
*/
public function getFields() {
$f= array();
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name) continue;
$f[]= new Field($this->_class, $p);
}
return $f;
}
/**
* Retrieve a list of member variables declared in this class
*
* @return lang.reflect.Field[] array of field objects
*/
public function getDeclaredFields() {
$list= array();
if (self::$DECLARING_CLASS_BUG) {
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name || $p->getDeclaringClass()->getName() !== $this->_reflect->name) continue;
$list[]= new Field($this->_class, $p);
}
} else {
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name || $p->class !== $this->_reflect->name) continue;
$list[]= new Field($this->_class, $p);
}
}
return $list;
}
/**
* Retrieve a field by a specified name.
*
* @param string name
* @return lang.reflect.Field
* @throws lang.ElementNotFoundException
*/
public function getField($name) {
if ($this->hasField($name)) {
return new Field($this->_class, $this->_reflect->getProperty($name));
}
raise('lang.ElementNotFoundException', 'No such field "'.$name.'" in class '.$this->name);
}
/**
* Checks whether this class has a field named "$field" or not.
*
* @param string field the fields's name
* @return bool TRUE if field exists
*/
public function hasField($field) {
return '__id' == $field ? FALSE : $this->_reflect->hasProperty($field);
}
/**
* Retrieve the parent class's class object. Returns NULL if there
* is no parent class.
*
* @return lang.XPClass class object
*/
public function getParentclass() {
return ($parent= $this->_reflect->getParentClass()) ? new self($parent) : NULL;
}
/**
* Checks whether this class has a constant named "$constant" or not
*
* @param string constant
* @return bool
*/
public function hasConstant($constant) {
return $this->_reflect->hasConstant($constant);
}
/**
* Retrieve a constant by a specified name.
*
* @param string constant
* @return var
* @throws lang.ElementNotFoundException in case constant does not exist
*/
public function getConstant($constant) {
if ($this->hasConstant($constant)) {
return $this->_reflect->getConstant($constant);
}
raise('lang.ElementNotFoundException', 'No such constants "'.$constant.'" in class '.$this->name);
}
/**
* Cast a given object to the class represented by this object
*
* @param lang.Generic expression
* @return lang.Generic the given expression
* @throws lang.ClassCastException
*/
public function cast(Generic $expression= NULL) {
if (NULL === $expression) {
return xp::null();
} else if (is($this->name, $expression)) {
return $expression;
}
raise('lang.ClassCastException', 'Cannot cast '.xp::typeOf($expression).' to '.$this->name);
}
/**
* Tests whether this class is a subclass of a specified class.
*
* @param var class either a string or an XPClass object
* @return bool
*/
public function isSubclassOf($class) {
if (!($class instanceof self)) $class= XPClass::forName($class);
if ($class->name == $this->name) return FALSE; // Catch bordercase (ZE bug?)
return $this->_reflect->isSubclassOf($class->_reflect);
}
/**
* Determines whether the specified object is an instance of this
* class. This is the equivalent of the is() core functionality.
*
* Examples
* ========
* <code>
* uses('io.File', 'io.TempFile');
* $class= XPClass::forName('io.File');
*
* var_dump($class->isInstance(new TempFile())); // TRUE
* var_dump($class->isInstance(new File())); // TRUE
* var_dump($class->isInstance(new Object())); // FALSE
* </code>
*
* @param var obj
* @return bool
*/
public function isInstance($obj) {
return is($this->name, $obj);
}
/**
* Determines if this XPClass object represents an interface type.
*
* @return bool
*/
public function isInterface() {
return $this->_reflect->isInterface();
}
/**
* Determines if this XPClass object represents an interface type.
*
* @return bool
*/
public function isEnum() {
$e= xp::reflect('lang.Enum');
return class_exists($e) && $this->_reflect->isSubclassOf($e);
}
/**
* Retrieve interfaces this class implements
*
* @return lang.XPClass[]
*/
public function getInterfaces() {
$r= array();
foreach ($this->_reflect->getInterfaces() as $iface) {
$r[]= new self($iface->getName());
}
return $r;
}
/**
* Retrieve interfaces this class implements in its declaration
*
* @return lang.XPClass[]
*/
public function getDeclaredInterfaces() {
$is= $this->_reflect->getInterfaces();
if ($parent= $this->_reflect->getParentclass()) {
$ip= $parent->getInterfaces();
} else {
$ip= array();
}
$filter= array();
foreach ($is as $iname => $i) {
// Parent class implements this interface
if (isset($ip[$iname])) {
$filter[$iname]= TRUE;
continue;
}
// Interface is implemented because it's the parent of another interface
foreach ($i->getInterfaces() as $pname => $p) {
if (isset($is[$pname])) $filter[$pname]= TRUE;
}
}
$r= array();
foreach ($is as $iname => $i) {
if (!isset($filter[$iname])) $r[]= new self($i);
}
return $r;
}
/**
* Retrieves the api doc comment for this class. Returns NULL if
* no documentation is present.
*
* @return string
*/
public function getComment() {
if (!($details= self::detailsForClass($this->name))) return NULL;
return $details['class'][DETAIL_COMMENT];
}
/**
* Retrieves this class' modifiers
*
* @see xp://lang.reflect.Modifiers
* @return int
*/
public function getModifiers() {
$r= MODIFIER_PUBLIC;
// Map PHP reflection modifiers to generic form
$m= $this->_reflect->getModifiers();
$m & ReflectionClass::IS_EXPLICIT_ABSTRACT && $r |= MODIFIER_ABSTRACT;
$m & ReflectionClass::IS_IMPLICIT_ABSTRACT && $r |= MODIFIER_ABSTRACT;
$m & ReflectionClass::IS_FINAL && $r |= MODIFIER_FINAL;
return $r;
}
/**
* Check whether an annotation exists
*
* @param string name
* @param string key default NULL
* @return bool
*/
public function hasAnnotation($name, $key= NULL) {
$details= self::detailsForClass($this->name);
return $details && ($key
? @array_key_exists($key, @$details['class'][DETAIL_ANNOTATIONS][$name])
: @array_key_exists($name, @$details['class'][DETAIL_ANNOTATIONS])
);
}
/**
* Retrieve annotation by name
*
* @param string name
* @param string key default NULL
* @return var
* @throws lang.ElementNotFoundException
*/
public function getAnnotation($name, $key= NULL) {
$details= self::detailsForClass($this->name);
if (!$details || !($key
? @array_key_exists($key, @$details['class'][DETAIL_ANNOTATIONS][$name])
: @array_key_exists($name, @$details['class'][DETAIL_ANNOTATIONS])
)) return raise(
'lang.ElementNotFoundException',
'Annotation "'.$name.($key ? '.'.$key : '').'" does not exist'
);
return ($key
? $details['class'][DETAIL_ANNOTATIONS][$name][$key]
: $details['class'][DETAIL_ANNOTATIONS][$name]
);
}
/**
* Retrieve whether a method has annotations
*
* @return bool
*/
public function hasAnnotations() {
$details= self::detailsForClass($this->name);
return $details ? !empty($details['class'][DETAIL_ANNOTATIONS]) : FALSE;
}
/**
* Retrieve all of a method's annotations
*
* @return array annotations
*/
public function getAnnotations() {
$details= self::detailsForClass($this->name);
return $details ? $details['class'][DETAIL_ANNOTATIONS] : array();
}
/**
* Retrieve the class loader a class was loaded with.
*
* @return lang.IClassLoader
*/
public function getClassLoader() {
return self::_classLoaderFor($this->name);
}
/**
* Fetch a class' classloader by its name
*
* @param string name fqcn of class
* @return lang.IClassLoader
*/
protected static function _classLoaderFor($name) {
if (isset(xp::$registry[$l= 'classloader.'.$name])) {
sscanf(xp::$registry[$l], '%[^:]://%[^$]', $cl, $argument);
return call_user_func(array(xp::reflect($cl), 'instanceFor'), $argument);
}
return NULL; // Internal class, e.g.
}
/**
* Parses annotation string
*
* @param string input
* @param string context the class name, and optionally the line number.
* @return [:var]
* @throws lang.ClassFormatException
*/
public static function parseAnnotations($input, $context) {
$input= trim($input, "[]# \t\n\r").']';
$offset= 0;
$annotations= array();
$annotation= $value= NULL;
$length= strlen($input);
while ($offset < $length) {
$state= $input{$offset};
if ('@' === $state) {
$s= strcspn($input, ',(]', $offset);
$annotation= substr($input, $offset+ 1, $s- 1);
$offset+= $s;
} else if (']' === $state) {
$annotations[$annotation]= $value;
break;
} else if ('(' === $state) {
$peek= substr($input, $offset+ 1, strcspn($input, '="\')', $offset));
if ('\'' === $peek{0} || '"' === $peek{0}) {
$p= $offset+ 2;
$q= $peek{0};
while (($s= strcspn($input, $q, $p)) !== 0) {
$p+= $s;
if ('\\' !== $input{$p- 1}) break;
$p++;
}
if (!is_string($value= @eval('return '.substr($input, $offset+ 1, $p - $offset).';'))) {
raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed string in '.$context);
}
$offset= $p+ 1;
} else if ('array' === substr($peek, 0, 5)) {
$b= 1;
$p= $offset+ 1+ 6;
while ($b > 0) {
$p+= strcspn($input, '()"\'', $p);
if ($p > $length) break;
if ('(' === $input{$p}) $b++; else if (')' === $input{$p}) $b--; else if ('\'' === $input{$p} || '"' === $input{$p}) {
$q= $input{$p};
$p++;
while (($s= strcspn($input, $q, $p)) !== 0) {
$p+= $s;
if ('\\' !== $input{$p- 1}) break;
$p++;
}
}
$p++;
}
if (!is_array($value= @eval('return '.substr($input, $offset+ 1, $p- $offset- 1).';'))) {
raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed array in '.$context);
}
$offset= $p;
} else if ('=' !== $peek{strlen($peek)- 1}) {
$value= eval('return '.substr($peek, 0, -1).';');
$offset+= strlen($peek);
} else {
$value= array();
do {
$key= trim($peek, '= ');
$offset+= strlen($peek)+ 1;
$offset+= strspn($input, ' ', $offset);
if ($offset >= $length) {
break;
} else if ('array' === substr($input, $offset, 5)) {
$b= 1;
$p= $offset+ 6;
while ($b > 0) {
$p+= strcspn($input, '()"\'', $p);
if ($p > $length) break;
if ('(' === $input{$p}) $b++; else if (')' === $input{$p}) $b--; else if ('\'' === $input{$p} || '"' === $input{$p}) {
$q= $input{$p};
$p++;
while (($s= strcspn($input, $q, $p)) !== 0) {
$p+= $s;
if ('\\' !== $input{$p- 1}) break;
$p++;
}
}
$p++;
}
if (!is_array($value[$key]= @eval('return '.substr($input, $offset, $p- $offset).';'))) {
raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed array in '.$context);
}
$offset= $p;
} else if ('\'' === $input{$offset} || '"' === $input{$offset}) {
$p= $offset+ 1;
$q= $input{$offset};
while (($s= strcspn($input, $q, $p)) !== 0) {
$p+= $s;
if ('\\' !== $input{$p- 1}) break;
$p++;
}
if (!is_string($value[$key]= @eval('return '.substr($input, $offset, $p - $offset + 1).';'))) {
raise('lang.ClassFormatException', 'Parse error: Unterminated or malformed string in '.$context);
}
$offset= $p+ 1;
} else {
$s= strcspn($input, ',)', $offset);
$value[$key]= eval('return '.substr($input, $offset, $s).';');
$offset+= $s;
}
// Find next key
$s= strcspn($input, '="\')', $offset);
$peek= substr($input, $offset+ 1, $s);
} while ($s);
}
$offset++; // ")"
if ($offset > $length) {
raise('lang.ClassFormatException', 'Parse error: Expecting ] in '.$context);
}
} else if (',' === $state) {
$annotations[$annotation]= $value;
$annotation= $value= NULL;
if (FALSE === ($offset= strpos($input, '@', $offset))) {
raise('lang.ClassFormatException', 'Parse error: Expecting @ in '.$context);
}
} else {
raise('lang.ClassFormatException', 'Parse error: Unknown state '.$state.' at position '.$offset.' in '.$context);
}
}
return $annotations;
}
/**
* Retrieve details for a specified class. Note: Results from this
* method are cached!
*
* @param string class fully qualified class name
* @return array or NULL to indicate no details are available
*/
public static function detailsForClass($class) {
if (!$class) return NULL; // Border case
if (isset(xp::$registry['details.'.$class])) return xp::$registry['details.'.$class];
// Retrieve class' sourcecode
$cl= self::_classLoaderFor($class);
if (!$cl || !($bytes= $cl->loadClassBytes($class))) return NULL;
$details= array(array(), array());
$annotations= array();
$comment= NULL;
$members= TRUE;
$parsed= '';
$tokens= token_get_all($bytes);
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
switch ($tokens[$i][0]) {
case T_DOC_COMMENT:
$comment= $tokens[$i][1];
break;
case T_COMMENT:
if ('#' === $tokens[$i][1]{0}) { // Annotations
if ('[' === $tokens[$i][1]{1}) {
$parsed= substr($tokens[$i][1], 2);
} else {
$parsed.= substr($tokens[$i][1], 1);
}
if (']' == substr(rtrim($tokens[$i][1]), -1)) {
$annotations= self::parseAnnotations(
trim($parsed, " \t\n\r"),
$class.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : '')
);
$parsed= '';
}
}
break;
case T_CLASS:
case T_INTERFACE:
if ('' !== $parsed) raise(
'lang.ClassFormatException',
'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : '')
);
$details['class']= array(
DETAIL_COMMENT => trim(preg_replace('/\n \* ?/', "\n", "\n".substr(
$comment,
4, // "/**\n"
strpos($comment, '* @')- 2 // position of first details token
))),
DETAIL_ANNOTATIONS => $annotations
);
$annotations= array();
$comment= NULL;
break;
case T_VARIABLE:
if (!$members) break;
// Have a member variable
'' === $parsed || raise('lang.ClassFormatException', 'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.', line '.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : ''));
$name= substr($tokens[$i][1], 1);
$details[0][$name]= array(
DETAIL_ANNOTATIONS => $annotations
);
$annotations= array();
break;
case T_FUNCTION:
'' === $parsed || raise('lang.ClassFormatException', 'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.', line '.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : ''));
$members= FALSE;
while (T_STRING !== $tokens[$i][0]) $i++;
$m= $tokens[$i][1];
$details[1][$m]= array(
DETAIL_ARGUMENTS => array(),
DETAIL_RETURNS => 'void',
DETAIL_THROWS => array(),
DETAIL_COMMENT => trim(preg_replace('/\n \* ?/', "\n", "\n".substr(
$comment,
4, // "/**\n"
strpos($comment, '* @')- 2 // position of first details token
))),
DETAIL_ANNOTATIONS => $annotations,
DETAIL_NAME => $tokens[$i][1]
);
$matches= NULL;
preg_match_all(
'/@([a-z]+)\s*([^<\r\n]+<[^>]+>|[^\r\n ]+) ?([^\r\n ]+)?/',
$comment,
$matches,
PREG_SET_ORDER
);
$annotations= array();
$comment= NULL;
foreach ($matches as $match) {
switch ($match[1]) {
case 'param':
$details[1][$m][DETAIL_ARGUMENTS][]= $match[2];
break;
case 'return':
$details[1][$m][DETAIL_RETURNS]= $match[2];
break;
case 'throws':
$details[1][$m][DETAIL_THROWS][]= $match[2];
break;
}
}
break;
default:
// Empty
}
}
// Return details for specified class
xp::$registry['details.'.$class]= $details;
return $details;
}
/**
* Retrieve details for a specified class and method. Note: Results
* from this method are cached!
*
* @param string class unqualified class name
* @param string method
* @return array or NULL if not available
*/
public static function detailsForMethod($class, $method) {
$details= self::detailsForClass(xp::nameOf($class));
return $details ? (isset($details[1][$method]) ? $details[1][$method] : NULL) : NULL;
}
/**
* Retrieve details for a specified class and field. Note: Results
* from this method are cached!
*
* @param string class unqualified class name
* @param string method
* @return array or NULL if not available
*/
public static function detailsForField($class, $field) {
$details= self::detailsForClass(xp::nameOf($class));
return $details ? (isset($details[0][$field]) ? $details[0][$field] : NULL) : NULL;
}
/**
* Creates a generic type
*
* @param lang.XPClass self
* @param lang.Type[] arguments
* @return lang.XPClass
*/
public static function createGenericType(XPClass $self, array $arguments) {
// Verify
if (!$self->isGenericDefinition()) {
throw new IllegalStateException('Class '.$self->name.' is not a generic definition');
}
$components= $self->genericComponents();
$cs= sizeof($components);
if ($cs != sizeof($arguments)) {
throw new IllegalArgumentException(sprintf(
'Class %s expects %d component(s) <%s>, %d argument(s) given',
$self->name,
$cs,
implode(', ', $components),
sizeof($arguments)
));
}
// Compose names
$cn= $qc= '';
foreach ($arguments as $typearg) {
$cn.= '¸'.$typearg->literal();
$qc.= ','.$typearg->getName();
}
$name= xp::reflect($self->name).'··'.substr($cn, 1);
$qname= $self->name.'<'.substr($qc, 1).'>';
// Create class if it doesn't exist yet
if (!class_exists($name, FALSE) && !interface_exists($name, FALSE)) {
$meta= isset(xp::$registry['details.'.$self->name]) ? xp::$registry['details.'.$self->name] : array(
'class' => NULL,
0 => array(),
1 => array()
);
// Parse placeholders into a lookup map
$placeholders= array();
foreach ($components as $i => $component) {
$placeholders[$component]= $arguments[$i]->getName();
}
// Work on sourcecode
$cl= self::_classLoaderFor($self->name);
if (!$cl || !($bytes= $cl->loadClassBytes($self->name))) {
throw new IllegalStateException($self->name);
}
// Replace source
$src= '';
$comment= NULL;
$annotations= array();
$annotation= NULL;
$matches= array();
$state= array(0);
$counter= 0;
$tokens= token_get_all($bytes);
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
if (T_COMMENT === $tokens[$i][0] && '#' === $tokens[$i][1]{0}) {
$annotations= eval('return array('.preg_replace(
array('/@([a-z_]+),/i', '/@([a-z_]+)\(\'([^\']+)\'\)/ie', '/@([a-z_]+)\(/i', '/([^a-z_@])([a-z_]+) *= */i'),
array('\'$1\' => NULL,', '"\'$1\' => urldecode(\'".urlencode(\'$2\')."\')"', '\'$1\' => array(', '$1\'$2\' => '),
trim($tokens[$i][1], "[]# \t\n\r").','
).');');
continue;
} else if (T_DOC_COMMENT === $tokens[$i][0]) {
$matches= NULL;
$comment= trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr(
$tokens[$i][1],
4, // "/**\n"
strpos($tokens[$i][1], '* @')- 2 // position of first details token
)));
preg_match_all(
'/@([a-z]+)\s*([^<\r\n]+<[^>]+>|[^\r\n ]+) ?([^\r\n ]+)?/',
$tokens[$i][1],
$matches,
PREG_SET_ORDER
);
}
if (0 === $state[0]) {
if (T_ABSTRACT === $tokens[$i][0] || T_FINAL === $tokens[$i][0]) {
$src.= $tokens[$i][1].' ';
} else if (T_CLASS === $tokens[$i][0] || T_INTERFACE === $tokens[$i][0]) {
if (NULL === $meta['class']) {
$meta['class']= array(DETAIL_COMMENT => $comment, DETAIL_ANNOTATIONS => $annotations);
}
$meta['class'][DETAIL_GENERIC]= array($self->name, $arguments);
$src.= $tokens[$i][1].' '.$name;
array_unshift($state, $tokens[$i][0]);
}
continue;
} else if (T_CLASS === $state[0]) {
if (T_EXTENDS === $tokens[$i][0]) {
if (isset($annotations['generic']['parent'])) {
$xargs= array();
foreach (explode(',', $annotations['generic']['parent']) as $j => $placeholder) {
$xargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= ' extends '.self::createGenericType($self->getParentClass(), $xargs)->literal();
} else {
$src.= ' extends '.$tokens[$i+ 2][1];
}
} else if (T_IMPLEMENTS === $tokens[$i][0]) {
$src.= ' implements';
$counter= 0;
$annotation= @$annotations['generic']['implements'];
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
$src.= ' {';
}
continue;
} else if (T_INTERFACE === $state[0]) {
if (T_EXTENDS === $tokens[$i][0]) {
$src.= ' extends';
$counter= 0;
$annotation= @$annotations['generic']['extends'];
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
$src.= ' {';
}
continue;
} else if (1 === $state[0]) { // Class body
if (T_FUNCTION === $tokens[$i][0]) {
$braces= 0;
$parameters= array();
array_unshift($state, 3);
array_unshift($state, 2);
$m= $tokens[$i+ 2][1];
if (isset($meta[1][$m])) {
$annotations= $meta[1][$m][DETAIL_ANNOTATIONS];
} else {
$meta[1][$m]= array(
DETAIL_ARGUMENTS => array(),
DETAIL_RETURNS => 'void',
DETAIL_THROWS => array(),
DETAIL_COMMENT => $comment,
DETAIL_ANNOTATIONS => $annotations,
DETAIL_NAME => $m
);
foreach ($matches as $match) {
switch ($match[1]) {
case 'param': $meta[1][$m][DETAIL_ARGUMENTS][]= $match[2]; break;
case 'return': $meta[1][$m][DETAIL_RETURNS]= $match[2]; break;
case 'throws': $meta[1][$m][DETAIL_THROWS][]= $match[2]; break;
}
}
}
} else if ('}' === $tokens[$i][0]) {
$src.= '}';
break;
} else if (T_CLOSE_TAG === $tokens[$i][0]) {
break;
}
} else if (2 === $state[0]) { // Method declaration
if ('(' === $tokens[$i][0]) {
$braces++;
} else if (')' === $tokens[$i][0]) {
$braces--;
if (0 === $braces) array_shift($state);
} else if (T_VARIABLE === $tokens[$i][0]) {
$parameters[]= $tokens[$i][1];
}
} else if (3 === $state[0]) { // Method body
if (';' === $tokens[$i][0]) {
// Abstract method
array_shift($state);
} else if ('{' === $tokens[$i][0]) {
$braces= 1;
array_shift($state);
array_unshift($state, 4);
$src.= '{';
if (isset($annotations['generic']['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations['generic']['return'], $placeholders);
}
if (isset($annotations['generic']['params'])) {
$generic= array();
foreach (explode(',', $annotations['generic']['params']) as $j => $placeholder) {
if ('' === ($replaced= strtr(ltrim($placeholder), $placeholders))) {
$generic[$j]= NULL;
} else {
$meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced;
$generic[$j]= $replaced;
}
}
foreach ($generic as $j => $type) {
if (NULL === $type) {
continue;
} else if ('...' === substr($type, -3)) {
$src.= $j ? '$·args= array_slice(func_get_args(), '.$j.');' : '$·args= func_get_args();';
$src.= (
' if (!is(\''.substr($generic[$j], 0, -3).'[]\', $·args)) throw new IllegalArgumentException('.
'"Vararg '.($j + 1).' passed to ".__METHOD__."'.
' must be of '.$type.', ".xp::stringOf($·args)." given"'.
');'
);
} else {
$src.= (
' if (!is(\''.$generic[$j].'\', '.$parameters[$j].')) throw new IllegalArgumentException('.
'"Argument '.($j + 1).' passed to ".__METHOD__."'.
' must be of '.$type.', ".xp::typeOf('.$parameters[$j].')." given"'.
');'
);
}
}
$annotations= array();
}
unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']);
continue;
}
} else if (4 === $state[0]) { // Method body
if ('{' === $tokens[$i][0]) {
$braces++;
} else if ('}' === $tokens[$i][0]) {
$braces--;
if (0 === $braces) array_shift($state);
}
} else if (5 === $state[0]) { // Implements (class), Extends (interface)
if (T_STRING === $tokens[$i][0]) {
if (isset($annotation[$counter])) {
$iargs= array();
foreach (explode(',', $annotation[$counter]) as $j => $placeholder) {
$iargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= self::createGenericType(new XPClass(new ReflectionClass($tokens[$i][1])), $iargs)->literal();
} else {
$src.= $tokens[$i][1];
}
$counter++;
continue;
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
}
}
$src.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
}
// Create class
// DEBUG fputs(STDERR, "@* ".substr($src, 0, strpos($src, '{'))." -> $qname\n");
eval($src);
method_exists($name, '__static') && call_user_func(array($name, '__static'));
unset($meta['class'][DETAIL_ANNOTATIONS]['generic']);
xp::$registry['details.'.$qname]= $meta;
xp::$registry['class.'.$name]= $qname;
}
return new XPClass(new ReflectionClass($name));
}
/**
* Reflectively creates a new type
*
* @param lang.Type[] arguments
* @return lang.XPClass
* @throws lang.IllegalStateException if this class is not a generic definition
* @throws lang.IllegalArgumentException if number of arguments does not match components
*/
public function newGenericType(array $arguments) {
return self::createGenericType($this, $arguments);
}
/**
* Returns generic type components
*
* @return string[]
* @throws lang.IllegalStateException if this class is not a generic definition
*/
public function genericComponents() {
if (!$this->isGenericDefinition()) {
throw new IllegalStateException('Class '.$this->name.' is not a generic definition');
}
$components= array();
foreach (explode(',', $this->getAnnotation('generic', 'self')) as $name) {
$components[]= ltrim($name);
}
return $components;
}
/**
* Returns whether this class is a generic definition
*
* @return bool
*/
public function isGenericDefinition() {
return $this->hasAnnotation('generic', 'self');
}
/**
* Returns generic type definition
*
* @return lang.XPClass
* @throws lang.IllegalStateException if this class is not a generic
*/
public function genericDefinition() {
if (!($details= self::detailsForClass($this->name))) return NULL;
if (!isset($details['class'][DETAIL_GENERIC])) {
throw new IllegalStateException('Class '.$this->name.' is not generic');
}
return XPClass::forName($details['class'][DETAIL_GENERIC][0]);
}
/**
* Returns generic type arguments
*
* @return lang.Type[]
* @throws lang.IllegalStateException if this class is not a generic
*/
public function genericArguments() {
if (!($details= self::detailsForClass($this->name))) return NULL;
if (!isset($details['class'][DETAIL_GENERIC])) {
throw new IllegalStateException('Class '.$this->name.' is not generic');
}
if (!isset($details['class'][DETAIL_GENERIC][1])) {
$details['class'][DETAIL_GENERIC][1]= array_map(
array(xp::reflect('lang.Type'), 'forName'),
$details['class'][DETAIL_GENERIC][2]
);
unset($details['class'][DETAIL_GENERIC][2]);
}
return $details['class'][DETAIL_GENERIC][1];
}
/**
* Returns whether this class is generic
*
* @return bool
*/
public function isGeneric() {
if (!($details= self::detailsForClass($this->name))) return FALSE;
return isset($details['class'][DETAIL_GENERIC]);
}
/**
* Returns the XPClass object associated with the class with the given
* string name. Uses the default classloader if none is specified.
*
* @param string name - e.g. "io.File", "rdbms.mysql.MySQL"
* @param lang.IClassLoader classloader default NULL
* @return lang.XPClass class object
* @throws lang.ClassNotFoundException when there is no such class
*/
public static function forName($name, IClassLoader $classloader= NULL) {
if (NULL === $classloader) {
$classloader= ClassLoader::getDefault();
}
return $classloader->loadClass((string)$name);
}
/**
* Returns type literal
*
* @return string
*/
public function literal() {
return xp::reflect($this->name);
}
/**
* Returns an array containing class objects representing all the
* public classes
*
* @return lang.XPClass[] class objects
*/
public static function getClasses() {
$ret= array();
foreach (get_declared_classes() as $name) {
if (isset(xp::$registry['class.'.$name])) $ret[]= new self($name);
}
return $ret;
}
}
?>
<?php
/* This class is part of the XP framework
*
* $Id$
*/
uses(
'lang.Type',
'lang.reflect.Method',
'lang.reflect.Field',
'lang.reflect.Constructor',
'lang.reflect.Modifiers',
'lang.reflect.Package'
);
define('DETAIL_ARGUMENTS', 1);
define('DETAIL_RETURNS', 2);
define('DETAIL_THROWS', 3);
define('DETAIL_COMMENT', 4);
define('DETAIL_ANNOTATIONS', 5);
define('DETAIL_NAME', 6);
define('DETAIL_GENERIC', 7);
/**
* Represents classes. Every instance of an XP class has an method
* called getClass() which returns an instance of this class.
*
* Warning
* =======
* Do not construct this class publicly, instead use either the
* $o->getClass() syntax or the static method
* $class= XPClass::forName('fully.qualified.Name')
*
* Examples
* ========
* To retrieve the fully qualified name of a class, use this:
* <code>
* $o= new File('...');
* echo 'The class name for $o is '.$o->getClass()->getName();
* </code>
*
* Create an instance of a class:
* <code>
* $instance= XPClass::forName('util.Binford')->newInstance();
* </code>
*
* Invoke a method by its name:
* <code>
* try {
* $instance->getClass()->getMethod('connect')->invoke($instance);
* } catch (TargetInvocationException $e) {
* $e->getCause()->printStackTrace();
* }
* </code>
*
* @see xp://lang.Object#getClass
* @see xp://lang.XPClass#forName
* @test xp://net.xp_framework.unittest.reflection.ReflectionTest
* @test xp://net.xp_framework.unittest.reflection.ClassDetailsTest
* @test xp://net.xp_framework.unittest.reflection.IsInstanceTest
* @test xp://net.xp_framework.unittest.reflection.ClassCastingTest
* @purpose Reflection
*/
class XPClass extends Type {
protected $_class= NULL;
public $_reflect= NULL;
private static $DECLARING_CLASS_BUG= FALSE;
static function __static() {
self::$DECLARING_CLASS_BUG= version_compare(PHP_VERSION, '5.2.10', 'lt');
}
/**
* Constructor
*
* @param var ref either a class name, a ReflectionClass instance or an object
* @throws lang.IllegalStateException
*/
public function __construct($ref) {
if ($ref instanceof ReflectionClass) {
$this->_reflect= $ref;
$this->_class= $ref->getName();
} else if (is_object($ref)) {
$this->_reflect= new ReflectionClass($ref);
$this->_class= get_class($ref);
} else {
try {
$this->_reflect= new ReflectionClass((string)$ref);
} catch (ReflectionException $e) {
throw new IllegalStateException($e->getMessage());
}
$this->_class= $ref;
}
parent::__construct(xp::nameOf($this->_class));
}
/**
* Returns simple name
*
* @return string
*/
public function getSimpleName() {
return FALSE === ($p= strrpos(substr($this->name, 0, strcspn($this->name, '<')), '.'))
? $this->name // Already unqualified
: substr($this->name, $p+ 1) // Full name
;
}
/**
* Retrieves the package associated with this class
*
* @return lang.reflect.Package
*/
public function getPackage() {
return Package::forName(substr($this->name, 0, strrpos($this->name, '.')));
}
/**
* Creates a new instance of the class represented by this Class object.
* The class is instantiated as if by a new expression with an empty argument list.
*
* Example
* =======
* <code>
* try {
* $o= XPClass::forName($name)->newInstance();
* } catch (ClassNotFoundException $e) {
* // handle it!
* }
* </code>
*
* Example (passing arguments)
* ===========================
* <code>
* try {
* $o= XPClass::forName('peer.Socket')->newInstance('localhost', 6100);
* } catch (ClassNotFoundException $e) {
* // handle it!
* }
* </code>
*
* @param var* args
* @return lang.Object
* @throws lang.IllegalAccessException in case this class cannot be instantiated
*/
public function newInstance() {
if ($this->_reflect->isInterface()) {
throw new IllegalAccessException('Cannot instantiate interfaces ('.$this->name.')');
} else if ($this->_reflect->isAbstract()) {
throw new IllegalAccessException('Cannot instantiate abstract classes ('.$this->name.')');
}
try {
if (!$this->hasConstructor()) return $this->_reflect->newInstance();
$args= func_get_args();
return $this->_reflect->newInstanceArgs($args);
} catch (ReflectionException $e) {
throw new IllegalAccessException($e->getMessage());
}
}
/**
* Gets class methods for this class
*
* @return lang.reflect.Method[]
*/
public function getMethods() {
$list= array();
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2)) continue;
$list[]= new Method($this->_class, $m);
}
return $list;
}
/**
* Gets class methods declared by this class
*
* @return lang.reflect.Method[]
*/
public function getDeclaredMethods() {
$list= array();
if (self::$DECLARING_CLASS_BUG) {
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2) || $m->getDeclaringClass()->getName() !== $this->_reflect->name) continue;
$list[]= new Method($this->_class, $m);
}
} else {
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2) || $m->class !== $this->_reflect->name) continue;
$list[]= new Method($this->_class, $m);
}
}
return $list;
}
/**
* Gets a method by a specified name.
*
* @param string name
* @return lang.reflect.Method
* @see xp://lang.reflect.Method
* @throws lang.ElementNotFoundException
*/
public function getMethod($name) {
if ($this->hasMethod($name)) {
return new Method($this->_class, $this->_reflect->getMethod($name));
}
raise('lang.ElementNotFoundException', 'No such method "'.$name.'" in class '.$this->name);
}
/**
* Checks whether this class has a method named "$method" or not.
*
* Note
* ====
* Since in PHP, methods are case-insensitive, calling hasMethod('toString')
* will provide the same result as hasMethod('tostring')
*
* @param string method the method's name
* @return bool TRUE if method exists
*/
public function hasMethod($method) {
return ((0 === strncmp('__', $method, 2))
? FALSE
: $this->_reflect->hasMethod($method)
);
}
/**
* Retrieve if a constructor exists
*
* @return bool
*/
public function hasConstructor() {
return $this->_reflect->hasMethod('__construct');
}
/**
* Retrieves this class' constructor.
*
* @return lang.reflect.Constructor
* @see xp://lang.reflect.Constructor
* @throws lang.ElementNotFoundException
*/
public function getConstructor() {
if ($this->hasConstructor()) {
return new Constructor($this->_class, $this->_reflect->getMethod('__construct'));
}
raise('lang.ElementNotFoundException', 'No constructor in class '.$this->name);
}
/**
* Retrieve a list of all member variables
*
* @return lang.reflect.Field[] array of field objects
*/
public function getFields() {
$f= array();
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name) continue;
$f[]= new Field($this->_class, $p);
}
return $f;
}
/**
* Retrieve a list of member variables declared in this class
*
* @return lang.reflect.Field[] array of field objects
*/
public function getDeclaredFields() {
$list= array();
if (self::$DECLARING_CLASS_BUG) {
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name || $p->getDeclaringClass()->getName() !== $this->_reflect->name) continue;
$list[]= new Field($this->_class, $p);
}
} else {
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name || $p->class !== $this->_reflect->name) continue;
$list[]= new Field($this->_class, $p);
}
}
return $list;
}
/**
* Retrieve a field by a specified name.
*
* @param string name
* @return lang.reflect.Field
* @throws lang.ElementNotFoundException
*/
public function getField($name) {
if ($this->hasField($name)) {
return new Field($this->_class, $this->_reflect->getProperty($name));
}
raise('lang.ElementNotFoundException', 'No such field "'.$name.'" in class '.$this->name);
}
/**
* Checks whether this class has a field named "$field" or not.
*
* @param string field the fields's name
* @return bool TRUE if field exists
*/
public function hasField($field) {
return '__id' == $field ? FALSE : $this->_reflect->hasProperty($field);
}
/**
* Retrieve the parent class's class object. Returns NULL if there
* is no parent class.
*
* @return lang.XPClass class object
*/
public function getParentclass() {
return ($parent= $this->_reflect->getParentClass()) ? new self($parent) : NULL;
}
/**
* Checks whether this class has a constant named "$constant" or not
*
* @param string constant
* @return bool
*/
public function hasConstant($constant) {
return $this->_reflect->hasConstant($constant);
}
/**
* Retrieve a constant by a specified name.
*
* @param string constant
* @return var
* @throws lang.ElementNotFoundException in case constant does not exist
*/
public function getConstant($constant) {
if ($this->hasConstant($constant)) {
return $this->_reflect->getConstant($constant);
}
raise('lang.ElementNotFoundException', 'No such constants "'.$constant.'" in class '.$this->name);
}
/**
* Cast a given object to the class represented by this object
*
* @param lang.Generic expression
* @return lang.Generic the given expression
* @throws lang.ClassCastException
*/
public function cast(Generic $expression= NULL) {
if (NULL === $expression) {
return xp::null();
} else if (is($this->name, $expression)) {
return $expression;
}
raise('lang.ClassCastException', 'Cannot cast '.xp::typeOf($expression).' to '.$this->name);
}
/**
* Tests whether this class is a subclass of a specified class.
*
* @param var class either a string or an XPClass object
* @return bool
*/
public function isSubclassOf($class) {
if (!($class instanceof self)) $class= XPClass::forName($class);
if ($class->name == $this->name) return FALSE; // Catch bordercase (ZE bug?)
return $this->_reflect->isSubclassOf($class->_reflect);
}
/**
* Determines whether the specified object is an instance of this
* class. This is the equivalent of the is() core functionality.
*
* Examples
* ========
* <code>
* uses('io.File', 'io.TempFile');
* $class= XPClass::forName('io.File');
*
* var_dump($class->isInstance(new TempFile())); // TRUE
* var_dump($class->isInstance(new File())); // TRUE
* var_dump($class->isInstance(new Object())); // FALSE
* </code>
*
* @param var obj
* @return bool
*/
public function isInstance($obj) {
return is($this->name, $obj);
}
/**
* Determines if this XPClass object represents an interface type.
*
* @return bool
*/
public function isInterface() {
return $this->_reflect->isInterface();
}
/**
* Determines if this XPClass object represents an interface type.
*
* @return bool
*/
public function isEnum() {
$e= xp::reflect('lang.Enum');
return class_exists($e) && $this->_reflect->isSubclassOf($e);
}
/**
* Retrieve interfaces this class implements
*
* @return lang.XPClass[]
*/
public function getInterfaces() {
$r= array();
foreach ($this->_reflect->getInterfaces() as $iface) {
$r[]= new self($iface->getName());
}
return $r;
}
/**
* Retrieve interfaces this class implements in its declaration
*
* @return lang.XPClass[]
*/
public function getDeclaredInterfaces() {
$is= $this->_reflect->getInterfaces();
if ($parent= $this->_reflect->getParentclass()) {
$ip= $parent->getInterfaces();
} else {
$ip= array();
}
$filter= array();
foreach ($is as $iname => $i) {
// Parent class implements this interface
if (isset($ip[$iname])) {
$filter[$iname]= TRUE;
continue;
}
// Interface is implemented because it's the parent of another interface
foreach ($i->getInterfaces() as $pname => $p) {
if (isset($is[$pname])) $filter[$pname]= TRUE;
}
}
$r= array();
foreach ($is as $iname => $i) {
if (!isset($filter[$iname])) $r[]= new self($i);
}
return $r;
}
/**
* Retrieves the api doc comment for this class. Returns NULL if
* no documentation is present.
*
* @return string
*/
public function getComment() {
if (!($details= self::detailsForClass($this->name))) return NULL;
return $details['class'][DETAIL_COMMENT];
}
/**
* Retrieves this class' modifiers
*
* @see xp://lang.reflect.Modifiers
* @return int
*/
public function getModifiers() {
$r= MODIFIER_PUBLIC;
// Map PHP reflection modifiers to generic form
$m= $this->_reflect->getModifiers();
$m & ReflectionClass::IS_EXPLICIT_ABSTRACT && $r |= MODIFIER_ABSTRACT;
$m & ReflectionClass::IS_IMPLICIT_ABSTRACT && $r |= MODIFIER_ABSTRACT;
$m & ReflectionClass::IS_FINAL && $r |= MODIFIER_FINAL;
return $r;
}
/**
* Check whether an annotation exists
*
* @param string name
* @param string key default NULL
* @return bool
*/
public function hasAnnotation($name, $key= NULL) {
$details= self::detailsForClass($this->name);
return $details && ($key
? @array_key_exists($key, @$details['class'][DETAIL_ANNOTATIONS][$name])
: @array_key_exists($name, @$details['class'][DETAIL_ANNOTATIONS])
);
}
/**
* Retrieve annotation by name
*
* @param string name
* @param string key default NULL
* @return var
* @throws lang.ElementNotFoundException
*/
public function getAnnotation($name, $key= NULL) {
$details= self::detailsForClass($this->name);
if (!$details || !($key
? @array_key_exists($key, @$details['class'][DETAIL_ANNOTATIONS][$name])
: @array_key_exists($name, @$details['class'][DETAIL_ANNOTATIONS])
)) return raise(
'lang.ElementNotFoundException',
'Annotation "'.$name.($key ? '.'.$key : '').'" does not exist'
);
return ($key
? $details['class'][DETAIL_ANNOTATIONS][$name][$key]
: $details['class'][DETAIL_ANNOTATIONS][$name]
);
}
/**
* Retrieve whether a method has annotations
*
* @return bool
*/
public function hasAnnotations() {
$details= self::detailsForClass($this->name);
return $details ? !empty($details['class'][DETAIL_ANNOTATIONS]) : FALSE;
}
/**
* Retrieve all of a method's annotations
*
* @return array annotations
*/
public function getAnnotations() {
$details= self::detailsForClass($this->name);
return $details ? $details['class'][DETAIL_ANNOTATIONS] : array();
}
/**
* Retrieve the class loader a class was loaded with.
*
* @return lang.IClassLoader
*/
public function getClassLoader() {
return self::_classLoaderFor($this->name);
}
/**
* Fetch a class' classloader by its name
*
* @param string name fqcn of class
* @return lang.IClassLoader
*/
protected static function _classLoaderFor($name) {
if (isset(xp::$registry[$l= 'classloader.'.$name])) {
sscanf(xp::$registry[$l], '%[^:]://%[^$]', $cl, $argument);
return call_user_func(array(xp::reflect($cl), 'instanceFor'), $argument);
}
return NULL; // Internal class, e.g.
}
/**
* Parses annotation string
*
* @param string input
* @param string context the class name, and optionally the line number.
* @return [:var]
* @throws lang.ClassFormatException
*/
public static function parseAnnotations($input, $context) {
ob_start();
$annotations= eval('return array('.($eval= preg_replace(
array('/@([a-z_]+),/i', '/@([a-z_]+)\(\'([^\']+)\'\)/ie', '/@([a-z_]+)\(/i', '/(\(|, *)([a-z_]+) *= */i'),
array('\'$1\' => NULL,', '"\'$1\' => urldecode(\'".urlencode(\'$2\')."\')"', '\'$1\' => array(', '$1\'$2\' => '),
trim($input, "[]# \t\n\r").','
)).');');
$msg= ltrim(ob_get_contents(), ini_get('error_prepend_string')."\r\n\t ");
if (FALSE === $annotations || $msg) {
ob_end_clean();
xp::gc();
raise('lang.ClassFormatException', 'Parse error: '.$msg.' of "'.addcslashes($eval, "\0..\17").'"');
}
ob_end_clean();
return $annotations;
}
/**
* Retrieve details for a specified class. Note: Results from this
* method are cached!
*
* @param string class fully qualified class name
* @return array or NULL to indicate no details are available
*/
public static function detailsForClass($class) {
if (!$class) return NULL; // Border case
if (isset(xp::$registry['details.'.$class])) return xp::$registry['details.'.$class];
// Retrieve class' sourcecode
$cl= self::_classLoaderFor($class);
if (!$cl || !($bytes= $cl->loadClassBytes($class))) return NULL;
$details= array(array(), array());
$annotations= array();
$comment= NULL;
$members= TRUE;
$parsed= '';
$tokens= token_get_all($bytes);
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
switch ($tokens[$i][0]) {
case T_DOC_COMMENT:
$comment= $tokens[$i][1];
break;
case T_COMMENT:
if ('#' === $tokens[$i][1]{0}) { // Annotations
if ('[' === $tokens[$i][1]{1}) {
$parsed= substr($tokens[$i][1], 2);
} else {
$parsed.= substr($tokens[$i][1], 1);
}
if (']' == substr(rtrim($tokens[$i][1]), -1)) {
$annotations= self::parseAnnotations(
trim($parsed, " \t\n\r"),
$class.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : '')
);
$parsed= '';
}
}
break;
case T_CLASS:
case T_INTERFACE:
if ('' !== $parsed) raise(
'lang.ClassFormatException',
'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : '')
);
$details['class']= array(
DETAIL_COMMENT => trim(preg_replace('/\n \* ?/', "\n", "\n".substr(
$comment,
4, // "/**\n"
strpos($comment, '* @')- 2 // position of first details token
))),
DETAIL_ANNOTATIONS => $annotations
);
$annotations= array();
$comment= NULL;
break;
case T_VARIABLE:
if (!$members) break;
// Have a member variable
'' === $parsed || raise('lang.ClassFormatException', 'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.', line '.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : ''));
$name= substr($tokens[$i][1], 1);
$details[0][$name]= array(
DETAIL_ANNOTATIONS => $annotations
);
$annotations= array();
break;
case T_FUNCTION:
'' === $parsed || raise('lang.ClassFormatException', 'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.', line '.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : ''));
$members= FALSE;
while (T_STRING !== $tokens[$i][0]) $i++;
$m= $tokens[$i][1];
$details[1][$m]= array(
DETAIL_ARGUMENTS => array(),
DETAIL_RETURNS => 'void',
DETAIL_THROWS => array(),
DETAIL_COMMENT => trim(preg_replace('/\n \* ?/', "\n", "\n".substr(
$comment,
4, // "/**\n"
strpos($comment, '* @')- 2 // position of first details token
))),
DETAIL_ANNOTATIONS => $annotations,
DETAIL_NAME => $tokens[$i][1]
);
$matches= NULL;
preg_match_all(
'/@([a-z]+)\s*([^<\r\n]+<[^>]+>|[^\r\n ]+) ?([^\r\n ]+)?/',
$comment,
$matches,
PREG_SET_ORDER
);
$annotations= array();
$comment= NULL;
foreach ($matches as $match) {
switch ($match[1]) {
case 'param':
$details[1][$m][DETAIL_ARGUMENTS][]= $match[2];
break;
case 'return':
$details[1][$m][DETAIL_RETURNS]= $match[2];
break;
case 'throws':
$details[1][$m][DETAIL_THROWS][]= $match[2];
break;
}
}
break;
default:
// Empty
}
}
// Return details for specified class
xp::$registry['details.'.$class]= $details;
return $details;
}
/**
* Retrieve details for a specified class and method. Note: Results
* from this method are cached!
*
* @param string class unqualified class name
* @param string method
* @return array or NULL if not available
*/
public static function detailsForMethod($class, $method) {
$details= self::detailsForClass(xp::nameOf($class));
return $details ? (isset($details[1][$method]) ? $details[1][$method] : NULL) : NULL;
}
/**
* Retrieve details for a specified class and field. Note: Results
* from this method are cached!
*
* @param string class unqualified class name
* @param string method
* @return array or NULL if not available
*/
public static function detailsForField($class, $field) {
$details= self::detailsForClass(xp::nameOf($class));
return $details ? (isset($details[0][$field]) ? $details[0][$field] : NULL) : NULL;
}
/**
* Creates a generic type
*
* @param lang.XPClass self
* @param lang.Type[] arguments
* @return lang.XPClass
*/
public static function createGenericType(XPClass $self, array $arguments) {
// Verify
if (!$self->isGenericDefinition()) {
throw new IllegalStateException('Class '.$self->name.' is not a generic definition');
}
$components= $self->genericComponents();
$cs= sizeof($components);
if ($cs != sizeof($arguments)) {
throw new IllegalArgumentException(sprintf(
'Class %s expects %d component(s) <%s>, %d argument(s) given',
$self->name,
$cs,
implode(', ', $components),
sizeof($arguments)
));
}
// Compose names
$cn= $qc= '';
foreach ($arguments as $typearg) {
$cn.= '¸'.$typearg->literal();
$qc.= ','.$typearg->getName();
}
$name= xp::reflect($self->name).'··'.substr($cn, 1);
$qname= $self->name.'<'.substr($qc, 1).'>';
// Create class if it doesn't exist yet
if (!class_exists($name, FALSE) && !interface_exists($name, FALSE)) {
$meta= isset(xp::$registry['details.'.$self->name]) ? xp::$registry['details.'.$self->name] : array(
'class' => NULL,
0 => array(),
1 => array()
);
// Parse placeholders into a lookup map
$placeholders= array();
foreach ($components as $i => $component) {
$placeholders[$component]= $arguments[$i]->getName();
}
// Work on sourcecode
$cl= self::_classLoaderFor($self->name);
if (!$cl || !($bytes= $cl->loadClassBytes($self->name))) {
throw new IllegalStateException($self->name);
}
// Replace source
$src= '';
$comment= NULL;
$annotations= array();
$annotation= NULL;
$matches= array();
$state= array(0);
$counter= 0;
$tokens= token_get_all($bytes);
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
if (T_COMMENT === $tokens[$i][0] && '#' === $tokens[$i][1]{0}) {
$annotations= eval('return array('.preg_replace(
array('/@([a-z_]+),/i', '/@([a-z_]+)\(\'([^\']+)\'\)/ie', '/@([a-z_]+)\(/i', '/([^a-z_@])([a-z_]+) *= */i'),
array('\'$1\' => NULL,', '"\'$1\' => urldecode(\'".urlencode(\'$2\')."\')"', '\'$1\' => array(', '$1\'$2\' => '),
trim($tokens[$i][1], "[]# \t\n\r").','
).');');
continue;
} else if (T_DOC_COMMENT === $tokens[$i][0]) {
$matches= NULL;
$comment= trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr(
$tokens[$i][1],
4, // "/**\n"
strpos($tokens[$i][1], '* @')- 2 // position of first details token
)));
preg_match_all(
'/@([a-z]+)\s*([^<\r\n]+<[^>]+>|[^\r\n ]+) ?([^\r\n ]+)?/',
$tokens[$i][1],
$matches,
PREG_SET_ORDER
);
}
if (0 === $state[0]) {
if (T_ABSTRACT === $tokens[$i][0] || T_FINAL === $tokens[$i][0]) {
$src.= $tokens[$i][1].' ';
} else if (T_CLASS === $tokens[$i][0] || T_INTERFACE === $tokens[$i][0]) {
if (NULL === $meta['class']) {
$meta['class']= array(DETAIL_COMMENT => $comment, DETAIL_ANNOTATIONS => $annotations);
}
$meta['class'][DETAIL_GENERIC]= array($self->name, $arguments);
$src.= $tokens[$i][1].' '.$name;
array_unshift($state, $tokens[$i][0]);
}
continue;
} else if (T_CLASS === $state[0]) {
if (T_EXTENDS === $tokens[$i][0]) {
if (isset($annotations['generic']['parent'])) {
$xargs= array();
foreach (explode(',', $annotations['generic']['parent']) as $j => $placeholder) {
$xargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= ' extends '.self::createGenericType($self->getParentClass(), $xargs)->literal();
} else {
$src.= ' extends '.$tokens[$i+ 2][1];
}
} else if (T_IMPLEMENTS === $tokens[$i][0]) {
$src.= ' implements';
$counter= 0;
$annotation= @$annotations['generic']['implements'];
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
$src.= ' {';
}
continue;
} else if (T_INTERFACE === $state[0]) {
if (T_EXTENDS === $tokens[$i][0]) {
$src.= ' extends';
$counter= 0;
$annotation= @$annotations['generic']['extends'];
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
$src.= ' {';
}
continue;
} else if (1 === $state[0]) { // Class body
if (T_FUNCTION === $tokens[$i][0]) {
$braces= 0;
$parameters= array();
array_unshift($state, 3);
array_unshift($state, 2);
$m= $tokens[$i+ 2][1];
if (isset($meta[1][$m])) {
$annotations= $meta[1][$m][DETAIL_ANNOTATIONS];
} else {
$meta[1][$m]= array(
DETAIL_ARGUMENTS => array(),
DETAIL_RETURNS => 'void',
DETAIL_THROWS => array(),
DETAIL_COMMENT => $comment,
DETAIL_ANNOTATIONS => $annotations,
DETAIL_NAME => $m
);
foreach ($matches as $match) {
switch ($match[1]) {
case 'param': $meta[1][$m][DETAIL_ARGUMENTS][]= $match[2]; break;
case 'return': $meta[1][$m][DETAIL_RETURNS]= $match[2]; break;
case 'throws': $meta[1][$m][DETAIL_THROWS][]= $match[2]; break;
}
}
}
} else if ('}' === $tokens[$i][0]) {
$src.= '}';
break;
} else if (T_CLOSE_TAG === $tokens[$i][0]) {
break;
}
} else if (2 === $state[0]) { // Method declaration
if ('(' === $tokens[$i][0]) {
$braces++;
} else if (')' === $tokens[$i][0]) {
$braces--;
if (0 === $braces) array_shift($state);
} else if (T_VARIABLE === $tokens[$i][0]) {
$parameters[]= $tokens[$i][1];
}
} else if (3 === $state[0]) { // Method body
if (';' === $tokens[$i][0]) {
// Abstract method
array_shift($state);
} else if ('{' === $tokens[$i][0]) {
$braces= 1;
array_shift($state);
array_unshift($state, 4);
$src.= '{';
if (isset($annotations['generic']['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations['generic']['return'], $placeholders);
}
if (isset($annotations['generic']['params'])) {
$generic= array();
foreach (explode(',', $annotations['generic']['params']) as $j => $placeholder) {
if ('' === ($replaced= strtr(ltrim($placeholder), $placeholders))) {
$generic[$j]= NULL;
} else {
$meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced;
$generic[$j]= $replaced;
}
}
foreach ($generic as $j => $type) {
if (NULL === $type) {
continue;
} else if ('...' === substr($type, -3)) {
$src.= $j ? '$·args= array_slice(func_get_args(), '.$j.');' : '$·args= func_get_args();';
$src.= (
' if (!is(\''.substr($generic[$j], 0, -3).'[]\', $·args)) throw new IllegalArgumentException('.
'"Vararg '.($j + 1).' passed to ".__METHOD__."'.
' must be of '.$type.', ".xp::stringOf($·args)." given"'.
');'
);
} else {
$src.= (
' if (!is(\''.$generic[$j].'\', '.$parameters[$j].')) throw new IllegalArgumentException('.
'"Argument '.($j + 1).' passed to ".__METHOD__."'.
' must be of '.$type.', ".xp::typeOf('.$parameters[$j].')." given"'.
');'
);
}
}
$annotations= array();
}
unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']);
continue;
}
} else if (4 === $state[0]) { // Method body
if ('{' === $tokens[$i][0]) {
$braces++;
} else if ('}' === $tokens[$i][0]) {
$braces--;
if (0 === $braces) array_shift($state);
}
} else if (5 === $state[0]) { // Implements (class), Extends (interface)
if (T_STRING === $tokens[$i][0]) {
if (isset($annotation[$counter])) {
$iargs= array();
foreach (explode(',', $annotation[$counter]) as $j => $placeholder) {
$iargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= self::createGenericType(new XPClass(new ReflectionClass($tokens[$i][1])), $iargs)->literal();
} else {
$src.= $tokens[$i][1];
}
$counter++;
continue;
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
}
}
$src.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
}
// Create class
// DEBUG fputs(STDERR, "@* ".substr($src, 0, strpos($src, '{'))." -> $qname\n");
eval($src);
method_exists($name, '__static') && call_user_func(array($name, '__static'));
unset($meta['class'][DETAIL_ANNOTATIONS]['generic']);
xp::$registry['details.'.$qname]= $meta;
xp::$registry['class.'.$name]= $qname;
}
return new XPClass(new ReflectionClass($name));
}
/**
* Reflectively creates a new type
*
* @param lang.Type[] arguments
* @return lang.XPClass
* @throws lang.IllegalStateException if this class is not a generic definition
* @throws lang.IllegalArgumentException if number of arguments does not match components
*/
public function newGenericType(array $arguments) {
return self::createGenericType($this, $arguments);
}
/**
* Returns generic type components
*
* @return string[]
* @throws lang.IllegalStateException if this class is not a generic definition
*/
public function genericComponents() {
if (!$this->isGenericDefinition()) {
throw new IllegalStateException('Class '.$this->name.' is not a generic definition');
}
$components= array();
foreach (explode(',', $this->getAnnotation('generic', 'self')) as $name) {
$components[]= ltrim($name);
}
return $components;
}
/**
* Returns whether this class is a generic definition
*
* @return bool
*/
public function isGenericDefinition() {
return $this->hasAnnotation('generic', 'self');
}
/**
* Returns generic type definition
*
* @return lang.XPClass
* @throws lang.IllegalStateException if this class is not a generic
*/
public function genericDefinition() {
if (!($details= self::detailsForClass($this->name))) return NULL;
if (!isset($details['class'][DETAIL_GENERIC])) {
throw new IllegalStateException('Class '.$this->name.' is not generic');
}
return XPClass::forName($details['class'][DETAIL_GENERIC][0]);
}
/**
* Returns generic type arguments
*
* @return lang.Type[]
* @throws lang.IllegalStateException if this class is not a generic
*/
public function genericArguments() {
if (!($details= self::detailsForClass($this->name))) return NULL;
if (!isset($details['class'][DETAIL_GENERIC])) {
throw new IllegalStateException('Class '.$this->name.' is not generic');
}
if (!isset($details['class'][DETAIL_GENERIC][1])) {
$details['class'][DETAIL_GENERIC][1]= array_map(
array(xp::reflect('lang.Type'), 'forName'),
$details['class'][DETAIL_GENERIC][2]
);
unset($details['class'][DETAIL_GENERIC][2]);
}
return $details['class'][DETAIL_GENERIC][1];
}
/**
* Returns whether this class is generic
*
* @return bool
*/
public function isGeneric() {
if (!($details= self::detailsForClass($this->name))) return FALSE;
return isset($details['class'][DETAIL_GENERIC]);
}
/**
* Returns the XPClass object associated with the class with the given
* string name. Uses the default classloader if none is specified.
*
* @param string name - e.g. "io.File", "rdbms.mysql.MySQL"
* @param lang.IClassLoader classloader default NULL
* @return lang.XPClass class object
* @throws lang.ClassNotFoundException when there is no such class
*/
public static function forName($name, IClassLoader $classloader= NULL) {
if (NULL === $classloader) {
$classloader= ClassLoader::getDefault();
}
return $classloader->loadClass((string)$name);
}
/**
* Returns type literal
*
* @return string
*/
public function literal() {
return xp::reflect($this->name);
}
/**
* Returns an array containing class objects representing all the
* public classes
*
* @return lang.XPClass[] class objects
*/
public static function getClasses() {
$ret= array();
foreach (get_declared_classes() as $name) {
if (isset(xp::$registry['class.'.$name])) $ret[]= new self($name);
}
return $ret;
}
}
?>
<?php
/* This class is part of the XP framework
*
* $Id$
*/
uses(
'lang.Type',
'lang.reflect.Method',
'lang.reflect.Field',
'lang.reflect.Constructor',
'lang.reflect.Modifiers',
'lang.reflect.Package'
);
define('DETAIL_ARGUMENTS', 1);
define('DETAIL_RETURNS', 2);
define('DETAIL_THROWS', 3);
define('DETAIL_COMMENT', 4);
define('DETAIL_ANNOTATIONS', 5);
define('DETAIL_NAME', 6);
define('DETAIL_GENERIC', 7);
/**
* Represents classes. Every instance of an XP class has an method
* called getClass() which returns an instance of this class.
*
* Warning
* =======
* Do not construct this class publicly, instead use either the
* $o->getClass() syntax or the static method
* $class= XPClass::forName('fully.qualified.Name')
*
* Examples
* ========
* To retrieve the fully qualified name of a class, use this:
* <code>
* $o= new File('...');
* echo 'The class name for $o is '.$o->getClass()->getName();
* </code>
*
* Create an instance of a class:
* <code>
* $instance= XPClass::forName('util.Binford')->newInstance();
* </code>
*
* Invoke a method by its name:
* <code>
* try {
* $instance->getClass()->getMethod('connect')->invoke($instance);
* } catch (TargetInvocationException $e) {
* $e->getCause()->printStackTrace();
* }
* </code>
*
* @see xp://lang.Object#getClass
* @see xp://lang.XPClass#forName
* @test xp://net.xp_framework.unittest.reflection.ReflectionTest
* @test xp://net.xp_framework.unittest.reflection.ClassDetailsTest
* @test xp://net.xp_framework.unittest.reflection.IsInstanceTest
* @test xp://net.xp_framework.unittest.reflection.ClassCastingTest
* @purpose Reflection
*/
class XPClass extends Type {
protected $_class= NULL;
public $_reflect= NULL;
private static $DECLARING_CLASS_BUG= FALSE;
static function __static() {
self::$DECLARING_CLASS_BUG= version_compare(PHP_VERSION, '5.2.10', 'lt');
}
/**
* Constructor
*
* @param var ref either a class name, a ReflectionClass instance or an object
* @throws lang.IllegalStateException
*/
public function __construct($ref) {
if ($ref instanceof ReflectionClass) {
$this->_reflect= $ref;
$this->_class= $ref->getName();
} else if (is_object($ref)) {
$this->_reflect= new ReflectionClass($ref);
$this->_class= get_class($ref);
} else {
try {
$this->_reflect= new ReflectionClass((string)$ref);
} catch (ReflectionException $e) {
throw new IllegalStateException($e->getMessage());
}
$this->_class= $ref;
}
parent::__construct(xp::nameOf($this->_class));
}
/**
* Returns simple name
*
* @return string
*/
public function getSimpleName() {
return FALSE === ($p= strrpos(substr($this->name, 0, strcspn($this->name, '<')), '.'))
? $this->name // Already unqualified
: substr($this->name, $p+ 1) // Full name
;
}
/**
* Retrieves the package associated with this class
*
* @return lang.reflect.Package
*/
public function getPackage() {
return Package::forName(substr($this->name, 0, strrpos($this->name, '.')));
}
/**
* Creates a new instance of the class represented by this Class object.
* The class is instantiated as if by a new expression with an empty argument list.
*
* Example
* =======
* <code>
* try {
* $o= XPClass::forName($name)->newInstance();
* } catch (ClassNotFoundException $e) {
* // handle it!
* }
* </code>
*
* Example (passing arguments)
* ===========================
* <code>
* try {
* $o= XPClass::forName('peer.Socket')->newInstance('localhost', 6100);
* } catch (ClassNotFoundException $e) {
* // handle it!
* }
* </code>
*
* @param var* args
* @return lang.Object
* @throws lang.IllegalAccessException in case this class cannot be instantiated
*/
public function newInstance() {
if ($this->_reflect->isInterface()) {
throw new IllegalAccessException('Cannot instantiate interfaces ('.$this->name.')');
} else if ($this->_reflect->isAbstract()) {
throw new IllegalAccessException('Cannot instantiate abstract classes ('.$this->name.')');
}
try {
if (!$this->hasConstructor()) return $this->_reflect->newInstance();
$args= func_get_args();
return $this->_reflect->newInstanceArgs($args);
} catch (ReflectionException $e) {
throw new IllegalAccessException($e->getMessage());
}
}
/**
* Gets class methods for this class
*
* @return lang.reflect.Method[]
*/
public function getMethods() {
$list= array();
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2)) continue;
$list[]= new Method($this->_class, $m);
}
return $list;
}
/**
* Gets class methods declared by this class
*
* @return lang.reflect.Method[]
*/
public function getDeclaredMethods() {
$list= array();
if (self::$DECLARING_CLASS_BUG) {
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2) || $m->getDeclaringClass()->getName() !== $this->_reflect->name) continue;
$list[]= new Method($this->_class, $m);
}
} else {
foreach ($this->_reflect->getMethods() as $m) {
if (0 == strncmp('__', $m->getName(), 2) || $m->class !== $this->_reflect->name) continue;
$list[]= new Method($this->_class, $m);
}
}
return $list;
}
/**
* Gets a method by a specified name.
*
* @param string name
* @return lang.reflect.Method
* @see xp://lang.reflect.Method
* @throws lang.ElementNotFoundException
*/
public function getMethod($name) {
if ($this->hasMethod($name)) {
return new Method($this->_class, $this->_reflect->getMethod($name));
}
raise('lang.ElementNotFoundException', 'No such method "'.$name.'" in class '.$this->name);
}
/**
* Checks whether this class has a method named "$method" or not.
*
* Note
* ====
* Since in PHP, methods are case-insensitive, calling hasMethod('toString')
* will provide the same result as hasMethod('tostring')
*
* @param string method the method's name
* @return bool TRUE if method exists
*/
public function hasMethod($method) {
return ((0 === strncmp('__', $method, 2))
? FALSE
: $this->_reflect->hasMethod($method)
);
}
/**
* Retrieve if a constructor exists
*
* @return bool
*/
public function hasConstructor() {
return $this->_reflect->hasMethod('__construct');
}
/**
* Retrieves this class' constructor.
*
* @return lang.reflect.Constructor
* @see xp://lang.reflect.Constructor
* @throws lang.ElementNotFoundException
*/
public function getConstructor() {
if ($this->hasConstructor()) {
return new Constructor($this->_class, $this->_reflect->getMethod('__construct'));
}
raise('lang.ElementNotFoundException', 'No constructor in class '.$this->name);
}
/**
* Retrieve a list of all member variables
*
* @return lang.reflect.Field[] array of field objects
*/
public function getFields() {
$f= array();
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name) continue;
$f[]= new Field($this->_class, $p);
}
return $f;
}
/**
* Retrieve a list of member variables declared in this class
*
* @return lang.reflect.Field[] array of field objects
*/
public function getDeclaredFields() {
$list= array();
if (self::$DECLARING_CLASS_BUG) {
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name || $p->getDeclaringClass()->getName() !== $this->_reflect->name) continue;
$list[]= new Field($this->_class, $p);
}
} else {
foreach ($this->_reflect->getProperties() as $p) {
if ('__id' === $p->name || $p->class !== $this->_reflect->name) continue;
$list[]= new Field($this->_class, $p);
}
}
return $list;
}
/**
* Retrieve a field by a specified name.
*
* @param string name
* @return lang.reflect.Field
* @throws lang.ElementNotFoundException
*/
public function getField($name) {
if ($this->hasField($name)) {
return new Field($this->_class, $this->_reflect->getProperty($name));
}
raise('lang.ElementNotFoundException', 'No such field "'.$name.'" in class '.$this->name);
}
/**
* Checks whether this class has a field named "$field" or not.
*
* @param string field the fields's name
* @return bool TRUE if field exists
*/
public function hasField($field) {
return '__id' == $field ? FALSE : $this->_reflect->hasProperty($field);
}
/**
* Retrieve the parent class's class object. Returns NULL if there
* is no parent class.
*
* @return lang.XPClass class object
*/
public function getParentclass() {
return ($parent= $this->_reflect->getParentClass()) ? new self($parent) : NULL;
}
/**
* Checks whether this class has a constant named "$constant" or not
*
* @param string constant
* @return bool
*/
public function hasConstant($constant) {
return $this->_reflect->hasConstant($constant);
}
/**
* Retrieve a constant by a specified name.
*
* @param string constant
* @return var
* @throws lang.ElementNotFoundException in case constant does not exist
*/
public function getConstant($constant) {
if ($this->hasConstant($constant)) {
return $this->_reflect->getConstant($constant);
}
raise('lang.ElementNotFoundException', 'No such constants "'.$constant.'" in class '.$this->name);
}
/**
* Cast a given object to the class represented by this object
*
* @param lang.Generic expression
* @return lang.Generic the given expression
* @throws lang.ClassCastException
*/
public function cast(Generic $expression= NULL) {
if (NULL === $expression) {
return xp::null();
} else if (is($this->name, $expression)) {
return $expression;
}
raise('lang.ClassCastException', 'Cannot cast '.xp::typeOf($expression).' to '.$this->name);
}
/**
* Tests whether this class is a subclass of a specified class.
*
* @param var class either a string or an XPClass object
* @return bool
*/
public function isSubclassOf($class) {
if (!($class instanceof self)) $class= XPClass::forName($class);
if ($class->name == $this->name) return FALSE; // Catch bordercase (ZE bug?)
return $this->_reflect->isSubclassOf($class->_reflect);
}
/**
* Determines whether the specified object is an instance of this
* class. This is the equivalent of the is() core functionality.
*
* Examples
* ========
* <code>
* uses('io.File', 'io.TempFile');
* $class= XPClass::forName('io.File');
*
* var_dump($class->isInstance(new TempFile())); // TRUE
* var_dump($class->isInstance(new File())); // TRUE
* var_dump($class->isInstance(new Object())); // FALSE
* </code>
*
* @param var obj
* @return bool
*/
public function isInstance($obj) {
return is($this->name, $obj);
}
/**
* Determines if this XPClass object represents an interface type.
*
* @return bool
*/
public function isInterface() {
return $this->_reflect->isInterface();
}
/**
* Determines if this XPClass object represents an interface type.
*
* @return bool
*/
public function isEnum() {
$e= xp::reflect('lang.Enum');
return class_exists($e) && $this->_reflect->isSubclassOf($e);
}
/**
* Retrieve interfaces this class implements
*
* @return lang.XPClass[]
*/
public function getInterfaces() {
$r= array();
foreach ($this->_reflect->getInterfaces() as $iface) {
$r[]= new self($iface->getName());
}
return $r;
}
/**
* Retrieve interfaces this class implements in its declaration
*
* @return lang.XPClass[]
*/
public function getDeclaredInterfaces() {
$is= $this->_reflect->getInterfaces();
if ($parent= $this->_reflect->getParentclass()) {
$ip= $parent->getInterfaces();
} else {
$ip= array();
}
$filter= array();
foreach ($is as $iname => $i) {
// Parent class implements this interface
if (isset($ip[$iname])) {
$filter[$iname]= TRUE;
continue;
}
// Interface is implemented because it's the parent of another interface
foreach ($i->getInterfaces() as $pname => $p) {
if (isset($is[$pname])) $filter[$pname]= TRUE;
}
}
$r= array();
foreach ($is as $iname => $i) {
if (!isset($filter[$iname])) $r[]= new self($i);
}
return $r;
}
/**
* Retrieves the api doc comment for this class. Returns NULL if
* no documentation is present.
*
* @return string
*/
public function getComment() {
if (!($details= self::detailsForClass($this->name))) return NULL;
return $details['class'][DETAIL_COMMENT];
}
/**
* Retrieves this class' modifiers
*
* @see xp://lang.reflect.Modifiers
* @return int
*/
public function getModifiers() {
$r= MODIFIER_PUBLIC;
// Map PHP reflection modifiers to generic form
$m= $this->_reflect->getModifiers();
$m & ReflectionClass::IS_EXPLICIT_ABSTRACT && $r |= MODIFIER_ABSTRACT;
$m & ReflectionClass::IS_IMPLICIT_ABSTRACT && $r |= MODIFIER_ABSTRACT;
$m & ReflectionClass::IS_FINAL && $r |= MODIFIER_FINAL;
return $r;
}
/**
* Check whether an annotation exists
*
* @param string name
* @param string key default NULL
* @return bool
*/
public function hasAnnotation($name, $key= NULL) {
$details= self::detailsForClass($this->name);
return $details && ($key
? @array_key_exists($key, @$details['class'][DETAIL_ANNOTATIONS][$name])
: @array_key_exists($name, @$details['class'][DETAIL_ANNOTATIONS])
);
}
/**
* Retrieve annotation by name
*
* @param string name
* @param string key default NULL
* @return var
* @throws lang.ElementNotFoundException
*/
public function getAnnotation($name, $key= NULL) {
$details= self::detailsForClass($this->name);
if (!$details || !($key
? @array_key_exists($key, @$details['class'][DETAIL_ANNOTATIONS][$name])
: @array_key_exists($name, @$details['class'][DETAIL_ANNOTATIONS])
)) return raise(
'lang.ElementNotFoundException',
'Annotation "'.$name.($key ? '.'.$key : '').'" does not exist'
);
return ($key
? $details['class'][DETAIL_ANNOTATIONS][$name][$key]
: $details['class'][DETAIL_ANNOTATIONS][$name]
);
}
/**
* Retrieve whether a method has annotations
*
* @return bool
*/
public function hasAnnotations() {
$details= self::detailsForClass($this->name);
return $details ? !empty($details['class'][DETAIL_ANNOTATIONS]) : FALSE;
}
/**
* Retrieve all of a method's annotations
*
* @return array annotations
*/
public function getAnnotations() {
$details= self::detailsForClass($this->name);
return $details ? $details['class'][DETAIL_ANNOTATIONS] : array();
}
/**
* Retrieve the class loader a class was loaded with.
*
* @return lang.IClassLoader
*/
public function getClassLoader() {
return self::_classLoaderFor($this->name);
}
/**
* Fetch a class' classloader by its name
*
* @param string name fqcn of class
* @return lang.IClassLoader
*/
protected static function _classLoaderFor($name) {
if (isset(xp::$registry[$l= 'classloader.'.$name])) {
sscanf(xp::$registry[$l], '%[^:]://%[^$]', $cl, $argument);
return call_user_func(array(xp::reflect($cl), 'instanceFor'), $argument);
}
return NULL; // Internal class, e.g.
}
/**
* Parses annotation string
*
* @param string input
* @param string context the class name, and optionally the line number.
* @return [:var]
* @throws lang.ClassFormatException
*/
public static function parseAnnotations($input, $context) {
$offset= 2; // "#["
$annotations= array();
$annotation= $value= NULL;
while (1) {
$state= $input{$offset};
if ('@' === $state) {
$span= strcspn($input, ',(]', $offset);
$annotation= substr($input, $offset+ 1, $span- 1);
$offset+= $span;
} else if (']' === $state) {
$annotations[$annotation]= $value;
break;
} else if ('(' === $state) {
$brackets= 1;
$voffset= $offset;
do {
$span= strcspn($input, '()', $offset+ 1);
$offset+= $span + 1;
$state= $input{$offset};
if (')' === $state) {
$brackets--;
} else if ('(' === $state) {
$brackets++;
} else {
raise('lang.ClassFormatException', 'Unclosed (');
}
} while ($brackets);
$value= substr($input, $voffset+ 1, $offset- $voffset- 1);
if ("'" === $value{0} || "'" === $value{0}) {
$value= substr($value, 1, -1);
} else {
$eval= preg_replace('/(^|, *)([a-z]+) *= */i', '$1\'$2\' => ', $value);
$value= eval('return array('.$eval.');');
if (FALSE === $value) {
raise('lang.ClassFormatException', $eval);
}
}
$offset++; // ")"
} else if (',' === $state) {
$annotations[$annotation]= $value;
$annotation= $value= NULL;
if (FALSE === ($offset= strpos($input, '@', $offset))) {
raise('lang.ClassFormatException', 'Expecting @');
}
} else {
raise('lang.ClassFormatException', 'Unknown state '.$state);
}
}
return $annotations;
}
/**
* Retrieve details for a specified class. Note: Results from this
* method are cached!
*
* @param string class fully qualified class name
* @return array or NULL to indicate no details are available
*/
public static function detailsForClass($class) {
if (!$class) return NULL; // Border case
if (isset(xp::$registry['details.'.$class])) return xp::$registry['details.'.$class];
// Retrieve class' sourcecode
$cl= self::_classLoaderFor($class);
if (!$cl || !($bytes= $cl->loadClassBytes($class))) return NULL;
$details= array(array(), array());
$annotations= array();
$comment= NULL;
$members= TRUE;
$parsed= '';
$tokens= token_get_all($bytes);
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
switch ($tokens[$i][0]) {
case T_DOC_COMMENT:
$comment= $tokens[$i][1];
break;
case T_COMMENT:
if ('#' === $tokens[$i][1]{0}) { // Annotations
if ('[' === $tokens[$i][1]{1}) {
$parsed= substr($tokens[$i][1], 2);
} else {
$parsed.= substr($tokens[$i][1], 1);
}
if (']' == substr(rtrim($tokens[$i][1]), -1)) {
$annotations= self::parseAnnotations(
trim($parsed, " \t\n\r"),
$class.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : '')
);
$parsed= '';
}
}
break;
case T_CLASS:
case T_INTERFACE:
if ('' !== $parsed) raise(
'lang.ClassFormatException',
'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : '')
);
$details['class']= array(
DETAIL_COMMENT => trim(preg_replace('/\n \* ?/', "\n", "\n".substr(
$comment,
4, // "/**\n"
strpos($comment, '* @')- 2 // position of first details token
))),
DETAIL_ANNOTATIONS => $annotations
);
$annotations= array();
$comment= NULL;
break;
case T_VARIABLE:
if (!$members) break;
// Have a member variable
'' === $parsed || raise('lang.ClassFormatException', 'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.', line '.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : ''));
$name= substr($tokens[$i][1], 1);
$details[0][$name]= array(
DETAIL_ANNOTATIONS => $annotations
);
$annotations= array();
break;
case T_FUNCTION:
'' === $parsed || raise('lang.ClassFormatException', 'Unterminated annotation "'.addcslashes($parsed, "\0..\17").'" in '.$class.', line '.(isset($tokens[$i][2]) ? ', line '.$tokens[$i][2] : ''));
$members= FALSE;
while (T_STRING !== $tokens[$i][0]) $i++;
$m= $tokens[$i][1];
$details[1][$m]= array(
DETAIL_ARGUMENTS => array(),
DETAIL_RETURNS => 'void',
DETAIL_THROWS => array(),
DETAIL_COMMENT => trim(preg_replace('/\n \* ?/', "\n", "\n".substr(
$comment,
4, // "/**\n"
strpos($comment, '* @')- 2 // position of first details token
))),
DETAIL_ANNOTATIONS => $annotations,
DETAIL_NAME => $tokens[$i][1]
);
$matches= NULL;
preg_match_all(
'/@([a-z]+)\s*([^<\r\n]+<[^>]+>|[^\r\n ]+) ?([^\r\n ]+)?/',
$comment,
$matches,
PREG_SET_ORDER
);
$annotations= array();
$comment= NULL;
foreach ($matches as $match) {
switch ($match[1]) {
case 'param':
$details[1][$m][DETAIL_ARGUMENTS][]= $match[2];
break;
case 'return':
$details[1][$m][DETAIL_RETURNS]= $match[2];
break;
case 'throws':
$details[1][$m][DETAIL_THROWS][]= $match[2];
break;
}
}
break;
default:
// Empty
}
}
// Return details for specified class
xp::$registry['details.'.$class]= $details;
return $details;
}
/**
* Retrieve details for a specified class and method. Note: Results
* from this method are cached!
*
* @param string class unqualified class name
* @param string method
* @return array or NULL if not available
*/
public static function detailsForMethod($class, $method) {
$details= self::detailsForClass(xp::nameOf($class));
return $details ? (isset($details[1][$method]) ? $details[1][$method] : NULL) : NULL;
}
/**
* Retrieve details for a specified class and field. Note: Results
* from this method are cached!
*
* @param string class unqualified class name
* @param string method
* @return array or NULL if not available
*/
public static function detailsForField($class, $field) {
$details= self::detailsForClass(xp::nameOf($class));
return $details ? (isset($details[0][$field]) ? $details[0][$field] : NULL) : NULL;
}
/**
* Creates a generic type
*
* @param lang.XPClass self
* @param lang.Type[] arguments
* @return lang.XPClass
*/
public static function createGenericType(XPClass $self, array $arguments) {
// Verify
if (!$self->isGenericDefinition()) {
throw new IllegalStateException('Class '.$self->name.' is not a generic definition');
}
$components= $self->genericComponents();
$cs= sizeof($components);
if ($cs != sizeof($arguments)) {
throw new IllegalArgumentException(sprintf(
'Class %s expects %d component(s) <%s>, %d argument(s) given',
$self->name,
$cs,
implode(', ', $components),
sizeof($arguments)
));
}
// Compose names
$cn= $qc= '';
foreach ($arguments as $typearg) {
$cn.= '¸'.$typearg->literal();
$qc.= ','.$typearg->getName();
}
$name= xp::reflect($self->name).'··'.substr($cn, 1);
$qname= $self->name.'<'.substr($qc, 1).'>';
// Create class if it doesn't exist yet
if (!class_exists($name, FALSE) && !interface_exists($name, FALSE)) {
$meta= isset(xp::$registry['details.'.$self->name]) ? xp::$registry['details.'.$self->name] : array(
'class' => NULL,
0 => array(),
1 => array()
);
// Parse placeholders into a lookup map
$placeholders= array();
foreach ($components as $i => $component) {
$placeholders[$component]= $arguments[$i]->getName();
}
// Work on sourcecode
$cl= self::_classLoaderFor($self->name);
if (!$cl || !($bytes= $cl->loadClassBytes($self->name))) {
throw new IllegalStateException($self->name);
}
// Replace source
$src= '';
$comment= NULL;
$annotations= array();
$annotation= NULL;
$matches= array();
$state= array(0);
$counter= 0;
$tokens= token_get_all($bytes);
for ($i= 0, $s= sizeof($tokens); $i < $s; $i++) {
if (T_COMMENT === $tokens[$i][0] && '#' === $tokens[$i][1]{0}) {
$annotations= eval('return array('.preg_replace(
array('/@([a-z_]+),/i', '/@([a-z_]+)\(\'([^\']+)\'\)/ie', '/@([a-z_]+)\(/i', '/([^a-z_@])([a-z_]+) *= */i'),
array('\'$1\' => NULL,', '"\'$1\' => urldecode(\'".urlencode(\'$2\')."\')"', '\'$1\' => array(', '$1\'$2\' => '),
trim($tokens[$i][1], "[]# \t\n\r").','
).');');
continue;
} else if (T_DOC_COMMENT === $tokens[$i][0]) {
$matches= NULL;
$comment= trim(preg_replace('/\n\s+\* ?/', "\n", "\n".substr(
$tokens[$i][1],
4, // "/**\n"
strpos($tokens[$i][1], '* @')- 2 // position of first details token
)));
preg_match_all(
'/@([a-z]+)\s*([^<\r\n]+<[^>]+>|[^\r\n ]+) ?([^\r\n ]+)?/',
$tokens[$i][1],
$matches,
PREG_SET_ORDER
);
}
if (0 === $state[0]) {
if (T_ABSTRACT === $tokens[$i][0] || T_FINAL === $tokens[$i][0]) {
$src.= $tokens[$i][1].' ';
} else if (T_CLASS === $tokens[$i][0] || T_INTERFACE === $tokens[$i][0]) {
if (NULL === $meta['class']) {
$meta['class']= array(DETAIL_COMMENT => $comment, DETAIL_ANNOTATIONS => $annotations);
}
$meta['class'][DETAIL_GENERIC]= array($self->name, $arguments);
$src.= $tokens[$i][1].' '.$name;
array_unshift($state, $tokens[$i][0]);
}
continue;
} else if (T_CLASS === $state[0]) {
if (T_EXTENDS === $tokens[$i][0]) {
if (isset($annotations['generic']['parent'])) {
$xargs= array();
foreach (explode(',', $annotations['generic']['parent']) as $j => $placeholder) {
$xargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= ' extends '.self::createGenericType($self->getParentClass(), $xargs)->literal();
} else {
$src.= ' extends '.$tokens[$i+ 2][1];
}
} else if (T_IMPLEMENTS === $tokens[$i][0]) {
$src.= ' implements';
$counter= 0;
$annotation= @$annotations['generic']['implements'];
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
$src.= ' {';
}
continue;
} else if (T_INTERFACE === $state[0]) {
if (T_EXTENDS === $tokens[$i][0]) {
$src.= ' extends';
$counter= 0;
$annotation= @$annotations['generic']['extends'];
array_unshift($state, 5);
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
$src.= ' {';
}
continue;
} else if (1 === $state[0]) { // Class body
if (T_FUNCTION === $tokens[$i][0]) {
$braces= 0;
$parameters= array();
array_unshift($state, 3);
array_unshift($state, 2);
$m= $tokens[$i+ 2][1];
if (isset($meta[1][$m])) {
$annotations= $meta[1][$m][DETAIL_ANNOTATIONS];
} else {
$meta[1][$m]= array(
DETAIL_ARGUMENTS => array(),
DETAIL_RETURNS => 'void',
DETAIL_THROWS => array(),
DETAIL_COMMENT => $comment,
DETAIL_ANNOTATIONS => $annotations,
DETAIL_NAME => $m
);
foreach ($matches as $match) {
switch ($match[1]) {
case 'param': $meta[1][$m][DETAIL_ARGUMENTS][]= $match[2]; break;
case 'return': $meta[1][$m][DETAIL_RETURNS]= $match[2]; break;
case 'throws': $meta[1][$m][DETAIL_THROWS][]= $match[2]; break;
}
}
}
} else if ('}' === $tokens[$i][0]) {
$src.= '}';
break;
} else if (T_CLOSE_TAG === $tokens[$i][0]) {
break;
}
} else if (2 === $state[0]) { // Method declaration
if ('(' === $tokens[$i][0]) {
$braces++;
} else if (')' === $tokens[$i][0]) {
$braces--;
if (0 === $braces) array_shift($state);
} else if (T_VARIABLE === $tokens[$i][0]) {
$parameters[]= $tokens[$i][1];
}
} else if (3 === $state[0]) { // Method body
if (';' === $tokens[$i][0]) {
// Abstract method
array_shift($state);
} else if ('{' === $tokens[$i][0]) {
$braces= 1;
array_shift($state);
array_unshift($state, 4);
$src.= '{';
if (isset($annotations['generic']['return'])) {
$meta[1][$m][DETAIL_RETURNS]= strtr($annotations['generic']['return'], $placeholders);
}
if (isset($annotations['generic']['params'])) {
$generic= array();
foreach (explode(',', $annotations['generic']['params']) as $j => $placeholder) {
if ('' === ($replaced= strtr(ltrim($placeholder), $placeholders))) {
$generic[$j]= NULL;
} else {
$meta[1][$m][DETAIL_ARGUMENTS][$j]= $replaced;
$generic[$j]= $replaced;
}
}
foreach ($generic as $j => $type) {
if (NULL === $type) {
continue;
} else if ('...' === substr($type, -3)) {
$src.= $j ? '$·args= array_slice(func_get_args(), '.$j.');' : '$·args= func_get_args();';
$src.= (
' if (!is(\''.substr($generic[$j], 0, -3).'[]\', $·args)) throw new IllegalArgumentException('.
'"Vararg '.($j + 1).' passed to ".__METHOD__."'.
' must be of '.$type.', ".xp::stringOf($·args)." given"'.
');'
);
} else {
$src.= (
' if (!is(\''.$generic[$j].'\', '.$parameters[$j].')) throw new IllegalArgumentException('.
'"Argument '.($j + 1).' passed to ".__METHOD__."'.
' must be of '.$type.', ".xp::typeOf('.$parameters[$j].')." given"'.
');'
);
}
}
$annotations= array();
}
unset($meta[1][$m][DETAIL_ANNOTATIONS]['generic']);
continue;
}
} else if (4 === $state[0]) { // Method body
if ('{' === $tokens[$i][0]) {
$braces++;
} else if ('}' === $tokens[$i][0]) {
$braces--;
if (0 === $braces) array_shift($state);
}
} else if (5 === $state[0]) { // Implements (class), Extends (interface)
if (T_STRING === $tokens[$i][0]) {
if (isset($annotation[$counter])) {
$iargs= array();
foreach (explode(',', $annotation[$counter]) as $j => $placeholder) {
$iargs[]= Type::forName(strtr(ltrim($placeholder), $placeholders));
}
$src.= self::createGenericType(new XPClass(new ReflectionClass($tokens[$i][1])), $iargs)->literal();
} else {
$src.= $tokens[$i][1];
}
$counter++;
continue;
} else if ('{' === $tokens[$i][0]) {
array_shift($state);
array_unshift($state, 1);
}
}
$src.= is_array($tokens[$i]) ? $tokens[$i][1] : $tokens[$i];
}
// Create class
// DEBUG fputs(STDERR, "@* ".substr($src, 0, strpos($src, '{'))." -> $qname\n");
eval($src);
method_exists($name, '__static') && call_user_func(array($name, '__static'));
unset($meta['class'][DETAIL_ANNOTATIONS]['generic']);
xp::$registry['details.'.$qname]= $meta;
xp::$registry['class.'.$name]= $qname;
}
return new XPClass(new ReflectionClass($name));
}
/**
* Reflectively creates a new type
*
* @param lang.Type[] arguments
* @return lang.XPClass
* @throws lang.IllegalStateException if this class is not a generic definition
* @throws lang.IllegalArgumentException if number of arguments does not match components
*/
public function newGenericType(array $arguments) {
return self::createGenericType($this, $arguments);
}
/**
* Returns generic type components
*
* @return string[]
* @throws lang.IllegalStateException if this class is not a generic definition
*/
public function genericComponents() {
if (!$this->isGenericDefinition()) {
throw new IllegalStateException('Class '.$this->name.' is not a generic definition');
}
$components= array();
foreach (explode(',', $this->getAnnotation('generic', 'self')) as $name) {
$components[]= ltrim($name);
}
return $components;
}
/**
* Returns whether this class is a generic definition
*
* @return bool
*/
public function isGenericDefinition() {
return $this->hasAnnotation('generic', 'self');
}
/**
* Returns generic type definition
*
* @return lang.XPClass
* @throws lang.IllegalStateException if this class is not a generic
*/
public function genericDefinition() {
if (!($details= self::detailsForClass($this->name))) return NULL;
if (!isset($details['class'][DETAIL_GENERIC])) {
throw new IllegalStateException('Class '.$this->name.' is not generic');
}
return XPClass::forName($details['class'][DETAIL_GENERIC][0]);
}
/**
* Returns generic type arguments
*
* @return lang.Type[]
* @throws lang.IllegalStateException if this class is not a generic
*/
public function genericArguments() {
if (!($details= self::detailsForClass($this->name))) return NULL;
if (!isset($details['class'][DETAIL_GENERIC])) {
throw new IllegalStateException('Class '.$this->name.' is not generic');
}
if (!isset($details['class'][DETAIL_GENERIC][1])) {
$details['class'][DETAIL_GENERIC][1]= array_map(
array(xp::reflect('lang.Type'), 'forName'),
$details['class'][DETAIL_GENERIC][2]
);
unset($details['class'][DETAIL_GENERIC][2]);
}
return $details['class'][DETAIL_GENERIC][1];
}
/**
* Returns whether this class is generic
*
* @return bool
*/
public function isGeneric() {
if (!($details= self::detailsForClass($this->name))) return FALSE;
return isset($details['class'][DETAIL_GENERIC]);
}
/**
* Returns the XPClass object associated with the class with the given
* string name. Uses the default classloader if none is specified.
*
* @param string name - e.g. "io.File", "rdbms.mysql.MySQL"
* @param lang.IClassLoader classloader default NULL
* @return lang.XPClass class object
* @throws lang.ClassNotFoundException when there is no such class
*/
public static function forName($name, IClassLoader $classloader= NULL) {
if (NULL === $classloader) {
$classloader= ClassLoader::getDefault();
}
return $classloader->loadClass((string)$name);
}
/**
* Returns type literal
*
* @return string
*/
public function literal() {
return xp::reflect($this->name);
}
/**
* Returns an array containing class objects representing all the
* public classes
*
* @return lang.XPClass[] class objects
*/
public static function getClasses() {
$ret= array();
foreach (get_declared_classes() as $name) {
if (isset(xp::$registry['class.'.$name])) $ret[]= new self($name);
}
return $ret;
}
}
?>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment