Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<?php
2
 
3
/*
4
 *  $Id: IntrospectionHelper.php 144 2007-02-05 15:19:00Z hans $
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 === strcasecmp($elClass, StringHelper::unqualify($class))) {
509
                        return $class;
510
                    }
511
                }
512
                return "$elClass (unknown)";
513
        } else {
514
            // ->getTag() method does exist, so use it
515
            $elName = $element->getTag();
516
            if (isset($taskdefs[$elName])) {
517
                return $taskdefs[$elName];
518
            } elseif (isset($typedefs[$elName])) {
519
 
520
                return $typedefs[$elName];
521
            } else {
522
                return "$elName (unknown)";
523
            }
524
        }
525
    }
526
 
527
    /** extract the name of a property from a method name - subtracting  a given prefix. */
528
    function getPropertyName($methodName, $prefix) {
529
        $start = strlen($prefix);
530
        return strtolower(substr($methodName, $start));
531
    }
532
 
533
    /**
534
     * Prints warning message to screen if -debug was used.
535
     */
536
    function warn($msg) {
537
        if (Phing::getMsgOutputLevel() === Project::MSG_DEBUG) {
538
            print("[IntrospectionHelper] " . $msg . "\n");
539
        }
540
    }
541
 
542
}