Tuesday, March 2, 2010

Extending PHP classes at run time :: Base Object :: Part 5

In part 4 of this article we have extended objects at run time. Now wouldn't it be nice if we could do the same with classes? Also at run time.

Sure we can. Unfortunately, the __staticCall, __staticGet and __staticSet are not implemented in PHP 5.3, so we will have to improvise while still keeping the syntax pretty.

I am not going to list all the multiple modifications I did to the base object class, I will just dump the whole thing here, take a look:

class Object {
        
        /**************** PRIVATE ****************/
        
        /**************** PROTECTED **************/
        
        protected static $_aClassProperties = array();
        protected $_aProperties = array();
        
        /**************** STATIC ****************/

        public static function construct() {
            $aArgs = func_get_args();
            $nArgCount = count($aArgs);
            $aArgTokens = array();
            for ($i=0; $i<$nArgCount; $i++) {
                $aArgTokens[] = '$aArgs['.$i.']';
            }
            $sArgTokens = implode(',', $aArgTokens);
            $sEval = 'return new '.get_called_class().'('.$sArgTokens.');';
            return eval($sEval);
        }

        /**************** PUBLIC ****************/
        
        public function __construct() {
            if (!isset(self::$_aClassProperties[get_called_class()])) {
                self::$_aClassProperties[get_called_class()] = array();
            }
            $this->extend(self::$_aClassProperties[get_called_class()]);
        }
        public function __call($sMethod, $aArgs) {
            if (isset($this->_aProperties[$sMethod])) {
                $f = $this->_aProperties[$sMethod];
                $nArgCount = count($aArgs);
                $aArgTokens = array('$this');
                for ($i=0; $i<$nArgCount; $i++) {
                    $aArgTokens[] = '$aArgs['.$i.']';
                }
                $sArgTokens = implode(',', $aArgTokens);
                $sEval = 'return $f('.$sArgTokens.');';
                return eval($sEval);
            } else {
                throw new Exception('Method '.$sMethod.' does not exist in class '.get_class($this));
            }
        }
        public function __get($sProperty) {
            return isset($this->_aProperties[$sProperty]) 
                ? $this->_aProperties[$sProperty] 
                : null;
        }
        public function __set($sProperty, $mValue) {
            $this->_aProperties[$sProperty] = $mValue;
            return $mValue; 
        }
        public function extend($mBase) {
            $sClass = get_called_class();
            if (!isset(self::$_aClassProperties[$sClass])) {
                self::$_aClassProperties[$sClass] = array();
            }
            
            if ($this) {                             // object call
                if (!is_array($mBase)) {
                    $mBase = $mBase->_aProperties;
                }
                $this->_aProperties = array_merge(
                    $this->_aProperties, 
                    $mBase
                );
            } else {                                 // static call
                if (is_string($mBase)) {            // class name is supplied
                    $mBase = self::$_aClassProperties[$mBase];
                }
                self::$_aClassProperties[$sClass] = array_merge(
                    self::$_aClassProperties[$sClass],
                    $mBase
                );
            }
        }
    }

Suffice it to say that I have made the following things possible:

class A extends Object {};
    A::extend(array(
        'method3' => function($owner) { print 'method3'; }
    ));
    class B extends Object {};
    B::extend('A');
    
    $o1 = Object::construct();
    $o1->method1 = function($owner) { return 'method1'; };
    $o2 = B::construct();
    $o2->method2 = function($owner) { return 'method2'; };
    $o2->extend($o1);
    print $o2->method1(); // output: method1
    print $o2->method2(); // output: method2
    print $o2->method3(); // output: method3 

To rephrase the above: now we can use the same extend() method to respond to static calls. Specifically two types of static calls extend([string ClassName]) and extend([array aProperties]).

All the properties extended at class level are copied to the object when it is instantiated. Of course if a constructor is overridden in subclass, it would have to call parent::__construct().

Cool?

1 comment:

Eran Smith said...
This comment has been removed by the author.