PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/pope/lib/class.extensibleobject.php

https://bitbucket.org/photocrati/nextgen-gallery
PHP | 840 lines | 460 code | 131 blank | 249 comment | 71 complexity | 09c3dd4df2a40678cd0536773fee1199 MD5 | raw file
Possible License(s): GPL-2.0, BSD-3-Clause
  1. <?php
  2. include_once('class.pope_cache.php');
  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, $skip_empty=FALSE)
  16. {
  17. if ($a2) {
  18. foreach ($a2 as $key => $value) {
  19. if ($skip_empty && $value === '' OR is_null($value)) continue;
  20. if (isset($a1[$key])) {
  21. if (is_array($value)) {
  22. $a1[$key] = $this->array_merge_assoc($a1[$key], $value);
  23. }
  24. else {
  25. $a1[$key] = $value;
  26. }
  27. }
  28. else $a1[$key] = $value;
  29. }
  30. }
  31. return $a1;
  32. }
  33. /**
  34. * Returns TRUE if a property is empty
  35. * @param string $var
  36. * @return boolean
  37. */
  38. function is_empty($var, $element=FALSE)
  39. {
  40. if (is_array($var) && $element) {
  41. if (isset($var[$element])) $var = $var[$element];
  42. else $var = FALSE;
  43. }
  44. return (is_null($var) OR (is_string($var) AND strlen($var) == 0) OR $var === FALSE);
  45. }
  46. }
  47. /**
  48. * An ExtensibleObject can be extended at runtime with methods from another
  49. * class.
  50. *
  51. * - Mixins may be added or removed at any time during runtime
  52. * - The path to the mixin is cached so that subsequent method calls are
  53. * faster
  54. * - Pre and post hooks can be added or removed at any time during runtime.
  55. * - Each method call has a list of associated properties that can be modified
  56. * by pre/post hooks, such as: return_value, run_pre_hooks, run_post_hooks, etc
  57. * - Methods can be replaced by other methods at runtime
  58. * - Objects can implement interfaces, and are constrained to implement all
  59. * methods as defined by the interface
  60. * - All methods are public. There's no added security by having private/protected
  61. * members, as monkeypatching can always expose any method. Instead, protect
  62. * your methods using obscurity. Conventionally, use an underscore to define
  63. * a method that's private to an API
  64. */
  65. class ExtensibleObject extends PopeHelpers
  66. {
  67. static $enforce_interfaces=TRUE;
  68. var $_mixins = array();
  69. var $_mixin_priorities = array();
  70. var $_method_map_cache = array();
  71. var $_disabled_map = array();
  72. var $_interfaces = array();
  73. var $_throw_error = TRUE;
  74. var $_wrapped_instance = FALSE;
  75. var $object = NULL;
  76. /**
  77. * Defines a new ExtensibleObject. Any subclass should call this constructor.
  78. * Subclasses are expected to provide the following:
  79. * define_instance() - adds extensions which provide instance methods
  80. * define_class() - adds extensions which provide static methods
  81. * initialize() - used to initialize the state of the object
  82. */
  83. function __construct()
  84. {
  85. // TODO This can be removed in the future. The Photocrati Theme currently requires this.
  86. $this->object = $this;
  87. $args = func_get_args();
  88. // Define the instance
  89. if (method_exists($this, 'define_instance'))
  90. {
  91. $reflection = new ReflectionMethod($this, 'define_instance');
  92. $reflection->invokeArgs($this, $args);
  93. }
  94. elseif (method_exists($this, 'define')) {
  95. $reflection = new ReflectionMethod($this, 'define');
  96. $reflection->invokeArgs($this, $args);
  97. }
  98. if (self::$enforce_interfaces) $this->_enforce_interface_contracts();
  99. if (!isset($args[0]) || $args[0] != __EXTOBJ_NO_INIT__) {
  100. // Initialize the state of the object
  101. if (method_exists($this, 'initialize')) {
  102. $reflection = new ReflectionMethod($this, 'initialize');
  103. $reflection->invokeArgs($this, $args);
  104. }
  105. }
  106. }
  107. /**
  108. * Adds an extension class to the object. The extension provides
  109. * methods for this class to expose as it's own
  110. * @param string $class
  111. */
  112. function add_mixin($class, $instantiate=FALSE)
  113. {
  114. $retval = TRUE;
  115. if (!$this->has_mixin($class)) {
  116. // We used to instantiate the class, but I figure
  117. // we might as well wait till the method is called to
  118. // save memory. Instead, the _call() method calls the
  119. // _instantiate_mixin() method below.
  120. $this->_mixins[$class] = NULL; // new $class();
  121. array_unshift($this->_mixin_priorities, $class);
  122. // Instantiate the mixin immediately, if requested
  123. if ($instantiate) $this->_instantiate_mixin($class);
  124. $this->_flush_cache();
  125. }
  126. else $retval = FALSE;
  127. return $retval;
  128. }
  129. /**
  130. * Determines if a mixin has been added to this class
  131. * @param string $klass
  132. * @return bool
  133. */
  134. function has_mixin($klass)
  135. {
  136. return array_key_exists($klass, $this->_mixins);
  137. }
  138. /**
  139. * Stores the instantiated class
  140. * @param string $class
  141. * @return mixed
  142. */
  143. function &_instantiate_mixin($class)
  144. {
  145. $retval = FALSE;
  146. if (isset($this->_mixins[$class]))
  147. $retval = $this->_mixins[$class];
  148. else {
  149. $obj= new $class();
  150. $obj->object = $this;
  151. $retval = $this->_mixins[$class] = &$obj;
  152. if (method_exists($obj, 'initialize')) $obj->initialize();
  153. unset($obj->object);
  154. }
  155. return $retval;
  156. }
  157. /**
  158. * Deletes an extension from the object. The methods provided by that
  159. * extension are no longer available for the object
  160. * @param string $class
  161. */
  162. function del_mixin($class)
  163. {
  164. unset($this->_mixins[$class]);
  165. $index = array_search($class, $this->_mixin_priorities);
  166. unset($this->_mixin_priorities[$index]);
  167. $this->_flush_cache();
  168. }
  169. function remove_mixin($class)
  170. {
  171. $this->del_mixin($class);
  172. }
  173. /**
  174. * Returns the Mixin which provides the specified method
  175. * @param string $method
  176. */
  177. function get_mixin_providing($method, $return_obj=FALSE)
  178. {
  179. $retval = FALSE;
  180. // If it's cached, then we've got it easy
  181. if ($this->is_cached($method)) {
  182. $klass = $this->_method_map_cache[$method];
  183. return $return_obj ? $this->_instantiate_mixin($klass) : $klass;
  184. }
  185. // Otherwise, we have to look it up
  186. else {
  187. foreach ($this->_mixin_priorities as $class_name) {
  188. if (method_exists($class_name, $method) && !$this->is_mixin_disabled_for($method, $class_name)) {
  189. $object = $this->_instantiate_mixin($class_name);
  190. $this->_cache_method($class_name, $method);
  191. $retval = $return_obj ? $object : $class_name;
  192. break;
  193. }
  194. elseif (!class_exists($class_name)) {
  195. throw new RuntimeException("{$class_name} does not exist.");
  196. }
  197. }
  198. }
  199. return $retval;
  200. }
  201. function is_mixin_disabled_for($method, $mixin_klass)
  202. {
  203. $retval = FALSE;
  204. if (isset($this->_disabled_map[$method])) {
  205. $retval = in_array($mixin_klass, $this->_disabled_map[$method]);
  206. }
  207. return $retval;
  208. }
  209. function disable_mixin_for($method, $mixin_klass)
  210. {
  211. if (!isset($this->_disabled_map[$method])) {
  212. $this->_disabled_map[$method] = array($mixin_klass);
  213. }
  214. else if (!in_array($mixin_klass, $this->_disabled_map[$method])) {
  215. array_push($this->_disabled_map[$method], $mixin_klass);
  216. }
  217. unset($this->_method_map_cache[$method]);
  218. }
  219. function enable_mixin_for($method, $mixin_klass)
  220. {
  221. if (isset($this->_disabled_map[$method])) {
  222. if (($index = array_search($mixin_klass, $this->_disabled_map[$method])) !== FALSE) {
  223. unset($this->_disabled_map[$method][$index]);
  224. }
  225. }
  226. }
  227. /**
  228. * When an ExtensibleObject is instantiated, it checks whether all
  229. * the registered extensions combined provide the implementation as required
  230. * by the interfaces registered for this object
  231. */
  232. function _enforce_interface_contracts()
  233. {
  234. $errors = array();
  235. foreach ($this->_interfaces as $i) {
  236. $r = new ReflectionClass($i);
  237. foreach ($r->getMethods() as $m) {
  238. if (!$this->has_method($m->name)) {
  239. $klass = $this->get_class_name($this);
  240. $errors[] = "`{$klass}` does not implement `{$m->name}` as required by `{$i}`";
  241. }
  242. }
  243. }
  244. if ($errors) throw new Exception(implode(". ", $errors));
  245. }
  246. /**
  247. * Implement a defined interface. Does the same as the 'implements' keyword
  248. * for PHP, except this method takes into account extensions
  249. * @param string $interface
  250. */
  251. function implement($interface)
  252. {
  253. $this->_interfaces[] = $interface;
  254. }
  255. /**
  256. * Wraps a class within an ExtensibleObject class.
  257. * @param string $klass
  258. * @param array callback, used to tell ExtensibleObject how to instantiate
  259. * the wrapped class
  260. */
  261. function wrap($klass, $callback=FALSE, $args=array())
  262. {
  263. if ($callback) {
  264. $this->_wrapped_instance = call_user_func($callback, $args);
  265. }
  266. else {
  267. $this->_wrapped_instance = new $klass();
  268. }
  269. }
  270. /**
  271. * Determines if the ExtensibleObject is a wrapper for an existing class
  272. */
  273. function is_wrapper()
  274. {
  275. return $this->_wrapped_instance ? TRUE : FALSE;
  276. }
  277. /**
  278. * Returns the name of the class which this ExtensibleObject wraps
  279. * @return string
  280. */
  281. function &get_wrapped_instance()
  282. {
  283. return $this->_wrapped_instance;
  284. }
  285. /**
  286. * Returns TRUE if the wrapped class provides the specified method
  287. */
  288. function wrapped_class_provides($method)
  289. {
  290. $retval = FALSE;
  291. // Determine if the wrapped class is another ExtensibleObject
  292. if (method_exists($this->_wrapped_instance, 'has_method')) {
  293. $retval = $this->_wrapped_instance->has_method($method);
  294. }
  295. elseif (method_exists($this->_wrapped_instance, $method)){
  296. $retval = TRUE;
  297. }
  298. return $retval;
  299. }
  300. /**
  301. * Provides a means of calling static methods, provided by extensions
  302. * @param string $method
  303. * @return mixed
  304. */
  305. static function get_class()
  306. {
  307. // Note: this function is static so $this is not defined
  308. $klass = self::get_class_name();
  309. $obj = new $klass(__EXTOBJ_STATIC__);
  310. return $obj;
  311. }
  312. /**
  313. * Gets the name of the ExtensibleObject
  314. * @return string
  315. */
  316. static function get_class_name($obj = null)
  317. {
  318. if ($obj)
  319. return get_class($obj);
  320. elseif (function_exists('get_called_class'))
  321. return get_called_class();
  322. else
  323. return get_class();
  324. }
  325. /**
  326. * Gets a property from a wrapped object
  327. * @param string $property
  328. * @return mixed
  329. */
  330. function __get($property)
  331. {
  332. $retval = NULL;
  333. if ($property == 'object') return $this;
  334. else if ($this->is_wrapper()) {
  335. try {
  336. $reflected_prop = new ReflectionProperty($this->_wrapped_instance, $property);
  337. // setAccessible method is only available for PHP 5.3 and above
  338. if (method_exists($reflected_prop, 'setAccessible')) {
  339. $reflected_prop->setAccessible(TRUE);
  340. }
  341. $retval = $reflected_prop->getValue($this->_wrapped_instance);
  342. }
  343. catch (ReflectionException $ex)
  344. {
  345. $retval = $this->_wrapped_instance->$property;
  346. }
  347. }
  348. return $retval;
  349. }
  350. /**
  351. * Determines if a property (dynamic or not) exists for the object
  352. * @param string $property
  353. * @return boolean
  354. */
  355. function __isset($property)
  356. {
  357. $retval = FALSE;
  358. if (property_exists($this, $property)) {
  359. $retval = isset($this->$property);
  360. }
  361. elseif ($this->is_wrapper() && property_exists($this->_wrapped_instance, $property)) {
  362. $retval = isset($this->$property);
  363. }
  364. return $retval;
  365. }
  366. /**
  367. * Sets a property on a wrapped object
  368. * @param string $property
  369. * @param mixed $value
  370. * @return mixed
  371. */
  372. function __set($property, $value)
  373. {
  374. $retval = NULL;
  375. if ($this->is_wrapper()) {
  376. try {
  377. $reflected_prop = new ReflectionProperty($this->_wrapped_instance, $property);
  378. // The property must be accessible, but this is only available
  379. // on PHP 5.3 and above
  380. if (method_exists($reflected_prop, 'setAccessible')) {
  381. $reflected_prop->setAccessible(TRUE);
  382. }
  383. $retval = &$reflected_prop->setValue($this->_wrapped_instance, $value);
  384. }
  385. // Sometimes reflection can fail. In that case, we need
  386. // some ingenuity as a failback
  387. catch (ReflectionException $ex) {
  388. $this->_wrapped_instance->$property = $value;
  389. $retval = &$this->_wrapped_instance->$property;
  390. }
  391. }
  392. else {
  393. $this->$property = $value;
  394. $retval = &$this->$property;
  395. }
  396. return $retval;
  397. }
  398. /**
  399. * Finds a method defined by an extension and calls it. However, execution
  400. * is a little more in-depth:
  401. * 1) Execute all global pre-hooks and any pre-hooks specific to the requested
  402. * method. Each method call has instance properties that can be set by
  403. * other hooks to modify the execution. For example, a pre hook can
  404. * change the 'run_pre_hooks' property to be false, which will ensure that
  405. * all other pre hooks will NOT be executed.
  406. * 2) Runs the method. Checks whether the path to the method has been cached
  407. * 3) Execute all global post-hooks and any post-hooks specific to the
  408. * requested method. Post hooks can access method properties as well. A
  409. * common usecase is to return the value of a post hook instead of the
  410. * actual method call. To do this, set the 'return_value' property.
  411. * @param string $method
  412. * @param array $args
  413. * @return mixed
  414. */
  415. function __call($method, $args)
  416. {
  417. $retval = NULL;
  418. if (($this->get_mixin_providing($method))) {
  419. $retval = $this->_exec_cached_method($method, $args);
  420. }
  421. // This is NOT a wrapped class, and no extensions provide the method
  422. else {
  423. // Perhaps this is a wrapper and the wrapped object
  424. // provides this method
  425. if ($this->is_wrapper() && $this->wrapped_class_provides($method))
  426. {
  427. $object = $this->add_wrapped_instance_method($method);
  428. $retval = call_user_func_array(
  429. array(&$object, $method),
  430. $args
  431. );
  432. }
  433. elseif ($this->_throw_error) {
  434. if (defined('POPE_DEBUG') && POPE_DEBUG)
  435. print_r(debug_backtrace());
  436. throw new Exception("`{$method}` not defined for " . get_class());
  437. }
  438. }
  439. return $retval;
  440. }
  441. /**
  442. * Adds the implementation of a wrapped instance method to the ExtensibleObject
  443. * @param string $method
  444. * @return Mixin
  445. */
  446. function add_wrapped_instance_method($method)
  447. {
  448. $retval = $this->get_wrapped_instance();
  449. // If the wrapped instance is an ExtensibleObject, then we don't need
  450. // to use reflection
  451. if (!is_subclass_of($this->get_wrapped_instance(), 'ExtensibleObject')) {
  452. $func = new ReflectionMethod($this->get_wrapped_instance(), $method);
  453. // Get the entire method definition
  454. $filename = $func->getFileName();
  455. $start_line = $func->getStartLine() - 1; // it's actually - 1, otherwise you wont get the function() block
  456. $end_line = $func->getEndLine();
  457. $length = $end_line - $start_line;
  458. $source = file($filename);
  459. $body = implode("", array_slice($source, $start_line, $length));
  460. $body = preg_replace("/^\s{0,}private|protected\s{0,}/", '', $body);
  461. // Change the context
  462. $body = str_replace('$this', '$this->object', $body);
  463. $body = str_replace('$this->object->object', '$this->object', $body);
  464. $body = str_replace('$this->object->$', '$this->object->', $body);
  465. // Define method for mixin
  466. $mixin_klass = "Mixin_AutoGen_{$method}";
  467. if (!class_exists($mixin_klass)) {
  468. eval("class {$mixin_klass} extends Mixin{
  469. {$body}
  470. }");
  471. }
  472. $this->add_mixin($mixin_klass);
  473. $retval = $this->_instantiate_mixin($mixin_klass);
  474. $this->_cache_method($mixin_klass, $method);
  475. }
  476. return $retval;
  477. }
  478. /**
  479. * Provides an alternative way to call methods
  480. */
  481. function call_method($method, $args=array())
  482. {
  483. if (method_exists($this, $method))
  484. {
  485. $reflection = new ReflectionMethod($this, $method);
  486. return $reflection->invokeArgs($this, array($args));
  487. }
  488. else {
  489. return $this->__call($method, $args);
  490. }
  491. }
  492. /**
  493. * Returns TRUE if the method in particular has been cached
  494. * @param string $method
  495. * @return type
  496. */
  497. function is_cached($method)
  498. {
  499. return isset($this->_method_map_cache[$method]);
  500. }
  501. /**
  502. * Caches the path to the extension which provides a particular method
  503. * @param string $klass
  504. * @param string $method
  505. */
  506. function _cache_method($klass, $method)
  507. {
  508. $this->_method_map_cache[$method] = $klass;
  509. }
  510. /**
  511. * Flushes the method cache
  512. */
  513. function _flush_cache()
  514. {
  515. $this->_method_map_cache = array();
  516. }
  517. /**
  518. * Returns TRUE if the object provides the particular method
  519. * @param string $method
  520. * @return boolean
  521. */
  522. function has_method($method)
  523. {
  524. $retval = FALSE;
  525. // Have we looked up this method before successfully?
  526. if ($this->is_cached($method)) {
  527. $retval = TRUE;
  528. }
  529. // Is this a local PHP method?
  530. elseif (method_exists($this, $method)) {
  531. $retval = TRUE;
  532. }
  533. // Is a mixin providing this method
  534. elseif ($this->get_mixin_providing($method)) {
  535. $retval = TRUE;
  536. }
  537. elseif ($this->is_wrapper() && $this->wrapped_class_provides($method)) {
  538. $retval = TRUE;
  539. }
  540. return $retval;
  541. }
  542. /**
  543. * Executes a cached method
  544. * @param string $method
  545. * @param array $args
  546. * @return mixed
  547. */
  548. function _exec_cached_method($method, $args=array())
  549. {
  550. $klass = $this->_method_map_cache[$method];
  551. $object = $this->_instantiate_mixin($klass);
  552. $object->object = $this;
  553. $reflection = new ReflectionMethod($object, $method);
  554. return $reflection->invokeArgs($object, $args);
  555. }
  556. /**
  557. * Returns TRUE if the ExtensibleObject has decided to implement a
  558. * particular interface
  559. * @param string $interface
  560. * @return boolean
  561. */
  562. function implements_interface($interface)
  563. {
  564. return in_array($interface, $this->_interfaces);
  565. }
  566. function get_class_definition_dir($parent=FALSE)
  567. {
  568. return dirname($this->get_class_definition_file($parent));
  569. }
  570. function get_class_definition_file($parent=FALSE)
  571. {
  572. $klass = $this->get_class_name($this);
  573. $r = new ReflectionClass($klass);
  574. if ($parent) {
  575. $parent = $r->getParentClass();
  576. return $parent->getFileName();
  577. }
  578. return $r->getFileName();
  579. }
  580. /**
  581. * Returns get_class_methods() optionally limited by Mixin
  582. *
  583. * @param string (optional) Only show functions provided by a mixin
  584. * @return array Results from get_class_methods()
  585. */
  586. public function get_instance_methods($name = null)
  587. {
  588. if (is_string($name))
  589. {
  590. $methods = array();
  591. foreach ($this->_method_map_cache as $method => $mixin) {
  592. if ($name == get_class($mixin))
  593. {
  594. $methods[] = $method;
  595. }
  596. }
  597. return $methods;
  598. } else {
  599. $methods = get_class_methods($this);
  600. foreach ($this->_mixins as $mixin) {
  601. $methods = array_unique(array_merge($methods, get_class_methods($mixin)));
  602. sort($methods);
  603. }
  604. return $methods;
  605. }
  606. }
  607. function get_parent_mixin_providing($method, $return_obj=FALSE, $levels=1)
  608. {
  609. $disabled_mixins = array();
  610. for ($i=0; $i<$levels; $i++) {
  611. if (($klass = $this->get_mixin_providing($method))) {
  612. $this->disable_mixin_for($method, $klass);
  613. $disabled_mixins[] = $klass;
  614. // Get the method map cache
  615. $orig_method_map = $this->_method_map_cache;
  616. $this->_method_map_cache = (array)C_Pope_Cache::get(
  617. array($this->context, $this->_mixin_priorities, $this->_disabled_map),
  618. $this->_method_map_cache
  619. );
  620. }
  621. }
  622. $retval = $this->get_mixin_providing($method, $return_obj);
  623. // Re-enable mixins
  624. foreach ($disabled_mixins as $klass) {
  625. $this->enable_mixin_for($method, $klass);
  626. }
  627. return $retval;
  628. }
  629. }
  630. /**
  631. * An mixin provides methods for an ExtensibleObject to use
  632. */
  633. class Mixin extends PopeHelpers
  634. {
  635. /**
  636. * The ExtensibleObject which called the extension's method
  637. * @var ExtensibleObject
  638. */
  639. var $object;
  640. /**
  641. * The name of the method called on the ExtensibleObject
  642. * @var type
  643. */
  644. var $method_called;
  645. /**
  646. * There really isn't any concept of 'parent' method. An ExtensibleObject
  647. * instance contains an ordered array of extension classes, which provides
  648. * the method implementations for the instance to use. Suppose that an
  649. * ExtensibleObject has two extension, and both have the same methods.The
  650. * last extension appears to 'override' the first extension. So, instead of calling
  651. * a 'parent' method, we're actually just calling an extension that was added sooner than
  652. * the one that is providing the current method implementation.
  653. */
  654. function call_parent($method)
  655. {
  656. $retval = NULL;
  657. // To simulate a 'parent' call, we remove the current mixin providing the
  658. // implementation.
  659. $klass = $this->object->get_mixin_providing($method);
  660. // Perform the routine described above...
  661. $this->object->disable_mixin_for($method, $klass);
  662. // Get the method map cache
  663. $orig_method_map = $this->object->_method_map_cache;
  664. $this->object->_method_map_cache = (array)C_Pope_Cache::get(
  665. array($this->object->context, $this->object->_mixin_priorities, $this->object->_disabled_map),
  666. $this->object->_method_map_cache
  667. );
  668. // Call anchor
  669. $args = func_get_args();
  670. // Remove $method parameter
  671. array_shift($args);
  672. // Execute the method
  673. $retval = $this->object->call_method($method, $args);
  674. // Cache the method map for this configuration of mixins
  675. C_Pope_Cache::set(
  676. array($this->object->context, $this->object->_mixin_priorities, $this->object->_disabled_map),
  677. $this->object->_method_map_cache
  678. );
  679. // Re-enable mixins;
  680. // $this->object->add_mixin($klass);
  681. $this->object->enable_mixin_for($method, $klass);
  682. // Restore the original method map
  683. $this->object->_method_map_cache = $orig_method_map;
  684. return $retval;
  685. }
  686. /**
  687. * Although is is preferrable to call $this->object->method(), sometimes
  688. * it's nice to use $this->method() instead.
  689. * @param string $method
  690. * @param array $args
  691. * @return mixed
  692. */
  693. function __call($method, $args)
  694. {
  695. if ($this->object->has_method($method)) {
  696. return call_user_func_array(array(&$this->object, $method), $args);
  697. }
  698. }
  699. /**
  700. * Although extensions can have state, it's probably more desirable to maintain
  701. * the state in the parent object to keep a sane environment
  702. * @param string $property
  703. * @return mixed
  704. */
  705. function __get($property)
  706. {
  707. return $this->object->$property;
  708. }
  709. }