<?php
/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
/**
* This file defines PEAR_Delegator, which provides delegation capabilities.
*
* PHP version 5
*
* @package PEAR_Delegator
* @author Michael Witten <lingwitt@yahoo.com>
* @copyright 2004-2005 Michael Witten
* @license http://www.php.net/license/3_0.txt PHP License 3.0
* @link http://pear.php.net/package/PEAR_Delegator
*/
// /* vim: set expandtab tabstop=4 shiftwidth=4: */
// +----------------------------------------------------------------------+
// | PHP Version 5 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1997-2004 The PHP Group |
// +----------------------------------------------------------------------+
// | This source file is subject to version 3.0 of the PHP license, |
// | that is bundled with this package in the file LICENSE, and is |
// | available through the world-wide-web at the following url: |
// | http://www.php.net/license/3_0.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: Michael Witten 4100 85 <LingWitt@yahoo.com> |
// | Martin Jansen <mj@php.net> for overwhelming encouragement |
// +----------------------------------------------------------------------+
// 4100 85
// $Id$
/**
* This includes the exceptions thrown.
*/
require_once 'Delegator/DelegatorExceptions.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 introduced. 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, however, 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_Delegator
* {
* 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. Note that the constructor and destructor
* //are unnecessary, as this delegate will be added statically.
*
* 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, and they can be
* either class objects or instance objects. Class objects have their methods
* called statically.
*
* You may wonder about the performance impact of this model. In actuality,
* there should be little extra overhead after the first call to a delegated
* method. This is due to a caching scheme: When methods are called upon a
* delegator, the delegator checks another associated array
* that contains method names as keys and the proper delegates as values.
* If the key (method name) is cached in this manner, then the method is
* immediatly invoked 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_Delegator implements the __call() method. 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 functionName($owner, ...);
* </code>
* Note, however, that the user of the method need only consider those
* parameters that follow the first parameter.
*
* One of the benefits of this scheme is the ability to have delegate hierarchies.
* That is, a delegator could have a delegate that is a delegator, and
* the PEAR_Delegator class recognizes this by viewing such delegates as subdelegates,
* treating such subdelegates as subclasses would be treated. This allows for such capabilities
* as pseudo-overriding:
* <code>
* //In our Foo class we could define a bar() method as follows:
* public function bar()
* {
* $args = func_get_args();
* $this->forwardMethod('bar', $args);
*
* echo 'foobar';
* }
*</code>
* Now, the delegate's implementation would be called as well. This of course means that
* you can also completely override the delegate method, and not even call it.
*
* 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, in fact, a
* subset of the scheme presented here. In otherwords, you can achieve the
* same effect by limiting the forwarding mechanism:
* <code>
* //In our Foo class, we could define a bar() method as follows:
* public function bar()
* {
* if ($this->hasDelegate()) {
* $args = func_get_args();
* return $this->forwardMethod('bar', $args);
* }
*
* echo 'foobar';
* }
*
* //Now, if you call:
* $foo->bar();
* //you get the functionality provided by the class,
* //but if you set the delegate:
* $foo->setDelegate(Delegate); //or instantiate the
* //delegate and set the object.
* //You can use its functionality instead.
* //
* </code>
*
* Some might also worry about the flexibility of errors. This is not a trouble
* at all. In fact, the error output of this class is more direct in many
* cases than PHP's own error output. It will give you the file and line of
* the error in user code and its messages are modeled after those of PHP.
*
* Terminology:
* <pre>
* * owner: a delegator.
* * subdelegate: A delegator that is a delegate.
* * native delegate: A delegate that is an immediate delegate, not a delegate
* of a delegate.
* * static (class) delegate: A delegate that is simply the class.
* * dynamic (object) delegate: A delegate that is an instantiated class.
* </pre>
*
* @since PHP 5.0.0
* @author Michael Witten <LingWitt@yahoo.com> 4100 85
* @see http://pear.php.net/manual/
* @package PEAR_Delegator
*/
class PEAR_Delegator
{
/**
* An associative array with delegate classnames as keys
* and objects as values.
* @var array
*/
public $_delegates = array();
/**
* An associative array with delegated methods as keys
* and delegate objects as values.
* @var array
*/
public $_method_map = array();
/**
* This is checked by the forwarding mechanism to find
* which object should be considered the calling object.
* @var string
*/
public $_forwardingMethod;
/**
* Constructs a delegator.
*/
public function __construct()
{
}
/**
* Destroys a delegator.
*
* When a delegator is destroyed, it automatically removes all of the delegates,
* so it is unnessary for user code to do so.
*/
public function __destruct()
{
$this->removeAllDelegates();
}
/**
* Add extensions to this class.
*
* This adds the PEAR_Delegator_Extensions delegate.
*/
public function addExtensions()
{
if (!class_exists(PEAR_Delegator_Extensions)) {
require_once 'Delegator/DelegatorExtensions.php';
$this->addDelegate(new PEAR_Delegator_Extensions);
}
}
/**
* 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 added as a static delegate, otherwise a fatal error
* is raised. If it is an object, it is stored as a delegate; thus,
* there are two types of delegates: static and dynamic delegates.
*
* @see PEAR_Delegator::setDelegate()
*
* @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)) {
if (!class_exists($delegate)) {
$exception = new PEAR_Delegator_ExceptionDelegateUndefined($delegate);
die('<b>Fatal error</b>: ' . $exception->getMessage());
}
$delegateClass = $delegate;
} else {
$delegateClass = get_class($delegate);
}
$this->_delegates[$delegateClass] = $delegate;
}
}
/**
* Sets the delegator's one delegate.
*
* This method takes a classname or an object and makes it the only delegate.
* In actuality, it removes all of the delegates and then adds the specified
* delegate. This is useful for using the delegation method for the traditional
* delegate model.
*
* @see PEAR_Delegator::addDelegate()
* @uses PEAR_Delegator::removeAllDelegates(), PEAR_Delegator::addDelegate()
* @param mixed $delegate This specifies either a classname or an object.
*/
public function setDelegate($delegate)
{
$this->removeAllDelegates();
if ($delegate) {
$this->addDelegate($delegate);
}
}
/**
* Gets the associated array of delegate classes => delegates.
*
* Note: Cloning may not work after this.
* @see PEAR_Delegator::getAllDelegates(), PEAR_Delegator::getDelegate(),
* PEAR_Delegator_Extensions::getDelegateExact(), PEAR_Delegator_Extensions::getDelegateRecursive(),
* PEAR_Delegator_Extensions::getDelegateRecursiveExact()
* @return array A reference to the _delegates array.
*/
public function &getAllDelegates()
{
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 classnames, including subdelegates,
* which are native to the caller. That is, if one of the delegates is
* a delegator and it contains a delegate of the specified type,
* it will be returned regardless of its own class type.
*
* @see PEAR_Delegator::getAllDelegates(), PEAR_Delegator::getDelegate(),
* PEAR_Delegator_Extensions::getDelegateExact(), PEAR_Delegator_Extensions::getDelegateRecursive(),
* PEAR_Delegator_Extensions::getDelegateRecursiveExact()
* @param class $classname1 This specifies a delegate classname. Any
* number of arguments after this is acceptable.
* @return array <pre>The result is an
* associative array of the form:
* Array
* (
* [classname1] = Array
* (
* delegate11
* delegate12
* ...
* )
* [classnamei] = Array
* (
* delegate1i
* delegate1i
* ...
* )
* ...
* )
* Note: If a single classname is passed, a traditional array with numbered
* elements is returned.
* </pre>
*/
public function getDelegate($classname1)
{
$args = func_get_args();
$result = null;
foreach ($args as $classname) {
foreach ($this->_delegates as $delegate) {
if (PEAR_Delegator::is_a($delegate, $classname)) {
$result[$classname][] = $delegate;
continue;
}
}
}
if ($result) {
return (count($args) > 1) ? $result : $result[$classname1];
} else {
return null;
}
}
/**
* Gets the native delegate objects that respond to a certain method.
*
* The method returns delegates native to the calling delegator, which can
* respond to the method in question, whether it be defined in the native delegate
* or in a delegate deeper in the hierarchy.
*
* @see PEAR_Delegator_Extensions::getDelegateForMethodRecursive(), PEAR_Delegator_Extensions::getDelegateForMethodRecursiveExact(),
* PEAR_Delegator_Extensions::getDelegateForMethodFirst(), PEAR_Delegator::method_exists()
* @uses PEAR_Delegator::method_exists()
* @param string $method1,... This specifies the method for whose responder is searched.
* @return array <pre>The result is an
* associative array of the form:
* Array
* (
* [method1] = Array
* (
* delegate11
* delegate12
* ...
* )
* [methodi] = Array
* (
* delegate1i
* delegate1i
* ...
* )
* ...
* )
* Note: If a single classname is passed, a traditional array with numbered
* elements is returned.
* </pre>
*/
public function getDelegateForMethod($method1)
{
$result = array();
$args = func_get_args();
foreach ($args as $method) {
foreach ($this->_delegates as $delegate) {
if (PEAR_Delegator::method_exists($delegate, $method)) {
$result[$method][] = $delegate;
}
}
}
if (count($result)) {
return (count($args) > 1) ? $result : $result[$method1];
}
else {
return null;
}
}
/**
* Determines whether or not the calling object adopts a particular delegate.
*
* This returns the availability of a delegate, including subdelegates.
*
* @see PEAR_Delegator_Extensions::hasDelegateExact(), PEAR_Delegator::is_a();
* @param mixed $specifier 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. If $specifier is null,
* then this returns whether or not the caller has any delegates.
* @return bool If the calling object has adopted the specifed class name
*/
public function hasDelegate($specifier = null)
{
if ($specifier == null) {
return (count($this->_delegates)) ? true : false;
}
elseif (is_string($specifier)) {
$specifier = $specifier;
if (array_key_exists($specifier, $this->_delegates)) {
return true;
}
foreach ($this->_delegates as $delegate) {
if (PEAR_Delegator::is_aExact($delegate, $specifier)) {
return true;
}
if (is_object($delegate) && ($delegate instanceof PEAR_Delegator && $delegate->hasDelegate($specifier))) {
return true;
}
}
} else {
foreach ($this->_delegates as $delegate) {
if ($delegate === $specifier) {
return true;
}
}
}
return false;
}
/**
* Removes all delegates.
*
* This completely cleans the calling object of any delegates.
*
* @see PEAR_Delegator::removeDelegate(), PEAR_Delegator_Extensions::removeDelegateRecursiveExact()
*/
public function removeAllDelegates()
{
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.
*
* @param object $filterDelegate Specifies the delegate instance, whose information is
* is to be removed.
* @return array The method map without the $filterdelegate
*/
public 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.
*
* @param 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 searches the for that
* delegate to remove.
* @see PEAR_Delegator::removeAllDelegates(), PEAR_Delegator_Extensions::removeDelegateRecursiveExact()
* @uses PEAR_Delegator::getDelegate(), PEAR_Delegator::filterMethodMapWithDelegate()
*/
public function removeDelegate($specifier)
{
$args = func_get_args();
$delegates = call_user_func_array(array($this, 'getDelegate'), $args);
foreach ($delegates as $delegateArray) {
foreach ($delegateArray as $delegate) {
unset($this->_delegates[is_string($delegate) ? $delegate : get_class($delegate)]);
$this->_method_map = $this->filterMethodMapWithDelegate($delegate);
}
}
}
/**
* Determines if a class or instance object is of the given type.
*
* This method is analogous to the is_a() method of PHP. However,
* it handles classes too.
*
* @see PEAR_Delegator::is_a()
* @param mixed $specifier Specifies the delegate with either
* a class or instantiated object.
* @param class $classname The classname type to check against.
* @return bool true if it is, false if it is not.
*/
public function is_aExact($specifier, $classname)
{
if (is_string($specifier)) {
if ((new $specifier) instanceof $classname) {
return true;
}
}
elseif ($specifier instanceof $classname) {
return true;
}
return false;
}
/**
* Determines if a class or instance object is of the given type.
*
* This method is an extension of the is_aExact() method. It also
* handles subdelegates, so it returns true if a delegator
* is passed in and has a delegate of type $classname, whether
* or not the delegator is of type $classname.
*
* @uses PEAR_Delegator::hasDelegate()
* @param mixed $specifier Specifies the delegate with either
* a class or instantiated object.
* @param class $classname The classname type to check against.
* @return bool true if it is, false if it is not.
*/
public function is_a($specifier, $classname)
{
if (PEAR_Delegator::is_aExact($specifier, $classname)) {
return true;
}
elseif (is_object($specifier) && (($specifier instanceof PEAR_Delegator) && $specifier->hasDelegate($classname))) {
return true;
}
return false;
}
/**
* Determines if a class or instance object responds to a method.
*
* This method is analogous to the method_exists() method of PHP. However,
* it handles classes too.
*
* @see PEAR_Delegator::method_exists()
* @param mixed $specifier Specifies the delegate with either
* a class or instantiated object.
* @param class $classname The method to look for.
* @return bool true if it is, false if it is not.
*/
public function method_existsExact($specifier, $method)
{
if (is_string($specifier)) {
if (method_exists(new $specifier, $method)) {
return true;
}
} elseif (method_exists($specifier, $method)) {
return true;
}
return false;
}
/**
* Determines if a class or instance object responds to a method.
*
* This method is an extension of the method_existsExact() method.
* It also handles subdelegates, so it returns true if a delegator
* is passed in and has a delegate that can implement $method,
* whether or not the delegator can implement the $method.
*
* @see PEAR_Delegator::method_existsExact()
* @param mixed $specifier Specifies the delegate with either
* a class or instantiated object.
* @param class $classname The method to look for.
* @return bool true if it is, false if it is not.
*/
public function method_exists($specifier, $method)
{
if (PEAR_Delegator::method_existsExact($specifier, $method)) {
return true;
} elseif (is_object($specifier) && ($specifier instanceof PEAR_Delegator)) {
foreach ($specifier->_delegates as $delegate) {
if (PEAR_Delegator::method_exists($delegate, $method)) {
return true;
}
}
}
return false;
}
/**
* Finds whether or not the object or one of its delegates implements a method
*
* Finds whether or not the calling object can perform the given method name.
* The calling object can perform the given method if it or one of its delegates
* can do so. This means that it also searches delegates that are themselves
* delegators.
*
* @see PEAR_Delegator::method_exists()
* @uses PEAR_Delegator::method_exists()
* @param string $method The method name that is to be searched for availability.
* @return bool true if it does, false if it does not.
*/
public function respondsToMethod($method)
{
return PEAR_Delegator::method_exists($this, $method);
}
/**
* 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.
*
* Note: This caches the first responding delegate.
*
* This is an internal method and should not be invoked.
*
* @param string $method The method name that is to be cached. This must
* be a lowercase string.
* @throws PEAR_Delegator_ExceptionNoDelegateForMethod If the method
*/
protected function cacheMethod($method)
{
$delegates = $this->getDelegateForMethod($method);
if ($delegates && ($delegate = $delegates[0])) {
$this->_method_map[$method] = $delegate;
return;
}
throw new PEAR_Delegator_ExceptionMethodUndefined($method);
}
/**
* This informs a delegator that is a delegate not to pass itself.
*
* This informs a delegator that is a delegate not to pass itself
* as the calling object if it needs to forward a method to a delegate.
* Thus, a chain of responders is established with the initial caller
* as the caller.
*
* This is an internal method and should not be invoked.
*
* @param string $method The method that is being forwarded. This must
* be a lowercase string.
*/
protected function setForwardingMethod($method)
{
$this->_forwardingMethod = $method;
}
/**
* This can be used by delegators for pseudo-method-overriding a method.
*
* This is the public interface to the __call() method, and it allows
* for a method to be forwarded to the delegation system, so that
* pseudo-method-overriding can occur.
*
* @see PEAR_Delegator::__call()
* @param string $method See the PHP documentation.
* @param string $args See the PHP documentation
*/
public function forwardMethod($method, $args)
{
return $this->__call($method, $args);
}
/**
* 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.
*
* This is an internal method and should not be invoked.
*
* @see PEAR_Delegator::forwardMethod(), PEAR_Delegator::cacheMethod()
* @param string $method See the PHP documentation.
* @param string $args See the PHP documentation.
* @return mixed the result of the called method.
* @uses PEAR_Delegator::cacheMethod()
*/
public function __call($method, $args)
{
//It is necessary to convert the string to lowercase since PHP doesn't
//differentiate between case when calling methods. This distills the
//situation to one case.
$method = strtolower($method);
if ($method != $this->_forwardingMethod) {
$args = array_merge(array($this), $args);
}
if (!array_key_exists($method, $this->_method_map)) {
try {
$this->cacheMethod($method);
} catch(PEAR_Delegator_ExceptionMethodUndefined $exception) {
die('<b>Fatal error</b>: ' . $exception->getMessage());
}
}
$delegate = $this->_method_map[$method];
if ($delegate instanceof PEAR_Delegator) {
$delegate->setForwardingMethod($method);
$result = call_user_func_array(array($delegate, $method), $args);
$delegate->setForwardingMethod(null);
return $result;
}
return call_user_func_array(array($delegate, $method), $args);
}
}
?>