Martijn's PHP Coding Blog


November 10th, 2009

The Ultimate PHP5 OOP Class - 1

If you're new here, you may want to subscribe to my RSS feed. Thanks for visiting!

After reading up on PHP5 object orientation I decided to build a little test class that implemented as many of the OOP features offered by PHP5 as I could find.

Think magic functions, constants, cloning, getters and setters, abstract, final, inheritance, serialization and of course var_export support.

If I missed any concepts feel free to drop a note.

<?php

    /* The Ultimate PHP5 OOP Class
     *
     * Implements most of PHP5's OOP functionality in a single demonstration
     */

	/* PHP_VERSION_ID is available as of PHP 5.2.7, if our
	 * version is lower than that, then emulate it. Some of the
	 * functionality we test here is only available in 5.3.0 and
	 * higher.
	*/

	if(!defined('PHP_VERSION_ID'))
	{
    	$version = explode('.',PHP_VERSION);
	    define('PHP_VERSION_ID', ($version[0] * 10000 + $version[1] * 100 + $version[2]));
	}

    /* Interfaces specify a set of functions that a class MUST
     * implement.
     */

    interface DemoInterface
    {
        public function InterfaceTest();
    }

    /* Abstract classes cannot be instantiated. They define concepts that
     * their derived classes must implement.
     */

    abstract class DemoAbstract
    {

    	/* Constructor
    	 *
    	 */

        public function __construct()
        {
            $this->LogEntry(__CLASS__,__METHOD__);
        }

        /* LogEntry
         *
         * Reports on each methods entry. Defined in the parent class so that
         * we can call it from this constructor as well.
         *
         * @param string	Methodname (from __METHOD__)
         */

        public function LogEntry($methodName)
        {
        	echo "{$methodName} Called" . PHP_EOL;
        }    	

        abstract public function AbstractTest();
    }

    /* The DemoPHP5Class is marked as "final". Because of this selfish act
     * nobody is able to derive from this work and create their own sub class.
     */

    final class DemoPHP5Class extends DemoAbstract implements DemoInterface
    {
        /* Class Constants cannot be changed */
        const VERSION = '1.0';

        /* Private members are only available inside this class */
        private $_className;
        private $_constructorName;
        private $_fileName;
        private $_instanceCount;

        /*  Location for overloaded data. */
        private $_data = array();

        /* As the class is marked final there really is not point in declaring
         * any protected (accessible by derived classes) variables but
         * for good measure we include one.
         */

        protected $_meaningOfLife;

        /* The constructor is labeled as final. Because of this derived
         * classes are unable to implement their own constructor.
         */

        final function __construct()
        {
        	parent::__construct(); 					// Calling the parent constructor

            $this->_className = __CLASS__;          // Magic constants
            $this->_constructorName = __METHOD__;
            $this->_fileName = __FILE__;
            $this->_instanceCount = 0;              // Original, not a clone
            $this->_meaningOfLife = 42;             // No need to say more

			$this->LogEntry(__CLASS__,__METHOD__);
        }

        final function __destruct()
        {
			$this->LogEntry(__METHOD__);
        }

        /* Implements the Abstract members from the DemoAbstract
         * class.
         *
         * @returns string Notification message
         */

        public function AbstractTest()
        {
			$this->LogEntry(__METHOD__);
        }

        /* Implements the DemoInteface
         *
         * @returns string Notification message
         */

        public function InterfaceTest()
        {
			$this->LogEntry(__METHOD__);
        }

        /* Static functions are not part of an instance
         * and can only be called through "classname::staticmethod"
         */

        public static function StaticTest($number)
        {
        	/* Note how LogEntry is NOT static, but we can still call it */
        	/* A warning is generated in STRICT mode					 */
 			parent::LogEntry(__CLASS__,__METHOD__);
        	echo "StaticTest call with ({$number}) succesfull" . PHP_EOL;
        }

        /* Returns the name of the class
         *
         * @returns string Name of the class as discovered by the constructor
         */

        public function GetClassName()
        {
        	$this->LogEntry(__METHOD__);
            return $this->_className;
        }

        /* Set the meaning of life
         *
         * @param   int         Meaning of life
         * @thows   Exception   On invalid input
         * @returns this        Allow for chaining of calls
         */

        public function SetMeaningOfLife($meaningOfLife)
        {
			$this->LogEntry(__METHOD__);

            if (is_int($meaningOfLife))
            {
                $this->_meaningOfLife = $meaningOfLife;
            }
            else
            {
                throw new Exception('Meaning of Life doubted');
            }
            return $this;
        }

        /* Returns the meaning of life
         *
         * @returns int      Meaning of life as a core value
         */

        public function GetMeaningOfLife()
        {
			$this->LogEntry(__METHOD__);
            return $this->_meaningOfLife;
        }

        /* Returns the instance count
         *
         * @returns int 	Instance count
         */

        public function GetInstanceCount()
        {
        	return $this->_instanceCount;
        }

        /* Magic Methods
         *
         * These methods have special meaning inside of PHP5.
         */

        /* Return a string description of the class
         *
         * @returns string  Whatever the class fancies. Usually a description
         */

        public function __toString()
        {
			$this->LogEntry(__METHOD__);
            return '__toString called ; the meaning of life is our domain' . PHP_EOL;
        }

        /* From PHP 5.3.0 its possible to "invoke" a class, using it like
         * a function. Eg. 'DemoPHP5Class(50)'.
         */

        public function __invoke($x)
        {
			$this->LogEntry(__METHOD__);
            echo "__invoke was called with {$x}" . PHP_EOL;
        }

        /* PHP 5 allows the simulation of properties. If a property does not
         * exist when attempting to write the __set method is called giving
         * you an opportunity to handle the variable yourself.
         */

        /* Called when a not-class defined property is set by the user. For
         * example "DemoPHP5Class->FooBar = 500". We add an entry to the
         * $this->_data table.
         *
         * @param   string      A valid name
         * @param   undefined   Any valid PHP value
         * @throws  Exception   Only strings are considered valid names
         */

        public function __set($name,$value)
        {
			$this->LogEntry(__METHOD__);
            if (is_string($name)) {
             $this->_data[$name] = $value;
             return;
            }
            throw new Exception('Invalid Property name');
        }

        /* Called when a not class defined property is retrieved. We
         * attempt to satisfy the called by checking $this->_array.
         *
         * @param    string    A valid name
         * @throws   Exception When the property is not defined
         * @returns  undefined Value of the named property
         */

        public function __get($name)
        {
			$this->LogEntry(__METHOD__);
        	if (is_string($name) &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; array_key_exists($name, $this->_data))
                return $this->_data[$name];
            throw new Exception("Attempted to read undefined proporty");
        }

        /* This method is called whenever we try to access an inaccessible
         * property. We check $this->_data to see if a matching key exists.
         *
         * @param   string   A valid string property name
         * @returns bool     True on finding the property in the array
         */

        public function __isset($name)
        {
			$this->LogEntry(__METHOD__);
        	if (is_string($name) &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; array_key_exists($name, $this->_data))
                return true;
        }

        /* This method is called when unset is called on an inaccessible
         * class property. We check $this->_data and try to unset it.
         *
         * @param   string      A valid string property name
         * @throws  Exception   The property must exist, mmmm, ok?
         */

        public function __unset($name)
        {
		   $this->LogEntry(__METHOD__);
           if (is_string($name) &amp;amp;amp;amp;amp;&amp;amp;amp;amp;amp; array_key_exists($name, $this->_data))
            unset($this->_data[$name]);
           else
            throw new Exception("Unsetting non-existant property");
        }

        /* When a class is serialized (eg. put away into cold storage)
         * it is checked for a __sleep method. If it exists it is called
         * to allow the class to prepare a list with names of its most important
         * variables for safe storage.
         *
         * @returns array Critical variables that need to be stored
         */

        public function __sleep()
        {
			$this->LogEntry(__METHOD__);
            return array("_meaningOfLife","_className","_constructorName",
                         "_fileName","_data");
        }

        /* On deserialization the class needs to re-initialise.
         * This is a good point to re-establish the database connection and
         * any other critical (but not storable) resources.
         */

        public function __wakeup()
        {
			$this->LogEntry(__METHOD__);
        }

        /* When an object is "cloned" an exact copy is created. This is not
         * always desirable (think resource handles that are now shared by two
         * objects). The __clone magic function is called on the COPY and
         * allows it to initialise any variables it sees fit.
         */

        public function __clone()
        {
			$this->LogEntry(__METHOD__);
        	// Clones know they are copies because only the original
            // object has an _instanceCount value of 0.
            $this->_instanceCount++;
        }

        /* Used by var_export which allows the creation of a PHP evaluable
         * string that would re-create this object.
         *
         * See also: http://www.thoughtlabs.com/2008/02/02/phps-mystical-__set_state-method/
         */

        public static function __set_state($an_array)
        {
        	echo "static::__set_state called" . PHP_EOL;			

			// Create a new class, set its parameters and
			// exit. 

			$tmp = new DemoPHP5Class();
			foreach($an_array as $name => $value)
			 $tmp->$name = $value;

			return $tmp;
        }

    }

    /* Enable strict warnings */
    error_reporting(E_ALL | E_STRICT);

    /* Test Code */
    $demo = new DemoPHP5Class();

    /* Example of a class constant */
    echo "Class version: " . DemoPHP5Class::VERSION . PHP_EOL;  

    /* Test #1:  Abstract method in parent class, simply call it*/
	$demo->AbstractTest();

	/* Test #2:  Call the Interface member */
	$demo->InterfaceTest();

	/* Test #3:  Call of a static member */
	DemoPHP5Class::StaticTest(20);

	/* Test #4:  Call a regular get method  */
	$demo->GetClassName();

	/* Test #5:  Call a regular set method with valid param */
	$demo->SetMeaningOfLife(43);

	/* Test #6:  Call a regular set method with INVALID param */
	try {
		$demo->SetMeaningOfLife(42.5);
	}
	catch (Exception $e) {
		echo "Exception Caught OK" . PHP_EOL;
	}

	/* Test #7: Try to print the object , calling __tostring() */
	echo $demo;

	/* Test #8: Invoke the object , bit like magic really */
	/* 			only available from PHP 5.3.0 up	      */
	if (PHP_VERSION_ID > 50300)
	 $demo('abacradaba');

	/* Test #9: Set an non-existant member of the object, causing
	 * a call to __set, which will emulate/store the value
	 */		 

	$demo->non_existing_member = 256;

	/* Test #10: Confirm that the new member has been set */

	if (isset($demo->non_existing_member))
	 echo 'Confirmed existance of \$demo->non_existing_member' . PHP_EOL;
	else
	 echo 'Failed to verify setting of \$demo->non_existing_member' . PHP_EOL;

	/* Test #11: Retrieve the non-existing member, causing a call
	 * to __get, which will attempt to retrieve it from the internal
	 * storage array.
	 */

	if ($demo->non_existing_member == 256)
		echo "Retrieval of \$demo->non_existing_member succesfull" . PHP_EOL;
	else
		echo "Retrieval of \$demo->non_existing_member FAILED" . PHP_EOL;

	/* Test #12: Remove the member, and test if this was succesfull */

	unset($demo->non_existing_member);
	if (!isset($demo->non_existing_member))
	 	echo 'Confirmed removal of \$demo->non_existing_member' . PHP_EOL;
	else
	 	echo 'Failed to remove \$demo->non_existing_member' . PHP_EOL;

	/* Test #13: Serialization , put the object to sleep and turn it
	 * into a form that we can store away savely.
	 */

	$serialized_data = serialize( $demo );
	$demo2 = unserialize($serialized_data);  

	/* Test #14: Cloning. After cloning the object the clone should
	 * have a higher instance value than the original object because
	 * of the additional changes made by __clone
	 */

	$demo3 = clone $demo;
	if ($demo3->GetInstanceCount() > $demo->GetInstanceCount())
	 	echo "Succesfull cloning; instance count has been increased. " . PHP_EOL;
	else
	 	echo "Failed to clone; instance count is the same. " . PHP_EOL;

	/* Test 15: Var_Export must produce valid PHP code that can be
	 * run through 'eval'. It has trouble with objects , so it needs
	 * a little help from the __set_state magic function
	 */

	eval('$demo4 = ' . var_export($demo, true) . ';');

	/* Remember, we modified the meaning of life to 43 in $demo */
	if ($demo4->GetMeaningOfLife()==$demo->GetMeaningOfLife())
	 echo "Succesfull re-creation of the \$demo class" . PHP_EOL;
	else
	 echo "Failed to recreate. The meaning of life was lost. " . PHP_EOL; 

	/* EOF */

?>