1
<?php
2
/**
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the LGPL. For more information please see
17
 * <http://phing.info>.
18
 */
19

20
/**
21
 * Helper class that collects the methods that a task or nested element
22
 * holds to set attributes, create nested elements or hold PCDATA
23
 * elements.
24
 *
25
 *<ul>
26
 * <li><strong>SMART-UP INLINE DOCS</strong></li>
27
 * <li><strong>POLISH-UP THIS CLASS</strong></li>
28
 *</ul>
29
 *
30
 * @author    Andreas Aderhold <andi@binarycloud.com>
31
 * @author    Hans Lellelid <hans@xmpl.org>
32
 * @copyright 2001,2002 THYRELL. All rights reserved
33
 * @package   phing
34
 */
35
class IntrospectionHelper
36
{
37

38
    /**
39
     * Holds the attribute setter methods.
40
     *
41
     * @var array string[]
42
     */
43
    private $attributeSetters = [];
44

45
    /**
46
     * Holds methods to create nested elements.
47
     *
48
     * @var array string[]
49
     */
50
    private $nestedCreators = [];
51

52
    /**
53
     * Holds methods to store configured nested elements.
54
     *
55
     * @var array string[]
56
     */
57
    private $nestedStorers = [];
58

59
    /**
60
     * Map from attribute names to nested types.
61
     */
62
    private $nestedTypes = [];
63

64
    /**
65
     * New idea in phing: any class can register certain
66
     * keys -- e.g. "task.current_file" -- which can be used in
67
     * task attributes, if supported.  In the build XML these
68
     * are referred to like this:
69
     *         <regexp pattern="\n" replace="%{task.current_file}"/>
70
     * In the type/task a listener method must be defined:
71
     *         function setListeningReplace($slot) {}
72
     *
73
     * @var array string[]
74
     */
75
    private $slotListeners = [];
76

77
    /**
78
     * The method to add PCDATA stuff.
79
     *
80
     * @var string Method name of the addText (redundant?) method, if class supports it :)
81
     */
82
    private $methodAddText = null;
83

84
    /**
85
     * The Class that's been introspected.
86
     *
87
     * @var object
88
     */
89
    private $bean;
90

91
    /**
92
     * The cache of IntrospectionHelper classes instantiated by getHelper().
93
     *
94
     * @var array IntrospectionHelpers[]
95
     */
96
    private static $helpers = [];
97

98
    /**
99
     * Factory method for helper objects.
100
     *
101
     * @param  string $class The class to create a Helper for
102
     * @return IntrospectionHelper
103
     */
104 1
    public static function getHelper($class)
105
    {
106 1
        if (!isset(self::$helpers[$class])) {
107 1
            self::$helpers[$class] = new IntrospectionHelper($class);
108
        }
109

110 1
        return self::$helpers[$class];
111
    }
112

113
    /**
114
     * This function constructs a new introspection helper for a specific class.
115
     *
116
     * This method loads all methods for the specified class and categorizes them
117
     * as setters, creators, slot listeners, etc.  This way, the setAttribue() doesn't
118
     * need to perform any introspection -- either the requested attribute setter/creator
119
     * exists or it does not & a BuildException is thrown.
120
     *
121
     * @param  string $class The classname for this IH.
122
     * @throws BuildException
123
     */
124 1
    public function __construct($class)
125
    {
126 1
        $this->bean = new ReflectionClass($class);
127

128
        //$methods = get_class_methods($bean);
129 1
        foreach ($this->bean->getMethods() as $method) {
130 1
            if ($method->isPublic()) {
131
                // We're going to keep case-insensitive method names
132
                // for as long as we're allowed :)  It makes it much
133
                // easier to map XML attributes to PHP class method names.
134 1
                $name = strtolower($method->getName());
135

136
                // There are a few "reserved" names that might look like attribute setters
137
                // but should actually just be skipped.  (Note: this means you can't ever
138
                // have an attribute named "location" or "tasktype" or a nested element container
139
                // named "task" [TaskContainer::addTask(Task)].)
140
                if (
141 1
                    $name === "setlocation"
142 1
                    || $name === "settasktype"
143 1
                    || ('addtask' === $name
144 1
                    && $this->isContainer()
145 1
                    && count($method->getParameters()) === 1
146 1
                        && Task::class === $method->getParameters()[0])
147
                ) {
148 1
                    continue;
149
                }
150

151 1
                if ($name === "addtext") {
152 1
                    $this->methodAddText = $method;
153 1
                } elseif (strpos($name, "setlistening") === 0) {
154
                    // Phing supports something unique called "RegisterSlots"
155
                    // These are dynamic values that use a basic slot system so that
156
                    // classes can register to listen to specific slots, and the value
157
                    // will always be grabbed from the slot (and never set in the project
158
                    // component).  This is useful for things like tracking the current
159
                    // file being processed by a filter (e.g. AppendTask sets an append.current_file
160
                    // slot, which can be ready by the XSLTParam type.)
161

162 1
                    if (count($method->getParameters()) !== 1) {
163 0
                        throw new BuildException(
164 0
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() must take exactly one parameter."
165
                        );
166
                    }
167

168 1
                    $this->slotListeners[$name] = $method;
169 1
                } elseif (strpos($name, "set") === 0 && count($method->getParameters()) === 1) {
170 1
                    $this->attributeSetters[$name] = $method;
171 1
                } elseif (strpos($name, "create") === 0) {
172 1
                    if ($method->getNumberOfRequiredParameters() > 0) {
173 1
                        throw new BuildException(
174 1
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() may not take any parameters."
175
                        );
176
                    }
177

178 1
                    if ($method->hasReturnType()) {
179 1
                        $this->nestedTypes[$name] = $method->getReturnType();
180
                    } else {
181 1
                        preg_match('/@return[\s]+([\w]+)/', $method->getDocComment(), $matches);
182 1
                        if (!empty($matches[1]) && class_exists($matches[1], false)) {
183 1
                            $this->nestedTypes[$name] = $matches[1];
184
                        } else {
185
                            // assume that method createEquals() creates object of type "Equals"
186
                            // (that example would be false, of course)
187 1
                            $this->nestedTypes[$name] = $this->getPropertyName($name, "create");
188
                        }
189
                    }
190

191 1
                    $this->nestedCreators[$name] = $method;
192 1
                } elseif (strpos($name, "addconfigured") === 0) {
193
                    // *must* use class hints if using addConfigured ...
194

195
                    // 1 param only
196 1
                    $params = $method->getParameters();
197

198 1
                    if (count($params) < 1) {
199 0
                        throw new BuildException(
200 0
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() must take at least one parameter."
201
                        );
202
                    }
203

204 1
                    if (count($params) > 1) {
205 0
                        $this->warn(
206 0
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() takes more than one parameter. (IH only uses the first)"
207
                        );
208
                    }
209

210 1
                    $classname = null;
211

212 1
                    if (($hint = $params[0]->getClass()) !== null) {
213 1
                        $classname = $hint->getName();
214
                    }
215

216 1
                    if ($classname === null) {
217 1
                        throw new BuildException(
218 1
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() method MUST use a class hint to indicate the class type of parameter."
219
                        );
220
                    }
221

222 1
                    $this->nestedTypes[$name] = $classname;
223

224 1
                    $this->nestedStorers[$name] = $method;
225 1
                } elseif (strpos($name, "add") === 0) {
226
                    // *must* use class hints if using add ...
227

228
                    // 1 param only
229 1
                    $params = $method->getParameters();
230 1
                    if (count($params) < 1) {
231 0
                        throw new BuildException(
232 0
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() must take at least one parameter."
233
                        );
234
                    }
235

236 1
                    if (count($params) > 1) {
237 0
                        $this->warn(
238 0
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() takes more than one parameter. (IH only uses the first)"
239
                        );
240
                    }
241

242 1
                    $classname = null;
243

244 1
                    if (($hint = $params[0]->getClass()) !== null) {
245 1
                        $classname = $hint->getName();
246
                    }
247

248
                    // we don't use the classname here, but we need to make sure it exists before
249
                    // we later try to instantiate a non-existent class
250 1
                    if ($classname === null) {
251 1
                        throw new BuildException(
252 1
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() method MUST use a class hint to indicate the class type of parameter."
253
                        );
254
                    }
255

256 1
                    $this->nestedCreators[$name] = $method;
257
                }
258
            } // if $method->isPublic()
259
        } // foreach
260
    }
261

262
    /**
263
     * Indicates whether the introspected class is a task container, supporting arbitrary nested tasks/types.
264
     *
265
     * @return bool true if the introspected class is a container; false otherwise.
266
     */
267 1
    public function isContainer()
268
    {
269 1
        return $this->bean->implementsInterface(TaskContainer::class);
270
    }
271

272
    /**
273
     * Sets the named attribute.
274
     *
275
     * @param  Project $project
276
     * @param  object $element
277
     * @param  string $attributeName
278
     * @param  mixed $value
279
     * @throws BuildException
280
     */
281 1
    public function setAttribute(Project $project, $element, $attributeName, &$value)
282
    {
283
        // we want to check whether the value we are setting looks like
284
        // a slot-listener variable:  %{task.current_file}
285
        //
286
        // slot-listener variables are not like properties, in that they cannot be mixed with
287
        // other text values.  The reason for this disparity is that properties are only
288
        // set when first constructing objects from XML, whereas slot-listeners are always dynamic.
289
        //
290
        // This is made possible by PHP5 (objects automatically passed by reference) and PHP's loose
291
        // typing.
292 1
        if (StringHelper::isSlotVar($value)) {
293 0
            $as = "setlistening" . strtolower($attributeName);
294

295 0
            if (!isset($this->slotListeners[$as])) {
296 0
                $msg = $this->getElementName(
297 0
                    $project,
298
                    $element
299 0
                ) . " doesn't support a slot-listening '$attributeName' attribute.";
300 0
                throw new BuildException($msg);
301
            }
302

303 0
            $method = $this->slotListeners[$as];
304

305 0
            $key = StringHelper::slotVar($value);
306 0
            $value = Register::getSlot(
307 0
                $key
308
            ); // returns a RegisterSlot object which will hold current value of that register (accessible using getValue())
309
        } else {
310
            // Traditional value options
311

312 1
            $as = "set" . strtolower($attributeName);
313

314 1
            if (!isset($this->attributeSetters[$as])) {
315 1
                if ($element instanceof DynamicAttribute) {
316 1
                    $element->setDynamicAttribute($attributeName, (string) $value);
317 1
                    return;
318
                }
319 1
                $msg = $this->getElementName($project, $element) . " doesn't support the '$attributeName' attribute.";
320 1
                throw new BuildException($msg);
321
            }
322

323 1
            $method = $this->attributeSetters[$as];
324

325 1
            if ($as == "setrefid") {
326 1
                $value = new Reference($project, $value);
327
            } else {
328 1
                $params = $method->getParameters();
329 1
                $reflectedAttr = null;
330

331
                // try to determine parameter type
332 1
                if (($argType = $params[0]->getType()) !== null) {
333
                    /** @var ReflectionNamedType $argType */
334 1
                    $reflectedAttr = $argType->getName();
335 1
                } elseif (($classType = $params[0]->getClass()) !== null) {
336
                    /** @var ReflectionClass $classType */
337 0
                    $reflectedAttr = $classType->getName();
338
                }
339

340
                // value is a string representation of a boolean type,
341
                // convert it to primitive
342 1
                if ($reflectedAttr === 'bool' || ($reflectedAttr !== 'string' && StringHelper::isBoolean($value))) {
343 1
                    $value = StringHelper::booleanValue($value);
344
                }
345

346
                // there should only be one param; we'll just assume ....
347 1
                if ($reflectedAttr !== null) {
348 1
                    switch (strtolower($reflectedAttr)) {
349 1
                        case "phingfile":
350 1
                            $value = $project->resolveFile($value);
351 1
                            break;
352 1
                        case "path":
353 1
                            $value = new Path($project, $value);
354 1
                            break;
355 1
                        case "reference":
356 0
                            $value = new Reference($project, $value);
357 0
                            break;
358
                        // any other object params we want to support should go here ...
359
                    }
360
                } // if hint !== null
361
            } // if not setrefid
362
        } // if is slot-listener
363

364
        try {
365 1
            $project->log(
366 1
                "    -calling setter " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
367 1
                Project::MSG_DEBUG
368
            );
369 1
            $method->invoke($element, $value);
370 1
        } catch (Exception $exc) {
371 1
            throw new BuildException($exc->getMessage(), $exc);
372
        }
373
    }
374

375
    /**
376
     * Adds PCDATA areas.
377
     *
378
     * @param  Project $project
379
     * @param  string $element
380
     * @param  string $text
381
     * @throws BuildException
382
     */
383 1
    public function addText(Project $project, $element, $text)
384
    {
385 1
        if ($this->methodAddText === null) {
386 1
            $msg = $this->getElementName($project, $element) . " doesn't support nested text data.";
387 1
            throw new BuildException($msg);
388
        }
389
        try {
390 1
            $method = $this->methodAddText;
391 1
            $method->invoke($element, $text);
392 0
        } catch (Exception $exc) {
393 0
            throw new BuildException($exc->getMessage(), $exc);
394
        }
395
    }
396

397
    /**
398
     * Creates a named nested element.
399
     *
400
     * Valid creators can be in the form createFoo() or addFoo(Bar).
401
     *
402
     * @param  Project $project
403
     * @param  object $element Object the XML tag is child of.
404
     *                              Often a task object.
405
     * @param  string $elementName XML tag name
406
     * @return object         Returns the nested element.
407
     * @throws BuildException
408
     */
409 1
    public function createElement(Project $project, $element, $elementName)
410
    {
411 1
        $addMethod = "add" . strtolower($elementName);
412 1
        $createMethod = "create" . strtolower($elementName);
413 1
        $nestedElement = null;
414

415 1
        if (isset($this->nestedCreators[$createMethod])) {
416 1
            $method = $this->nestedCreators[$createMethod];
417
            try { // try to invoke the creator method on object
418 1
                $project->log(
419 1
                    "    -calling creator " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
420 1
                    Project::MSG_DEBUG
421
                );
422 1
                $nestedElement = $method->invoke($element);
423 1
            } catch (Exception $exc) {
424 1
                throw new BuildException($exc->getMessage(), $exc);
425
            }
426 1
        } elseif (isset($this->nestedCreators[$addMethod])) {
427 1
            $method = $this->nestedCreators[$addMethod];
428

429
            // project components must use class hints to support the add methods
430

431
            try { // try to invoke the adder method on object
432 1
                $project->log(
433 1
                    "    -calling adder " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
434 1
                    Project::MSG_DEBUG
435
                );
436
                // we've already assured that correct num of params
437
                // exist and that method is using class hints
438 1
                $params = $method->getParameters();
439

440 1
                $classname = null;
441

442 1
                if (($hint = $params[0]->getClass()) !== null) {
443 1
                    $classname = $hint->getName();
444
                }
445

446
                // create a new instance of the object and add it via $addMethod
447 1
                $clazz = new ReflectionClass($classname);
448 1
                if ($clazz->getConstructor() !== null && $clazz->getConstructor()->getNumberOfRequiredParameters() >= 1) {
449 1
                    $nestedElement = new $classname(Phing::getCurrentProject() ?? $project);
450
                } else {
451 1
                    $nestedElement = new $classname();
452
                }
453

454 1
                if ($nestedElement instanceof Task && $element instanceof Task) {
455 1
                    $nestedElement->setOwningTarget($element->getOwningTarget());
456
                }
457

458 1
                $method->invoke($element, $nestedElement);
459 1
            } catch (Exception $exc) {
460 1
                throw new BuildException($exc->getMessage(), $exc);
461
            }
462 1
        } elseif ($this->bean->implementsInterface("CustomChildCreator")) {
463 1
            $method = $this->bean->getMethod('customChildCreator');
464

465
            try {
466 1
                $nestedElement = $method->invoke($element, strtolower($elementName), $project);
467 0
            } catch (Exception $exc) {
468 0
                throw new BuildException($exc->getMessage(), $exc);
469
            }
470
        } else {
471
            //try the add method for the element's parent class
472 1
            $typedefs = $project->getDataTypeDefinitions();
473 1
            if (isset($typedefs[$elementName])) {
474 0
                $elementClass = Phing::import($typedefs[$elementName]);
475 0
                $parentClass = get_parent_class($elementClass);
476 0
                $addMethod = 'add' . strtolower($parentClass);
477

478 0
                if (isset($this->nestedCreators[$addMethod])) {
479 0
                    $method = $this->nestedCreators[$addMethod];
480
                    try {
481 0
                        $project->log(
482
                            "    -calling parent adder "
483 0
                            . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
484 0
                            Project::MSG_DEBUG
485
                        );
486 0
                        $nestedElement = new $elementClass();
487 0
                        $method->invoke($element, $nestedElement);
488 0
                    } catch (Exception $exc) {
489 0
                        throw new BuildException($exc->getMessage(), $exc);
490
                    }
491
                }
492
            }
493 1
            if ($nestedElement === null) {
494 1
                $msg = $this->getElementName($project, $element) . " doesn't support the '$elementName' creator/adder.";
495 1
                throw new BuildException($msg);
496
            }
497
        }
498

499 1
        if ($nestedElement instanceof ProjectComponent) {
500 1
            $nestedElement->setProject($project);
501
        }
502

503 1
        return $nestedElement;
504
    }
505

506
    /**
507
     * Creates a named nested element.
508
     *
509
     * @param  Project $project
510
     * @param  string $element
511
     * @param  string $child
512
     * @param  string|null $elementName
513
     * @return void
514
     * @throws BuildException
515
     */
516 0
    public function storeElement($project, $element, $child, $elementName = null)
517
    {
518 0
        if ($elementName === null) {
519 0
            return;
520
        }
521

522 0
        $storer = "addconfigured" . strtolower($elementName);
523

524 0
        if (isset($this->nestedStorers[$storer])) {
525 0
            $method = $this->nestedStorers[$storer];
526

527
            try {
528 0
                $project->log(
529 0
                    "    -calling storer " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
530 0
                    Project::MSG_DEBUG
531
                );
532 0
                $method->invoke($element, $child);
533 0
            } catch (Exception $exc) {
534 0
                throw new BuildException($exc->getMessage(), $exc);
535
            }
536
        }
537
    }
538

539
    /**
540
     * Does the introspected class support PCDATA?
541
     *
542
     * @return boolean
543
     */
544 1
    public function supportsCharacters()
545
    {
546 1
        return ($this->methodAddText !== null);
547
    }
548

549
    /**
550
     * Return all attribues supported by the introspected class.
551
     *
552
     * @return string[]
553
     */
554 0
    public function getAttributes()
555
    {
556 0
        $attribs = [];
557 0
        foreach (array_keys($this->attributeSetters) as $setter) {
558 0
            $attribs[] = $this->getPropertyName($setter, "set");
559
        }
560

561 0
        return $attribs;
562
    }
563

564
    /**
565
     * Return all nested elements supported by the introspected class.
566
     *
567
     * @return string[]
568
     */
569 0
    public function getNestedElements()
570
    {
571 0
        return $this->nestedTypes;
572
    }
573

574
    /**
575
     * Get the name for an element.
576
     * When possible the full classnam (phing.tasks.system.PropertyTask) will
577
     * be returned.  If not available (loaded in taskdefs or typedefs) then the
578
     * XML element name will be returned.
579
     *
580
     * @param  Project $project
581
     * @param  object $element The Task or type element.
582
     * @return string  Fully qualified class name of element when possible.
583
     */
584 1
    public function getElementName(Project $project, $element)
585
    {
586 1
        $taskdefs = $project->getTaskDefinitions();
587 1
        $typedefs = $project->getDataTypeDefinitions();
588

589
        // check if class of element is registered with project (tasks & types)
590
        // most element types don't have a getTag() method
591 1
        $elClass = get_class($element);
592

593 1
        if (!in_array('getTag', get_class_methods($elClass))) {
594
            // loop through taskdefs and typesdefs and see if the class name
595
            // matches (case-insensitive) any of the classes in there
596 1
            foreach (array_merge($taskdefs, $typedefs) as $elName => $class) {
597 1
                if (0 === strcasecmp($elClass, StringHelper::unqualify($class))) {
598 1
                    return $class;
599
                }
600
            }
601

602 1
            return "$elClass (unknown)";
603
        }
604

605
// ->getTag() method does exist, so use it
606 0
        $elName = $element->getTag();
607 0
        if (isset($taskdefs[$elName])) {
608 0
            return $taskdefs[$elName];
609
        }
610

611 0
        if (isset($typedefs[$elName])) {
612 0
            return $typedefs[$elName];
613
        }
614

615 0
        return "$elName (unknown)";
616
    }
617

618
    /**
619
     * Extract the name of a property from a method name - subtracting  a given prefix.
620
     *
621
     * @param  string $methodName
622
     * @param  string $prefix
623
     * @return string
624
     */
625 1
    public function getPropertyName($methodName, $prefix)
626
    {
627 1
        $start = strlen($prefix);
628

629 1
        return strtolower(substr($methodName, $start));
630
    }
631

632
    /**
633
     * Prints warning message to screen if -debug was used.
634
     *
635
     * @param string $msg
636
     */
637 0
    public function warn($msg)
638
    {
639 0
        if (Phing::getMsgOutputLevel() === Project::MSG_DEBUG) {
640 0
            print("[IntrospectionHelper] " . $msg . "\n");
641
        }
642
    }
643
}

Read our documentation on viewing source code .

Loading