Mirror, mirror on the wall - Building a new PHP reflection library (Nomad PHP EU 2016)
-
Upload
james-titcumb -
Category
Technology
-
view
61 -
download
3
Transcript of Mirror, mirror on the wall - Building a new PHP reflection library (Nomad PHP EU 2016)
@asgrim
Mirror, mirror on the wall: Building a new PHP reflection library
James TitcumbNomad PHP Europe - November 2016
James Titcumbwww.jamestitcumb.comwww.roave.comwww.phphants.co.ukwww.phpsouthcoast.co.uk@asgrim
Who is this guy?
@asgrim
Reflection
© 1937 Disney’s Snow White - disneyscreencaps.com
@asgrim
@asgrim
Mostly this...public function testSomething()
{
$myObj = new Thing();
$propReflection = new \ReflectionProperty($myObj, 'foo');
$propReflection->setAccessible(true);
$propReflection->setValue($myObj, 'whatever');
// ... whatever ...
}
@asgrim
● Structure● Metadata● Values● Type introspection● Modification
Reflection
@asgrim
How does it work?
@asgrim
zend_object (zend_types.h)● zend_class_entry *ce (zend.h)
○ zval* static_members_table○ HashTable function_table○ HashTable properties_info○ HashTable constants_table○ zend_class_entry** interfaces○ zend_class_entry** traits○ (…other stuff…)
Roughly...
@asgrim
GET_REFLECTION_OBJECT_PTR(ce);
lc_name = zend_str_tolower_dup(name, name_len);
if ((ce == zend_ce_closure && (name_len == sizeof(ZEND_INVOKE_FUNC_NAME)-1)
&& memcmp(lc_name, ZEND_INVOKE_FUNC_NAME, sizeof(ZEND_INVOKE_FUNC_NAME)-1) == 0)
|| zend_hash_str_exists(&ce->function_table, lc_name, name_len)) {
efree(lc_name);
RETURN_TRUE;
} else {
efree(lc_name);
RETURN_FALSE;
}
ReflectionClass->hasMethod
@asgrim
Okay. What now?
@asgrim
github.com/ /BetterReflection
Better Reflection!
@asgrim
What?
@asgrim
Why?
@asgrim
Features!
@asgrim
How?
@asgrim
@asgrim
@asgrim
Reflector
Source Locator
PhpParser
Reflection
@asgrim
Core reflection$reflection = new ReflectionClass(
\My\ExampleClass::class
);
$this->assertSame(
'ExampleClass',
$reflection->getShortName()
);
@asgrim
Better Reflection$reflection = ReflectionClass::createFromName(
\My\ExampleClass::class
);
$this->assertSame(
'ExampleClass',
$reflection->getShortName()
);
@asgrim
createFromName// In ReflectionClass :
public static function createFromName($className)
{
return ClassReflector::buildDefaultReflector()->reflect($className);
}
@asgrim
buildDefaultReflector// In ClassReflector :
public static function buildDefaultReflector()
{
return new self(new AggregateSourceLocator([
new PhpInternalSourceLocator(),
new EvaledCodeSourceLocator(),
new AutoloadSourceLocator(),
]));
}
@asgrim
Reflector
Source Locator
PhpParser
Reflection
@asgrim
Source Locators
● PhpInternalSourceLocator● EvaledCodeSourceLocator● AggregateSourceLocator● ClosureSourceLocator● ComposerSourceLocator● SingleFileSourceLocator● StringSourceLocator● DirectoriesSourceLocator● FileIteratorSourceLocator
@asgrim
StringSourceLocatoruse BetterReflection\Reflector\ClassReflector;
use BetterReflection\SourceLocator\Type\StringSourceLocator;
$source = <<<EOF
<?php
class MyClassInString {}
EOF;
$reflector = new ClassReflector(new StringSourceLocator($source));
$classInfo = $reflector->reflect(MyClassInString::class);
@asgrim
However…
@asgrim
AutoloadSourceLocator
ReflectionClass::createFromName(new MyClass)
replace stream wrapper
disable error handling
call “class_exists”
restore stream wrapper
restore error handling
store attempted filename load
DO NOT LOAD FILE!
return stored filename
Read file and parse AST!
@asgrim
What’s next?
Now we have CODE!
@asgrim
Magic superpowers
source: http://goo.gl/HORwLQ
@asgrim
So what is AST?
@asgrim
Reflector
Source Locator
PhpParser
Reflection
@asgrim
PHP Parser<?php
use PhpParser\ParserFactory;
$parser = (new ParserFactory)
->create(ParserFactory::PREFER_PHP7);
print_r($parser->parse(
file_get_contents('ast-demo-src.php')
));
@asgrim
ast-demo-src.php
<?php
echo "Hello world";
@asgrim
AST representation
Echo statement
`-- String, value "Hello world"
@asgrim
ast-demo-src.php
<?php
echo "Hello " . "world";
@asgrim
AST representation
Echo statement
`-- Concat
|-- Left
| `-- String, value "Hello "
`-- Right
`-- String, value "world"
@asgrim
ast-demo-src.php
<?php
$a = 5;
$b = 3;
echo $a + ($b * 2);
@asgrim
AST representationAssign statement
|-- Variable $a
`-- Integer, value 5
Assign statement
|-- Variable $b
`-- Integer, value 3
Echo statement
`-- Add operation
|-- Left
| `-- Variable $a
`-- Right
`-- Multiply operation
|-- Left
| `-- Variable $b
`-- Right
`-- Integer, value 2
@asgrim
So what?
@asgrim
Reflector
Source Locator
PhpParser
Reflection
@asgrim
AST to Reflection
@asgrim
Benefits?
@asgrim
Example class<?php
class Foo
{
private $bar;
public function thing()
{
}
}
@asgrim
AST representationClass, name Foo
|-- Statements
| |-- Property, name bar
| | |-- Type [private]
| | `-- Attributes [start line: 7, end line: 9]
| `-- Method, name thing
| |-- Type [public]
| |-- Parameters [...]
| |-- Statements [...]
| `-- Attributes [start line: 7, end line: 9]
`-- Attributes [start line: 3, end line: 10]
@asgrim
php-ast extension
@asgrim
Here be dragons!
Some voodoo...
@asgrim
MyClass
class MyClass
{
public function foo()
{
return 5;
}
}
@asgrim
Create the reflection// Create the reflection first
// ***BEFORE*** class is loaded
$classInfo = ReflectionClass::createFromName('MyClass');
// Or use specific source locators as already shown
@asgrim
Override the body// Override the body...!
$methodInfo = $classInfo->getMethod('foo');
$methodInfo->setBodyFromClosure(function () {
return 4;
});
@asgrim
Register Better Reflection autoloaderuse BetterReflection\Util\Autoload\ClassLoader;
use BetterReflection\Util\Autoload\ClassLoaderMethod\EvalLoader;
use BetterReflection\Util\Autoload\ClassPrinter\PhpParserPrinter;
// Note - this part is WIP at the moment, still in PR :)
$loader = new ClassLoader(new EvalLoader(new PhpParserPrinter()));
$loader->addClass($classInfo);
@asgrim
Create the patched class
// Now create an instance, and call the
// method on this...
$c = new MyClass();
var_dump($c->foo()); // will be 4!!!
@asgrim
astkit
@asgrim
astkit$if = AstKit::parseString(<<<EOD
if (true) {
echo "This is a triumph.\n";
} else {
echo "The cake is a lie.\n";
}
EOD
);
$if->execute(); // First run, program is as-seen above
$const = $if->getChild(0)->getChild(0);
// Replace the "true" constant in the condition with false
$const->graft(0, false);
// Can also graft other AstKit nodes, instead of constants
$if->execute(); // Second run now takes the else path
@asgrim
Difficulties...
ReflectionClass implements Reflector {
/* Constants */
const integer IS_IMPLICIT_ABSTRACT = 16 ;
const integer IS_EXPLICIT_ABSTRACT = 32 ;
const integer IS_FINAL = 64 ;
/* Properties */
public $name ;
/* Methods */
public __construct ( mixed $argument )
public static string export ( mixed $argument [, bool $return = false ] )
public mixed getConstant ( string $name )
public array getConstants ( void )
public ReflectionMethod getConstructor ( void )
public array getDefaultProperties ( void )
public string getDocComment ( void )
public int getEndLine ( void )
public ReflectionExtension getExtension ( void )
public string getExtensionName ( void )
public string getFileName ( void )
public array getInterfaceNames ( void )
public array getInterfaces ( void )
public ReflectionMethod getMethod ( string $name )
public array getMethods ([ int $filter ] )
public int getModifiers ( void )
public string getName ( void )
public string getNamespaceName ( void )
public object getParentClass ( void )
public array getProperties ([ int $filter ] )
public ReflectionProperty getProperty ( string $name )
public string getShortName ( void )
public int getStartLine ( void )
public array getStaticProperties ( void )
Reflection API is a big!public mixed getStaticPropertyValue ( string $name [, mixed &$def_value ] )
public array getTraitAliases ( void )
public array getTraitNames ( void )
public array getTraits ( void )
public bool hasConstant ( string $name )
public bool hasMethod ( string $name )
public bool hasProperty ( string $name )
public bool implementsInterface ( string $interface )
public bool inNamespace ( void )
public bool isAbstract ( void )
public bool isAnonymous ( void )
public bool isCloneable ( void )
public bool isFinal ( void )
public bool isInstance ( object $object )
public bool isInstantiable ( void )
public bool isInterface ( void )
public bool isInternal ( void )
public bool isIterateable ( void )
public bool isSubclassOf ( string $class )
public bool isTrait ( void )
public bool isUserDefined ( void )
public object newInstance ( mixed $args [, mixed $... ] )
public object newInstanceArgs ([ array $args ] )
public object newInstanceWithoutConstructor ( void )
public void setStaticPropertyValue ( string $name , string $value )
public string __toString ( void )
}
@asgrim
Type determination<?php
namespace ??????????;
use ?????????????????????????????????????;
class Foo
{
public function something()
{
throw new InvalidArgumentException('Oh noes!');
}
}
@asgrim
Type determination<?php
namespace My\Package;
use Some\Package\InvalidArgumentException;
class Foo
{
public function something()
{
throw new InvalidArgumentException('Oh noes!');
}
}
@asgrim
Type determination
● FindParameterType● FindPropertyType● FindReturnType● FindTypeFromAst
@asgrim
Type determination$finder = new FindTypeFromAst();
$namespace = '';
if ($method->getDeclaringClass()->inNamespace()) {
$namespace = $method->getDeclaringClass()->getNamespaceName();
}
$type = $finder(
$className,
$method->getLocatedSource(),
$namespace
);
@asgrim
DocBlock Parent Traversal Type Resolution
@asgrim
class Foo {
/**
* @return int
*/
public function myMethod() { /* ... */ }
}
class Bar extends Foo {
/**
* {@inheritDoc}
*/
public function myMethod() { /* ... */ }
}
DocBlock Parent Traversal Type Resolution
It’s an “int” return type!
@asgrim
interface Blammo {
/**
* @return string
*/
public function myMethod();
}
class Foo {
/**
* @return int
*/
public function myMethod() { /* ... */ }
}
class Bar extends Foo implements Blammo {
/**
* {@inheritDoc}
*/
public function myMethod() { /* ... */ }
}
DocBlock Parent Traversal Type Resolution
Return type: ¯\_(ツ)_/¯
@asgrim
interface Blammo {
/**
* @return string
*/
public function myMethod();
}
class Foo {
/**
* @return int
*/
public function myMethod() { /* ... */ }
}
class Bar extends Foo implements Blammo {
/**
* {@inheritDoc}
*/
public function myMethod() { /* ... */ }
}
DocBlock Parent Traversal Type Resolution
Return type: int|string
@asgrim
Loading Modified Reflections
@asgrim
Loading Modified Reflections
$methodInfo = $classInfo->getMethod('foo');
$methodInfo->setBodyFromClosure(function () {
// Nasty, evil, malicious code here ???
});
@asgrim
Reflecting Internal Functions
@asgrim
@asgrim
Reflecting Closures
export
__toString
createFromName
createFromInstance
createFromNode
getShortName
getName
getNamespaceName
inNamespace
getMethods
getImmediateMethods
getMethod
hasMethod
getConstants
getConstant
hasConstant
getConstructor
getProperties
getProperty
hasProperty
getDefaultProperties
getFileName
getLocatedSource
Better Reflection API is BIGGERERgetStartLine
getEndLine
getParentClass
getDocComment
isInternal
isUserDefined
isAbstract
isFinal
getModifiers
isTrait
isInterface
getTraits
getTraitNames
getTraitAliases
getInterfaces
getImmediateInterfaces
getInterfaceNames
isInstance
isSubclassOf
implementsInterface
isInstantiable
isCloneable
isIterateable
__clone
getStaticPropertyValue
setStaticPropertyValue
getAst
setFinal
removeMethod
addMethod
addProperty
removeProperty
@asgrim
(at least for now)
Out of scope
@asgrim
It’s not fast :(
@asgrim
@asgrim
Reflecting from STDIN
@asgrim
@asgrim
HHVM
@asgrim
Reflection(Zend)Extension
@asgrim
Instantiation & Invocation
@asgrim
Use Cases
@asgrim
API diff tool
@asgrim
What’s next?
@asgrim
Your ideas welcome!¯\_(ツ)_/¯
@asgrim
● https://github.com/nikic/PHP-Parser● https://github.com/nikic/php-ast● https://github.com/sgolemon/astkit● https://gist.github.com/sgolemon/f65fafadd90
ed26f05be
Resources
@asgrim
github.com/ /BetterReflection
Better Reflection
Any questions? :)
https://joind.in/talk/c88eaJames Titcumb @asgrim