Page 1 of 1

Parent class returns instance of child?

Posted: Fri May 19, 2006 9:24 am
by Hades
I'd appreciate it if someone could take a look at my code and offer some advice as to where I'm going wrong.

Here's the code...

Config class is an extension of my DataObject class (similar to the one from Maugrim's tutorial on this site.

Code: Select all

<?php

class Config extends DataObject {

	function Config($fields=null) {
		// If the "_Extra" file exists this is where it will be located.
		$extra_file = BASE_DIR . '/models/' . $this->className . '_Extra.class.php';

		// Check if it exists.  If it exists check if it has already been used.
		if((file_exists($extra_file)) && (!isset($this->Extra))){
			require_once($extra_file); // require the extra class
			$class = $this->className.'_Extra'; // build the class name
			$instance &= new $class(); // instantiate the _Extra class
			return $instance; // Return the instance instead of this class
		} else {
			// This section will be called if this constructor is called from the _Extra class
			parent::DataObject($fields); // Run this class' parent constructor

			// Set some stuff
			$this->tableName = 'config';
			$this->className = 'Config';
			$this->primaryKey = 'id';
		}
	}

	function getId() { return $this->data['id']; }
	function setId($var) { $this->data['id'] = $var;  $this->changedFields[] = 'id'; }

	function getValue() { return $this->data['value']; }
	function setValue($var) { $this->data['value'] = $var;  $this->changedFields[] = 'value'; }

}

?>

The Config_Extra class I am using to try to dynamically add additional methods to the Config class without needing to instantiate the Config_Extra class directly in my front end code. Rather I'd like the constructor for the Config class to determine whether there are any extras to include and sort it out there.

Code: Select all

<?php
class Config_Extra extends Config {

	var $Extra = true; // Defines if this file has been used yet or not.

	function Config_Extra($fields=null){
		// Call base constructor
		parent::Config($fields);
	}

	// this is the extra method I want to dynamically include in the parent class
	// by just instantiating the parent class in my front code.
	function getTagLine(){
		$this->setId('tag_line');
		$this->getByPk();

		return $this->data['value'];
	}
}
?>
When I try the following ...

Code: Select all

<?php
include(BASE_DIR.'/models/Config.class.php');
$test = new Config();
echo $test->getTagLine();
?>
I get the error ...

Code: Select all

Fatal error: Call to undefined method Config::getTagLine() <snip>
Can anyone offer any advice on this as it's driving me crazy.

Thanks.

Posted: Fri May 19, 2006 10:49 am
by BadgerC82
Basically with this code:

Code: Select all

$test = new Config(); 
echo $test->getTagLine();
You are doing this:

Config->getTagLine();

Now the getTagLine function does not exist in a Config class object it extends in the Config_Extra class.

you want to do:

Code: Select all

$test = new Config_Extra(); 
echo $test->getTagLine();
:)

Posted: Fri May 19, 2006 11:15 am
by Hades
Thanks for the response but I understand inheritence.

What I'm trying to do is make it so that Config's constructor automatically includes Config_Extra and returns a Config_Extra object if it exists.

So I can do

Code: Select all

<?php
$test = new Config();
?>
and it will automatically create an instance of Config_Extra if that class exists.

Posted: Fri May 19, 2006 11:37 am
by Chris Corbyn
Hades wrote:Thanks for the response but I understand inheritence.

What I'm trying to do is make it so that Config's constructor automatically includes Config_Extra and returns a Config_Extra object if it exists.

So I can do

Code: Select all

<?php
$test = new Config();
?>
and it will automatically create an instance of Config_Extra if that class exists.
I'm kinda failing to see the point here... you could automagially load the other class into the parent class, but you can't make an object return something different. Perhaps don't instantiate it with the constructor, try something along the lines of the singleton pattern instead. It looks like you're using PHP4 so we'll go that way:

Code: Select all

<?php

class Config extends DataObject {

        function Config($fields=null) {
                /* stuff here */
        }

        function getId() { return $this->data['id']; }
        function setId($var) { $this->data['id'] = $var;  $this->changedFields[] = 'id'; }

        function getValue() { return $this->data['value']; }
        function setValue($var) { $this->data['value'] = $var;  $this->changedFields[] = 'value'; }

        static function &getInstance()
        {
             static $instance;
             if (!$instance)
             {
                      if (in_array('Config_Extra', get_declared_classes()))
                      {
                             $instance = array( new Config_Extra );
                      }
                      else
                      {
                             $instance = array( new Config );
                      }
             }
              return $instance[0];
        }
}

class Config_Extra extends Config {

        var $Extra = true; // Defines if this file has been used yet or not.

        function Config_Extra($fields=null){
                // Call base constructor
                parent::Config($fields);
        }

        // this is the extra method I want to dynamically include in the parent class
        // by just instantiating the parent class in my front code.
        function getTagLine(){
                $this->setId('tag_line');
                $this->getByPk();

                return $this->data['value'];
        }
}

$object =& Config::getInstance();

var_dump($object);
But this seems incredibly messy. Could you elaborate on where it would be used and maybe we can think up a nicer solution :)

EDIT | Oops - corrected (I wrote new array() :P)

Posted: Fri May 19, 2006 1:07 pm
by Hades
Thanks for that response.

I got there just slightly ahead of you with the singleton idea but modified it slightly as I wanted it to create a new instance on each call.

./lib/DataObject.class.php

Code: Select all

<?php
class DataObject {

	var $exists = false;
	var $data = array();
	var $changedFields = array();
	var $primaryKey;
	var $tableName;
	var $className;
	var $dao;

	function DataObject($fields=null) {
		$this->dao =& DataAccess::getInstance();
		if(!is_null($fields))
		{
			$this->import($fields);
		}
	}

