PageRenderTime 54ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/pope/lib/class.extensibleobject.php

https://bitbucket.org/benkeen/nextgen-gallery
PHP | 1015 lines | 519 code | 159 blank | 337 comment | 76 complexity | cb659f99963dfc694752f58574128358 MD5 | raw file
  1. <?php
  2. define('__EXTOBJ_STATIC__', '__STATICALLY_CALLED__');
  3. define('__EXTOBJ_NO_INIT__', '__NO_INIT__');
  4. /**
  5. * Provides helper methods for Pope objects
  6. */
  7. class PopeHelpers
  8. {
  9. /**
  10. * Merges two associative arrays
  11. * @param array $a1
  12. * @param array $a2
  13. * @return array
  14. */
  15. function array_merge_assoc($a1, $a2)
  16. {
  17. foreach ($a2 as $key => $value) {
  18. if (isset($a1[$key])) {
  19. if (is_array($value)) {
  20. $a1[$key] = $this->array_merge_assoc($a1[$key], $value);
  21. }
  22. else {
  23. $a1[$key] = $value;
  24. }
  25. }
  26. else $a1[$key] = $value;
  27. }
  28. return $a1;
  29. }
  30. /**
  31. * Returns TRUE if a property is empty
  32. * @param string $var
  33. * @return boolean
  34. */
  35. function is_empty($var, $element=FALSE)
  36. {
  37. if (is_array($var) && $element) {
  38. if (isset($var[$element])) $var = $var[$element];
  39. else $var = FALSE;
  40. }
  41. return (is_null($var) OR (is_string($var) AND strlen($var) == 0) OR $var === FALSE);
  42. }
  43. }
  44. /**
  45. * An ExtensibleObject can be extended at runtime with methods from another
  46. * class.
  47. *
  48. * - Mixins may be added or removed at any time during runtime
  49. * - The path to the mixin is cached so that subsequent method calls are
  50. * faster
  51. * - Pre and post hooks can be added or removed at any time during runtime.
  52. * - Each method call has a list of associated properties that can be modified
  53. * by pre/post hooks, such as: return_value, run_pre_hooks, run_post_hooks, etc
  54. * - Methods can be replaced by other methods at runtime
  55. * - Objects can implement interfaces, and are constrained to implement all
  56. * methods as defined by the interface
  57. * - All methods are public. There's no added security by having private/protected
  58. * members, as monkeypatching can always expose any method. Instead, protect
  59. * your methods using obscurity. Conventionally, use an underscore to define
  60. * a method that's private to an API
  61. */
  62. class ExtensibleObject extends PopeHelpers
  63. {
  64. const METHOD_PROPERTY_RUN='run';
  65. const METHOD_PROPERTY_RUN_POST_HOOKS='run_post_hooks';
  66. const METHOD_PROPERTY_RUN_PRE_HOOKS='run_pre_hooks';
  67. const METHOD_PROPERTY_RETURN_VALUE='return_value';
  68. var $_mixins = array();
  69. var $_mixin_priorities = array();
  70. var $_pre_hooks = array();
  71. var $_global_pre_hooks = array();
  72. var $_global_post_hooks= array();
  73. var $_post_hooks = array();
  74. var $_method_map_cache = array();
  75. var $_interfaces = array();
  76. var $_overrides = array();
  77. var $_aliases = array();
  78. var $_method_properties = array();
  79. var $_throw_error = TRUE;
  80. var $_wrapped_class = FALSE;
  81. /**
  82. * Defines a new ExtensibleObject. Any subclass should call this constructor.
  83. * Subclasses are expected to provide the following:
  84. * define_instance() - adds extensions which provide instance methods
  85. * define_class() - adds extensions which provide static methods
  86. * initialize() - used to initialize the state of the object
  87. */
  88. function __construct()
  89. {
  90. $args = func_get_args();
  91. $define_instance = TRUE;
  92. $init_instance = TRUE;
  93. // The first argument could be a flag to ExtensibleObject
  94. // which indicates that only static-like methods will be called
  95. if (count($args) >= 1) {
  96. $first_arg = $args[0];
  97. if (is_string($first_arg)) {
  98. switch ($first_arg) {
  99. case __EXTOBJ_STATIC__:
  100. {
  101. $define_instance = FALSE;
  102. $init_instance = FALSE;
  103. if (method_exists($this, 'define_class')) {
  104. call_user_func_array(array($this, 'define_class'), $args);
  105. }
  106. elseif (method_exists($this, 'define_static')) {
  107. call_user_func_array(array($this, 'define_static'), $args);
  108. }
  109. break;
  110. }
  111. case __EXTOBJ_NO_INIT__:
  112. {
  113. $init_instance = FALSE;
  114. break;
  115. }
  116. }
  117. }
  118. }
  119. // Are we to define instance methods?
  120. if ($define_instance) {
  121. if (method_exists($this, 'define_instance')) {
  122. call_user_func_array(array($this, 'define_instance'), $args);
  123. }
  124. elseif (method_exists($this, 'define')) {
  125. call_user_func_array(array($this, 'define'), $args);
  126. }
  127. $this->_enforce_interface_contracts();
  128. if ($init_instance) {
  129. // Initialize the state of the object
  130. if (method_exists($this, 'initialize')) {
  131. call_user_func_array(array($this, 'initialize'), $args);
  132. }
  133. }
  134. }
  135. }
  136. /**
  137. * Adds an extension class to the object. The extension provides
  138. * methods for this class to expose as it's own
  139. * @param string $class
  140. */
  141. function add_mixin($class, $instantiate=FALSE)
  142. {
  143. // We used to instantiate the class, but I figure
  144. // we might as well wait till the method is called to
  145. // save memory. Instead, the _call() method calls the
  146. // _instantiate_mixin() method below.
  147. $this->_mixins[$class] = FALSE; // new $class();
  148. array_unshift($this->_mixin_priorities, $class);
  149. $this->_flush_cache();
  150. // Should we instantiate the object now?
  151. if ($instantiate) $this->_instantiate_mixin($class);
  152. }
  153. /**
  154. * Stores the instantiated class
  155. * @param string $class
  156. * @return mixed
  157. */
  158. function _instantiate_mixin($class)
  159. {
  160. $retval = FALSE;
  161. if ($this->_mixins[$class])
  162. $retval = $this->_mixins[$class];
  163. else {
  164. $obj= new $class();
  165. $obj->object = &$this;
  166. $retval = $this->_mixins[$class] = &$obj;
  167. if (method_exists($obj, 'initialize')) $obj->initialize();
  168. }
  169. return $retval;
  170. }
  171. /**
  172. * Deletes an extension from the object. The methods provided by that
  173. * extension are no longer available for the object
  174. * @param string $class
  175. */
  176. function del_mixin($class)
  177. {
  178. unset($this->_mixins[$class]);
  179. $index = array_search($class, $this->_mixin_priorities);
  180. if ($index !== FALSE) {
  181. unset($this->_mixin_priorities[$index]);
  182. $this->_flush_cache();
  183. }
  184. }
  185. function remove_mixin($class)
  186. {
  187. $this->del_mixin($class);
  188. }
  189. /**
  190. * Replaces an extension methods with that of another class.
  191. * @param string $method
  192. * @param string $class
  193. * @param string $new_method
  194. */
  195. function replace_method($method, $class, $new_method=FALSE)
  196. {
  197. if (!$new_method) $new_method = $method;
  198. $this->_overrides[$method] = $class;
  199. $this->add_pre_hook($method, $class, $new_method);
  200. $this->_flush_cache();
  201. }
  202. /**
  203. * Restores a method that was replaced by a former call to replace_method()
  204. * @param string $method
  205. */
  206. function restore_method($method)
  207. {
  208. $class = $this->_overrides[$method];
  209. unset($this->_overrides[$method]);
  210. $this->del_pre_hook($method, $class);
  211. $this->_flush_cache();
  212. }
  213. /**
  214. * Returns the Mixin which provides the specified method
  215. * @param string $method
  216. */
  217. function get_mixin_providing($method)
  218. {
  219. $retval = FALSE;
  220. // If it's cached, then we've got it easy
  221. if ($this->is_cached($method)) {
  222. $object = $this->_method_map_cache[$method];
  223. $retval = get_class($object);
  224. }
  225. // Otherwise, we have to look it up
  226. else {
  227. foreach ($this->_mixin_priorities as $klass) {
  228. $object = $this->_instantiate_mixin($klass);
  229. if (method_exists($object, $method)) {
  230. $retval = get_class($object);
  231. $this->_cache_method($method, $method);
  232. }
  233. }
  234. }
  235. return $retval;
  236. }
  237. /**
  238. * When an ExtensibleObject is instantiated, it checks whether all
  239. * the registered extensions combined provide the implementation as required
  240. * by the interfaces registered for this object
  241. */
  242. function _enforce_interface_contracts()
  243. {
  244. $errors = array();
  245. foreach ($this->_interfaces as $i) {
  246. $r = new ReflectionClass($i);
  247. foreach ($r->getMethods() as $m) {
  248. if (!$this->has_method($m->name)) {
  249. $class = get_class($this);
  250. $errors[] = "`{$class}` does not implement `{$m->name}` as required by `{$i}`";
  251. }
  252. }
  253. }
  254. if ($errors) throw new Exception(implode(". ", $errors));
  255. }
  256. /**
  257. * Implement a defined interface. Does the same as the 'implements' keyword
  258. * for PHP, except this method takes into account extensions
  259. * @param string $interface
  260. */
  261. function implement($interface)
  262. {
  263. $this->_interfaces[] = $interface;
  264. }
  265. /**
  266. * Adds a hook that gets executed before every method call
  267. * @param string $name
  268. * @param string $class
  269. * @param string $hook_method
  270. */
  271. function add_global_pre_hook($name, $class, $hook_method)
  272. {
  273. add_pre_hook('*', $name, $class, $hook_method);
  274. }
  275. /**
  276. * Adds a hook that will get executed before a particular method call
  277. * @param string $method
  278. * @param string $name
  279. * @param string $class
  280. * @param string $hook_method
  281. */
  282. function add_pre_hook($method, $name, $class, $hook_method=FALSE)
  283. {
  284. if (!$hook_method) $hook_method = $method;
  285. // Is this a global pre hook?
  286. if ($method == '*') {
  287. $this->_global_pre_hooks[$name] = array(
  288. new $class,
  289. $hook_method
  290. );
  291. }
  292. // This is a method-specific pre hook
  293. else {
  294. if (!isset($this->_pre_hooks[$method])) {
  295. $this->_pre_hooks[$method] = array();
  296. }
  297. $this->_pre_hooks[$method][$name] = array(
  298. new $class,
  299. $hook_method
  300. );
  301. }
  302. }
  303. /**
  304. * Adds a hook to be called after a particular method call
  305. * @param string $method
  306. * @param string $hook_name
  307. * @param string $class
  308. * @param string $hook_method
  309. */
  310. function add_post_hook($method, $hook_name, $class, $hook_method=FALSE)
  311. {
  312. // Is this a global post hook?
  313. if ($method == '*') {
  314. $this->_post_hooks[$hook_name] = array(
  315. new $class,
  316. $hook_method
  317. );
  318. }
  319. // This is a method-specific post hook
  320. else {
  321. if (!$hook_method) $hook_method = $method;
  322. if (!isset($this->_post_hooks[$method])) {
  323. $this->_post_hooks[$method] = array();
  324. }
  325. $this->_post_hooks[$method][$hook_name] = array(
  326. new $class,
  327. $hook_method
  328. );
  329. }
  330. }
  331. /**
  332. * Deletes a hook that's executed before the specified method
  333. * @param string $method
  334. * @param string $name
  335. */
  336. function del_pre_hook($method, $name)
  337. {
  338. unset($this->_pre_hooks[$method][$name]);
  339. }
  340. /**
  341. * Deletes all pre hooks registered
  342. **/
  343. function del_pre_hooks($method=FALSE)
  344. {
  345. if (!$method)
  346. $this->_pre_hooks = array();
  347. else
  348. unset($this->_pre_hooks[$method]);
  349. }
  350. /**
  351. * Deletes a hook that's executed after the specified method
  352. * @param string $method
  353. * @param string $name
  354. */
  355. function del_post_hook($method, $name)
  356. {
  357. unset($this->_post_hooks[$method][$name]);
  358. }
  359. /**
  360. * Deletes all post hooks
  361. */
  362. function del_post_hooks($method=FALSE)
  363. {
  364. if (!$method)
  365. $this->_post_hooks = array();
  366. else
  367. unset($this->_post_hooks[$method]);
  368. }
  369. /**
  370. * Wraps a class within an ExtensibleObject class.
  371. * @param string $klass
  372. * @param array callback, used to tell ExtensibleObject how to instantiate
  373. * the wrapped class
  374. */
  375. function wrap($klass, $callback=FALSE, $args=array())
  376. {
  377. if ($callback) {
  378. $this->_wrapped_class = call_user_func($callback, $args);
  379. }
  380. else {
  381. $this->_wrapped_class = new $klass();
  382. }
  383. }
  384. /**
  385. * Determines if the ExtensibleObject is a wrapper for an existing class
  386. */
  387. function is_wrapper()
  388. {
  389. return $this->_wrapped_class ? TRUE : FALSE;
  390. }
  391. /**
  392. * Returns the name of the class which this ExtensibleObject wraps
  393. * @return string
  394. */
  395. function &get_wrapped_class()
  396. {
  397. return $this->_wrapped_class;
  398. }
  399. /**
  400. * Returns TRUE if the wrapped class provides the specified method
  401. */
  402. function wrapped_class_provides($method)
  403. {
  404. $retval = FALSE;
  405. // Determine if the wrapped class is another ExtensibleObject
  406. if (method_exists($this->_wrapped_class, 'has_method')) {
  407. $retval = $this->_wrapped_class->has_method($method);
  408. }
  409. else if (method_exists($this->_wrapped_class, $method)){
  410. $retval = TRUE;
  411. }
  412. return $retval;
  413. }
  414. /**
  415. * Provides a means of calling static methods, provided by extensions
  416. * @param string $method
  417. * @return mixed
  418. */
  419. static function get_class()
  420. {
  421. $klass = get_class();
  422. $obj = new $klass(__EXTOBJ_STATIC__);
  423. return $obj;
  424. }
  425. /**
  426. * Gets a property from a wrapped object
  427. * @param string $property
  428. * @return mixed
  429. */
  430. function __get($property)
  431. {
  432. if ($this->is_wrapper()) {
  433. return $this->_wrapped_class->$property;
  434. }
  435. else return NULL;
  436. }
  437. /**
  438. * Sets a property on a wrapped object
  439. * @param string $property
  440. * @param mixed $value
  441. * @return mixed
  442. */
  443. function __set($property, $value)
  444. {
  445. if ($this->is_wrapper()) {
  446. return $this->_wrapped_class->$property = $value;
  447. }
  448. else {
  449. return $this->$property = $value;
  450. }
  451. }
  452. /**
  453. * Finds a method defined by an extension and calls it. However, execution
  454. * is a little more in-depth:
  455. * 1) Execute all global pre-hooks and any pre-hooks specific to the requested
  456. * method. Each method call has instance properties that can be set by
  457. * other hooks to modify the execution. For example, a pre hook can
  458. * change the 'run_pre_hooks' property to be false, which will ensure that
  459. * all other pre hooks will NOT be executed.
  460. * 2) Runs the method. Checks whether the path to the method has been cached
  461. * 3) Execute all global post-hooks and any post-hooks specific to the
  462. * requested method. Post hooks can access method properties as well. A
  463. * common usecase is to return the value of a post hook instead of the
  464. * actual method call. To do this, set the 'return_value' property.
  465. * @param string $method
  466. * @param array $args
  467. * @return mixed
  468. */
  469. function __call($method, $args)
  470. {
  471. $this->clear_method_properties($method);
  472. // Run pre hooks?
  473. if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN_PRE_HOOKS)) {
  474. // Combine global and method-specific pre hooks
  475. $prehooks = $this->_global_pre_hooks;
  476. if (isset($this->_pre_hooks[$method])) {
  477. $prehooks = array_merge($prehooks, $this->_pre_hooks[$method]);
  478. }
  479. // Apply each hook
  480. foreach ($prehooks as $hook_name => $hook) {
  481. $this->_run_prehook($hook_name, $method, $hook[0], $hook[1], $args);
  482. }
  483. }
  484. // Are we to run the actual method? A pre hook might have told us
  485. // not to
  486. if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN) &&
  487. !isset($this->_overrides[$method])) {
  488. // Try to fetch the method from the cache
  489. if ($this->is_cached($method)) {
  490. $this->set_method_property(
  491. $method,
  492. self::METHOD_PROPERTY_RETURN_VALUE,
  493. $this->_exec_cached_method($method, $args)
  494. );
  495. }
  496. // No cached method exists.
  497. // Iterate through each extension, retrieving the most recently
  498. // added extensions first and find the method
  499. else {
  500. $found = FALSE;
  501. // Perhaps an extension provides this method
  502. foreach ($this->_mixin_priorities as $klass) {
  503. $object = $this->_instantiate_mixin($klass);
  504. if (method_exists($object, $method)) {
  505. // Cache the class which provides this method, to make
  506. // lookups easier the next time around
  507. $this->_cache_method($object, $method);
  508. // Calls the method and sets the return value
  509. $this->set_method_property(
  510. $method,
  511. self::METHOD_PROPERTY_RETURN_VALUE,
  512. call_user_func_array(array($object, $method), $args)
  513. );
  514. $found = TRUE;
  515. break;
  516. }
  517. }
  518. // This is NOT a wrapped class, and no extensions provide the method
  519. if (!$found){
  520. // Perhaps this is a wrapper and the wrapped object
  521. // provides this method
  522. if ($this->is_wrapper() && $this->wrapped_class_provides($method)) {
  523. $found = TRUE;
  524. $this->set_method_property(
  525. $method,
  526. self::METHOD_PROPERTY_RETURN_VALUE,
  527. call_user_func_array(array($this->_wrapped_class, $method), $args)
  528. );
  529. }
  530. elseif ($this->_throw_error) throw new Exception("`{$method}` not defined for ".get_class());
  531. else return $found;
  532. }
  533. }
  534. }
  535. // Are we to run post hooks? A pre hook might have told us not to
  536. if ($this->get_method_property($method, self::METHOD_PROPERTY_RUN_POST_HOOKS)) {
  537. // Combine global and method-specific post hooks
  538. $posthooks = $this->_global_post_hooks;
  539. if (isset($this->_post_hooks[$method])) {
  540. $posthooks = array_merge($posthooks, $this->_post_hooks[$method]);
  541. }
  542. // Apply each hook
  543. foreach ($posthooks as $hook_name => $hook) {
  544. $this->_run_post_hook($hook_name, $method, $hook[0], $hook[1], $args);
  545. }
  546. }
  547. return $this->get_method_property($method, self::METHOD_PROPERTY_RETURN_VALUE);
  548. }
  549. /**
  550. * Provides an alternative way to call methods
  551. */
  552. function call_method($method, $args=array())
  553. {
  554. if (method_exists($this, $method))
  555. return call_user_func_array(array(&$this, $method), $args);
  556. else
  557. return $this->__call($method, $args);
  558. }
  559. /**
  560. * Returns TRUE if the method in particular has been cached
  561. * @param string $method
  562. * @return type
  563. */
  564. function is_cached($method)
  565. {
  566. return isset($this->_method_map_cache[$method]);
  567. }
  568. /**
  569. * Caches the path to the extension which provides a particular method
  570. * @param string $object
  571. * @param string $method
  572. */
  573. function _cache_method($object, $method)
  574. {
  575. $this->_method_map_cache[$method] = $object;
  576. }
  577. /**
  578. * Flushes the method cache
  579. */
  580. function _flush_cache()
  581. {
  582. $this->_method_map_cache = array();
  583. }
  584. /**
  585. * Returns TRUE if the object provides the particular method
  586. * @param string $method
  587. * @return boolean
  588. */
  589. function has_method($method)
  590. {
  591. $retval = FALSE;
  592. if (!$this->is_cached($method)) {
  593. if ($this->is_wrapper() && $this->wrapped_class_provides($method)) $retval = TRUE;
  594. elseif (method_exists($this, $method)) $retval = TRUE;
  595. else{
  596. foreach ($this->_mixin_priorities as $klass) {
  597. $object = $this->_instantiate_mixin($klass);
  598. if (method_exists($object, $method)) {
  599. $retval = TRUE;
  600. $this->_cache_method($object, $method);
  601. break;
  602. }
  603. }
  604. }
  605. }
  606. else $retval = TRUE;
  607. return $retval;
  608. }
  609. /**
  610. * Runs a particular pre hook for the specified method. The return value
  611. * is assigned to the "[hook_name]_prehook_retval" method property
  612. * @param string $hook_name
  613. * @param string $method_called
  614. * @param Ext $object
  615. * @param string $hook_method
  616. *
  617. */
  618. function _run_prehook($hook_name, $method_called, $object, $hook_method, $args=array())
  619. {
  620. $object->object = &$this;
  621. $object->method_called = $method_called;
  622. // Are we STILL to execute pre hooks? A pre-executed hook
  623. // might have changed this
  624. if ($this->get_method_property($method_called, 'run_pre_hooks')) {
  625. $this->set_method_property($method_called, $hook_name.'_prehook_retval',
  626. call_user_func_array(array(&$object, $hook_method),$args)
  627. );
  628. }
  629. }
  630. /**
  631. * Runs the specified post hook for the specified method
  632. * @param string $hook_name
  633. * @param string $method_called
  634. * @param Ext $object
  635. * @param string $hook_method
  636. */
  637. function _run_post_hook($hook_name, $method_called, $object, $hook_method, $args=array())
  638. {
  639. $object->object = &$this;
  640. $object->method_called = $method_called;
  641. // Are we STILL to execute pre hooks? A pre-executed hook
  642. // might have changed this
  643. if ($this->get_method_property($method_called, 'run_post_hooks')) {
  644. $this->set_method_property($method_called, $hook_name.'_post_hook_retval',
  645. call_user_func_array(array(&$object, $hook_method), $args)
  646. );
  647. }
  648. }
  649. /**
  650. * Returns TRUE if a pre-hook has been registered for the specified method
  651. * @param string $method
  652. * @return boolean
  653. */
  654. function have_prehook_for($method)
  655. {
  656. return isset($this->_pre_hooks[$method]);
  657. }
  658. /**
  659. * Returns TRUE if a posthook has been registered for the specified method
  660. * @param string $method
  661. * @return boolean
  662. */
  663. function have_posthook_for($method)
  664. {
  665. return isset($this->_post_hooks[$method]);
  666. }
  667. /**
  668. * Executes a cached method
  669. * @param string $method
  670. * @param array $args
  671. * @return mixed
  672. */
  673. function _exec_cached_method($method, $args=array())
  674. {
  675. $object = $this->_method_map_cache[$method];
  676. $object->object = &$this;
  677. return call_user_func_array(array(&$object, $method), $args);
  678. }
  679. /**
  680. * Sets the value of a method property
  681. * @param string $method
  682. * @param string $property
  683. * @param mixed $value
  684. */
  685. function set_method_property($method, $property, $value)
  686. {
  687. if (!isset($this->_method_properties[$method])) {
  688. $this->_method_properties[$method] = array();
  689. }
  690. $this->_method_properties[$method][$property] = $value;
  691. }
  692. /**
  693. * Gets the value of a method property
  694. * @param string $method
  695. * @param string $property
  696. */
  697. function get_method_property($method, $property)
  698. {
  699. $retval = FALSE;
  700. if (isset($this->_method_properties[$method][$property])) {
  701. $retval = $this->_method_properties[$method][$property];
  702. }
  703. return $retval;
  704. }
  705. /**
  706. * Clears all method properties to have their default values. This is called
  707. * before every method call (before pre-hooks)
  708. * @param string $method
  709. */
  710. function clear_method_properties($method)
  711. {
  712. $this->_method_properties[$method] = array(
  713. 'run' => TRUE,
  714. 'run_pre_hooks' => TRUE,
  715. 'run_post_hooks' => TRUE
  716. );
  717. }
  718. /**
  719. * Returns TRUE if the ExtensibleObject has decided to implement a
  720. * particular interface
  721. * @param string $interface
  722. * @return boolean
  723. */
  724. function implements_interface($interface)
  725. {
  726. return in_array($interface, $this->_interfaces);
  727. }
  728. function get_class_definition_dir($parent=FALSE)
  729. {
  730. return dirname($this->get_class_definition_file($parent));
  731. }
  732. function get_class_definition_file($parent=FALSE)
  733. {
  734. $klass = get_class($this);
  735. $r = new ReflectionClass($klass);
  736. if ($parent) {
  737. $parent = $r->getParentClass();
  738. return $parent->getFileName();
  739. }
  740. return $r->getFileName();
  741. }
  742. }
  743. /**
  744. * An mixin provides methods for an ExtensibleObject to use
  745. */
  746. class Mixin extends PopeHelpers
  747. {
  748. /**
  749. * The ExtensibleObject which called the extension's method
  750. * @var ExtensibleObject
  751. */
  752. var $object;
  753. /**
  754. * The name of the method called on the ExtensibleObject
  755. * @var type
  756. */
  757. var $method_called;
  758. /**
  759. * There really isn't any concept of 'parent' method. An ExtensibleObject
  760. * instance contains an ordered array of extension classes, which provides
  761. * the method implementations for the instance to use. Suppose that an
  762. * ExtensibleObject has two extension, and both have the same methods.The
  763. * last extension appears to 'override' the first extension. So, instead of calling
  764. * a 'parent' method, we're actually just calling an extension that was added sooner than
  765. * the one that is providing the current method implementation.
  766. */
  767. function call_parent($method=FALSE)
  768. {
  769. $retval = NULL;
  770. // To simulate a 'parent' call, we remove the current extension from the
  771. // ExtensibleObject that is providing the method's implementation, re-emit
  772. // the call on the instance to trigger the implementation from the previously
  773. // added extension, and then restore things by re-adding the current extension.
  774. // It's complicated, but it works.
  775. // We need to determine the name of the extension. Because PHP 5.2 is
  776. // missing get_called_class(), we have to look it up in the backtrace
  777. $backtrace = debug_backtrace();
  778. $klass = get_class($backtrace[0]['object']);
  779. // Because we already have the backtrace(), we'll let the user omit specifying
  780. // the name of the called method and look it up using the backtrace as well
  781. if (!$method) $method = $backtrace[1]['function'];
  782. if (strpos($method, '::') !== FALSE) $method = array_pop(explode('::', $method));
  783. // Perform the algorithm stated at the beginning of this function
  784. // We also clone the object to ensure that method properties can be
  785. // changed in another mixin's implementation of the method being called.
  786. $new_obj = clone $this->object;
  787. $new_obj->del_pre_hooks($method);
  788. $new_obj->del_post_hooks($method);
  789. $new_obj->del_mixin($klass);
  790. $args = func_get_args();
  791. array_shift($args);
  792. $retval = $new_obj->call_method($method, $args);
  793. unset($new_obj);
  794. return $retval;
  795. }
  796. /**
  797. * Although is is preferrable to call $this->object->method(), sometimes
  798. * it's nice to use $this->method() instead.
  799. * @param string $method
  800. * @param array $args
  801. * @return mixed
  802. */
  803. function __call($method, $args)
  804. {
  805. if ($this->object->has_method($method)) {
  806. return call_user_func_array(array(&$this->object, $method), $args);
  807. }
  808. }
  809. /**
  810. * Although extensions can have state, it's probably more desirable to maintain
  811. * the state in the parent object to keep a sane environment
  812. * @param string $property
  813. * @return mixed
  814. */
  815. function __get($property)
  816. {
  817. return $this->object->$property;
  818. }
  819. }
  820. /**
  821. * An extension which has the purpose of being used as a hook
  822. */
  823. class Hook extends Mixin
  824. {
  825. // Similiar to a mixin's call_parent method.
  826. // If a hook needs to call the method that it applied the
  827. // Hook n' Anchor pattern to, then this method should be called
  828. function call_anchor()
  829. {
  830. $retval = NULL;
  831. /**
  832. * @var ExtensibleObject $new_obj
  833. */
  834. $new_obj = clone $this->object;
  835. $new_obj->del_pre_hooks($this->method_called);
  836. $new_obj->del_post_hooks($this->method_called);
  837. // Call anchor
  838. $args = func_get_args();
  839. $retval = $new_obj->call_method($this->method_called, $args);
  840. unset($new_obj);
  841. return $retval;
  842. }
  843. /**
  844. * Provides an alias for call_anchor, as there's no parent
  845. * to call in the context of a hook.
  846. * @param string $method, ignored
  847. */
  848. function call_parent($method = FALSE)
  849. {
  850. $args = func_get_args();
  851. return call_user_func_array(
  852. array(&$this, 'call_anchor'),
  853. $args
  854. );
  855. }
  856. };