Generic get property function - Good Idea?

Is this a bad idea


 public function getObjVar($var_name){
        return $this->$var_name;
 }

The TrackLogins class has these properties:

  • $this->login_x;
  • $this->wait_time;
  • $this->allowed_tries;

I get the correct results - these tests pass or echo their value renders the correct result. The variables don’t seem to be obscured as it is the first parameter of the method:


function testGetObjectVariableName(){
        $o_TrackLogins = new TrackLogins($this->sess);
        $o_TrackLogins->setLoginLimits(0,5,150);
        $this->assertEqual(0, $o_TrackLogins->getObjVar('loginX'));
        $this->assertEqual(5, $o_TrackLogins->getObjVar('allowed_tries'));
        $this->assertEqual(150, $o_TrackLogins->getObjVar('wait_time'));
    }

Your thoughts are appreciated.
Steve

You don’t actually have to write __set() and __get() to use these. They’re magic :slight_smile:

Try this instead:


$var = $o_TrackLogins->loginX; // get the value for loginX
$o_TrackLogins->loginX = 15; // set the value for loginX

By writing it like this, the __get() and __set() methods will be automagically called for you.

Uhm, use the magic __get function


public function __get($var) {
  return $this->$var;
}

__get is automatically invoked whenever an attempt is made to access a protected, private or unknown member of a class. Your __get function then has the responsibility of negotiating the request.

The related function __set does the same thing while setting values.

Finally, there’s __call which allows you to control access to protected, private and unknown methods though this isn’t used as often as the prior two.

