/pope/lib/class.extensibleobject.php
PHP | 1015 lines | 519 code | 159 blank | 337 comment | 76 complexity | cb659f99963dfc694752f58574128358 MD5 | raw file
- <?php
- define('__EXTOBJ_STATIC__', '__STATICALLY_CALLED__');
- define('__EXTOBJ_NO_INIT__', '__NO_INIT__');
- /**
- * Provides helper methods for Pope objects
- */
- class PopeHelpers
- {
- /**
- * Merges two associative arrays
- * @param array $a1
- * @param array $a2
- * @return array
- */
- function array_merge_assoc($a1, $a2)
- {
- foreach ($a2 as $key => $value) {
- if (isset($a1[$key])) {
- if (is_array($value)) {
- $a1[$key] = $this->array_merge_assoc($a1[$key], $value);
- }
- else {
- $a1[$key] = $value;
- }
- }
- else $a1[$key] = $value;
- }
- return $a1;
- }
- /**
- * Returns TRUE if a property is empty
- * @param string $var
- * @return boolean
- */
- function is_empty($var, $element=FALSE)
- {
- if (is_array($var) && $element) {
- if (isset($var[$element])) $var = $var[$element];
- else $var = FALSE;
- }
- return (is_null($var) OR (is_string($var) AND strlen($var) == 0) OR $var === FALSE);
- }
- }
- /**
- * An ExtensibleObject can be extended at runtime with methods from another
- * class.
- *
- * - Mixins may be added or removed at any time during runtime
- * - The path to the mixin is cached so that subsequent method calls are
- * faster
- * - Pre and post hooks can be added or removed at any time during runtime.
- * - Each method call has a list of associated properties that can be modified
- * by pre/post hooks, such as: return_value, run_pre_hooks, run_post_hooks, etc
- * - Methods can be replaced by other methods at runtime
- * - Objects can implement interfaces, and are constrained to implement all
- * methods as defined by the interface
- * - All methods are public. There's no added security by having private/protected
- * members, as monkeypatching can always expose any method. Instead, protect
- * your methods using obscurity. Conventionally, use an underscore to define
- * a method that's private to an API
- */
- class ExtensibleObject extends PopeHelpers
- {
- const METHOD_PROPERTY_RUN='run';
- const METHOD_PROPERTY_RUN_POST_HOOKS='run_post_hooks';
- const METHOD_PROPERTY_RUN_PRE_HOOKS='run_pre_hooks';
- const METHOD_PROPERTY_RETURN_VALUE='return_value';
- var $_mixins = array();
- var $_mixin_priorities = array();
- var $_pre_hooks = array();
- var $_global_pre_hooks = array();
- var $_global_post_hooks= array();
- var $_post_hooks = array();
- var $_method_map_cache = array();
- var $_interfaces = array();
- var $_overrides = array();
- var $_aliases = array();
- var $_method_properties = array();
- var $_throw_error = TRUE;
- var $_wrapped_class = FALSE;
- /**
- * Defines a new ExtensibleObject. Any subclass should call this constructor.
- * Subclasses are expected to provide the following:
- * define_instance() - adds extensions which provide instance methods
- * define_class() - adds extensions which provide static methods
- * initialize() - used to initialize the state of the object
- */
- function __construct()
- {
- $args = func_get_args();
- $define_instance = TRUE;
- $init_instance = TRUE;
- // The first argument could be a flag to ExtensibleObject
- // which indicates that only static-like methods will be called
- if (count($args) >= 1) {
- $first_arg = $args[0];
- if (is_string($first_arg)) {
- switch ($first_arg) {
- case __EXTOBJ_STATIC__:
- {
- $define_instance = FALSE;
- $init_instance = FALSE;
- if (method_exists($this, 'define_class')) {
- call_user_func_array(array($this, 'define_class'), $args);
- }
- elseif (method_exists($this, 'define_static')) {
- call_user_func_array(array($this, 'define_static'), $args);
- }
- break;
- }
- case __EXTOBJ_NO_INIT__:
- {
- $init_instance = FALSE;
- break;
- }
- }
- }
- }
- // Are we to define instance methods?
- if ($define_instance) {
- if (method_exists($this, 'define_instance')) {
- call_user_func_array(array($this, 'define_instance'), $args);
- }
- elseif (method_exists($this, 'define')) {
- call_user_func_array(array($this, 'define'), $args);
- }
- $this->_enforce_interface_contracts();
- if ($init_instance) {
- // Initialize the state of the object
- if (method_exists($this, 'initialize')) {
- call_user_func_array(array($this, 'initialize'), $args);
- }
- }
- }
- }
- /**
- * Adds an extension class to the object. The extension provides
- * methods for this class to expose as it's own
- * @param string $class
- */
- function add_mixin($class, $instantiate=FALSE)
- {
- // We used to instantiate the class, but I figure
- // we might as well wait till the method is called to
- // save memory. Instead, the _call() method calls the
- // _instantiate_mixin() method below.
- $this->_mixins[$class] = FALSE; // new $class();
- array_unshift($this->_mixin_priorities, $class);
- $this->_flush_cache();
- // Should we instantiate the object now?
- if ($instantiate) $this->_instantiate_mixin($class);
- }
- /**
- * Stores the instantiated class
- * @param string $class
- * @return mixed
- */
- function _instantiate_mixin($class)
- {
- $retval = FALSE;
- if ($this->_mixins[$class])
- $retval = $this->_mixins[$class];
- else {
- $obj= new $class();
- $obj->object = &$this;
- $retval = $this->_mixins[$class] = &$obj;
- if (method_exists($obj, 'initialize')) $obj->initialize();
- }
- return $retval;
- }
- /**
- * Deletes an extension from the object. The methods provided by that
- * extension are no longer available for the object
- * @param string $class
- */
- function del_mixin($class)
- {
- unset($this->_mixins[$class]);
- $index = array_search($class, $this->_mixin_priorities);
- if ($index !== FALSE) {
- unset($this->_mixin_priorities[$index]);
- $this->_flush_cache();
- }
- }
- function remove_mixin($class)
- {
- $this->del_mixin($class);
- }
- /**
- * Replaces an extension methods with that of another class.
- * @param string $method
- * @param string $class
- * @param string $new_method
- */
- function replace_method($method, $class, $new_method=FALSE)
- {
- if (!$new_method) $new_method = $method;
- $this->_overrides[$method] = $class;
- $this->add_pre_hook($method, $class, $new_method);
- $this->_flush_cache();
- }
- /**
- * Restores a method that was replaced by a former call to replace_method()
- * @param string $method
- */
- function restore_method($method)
- {
- $class = $this->_overrides[$method];
- unset($this->_overrides[$method]);
- $this->del_pre_hook($method, $class);
- $this->_flush_cache();
- }
- /**
- * Returns the Mixin which provides the specified method
- * @param string $method
- */
- function get_mixin_providing($method)
- {
- $retval = FALSE;
- // If it's cached, then we've got it easy
- if ($this->is_cached($method)) {
- $object = $this->_method_map_cache[$method];
- $retval = get_class($object);
- }
- // Otherwise, we have to look it up
- else {
- foreach ($this->_mixin_priorities as $klass) {
- $object = $this->_instantiate_mixin($klass);
- if (method_exists($object, $method)) {
- $retval = get_class($object);
- $this->_cache_method($method, $method);
- }
- }
- }
- return $retval;
- }
- /**
- * When an ExtensibleObject is instantiated, it checks whether all
- * the registered extensions combined provide the implementation as required
- * by the interfaces registered for this object
- */
- function _enforce_interface_contracts()
- {
- $errors = array();
- foreach ($this->_interfaces as $i) {
- $r = new ReflectionClass($i);
- foreach ($r->getMethods() as $m) {
- if (!$this->has_method($m->name)) {
- $class = get_class($this);
- $errors[] = "`{$class}` does not implement `{$m->name}` as required by `{$i}`";
- }
- }
- }
- if ($errors) throw new Exception(implode(". ", $errors));
- }
- /**
- * Implement a defined interface. Does the same as the 'implements' keyword
- * for PHP, except this method takes into account extensions
- * @param string $interface
- */
- function implement($interface)
- {
- $this->_interfaces[] = $interface;
- }
- /**
- * Adds a hook that gets executed before every method call
- * @param string $name
- * @param string $class
- * @param string $hook_method
- */
- function add_global_pre_hook($name, $class, $hook_method)
- {
- add_pre_hook('*', $name, $class, $hook_method);
- }
- /**
- * Adds a hook that will get executed before a particular method call
- * @param string $method
- * @param string $name
- * @param string $class
- * @param string $hook_method
- */
- function add_pre_hook($method, $name, $class, $hook_method=FALSE)
- {
- if (!$hook_method) $hook_method = $method;
- // Is this a global pre hook?
- if ($method == '*') {
- $this->_global_pre_hooks[$name] = array(
- new $class,
- $hook_method
- );
- }
- // This is a method-specific pre hook
- else {
- if (!isset($this->_pre_hooks[$method])) {
- $this->_pre_hooks[$method] = array();
- }
- $this->_pre_hooks[$method][$name] = array(
- new $class,
- $hook_method
- );
- }
- }
- /**
- * Adds a hook to be called after a particular method call
- * @param string $method
- * @param string $hook_name
- * @param string $class
- * @param string $hook_method
- */
- function add_post_hook($method, $hook_name, $class, $hook_method=FALSE)
- {
- // Is this a global post hook?
- if ($method == '*') {
- $this->_post_hooks[$hook_name] = array(
- new $class,
- $hook_method
- );
- }
- // This is a method-specific post hook
- else {
- if (!$hook_method) $hook_method = $method;
- if (!isset($this->_post_hooks[$method])) {
- $this->_post_hooks[$method] = array();
- }
- $this->_post_hooks[$method][$hook_name] = array(
- new $class,
- $hook_method
- );
- }
- }
- /**
- * Deletes a hook that's executed before the specified method
- * @param string $method
- * @param string $name
- */
- function del_pre_hook($method, $name)
- {
- unset($this->_pre_hooks[$method][$name]);
- }
- /**
- * Deletes all pre hooks registered
- **/
- function del_pre_hooks($method=FALSE)
- {
- if (!$method)
- $this->_pre_hooks = array();
- else
- unset($this->_pre_hooks[$method]);
- }
- /**
- * Deletes a hook that's executed after the specified method
- * @param string $method
- * @param string $name
- */
- function del_post_hook($method, $name)
- {
- unset($this->_post_hooks[$method][$name]);
- }
- /**
- * Deletes all post hooks
- */
- function del_post_hooks($method=FALSE)
- {
- if (!$method)
- $this->_post_hooks = array();
- else
- unset($this->_post_hooks[$method]);
- }
- /**
- * Wraps a class within an ExtensibleObject class.
- * @param string $klass
- * @param array callback, used to tell ExtensibleObject how to instantiate
- * the wrapped class
- */
- function wrap($klass, $callback=FALSE, $args=array())
- {
- if ($callback) {
- $this->_wrapped_class = call_user_func($callback, $args);
- }
- else {
- $this->_wrapped_class = new $klass();
- }
- }
- /**
- * Determines if the ExtensibleObject is a wrapper for an existing class
- */
- function is_wrapper()
- {
- return $this->_wrapped_class ? TRUE : FALSE;
- }
- /**
- * Returns the name of the class which this ExtensibleObject wraps
- * @return string
- */
- function &get_wrapped_class()
- {
- return $this->_wrapped_class;
- }
- /**
- * Returns TRUE if the wrapped class provides the specified method
- */
- function wrapped_class_provides($method)
- {
- $retval = FALSE;
- // Determine if the wrapped class is another ExtensibleObject
- if (method_exists($this->_wrapped_class, 'has_method')) {
- $retval = $this->_wrapped_class->has_method($method);
- }
- else if (method_exists($this->_wrapped_class, $method)){
- $retval = TRUE;
- }
- return $retval;
- }
- /**
- * Provides a means of calling static methods, provided by extensions
- * @param string $method
- * @return mixed
- */
- static function get_class()
- {
- $klass = get_class();
- $obj = new $klass(__EXTOBJ_STATIC__);
- return $obj;
- }
- /**
- * Gets a property from a wrapped object
- * @param string $property
- * @return mixed
- */
- function __get($property)
- {
- if ($this->is_wrapper()) {
- return $this->_wrapped_class->$property;
- }
- else return NULL;
- }
- /**
- * Sets a property on a wrapped object
- * @param string $property
- * @param mixed $value
- * @return mixed
- */
- function __set($property, $value)
- {
- if ($this->is_wrapper()) {
- return $this->_wrapped_class->$property = $value;
- }
- else {
- return $this->$property = $value;
- }
- }
- /**
- * Finds a method defined by an extension and calls it. However, execution
- * is a little more in-depth:
- * 1) Execute all global pre-hooks and any pre-hooks specific to the requested
- * method. Each method call has instance properties that can be set by
- * other hooks to modify the execution. For example, a pre hook can
- * change the 'run_pre_hooks' property to be false, which will ensure that
- * all other pre hooks will NOT be executed.
- * 2) Runs the method. Checks whether the path to the method has been cached
- * 3) Execute all global post-hooks and any post-hooks specific to the
- * requested method. Post hooks can access method properties as well. A
- * common usecase is to return the value of a post hook instead of the
- * actual method call. To do this, set the 'return_value' property.
- * @param string $method
- * @param array $args
- * @return mixed
- */
- function __call($method, $args)
- {
- $this->clear_method_properties($method);
- // Run pre hooks?
- if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN_PRE_HOOKS)) {
- // Combine global and method-specific pre hooks
- $prehooks = $this->_global_pre_hooks;
- if (isset($this->_pre_hooks[$method])) {
- $prehooks = array_merge($prehooks, $this->_pre_hooks[$method]);
- }
- // Apply each hook
- foreach ($prehooks as $hook_name => $hook) {
- $this->_run_prehook($hook_name, $method, $hook[0], $hook[1], $args);
- }
- }
- // Are we to run the actual method? A pre hook might have told us
- // not to
- if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN) &&
- !isset($this->_overrides[$method])) {
- // Try to fetch the method from the cache
- if ($this->is_cached($method)) {
- $this->set_method_property(
- $method,
- self::METHOD_PROPERTY_RETURN_VALUE,
- $this->_exec_cached_method($method, $args)
- );
- }
- // No cached method exists.
- // Iterate through each extension, retrieving the most recently
- // added extensions first and find the method
- else {
- $found = FALSE;
- // Perhaps an extension provides this method
- foreach ($this->_mixin_priorities as $klass) {
- $object = $this->_instantiate_mixin($klass);
- if (method_exists($object, $method)) {
- // Cache the class which provides this method, to make
- // lookups easier the next time around
- $this->_cache_method($object, $method);
- // Calls the method and sets the return value
- $this->set_method_property(
- $method,
- self::METHOD_PROPERTY_RETURN_VALUE,
- call_user_func_array(array($object, $method), $args)
- );
- $found = TRUE;
- break;
- }
- }
- // This is NOT a wrapped class, and no extensions provide the method
- if (!$found){
- // Perhaps this is a wrapper and the wrapped object
- // provides this method
- if ($this->is_wrapper() && $this->wrapped_class_provides($method)) {
- $found = TRUE;
- $this->set_method_property(
- $method,
- self::METHOD_PROPERTY_RETURN_VALUE,
- call_user_func_array(array($this->_wrapped_class, $method), $args)
- );
- }
- elseif ($this->_throw_error) throw new Exception("`{$method}` not defined for ".get_class());
- else return $found;
- }
- }
- }
- // Are we to run post hooks? A pre hook might have told us not to
- if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN_POST_HOOKS)) {
- // Combine global and method-specific post hooks
- $posthooks = $this->_global_post_hooks;
- if (isset($this->_post_hooks[$method])) {
- $posthooks = array_merge($posthooks, $this->_post_hooks[$method]);
- }
- // Apply each hook
- foreach ($posthooks as $hook_name => $hook) {
- $this->_run_post_hook($hook_name, $method, $hook[0], $hook[1], $args);
- }
- }
- return $this->get_method_property($method, self::METHOD_PROPERTY_RETURN_VALUE);
- }
- /**
- * Provides an alternative way to call methods
- */
- function call_method($method, $args=array())
- {
- if (method_exists($this, $method))
- return call_user_func_array(array(&$this, $method), $args);
- else
- return $this->__call($method, $args);
- }
- /**
- * Returns TRUE if the method in particular has been cached
- * @param string $method
- * @return type
- */
- function is_cached($method)
- {
- return isset($this->_method_map_cache[$method]);
- }
- /**
- * Caches the path to the extension which provides a particular method
- * @param string $object
- * @param string $method
- */
- function _cache_method($object, $method)
- {
- $this->_method_map_cache[$method] = $object;
- }
- /**
- * Flushes the method cache
- */
- function _flush_cache()
- {
- $this->_method_map_cache = array();
- }
- /**
- * Returns TRUE if the object provides the particular method
- * @param string $method
- * @return boolean
- */
- function has_method($method)
- {
- $retval = FALSE;
- if (!$this->is_cached($method)) {
- if ($this->is_wrapper() && $this->wrapped_class_provides($method)) $retval = TRUE;
- elseif (method_exists($this, $method)) $retval = TRUE;
- else{
- foreach ($this->_mixin_priorities as $klass) {
- $object = $this->_instantiate_mixin($klass);
- if (method_exists($object, $method)) {
- $retval = TRUE;
- $this->_cache_method($object, $method);
- break;
- }
- }
- }
- }
- else $retval = TRUE;
- return $retval;
- }
- /**
- * Runs a particular pre hook for the specified method. The return value
- * is assigned to the "[hook_name]_prehook_retval" method property
- * @param string $hook_name
- * @param string $method_called
- * @param Ext $object
- * @param string $hook_method
- *
- */
- function _run_prehook($hook_name, $method_called, $object, $hook_method, $args=array())
- {
- $object->object = &$this;
- $object->method_called = $method_called;
- // Are we STILL to execute pre hooks? A pre-executed hook
- // might have changed this
- if ($this->get_method_property($method_called, 'run_pre_hooks')) {
- $this->set_method_property($method_called, $hook_name.'_prehook_retval',
- call_user_func_array(array(&$object, $hook_method),$args)
- );
- }
- }
- /**
- * Runs the specified post hook for the specified method
- * @param string $hook_name
- * @param string $method_called
- * @param Ext $object
- * @param string $hook_method
- */
- function _run_post_hook($hook_name, $method_called, $object, $hook_method, $args=array())
- {
- $object->object = &$this;
- $object->method_called = $method_called;
- // Are we STILL to execute pre hooks? A pre-executed hook
- // might have changed this
- if ($this->get_method_property($method_called, 'run_post_hooks')) {
- $this->set_method_property($method_called, $hook_name.'_post_hook_retval',
- call_user_func_array(array(&$object, $hook_method), $args)
- );
- }
- }
- /**
- * Returns TRUE if a pre-hook has been registered for the specified method
- * @param string $method
- * @return boolean
- */
- function have_prehook_for($method)
- {
- return isset($this->_pre_hooks[$method]);
- }
- /**
- * Returns TRUE if a posthook has been registered for the specified method
- * @param string $method
- * @return boolean
- */
- function have_posthook_for($method)
- {
- return isset($this->_post_hooks[$method]);
- }
- /**
- * Executes a cached method
- * @param string $method
- * @param array $args
- * @return mixed
- */
- function _exec_cached_method($method, $args=array())
- {
- $object = $this->_method_map_cache[$method];
- $object->object = &$this;
- return call_user_func_array(array(&$object, $method), $args);
- }
- /**
- * Sets the value of a method property
- * @param string $method
- * @param string $property
- * @param mixed $value
- */
- function set_method_property($method, $property, $value)
- {
- if (!isset($this->_method_properties[$method])) {
- $this->_method_properties[$method] = array();
- }
- $this->_method_properties[$method][$property] = $value;
- }
- /**
- * Gets the value of a method property
- * @param string $method
- * @param string $property
- */
- function get_method_property($method, $property)
- {
- $retval = FALSE;
- if (isset($this->_method_properties[$method][$property])) {
- $retval = $this->_method_properties[$method][$property];
- }
- return $retval;
- }
- /**
- * Clears all method properties to have their default values. This is called
- * before every method call (before pre-hooks)
- * @param string $method
- */
- function clear_method_properties($method)
- {
- $this->_method_properties[$method] = array(
- 'run' => TRUE,
- 'run_pre_hooks' => TRUE,
- 'run_post_hooks' => TRUE
- );
- }
- /**
- * Returns TRUE if the ExtensibleObject has decided to implement a
- * particular interface
- * @param string $interface
- * @return boolean
- */
- function implements_interface($interface)
- {
- return in_array($interface, $this->_interfaces);
- }
- function get_class_definition_dir($parent=FALSE)
- {
- return dirname($this->get_class_definition_file($parent));
- }
- function get_class_definition_file($parent=FALSE)
- {
- $klass = get_class($this);
- $r = new ReflectionClass($klass);
- if ($parent) {
- $parent = $r->getParentClass();
- return $parent->getFileName();
- }
- return $r->getFileName();
- }
- }
- /**
- * An mixin provides methods for an ExtensibleObject to use
- */
- class Mixin extends PopeHelpers
- {
- /**
- * The ExtensibleObject which called the extension's method
- * @var ExtensibleObject
- */
- var $object;
- /**
- * The name of the method called on the ExtensibleObject
- * @var type
- */
- var $method_called;
- /**
- * There really isn't any concept of 'parent' method. An ExtensibleObject
- * instance contains an ordered array of extension classes, which provides
- * the method implementations for the instance to use. Suppose that an
- * ExtensibleObject has two extension, and both have the same methods.The
- * last extension appears to 'override' the first extension. So, instead of calling
- * a 'parent' method, we're actually just calling an extension that was added sooner than
- * the one that is providing the current method implementation.
- */
- function call_parent($method=FALSE)
- {
- $retval = NULL;
- // To simulate a 'parent' call, we remove the current extension from the
- // ExtensibleObject that is providing the method's implementation, re-emit
- // the call on the instance to trigger the implementation from the previously
- // added extension, and then restore things by re-adding the current extension.
- // It's complicated, but it works.
- // We need to determine the name of the extension. Because PHP 5.2 is
- // missing get_called_class(), we have to look it up in the backtrace
- $backtrace = debug_backtrace();
- $klass = get_class($backtrace[0]['object']);
- // Because we already have the backtrace(), we'll let the user omit specifying
- // the name of the called method and look it up using the backtrace as well
- if (!$method) $method = $backtrace[1]['function'];
- if (strpos($method, '::') !== FALSE) $method = array_pop(explode('::', $method));
- // Perform the algorithm stated at the beginning of this function
- // We also clone the object to ensure that method properties can be
- // changed in another mixin's implementation of the method being called.
- $new_obj = clone $this->object;
- $new_obj->del_pre_hooks($method);
- $new_obj->del_post_hooks($method);
- $new_obj->del_mixin($klass);
- $args = func_get_args();
- array_shift($args);
- $retval = $new_obj->call_method($method, $args);
- unset($new_obj);
- return $retval;
- }
- /**
- * Although is is preferrable to call $this->object->method(), sometimes
- * it's nice to use $this->method() instead.
- * @param string $method
- * @param array $args
- * @return mixed
- */
- function __call($method, $args)
- {
- if ($this->object->has_method($method)) {
- return call_user_func_array(array(&$this->object, $method), $args);
- }
- }
- /**
- * Although extensions can have state, it's probably more desirable to maintain
- * the state in the parent object to keep a sane environment
- * @param string $property
- * @return mixed
- */
- function __get($property)
- {
- return $this->object->$property;
- }
- }
- /**
- * An extension which has the purpose of being used as a hook
- */
- class Hook extends Mixin
- {
- // Similiar to a mixin's call_parent method.
- // If a hook needs to call the method that it applied the
- // Hook n' Anchor pattern to, then this method should be called
- function call_anchor()
- {
- $retval = NULL;
- /**
- * @var ExtensibleObject $new_obj
- */
- $new_obj = clone $this->object;
- $new_obj->del_pre_hooks($this->method_called);
- $new_obj->del_post_hooks($this->method_called);
- // Call anchor
- $args = func_get_args();
- $retval = $new_obj->call_method($this->method_called, $args);
- unset($new_obj);
- return $retval;
- }
- /**
- * Provides an alias for call_anchor, as there's no parent
- * to call in the context of a hook.
- * @param string $method, ignored
- */
- function call_parent($method = FALSE)
- {
- $args = func_get_args();
- return call_user_func_array(
- array(&$this, 'call_anchor'),
- $args
- );
- }
- };