Diving into the strange world of PHP Reflection API

Being a Mouf developer, I have been quite heavily relying on the PHP Reflection API, and so far, I must say things were quite ok.

PHP provides a nice API to access your code structure through ReflectionClass, ReflectionMethod, ReflectionProperty, etc...

Recently, I started adding support for traits in Mouf. This means that I need to do some reflection on traits. In particular, I need to answer this question:

Given a class, and a method name, I need to be able to know where a method is declared. Especially, I need to know if it was declared in a trait.

Round 1: getDeclaringClass behaviour

At first, I though that I could use the MoufReflection::getDeclaringClass() method. Since this method is used to get the class that declares a function, if the function is declared in a trait, it would make perfect sense to return the trait, would it not? It turns out I was wrong...

// Let's declare a simple trait
trait MyTrait {
  public function myFunction() {
  }
}

// Let's declare a class using that trait
class MyClass {
  use MyTrait;
}

$reflectionClass = new ReflectionClass("MyClass");
$reflectionMethod = $reflectionClass->getMethod("myFunction");

// This outputs "MyClass"
echo $reflectionMethod->getDeclaringClass()->getName();

PHP behaviour is to return the name of the class that contains the trait, instead of the name of the trait. Of course, there is no getDeclaringTrait() method available.

I'm not the only one who has spotted this, someone already opened a bug about this: https://bugs.php.net/bug.php?id=64963
As you can see, for the PHP developers team, it is not obvious this is a bug since "Traits are copied into classes".

Ok, I understand the logic behind this, but seriously, if we are using a reflection API, it is to learn the structure of the code as it was developed by the developer. I mean, the getDeclaringClass() is designed to find in which class a method was declared (in case there are serveral extended classes). It would make perfect sense to return the trait if the method was declared in the trait.

But anyway, this is a limitation, so I'll try to work around it.

Round 2: finding a workaround

I could try to analyze the traits included in my class, and then try to see if a trait contains the method I am including.

The code would look like this:

function getDeclaringTrait($className, $methodName) {
    $reflectionClass = new ReflectionClass($className);

    // Let's scan all traits
    $traits = $reflectionClass->getTraits();
    foreach ($traits as $trait) {
        // If the trait has a method, that's it!
        if ($trait->hasMethod($methodName)) {
            return $trait;
        }
    }
    $reflectionMethod = $reflectionClass->getMethod($methodName);
    return $reflectionMethod->getDeclaringClass();
}

Basically, it scans all the traits, and if it finds a method, it will attribute that method to the trait.

It turns out it does not work. Indeed, what if my class is overloading a method of the trait? I have no way to make the distinction between a method that is overloaded and the method of the trait!

Here is a sample that defeats my "getDeclaringTrait" function:

// Let's declare a simple trait
trait MyTrait {
  public function myFunction() {
  }
}

// Let's declare a class using that trait
class MyClass {
  use MyTrait;

  // Let's overload my trait declaration
  public function myFunction() {
  }
}

In this sample, using the ReflectionAPI, I have absolutely no way to know if the "myFunction" method comes from MyClass or of MyTrait. And I can find dozens of other samples were it does not work. For instance, if a trait extends another trait, or if the "insteadof" keyword is used to override one trait method with another...

Round 3: finding another workaround

I'm not the kind of guy that gives up so easily. So I figured out I would give another shot at this problem. Looking at the ReflectionMethod methods, I noticed the getFileName,getStartLine and getEndLine methods. So I figured out that I might use this information to "locate" the method and find what files it belongs. This is only partly true, because the method might be declared into a file that would be included or required in the class or trait. But, let's pretend that no one does that.

Then, this code should solve my problem.