	function import($fields=null) {
		foreach($fields as $key=>$val)
		{
			$this->setValue($key, $val);
		}
	}

	function setValue($key, $val) {
		$this->data[$key] = $val;
	}

	function getValue($key) {
		return $this->data[$key];
	}

	function &getArray() {
		return $this->data;
	}

	function clearData() {
		$this->data = array();
		$this->changedFields = array();
		$this->exists = false;
	}

	function getPrimaryKey() {
		return $this->primaryKey;
	}


	function getPrimaryKeyValue() {
		return $this->data[$this->primaryKey];
	}

	function setPrimaryKeyValue($value) {
		$this->data[$this->primaryKey] = $value;
	}

	function getClassName() {
		return $this->className;
	}

	function getTableName() {
		return $this->tableName;
	}

	function getExists() {
		return $this->exists;
	}

	function setExists($var = true) {
		$this->exists = $var;
	}

	function getChangedFields() {
		$this->changedFields = array_unique($this->changedFields); // remove duplicate values
		return $this->changedFields;
	}

	function clearChangedFields() {
		$this->changedFields = array();
	}

	function getByPk($value=null) {
		if(isset($value))
		{
			$this->dao->getByPk($this, $value);
		}
		else
		{
			$this->dao->getByPk($this);
		}
	}

	function save() {
		$this->dao->save($this);
	}

	function delete() {
		$this->dao->delete($this);
	}

	function getRow() {
		$this->dao->getRow($this);
	}

	function getAll() {
		return $this->dao->getAll($this);
	}
}
?>
./models/Config.class.php

Code: Select all

<?php

class Config extends DataObject {

	function Config($fields=null) {
		parent::DataObject($fields);
		$this->tableName = 'config';
		$this->className = 'Config';
		$this->primaryKey = 'id';
	}

	function &newInstance($fields=null) {
		$instance = null;
		$file = MODEL_DIR . 'extras/Config_Extra.class.php';
		if((file_exists($file))){
			require_once($file);
			$class = 'Config_Extra';
			if(class_exists($class)){
				$instance =& new $class($fields);
			} else {
				$instance =& new Config($fields);
			}
		} else {
			$instance =& new Config($fields);
		}
		return $instance;
	}

	function getId() { return $this->data['id']; }
	function setId($var) { $this->data['id'] = $var;  $this->changedFields[] = 'id'; }

	function getVal() { return $this->data['val']; }
	function setVal($var) { $this->data['val'] = $var;  $this->changedFields[] = 'val'; }

}

?>
./models/extras/Config_Extras.class.php

Code: Select all

class Config_Extra extends Config {

	function Config_Extra($fields=null){
		// Call base constructor
		parent::Config($fields);
	}

	// this is the extra method I want to dynamically include in the parent class
	// by just instantiating the parent class in my front code.
	function getTagLine(){
		$this->setPrimaryKeyValue('tag_line');
		$this->getByPk();

		return $this->getVal();
	}
}

Basically I have a script that automagically generates the first level of DataObject children from the tables in my database.

The problem was that some of those classes were then added to with what Maugrim calls "convenience methods". i.e. Methods that are not strictly needed but do something that would otherwise be repetitive.

This meant that, should the database schema change, the classes would have to be regenerated and the convenience methods would have to be re-added.

Since not all DataObjects will have convenience methods the controller I use needs to instantiate the relevant first level DataObject child class without knowing if it has any convenience methods.

Hence why I wanted a way of getting these classes to automagically redirect the constructor to the extras class if it existed.

Note: I was using the code above as a test case only... just wanted to get something working before porting it to the rest of the classes.

Posted: Fri May 19, 2006 2:56 pm
by Chris Corbyn
OK I see.... While I was writing the singleton stuff I pondered if a singleton was strictly necessary but figured it was configuration stuff, which is commonly placed in singletons. Using class_exists() is certainly a lot cleaner than my messy idea :P Forgot that function existed. Good work.

Posted: Sat May 20, 2006 12:26 am
by Hades
Thanks d11wtq... It's good to see that I'm on the write track anyhow :)

I was actually quite impressed with myself after doing all that then reading your post and having it confirm that I was indeed going about it the right way. Thanks :mrgreen:

Posted: Sat May 20, 2006 2:54 pm
by bdlang
I've read this thread about a dozen times, thinking about the Config class using the Factory method to return an instance of itself or it's child class. For your consideration:

Code: Select all

class ConfigFactory {

    // singleton method returns instance of correct class
    function &buildConfig($fields=null) {
                $instance = null;
                $file = MODEL_DIR . 'extras/Config_Extra.class.php';
                if((file_exists($file))){
                        require_once($file);
                        $class = 'Config_Extra';
                        if(class_exists($class)){
                                $instance =& new $class($fields);
                        } else {
                                $instance =& new Config($fields);
                        }
                } else {
                        $instance =& new Config($fields);
                }
                return $instance;
        }   
    
}


class Config extends DataObject {

        function Config($fields=null) {
                parent::DataObject($fields);
                $this->tableName = 'config';
                $this->className = 'Config';
                $this->primaryKey = 'id';
        }

        function getId() { return $this->data['id']; }
        function setId($var) { $this->data['id'] = $var;  $this->changedFields[] = 'id'; }

        function getVal() { return $this->data['val']; }
        function setVal($var) { $this->data['val'] = $var;  $this->changedFields[] = 'val'; }

}


// untested example
$configObj =& ConfigFactory::buildConfig(params);
One more level of abstraction returns the appropriate object, and the Config class can do it's job without taking on the extra task of determining which class to use.

Untested, obviously, but food for thought. :)