ExtensibleObject

An ExtensibleObject can be extended at runtime with methods from another class.

Defined (1)

The class is defined in the following location(s).

/pope/lib/class.extensibleobject.php  
  1. class ExtensibleObject extends PopeHelpers 
  2. static $enforce_interfaces=TRUE; 
  3.  
  4. var $_mixins = array(); 
  5. var $_mixin_priorities = array(); 
  6. var $_method_map_cache = array(); 
  7. var $_disabled_map = array(); 
  8. var $_interfaces = array(); 
  9. var $_throw_error = TRUE; 
  10. var $_wrapped_instance = FALSE; 
  11. var $object = NULL; 
  12.  
  13. /** 
  14. * Defines a new ExtensibleObject. Any subclass should call this constructor. 
  15. * Subclasses are expected to provide the following: 
  16. * define_instance() - adds extensions which provide instance methods 
  17. * define_class() - adds extensions which provide static methods 
  18. * initialize() - used to initialize the state of the object 
  19. */ 
  20. function __construct() 
  21. // TODO This can be removed in the future. The Photocrati Theme currently requires this. 
  22. $this->object = $this; 
  23.  
  24. $args = func_get_args(); 
  25.  
  26. // Define the instance 
  27. if (method_exists($this, 'define_instance')) 
  28. $reflection = new ReflectionMethod($this, 'define_instance'); 
  29. $reflection->invokeArgs($this, $args); 
  30. elseif (method_exists($this, 'define')) { 
  31. $reflection = new ReflectionMethod($this, 'define'); 
  32. $reflection->invokeArgs($this, $args); 
  33. if (self::$enforce_interfaces) $this->_enforce_interface_contracts(); 
  34.  
  35. if (!isset($args[0]) || $args[0] != __EXTOBJ_NO_INIT__) { 
  36. // Initialize the state of the object 
  37. if (method_exists($this, 'initialize')) { 
  38. $reflection = new ReflectionMethod($this, 'initialize'); 
  39. $reflection->invokeArgs($this, $args); 
  40.  
  41.  
  42. /** 
  43. * Adds an extension class to the object. The extension provides 
  44. * methods for this class to expose as it's own 
  45. * @param string $class 
  46. */ 
  47. function add_mixin($class, $instantiate=FALSE) 
  48. $retval = TRUE; 
  49.  
  50. if (!$this->has_mixin($class)) { 
  51. // We used to instantiate the class, but I figure 
  52. // we might as well wait till the method is called to 
  53. // save memory. Instead, the _call() method calls the 
  54. // _instantiate_mixin() method below. 
  55. $this->_mixins[$class] = NULL; // new $class(); 
  56. array_unshift($this->_mixin_priorities, $class); 
  57.  
  58. // Instantiate the mixin immediately, if requested 
  59. if ($instantiate) $this->_instantiate_mixin($class); 
  60. $this->_flush_cache(); 
  61.  
  62. else $retval = FALSE; 
  63.  
  64. return $retval; 
  65.  
  66.  
  67. /** 
  68. * Determines if a mixin has been added to this class 
  69. * @param string $klass 
  70. * @return bool 
  71. */ 
  72. function has_mixin($klass) 
  73. return array_key_exists($klass, $this->_mixins); 
  74.  
  75.  
  76. /** 
  77. * Stores the instantiated class 
  78. * @param string $class 
  79. * @return mixed 
  80. */ 
  81. function &_instantiate_mixin($class) 
  82. $retval = FALSE; 
  83. if (isset($this->_mixins[$class])) 
  84. $retval = $this->_mixins[$class]; 
  85. else { 
  86. $obj= new $class(); 
  87. $obj->object = $this; 
  88. $retval = $this->_mixins[$class] = &$obj; 
  89. if (method_exists($obj, 'initialize')) $obj->initialize(); 
  90. unset($obj->object); 
  91.  
  92. return $retval; 
  93.  
  94.  
  95. /** 
  96. * Deletes an extension from the object. The methods provided by that 
  97. * extension are no longer available for the object 
  98. * @param string $class 
  99. */ 
  100. function del_mixin($class) 
  101. unset($this->_mixins[$class]); 
  102. $index = array_search($class, $this->_mixin_priorities); 
  103. unset($this->_mixin_priorities[$index]); 
  104. $this->_flush_cache(); 
  105.  
  106.  
  107. function remove_mixin($class) 
  108. $this->del_mixin($class); 
  109.  
  110.  
  111. /** 
  112. * Returns the Mixin which provides the specified method 
  113. * @param string $method 
  114. */ 
  115. function get_mixin_providing($method, $return_obj=FALSE) 
  116. $retval = FALSE; 
  117.  
  118. // If it's cached, then we've got it easy 
  119. if ($this->is_cached($method)) { 
  120. $klass = $this->_method_map_cache[$method]; 
  121. return $return_obj ? $this->_instantiate_mixin($klass) : $klass; 
  122.  
  123. // Otherwise, we have to look it up 
  124. else { 
  125. foreach ($this->_mixin_priorities as $class_name) { 
  126. if (method_exists($class_name, $method) && !$this->is_mixin_disabled_for($method, $class_name)) { 
  127. $object = $this->_instantiate_mixin($class_name); 
  128. $this->_cache_method($class_name, $method); 
  129. $retval = $return_obj ? $object : $class_name; 
  130. break; 
  131. elseif (!class_exists($class_name)) { 
  132. throw new RuntimeException("{$class_name} does not exist."); 
  133.  
  134. return $retval; 
  135.  
  136. function is_mixin_disabled_for($method, $mixin_klass) 
  137. $retval = FALSE; 
  138.  
  139. if (isset($this->_disabled_map[$method])) { 
  140. $retval = in_array($mixin_klass, $this->_disabled_map[$method]); 
  141.  
  142. return $retval; 
  143.  
  144. function disable_mixin_for($method, $mixin_klass) 
  145. if (!isset($this->_disabled_map[$method])) { 
  146. $this->_disabled_map[$method] = array($mixin_klass); 
  147. else if (!in_array($mixin_klass, $this->_disabled_map[$method])) { 
  148. array_push($this->_disabled_map[$method], $mixin_klass); 
  149.  
  150. unset($this->_method_map_cache[$method]); 
  151.  
  152. function enable_mixin_for($method, $mixin_klass) 
  153. if (isset($this->_disabled_map[$method])) { 
  154. if (($index = array_search($mixin_klass, $this->_disabled_map[$method])) !== FALSE) { 
  155. unset($this->_disabled_map[$method][$index]); 
  156.  
  157. /** 
  158. * When an ExtensibleObject is instantiated, it checks whether all 
  159. * the registered extensions combined provide the implementation as required 
  160. * by the interfaces registered for this object 
  161. */ 
  162. function _enforce_interface_contracts() 
  163. $errors = array(); 
  164.  
  165. foreach ($this->_interfaces as $i) { 
  166. $r = new ReflectionClass($i); 
  167. foreach ($r->getMethods() as $m) { 
  168. if (!$this->has_method($m->name)) { 
  169. $klass = $this->get_class_name($this); 
  170. $errors[] = "`{$klass}` does not implement `{$m->name}` as required by `{$i}`"; 
  171.  
  172. if ($errors) throw new Exception(implode(". ", $errors)); 
  173.  
  174.  
  175. /** 
  176. * Implement a defined interface. Does the same as the 'implements' keyword 
  177. * for PHP, except this method takes into account extensions 
  178. * @param string $interface 
  179. */ 
  180. function implement($interface) 
  181. $this->_interfaces[] = $interface; 
  182.  
  183. /** 
  184. * Wraps a class within an ExtensibleObject class. 
  185. * @param string $klass 
  186. * @param array callback, used to tell ExtensibleObject how to instantiate 
  187. * the wrapped class 
  188. */ 
  189. function wrap($klass, $callback=FALSE, $args=array()) 
  190. if ($callback) { 
  191. $this->_wrapped_instance = call_user_func($callback, $args); 
  192. else { 
  193. $this->_wrapped_instance = new $klass(); 
  194.  
  195.  
  196. /** 
  197. * Determines if the ExtensibleObject is a wrapper for an existing class 
  198. */ 
  199. function is_wrapper() 
  200. return $this->_wrapped_instance ? TRUE : FALSE; 
  201.  
  202.  
  203. /** 
  204. * Returns the name of the class which this ExtensibleObject wraps 
  205. * @return string 
  206. */ 
  207. function &get_wrapped_instance() 
  208. return $this->_wrapped_instance; 
  209.  
  210.  
  211. /** 
  212. * Returns TRUE if the wrapped class provides the specified method 
  213. */ 
  214. function wrapped_class_provides($method) 
  215. $retval = FALSE; 
  216.  
  217. // Determine if the wrapped class is another ExtensibleObject 
  218. if (method_exists($this->_wrapped_instance, 'has_method')) { 
  219. $retval = $this->_wrapped_instance->has_method($method); 
  220. elseif (method_exists($this->_wrapped_instance, $method)) { 
  221. $retval = TRUE; 
  222.  
  223. return $retval; 
  224.  
  225.  
  226. /** 
  227. * Provides a means of calling static methods, provided by extensions 
  228. * @param string $method 
  229. * @return mixed 
  230. */ 
  231. static function get_class() 
  232. // Note: this function is static so $this is not defined 
  233. $klass = self::get_class_name(); 
  234. $obj = new $klass(__EXTOBJ_STATIC__); 
  235. return $obj; 
  236.  
  237.  
  238. /** 
  239. * Gets the name of the ExtensibleObject 
  240. * @return string 
  241. */ 
  242. static function get_class_name($obj = null) 
  243. if ($obj) 
  244. return get_class($obj); 
  245. elseif (function_exists('get_called_class')) 
  246. return get_called_class(); 
  247. else 
  248. return get_class(); 
  249.  
  250. /** 
  251. * Gets a property from a wrapped object 
  252. * @param string $property 
  253. * @return mixed 
  254. */ 
  255. function __get($property) 
  256. $retval = NULL; 
  257.  
  258. if ($property == 'object') return $this; 
  259. else if ($this->is_wrapper()) { 
  260. try { 
  261. $reflected_prop = new ReflectionProperty($this->_wrapped_instance, $property); 
  262.  
  263. // setAccessible method is only available for PHP 5.3 and above 
  264. if (method_exists($reflected_prop, 'setAccessible')) { 
  265. $reflected_prop->setAccessible(TRUE); 
  266.  
  267. $retval = $reflected_prop->getValue($this->_wrapped_instance); 
  268. catch (ReflectionException $ex) 
  269. $retval = $this->_wrapped_instance->$property; 
  270.  
  271. return $retval; 
  272.  
  273. /** 
  274. * Determines if a property (dynamic or not) exists for the object 
  275. * @param string $property 
  276. * @return boolean 
  277. */ 
  278. function __isset($property) 
  279. $retval = FALSE; 
  280.  
  281. if (property_exists($this, $property)) { 
  282. $retval = isset($this->$property); 
  283. elseif ($this->is_wrapper() && property_exists($this->_wrapped_instance, $property)) { 
  284. $retval = isset($this->$property); 
  285.  
  286. return $retval; 
  287.  
  288.  
  289. /** 
  290. * Sets a property on a wrapped object 
  291. * @param string $property 
  292. * @param mixed $value 
  293. * @return mixed 
  294. */ 
  295. function __set($property, $value) 
  296. $retval = NULL; 
  297.  
  298. if ($this->is_wrapper()) { 
  299. try { 
  300. $reflected_prop = new ReflectionProperty($this->_wrapped_instance, $property); 
  301.  
  302. // The property must be accessible, but this is only available 
  303. // on PHP 5.3 and above 
  304. if (method_exists($reflected_prop, 'setAccessible')) { 
  305. $reflected_prop->setAccessible(TRUE); 
  306.  
  307. $retval = &$reflected_prop->setValue($this->_wrapped_instance, $value); 
  308.  
  309. // Sometimes reflection can fail. In that case, we need 
  310. // some ingenuity as a failback 
  311. catch (ReflectionException $ex) { 
  312. $this->_wrapped_instance->$property = $value; 
  313. $retval = &$this->_wrapped_instance->$property; 
  314.  
  315. else { 
  316. $this->$property = $value; 
  317. $retval = &$this->$property; 
  318. return $retval; 
  319.  
  320.  
  321. /** 
  322. * Finds a method defined by an extension and calls it. However, execution 
  323. * is a little more in-depth: 
  324. * 1) Execute all global pre-hooks and any pre-hooks specific to the requested 
  325. * method. Each method call has instance properties that can be set by 
  326. * other hooks to modify the execution. For example, a pre hook can 
  327. * change the 'run_pre_hooks' property to be false, which will ensure that 
  328. * all other pre hooks will NOT be executed. 
  329. * 2) Runs the method. Checks whether the path to the method has been cached 
  330. * 3) Execute all global post-hooks and any post-hooks specific to the 
  331. * requested method. Post hooks can access method properties as well. A 
  332. * common usecase is to return the value of a post hook instead of the 
  333. * actual method call. To do this, set the 'return_value' property. 
  334. * @param string $method 
  335. * @param array $args 
  336. * @return mixed 
  337. */ 
  338. function __call($method, $args) 
  339. $retval = NULL; 
  340.  
  341. if (($this->get_mixin_providing($method))) { 
  342. $retval = $this->_exec_cached_method($method, $args); 
  343.  
  344. // This is NOT a wrapped class, and no extensions provide the method 
  345. else { 
  346. // Perhaps this is a wrapper and the wrapped object 
  347. // provides this method 
  348. if ($this->is_wrapper() && $this->wrapped_class_provides($method)) 
  349. $object = $this->add_wrapped_instance_method($method); 
  350. $retval = call_user_func_array( 
  351. array(&$object, $method),  
  352. $args 
  353. ); 
  354. elseif ($this->_throw_error) { 
  355. if (defined('POPE_DEBUG') && POPE_DEBUG) 
  356. print_r(debug_backtrace()); 
  357. throw new Exception("`{$method}` not defined for " . get_class()); 
  358.  
  359. return $retval; 
  360.  
  361.  
  362. /** 
  363. * Adds the implementation of a wrapped instance method to the ExtensibleObject 
  364. * @param string $method 
  365. * @return Mixin 
  366. */ 
  367. function add_wrapped_instance_method($method) 
  368. $retval = $this->get_wrapped_instance(); 
  369.  
  370. // If the wrapped instance is an ExtensibleObject, then we don't need 
  371. // to use reflection 
  372. if (!is_subclass_of($this->get_wrapped_instance(), 'ExtensibleObject')) { 
  373. $func = new ReflectionMethod($this->get_wrapped_instance(), $method); 
  374.  
  375. // Get the entire method definition 
  376. $filename = $func->getFileName(); 
  377. $start_line = $func->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block 
  378. $end_line = $func->getEndLine(); 
  379. $length = $end_line - $start_line; 
  380. $source = file($filename); 
  381. $body = implode("", array_slice($source, $start_line, $length)); 
  382. $body = preg_replace("/^\s{0, }private|protected\s{0, }/", '', $body); 
  383.  
  384. // Change the context 
  385. $body = str_replace('$this', '$this->object', $body); 
  386. $body = str_replace('$this->object->object', '$this->object', $body); 
  387. $body = str_replace('$this->object->$', '$this->object->', $body); 
  388.  
  389. // Define method for mixin 
  390. $mixin_klass = "Mixin_AutoGen_{$method}"; 
  391. if (!class_exists($mixin_klass)) { 
  392. eval("class {$mixin_klass} extends Mixin{ 
  393. {$body} 
  394. }"); 
  395. $this->add_mixin($mixin_klass); 
  396. $retval = $this->_instantiate_mixin($mixin_klass); 
  397. $this->_cache_method($mixin_klass, $method); 
  398.  
  399.  
  400. return $retval; 
  401.  
  402.  
  403. /** 
  404. * Provides an alternative way to call methods 
  405. */ 
  406. function call_method($method, $args=array()) 
  407. if (method_exists($this, $method)) 
  408. $reflection = new ReflectionMethod($this, $method); 
  409. return $reflection->invokeArgs($this, array($args)); 
  410. else { 
  411. return $this->__call($method, $args); 
  412.  
  413.  
  414. /** 
  415. * Returns TRUE if the method in particular has been cached 
  416. * @param string $method 
  417. * @return type 
  418. */ 
  419. function is_cached($method) 
  420. return isset($this->_method_map_cache[$method]); 
  421.  
  422.  
  423. /** 
  424. * Caches the path to the extension which provides a particular method 
  425. * @param string $klass 
  426. * @param string $method 
  427. */ 
  428. function _cache_method($klass, $method) 
  429. $this->_method_map_cache[$method] = $klass; 
  430.  
  431. /** 
  432. * Flushes the method cache 
  433. */ 
  434. function _flush_cache() 
  435. $this->_method_map_cache = array(); 
  436.  
  437.  
  438. /** 
  439. * Returns TRUE if the object provides the particular method 
  440. * @param string $method 
  441. * @return boolean 
  442. */ 
  443. function has_method($method) 
  444. $retval = FALSE; 
  445.  
  446. // Have we looked up this method before successfully? 
  447. if ($this->is_cached($method)) { 
  448. $retval = TRUE; 
  449.  
  450. // Is this a local PHP method? 
  451. elseif (method_exists($this, $method)) { 
  452. $retval = TRUE; 
  453.  
  454. // Is a mixin providing this method 
  455. elseif ($this->get_mixin_providing($method)) { 
  456. $retval = TRUE; 
  457.  
  458. elseif ($this->is_wrapper() && $this->wrapped_class_provides($method)) { 
  459. $retval = TRUE; 
  460.  
  461. return $retval; 
  462.  
  463. /** 
  464. * Executes a cached method 
  465. * @param string $method 
  466. * @param array $args 
  467. * @return mixed 
  468. */ 
  469. function _exec_cached_method($method, $args=array()) 
  470. $klass = $this->_method_map_cache[$method]; 
  471. $object = $this->_instantiate_mixin($klass); 
  472. $object->object = $this; 
  473. $reflection = new ReflectionMethod($object, $method); 
  474. return $reflection->invokeArgs($object, $args); 
  475.  
  476. /** 
  477. * Returns TRUE if the ExtensibleObject has decided to implement a 
  478. * particular interface 
  479. * @param string $interface 
  480. * @return boolean 
  481. */ 
  482. function implements_interface($interface) 
  483. return in_array($interface, $this->_interfaces); 
  484.  
  485. function get_class_definition_dir($parent=FALSE) 
  486. return dirname($this->get_class_definition_file($parent)); 
  487.  
  488. function get_class_definition_file($parent=FALSE) 
  489. $klass = $this->get_class_name($this); 
  490. $r = new ReflectionClass($klass); 
  491. if ($parent) { 
  492. $parent = $r->getParentClass(); 
  493. return $parent->getFileName(); 
  494. return $r->getFileName(); 
  495.  
  496. /** 
  497. * Returns get_class_methods() optionally limited by Mixin 
  498. * @param string (optional) Only show functions provided by a mixin 
  499. * @return array Results from get_class_methods() 
  500. */ 
  501. public function get_instance_methods($name = null) 
  502. if (is_string($name)) 
  503. $methods = array(); 
  504. foreach ($this->_method_map_cache as $method => $mixin) { 
  505. if ($name == get_class($mixin)) 
  506. $methods[] = $method; 
  507. return $methods; 
  508. } else { 
  509. $methods = get_class_methods($this); 
  510. foreach ($this->_mixins as $mixin) { 
  511. $methods = array_unique(array_merge($methods, get_class_methods($mixin))); 
  512. sort($methods); 
  513.  
  514. return $methods; 
  515.  
  516. function get_parent_mixin_providing($method, $return_obj=FALSE, $levels=1) 
  517. $disabled_mixins = array(); 
  518.  
  519. for ($i=0; $i<$levels; $i++) { 
  520. if (($klass = $this->get_mixin_providing($method))) { 
  521. $this->disable_mixin_for($method, $klass); 
  522. $disabled_mixins[] = $klass; 
  523.  
  524. // Get the method map cache 
  525. $orig_method_map = $this->_method_map_cache; 
  526. $this->_method_map_cache = (array)C_Pope_Cache::get( 
  527. array($this->context, $this->_mixin_priorities, $this->_disabled_map),  
  528. $this->_method_map_cache 
  529. ); 
  530.  
  531. $retval = $this->get_mixin_providing($method, $return_obj); 
  532.  
  533. // Re-enable mixins 
  534. foreach ($disabled_mixins as $klass) { 
  535. $this->enable_mixin_for($method, $klass); 
  536.  
  537. return $retval;