IntrospectionHelper.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. <?php
  2. /*
  3. * $Id: IntrospectionHelper.php 905 2010-10-05 16:28:03Z mrook $
  4. *
  5. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  6. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  7. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  8. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  9. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  10. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  11. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  12. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  13. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  14. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  15. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  16. *
  17. * This software consists of voluntary contributions made by many individuals
  18. * and is licensed under the LGPL. For more information please see
  19. * <http://phing.info>.
  20. */
  21. include_once 'phing/types/Reference.php';
  22. include_once 'phing/types/Path.php';
  23. include_once 'phing/util/StringHelper.php';
  24. /**
  25. * Helper class that collects the methods that a task or nested element
  26. * holds to set attributes, create nested elements or hold PCDATA
  27. * elements.
  28. *
  29. *<ul>
  30. * <li><strong>SMART-UP INLINE DOCS</strong></li>
  31. * <li><strong>POLISH-UP THIS CLASS</strong></li>
  32. *</ul>
  33. *
  34. * @author Andreas Aderhold <andi@binarycloud.com>
  35. * @author Hans Lellelid <hans@xmpl.org>
  36. * @copyright © 2001,2002 THYRELL. All rights reserved
  37. * @version $Revision: 905 $
  38. * @package phing
  39. */
  40. class IntrospectionHelper {
  41. /**
  42. * Holds the attribute setter methods.
  43. *
  44. * @var array string[]
  45. */
  46. private $attributeSetters = array();
  47. /**
  48. * Holds methods to create nested elements.
  49. *
  50. * @var array string[]
  51. */
  52. private $nestedCreators = array();
  53. /**
  54. * Holds methods to store configured nested elements.
  55. *
  56. * @var array string[]
  57. */
  58. private $nestedStorers = array();
  59. /**
  60. * Map from attribute names to nested types.
  61. */
  62. private $nestedTypes = array();
  63. /**
  64. * New idea in phing: any class can register certain
  65. * keys -- e.g. "task.current_file" -- which can be used in
  66. * task attributes, if supported. In the build XML these
  67. * are referred to like this:
  68. * <regexp pattern="\n" replace="%{task.current_file}"/>
  69. * In the type/task a listener method must be defined:
  70. * function setListeningReplace($slot) {}
  71. * @var array string[]
  72. */
  73. private $slotListeners = array();
  74. /**
  75. * The method to add PCDATA stuff.
  76. *
  77. * @var string Method name of the addText (redundant?) method, if class supports it :)
  78. */
  79. private $methodAddText = null;
  80. /**
  81. * The Class that's been introspected.
  82. *
  83. * @var object
  84. * @access private
  85. */
  86. private $bean;
  87. /**
  88. * The cache of IntrospectionHelper classes instantiated by getHelper().
  89. * @var array IntrospectionHelpers[]
  90. */
  91. private static $helpers = array();
  92. /**
  93. * Factory method for helper objects.
  94. *
  95. * @param string $class The class to create a Helper for
  96. */
  97. public static function getHelper($class) {
  98. if (!isset(self::$helpers[$class])) {
  99. self::$helpers[$class] = new IntrospectionHelper($class);
  100. }
  101. return self::$helpers[$class];
  102. }
  103. /**
  104. * This function constructs a new introspection helper for a specific class.
  105. *
  106. * This method loads all methods for the specified class and categorizes them
  107. * as setters, creators, slot listeners, etc. This way, the setAttribue() doesn't
  108. * need to perform any introspection -- either the requested attribute setter/creator
  109. * exists or it does not & a BuildException is thrown.
  110. *
  111. * @param string $bean The classname for this IH.
  112. */
  113. function __construct($class) {
  114. $this->bean = new ReflectionClass($class);
  115. //$methods = get_class_methods($bean);
  116. foreach($this->bean->getMethods() as $method) {
  117. if ($method->isPublic()) {
  118. // We're going to keep case-insensitive method names
  119. // for as long as we're allowed :) It makes it much
  120. // easier to map XML attributes to PHP class method names.
  121. $name = strtolower($method->getName());
  122. // There are a few "reserved" names that might look like attribute setters
  123. // but should actually just be skipped. (Note: this means you can't ever
  124. // have an attribute named "location" or "tasktype" or a nested element named "task".)
  125. if ($name === "setlocation" || $name === "settasktype" || $name === "addtask") {
  126. continue;
  127. }
  128. if ($name === "addtext") {
  129. $this->methodAddText = $method;
  130. } elseif (strpos($name, "setlistening") === 0) {
  131. // Phing supports something unique called "RegisterSlots"
  132. // These are dynamic values that use a basic slot system so that
  133. // classes can register to listen to specific slots, and the value
  134. // will always be grabbed from the slot (and never set in the project
  135. // component). This is useful for things like tracking the current
  136. // file being processed by a filter (e.g. AppendTask sets an append.current_file
  137. // slot, which can be ready by the XSLTParam type.)
  138. if (count($method->getParameters()) !== 1) {
  139. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take exactly one parameter.");
  140. }
  141. $this->slotListeners[$name] = $method;
  142. } elseif (strpos($name, "set") === 0) {
  143. // A standard attribute setter.
  144. if (count($method->getParameters()) !== 1) {
  145. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take exactly one parameter.");
  146. }
  147. $this->attributeSetters[$name] = $method;
  148. } elseif (strpos($name, "create") === 0) {
  149. if (count($method->getParameters()) > 0) {
  150. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() may not take any parameters.");
  151. }
  152. // Because PHP doesn't support return types, we are going to do
  153. // two things here to guess return type:
  154. // 1) parse comments for an explicit value
  155. // 2) if that fails, assume that the part of the method after "create"
  156. // is the name of the return type (in many cases it is not)
  157. // This isn't super important -- i.e. we're not instantaiting classes
  158. // based on this information. It's more just so that IntrospectionHelper
  159. // can keep track of all the nested types -- and provide more helpful
  160. // exception messages, etc.
  161. preg_match('/@return[\s]+([\w]+)/', $method->getDocComment(), $matches);
  162. if (!empty($matches[1]) && class_exists($matches[1], false)) {
  163. $this->nestedTypes[$name] = $matches[1];
  164. } else {
  165. // assume that method createEquals() creates object of type "Equals"
  166. // (that example would be false, of course)
  167. $this->nestedTypes[$name] = $this->getPropertyName($name, "create");
  168. }
  169. $this->nestedCreators[$name] = $method;
  170. } elseif (strpos($name, "addconfigured") === 0) {
  171. // *must* use class hints if using addConfigured ...
  172. // 1 param only
  173. $params = $method->getParameters();
  174. if (count($params) < 1) {
  175. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take at least one parameter.");
  176. }
  177. if (count($params) > 1) {
  178. $this->warn($method->getDeclaringClass()->getName()."::".$method->getName()."() takes more than one parameter. (IH only uses the first)");
  179. }
  180. $classname = null;
  181. if (($hint = $params[0]->getClass()) !== null) {
  182. $classname = $hint->getName();
  183. }
  184. if ($classname === null) {
  185. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() method MUST use a class hint to indicate the class type of parameter.");
  186. }
  187. $this->nestedTypes[$name] = $classname;
  188. $this->nestedStorers[$name] = $method;
  189. } elseif (strpos($name, "add") === 0) {
  190. // *must* use class hints if using add ...
  191. // 1 param only
  192. $params = $method->getParameters();
  193. if (count($params) < 1) {
  194. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() must take at least one parameter.");
  195. }
  196. if (count($params) > 1) {
  197. $this->warn($method->getDeclaringClass()->getName()."::".$method->getName()."() takes more than one parameter. (IH only uses the first)");
  198. }
  199. $classname = null;
  200. if (($hint = $params[0]->getClass()) !== null) {
  201. $classname = $hint->getName();
  202. }
  203. // we don't use the classname here, but we need to make sure it exists before
  204. // we later try to instantiate a non-existant class
  205. if ($classname === null) {
  206. throw new BuildException($method->getDeclaringClass()->getName()."::".$method->getName()."() method MUST use a class hint to indicate the class type of parameter.");
  207. }
  208. $this->nestedCreators[$name] = $method;
  209. }
  210. } // if $method->isPublic()
  211. } // foreach
  212. }
  213. /** Sets the named attribute. */
  214. function setAttribute(Project $project, $element, $attributeName, &$value) {
  215. // we want to check whether the value we are setting looks like
  216. // a slot-listener variable: %{task.current_file}
  217. //
  218. // slot-listener variables are not like properties, in that they cannot be mixed with
  219. // other text values. The reason for this disparity is that properties are only
  220. // set when first constructing objects from XML, whereas slot-listeners are always dynamic.
  221. //
  222. // This is made possible by PHP5 (objects automatically passed by reference) and PHP's loose
  223. // typing.
  224. if (StringHelper::isSlotVar($value)) {
  225. $as = "setlistening" . strtolower($attributeName);
  226. if (!isset($this->slotListeners[$as])) {
  227. $msg = $this->getElementName($project, $element) . " doesn't support a slot-listening '$attributeName' attribute.";
  228. throw new BuildException($msg);
  229. }
  230. $method = $this->slotListeners[$as];
  231. $key = StringHelper::slotVar($value);
  232. $value = Register::getSlot($key); // returns a RegisterSlot object which will hold current value of that register (accessible using getValue())
  233. } else {
  234. // Traditional value options
  235. $as = "set".strtolower($attributeName);
  236. if (!isset($this->attributeSetters[$as])) {
  237. $msg = $this->getElementName($project, $element) . " doesn't support the '$attributeName' attribute.";
  238. throw new BuildException($msg);
  239. }
  240. $method = $this->attributeSetters[$as];
  241. if ($as == "setrefid") {
  242. $value = new Reference($value);
  243. } else {
  244. // value is a string representation of a boolean type,
  245. // convert it to primitive
  246. if (StringHelper::isBoolean($value)) {
  247. $value = StringHelper::booleanValue($value);
  248. }
  249. // does method expect a PhingFile object? if so, then
  250. // pass a project-relative file.
  251. $params = $method->getParameters();
  252. $classname = null;
  253. if (($hint = $params[0]->getClass()) !== null) {
  254. $classname = $hint->getName();
  255. }
  256. // there should only be one param; we'll just assume ....
  257. if ($classname !== null) {
  258. switch(strtolower($classname)) {
  259. case "phingfile":
  260. $value = $project->resolveFile($value);
  261. break;
  262. case "path":
  263. $value = new Path($project, $value);
  264. break;
  265. case "reference":
  266. $value = new Reference($value);
  267. break;
  268. // any other object params we want to support should go here ...
  269. }
  270. } // if hint !== null
  271. } // if not setrefid
  272. } // if is slot-listener
  273. try {
  274. $project->log(" -calling setter ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
  275. $method->invoke($element, $value);
  276. } catch(Exception $exc) {
  277. throw new BuildException($exc);
  278. }
  279. }
  280. /** Adds PCDATA areas.*/
  281. function addText(Project $project, $element, $text) {
  282. if ($this->methodAddText === null) {
  283. $msg = $this->getElementName($project, $element)." doesn't support nested text data.";
  284. throw new BuildException($msg);
  285. }
  286. try {
  287. $method = $this->methodAddText;
  288. $method->invoke($element, $text);
  289. } catch (Exception $exc) {
  290. throw new BuildException($exc);
  291. }
  292. }
  293. /**
  294. * Creates a named nested element.
  295. *
  296. * Valid creators can be in the form createFoo() or addFoo(Bar).
  297. * @return object Returns the nested element.
  298. * @throws BuildException
  299. */
  300. function createElement(Project $project, $element, $elementName) {
  301. $addMethod = "add".strtolower($elementName);
  302. $createMethod = "create".strtolower($elementName);
  303. $nestedElement = null;
  304. if (isset($this->nestedCreators[$createMethod])) {
  305. $method = $this->nestedCreators[$createMethod];
  306. try { // try to invoke the creator method on object
  307. $project->log(" -calling creator ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
  308. $nestedElement = $method->invoke($element);
  309. } catch (Exception $exc) {
  310. throw new BuildException($exc);
  311. }
  312. } elseif (isset($this->nestedCreators[$addMethod])) {
  313. $method = $this->nestedCreators[$addMethod];
  314. // project components must use class hints to support the add methods
  315. try { // try to invoke the adder method on object
  316. $project->log(" -calling adder ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
  317. // we've already assured that correct num of params
  318. // exist and that method is using class hints
  319. $params = $method->getParameters();
  320. $classname = null;
  321. if (($hint = $params[0]->getClass()) !== null) {
  322. $classname = $hint->getName();
  323. }
  324. // create a new instance of the object and add it via $addMethod
  325. $nestedElement = new $classname();
  326. $method->invoke($element, $nestedElement);
  327. } catch (Exception $exc) {
  328. throw new BuildException($exc);
  329. }
  330. } else {
  331. $msg = $this->getElementName($project, $element) . " doesn't support the '$elementName' creator/adder.";
  332. throw new BuildException($msg);
  333. }
  334. if ($nestedElement instanceof ProjectComponent) {
  335. $nestedElement->setProject($project);
  336. }
  337. return $nestedElement;
  338. }
  339. /**
  340. * Creates a named nested element.
  341. * @return void
  342. * @throws BuildException
  343. */
  344. function storeElement($project, $element, $child, $elementName = null) {
  345. if ($elementName === null) {
  346. return;
  347. }
  348. $storer = "addconfigured".strtolower($elementName);
  349. if (isset($this->nestedStorers[$storer])) {
  350. $method = $this->nestedStorers[$storer];
  351. try {
  352. $project->log(" -calling storer ".$method->getDeclaringClass()->getName()."::".$method->getName()."()", Project::MSG_DEBUG);
  353. $method->invoke($element, $child);
  354. } catch (Exception $exc) {
  355. throw new BuildException($exc);
  356. }
  357. }
  358. }
  359. /** Does the introspected class support PCDATA? */
  360. function supportsCharacters() {
  361. return ($this->methodAddText !== null);
  362. }
  363. /** Return all attribues supported by the introspected class. */
  364. function getAttributes() {
  365. $attribs = array();
  366. foreach (array_keys($this->attributeSetters) as $setter) {
  367. $attribs[] =$this->getPropertyName($setter, "set");
  368. }
  369. return $attribs;
  370. }
  371. /** Return all nested elements supported by the introspected class. */
  372. function getNestedElements() {
  373. return $this->nestedTypes;
  374. }
  375. /**
  376. * Get the the name for an element.
  377. * When possible the full classnam (phing.tasks.system.PropertyTask) will
  378. * be returned. If not available (loaded in taskdefs or typedefs) then the
  379. * XML element name will be returned.
  380. *
  381. * @param Project $project
  382. * @param object $element The Task or type element.
  383. * @return string Fully qualified class name of element when possible.
  384. */
  385. function getElementName(Project $project, $element) {
  386. $taskdefs = $project->getTaskDefinitions();
  387. $typedefs = $project->getDataTypeDefinitions();
  388. // check if class of element is registered with project (tasks & types)
  389. // most element types don't have a getTag() method
  390. $elClass = get_class($element);
  391. if (!in_array('getTag', get_class_methods($elClass))) {
  392. // loop through taskdefs and typesdefs and see if the class name
  393. // matches (case-insensitive) any of the classes in there
  394. foreach(array_merge($taskdefs, $typedefs) as $elName => $class) {
  395. if (0 === strcasecmp($elClass, StringHelper::unqualify($class))) {
  396. return $class;
  397. }
  398. }
  399. return "$elClass (unknown)";
  400. } else {
  401. // ->getTag() method does exist, so use it
  402. $elName = $element->getTag();
  403. if (isset($taskdefs[$elName])) {
  404. return $taskdefs[$elName];
  405. } elseif (isset($typedefs[$elName])) {
  406. return $typedefs[$elName];
  407. } else {
  408. return "$elName (unknown)";
  409. }
  410. }
  411. }
  412. /** extract the name of a property from a method name - subtracting a given prefix. */
  413. function getPropertyName($methodName, $prefix) {
  414. $start = strlen($prefix);
  415. return strtolower(substr($methodName, $start));
  416. }
  417. /**
  418. * Prints warning message to screen if -debug was used.
  419. */
  420. function warn($msg) {
  421. if (Phing::getMsgOutputLevel() === Project::MSG_DEBUG) {
  422. print("[IntrospectionHelper] " . $msg . "\n");
  423. }
  424. }
  425. }