Mirror, mirror on the wall - Building a new PHP reflection library (Nomad PHP EU 2016)

Preview:

Citation preview

@asgrim

Mirror, mirror on the wall: Building a new PHP reflection library

James TitcumbNomad PHP Europe - November 2016

@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

github.com/ /BetterReflection

Better Reflection

Any questions? :)

https://joind.in/talk/c88eaJames Titcumb @asgrim