Patching PHP code (used as a library) without changing the original code!

The article Fixing extensions without modifying their code reminded me of another patching technique that I used. My conviction and a hard look on the web had led me to an article Redefine PHP class methods or class giving me a head start to do what I wanted: patching third party code without touching the original to allow easy upgrades.

The trick used here to patch PHP code is to use PHP's "eval" function on PHP code patched inline.

Here is an example of how to use the Patch class that is provided further below. The first piece of code redefines a function in a class. The second piece of code redefines an entire class. The third piece of code patches some code using a 'str_replace'

$objPatch = new Patch('path_to_class_file.php');
 $objPatch->redefineFunction("
   protected function foo(\$arg1, \$arg2)
   {
       return \$arg1+\$arg2;
   }");
 eval($objPatch->getCode());
$objPatch = new Patch(dirname(__FILE__).DIRECTORY_SEPARATOR.'pathtoclass.php');
$objPatch->redefineClass('ReplacementClass','OriginalClass');
eval($objPatch->getCode());
$objPatch = new Patch("anotherclass.php");
// Next line disables the include of the patched file */
$objPatch->replaceCode('include_once ("PHP','$_dummy=("');
// Next line patches the file_exists condition (modify the path if the path does not exist)
$objPatch->replaceCode('if(file_exists($path))','if(!file_exists($path)){$path=$this->rootPath.$path;}if(file_exists($path))');
eval($objPatch->getCode());

The Patch class is next (I believe I patched that class a bit myself ;-) ).

/*
 * Based on: http://stackoverflow.com/questions/137006/php-redefine-class-methods-or-class
 * Usage:
 * $objPatch = new Patch('path_to_class_file.php');
 * $objPatch->redefineFunction("
 *   protected function foo(\$arg1, \$arg2)
 *   {
 *       return \$arg1+\$arg2;
 *   }");
 *
 *   or use 'redefineClass()"
 *
 * Then eval the new code:
 *
 * eval($objPatch->getCode());
 */
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 replaceCode($oldcode,$newcode) {
        $this->_code=str_replace($oldcode,$newcode,$this->_code);
    }

    function redefineClass($oldclass,$newclass) {
        if(preg_match("/(class $oldclass)/", $this->_code, $matches)) {
            $new_code = str_replace($matches[1],"class $newclass",$this->_code);
            $this->setCode($new_code);
            return true;
        }
        return false;
    }

    function getCode() {
        return $this->_code;
    }
}
4 0
5 followers
Viewed: 13 054 times
Version: 1.1
Category: Tips
Written by: le_top
Last updated by: le_top
Created on: Feb 28, 2013
Last updated: 11 years ago
Update Article

Revisions

View all history