PageRenderTime 60ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/ClassMixer.php

http://github.com/wellspringworldwide/PHP-ClassMixer
PHP | 976 lines | 482 code | 97 blank | 397 comment | 90 complexity | 6e577326020f5ac8dfdd2de6fed2b5bb MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /*******************************************************************************
  3. * PHP ClassMixer
  4. *
  5. * Authors:: anthony.gallagher@wellspringworldwide.com
  6. *
  7. * Copyright:: Copyright 2009, Wellspring Worldwide, LLC Inc. All Rights Reserved.
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. *
  20. *
  21. * Description:
  22. * ===========
  23. * ClassMixer was inspired by the 'Mixins for PHP' article
  24. * at http://www.advogato.org/article/470.html
  25. * and the Common Lisp Object System (CLOS).
  26. *
  27. *
  28. * Using Mixins:
  29. * ============
  30. * ClassMixer can be used to mix into a base class the functionality of multiple
  31. * other 'mixin' classes. The resulting new class contains all the methods and properties
  32. * of the base class, plus all the public methods and properties of the mixin
  33. * classes added to the mix.
  34. *
  35. *
  36. * Method Combinations:
  37. * ===================
  38. * In addition to adding mixin methods into a combined result class, ClassMixer allows
  39. * the mixed class to define how to combine the method results when more than one of the 'parent'
  40. * classes defines the same method.
  41. *
  42. *
  43. * Method Cutpoints:
  44. * ================
  45. * ClassMixer allows 'before' and 'after' cutpoints to be inserted into the mixed class' methods.
  46. * This limited form of Aspect-Oriented Programming allows mixin classes to define methods
  47. * will be called before or after the called method is executed.
  48. ******************************************************************************/
  49. /*******************************************************************************
  50. * Collection of helper functions for the ClassMixer
  51. ******************************************************************************/
  52. abstract class CM_Utils {
  53. /**
  54. * Computes whether the system has the minimum required PHP version.
  55. *
  56. * @param string $min_version_str
  57. * @return boolean
  58. */
  59. public static function php_min_version($min_version_str)
  60. {
  61. //Get the minimum version desired, and the PHP version of the system
  62. $min_version = explode('.', $min_version_str);
  63. $current_version = explode('.', PHP_VERSION);
  64. //Figure out if the system meets the minimum version
  65. if ($current_version[0] > $min_version[0]) {
  66. return true;
  67. }
  68. elseif ($current_version[0] == $min_version[0]) {
  69. if (empty($min_version[1]) || $min_version[1] == '*' ||
  70. $current_version[1] > $min_version[1]) {
  71. return true;
  72. }
  73. elseif ($current_version[1] == $min_version[1]) {
  74. if (empty($min_version[2]) || $min_version[2] == '*' ||
  75. $current_version[2] >= $min_version[2]) {
  76. return true;
  77. }
  78. }
  79. }
  80. return false;
  81. }
  82. }
  83. /*******************************************************************************
  84. * Collection of combinator functions for the ClassMixer
  85. ******************************************************************************/
  86. abstract class CM_Combinators {
  87. /**
  88. * A do-nothing function. Can be used to as a 'combinator' to
  89. * combine several functions that need to be called sequentially.
  90. */
  91. public static function execute() {}
  92. /**
  93. * This function will return the last of the result values returned by
  94. * the base classes of a mixed class
  95. *
  96. * @return <type> Last base class result
  97. */
  98. public static function last() {
  99. $args = func_get_args();
  100. if (!empty($args)) {
  101. $num_args = func_num_args();
  102. return $args[$num_args-1];
  103. }
  104. return null;
  105. }
  106. /**
  107. * Return the first of the results that is not empty
  108. *
  109. * @return type First non-empty result
  110. */
  111. public static function first_not_empty() {
  112. $args = func_get_args();
  113. foreach ($args as $arg) {
  114. if (!empty($arg)) {
  115. return $arg;
  116. }
  117. }
  118. return null;
  119. }
  120. /**
  121. * Aggregate the returns of the combined result values using OR
  122. *
  123. * @return boolean
  124. */
  125. public static function boolean_or() {
  126. $args = func_get_args();
  127. if (!empty($args)) {
  128. return array_reduce(create_function('$a, $b', 'return (is_null($a) ? false : $a) || $b;'), $args);
  129. }
  130. return null;
  131. }
  132. /**
  133. * Aggregate the returns of the combined result values using AND
  134. *
  135. * @return boolean
  136. */
  137. public static function boolean_and() {
  138. $args = func_get_args();
  139. if (!empty($args)) {
  140. return array_reduce(create_function('$a, $b', 'return (is_null($a) ? false : $a) && $b;'), $args);
  141. }
  142. return null;
  143. }
  144. }
  145. /*******************************************************************************
  146. * The ClassMixer class.
  147. * Contains functions to dynamically create classes by mixing
  148. * a set of existing classes.
  149. ******************************************************************************/
  150. abstract class ClassMixer {
  151. /***************************************************************************
  152. * PHP 5+ mixed method creation routines.
  153. **************************************************************************/
  154. /**
  155. * Generates code to be inserted in a generated method of the mixed class
  156. * that calls the BEFORE cutpoint of this generated method, if any.
  157. *
  158. * @param string $new_class
  159. * @param string $method
  160. * @param array $args_params
  161. * @return string Generated code to call the BEFORE cutpoint for the method.
  162. */
  163. private static function form_before_cutpoint_call5($new_class, $method, $args_params) {
  164. $has_args = (strlen($args_params) > 0);
  165. $bm_args_params = $has_args ? $args_params.", \$ret" : "\$ret";
  166. $ba_args_params = $has_args ? "'$new_class::$method', ".$args_params : "'$new_class::$method'";
  167. $cutpoint_code = "
  168. //Do before method calls
  169. if (in_array('BEFORE_ALL', get_class_methods('$new_class'))) {
  170. $new_class::BEFORE_ALL($ba_args_params);
  171. }
  172. if (in_array('BEFORE_$method', get_class_methods('$new_class'))) {
  173. \$ret = null;
  174. $new_class::\$__mixer_var =& \$ret;
  175. $new_class::BEFORE_$method($bm_args_params);
  176. if (!is_null($new_class::\$__mixer_var)) {
  177. return $new_class::\$__mixer_var;
  178. }
  179. }
  180. ";
  181. return $cutpoint_code;
  182. }
  183. /**
  184. * Generates code to be inserted in a generated method of the mixed class
  185. * that calls the BEFORE cutpoint of this generated method, if any.
  186. *
  187. * @param string $new_class
  188. * @param string $method
  189. * @param array $args_params
  190. * @return string Generated code to call the AFTER cutpoint for the method.
  191. */
  192. private static function form_after_cutpoint_call5($new_class, $method, $args_params) {
  193. $has_args = (strlen($args_params) > 0);
  194. $am_args_params = $has_args ? $args_params.", \$ret" : "\$ret";
  195. $aa_args_params = $has_args ? "'$new_class::$method', ".$args_params.", \$ret" : "'$new_class::$method', \$ret";
  196. $cutpoint_code = "
  197. //Do after method calls
  198. if (in_array('AFTER_$method', get_class_methods('$new_class'))) {
  199. $new_class::\$__mixer_var =& \$ret;
  200. $new_class::AFTER_$method($am_args_params);
  201. \$ret =& $new_class::\$__mixer_var;
  202. }
  203. if (in_array('AFTER_ALL', get_class_methods('$new_class'))) {
  204. $new_class::AFTER_ALL($aa_args_params);
  205. }
  206. ";
  207. return $cutpoint_code;
  208. }
  209. /**
  210. * Create the argument list for a method, given the reflection method.
  211. * This function analyzes the given method using the Reflection API to form two
  212. * strings:
  213. * (1) The method signature and
  214. * (2) A parameter list.
  215. * E.g. For a method with signature:
  216. * function foo($a, &$b, $c=5) { ... }
  217. * this function will return:
  218. * array('$a, &$b, $c=5', '$a, $b, $c')
  219. *
  220. * @param ReflectionMethod $reflect_method
  221. * @return array
  222. */
  223. public static function form_method_argument_list5($reflect_method) {
  224. $args_signature = array();
  225. $args_params = array();
  226. $reflect_params = $reflect_method->getParameters();
  227. //Obtain information about the parameter list of the method
  228. foreach ($reflect_params as $p) {
  229. //Get the name of the parameter
  230. $param_name = $p->getName();
  231. $args_params[] = '$'.$param_name;
  232. //Get the signature of the parameter to form the method signature
  233. $arg = '';
  234. $type_hint_class = $p->getClass();
  235. $type_hint = !is_null($type_hint_class) ? $type_hint_class->getName().' ' : '';
  236. $arg .= $type_hint;
  237. $ref = $p->isPassedByReference() ? '&' : '';
  238. $arg .= $ref;
  239. $arg .= '$'.$param_name;
  240. if ($p->isOptional()) {
  241. $arg .= '=';
  242. $arg .= var_export($p->getDefaultValue(), true);
  243. }
  244. $args_signature[] = $arg;
  245. }
  246. //Returns (1) a string replicating the method signature that can be used when
  247. // defining the mixed method, and (2) a string listing the names of the parameters
  248. // that can be used when calling the method.
  249. return array(implode(', ', $args_signature), implode(', ', $args_params));
  250. }
  251. /**
  252. * Creates a mixed method for the new mixed class.
  253. *
  254. * This function uses the Reflection API to obtain the parameter lists, which
  255. * properly finds parameters need to be passed by reference.
  256. * The generated code should also be faster than the equivalent PHP 4
  257. * method, since it does not need to use eval().
  258. *
  259. * @param string $new_class
  260. * @param string $method
  261. * @param array $bases
  262. * @param array $combinators
  263. * @param boolean $before_cutpoint
  264. * @param boolean $after_cutpoint
  265. * @return string Generated code for a method of the mixed class.
  266. */
  267. private static function form_class_method5($new_class, $method, $bases, $combinators=array(),
  268. $before_cutpoint=false, $after_cutpoint=false) {
  269. //When the method is a global cutpoint! Need to use old style PHP 4 mixed methods, as
  270. // these methods don't have an argument signature
  271. if (self::is_method_global_cutpoint($method)) {
  272. return self::form_class_method4($new_class, $method, $bases, $combinators,
  273. $before_cutpoint, $after_cutpoint);
  274. }
  275. //Get the combinator info
  276. list($combinator_name, $ordered_bases, $method_name, $method_modifiers) =
  277. self::parse_combinator_info($method, $bases, $combinators);
  278. //Get the method parameter list (this assumes that all the methods to be
  279. // combined have the same signature!)
  280. $b = $ordered_bases[0];
  281. $reflect_method = new ReflectionMethod($b, $method);
  282. //Get the access modifiers, if they have not been given
  283. if (is_null($method_modifiers)) {
  284. $is_protected = $reflect_method->isProtected() ? 'protected' : '';
  285. $is_static = $reflect_method->isStatic() ? 'static' : '';
  286. $method_modifiers = $is_protected.' '.$is_static;
  287. }
  288. //Get the return type (by value or reference)
  289. $return_type = $reflect_method->returnsReference() ? '&' : '';
  290. //Get the parameter list
  291. $args = self::form_method_argument_list5($reflect_method);
  292. $args_signature = $args[0];
  293. $args_params = $args[1];
  294. //Form the method signature
  295. $method_signature = "$method_modifiers function$return_type $method_name($args_signature)";
  296. //Create the before and after method calls, if any
  297. $is_method_cutpoint = self::is_method_cutpoint($method);
  298. $before_code = ($before_cutpoint && !$is_method_cutpoint) ?
  299. self::form_before_cutpoint_call5($new_class, $method, $args_params) : '';
  300. $after_code = ($after_cutpoint && !$is_method_cutpoint) ?
  301. self::form_after_cutpoint_call5($new_class, $method, $args_params) : '';
  302. $func_code = '';
  303. //By default, if no combinators, just execute the method call of the first base.
  304. if (is_null($combinator_name) || sizeof($ordered_bases) == 1) {
  305. $b = $ordered_bases[0];
  306. $func_code = "
  307. $method_signature {
  308. $before_code
  309. //Do method call
  310. \$ret =& $b::$method($args_params);
  311. $after_code
  312. //Return value
  313. return \$ret;
  314. }";
  315. }
  316. else {
  317. //Create the call string
  318. $func_array = array();
  319. foreach ($ordered_bases as $b) {
  320. $func_array[] = "$b::$method($args_params)";
  321. }
  322. $func_str = implode(', ', $func_array);
  323. $func_code = "
  324. $method_signature {
  325. $before_code
  326. //Do method call
  327. \$ret =& $combinator_name($func_str);
  328. $after_code
  329. //Return value
  330. return \$ret;
  331. }";
  332. }
  333. return $func_code;
  334. }
  335. /**
  336. * Generate a string with the variable definitions for the class
  337. *
  338. * @param array $mixins
  339. * @return string String of variable definitions
  340. */
  341. private static function form_class_variables5($mixins) {
  342. $props_arr = array();
  343. $props_arr['__mixer_var'] = 'public static $__mixer_var;';
  344. $props_arr['__mixer_mixins'] = 'public static $__mixer_mixins = '.var_export($mixins, true).';';
  345. foreach ($mixins as $mixin) {
  346. //Get the property array
  347. $mixin_ref = new ReflectionClass($mixin);
  348. $props = $mixin_ref->getProperties();
  349. $prop_defaults = $mixin_ref->getDefaultProperties();
  350. //Create the property definitions
  351. foreach($props as $prop){
  352. //Get the property name
  353. $prop_name = $prop->getName();
  354. //If it is already created, skip...
  355. if (isset($props_arr[$prop_name])) {
  356. continue;
  357. }
  358. //Don't copy over statics. In mixed class, need to fully qualify the
  359. // parent class when using statics. Besides, the reflection API
  360. // has a bug that does not copy statics well.
  361. if ($prop->isStatic()) {
  362. continue;
  363. }
  364. //Get the property value
  365. if (isset($prop_defaults[$prop_name])) {
  366. $prop_value = $prop_defaults[$prop_name];
  367. }
  368. else {
  369. $prop_value = null;
  370. }
  371. //Create the property
  372. if (is_null($prop_value)) {
  373. $props_arr[$prop_name] = "public \$$prop_name;";
  374. }
  375. else {
  376. $props_arr[$prop_name] = "public \$$prop_name = ".var_export($prop_value, true).";";
  377. }
  378. //Mark previously private and protected variables. Copying them over
  379. // as private is necessary so that they are accessible in the 'parent'
  380. // mixin class.
  381. if ($prop->isProtected()) {
  382. $props_arr[$prop_name] .= ' //was protected';
  383. }
  384. elseif($prop->isPrivate()) {
  385. $props_arr[$prop_name] .= ' //was private';
  386. }
  387. }
  388. }
  389. //Return the string of variables
  390. return implode("\n\t", $props_arr);
  391. }
  392. /***************************************************************************
  393. * PHP 4.2+ mixed method creation routines.
  394. **************************************************************************/
  395. /**
  396. * Generates code to be inserted in a generated method of the mixed class
  397. * that calls the BEFORE cutpoint of this generated method, if any.
  398. *
  399. * @param string $new_class
  400. * @param string $method
  401. * @return string
  402. */
  403. private static function form_before_cutpoint_call4($new_class, $method) {
  404. $cutpoint_code = "
  405. //Do before method calls
  406. \$has_args = (strlen(\$arg_str) > 0);
  407. if (in_array('BEFORE_ALL', get_class_methods('$new_class'))) {
  408. \$ba_arg_str = \$has_args ?
  409. \"'$new_class::$method', \".\$arg_str : \"'$new_class::$method'\";
  410. eval('$new_class::BEFORE_ALL('.\$ba_arg_str.');');
  411. }
  412. if (in_array('BEFORE_$method', get_class_methods('$new_class'))) {
  413. \$ret = null;
  414. eval('$new_class::BEFORE_$method('.\$arg_str.');');
  415. }
  416. ";
  417. return $cutpoint_code;
  418. }
  419. /**
  420. * Generates code to be inserted in a generated method of the mixed class
  421. * that calls the AFTER cutpoint of this generated method, if any.
  422. *
  423. * @param string $new_class
  424. * @param string $method
  425. * @return string
  426. */
  427. private static function form_after_cutpoint_call4($new_class, $method) {
  428. $cutpoint_code = "
  429. //Do after method call
  430. \$has_args = (strlen(\$arg_str) > 0);
  431. if (in_array('AFTER_$method', get_class_methods('$new_class'))) {
  432. eval('$new_class::AFTER_$method('.\$arg_str.');');
  433. }
  434. if (in_array('AFTER_ALL', get_class_methods('$new_class'))) {
  435. \$aa_arg_str = \$has_args ?
  436. \"'$new_class::$method', \".\$arg_str.\", \\\$ret\" : \"'$new_class::$method', \\\$ret\";
  437. eval('$new_class::AFTER_ALL('.\$aa_arg_str.');');
  438. }
  439. ";
  440. return $cutpoint_code;
  441. }
  442. /**
  443. * Form a string with the argument list for the methods called in mixer functions.
  444. * Needs to be public, because it is called from the mixed method.
  445. *
  446. * @param array $args
  447. * @return string
  448. */
  449. public static function form_method_argument_list4($args) {
  450. $argStrArr = array();
  451. foreach(array_keys($args) as $key) {
  452. $argStrArr[] = '$args['.$key.']';
  453. }
  454. return implode($argStrArr, ', ');
  455. }
  456. /**
  457. * Creates a mixed method for a mixin-based class.
  458. *
  459. * This function does not use the Reflection API introduced in
  460. * PHP 5, and it has limitations
  461. * with regards to the type of method that can be mixed.
  462. * Methods that accept reference variables are problematic. The mixed method loses
  463. * the reference, passing all arguments by value.
  464. *
  465. * @param string $new_class
  466. * @param string $method
  467. * @param array $bases
  468. * @param array $combinators
  469. * @param boolean $before_cutpoint
  470. * @param boolean $after_cutpoint
  471. * @return string Generated code for a method of the mixed class.
  472. */
  473. private static function form_class_method4($new_class, $method, $bases, $combinators=array(),
  474. $before_cutpoint=false, $after_cutpoint=false) {
  475. //Create the before and after method calls, if any
  476. $is_method_cutpoint = self::is_method_cutpoint($method);
  477. $before_code = ($before_cutpoint && !$is_method_cutpoint) ?
  478. self::form_before_cutpoint_call4($new_class, $method) : '';
  479. $after_code = ($after_cutpoint && !$is_method_cutpoint) ?
  480. self::form_after_cutpoint_call4($new_class, $method) : '';
  481. //Get the combinator information
  482. list($combinator_name, $ordered_bases, $method_name) = self::parse_combinator_info($method, $bases, $combinators);
  483. $func_code = '';
  484. //By default, if no combinators, just execute the method call of the first base.
  485. if (is_null($combinator_name) || sizeof($ordered_bases) == 1) {
  486. $b = $ordered_bases[0];
  487. $func_code = "
  488. function& $method_name() {
  489. //Get arguments for the method
  490. \$args = func_get_args();
  491. \$arg_str = ClassMixer::form_method_argument_list4(\$args);
  492. $before_code
  493. //Do method call
  494. eval('\$ret =& $b::$method('.\$arg_str.');');
  495. $after_code
  496. //Return value
  497. return \$ret;
  498. }";
  499. }
  500. else {
  501. $func_array = array();
  502. foreach ($ordered_bases as $b) {
  503. $func_array[] = "$b::$method("."'.\$arg_str.'".")";
  504. }
  505. $func_str = implode(', ', $func_array);
  506. $func_code = "
  507. function& $method_name() {
  508. //Get arguments for the method
  509. \$args = func_get_args();
  510. \$arg_str = ClassMixer::form_method_argument_list4(\$args);
  511. $before_code
  512. //Do method call
  513. eval('\$ret =& $combinator_name($func_str);');
  514. $after_code
  515. //Return value
  516. return \$ret;
  517. }";
  518. }
  519. return $func_code;
  520. }
  521. /**
  522. * Generate a string with the variable definitions for the class
  523. *
  524. * @param array $mixins
  525. * @return string String of variable definitions
  526. */
  527. private static function form_class_variables4($mixins) {
  528. $props_arr = array();
  529. $props_arr['__mixer_var'] = 'static $__mixer_var;';
  530. $props_arr['__mixer_mixins'] = 'static $__mixer_mixins = '.var_export($mixins, true).';';
  531. foreach ($mixins as $mixin) {
  532. foreach(get_class_vars($mixin) as $pub_var => $val) {
  533. //Already created, continue...
  534. if (isset($props_arr[$pub_var])) {
  535. continue;
  536. }
  537. //Create the class variable
  538. if (is_null($val)) {
  539. //No associated value, just add the class variable.
  540. $props_arr[$pub_var] = "var \$$pub_var;";
  541. }
  542. else {
  543. //There is an associated value, define the class variable and copy the value.
  544. $props_arr[$pub_var] = "var \$$pub_var = ".var_export($val, true).";";
  545. }
  546. }
  547. }
  548. //Return the string of variables
  549. return implode("\n\t", $props_arr);
  550. }
  551. /***************************************************************************
  552. * Interface functions to abstract the PHP version from the main mixer methods
  553. **************************************************************************/
  554. /**
  555. * Predicate to test if a method is a global cutpoint
  556. *
  557. * @param string $method
  558. * @return boolean True if the method is a global cutpoint.
  559. */
  560. private static function is_method_global_cutpoint($method) {
  561. return (strpos($method, 'BEFORE_ALL') === 0 || strpos($method, 'AFTER_ALL') === 0);
  562. }
  563. /**
  564. * Predicate to test if a method is a cutpoint
  565. *
  566. * @param string $method
  567. * @return boolean True if the method is a cutpoint.
  568. */
  569. private static function is_method_cutpoint($method) {
  570. return (strpos($method, 'BEFORE_') === 0 || strpos($method, 'AFTER_') === 0);
  571. }
  572. /**
  573. * Obtain the combinator information for a mixed method.
  574. * The combinator information can take two formats:
  575. * 1) A simple combinator that just specifies the combinator method to use, as a string
  576. * 2) A complex combinator that contains an array in the format
  577. * array('combinator_name', //The name of the combinator method to use,
  578. * array(*parents*), //An array with the names of which base classes to combine
  579. * 'method_name', //The name of the method in the mixed class
  580. * 'method_modifiers' //The method modifiers of the method in the mixed class
  581. * )
  582. * All of these parameters are optional. An undesired parameter can be set to null.
  583. *
  584. * @param string $method
  585. * @param array $bases
  586. * @param array $combinators
  587. * @return array Two-element array with the combinator method and the ordered list of bases
  588. */
  589. private static function parse_combinator_info($method, $bases, $combinators) {
  590. //If a combinator was given, parse the information.
  591. if (isset($combinators[$method])) {
  592. $combinator_info = $combinators[$method];
  593. if (is_array($combinator_info)) {
  594. //Obtain the format of our combinator array
  595. $size = sizeof($combinator_info);
  596. //(1) Get the name of the combinator function. If none, set it to null
  597. $combinator_name = isset($combinator_info[0]) ? $combinator_info[0] : null;
  598. //(2) Get the override parent bases to combine, if any
  599. if ($size > 1 && isset($combinator_info[1])) {
  600. $combinator_bases = $combinator_info[1];
  601. $ordered_bases = array_intersect($combinator_bases, $bases);
  602. }
  603. else {
  604. $ordered_bases = $bases;
  605. }
  606. //(3) Get the override method name, if any
  607. if ($size > 2 && isset($combinator_info[2])) {
  608. $method_name = $combinator_info[2];
  609. }
  610. else {
  611. $method_name = $method;
  612. }
  613. //(4) Get the override method modifiers, if any
  614. if ($size > 3 && isset($combinator_info[3])) {
  615. $method_modifiers = $combinator_info[3];
  616. }
  617. else {
  618. $method_modifiers = null;
  619. }
  620. }
  621. else {
  622. $combinator_name = $combinator_info;
  623. $ordered_bases = $bases;
  624. $method_name = $method;
  625. $method_modifiers = null;
  626. }
  627. return array($combinator_name, $ordered_bases, $method_name, $method_modifiers);
  628. }
  629. //No combinator, just return the bases and the combinator name
  630. return array(null, $bases, $method, null);
  631. }
  632. /**
  633. * Obtains the list of methods on this class that are eligible for mixing
  634. *
  635. * @param string $klass
  636. * @param boolean $is_parent
  637. * @return array List of methods eligible for mixing
  638. */
  639. private static function available_base_class_methods($klass, $is_parent=false) {
  640. $php5_available = CM_Utils::php_min_version('5');
  641. //Obtain the list of mixable methods
  642. if ($php5_available) {
  643. //Only allow all public methods for mixin classes, and public and protected
  644. // methods for the base class
  645. $available_methods = array();
  646. $reflect_klass = new ReflectionClass($klass);
  647. $reflect_methods = $reflect_klass->getMethods();
  648. foreach ($reflect_methods as $rm) {
  649. if ($rm->isAbstract() || ($is_parent && $rm->isFinal()) ||
  650. $rm->isConstructor() || $rm->isDestructor() ||
  651. $rm->isPrivate() || (!$is_parent && $rm->isProtected())) {
  652. continue;
  653. }
  654. $available_methods[] = $rm->getName();
  655. }
  656. return $available_methods;
  657. }
  658. else {
  659. //Get all methods (there were no access modifiers in PHP 4)
  660. return get_class_methods($klass);
  661. }
  662. }
  663. /**
  664. * Creates a mixed method for a mixin-based class.
  665. *
  666. * @param <type> $new_class
  667. * @param <type> $method
  668. * @param <type> $bases
  669. * @param <type> $combinators
  670. * @param <type> $before_cutpoint
  671. * @param <type> $after_cutpoint
  672. * @return <type>
  673. */
  674. private static function form_class_method($new_class, $method, $bases, $combinators=array(),
  675. $before_cutpoint=false, $after_cutpoint=false) {
  676. $php5_available = CM_Utils::php_min_version('5');
  677. if ($php5_available) {
  678. return self::form_class_method5($new_class, $method, $bases, $combinators,
  679. $before_cutpoint, $after_cutpoint);
  680. }
  681. else {
  682. return self::form_class_method4($new_class, $method, $bases, $combinators,
  683. $before_cutpoint, $after_cutpoint);
  684. }
  685. }
  686. /**
  687. * Generate a string with the variable definitions for the class
  688. *
  689. * @param array $mixins
  690. * @return string String of variable definitions
  691. */
  692. private static function form_class_variables($mixins) {
  693. $php5_available = CM_Utils::php_min_version('5');
  694. if ($php5_available) {
  695. return self::form_class_variables5($mixins);
  696. }
  697. else {
  698. return self::form_class_variables4($mixins);
  699. }
  700. }
  701. /***************************************************************************
  702. * Mixed class creation routines.
  703. **************************************************************************/
  704. /**
  705. * This is the core mixer function.
  706. * This function generates an eval-uable string that defines a new mixed class.
  707. * The mixed class combines the methods of the mixin classes with those
  708. * of the base class to produce a new class with all the methods of all
  709. * the 'parent' classes.
  710. *
  711. * Combinators:
  712. * Collisions of method names are resolved using the combinators. When
  713. * no combinator is given, and more than one class define a method,
  714. * the method of the first class will be used.
  715. *
  716. * Cutpoints:
  717. * BEFORE and AFTER cutpoints can be inserted on the methods specified
  718. * in the cutpoints arrays.
  719. *
  720. * @param string $new_class
  721. * @param string $base
  722. * @param array $mixins
  723. * @param array $combinators
  724. * @param array $before_cutpoints
  725. * @param array $after_cutpoints
  726. * @return string Generated mixed class.
  727. */
  728. public static function form_mixed_class($new_class, $base, $mixins, $combinators=array(),
  729. $before_cutpoints=array(), $after_cutpoints=array()) {
  730. //Check for PHP version
  731. if (!CM_Utils::php_min_version('4.2')) {
  732. throw Exception('ClassMixer requires PHP 4.2 or above');
  733. }
  734. //Get the interfaces implements by the base class and the mixins
  735. $interfaces = class_implements($base);
  736. foreach ($mixins as $mixin) {
  737. $interfaces = array_merge($interfaces, class_implements($mixin));
  738. }
  739. $str_interfaces = implode(', ', array_values($interfaces));
  740. $class_header = "
  741. class $new_class extends $base";
  742. if (strlen($str_interfaces) > 0) {
  743. // if we have interfaces add it to the class header
  744. $class_header .= " implements $str_interfaces";
  745. }
  746. //Add the mixin variables
  747. $str_var_code = self::form_class_variables($mixins);
  748. //Get the functions
  749. $funcs = array();
  750. $func_code = array();
  751. $based_on = array_merge(array($base), $mixins);
  752. foreach($based_on as $b) {
  753. foreach(self::available_base_class_methods($b, $b==$base) as $bm) {
  754. //Add the class as holding method $bm
  755. if (isset($funcs[$bm]))
  756. //Add the class $b as having method $bm to an existing array
  757. $funcs[$bm][] = $b;
  758. else {
  759. //Create an array that denotes that class $b has method $bm
  760. $funcs[$bm] = array($b);
  761. }
  762. }
  763. }
  764. //Get the names of all non-cutpoint functions
  765. $func_names = array();
  766. foreach (array_keys($funcs) as $func_name) {
  767. if (!self::is_method_cutpoint($func_name)) {
  768. $func_names[] = $func_name;
  769. }
  770. }
  771. //Add the combinators for the before and after cutpoints
  772. if ($before_cutpoints === true) {
  773. $before_cutpoints = $func_names;
  774. $before_cutpoints[] = 'ALL';
  775. }
  776. foreach ($before_cutpoints as $bc) {
  777. $bm = 'BEFORE_'.$bc;
  778. if (!array_key_exists($bm, $combinators)) {
  779. $combinators[$bm] = 'CM_Combinators::execute';
  780. }
  781. }
  782. if ($after_cutpoints === true) {
  783. $after_cutpoints = $func_names;
  784. $after_cutpoints[] = 'ALL';
  785. }
  786. foreach ($after_cutpoints as $ac) {
  787. $bm = 'AFTER_'.$ac;
  788. if (!array_key_exists($bm, $combinators)) {
  789. $combinators[$bm] = 'CM_Combinators::execute';
  790. }
  791. }
  792. //Create the methods for the mixed class
  793. foreach($funcs as $bm => $klasses) {
  794. //Allow before or after cutpoints for this method
  795. $before_cutpoint = in_array($bm, $before_cutpoints);
  796. $after_cutpoint = in_array($bm, $after_cutpoints);
  797. //We only need to create a method if it there are more than one parent class with
  798. // the given method, or the parent class with the method is not the base class
  799. if ($before_cutpoint || $after_cutpoint || sizeof($klasses) > 1 || $klasses[0] != $base) {
  800. $func_code[$bm] = self::form_class_method($new_class, $bm, $klasses, $combinators,
  801. $before_cutpoint, $after_cutpoint);
  802. }
  803. }
  804. $str_func_code = implode("\n", $func_code);
  805. //Start the class construction
  806. $code = "
  807. $class_header {
  808. $str_var_code
  809. $str_func_code
  810. }";
  811. return $code;
  812. }
  813. /**
  814. * Entry-point mixer function.
  815. * Simply evaluates the mixed class definition generated by form_mixed_class to
  816. * define the PHP class during runtime.
  817. *
  818. * @param string $new_class
  819. * @param string $base
  820. * @param array $mixins
  821. * @param array $combinators
  822. * @param array $before_cutpoints
  823. * @param array $after_cutpoints
  824. */
  825. public static function create_mixed_class($new_class, $base, $mixins, $combinators=array(),
  826. $before_cutpoints=array(), $after_cutpoints=array()) {
  827. $str_class = self::form_mixed_class($new_class, $base, $mixins, $combinators, $before_cutpoints, $after_cutpoints);
  828. eval($str_class);
  829. }
  830. /**
  831. * Second entry-point mixer function.
  832. * Loads a mixed class generated by form_mixed_class from an existing file, or
  833. * calls form_mixed_class and saves its output to the file
  834. * if it does not exists.
  835. * WARNING: The user running the PHP engine (typically Apache) must have
  836. * permissions to access this file.
  837. * If the file cannot be accessed, then fall-back to create_mixed_class
  838. *
  839. * @param string $mixed_class_file
  840. * @param boolean $rewrite
  841. * @param string $new_class
  842. * @param string $base
  843. * @param array $mixins
  844. * @param array $combinators
  845. * @param array $before_cutpoints
  846. * @param array $after_cutpoints
  847. */
  848. public static function require_mixed_class($mixed_class_file, $rewrite,
  849. $new_class, $base, $mixins, $combinators=array(),
  850. $before_cutpoints=array(), $after_cutpoints=array()) {
  851. try {
  852. //Check if we should write the file
  853. if ($rewrite || !file_exists($mixed_class_file)) {
  854. //Try to open the file for writing
  855. $fh = @fopen($mixed_class_file, 'w');
  856. if ($fh === false) {
  857. throw new Exception('ClassMixer could not write the cached class');
  858. }
  859. //Data to save to the file
  860. $header = <<<EOH
  861. <?php
  862. /**
  863. * This file was auto-generated by the ClassMixer. DO NOT EDIT MANUALLY.
  864. */
  865. EOH;
  866. $str_class = self::form_mixed_class($new_class, $base, $mixins, $combinators, $before_cutpoints, $after_cutpoints);
  867. //Save data
  868. fwrite($fh, $header);
  869. fwrite($fh, $str_class);
  870. fclose($fh);
  871. }
  872. require_once($mixed_class_file);
  873. }
  874. catch (Exception $e) {
  875. //OK, could not create the cached version. Just create it dynamically
  876. self::create_mixed_class($new_class, $base, $mixins, $combinators, $before_cutpoints, $after_cutpoints);
  877. }
  878. }
  879. }