function getDeclaringTrait($className, $methodName) {
    $reflectionClass = new ReflectionClass($className);
    $reflectionMethod = $reflectionClass->getMethod($methodName);
    
    $methodFile = $reflectionMethod->getFileName();
    $methodStartLine = $reflectionMethod->getStartLine();
    $methodEndLine = $reflectionMethod->getEndLine();
    
    // Let's scan all traits
    $traits = $reflectionClass->getTraits();
    foreach ($traits as $trait) {
        // If the trait has a method, is it the method we see?
        if ($trait->getFileName() == $methodFile 
                && $trait->getStartLine() <= $methodStartLine 
                && $trait->getEndLine() >= $methodEndLine) {
            return $trait;
        }
    }
    return $reflectionMethod->getDeclaringClass();
}

Yeah! This time, I was sure I had my solution! And once more... I was wrong! This works nicely for traits that are overloaded. This works also nicely with the "insteadof" keyword, and even with the "as" keyword, since we are not relying on the method name. It's almost perfect. Except.... traits can use traits!

Round 4: achieving victory

So I fixed my code one last time, to add a recursive check to all traits that might be used to compose other traits. And here we go:

/**
 * Finds the trait that declares $className::$methodName
 */
function getDeclaringTrait($className, $methodName) {
    $reflectionClass = new ReflectionClass($className);
    $reflectionMethod = $reflectionClass->getMethod($methodName);
    
    $methodFile = $reflectionMethod->getFileName();
    $methodStartLine = $reflectionMethod->getStartLine();
    $methodEndLine = $reflectionMethod->getEndLine();
    
    
    // Let's scan all traits
    $trait = deepScanTraits($reflectionClass->getTraits(), $methodFile, $methodStartLine, $methodEndLine);
    if ($trait != null) {
        return $trait;
    } else {
        return $reflectionMethod->getDeclaringClass();
    }
}

/**
 * Recursive method called to detect a method into a nested array of traits.
 * 
 * @param $traits ReflectionClass[]
 * @param $methodFile string
 * @param $methodStartLine int
 * @param $methodEndLine int
 * @return ReflectionClass|null
 */
function deepScanTraits(array $traits, $methodFile, $methodStartLine, $methodEndLine) {
    foreach ($traits as $trait) {
        // If the trait has a method, is it the method we see?
        if ($trait->getFileName() == $methodFile
                && $trait->getStartLine() <= $methodStartLine
                && $trait->getEndLine() >= $methodEndLine) {
            return $trait;
        }
        return deepScanTraits($trait->getTraits(), $methodFile, $methodStartLine, $methodEndLine);
    }
    return null;
}

This code is pretty much okay. It manages to detect sub-traits. For instance, it works with this sample:

// The sub trait that contains the method to detect
trait MySubTrait {
    public function myFunction() {
    }   
}

// Let's declare a trait that uses a sub trait
trait MyTrait {
    use MySubTrait;
}

// Let's declare a class using that trait
class MyClass {
  use MyTrait;
}

echo getDeclaringTrait("MyClass", "myFunction")->getName();

So basically, using the getDeclaringTrait function I wrote, I'm now finally able to find which trait is used to declare a method.

Score: David 1 - PHP 0

Or so I used to think....

Round 5: realizing I'm f*cked

At this point, I realize I did not quite explain why I so much need to locate the methods. This is because in Mouf, I need to know the type of variables, and these might be declared in the comments. Here is a sample:

namespace My\Namespace;

use Another\Namespace\Test

trait MyTrait {
    protected $test;
    
    /**
     * Sets the condition.
     * @param Test|string $test
     */
    public function setTest($test) {
        $this->test = $test;
    }
}

As you can see, setTest is a setter. In Mouf, I'm interested in setters, and I need to know their type. Their type is available in the @param annotation, so I need to parse this annotation. This is easy to do, because I've got the getDocBlock() method available in ReflectionMethod. But this is not enough! I have the name of the class, but not it's namespace. Hopefully, using the code I described above, I am now able to find the trait the method belongs to. Therefore, I'm able to find the file the method belongs to, I can parse the whole page, find the "use" statements and resolve the type manually. Yes, I know, this is a hell of a mess, but Mouf is doing all this behind the scenes!

