Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use Reflection to find the class that a method belongs to

I'm working in Magento, but this is more of a general PHP question. The situation is that within Magento, there are classes that extend classes that extend classes that extend classes. I want to be able to quickly find which class actually contains the definition for a method and/or if that method is actually magic.

So for instance, if I'm 10 levels deep into classes extending others, and 4 of those 10 classes have a method called getHtml, I want to be able to find out which one of those methods is actually being called when I call $this->getHtml(). I would also like to be able to tell if getHtml is actually a magic method.

How can I do this with the PHP Reflection Class, or any other programmatic means?

like image 880
Tyler V. Avatar asked Dec 19 '22 16:12

Tyler V.


1 Answers

(untested code below — if you find any bugs I'd appreciate an update in the comments)

The best you'll be able to do with the Reflection API is find the classes in the hierarchy where the method is not defined. The ReflectionClass's hasMethod feature will take parent classes into account — a better name for it might be aClassInTheHierarchyHasThisMethod. Consider this (quick top-my-head) inline function

    $getClassesWithMethod = function($classOrObject, $method, $return=false) use(&$getClassesWithMethod)
    {
        $return = $return ? $return : new ArrayObject;
        $r = new ReflectionClass($classOrObject);
        if($r->hasMethod($method))
        {
            $return['has ' . $method . ' method'][] = $r->getName();
        }
        else
        {
            $return['no '. $method . ' method'][] = $r->getName();
        }

        $parent = $r->getParentClass();
        if($parent)
        {
            $getClassesWithMethod($parent->getName(), $method, $return);
        }
        return $return;
    };

    $product = Mage::getModel('catalog/product'); 

    $classesWithMethod = $getClassesWithMethod($product, 'load');
    var_dump((array)$classesWithMethod);

Run the above, and you'll get

array (size=2)
  'has load method' => 
    array (size=4)
      0 => string 'Mage_Catalog_Model_Product' (length=26)
      1 => string 'Mage_Catalog_Model_Abstract' (length=27)
      2 => string 'Mage_Core_Model_Abstract' (length=24)
  'no load method' => 
    array (size=1)
      0 => string 'Varien_Object' (length=13)

So you know Varien_Object doesn't have the method load defined, which means it shows up first in Mage_Core_Model_Abstract. However, you won't know if there's also a definition in Mage_Catalog_Model_Abstract or Mage_Catalog_Model_Product. The Reflection API won't get you this.

What can get you this using the token_get_all method. This method can break a PHP file down into it's component PHP/Zend tokens. Once you have that, you can write a small parser in PHP that identifies method/function definitions in a specific class file. You can use this to recursively check the hierarchy. Again, an inline function.

    $getClassesWithConcreteDefinition = function($classOrObject,$method,$return=false) use(&$getClassesWithConcreteDefinition)
    {
        $return = $return ? $return : new ArrayObject;
        $r = new ReflectionClass($classOrObject);
        $tokens = token_get_all(file_get_contents($r->getFilename()));

        $is_function_context = false;
        foreach($tokens as $token)
        {
            if(!is_array($token)){continue;}                
            $token['name'] = token_name($token[0]);
            if($token['name'] == 'T_WHITESPACE'){ continue; }

            if($token['name'] == 'T_FUNCTION')
            { 
                $is_function_context = true;
                continue;
            }

            if($is_function_context)
            {
                if($token[1] == $method)
                {
                    $return[] = $r->getName();
                }
                $is_function_context = false;
                continue;
            }
        }

        $parent = $r->getParentClass();
        if($parent)
        {
            $getClassesWithConcreteDefinition($parent->getName(),$method,$return);
        }

        return $return;
    };

    $product = Mage::getModel('catalog/product');        

    $hasActualDefinition = $getClassesWithConcreteDefinition($product, 'setData');
    var_dump((array)$hasActualDefinition);

Here we're checking for the setData method. The above will return

array (size=2)
  0 => string 'Mage_Catalog_Model_Abstract' (length=27)
  1 => string 'Varien_Object' (length=13)

Because setData is defined in both the Mage_Catalog_Model_Abstract class and the Varien_Object class. You should be able to modify these functions to fit your own needs. Good luck!

like image 50
Alan Storm Avatar answered Dec 24 '22 00:12

Alan Storm