Return to Prototype Based Programming in PHP
With the implementation of the get_called_class() function in PHP, prototype based programming in PHP is possible. The new function is in both PHP5.3 and PHP6 snapshots. So, I went back to the drawing board and came up with a fully capable class. The $this keyword is usable, as well as being able to use the self, parent, and static (new with the late static binding patch) keywords. Code after the jump.
So let’s get straight to the code that does it.
prototype.php
<?php namespace com::schmalls::personal; /** * This class holds the prototype capabilities * * Extending this class makes it prototype capable. * Instantiating this class with the param and * method values allows a runtime method to be * added to a class. */ class prototype { /** * Holds the object prototype methods for all subclasses */ private static $_object_methods = array(); /** * Holds the static prototype methods for all subclasses */ private static $_static_methods = array(); /** * Holds all of the local prototype methods for this object */ private $_local_methods = array(); /** * Holds an instance of the prototype class */ private static $_prototype = null; /** * Method parameters */ private $___params = null; /** * Method body */ private $___method = null; /** * Whether the method has static method calls */ private $___static_calls = null; /** * Holds the last called class */ private $___class = null; /** * Whether the last call was static */ private $___static = null; /** * Default constructor * * Child classes should not call this class. When called with the params * and method values, it can be used to set a new prototype method. * * @param mixed $params string input like create_user_function or array of argument names * @param string $method method body */ public function __construct( $params = null, $method = null ) { // check if we are being used to make a prototype method if ( ( $params !== null ) && ( $method !== null ) ) { // check if this is an array if ( is_array( $params ) ) { // make the parameter string $params = implode( $params, ', ' ); } // check if this method uses the this keyword if ( strpos( $method, '$this' ) !== false ) { // splits the method body by parts in which the this keyword should not be altered $regex = '~' . // start regular expression '(/\*.*?\*/)' . // /* */ style comments '|' . // or '(#.*?$)' . // # style comments '|' . // or '(//.*?$)' . // // style comments '|' . // or '(\'.*?[^\\\\]\')' . // single quote strings '~' . // end regular expression 'i' . // match case insensitive 's' . // dot matches all characters including newlines 'm'; // match multiline; $pieces = preg_split( $regex, $method, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE ); // store how many keywords have been replaced $i = 0; // loop through each of the split pieces foreach( $pieces as $piece ) { // replace the this keyword with a safer name $method = substr_replace( // begin substring replace of piece section $method, // the main method body preg_replace( // begin preg replace of this keyword '/' . // start regular expression '\$this' . // match the this keyword '(?![[a-zA-Z0-9_\x7f-\xff])' . // not followed by another valid variable name character '/' . // end regular expression 'i', // match case insensitive '$___this', // replace with $___this $piece[0], // the piece string -1, // no limit $j // number of matches ), // end preg replace ( $piece[1] + ( $i * 3 ) ), // offset to beginning of piece strlen( $piece[0] ) // length of piece ); // end substring replace // increase how many keyword replacements have been made $i += $j; } } // set the method parameters string including space for the this reference $this->___params = trim( $params ); if ( $this->___params === '' ) { $this->___params .= '&$___this = null'; } else { $this->___params .= ', &$___this = null'; } // set the method body string $this->___method = $method; // check whether this method makes static calls and therefore has to be recreated on each call $this->___static_calls = ( strpos( $method, 'self' ) || strpos( $method, 'parent' ) || strpos( $method, 'static' ) ) !== false; } } /** * Allows the prototype variable to be used within objects as well as setting up the called class * * @param string $name * @return prototype */ public function __get( $name ) { // see if we are getting the prototype if ( $name === 'prototype' ) { // see if the static prototype variable has been instantiated if ( self::$_prototype === null ) { self::$_prototype = new prototype( get_called_class() ); } // set the calling class and set to dynamic self::$_prototype->___class = get_called_class(); self::$_prototype->___static = false; return self::$_prototype; } } /** * This is where the magic happens for dynamic prototype method calls * * @param string $name * @param array $arguments * @return mixed */ public function __call( $name, $arguments ) { // fix name $name = strtolower( $name ); $class = get_called_class(); // add this to arguments $arguments[] = &$this; // check if this is a local class method if ( isset( $this->_local_methods[$name] ) ) { // check if the method has static calls if ( is_array( $this->_local_methods[$name] ) ) { $method = create_function( // create function with static calls $this->_local_methods[$name]['params'], // parameters self::_fixStaticMethodCalls( // fix static calls $this->_local_methods[$name]['method'], // method body $class, // self get_parent_class( $class ), // parent $class // static ) // end fix static calls ); // end create function } else { // use previously created function $method = $this->_local_methods[$name]; } // call the method return call_user_func_array( $method, $arguments ); } else { // try object method $chain = &self::_getCallingChain( $class ); do { // get the current chain location $cur = &self::_getCur( $chain, self::$_object_methods ); if ( isset( $cur['_methods'][$name] ) ) { // check if the method has static calls if ( is_array( $cur['_methods'][$name] ) ) { $method = create_function( // create function with static calls $cur['_methods'][$name]['params'], // parameters self::_fixStaticMethodCalls( // fix static calls $cur['_methods'][$name]['method'], // method body $chain[count( $chain ) - 1], // self $chain[count( $chain ) - 2], // parent $class // static ) // end fix static calls ); // end create function } else { // use previously created method $method = $cur['_methods'][$name]; } // call the method return call_user_func_array( $method, $arguments ); } } while ( array_pop( $chain ) ); // apparently no object method, try static method return self::_staticCall( $name, $arguments, $class ); } } /** * Static call method * * @param string $name * @param array $arguments * @return mixed */ public static function __callStatic( $name, $arguments ) { // fix name $name = strtolower( $name ); $class = get_called_class(); return self::_staticCall( $name, $arguments, $class ); } /** * This is where the magic happens for setting prototype functions * * Child classes should call this if it is overridden * * @param string $name * @param mixed $value */ public function __set( $name, $value ) { // check if this is an instance of the prototype class with the correct format if ( $value instanceof self && ( isset( $value->___params ) && isset( $value->___method ) ) ) { // fix name $name = strtolower( $name ); // check for this keyword in non-object context if ( $this->___static && ( strpos( $value->___method, '$___this' ) !== false ) ) { trigger_error( 'Using $this when not in object context', E_USER_ERROR ); return; } $class = get_called_class(); // if we have static calls, we have to delay creation if ( $value->___static_calls ) { $method = array( 'params' => $value->___params, 'method' => $value->___method ); } else { $method = create_function( $value->___params, $value->___method ); } // check if this was through the prototype object if ( $class === __CLASS__ ) { // get the calling chain $chain = &self::_getCallingChain( $this->___class ); // check if the prototype object was called statically if ( $this->___static ) { $methods = &self::$_static_methods; } else { $methods = &self::$_object_methods; } // get current location in calling chain $cur = &self::_getCur( $chain, $methods ); // set the method $cur['_methods'][$name] = $method; } else { $this->_local_methods[$name] = $method; } } else { // this wasn't a prototype method $this->$name = $value; } } /** * Returns the prototype holder class * * The prototype holder is used to add prototype methods to * child classes that are global to all objects. The functions * also allow inheritance. This function should only be called * statically (i.e. mySubClass::prototype()->newMethod = new * prototype( '$arg', 'echo $arg;' ) ). To create object * methods, use the prototype variable (i.e. $my_object-> * prototype->newMethod = new prototype( '$arg', 'echo $arg;' ) * ). To create a local object method, don't use prototype * (i.e. $my_object->newMethod = new prototype( '$arg', * 'echo $arg;' ) ). * * @return prototype */ public static function prototype() { // see if the static prototype variable has been instantiated if ( self::$_prototype === null ) { self::$_prototype = new prototype( get_called_class() ); } // set the calling class and set to static self::$_prototype->___class = get_called_class(); self::$_prototype->___static = true; return self::$_prototype; } /** * Walks though parent classes and returns an array of the * calling chain * * @param string $class * @return array */ private static function &_getCallingChain( $class ) { $chain = array( $class ); // get classes until we hit the prototype class while ( $class !== __CLASS__ ) { $class = get_parent_class( $class ); $chain[] = $class; } // reverse the array to get the calling chain $chain = array_reverse( $chain ); return $chain; } /** * Gets the current location in the function holding array * * @param array &$chain * @param array &$cur * @return array */ private static function &_getCur( &$chain, &$cur ) { // loop until we have the current location foreach ( $chain as $class ) { $cur = &$cur[$class]; } return $cur; } /** * Fixes static method calls (self, parent, and static keywords) * * @param string $method * @param string $self * @param string $parent * @param string $static * @return string */ private static function _fixStaticMethodCalls( $method, $self, $parent, $static ) { // splits the method body by parts in which the keywords should not be altered $regex = '~' . // start regular expression '(/\*.*?\*/)' . // /* */ style comments '|' . // or '(#.*?$)' . // # style comments '|' . // or '(//.*?$)' . // // style comments '|' . // or '(\'.*?[^\\\\]\')' . // single quote strings '~' . // end regular expression 'i' . // match case insensitive 's' . // dot matches all characters including newlines 'm'; // match multiline $pieces = preg_split( $regex, $method, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE ); $i = 0; // build the search array $search = array( '/' . '(?<=$|[^a-zA-Z0-9_\x7f-\xff])' . // match beginning of line or any non variable character 'self' . // match the self keyword '(?=\s*::|$)' . // match paamayim nekudotayim or end of string '/' . // end regular expression 'i', // match case insensitive // same for parent keyword '/(?<=$|[^a-zA-Z0-9_\x7f-\xff])parent(?=\s*::|$)/i', // same for static keyword '/(?<=$|[^a-zA-Z0-9_\x7f-\xff])static(?=\s*::|$)/i' ); // build the replace array $replace = array( $self, $parent, $static ); // loop thorugh each piece foreach( $pieces as $piece ) { // replace the static calls $updated = preg_replace( $search, $replace, $piece[0], -1, $j ); // get the length of the original $length = strlen( $piece[0] ); // replace the piece $method = substr_replace( $method, $updated, ( $piece[1] + $i ), $length ); // update how much has been added (or removed) from the method $i += ( strlen( $updated ) - $length ); } return $method; } /** * This is where the magic happens for static prototype method calls * * @param string $name * @param array $arguments * @param string $class * @return mixed */ private static function _staticCall( $name, $arguments, $class ) { $chain = &self::_getCallingChain( $class ); do { // get the current chain location $cur = &self::_getCur( $chain, self::$_static_methods ); if ( isset( $cur['_methods'][$name] ) ) { // check if the method has static calls if ( is_array( $cur['_methods'][$name] ) ) { $method = create_function( // create function with static calls $cur['_methods'][$name]['params'], // parameters self::_fixStaticMethodCalls( // fix static calls $cur['_methods'][$name]['method'], // method body $chain[count( $chain ) - 1], // self $chain[count( $chain ) - 2], // parent $class // static ) // end fix static calls ); // end create function } else { // use previously created method $method = $cur['_methods'][$name]; } // call the method return call_user_func_array( $method, $arguments ); } } while ( array_pop( $chain ) ); // the method doesn't exist trigger_error( 'Call to undefined method ' . $class . '::' . $name . '()', E_USER_ERROR ); } }
I tried to document it as much as possible in the code, so I won’t explain it too much. I will point out that all of the regular expression calls have to do with runtime created functions not being able to use the $this keyword directly as well as allowing the static keywords to be used.
Now let’s do some sample code usage.
<?php include 'com/schmalls/personal/prototype.php'; import com::schmalls::personal::prototype; class A extends prototype { public $monkey = 'monkey'; const yes = 'yes'; } class C extends A {} $a = new A(); $b = new A(); $c = new C(); // create an object method that calls a static method $a->prototype->objectMethod = new prototype( '$test', ' /** * comment */ echo $test, \' $this\', \'<br />\', "\n"; // test echo $this->monkey, \'<br />\', "\n"; # monkey $this->monkey = "cool"; self::staticMethod(); ' ); // create a static method that uses self A::prototype()->staticMethod = new prototype( '', ' echo self::yes, \'<br />\', "\n"; echo \'<br />\', "\n"; ' ); // create a local method $a->localMethod = new prototype( '$test', ' // call the object method $this->objectMethod( $test ); ' ); // all three objects can call the object method, // but none of the static calls work $a->objectMethod( '$a->objectMethod' ); $b->objectMethod( '$b->objectMethod' ); $c->objectMethod( '$c->objectMethod' ); //A::objectMethod( 'A::objectMethod' ); //C::objectMethod( 'B::objectMethod' ); // all three objects can call the static method, // and all static calls work $a->staticMethod(); $b->staticMethod(); $c->staticMethod(); A::staticMethod(); C::staticMethod(); // only the $a object can call this one $a->localMethod( '$a->localMethod' ); //$b->localMethod( '$b->localMethod' ); //$c->localMethod( '$c->localMethod' ); //A::localMethod( 'A::localMethod' ); //C::localMethod( 'C::localMethod' );
April 4th, 2008 at 3:08 AM
Stunning. This is going to be massively useful for me once I’ve had a proper read through to understand how it all works.
Beautiful work!
June 18th, 2009 at 12:19 AM
Namespaces use backslash instead of double quotes now in PHP 5.3.
June 18th, 2009 at 2:15 AM
Sorry, not double quotes, but Paamayim Nekudotayim. Cheers.