<?php
    /**
     * This file defines PEAR_DelegateOwner, which provides delegation capabilities.
     * 
     * @package PEAR
     */
    
    /* vim: set expandtab tabstop=4 shiftwidth=4: */
    // +----------------------------------------------------------------------+
    // | PHP version 4ΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚ|
    // +----------------------------------------------------------------------+
    // | Copyright (c) 1997-2003 The PHP GroupΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚ|
    // +----------------------------------------------------------------------+
    // | This source file is subject to version 2.0 of the PHP license,ΚΚΚΚΚΚΚ|
    // | that is bundled with this package in the file LICENSE, and isΚΚΚΚΚΚΚΚ|
    // | available through the world-wide-web atΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚ|
    // | http://www.php.net/license/2_02.txt.ΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚ|
    // | If you did not receive a copy of the PHP license and are unable toΚΚΚ|
    // | obtain it through the world-wide-web, please send a note toΚΚΚΚΚΚΚΚΚΚ|
    // | license@php.net so we can mail you a copy immediately.ΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚ|
    // +----------------------------------------------------------------------+
    // | Authors: Herr Witten <LingWitt@yahoo.com>ΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚΚ       |
    // |ΚΚΚΚΚΚΚΚΚΚ                                                            |
    // +----------------------------------------------------------------------+
    //
    // $Id$
    
    require_once "PEAR.php";
    
    /**
     * Base class for objects that allow delegation.
     *
     * It is first necessary to discuss the role of delegates in PEAR.
     * With the advent of PHP 5, a whole host of new programming techniques
     * were introduces. Among them were class interfaces. These interfaces
     * provide much of the benefits of multiple inheritance (with regard to
     * methods) without adulterating the inheritance hierarchy. That is, an
     * object can inherit from a parent class as usual, but still possess
     * methods that qualify it as a member of another group of objects, related
     * by certain behavior but still exclusively separate in the hierarchy.
     * The interfaces only define the protocol to which each adopting class
     * must adhere, but they do not define the implementation. This is very
     * useful in many instances, but for some purposes, the implementation
     * remains viritually the same for all adopting parties. This is where
     * delegation enters.
     *
     * A delegate is a class that defines methods which are intended to be
     * called by an adopting object as if they were members of that object.
     * For instance,
     * <code>
     * class Foo extends PEAR_DelegateOwner
     * {
     *     public function _construct()
     *     {
     *         parent::_construct();
     *     }
     *
     *     public function _destruct()
     *     {
     *         parent::_destruct();
     *     }
     * }
     * 
     * $foo = new Foo();
     * $foo->bar() //This results in a runtime error,
     *             //Foo has no such method.
     * 
     * //Now define a delegate
     * class Delegate
     * {
     *     public function _construct()
     *     {
     *         parent::_construct();
     *     }
     *
     *     public function _destruct()
     *     {
     *         parent::_destruct();
     *     }
     *     
     *     public function bar()
     *     {
     *         echo "bar";
     *     }
     * }
     *
     * foo->addDelegate(Delegate);  //add the delegate.
     * foo->bar();                  //This will be called as if it 
     *                              //were a member of Foo.
     * </code>
     * As many delegates as necessary can be added to an object.
     * 
     * You may wonder about the performance impact of this model. In actually,
     * there should be little extra overhead after the first call to a delegated
     * method. This is due to a caching scheme: When class names are specified
     * as the parameters to the addDelegate() method, they are intern added to
     * an associated array as keys that correspond with instances. Thus, every
     * delegate class name that is specified becomes an object. When methods
     * are called upon delegate owners, they check another associated array
     * that contains method names as keys and the proper delegates as objects.
     * If the key (method name) is cached in this manner, then the method is
     * immediatly evoked on the proper delegate. If it does not exist, then
     * each delegate is searched until one that can respond is found and this
     * relationship is cached, otherwise, a fatal error is produced. THus no 
     * matter how many delegates a class has, all calls after the first should
     * only have a small latency.
     *
     * To call the method, PEAR_DelegateOwner implements the __call(). When it
     * finds the correct delegate, it calls the method, transparently inserting
     * a reference to the owning object ($this) as the first argument, so that
     * the delegate can act on its owner as if it is itself. Thus, delegated
     * methods must be defined as follows:
     * <code>
     * accesslevel function function1($owner, ....);
     * </code>
     * Note, however, that the user of the method need only consider those
     * parameters that follow the first parameter.
     *
     * In truth, this mode of delegation is unorthodox. The traditional model
     * of delegation is that an object delegates selected methods, calling its
     * own version unless one delegate is present. This feature is too simple
     * to implement generically, as this can be achieved by having the delegated
     * method check to see whether there is a delegate and then call that method,
     * or continue executing its own implementation.
     *
     * @since PHP 5.0.0
     * @author Herr Witten <LingWitt@yahoo.com>
     * @see http://pear.php.net/manual/
     * @package PEAR_DelegateOwner
     */
    class PEAR_DelegateOwner extends PEAR
    {
        /**#@+
         * @access protected
         */
        /**
         * @var array An associative array with delegate classnames as keys
         * and objects as values.
         */
        var $_delegates     = array();
        /**
         * @var array An associative array with delegated methods as keys 
         * and delegate objects as values.
         */
        var $_method_map    = array();
        /**#@-*/
        
        /**
         * Adds delegates to the calling object.
         * 
         * This method takes a list of classnames or objects. If an argument is
         * a classname, then the method determines if it is defined. If it is,
         * the class is instantiated and the object is stored, otherwise a fatal
         * error is thrown.
         * @access public
         * @param mixed $delegate,... This specifies either a classname or an object.
         */
        public function addDelegate($delegate)
        {
            $args = array_values(func_get_args());
            
            foreach ($args as $delegate)
            {
                if (is_string($delegate) && class_exists($delegate))
                    $delegate = new $delegate;
                            
                $this->_delegates[get_class($delegate)] = $delegate;
            }
        }
        
        /**
         * Gets the associated array of delegate classes => delegates.
         * 
         * @return array The return value is the _delegates array.
         * @access public
         */
        public function &getDelegates()
        {
            return $this->_delegates;
        }
        
        /**
         * Gets the delegate objects that are instances of the specified class.
         * 
         * This method returns instances of the specified classname as well as 
         * child instances of the specified classname.
         * 
         * @see getDelegateExact()
         * @access public
         * @param string $classname,... This specifies a delegate classname.
         * @return array <pre>As there could be multiple parameters, the result is an 
         * associative array of the form:
         * Array
         * (
         *     [classname1] = Array
         *                    (
         *                        delegate11
         *                        delegate12
         *                        ...
         *                    )
         *     [classnamei] = Array
         *                    (
         *                        delegate1i
         *                        delegate1i
         *                        ...
         *                    )
         *     ...
         * )
         * </pre>
         */
        public function getDelegate($classname)
        {
            $args = func_get_args();
            
            foreach ($args as $arg)
            {
                foreach ($this->_delegates as $delegate)
                {
                    if ($delegate instanceof $arg)
                    {
                        $results[$arg] = $delegate;
                    }
                }
            }
            
            return $results;
        }
        
        /**
         * Gets the delegate object that is an instance of the specified class.
         * 
         * This method returns one instance of the specified classname. This does
         * not return child class instances of the specified classname.
         * 
         * @see getDelegate()
         * @access public
         * @param string $classname This specifies a delegate classname
         * @return object A reference to the delegate object of type classname.
         */
        public function getDelegateExact($classname)
        {
            $classname = strtolower($classname);
            
            if (array_key_exists($classname, $this->_delegates))
            {
                return $this->_delegates[$classname];
            }
            else
            {
                return null;
            }
            
        }
        
        /**
         * Determines whether or not the calling object adopts a particular delegate.
         *
         * This is a convenience function that really just tests the return value of
         * getDelegate().
         *
         * @see hasDelegateExact()
         * @uses getDelegate(), $PEAR_DelegateOwner::$_delegates
         * @access public
         * @param string $delegate This specifies a delegate classname or object. If
         *                         $delegate is a string, then it adheres to the tests
         *                         of getDelegate(). If $delegate is an object, the _delegates 
         *                         array is searched for the object. This last behavior is more 
         *                         indicative of the hasDelegateExact() method, but it would be 
         *                         inconvenient to require the Exact for this simple instruction.
         * @return boolean If the calling object has adopted the specifed class name
         */
        public function hasDelegate($delegate)
        {        
            if (is_string($delegate) && $this->getDelegate($delegate))
            {
                return true;
            }
            else
            {
                foreach ($this->_delegates as $delegateObject)
                {
                    if ($delegateObject === $delegate)
                        return true;
                }
            }
            
            return false;
        }
        
        /**
         * Determines whether or not the calling object adopts a particular delegate.
         *
         * This is a convenience function that really just tests the return value of
         * getDelegateExact().
         *
         * @see hasDelegate()
         * @uses getDelegateExact(), hasDelegate()
         * @access public
         * @param string $delegate This specifies a delegate classname or object. If
         *                         $delegate is a string, then it adheres to the tests 
         *                         of getDelegateExact(). If $delegate is an object, then 
         *                         it adheres to the tests of hasDelegate().
         * @return boolean If the calling object has adopted the specifed class name
         */
        public function hasDelegateExact($delegate)
        {        
            if (is_string($delegate) && $this->getDelegateExact($classname))
            {
                return true;
            }
            else
            {
                return $this->hasDelegate($delegate);
            }         
            
            return false;
        }
        
        /**
         * Removes all delegates.
         *
         * This completely cleans the calling object of any delegates.
         *
         * @access public
         */
        public function removeDelegates()
        {
            unset($this->_method_map);
            unset($this->_delegates);
            
            $this->_method_map  = array();
            $this->_delegates   = array();
        }
        
        /**
         * Removes the unwanted entries from _method_map.
         *
         * This method cleans the _method_map array when a call to removeDelegate*()
         * is made.
         *
         * @internal
         * @access protected
         * $var object $filterDelegate Specifies the delegate, whose information is
         *                             is to be removed.
         */
        protected function filterMethodMapWithDelegate($filterDelegate)
        {
            $result = array();
            
            $method_map_keys    = array_keys($this->_method_map);
            $method_map_values  = array_values($this->_method_map);
            
            for ($i = 0, $count = count($method_map_values); $i < $count; $i++)
            {
                $delegate = $method_map_values[$i];
                
                if ($delegate === $filterDelegate)
                {
                    continue;
                }
                
                $result[$method_map_keys[$i]] = $delegate;
            }
            
            return $result;
        }
	
       /**
         * Removes the specified delegate.
         *
         * Takes a list of delegate classnames and delegate objects and removes them
         * from the calling object.
         *
         * @access public
         * $var mixed $specifier,... Specifies the delegate, whose information is
         *                           is to be removed. If it is a string, then it
         *                           adheres to the tests of getDelegate(). If it
         *                           is an object, then it removes the exact object,]
         *                           which is indicative of the removeDelegateExact()
         *                           method, but it would be silly to require an Exact
         *                           for such a simple instruction. In fact, these two
         *                           statements are analogous:
         * <code>
         * //given that $delegate is an object
         * $owningObject->removeDelegate($delegate);
         * $owningObject->removeDelegateExact(get_class($delegate));
         * </code
         *                           In actuality, this method calls removeDelegateExact().
         * @see removeDelegateExact(), getDelegate()
         * @uses removeDelegateExact(), filterMethodMapWithDelegate()
         */
        public function removeDelegate($specifier)
        {
            $args = func_get_args();
            
            foreach ($args as $arg)
            {
                if (is_object($arg))
                {
                    $this->removeDelegateExact(get_class($arg));
                }
                else
                {
                    foreach ($this->_delegates as $delegate)
                    {
                        if ($delegate instanceof $arg)
                        {
                            unset($this->_delegates[get_class($delegate)]);
                            $this->_method_map = $this->filterMethodMapWithDelegate($delegate);
                        }
                    }
                }
            }
        }
        
        /**
         * Removes the specified delegate.
         *
         * Takes a list of delegate classnames and delegate objects and removes them
         * from the calling object.
         *
         * @access public
         * $var mixed $specifier,... Specifies the delegate, whose information is
         *                            is to be removed. It adheres to the tests of 
         *                            getDelegateExact().
         * @see removeDelegate()
         * @uses getDelegateExact(), filterMethodMapWithDelegate()
         */
        public function removeDelegateExact($specifier)
        {
            $args = func_get_args();
            
            foreach ($args as $arg)
            {
                if (is_object($arg))
                {
                    $this->removeDelegateExact(get_class($arg));
                }
                else if ($delegate = $this->getDelegateExact($arg))
                {
                    unset($this->_delegates[get_class($delegate)]);
                    $this->_method_map = $this->filterMethodMapWithDelegate($delegate);       
                }
            }
        }
        
        /**
         * Stores the relationship between method names and delegates.
         *
         * Takes a method name, searches for the delegate that can handle
         * it, and stores the relationship in the _method_map array. This
         * method is called when the __call() method reveives an unrecognized
         * method. This caching of methods speeds up delegation. If the method
         * cannot be handled by any of the adopted delegates, then an Exception
         * is thrown.
         *
         * @internal
         * @access public
         * $var string $method The method name that is to be cached.
         * @see removeDelegate()
         * @uses getDelegateExact(), filterMethodMapWithDelegate()
         */
        protected function cacheMethod($method)
        {
            foreach ($this->_delegates as $delegate)
            {
                if (method_exists($delegate, $method))
                {
                    $this->_method_map[$method] = $delegate;
                    return;
                }
            }
            
            throw new Exception("No Such Method: $method()");
        }
        
        /**
         * Processes unrecognized method signatures.
         *
         * This checks the _method_map array for a cached relationship
         * between the method and any delegate. If one exists, the method
         * is immediately called and the result returned. If it does not,
         * then it calls the cacheMethod() method to find and cache the
         * method, after which is calls the unrecognized method on the
         * proper delegate or kills the PHP with an error.
         *
         * @internal
         * @access public
         * $var string $method See the PHP documentation.
         * $var string $args See the PHP documentation
         * @uses cacheMethod()
         */
        public function __call($method, $args)
        {
            $method = strtolower($method);
	    
	    $args = array_merge(array($this), $args);
            
            if (!array_key_exists($method, $this->_method_map))
            {
                try {$this->cacheMethod($method);}
                    catch(Exception $exception)
                    {
                        die ("Fatal: PEAR_DelegateOwner: " . $exception->getMessage());
                    }
            }
            
            return call_user_func_array(array($this->_method_map[$method], $method), $args);
        }
    }
?>