Jump to content

Hacking together a sort of Virtual Inheritance in PHP?


deadimp

Recommended Posts

Is there a way to easily utilize a style of virtual inheritance in PHP? I'm looking for a structure where I could use the 'dreaded diamond structure', mentioned in C++ FAQ Lite.

Where I would apply it:

I have a database interface class, and from that class are derivatives. I have two abstract classes (though I don't name them as abstract) that extend this class:

> OrderedItem - This manages manual ordering for database items, that way you can define a custom ordering.

> UserItem - This item is connected to a user, something like a forum post, a news post, a comment, etc.

What I would want to do is see if I could combine the two, have a child class extend both OrderedItem and UserItem (since at those levels of functionality there isn't really a collision).

 

Is there a viable way to do this? Or am I just pot out of luck?

It would suck if I was... I've never really need virtual inheritance in the language that has it, C++, but now that I do, I'm in a different one.

Link to comment
Share on other sites

In PHP5 yes you could. You'd make use of the __call, __get, and __set overloaders. You'd have to set up an array of the parents, then you'd simply use (method/property)_exists on each to see if it had it, and if it did you'd call it. If two methods share the same name, you might run into problems specifying which one, so watch out for that.

Link to comment
Share on other sites

The big problem here is that PHP does not support multiple inheritance.

 

i.e., there is no PHP analog to this:

 

class Baz : public Foo, public Bar
{
};

 

You might consider introducing a new class OrderedUserItem which extends OrdererdItem.  You can then derive classes from OrderedUserItem which require a manual ordering and are related to a user.

 

Best,

 

Patrick

Link to comment
Share on other sites

I had thought about making the combined OrderedUserItem, which would involve deriving from one of the two, and then incorporating the other's properties and explicitly calling that class's properties using PHP's method invocation (Class::method()) which are still $this-calls.

However, I didn't think that much about using object overloading to try and do all of that. But yeah, running into cases where they share the same function/property (which will happen, since they're both derivates of DBI, database interface) will be somewhat of a pain to resolve.

 

Thanks for the info!

Link to comment
Share on other sites

Delegation can stand in for multiple inheritance. Using the magic functions you can make this a a lot easier (but unfortunately also less transparent). Class members of a later added object that share a name with a previous are overridden, not unlike with inheritance.

 

<?php
class SomeClass extends Backbone_Tools_MagicOverloader {

public function __construct(){
	Backbone_Tools_MagicOverloader::setSubject(new SubjectOne);
	Backbone_Tools_MagicOverloader::setSubject(new SubjectTwo);
	Backbone_Tools_MagicOverloader::setSubject(new SubjectThree);
}
}
?>

 

<?php
/**
* Backbone_Tools_MagicOverloader
*
* Convenience tool for delegation to multiple subject objects that can be made type-safe.
*
*/
abstract class Backbone_Tools_MagicOverloader {

/**
* Ordered list of subjects. 
* @var Array
*/
private $subjects = array();

/**
* Name of exception class to use when calling undefined method.
* @var string
*/
private $eClass = 'Backbone_Tools_MagicOverloader_Exception';

/**
* Subject type to accept. 
* @var string
*/
private $acceptType;



/**
 * Sets the type of subjects accepted.
 *
 * @param string $str
 */
protected function setAcceptType($str){
	$this->acceptType = $str;
}


/**
 * Adds subject object.
 *
 * @param object $subject
 * @throws Backbone_Tools_MagicOverloader_Exception
 */
protected function setSubject($subject){
	if(is_object($subject)){
		if($this->acceptType && !$subject instanceof $this->acceptType){
			throw new Backbone_Tools_MagicOverloader_Exception(
				"Invalid subject type '".gettype($subject)."'. acceptType: '{$this->acceptType}'."
			);
		}
		$this->subjects[] = $subject;
		return;
	}
	throw new Backbone_Tools_MagicOverloader_Exception("Unable to set subject, not an object ($subject).");
}

/**
 * Sets exception type to throw when a method is not found.
 *
 * @param string $eClassStr
 */
protected function setExceptionClass($eClassStr){
	$this->eClass = $eClassStr;
}

/**
 * Directly forwards a call, even if defined by child class.
 *
 * @param string $method
 * @param array $args
 * @return mixed
 */
protected function forwardCall($method, $args = array()){
	return $this->__call($method, $args);
}

/**
 * Intercepts method calls that fell through child.
 *
 * @param string $name
 * @param array $params
 * @return mixed
 */
    protected function __call($name, $params){

        foreach($this->subjects as $subject){
       		if(method_exists($subject,$name)){
            	return call_user_func_array(
                	array($subject,$name),$params);
        	}
        }
        throw new $this->eClass('Call to undeclared method "'.get_class($this).'::'.$name.'".');
    }
    
    /**
     * Sets property directlty. Bypass iteration of subjects.
     *
     * @param string $property
     * @param mixed $value
     */
protected function bypassSet($property, $value){
	$this->$property = $value;
}
    
/**
 * Intercepts 'set' call that fell trough child. 
 *
 * @param string $property
 * @param string $value
 * @throws Backbone_Tools_MagicOverloader_Exception
 */
    protected function __set($property, $value){
    	
    	foreach($this->subjects as $subject){
        if(property_exists($subject, $property)){
            $subject->$property = $value;
            //Setting failed?
            if($subject->$property !== $value){
	    		throw new Backbone_Tools_MagicOverloader_Exception(
	    			'Writing property "'.$property.'" failed, optionally use MagicOverloader::bypassSubjects().'
	    		);
            }
            return;
        }
    	}
    	//Set property on this object (default php behaviour).
        $this->$property = $value;
    	
    }
    
    /**
     * Intercept 'get' call that fell trough child.
     *
     * @param unknown_type $property
     * @return unknown
     */
    protected function __get($property){
    	foreach($this->subjects as $subject){
        if(property_exists($subject, $property)){
            return $subject->$property;
        }
    	}
  		trigger_error('Undefined property '.get_class($this).'::'.$property, E_USER_NOTICE);
    return null;
    }
    
    /**
     * Check if property not set in child is set in one of the subjects.
     *
     * @param string $property
     * @return bool
     */
    protected function __isset($property){
    	foreach($this->subjects as $subject){
        if(property_exists($subject, $property)){
            return true;
        }
    	}
    	return false;
    }
    
    /**
     * Unsets ALL occurrences of $property in subjects.
     *
     * @param string $property
     */
    protected function __unset($property){
    	foreach($this->subjects as $subject){
        if(property_exists($subject, $property)){
            unset($this->subject->$property);
        }
    	}
    }
}
?>

Link to comment
Share on other sites

  • 1 month later...

What do you mean by "since call_user_func() doesn't scope inside the class"?

 

If for some reason you want to avoid call_user_func, surely you could use Refelction and not resort to eval???

 

<?php 
    function callMethod($class,$func,$arg=array()) {
	//...//
        $refl = new ReflectionClass($class);
        $reflMeth = $refl->getMethod($func);
        return $reflMeth->invoke(null);
    }
?>

Link to comment
Share on other sites

What do you mean by "since call_user_func() doesn't scope inside the class"?

I tried the following callbacks in a call_user_func() call inside of a $this method (non-static):

> array($class,"method") - If the method references to $this, then it would generate the standard error that $this was unknown. If you try and invoke the constructor, it will say that it cannot be invoked as a static. This isn't the same as simply calling [class]::method() directly in the code.

> array($this,"$class::method") OR "$class::method" - It would throw an error about invalid callback (this was on PHP 5.2.2, though. I've upgraded, but haven't tested this out the same yet).

I had made a comment about this on call_user_func() in the manual, but it seems to have gotten lost. I probably didn't submit it right or something or other (though it did tell me that it got through).

 

If for some reason you want to avoid call_user_func, surely you could use Refelction and not resort to eval???

Yeah, I don't know why, but I was afraid to look into the Reflection library. That's probably a whole lot better than what I'm doing now.

Thanks for the tip!

Link to comment
Share on other sites

Ah, crap. I just ran into a problem using ReflectionMethod.

If I try and invoke one of the extended class's methods using ReflectionMethod::invoke/invokeArgs(), it throws an exception with the message "Given object is not an instance of the class this method was declared in".

 

Code:

/virtual.php

<?php
//Thacmus [http://sourceforge.net/projects/thacmus/] - Core - 'Virtual' class

//Virtual class defintion
//Is there a way to do this more statically? Or no?
//To get all of the variables, a temp class has to be constructed
//I'm not sure if there are any magic overloads to fool the instanceof / is_a / == [class] operators...
class Virtual {
//That's right... Just about the only private variable in all of Thacmus
private $_base=null; //List of ReflectorClass's - Idea from 448191 (http://www.phpfreaks.com/forums/index.php/topic,152955.msg691897.html)
//Initialize 
	//Can only be called once
	//$ctor - Call constructors - suggested to be true
	//[...] - Classes to extend (define using string)
		//Format: string $class -OR- {[string] $class, $arg1, $arg2...} (args are for ctor)
		//__construct() will be called for the class if it exists.
	//Order of definition means priority. First defined is first constructed, and copied.
	//This also pertains to function search order. The first class that has a method undefined in the actual class has its method called.
	//This is unlike C++, where defining two basic functions would have the compiler stop. Remember: run-time.
function extend($ctor) {
	$list=&$this->_base;
	if ($list) {
		trigger_error("Virtual::extend(): Already called",E_USER_WARNING);
		dump_callstack();
	}
	$list=array();
	$func_arg=func_get_args();
	$start=1;
	if (!is_bool($ctor)) {
		$start=0; //Start at that, since the ctor flag is off
		$ctor=false;
	}
	for ($i=$start;$i<count($func_arg);$i++) {
		$def=&$func_arg[$i];
		$class=$def;
		$arg=array();
		if (is_array($def)) {
			$class=$def[0];
			$arg=$def;
			array_shift($arg); //Pop the front off
		}
		$class=new ReflectionClass($class);
		if ($ctor && $class->hasMethod("__construct")) {
			//Calling the ctor ought to init some of the variables
			$this->callMethod($class,"__construct",$arg);
			//What happens if a diamond structure causes one ctor (the very base) to be called twice... Igh...
		}
		//Check the base class, copy any variables not init'd by the ctor
			//Only shallow copying should be needed since objects can't be instantiated in the class definition
			//I'm not sure how well this will handle statics and private variables...
		//Note: This can get ugly with multiple classes... It might be alright with the dreaded diamond structure,
			//but using classes with the same variables but different initial values (that have a vital role as that) could screw some stuff up
		//Can't figure out how to get values of default properties from ReflectionProperty
		$vars=get_class_vars($class->getName()); //Gah! This doesn't exclude statics!
		foreach ($vars as $var => $value) {
			if (!property_exists($this,$var)) $this->$var=$value; //Not sure how the whole statics thing might affect this
		}
		//Add class name to base list
		$list[]=$class;
	}
}
//Redirect non-specified (ie. $this->func() instead of Class::func()) calls to class
function __call($func,$arg) {
	foreach ($this->_base as $class) {
		if ($class->hasMethod($func)) {
			return $this->callMethod($class,$func,$arg);
		}
	}
	//Didn't work, warn
	dump_callstack();
	trigger_error("Unknown virtual method: ".get_class($this)."::$func()",E_USER_FATAL);
}
//Call a method, since call_user_func() doesn't scope inside the class
	//I made a post about this on http://www.php.net/manual/en/function.call-user-func.php
function callMethod($class,$func,$arg=array()) {
	$call=$class->getMethod($func);
	return $call->invokeArgs($this,$arg);
}
}

/* //Example 
class Test extends Virtual { //Do not extend the other classes - extend only Virtual
function __construct() {
	//This call adds the class definitions, and calls the constructors if they exist
	Virtual::extend(true, OrderedItem, UserItem, array(SomeClass, "biscuit",-2));
}
}
//It may just be with my PHP installation, but non-defined constants return strings of that identifier
//Therefore, using the actual class names (not simply strings) will work
*/
?>

Test:

<?php
include("../thacmus/virtual.php");

class Test extends Virtual {
function __construct() {
	Virtual::extend(Sub);
}
function blarg() {
	echo "blarg ";
	Sub::test();
}
}
class Sub {
function stuff() {
	echo "stuff ";
}
}
$obj=new Test();
$obj->stuff();
?>

Link to comment
Share on other sites

You would have to use something like this, which makes it very much like what I use:

 

<?php
class Virtual {

private $reflClasses = array();
private $subjects = array();

function extend($class, Virtual $object, array $args = array()) {
	$this->reflClasses[$class] = new ReflectionClass($class);
	if(!isset($this->subjects[$this->reflClasses[$class]->getName()])){
		if($this->reflClasses[$class]->hasMethod('__construct')){
			$this->subjects[$this->reflClasses[$class]->getName()] = $this->reflClasses[$class]->newInstance($args);
		}
			else {
			$this->subjects[$this->reflClasses[$class]->getName()] = $this->reflClasses[$class]->newInstance();	
		}

	}
}
function __call($method, $args) {
	foreach($this->reflClasses as $reflClass) {
		if($reflClass->hasMethod($method)){
			$reflMeth = $reflClass->getMethod($method);
			if($reflMeth->isConstructor()){
				return $reflClass->newInstance($args);
			}
				else {
				return $reflMeth->invokeArgs($this->subjects[$reflClass->getName()], $args);		
			}
		}
	}
}
}

class Test extends Virtual {
function __construct() {
	Virtual::extend('Sub', $this);
}
function blarg() {
	echo "blarg ";
	Sub::test();
}
}
class Sub {
function stuff() {
	echo "stuff ";
}
}
$obj=new Test();
$obj->stuff();
?>

Link to comment
Share on other sites

  • 2 weeks later...

I'm not sure that delegating parent method calls to actual instances of a parent would work with the structure using DBI in Thacmus... DBI has two 'parts', the static part, which is your basic database wrapper, and the instance part, which has a child class extend it and use it for interfacing with the static part.

The main problem I'm thinking of is the dreaded diamond structure. My first example of having a Virtual class extend OrderedItem and UserItem would be an instance of a diamond structure, because both are DBI classes. Each DBI class has its id, the id for its row in its defined table. If I use delegation like that, I would have to make sure that the common date between the virtual base (DBI) is shared equally among them, and that could be a pain. Plus, I would need to make sure that the table they reference to in their database calls (CRUD) would be that of child Virtual class, not their own.

That's why I want to stick with simply scoping the parent methods over the child Virtual class, to simply the process of 'data synchronization' across the multiple types of parents.

 

Arg! Too much stuff to think of...

 

Thanks for the help so far.

Link to comment
Share on other sites

This thread is more than a year old. Please don't revive it unless you have something important to add.

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.