PageRenderTime 67ms CodeModel.GetById 2ms app.highlight 56ms RepoModel.GetById 1ms app.codeStats 1ms

/htdocs/wp-content/plugins/nextgen-gallery/pope/lib/class.extensibleobject.php

https://github.com/Fishgate/privatecollectionswp
PHP | 1419 lines | 766 code | 205 blank | 448 comment | 106 complexity | d418e066caba0aa6c52413035f79eedc MD5 | raw file
   1<?php
   2define('__EXTOBJ_STATIC__', '__STATICALLY_CALLED__');
   3define('__EXTOBJ_NO_INIT__', '__NO_INIT__');
   4
   5if (!defined('EXTENSIBLE_OBJECT_ENFORCE_INTERFACES')) {
   6    define('EXTENSIBLE_OBJECT_ENFORCE_INTERFACES', TRUE);
   7}
   8
   9
  10/**
  11 * Provides helper methods for Pope objects
  12 */
  13class PopeHelpers
  14{
  15    /**
  16     * Merges two associative arrays
  17     * @param array $a1
  18     * @param array $a2
  19     * @return array
  20     */
  21    function array_merge_assoc($a1, $a2, $skip_empty=FALSE)
  22    {
  23		if ($a2) {
  24			foreach ($a2 as $key => $value) {
  25				if ($skip_empty && $value === '' OR is_null($value)) continue;
  26				if (isset($a1[$key])) {
  27
  28					if (is_array($value)) {
  29						$a1[$key] = $this->array_merge_assoc($a1[$key], $value);
  30
  31					}
  32					else {
  33						$a1[$key] = $value;
  34					}
  35
  36				}
  37				else $a1[$key] = $value;
  38			}
  39		}
  40		return $a1;
  41    }
  42
  43
  44    /**
  45     * Returns TRUE if a property is empty
  46     * @param string $var
  47     * @return boolean
  48     */
  49    function is_empty($var, $element=FALSE)
  50    {
  51       if (is_array($var) && $element) {
  52           if (isset($var[$element])) $var = $var[$element];
  53           else $var = FALSE;
  54       }
  55
  56       return (is_null($var) OR (is_string($var) AND strlen($var) == 0) OR $var === FALSE);
  57    }
  58}
  59
  60
  61/**
  62 * An ExtensibleObject can be extended at runtime with methods from another
  63 * class.
  64 *
  65 * - Mixins may be added or removed at any time during runtime
  66 * - The path to the mixin is cached so that subsequent method calls are
  67 *   faster
  68 * - Pre and post hooks can be added or removed at any time during runtime.
  69 * - Each method call has a list of associated properties that can be modified
  70 *   by pre/post hooks, such as: return_value, run_pre_hooks, run_post_hooks, etc
  71 * - Methods can be replaced by other methods at runtime
  72 * - Objects can implement interfaces, and are constrained to implement all
  73 *   methods as defined by the interface
  74 * - All methods are public. There's no added security by having private/protected
  75 *   members, as monkeypatching can always expose any method. Instead, protect
  76 *   your methods using obscurity. Conventionally, use an underscore to define
  77 *   a method that's private to an API
  78 */
  79class ExtensibleObject extends PopeHelpers
  80{
  81    const METHOD_PROPERTY_RUN='run';
  82    const METHOD_PROPERTY_RUN_POST_HOOKS='run_post_hooks';
  83    const METHOD_PROPERTY_RUN_PRE_HOOKS='run_pre_hooks';
  84    const METHOD_PROPERTY_RETURN_VALUE='return_value';
  85
  86    var  $_mixins = array();
  87    var  $_mixin_priorities = array();
  88    var  $_pre_hooks = array();
  89    var  $_global_pre_hooks = array();
  90    var  $_global_post_hooks= array();
  91    var  $_post_hooks = array();
  92    var  $_method_map_cache = array();
  93    var  $_interfaces = array();
  94    var  $_overrides = array();
  95    var  $_aliases = array();
  96    var  $_method_properties = array();
  97    var  $_throw_error = TRUE;
  98    var  $_wrapped_instance = FALSE;
  99	var	 $object = NULL;
 100	var  $_disabled_pre_hooks = array();
 101	var  $_disabled_post_hooks = array();
 102	var  $_disabled_mixins = array();
 103
 104
 105    /**
 106     * Defines a new ExtensibleObject. Any subclass should call this constructor.
 107     * Subclasses are expected to provide the following:
 108     * define_instance() - adds extensions which provide instance methods
 109     * define_class() - adds extensions which provide static methods
 110     * initialize() - used to initialize the state of the object
 111     */
 112    function __construct()
 113    {
 114		// Mixins access their parent class by accessing $this->object.
 115		// Sometimes users mistakenly use $this->object within the parent object
 116		// itself. As it's becoming a common mistake, we define a $this->object
 117		// property which points to the current instance (itself)
 118		$this->object = $this;
 119
 120        $args = func_get_args();
 121        $define_instance = TRUE;
 122        $init_instance = TRUE;
 123
 124        // The first argument could be a flag to ExtensibleObject
 125        // which indicates that only static-like methods will be called
 126        if (count($args) >= 1) {
 127            $first_arg = $args[0];
 128            if (is_string($first_arg)) {
 129            	switch ($first_arg) {
 130            		case __EXTOBJ_STATIC__:
 131            		{
 132		              $define_instance = FALSE;
 133		              $init_instance = FALSE;
 134
 135		              if (method_exists($this, 'define_class')) {
 136						  $this->call_callback($this, 'define_class', $args);
 137		              }
 138		              elseif (method_exists($this, 'define_static')) {
 139						  $this->call_callback($this, 'define_static', $args);
 140		              }
 141
 142					  break;
 143            		}
 144            		case __EXTOBJ_NO_INIT__:
 145            		{
 146		              $init_instance = FALSE;
 147
 148            			break;
 149            		}
 150            	}
 151            }
 152        }
 153
 154        // Are we to define instance methods?
 155        if ($define_instance)
 156        {
 157            if (method_exists($this, 'define_instance'))
 158            {
 159                $reflection = new ReflectionMethod($this, 'define_instance');
 160                $reflection->invokeArgs($this, $args);
 161                // call_user_func_array(array($this, 'define_instance'), $args);
 162            }
 163            elseif (method_exists($this, 'define')) {
 164                $reflection = new ReflectionMethod($this, 'define');
 165                $reflection->invokeArgs($this, $args);
 166                // call_user_func_array(array($this, 'define'), $args);
 167            }
 168
 169            if (EXTENSIBLE_OBJECT_ENFORCE_INTERFACES) $this->_enforce_interface_contracts();
 170
 171			if ($init_instance)
 172            {
 173                // Initialize the state of the object
 174                if (method_exists($this, 'initialize')) {
 175                    $reflection = new ReflectionMethod($this, 'initialize');
 176                    $reflection->invokeArgs($this, $args);
 177                    // call_user_func_array(array($this, 'initialize'), $args);
 178                }
 179            }
 180        }
 181    }
 182
 183	/**
 184	 * Disabled prehooks for a particular method
 185	 * @param string $method
 186	 */
 187	function disable_pre_hooks($method)
 188	{
 189		$this->_disabled_pre_hooks[] = $method;
 190		return $this;
 191	}
 192
 193
 194	/**
 195	 * Enable prehooks for a particular method
 196	 * @param string $method
 197	 */
 198	function enable_pre_hooks($method)
 199	{
 200		$index = array_search($method, $this->_disabled_pre_hooks);
 201		if ($index !== FALSE) {
 202			unset($this->_disabled_pre_hooks[$index]);
 203		}
 204		return $this;
 205	}
 206
 207	/**
 208	 * Disabled posthooks for a particular method
 209	 * @param string $method
 210	 */
 211	function disable_post_hooks($method)
 212	{
 213		$this->_disabled_post_hooks[] = $method;
 214		return $this;
 215	}
 216
 217
 218	/**
 219	 * Enable post-hooks for a particular method
 220	 * @param string $method
 221	 */
 222	function enable_post_hooks($method)
 223	{
 224		$index = array_search($method, $this->_disabled_post_hooks);
 225		if ($index !== FALSE) {
 226			unset($this->_disabled_post_hooks[$index]);
 227		}
 228		return $this;
 229	}
 230
 231	/**
 232	 * Determines if post hooks are enabled for a particular method
 233	 * @param string $method
 234	 * @return bool
 235	 */
 236	function are_post_hooks_enabled($method)
 237	{
 238		return !empty($this->_post_hooks) && (!in_array($method, $this->_disabled_post_hooks));
 239	}
 240
 241
 242	/**
 243	 * Determines if pre hooks are enabled for a particular method
 244	 * @param string $method
 245	 * @return bool
 246	 */
 247	function are_pre_hooks_enabled($method)
 248	{
 249		return !empty($this->_pre_hooks) && (!in_array($method, $this->_disabled_pre_hooks));
 250	}
 251
 252
 253    /**
 254     * Adds an extension class to the object. The extension provides
 255     * methods for this class to expose as it's own
 256     * @param string $class
 257     */
 258    function add_mixin($class, $instantiate=FALSE)
 259    {
 260		$retval = TRUE;
 261
 262		if (!$this->has_mixin($class)) {
 263			// We used to instantiate the class, but I figure
 264			// we might as well wait till the method is called to
 265			// save memory. Instead, the _call() method calls the
 266			// _instantiate_mixin() method below.
 267			$this->_mixins[$class] = FALSE; // new $class();
 268			array_unshift($this->_mixin_priorities, $class);
 269			$this->_flush_cache();
 270
 271			// Should we instantiate the object now?
 272			if ($instantiate) $this->_instantiate_mixin($class);
 273		}
 274		else $retval = FALSE;
 275
 276		return $retval;
 277    }
 278
 279
 280	/**
 281	 * Determines if a mixin has been added to this class
 282	 * @param string $klass
 283	 * @return bool
 284	 */
 285	function has_mixin($klass)
 286	{
 287		return (isset($this->_mixins[$klass]));
 288	}
 289
 290
 291    /**
 292     * Stores the instantiated class
 293     * @param string $class
 294     * @return mixed
 295     */
 296    function _instantiate_mixin($class)
 297    {
 298        $retval = FALSE;
 299        if ($this->_mixins[$class])
 300            $retval = $this->_mixins[$class];
 301        else {
 302            $obj= new $class();
 303            $obj->object = &$this;
 304            $retval = $this->_mixins[$class] = &$obj;
 305            if (method_exists($obj, 'initialize')) $obj->initialize();
 306        }
 307
 308
 309        return $retval;
 310    }
 311
 312
 313    /**
 314     * Deletes an extension from the object. The methods provided by that
 315     * extension are no longer available for the object
 316     * @param string $class
 317     */
 318    function del_mixin($class)
 319    {
 320        unset($this->_mixins[$class]);
 321        $index = array_search($class, $this->_mixin_priorities);
 322        if ($index !== FALSE) {
 323            unset($this->_mixin_priorities[$index]);
 324			foreach ($this->_disabled_mixins as $method => $disabled_mixins) {
 325				$index = array_search($class, $disabled_mixins);
 326				if (is_int($index)) unset($this->_disabled_mixins[$method][$index]);
 327			}
 328            $this->_flush_cache();
 329        }
 330
 331    }
 332
 333
 334    function remove_mixin($class)
 335    {
 336        $this->del_mixin($class);
 337    }
 338
 339
 340    /**
 341     * Replaces an extension methods with that of another class.
 342     * @param string $method
 343     * @param string $class
 344     * @param string $new_method
 345     */
 346    function replace_method($method, $class, $new_method=FALSE)
 347    {
 348        if (!$new_method) $new_method = $method;
 349        $this->_overrides[$method] = $class;
 350        $this->add_pre_hook($method, "replacement_{$method}_{$class}_{$new_method}", $class, $new_method);
 351        $this->_flush_cache();
 352
 353    }
 354
 355
 356    /**
 357     * Restores a method that was replaced by a former call to replace_method()
 358     * @param string $method
 359     */
 360    function restore_method($method)
 361    {
 362        $class = $this->_overrides[$method];
 363        unset($this->_overrides[$method]);
 364        $this->del_pre_hook($method, $class);
 365        $this->_flush_cache();
 366    }
 367
 368
 369	/**
 370	 * Returns the Mixin which provides the specified method
 371	 * @param string $method
 372	 */
 373	function get_mixin_providing($method, $return_obj=FALSE)
 374	{
 375		$retval = FALSE;
 376
 377		// If it's cached, then we've got it easy
 378		if ($this->is_cached($method)) {
 379
 380			$object = $this->_method_map_cache[$method];
 381			$retval = get_class($object);
 382		}
 383
 384		// Otherwise, we have to look it up
 385		else {
 386            foreach ($this->get_mixin_priorities($method) as $klass) {
 387                $object = $this->_instantiate_mixin($klass);
 388                if (method_exists($object, $method)) {
 389                    $retval = $return_obj ? $object : get_class($object);
 390                    $this->_cache_method($object, $method);
 391                    break;
 392                }
 393            }
 394		}
 395
 396		return $retval;
 397	}
 398
 399
 400    /**
 401     * When an ExtensibleObject is instantiated, it checks whether all
 402     * the registered extensions combined provide the implementation as required
 403     * by the interfaces registered for this object
 404     */
 405    function _enforce_interface_contracts()
 406    {
 407        $errors = array();
 408
 409        foreach ($this->_interfaces as $i) {
 410            $r = new ReflectionClass($i);
 411            foreach ($r->getMethods() as $m) {
 412                if (!$this->has_method($m->name)) {
 413					$klass = $this->get_class_name($this);
 414                    $errors[] = "`{$klass}` does not implement `{$m->name}` as required by `{$i}`";
 415                }
 416            }
 417        }
 418
 419        if ($errors) throw new Exception(implode(". ", $errors));
 420    }
 421
 422
 423    /**
 424     * Implement a defined interface. Does the same as the 'implements' keyword
 425     * for PHP, except this method takes into account extensions
 426     * @param string $interface
 427     */
 428    function implement($interface)
 429    {
 430        $this->_interfaces[] = $interface;
 431    }
 432
 433
 434    /**
 435     * Adds a hook that gets executed before every method call
 436     * @param string $name
 437     * @param string $class
 438     * @param string $hook_method
 439     */
 440    function add_global_pre_hook($name, $class, $hook_method)
 441    {
 442        $this->add_pre_hook('*', $name, $class, $hook_method);
 443    }
 444
 445    /**
 446     * Adds a hook that gets executed after every method call
 447     *
 448     * @param string $name
 449     * @param string $class
 450     * @param string $hook_method
 451     */
 452    function add_global_post_hook($name, $class, $hook_method)
 453    {
 454        $this->add_pre_hook('*', $name, $class, $hook_method);
 455    }
 456
 457
 458    /**
 459     * Adds a hook that will get executed before a particular method call
 460     * @param string $method
 461     * @param string $name
 462     * @param string $class
 463     * @param string $hook_method
 464     */
 465    function add_pre_hook($method, $name, $class, $hook_method=FALSE)
 466    {
 467        if (!$hook_method) $hook_method = $method;
 468
 469        // Is this a global pre hook?
 470        if ($method == '*') {
 471            $this->_global_pre_hooks[$name] = array(
 472                new $class,
 473                $hook_method
 474            );
 475        }
 476
 477        // This is a method-specific pre hook
 478        else {
 479            if (!isset($this->_pre_hooks[$method])) {
 480                $this->_pre_hooks[$method] = array();
 481            }
 482
 483            $this->_pre_hooks[$method][$name] = array(
 484                new $class,
 485                $hook_method
 486            );
 487        }
 488    }
 489
 490
 491    /**
 492     * Adds a hook to be called after a particular method call
 493     * @param string $method
 494     * @param string $hook_name
 495     * @param string $class
 496     * @param string $hook_method
 497     */
 498    function add_post_hook($method, $hook_name, $class, $hook_method=FALSE)
 499    {
 500        // Is this a global post hook?
 501        if ($method == '*') {
 502            $this->_post_hooks[$hook_name] = array(
 503              new $class,
 504                $hook_method
 505            );
 506        }
 507
 508        // This is a method-specific post hook
 509        else {
 510            if (!$hook_method) $hook_method = $method;
 511
 512            if (!isset($this->_post_hooks[$method])) {
 513                $this->_post_hooks[$method] = array();
 514            }
 515
 516            $this->_post_hooks[$method][$hook_name] = array(
 517                new $class,
 518                $hook_method
 519            );
 520        }
 521    }
 522
 523
 524    /**
 525     * Deletes a hook that's executed before the specified method
 526     * @param string $method
 527     * @param string $name
 528     */
 529    function del_pre_hook($method, $name)
 530    {
 531
 532        unset($this->_pre_hooks[$method][$name]);
 533    }
 534
 535    /**
 536     * Deletes all pre hooks registered
 537    **/
 538    function del_pre_hooks($method=FALSE)
 539    {
 540        if (!$method)
 541            $this->_pre_hooks = array();
 542        else
 543            unset($this->_pre_hooks[$method]);
 544    }
 545
 546
 547    /**
 548     * Deletes a hook that's executed after the specified method
 549     * @param string $method
 550     * @param string $name
 551     */
 552    function del_post_hook($method, $name)
 553    {
 554        unset($this->_post_hooks[$method][$name]);
 555    }
 556
 557    /**
 558     * Deletes all post hooks
 559     */
 560    function del_post_hooks($method=FALSE)
 561    {
 562        if (!$method)
 563            $this->_post_hooks = array();
 564        else
 565            unset($this->_post_hooks[$method]);
 566    }
 567
 568
 569    /**
 570     * Wraps a class within an ExtensibleObject class.
 571     * @param string $klass
 572     * @param array callback, used to tell ExtensibleObject how to instantiate
 573     * the wrapped class
 574     */
 575    function wrap($klass, $callback=FALSE, $args=array())
 576    {
 577        if ($callback) {
 578            $this->_wrapped_instance = call_user_func($callback, $args);
 579        }
 580        else {
 581            $this->_wrapped_instance = new $klass();
 582        }
 583    }
 584
 585
 586    /**
 587     * Determines if the ExtensibleObject is a wrapper for an existing class
 588     */
 589    function is_wrapper()
 590    {
 591        return $this->_wrapped_instance ? TRUE : FALSE;
 592    }
 593
 594
 595    /**
 596     * Returns the name of the class which this ExtensibleObject wraps
 597     * @return object
 598     */
 599    function &get_wrapped_instance()
 600    {
 601        return $this->_wrapped_instance;
 602    }
 603
 604
 605    /**
 606     * Returns TRUE if the wrapped class provides the specified method
 607     */
 608    function wrapped_class_provides($method)
 609    {
 610        $retval = FALSE;
 611
 612        // Determine if the wrapped class is another ExtensibleObject
 613        if (method_exists($this->_wrapped_instance, 'has_method')) {
 614			$retval = $this->_wrapped_instance->has_method($method);
 615        }
 616        elseif (method_exists($this->_wrapped_instance, $method)){
 617            $retval = TRUE;
 618        }
 619
 620        return $retval;
 621    }
 622
 623
 624    /**
 625     * Provides a means of calling static methods, provided by extensions
 626     * @param string $method
 627     * @return mixed
 628     */
 629    static function get_class()
 630    {
 631		// Note: this function is static so $this is not defined
 632        $klass = self::get_class_name();
 633        $obj = new $klass(__EXTOBJ_STATIC__);
 634        return $obj;
 635    }
 636
 637
 638	/**
 639	 * Gets the name of the ExtensibleObject
 640	 * @return string
 641	 */
 642	static function get_class_name($obj = null)
 643	{
 644		if ($obj)
 645			return get_class($obj);
 646		elseif (function_exists('get_called_class'))
 647			return get_called_class();
 648		else
 649			return get_class();
 650	}
 651
 652	/**
 653     * Gets a property from a wrapped object
 654     * @param string $property
 655     * @return mixed
 656     */
 657    function &__get($property)
 658    {
 659		$retval = NULL;
 660        if ($this->is_wrapper()) {
 661			try {
 662				$reflected_prop = new ReflectionProperty($this->_wrapped_instance, $property);
 663
 664				// setAccessible method is only available for PHP 5.3 and above
 665				if (method_exists($reflected_prop, 'setAccessible')) {
 666					$reflected_prop->setAccessible(TRUE);
 667				}
 668
 669				$retval = $reflected_prop->getValue($this->_wrapped_instance);
 670			}
 671			catch (ReflectionException $ex)
 672			{
 673				$retval = $this->_wrapped_instance->$property;
 674			}
 675        }
 676
 677		return $retval;
 678    }
 679
 680	/**
 681	 * Determines if a property (dynamic or not) exists for the object
 682	 * @param string $property
 683	 * @return boolean
 684	 */
 685	function __isset($property)
 686	{
 687		$retval = FALSE;
 688
 689		if (property_exists($this, $property)) {
 690			$retval = isset($this->$property);
 691		}
 692		elseif ($this->is_wrapper() && property_exists($this->_wrapped_instance, $property)) {
 693			$retval = isset($this->$property);
 694		}
 695
 696		return $retval;
 697	}
 698
 699
 700    /**
 701     * Sets a property on a wrapped object
 702     * @param string $property
 703     * @param mixed $value
 704     * @return mixed
 705     */
 706    function &__set($property, $value)
 707    {
 708		$retval = NULL;
 709        if ($this->is_wrapper()) {
 710			try {
 711				$reflected_prop = new ReflectionProperty($this->_wrapped_instance, $property);
 712
 713				// The property must be accessible, but this is only available
 714				// on PHP 5.3 and above
 715				if (method_exists($reflected_prop, 'setAccessible')) {
 716					$reflected_prop->setAccessible(TRUE);
 717				}
 718
 719				$retval = &$reflected_prop->setValue($this->_wrapped_instance, $value);
 720			}
 721
 722			// Sometimes reflection can fail. In that case, we need
 723			// some ingenuity as a failback
 724			catch (ReflectionException $ex) {
 725				$this->_wrapped_instance->$property = $value;
 726				$retval = &$this->_wrapped_instance->$property;
 727			}
 728
 729        }
 730		else {
 731			$this->$property = $value;
 732			$retval = &$this->$property;
 733		}
 734        return $retval;
 735    }
 736
 737
 738    /**
 739     * Finds a method defined by an extension and calls it. However, execution
 740     * is a little more in-depth:
 741     * 1) Execute all global pre-hooks and any pre-hooks specific to the requested
 742     *    method. Each method call has instance properties that can be set by
 743     *    other hooks to modify the execution. For example, a pre hook can
 744     *    change the 'run_pre_hooks' property to be false, which will ensure that
 745     *    all other pre hooks will NOT be executed.
 746     * 2) Runs the method. Checks whether the path to the method has been cached
 747     * 3) Execute all global post-hooks and any post-hooks specific to the
 748     *    requested method. Post hooks can access method properties as well. A
 749     *    common usecase is to return the value of a post hook instead of the
 750     *    actual method call. To do this, set the 'return_value' property.
 751     * @param string $method
 752     * @param array $args
 753     * @return mixed
 754     */
 755    function __call($method, $args)
 756    {
 757        $this->reset_method_properties($method, $args);
 758
 759        // Run pre hooks?
 760        if ($this->are_pre_hooks_enabled($method) && $this->get_method_property($method, self::METHOD_PROPERTY_RUN_PRE_HOOKS)) {
 761
 762            // Combine global and method-specific pre hooks
 763            $prehooks = $this->_global_pre_hooks;
 764            if (isset($this->_pre_hooks[$method])) {
 765                $prehooks = array_merge($prehooks, $this->_pre_hooks[$method]);
 766            }
 767
 768            // Apply each hook
 769            foreach ($prehooks as $hook_name => $hook) {
 770				$method_args = $this->get_method_property($method, 'arguments', $args);
 771                $this->_run_prehook(
 772					$hook_name,
 773					$method,
 774					$hook[0],
 775					$hook[1],
 776					$method_args
 777				);
 778            }
 779        }
 780
 781        // Are we to run the actual method? A pre hook might have told us
 782        // not to
 783        if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN) && !isset($this->_overrides[$method]))
 784        {
 785            if (($this->get_mixin_providing($method))) {
 786                $this->set_method_property(
 787                    $method,
 788                    self::METHOD_PROPERTY_RETURN_VALUE,
 789                    $this->_exec_cached_method($method, $this->get_method_property($method, 'arguments'))
 790                );
 791            }
 792
 793            // This is NOT a wrapped class, and no extensions provide the method
 794            else {
 795                // Perhaps this is a wrapper and the wrapped object
 796                // provides this method
 797                if ($this->is_wrapper() && $this->wrapped_class_provides($method))
 798                {
 799                    $object = $this->add_wrapped_instance_method($method);
 800                    $this->set_method_property(
 801                        $method,
 802                        self::METHOD_PROPERTY_RETURN_VALUE,
 803                        call_user_func_array(
 804                            array(&$object, $method),
 805                            $this->get_method_property($method, 'arguments')
 806                        )
 807                    );
 808                }
 809                elseif ($this->_throw_error) {
 810                    throw new Exception("`{$method}` not defined for " . get_class());
 811                }
 812                else {
 813                    return FALSE;
 814                }
 815            }
 816        }
 817
 818        // Are we to run post hooks? A pre hook might have told us not to
 819        if ($this->are_post_hooks_enabled($method) && $this->get_method_property($method, self::METHOD_PROPERTY_RUN_POST_HOOKS)) {
 820
 821            // Combine global and method-specific post hooks
 822            $posthooks = $this->_global_post_hooks;
 823            if (isset($this->_post_hooks[$method])) {
 824                $posthooks = array_merge($posthooks, $this->_post_hooks[$method]);
 825            }
 826
 827            // Apply each hook
 828            foreach ($posthooks as $hook_name => $hook) {
 829				$method_args = $this->get_method_property($method, 'arguments', $args);
 830                $this->_run_post_hook(
 831					$hook_name,
 832					$method,
 833					$hook[0],
 834					$hook[1],
 835					$method_args
 836				);
 837            }
 838        }
 839
 840		// Get return value, clear all method properties, and then return
 841        $retval = $this->get_method_property($method, self::METHOD_PROPERTY_RETURN_VALUE);
 842		$this->remove_method_properties($method);
 843		return $retval;
 844    }
 845
 846
 847	/**
 848	 * Adds the implementation of a wrapped instance method to the ExtensibleObject
 849	 * @param string $method
 850	 * @return Mixin
 851	 */
 852	function add_wrapped_instance_method($method)
 853	{
 854		$retval = $this->get_wrapped_instance();
 855
 856		// If the wrapped instance is an ExtensibleObject, then we don't need
 857		// to use reflection
 858		if (!is_subclass_of($this->get_wrapped_instance(), 'ExtensibleObject')) {
 859			$func	= new ReflectionMethod($this->get_wrapped_instance(), $method);
 860
 861			// Get the entire method definition
 862			$filename = $func->getFileName();
 863			$start_line = $func->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block
 864			$end_line = $func->getEndLine();
 865			$length = $end_line - $start_line;
 866			$source = file($filename);
 867			$body = implode("", array_slice($source, $start_line, $length));
 868            $body = preg_replace("/^\s{0,}private|protected\s{0,}/", '', $body);
 869
 870			// Change the context
 871			$body = str_replace('$this', '$this->object', $body);
 872			$body = str_replace('$this->object->object', '$this->object', $body);
 873			$body = str_replace('$this->object->$', '$this->object->', $body);
 874
 875			// Define method for mixin
 876			$wrapped_klass = get_class($this->get_wrapped_instance());
 877			$mixin_klass = "Mixin_AutoGen_{$wrapped_klass}_{$method}";
 878			if (!class_exists($mixin_klass)) {
 879				eval("class {$mixin_klass} extends Mixin{
 880					{$body}
 881				}");
 882			}
 883			$this->add_mixin($mixin_klass);
 884			$retval = $this->_instantiate_mixin($mixin_klass);
 885			$this->_cache_method($retval, $method);
 886
 887		}
 888
 889		return $retval;
 890	}
 891
 892
 893    /**
 894     * Provides an alternative way to call methods
 895     */
 896    function call_method($method, $args=array())
 897    {
 898        if (method_exists($this, $method))
 899        {
 900            $reflection = new ReflectionMethod($this, $method);
 901            return $reflection->invokeArgs($this, array($args));
 902        }
 903        else {
 904            return $this->__call($method, $args);
 905        }
 906    }
 907
 908
 909    /**
 910     * Returns TRUE if the method in particular has been cached
 911     * @param string $method
 912     * @return type
 913     */
 914    function is_cached($method)
 915    {
 916        return isset($this->_method_map_cache[$method]);
 917    }
 918
 919
 920    /**
 921     * Caches the path to the extension which provides a particular method
 922     * @param string $object
 923     * @param string $method
 924     */
 925    function _cache_method($object, $method)
 926    {
 927        $this->_method_map_cache[$method] = $object;
 928    }
 929
 930
 931	/**
 932	 * Gets a list of mixins by their priority, excluding disabled mixins
 933	 * @param string $method
 934	 * @return array
 935	 */
 936	function get_mixin_priorities($method)
 937	{
 938		$retval = array();
 939		foreach ($this->_mixin_priorities as $mixin) {
 940			if ($this->is_mixin_disabled($method, $mixin))
 941                continue;
 942			$retval[] = $mixin;
 943		}
 944		return $retval;
 945	}
 946
 947
 948	/**
 949	 * Determines if a mixin is disabled for a particular method
 950	 * @param string $method
 951	 * @param string $mixin
 952	 * @return boolean
 953	 */
 954	function is_mixin_disabled($method, $mixin)
 955	{
 956		$retval = FALSE;
 957		if (isset($this->_disabled_mixins[$method]))
 958			if (in_array($mixin, $this->_disabled_mixins[$method]) !== FALSE)
 959				$retval = TRUE;
 960		return $retval;
 961	}
 962
 963
 964    /**
 965     * Flushes the method cache
 966     */
 967    function _flush_cache()
 968    {
 969        $this->_method_map_cache = array();
 970    }
 971
 972
 973    /**
 974     * Returns TRUE if the object provides the particular method
 975     * @param string $method
 976     * @return boolean
 977     */
 978    function has_method($method)
 979    {
 980        $retval = FALSE;
 981
 982        // Have we looked up this method before successfully?
 983        if ($this->is_cached($method)) {
 984            $retval = TRUE;
 985        }
 986
 987        // Is this a local PHP method?
 988        elseif (method_exists($this, $method)) {
 989            $retval = TRUE;
 990        }
 991
 992        // Is a mixin providing this method
 993        elseif ($this->get_mixin_providing($method)) {
 994            $retval = TRUE;
 995        }
 996
 997        elseif ($this->is_wrapper() && $this->wrapped_class_provides($method)) {
 998            $retval = TRUE;
 999        }
1000
1001        return $retval;
1002    }
1003
1004
1005    /**
1006     * Runs a particular pre hook for the specified method. The return value
1007     * is assigned to the "[hook_name]_prehook_retval" method property
1008     * @param string $hook_name
1009     * @param string $method_called
1010     * @param Ext $object
1011     * @param string $hook_method
1012     *
1013     */
1014    function _run_prehook($hook_name, $method_called, $object, $hook_method, &$args)
1015    {
1016        $object->object = &$this;
1017        $object->method_called = $method_called;
1018
1019        // Are we STILL to execute pre hooks? A pre-executed hook might have changed this
1020        if ($this->get_method_property($method_called, 'run_pre_hooks'))
1021        {
1022            $reflection = new ReflectionMethod($object, $hook_method);
1023            $this->set_method_property(
1024                $method_called,
1025                $hook_name . '_prehook_retval',
1026                $reflection->invokeArgs($object, $args)
1027            );
1028        }
1029    }
1030
1031    /**
1032     * Runs the specified post hook for the specified method
1033     * @param string $hook_name
1034     * @param string $method_called
1035     * @param Ext $object
1036     * @param string $hook_method
1037     */
1038    function _run_post_hook($hook_name, $method_called, $object, $hook_method, &$args)
1039    {
1040        $object->object = &$this;
1041        $object->method_called = $method_called;
1042
1043        // Are we STILL to execute post hooks? A post-executed hook might have changed this
1044        if ($this->get_method_property($method_called, 'run_post_hooks'))
1045        {
1046            $reflection = new ReflectionMethod($object, $hook_method);
1047            $this->set_method_property(
1048                $method_called,
1049                $hook_name . '_post_hook_retval',
1050                $reflection->invokeArgs($object, $args)
1051            );
1052        }
1053    }
1054
1055    /**
1056     * Returns TRUE if a pre-hook has been registered for the specified method
1057     * @param string $method
1058     * @return boolean
1059     */
1060    function have_prehook_for($method, $name = null)
1061    {
1062        if (is_null($name)) {
1063            return isset($this->_pre_hooks[$method]);
1064        } else {
1065            return isset($this->_pre_hooks[$method][$name]);
1066        }
1067
1068    }
1069
1070
1071    /**
1072     * Returns TRUE if a posthook has been registered for the specified method
1073     * @param string $method
1074     * @return boolean
1075     */
1076    function have_posthook_for($method, $name = null)
1077    {
1078        $retval = FALSE;
1079
1080        if (isset($this->_post_hooks[$method])) {
1081            if (!$name) $retval = TRUE;
1082            else $retval = isset($this->_post_hooks[$method][$name]);
1083        }
1084
1085        return $retval;
1086    }
1087
1088	/**
1089	 * Disables a mixin for a particular method. This ensures that even though
1090	 * mixin provides a particular method, it won't be used to provide the
1091	 * implementation
1092	 * @param string $method
1093	 * @param string $klass
1094	 */
1095	function disable_mixin($method, $klass)
1096	{
1097		unset($this->_method_map_cache[$method]);
1098		if (!isset($this->_disabled_mixins[$method])) {
1099			$this->_disabled_mixins[$method] = array();
1100		}
1101		$this->_disabled_mixins[$method][] = $klass;
1102	}
1103
1104
1105	/**
1106	 * Enable a mixin for a particular method, that was previously disabled
1107	 * @param string $method
1108	 * @param string $klass
1109	 */
1110	function enable_mixin($method, $klass)
1111	{
1112		unset($this->_method_map_cache[$method]);
1113		if (isset($this->_disabled_mixins[$method])) {
1114			$index = array_search($klass, $this->_disabled_mixins[$method]);
1115			if ($index !== FALSE) unset($this->_disabled_mixins[$method][$index]);
1116		}
1117	}
1118
1119
1120	/**
1121	 * Gets a list of mixins that are currently disabled for a particular method
1122	 * @see disable_mixin()
1123	 * @param string $method
1124	 * @return array
1125	 */
1126	function get_disabled_mixins_for($method)
1127	{
1128		$retval = array();
1129		if (isset($this->_disabled_mixins[$method])) {
1130			$retval =  $this->_disabled_mixins[$method];
1131		}
1132		return $retval;
1133	}
1134
1135
1136    /**
1137     * Executes a cached method
1138     * @param string $method
1139     * @param array $args
1140     * @return mixed
1141     */
1142    function _exec_cached_method($method, $args=array())
1143    {
1144        $object = $this->_method_map_cache[$method];
1145        $object->object = &$this;
1146        $reflection = new ReflectionMethod($object, $method);
1147        return $reflection->invokeArgs($object, $args);
1148    }
1149
1150
1151    /**
1152     * Sets the value of a method property
1153     * @param string $method
1154     * @param string $property
1155     * @param mixed $value
1156     */
1157    function set_method_property($method, $property, $value)
1158    {
1159        if (!isset($this->_method_properties[$method])) {
1160            $this->_method_properties[$method] = array();
1161        }
1162
1163        return $this->_method_properties[$method][$property] = $value;
1164    }
1165
1166
1167    /**
1168     * Gets the value of a method property
1169     * @param string $method
1170     * @param string $property
1171     */
1172    function get_method_property($method, $property, $default=NULL)
1173    {
1174        $retval = NULL;
1175
1176        if (isset($this->_method_properties[$method][$property])) {
1177            $retval = $this->_method_properties[$method][$property];
1178        }
1179
1180		if (is_null($retval)) $retval=$default;
1181
1182        return $retval;
1183    }
1184
1185
1186    /**
1187     * Clears all method properties to have their default values. This is called
1188     * before every method call (before pre-hooks)
1189     * @param string $method
1190     */
1191    function reset_method_properties($method, $args=array())
1192    {
1193        $this->_method_properties[$method] = array(
1194            'run'               => TRUE,
1195            'run_pre_hooks'     => TRUE,
1196            'run_post_hooks'    => TRUE,
1197			'arguments'			=> $args
1198        );
1199    }
1200
1201	/**
1202	 * Removes the cache of the method properties
1203	 * @param $method
1204	 */
1205	function remove_method_properties($method)
1206	{
1207		unset($this->_method_properties[$method]);
1208	}
1209
1210	/**
1211	 * Gets all method properties
1212	 * @return array
1213	 */
1214	function get_method_properties($method)
1215	{
1216		return $this->_method_properties[$method];
1217	}
1218
1219	/**
1220	 * Sets all method properties
1221	 * @param $method
1222	 * @param $props
1223	 */
1224	function set_method_properties($method, $props)
1225	{
1226		foreach ($props as $key => $value) {
1227			$this->set_method_property($method, $key, $value);
1228		}
1229	}
1230
1231    /**
1232     * Returns TRUE if the ExtensibleObject has decided to implement a
1233     * particular interface
1234     * @param string $interface
1235     * @return boolean
1236     */
1237    function implements_interface($interface)
1238    {
1239        return in_array($interface, $this->_interfaces);
1240    }
1241
1242    function get_class_definition_dir($parent=FALSE)
1243    {
1244        return dirname($this->get_class_definition_file($parent));
1245    }
1246
1247    function get_class_definition_file($parent=FALSE)
1248    {
1249		$klass = $this->get_class_name($this);
1250        $r = new ReflectionClass($klass);
1251        if ($parent) {
1252            $parent = $r->getParentClass();
1253            return $parent->getFileName();
1254        }
1255        return $r->getFileName();
1256    }
1257
1258    /**
1259     * Returns get_class_methods() optionally limited by Mixin
1260     *
1261     * @param string (optional) Only show functions provided by a mixin
1262     * @return array Results from get_class_methods()
1263     */
1264    public function get_instance_methods($name = null)
1265    {
1266        if (is_string($name))
1267        {
1268            $methods = array();
1269            foreach ($this->_method_map_cache as $method => $mixin) {
1270                if ($name == get_class($mixin))
1271                {
1272                    $methods[] = $method;
1273                }
1274            }
1275            return $methods;
1276        } else {
1277            $methods = get_class_methods($this);
1278            foreach ($this->_mixins as $mixin) {
1279                $methods = array_unique(array_merge($methods, get_class_methods($mixin)));
1280                sort($methods);
1281            }
1282
1283            return $methods;
1284        }
1285    }
1286}
1287
1288
1289/**
1290 * An mixin provides methods for an ExtensibleObject to use
1291 */
1292class Mixin extends PopeHelpers
1293{
1294    /**
1295     * The ExtensibleObject which called the extension's method
1296     * @var ExtensibleObject
1297     */
1298    var $object;
1299
1300    /**
1301     * The name of the method called on the ExtensibleObject
1302     * @var type
1303     */
1304    var $method_called;
1305
1306    /**
1307     * There really isn't any concept of 'parent' method. An ExtensibleObject
1308     * instance contains an ordered array of extension classes, which provides
1309     * the method implementations for the instance to use. Suppose that an
1310     * ExtensibleObject has two extension, and both have the same methods.The
1311     * last extension appears to 'override' the first extension. So, instead of calling
1312     * a 'parent' method, we're actually just calling an extension that was added sooner than
1313     * the one that is providing the current method implementation.
1314     */
1315    function call_parent($method)
1316    {
1317        $retval = NULL;
1318
1319        // To simulate a 'parent' call, we remove the current extension from the
1320        // ExtensibleObject that is providing the method's implementation, re-emit
1321        // the call on the instance to trigger the implementation from the previously
1322        // added extension, and then restore things by re-adding the current extension.
1323        // It's complicated, but it works.
1324
1325        // We need to determine the name of the extension. Because PHP 5.2 is
1326        // missing get_called_class(), we have to look it up in the backtrace
1327        $backtrace = debug_backtrace();
1328        $klass = get_class($backtrace[0]['object']);
1329
1330		// Get the method properties. We'll store this afterwards.
1331		$props = $this->object->get_method_properties($method);
1332
1333		// Perform the routine described above...
1334		$this->object->disable_pre_hooks($method);
1335		$this->object->disable_post_hooks($method);
1336		$this->object->disable_mixin($method, $klass);
1337
1338        // Call anchor
1339        $args = func_get_args();
1340
1341        // Remove $method parameter
1342        array_shift($args);
1343        $retval = $this->object->call_method($method, $args);
1344
1345		// Re-enable hooks
1346		$this->object->enable_pre_hooks($method);
1347		$this->object->enable_post_hooks($method);
1348		$this->object->enable_mixin($method, $klass);
1349
1350		// Re-set all method properties
1351		$this->object->set_method_properties($method, $props);
1352
1353        return $retval;
1354    }
1355
1356    /**
1357     * Although is is preferrable to call $this->object->method(), sometimes
1358     * it's nice to use $this->method() instead.
1359     * @param string $method
1360     * @param array $args
1361     * @return mixed
1362     */
1363    function __call($method, $args)
1364    {
1365        if ($this->object->has_method($method)) {
1366            return call_user_func_array(array(&$this->object, $method), $args);
1367        }
1368    }
1369
1370    /**
1371     * Although extensions can have state, it's probably more desirable to maintain
1372     * the state in the parent object to keep a sane environment
1373     * @param string $property
1374     * @return mixed
1375     */
1376    function __get($property)
1377    {
1378        return $this->object->$property;
1379    }
1380}
1381
1382/**
1383 * An extension which has the purpose of being used as a hook
1384 */
1385class Hook extends Mixin
1386{
1387    // Similiar to a mixin's call_parent method.
1388    // If a hook needs to call the method that it applied the
1389    // Hook n' Anchor pattern to, then this method should be called
1390    function call_anchor()
1391    {
1392		// Disable hooks, so that we call the anchor point
1393		$this->object->disable_pre_hooks($this->method_called);
1394		$this->object->disable_post_hooks($this->method_called);
1395
1396        // Call anchor
1397        $args = func_get_args();
1398        $retval = $this->object->call_method($this->method_called, $args);
1399
1400		// Re-enable hooks
1401		$this->object->enable_pre_hooks($this->method_called);
1402		$this->object->enable_post_hooks($this->method_called);
1403
1404		return $retval;
1405    }
1406
1407    /**
1408     * Provides an alias for call_anchor, as there's no parent
1409     * to call in the context of a hook.
1410     */
1411    function call_parent($method)
1412    {
1413        $args = func_get_args();
1414        return call_user_func_array(
1415            array(&$this, 'call_anchor'),
1416            $args
1417        );
1418    }
1419};