But Mouf does not only support setter injection. It also supports public properties injection. Therefore, I also need to be able to locate a property and in which file it is declared... And it is at this point that I realize that the ReflectionProperty class does not have a getFileName method, or a getStartLine method. Actually, it doesn't have a getDeclaringClass() method at all, which is very curious, since properties and methods are similar items regarding inheritence.

Why the hell does PHP does not treat properties and methods on equal terms?

Round 6: starting all over again, with properties

Hopefully, the problem is actually easier to solve with properties, because of a simple rule with traits in PHP that you can find in the documentation:

If a trait defines a property then a class can not define a property with the same name, otherwise an error is issued. It is an E_STRICT if the class definition is compatible (same visibility and initial value) or fatal error otherwise.

So we are sure that a property cannot be declared in a trait and overloaded in a class. Once more, we see that PHP does not treat methods and properties equally, which is still weird, but anyway, I'm starting getting used to it.

The solution is therefore simple, by doing a depth-first search of the trait tree, we can find if a property belongs to a trait, since we are sure it will only appear once! Let's do this!

/**
 * Finds the trait that declares $className::$propertyName
 */
function getDeclaringTraitForProperty($className, $propertyName) {
    $reflectionClass = new ReflectionClass($className);
    
    // Let's scan all traits
    $trait = deepScanTraitsForProperty($reflectionClass->getTraits(), $propertyName);
    if ($trait != null) {
        return $trait;
    }
    // The property is not part of the traits, let's find in which parent it is part of.
    if ($reflectionClass->getParentClass()) {
        $declaringClass = getDeclaringTraitForProperty($reflectionClass->getParentClass()->getName(), $propertyName);
        if ($declaringClass != null) {
            return $declaringClass;
        }
    }
    if ($reflectionClass->hasProperty($propertyName)) {
        return $reflectionClass;
    }
    
    return null;
}

/**
 * Recursive method called to detect a method into a nested array of traits.
 * 
 * @param $traits ReflectionClass[]
 * @param $propertyName string
 * @return ReflectionClass|null
 */
function deepScanTraitsForProperty(array $traits, $propertyName) {
    foreach ($traits as $trait) {
        // If the trait has a property, it's a win!
        $result = deepScanTraitsForProperty($trait->getTraits(), $propertyName);
        if ($result != null) {
            return $result;
        } else {
            if ($trait->hasProperty($propertyName)) {
                return $trait;
            }
        }
    }
    return null;
}

This code is almost ok. It can safely find the property hidden in this subtrait:

// The sub trait that contains the property to detect
trait MySubTrait {
    public $myProperty;
}

// Let's declare a trait that uses a sub trait
trait MyTrait {
    use MySubTrait;
}

// Let's declare a class using that trait
class MyClass {
    use MyTrait;
}

As long as the user does not declare a property in a trait twice (that triggers a E_STRICT warning), then the code will find the correct property. However, if the user declares the property twice and ignores E_STRICT warning, I'm completely f*cked.

So that's it!

Score: David 2 - PHP 0

It took me a whole day to find out how to solve all this. At the end of this long day, I feel that it would really have been much much simpler if PHP had a simple and coherent Reflection API. I mean, ReflectionMethod::getDeclaringClass should definitely return the trait if the method is declared in a trait. This is a complete no-brainer, and I wonder why people are arguing over this. And furthermore, there should be a ReflectionProperty::getDeclaringClass. It is not normal that all reflection items can be located in PHP except properties!

Come on PHP devs! PHP is a great language and I will definitely not participate in some PHP bashing. But we have to admit the Reflection API needs some polishing. I would really help if I could but my C is a bit rusty (not done any C in the last 15 years...) Come on guys! This can be improved... and if you still doubt about it, there are great frameworks that need this API :)

Edit: found another way to solve this problem: you can use the very nice PHP-Token-Reflection library. It is a reimplementation of the Reflection API that runs by parsing the whole files of your project. Of course, it is not as efficient as using the PHP native API, but it has also very nice features (for instance, you can run it on broken PHP code!)
Tags :