Redefine Class Methods or Class

只谈情不闲聊 提交于 2019-11-26 10:31:47
Jonathan Lonowski

It's called monkey patching. But, PHP doesn't have native support for it.

Though, as others have also pointed out, the runkit library is available for adding support to the language and is the successor to classkit. And, though it seemed to have been abandoned by its creator (having stated that it wasn't compatible with PHP 5.2 and later), the project does now appear to have a new home and maintainer.

I still can't say I'm a fan of its approach. Making modifications by evaluating strings of code has always seemed to me to be potentially hazardous and difficult to debug.

Still, runkit_method_redefine appears to be what you're looking for, and an example of its use can be found in /tests/runkit_method_redefine.phpt in the repository:

runkit_method_redefine('third_party_library', 'buggy_function', '',
    'return \'good result\''
);

runkit seems like a good solution but its not enabled by default and parts of it are still experimental. So I hacked together a small class which replaces function definitions in a class file. Example usage:

class Patch {

private $_code;

public function __construct($include_file = null) {
    if ( $include_file ) {
        $this->includeCode($include_file);
    }
}

public function setCode($code) {
    $this->_code = $code;
}

public function includeCode($path) {

    $fp = fopen($path,'r');
    $contents = fread($fp, filesize($path));
    $contents = str_replace('<?php','',$contents);
    $contents = str_replace('?>','',$contents);
    fclose($fp);        

    $this->setCode($contents);
}

function redefineFunction($new_function) {

    preg_match('/function (.+)\(/', $new_function, $aryMatches);
    $func_name = trim($aryMatches[1]);

    if ( preg_match('/((private|protected|public) function '.$func_name.'[\w\W\n]+?)(private|protected|public)/s', $this->_code, $aryMatches) ) {

        $search_code = $aryMatches[1];

        $new_code = str_replace($search_code, $new_function."\n\n", $this->_code);

        $this->setCode($new_code);

        return true;

    } else {

        return false;

    }

}

function getCode() {
    return $this->_code;
}
}

Then include the class to be modified and redefine its methods:

$objPatch = new Patch('path_to_class_file.php');
$objPatch->redefineFunction("
    protected function foo(\$arg1, \$arg2)
    {   
        return \$arg1+\$arg2;
    }");

Then eval the new code:

eval($objPatch->getCode());

A little crude but it works!

Till

For the sake of completeness - monkey patching is available in PHP through runkit. For details, see runkit_method_redefine().

How about wrapping it in another class like

class Wrapper {
 private $third_party_library;
 function __construct() { $this->third_party_library = new Third_party_library(); }
 function __call($method, $args) {
  return call_user_func_array(array($this->third_party_library, $method), $args);
 }
}

Yes, it's called extend:

<?php
class sd_third_party_library extends third_party_library
{
    function buggy_function() {
        return 'good result';
    }
    function other_functions(){
        return 'blah';
    }
}

I prefixed with "sd". ;-)

Keep in mind that when you extend a class to override methods, the method's signature has to match the original. So for example if the original said buggy_function($foo, $bar), it has to match the parameters in the class extending it.

PHP is pretty verbose about it.

For people that are still looking for this answer.

You should use extends in combination with namespaces.

like this:

namespace MyCustomName;

class third_party_library extends \third_party_library {
  function buggy_function() {
      return 'good result';
  }
  function other_functions(){
      return 'blah';
  }
}

Then to use it do like this:

use MyCustomName\third_party_library;

$test = new third_party_library();
$test->buggy_function();
//or static.
third_party_library::other_functions();

Zend Studio and PDT (eclipse based ide) have some built in refractoring tools. But there are no built in methods to do this.

Also you wouldn't want to have bad code in your system at all. Since it could be called upon by mistake.

If the library is explicitly creating the bad class and not using a locater or dependency system you are out of luck. There is no way to override a method on another class unless you subclass. The solution might be to create a patch file that fixes the library, so you can upgrade the library and re-apply the patch to fix that specific method.

You might be able to do this with runkit. http://php.net/runkit

Eric Lamb

There's alway extending the class with a new, proper, method and calling that class instead of the buggy one.

class my_better_class Extends some_buggy_class {
    function non_buggy_function() {
        return 'good result';
    }
}

(Sorry for the crappy formatting)

You can make a copy of the library class, with everything the same except the class name. Then override that renamed class.

It's not perfect, but it does improve the visibility of the extending class's changes. If you fetch the library with something like Composer, you'll have to commit the copy to source control and update it when you update the library.

In my case it was an old version of https://github.com/bshaffer/oauth2-server-php. I modified the library's autoloader to fetch my class file instead. My class file took on the original name and extended a copied version of one of the files.

Since you always have access to the base code in PHP, redefine the main class functions you want to override as follows, this should leave your interfaces intact:

class third_party_library {
    public static $buggy_function;
    public static $ranOnce=false;

    public function __construct(){
        if(!self::$ranOnce){
            self::$buggy_function = function(){ return 'bad result'; };
            self::$ranOnce=true;
        }
        .
        .
        .
    }
    function buggy_function() {
        return self::$buggy_function();
    }        
}

You may for some reason use a private variable but then you will only be able to access the function by extending the class or logic inside the class. Similarly it's possible you'd want to have different objects of the same class have different functions. If so, do't use static, but usually you want it to be static so you don't duplicate the memory use for each object made. The 'ranOnce' code just makes sure you only need to initialize it once for the class, not for every $myObject = new third_party_library()

Now, later on in your code or another class - whenever the logic hits a point where you need to override the function - simply do as follows:

$backup['buggy_function'] = third_party_library::$buggy_function;
third_party_library::$buggy_function = function(){
    //do stuff
    return $great_calculation;
}
.
.
.  //do other stuff that needs the override
.  //when finished, restore the original function
.
third_party_library::$buggy_function=$backup['buggy_function'];

As a side note, if you do all your class functions this way and use a string-based key/value store like public static $functions['function_name'] = function(...){...}; this can be useful for reflection. Not as much in PHP as other languages though because you can already grab the class and function names, but you can save some processing and future users of your class can use overrides in PHP. It is however, one extra level of indirection, so I would avoid using it on primitive classes wherever possible.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!