For more information see PHP Magic Methods and [url=http://www.php.net/manual/en/language.oop5.overloading.php#language.oop5.overloading.members]PHP Overloading

Thanks Michael,

I see that I was trying to reinvent the wheel!

Cheers,
Steve

I updated both the set methods and get methods with __get and __set and it works like a charm; none of my SimpleTest assertions failed after doing so.

For others this is the code:


protected function filterForPositiveInteger($value){
         if(is_numeric($value) == FALSE) {
                $value = 0;
        }
        if(gettype($value) == "string"){
            $is_int = intval($value);
            if(gettype($is_int) == "integer" ){
                $value = $is_int;
            } else {
                $value = 0;
            }
        }
        if(is_float($value)){
            $value = round($value);
        }
           if($value < 0){
            $value = 0;
        }   
        return $value;
    }

    public function __get($var_name){
        return $this->$var_name;
    }
    
     public function __set($var_name, $value){
         $value = $this-> filterForPositiveInteger($value);
        $this->$var_name = $value;
        $this->session->set($var_name, $value);
        return $this->$var_name;
    }


and they are used like:


         $o_TrackLogins = new TrackLogins($this->sess);
        $this->assertEqual(2, $o_TrackLogins->__set('loginX', 2));
        $this->assertEqual(9, $o_TrackLogins->__set('allowed_tries', 9));
        $this->assertEqual(500, $o_TrackLogins->__set('wait_time', 500));

and


        $o_TrackLogins = new TrackLogins($this->sess);
        $this->assertEqual(0, $o_TrackLogins->__get('loginX'));
        $this->assertEqual(0, $o_TrackLogins->__get('allowed_tries'));
        $this->assertEqual(0, $o_TrackLogins->__get('wait_time'));

Here is a whole list of assertions so I can really beat a dead horse to death :slight_smile:


function testSetLoginXSessionVariable(){
        $o_TrackLogins = new TrackLogins($this->sess);
        $o_TrackLogins->__set('loginX',15);
        $this->assertEqual(15,$this->sess->get('loginX'));
        $o_TrackLogins->__set('loginX',1);
        
        
        /* Boundary Conditions */
        
        /* Boundaries that convert to a positive integer */
        $this->assertEqual(1,$this->sess->get('loginX'));
        $o_TrackLogins->__set('loginX',1.2);
        $this->assertEqual(1,$this->sess->get('loginX'));
        $o_TrackLogins->__set('loginX',1.5);
        $this->assertEqual(2,$this->sess->get('loginX'));
        $o_TrackLogins->__set('loginX','3');
        $this->assertEqual(3,$this->sess->get('loginX'));
        
        /* Boundaries that are set to 0 as they don't convert to a positive integer */
        $o_TrackLogins->__set('loginX',-5);
        $this->assertEqual(0,$this->sess->get('loginX'));
        $o_TrackLogins->__set('loginX','string');
        $this->assertEqual(0,$this->sess->get('loginX'));
        $o_TrackLogins->__set('loginX',$o_TrackLogins);
        $this->assertEqual(0,$this->sess->get('loginX'));
        $a = array(1,2,3,4);
        $o_TrackLogins->__set('loginX',$a);
        $this->assertEqual(0,$this->sess->get('loginX'));
        $n = NULL;
        $o_TrackLogins->__set('loginX', $n);
        $this->assertEqual(0,$this->sess->get('loginX'));
    }
    

Thanks again Michael in this barren ghost town :wink:
Steve

Hi Immerse,

Thanks.

That is pretty magic.

I wonder why this is not seen in code examples?

Regards,
Steve

There are some docs here: Overloading.
I think the PHP manual stays away from using these functions in other samples purely due to fact that it might make the samples harder to read/ understand. Magic can be confusing.

That it can be. That’s the tradeoff with the magic setters and getters - they make the external code easier to read and follow, but when those functions fail or get wonky they can create headaches.

Thanks Immerse and Michael,

Yes after reading more about them I can see why they can become more confusing (especially when debugging).

Regards,
Steve

An alternate way would be to implement the ArrayAccess interface from the SPL

Consider this:


class sampleObject implements ArrayAccess
{
     protected $data = array();

     public function offsetExists( $offset ) {
          return isset( $this->data[ $offset ]);
     }
	
     public function offsetGet( $offset ) {
          return isset( $this->data[ $offset ] ) ? $this->data[ $offset ] : FALSE ;
     }
	
     public function offsetUnset( $offset ) {
          // First check to see if it even exists
          if( !isset( $this->data[ $offset ] ) ) {
               return false;
          }

          // It exists, so lets unset it
          unset( $this->data[ $offset ] );
          return true;
     }
	
     public function offsetSet($offset, $value) {
          $this->data[ $offset ] = $value;
     }
}

You may now use sampleObject as though it were an array



// Instantiate the object
$object = new sampleObject;

// Set a property
$object[ 'myKey' ] = 'myValue';

// Will even respond to isset()
echo ( isset( $object[ 'myKey' ] ) ) ? $object[ 'myKey' ] : 'Key is not set' ;

// And of course it will respond to unset() as well
unset( $object[ 'myKey' ] );

if( !isset( $object[ 'myKey' ] ) ) {
     echo 'hey where did it go?';
}

No, just… no.

Implementing ArrayAccess by itself can make the code confusing as hell to the maintenance programmer. After all, it’s an array, I should be able to iterate with foreach right?? It’s a huge, huge can of worms.

If you need an object to behave in an array manner extend ArrayObject. Then the object behaves much as one would expect. But that still doesn’t address the issue of getters and setters, why they are useful. Let’s look at a real world class.


<?php
/**
 * An HTMLResponder creates an HTML response. The most frequently
 * used responder, its children include specific types of html
 * blocks, such as pages, forms or form elements.
 * 
 * @author Michael
 *
 */
namespace PNL;

class HTMLResponder extends Responder {
	
	/**
	 * MIME Type. Required by responder interface.
	 */
	protected $type = 'text/html';
	
	/**
	 * The name of the responder since, more likely than not
	 * it will be nested into other responders. This name is
	 * used as the key it is inserted under.
	 * @var string
	 */
	protected $name = '';
	
	/**
	 * Parsed Output (The array to be parsed is held in the private #storage array)
	 * @var string
	 */
	protected $output = null;
	
	/**
	 * Collection of templates, a template library object.
	 */
	protected $templates = null;	
	
	/**
	 * The template we parse if told to respond or our __toString method is called.
	 */
	protected $template = '';
	
	/**
	 * The input variables of the parseTemplate function. This is for a bit of
	 * idiot proofing - to prevent extract overwriting the function's arguments
	 * Attempting to set $this through extract throws a PHP fatal error anyway.
	 * 
	 * @var array
	 */
	protected $parsing = array();
	
	/**
	 * CONSTRUCTOR
	 * 
	 * @param TemplateLibrary.
	 */
	public function __construct( TemplateLibrary $templates, $template, $name, array $output = array() ) {
		assert($templates->exists($template) && !empty($name));
		
		$this->templates = $templates;
		$this->template = $template;
		$this->name = $name;

		parent::__construct( $output );
	}
	
	public function __set($var, $val) {
		switch($var) {
			case 'template': 
				assert($this->templates->exists((string)$val));
				$this->template = (string) $val;
			break;
			case 'name': $this->name = (string) $val; break;
			default: trigger_error("No write access to $var", E_USER_NOTICE);
		}
	}
	
	public function __get( $var ) {
		switch($var) {
			case 'template': return $this->template;
			case 'name': return $this->name;
			default: trigger_error("No access to $var", E_USER_NOTICE);
		}
	}
	
	/**
	 * Parsing method for this responder type.
	 */
	public function parse( $template = null ) {
		if (is_null($template)) {
			$template = $this->template;
		}
		$this->output = $this->parseTemplate($this->template);
	}
		
	/**
	 * Parse a template. 
	 * 
	 * PHTML files resolve in the scope of this function! That means they have
	 * access to the $this variable and can use the protected functions of the
	 * responder.
	 *
	 * @param string Template path
	 * @param array output variables.
	 */
	protected function parseTemplate($template, array $output = array() ) {
		
		// Protect the arguments from being overwritten by extract.
		$this->parsing = array (
			'template' => $template,
			'output' => $output
		);
		
		// Extract the global output first.
		extract( $this->getArrayCopy() );
		
		// Now the template's local output, which overwrites any global stuff.
		if (!empty($this->parsing['output'])) {
			extract( $this->parsing['output'] );
		}
		
		// Start an output buffer to capture the results of the following include.
		ob_start();
		
		/*
		 * Do NOT rely on $this->parsing to remain unchanged through the life of the
		 * included file - if a subsequent template calls this function it
		 * will change the parsing var. After this include call is made that
		 * variable's job is done.
		 */
		if (!include($this->templates->find($this->parsing['template']))) {
			throw new FatalException('Unable to read template file '.$template);	
		}
		
		// Now return that output.
		return ob_get_clean();
	}
}

Responder in turn extends ArrayObject. Here we have both __get and __set used at the same time the object receives data and behaves just like an array. This object is given data that it will write into a template. The name and template properties are protected because their contents must be controlled - the program’s behavior becomes undefined if either of those properties isn’t a string - and the template property needs to check to see if the template even exists (though this is done as an assertion. Once the code is live this shouldn’t need to be rechecked).

Array behavior and get/set are two entirely separate areas of concern with different uses and purposes. They shouldn’t be crossed up.