root/branches/2.2/classes/phing/IntrospectionHelper.php

Revision 27, 21.9 kB (checked in by knut, 3 years ago)

ensure that autoload is never called from IntrospectionHelper (Ticket #1)

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