Passed
Push — master ( 20a3ba...08be1b )
by Siad
12:52
created

IntrospectionHelper::supportsCharacters()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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 <[email protected]>
31
 * @author    Hans Lellelid <[email protected]>
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 704
    public static function getHelper($class)
105
    {
106 704
        if (!isset(self::$helpers[$class])) {
107 146
            self::$helpers[$class] = new IntrospectionHelper($class);
108
        }
109
110 704
        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 146
    public function __construct($class)
125
    {
126 146
        $this->bean = new ReflectionClass($class);
127
128
        //$methods = get_class_methods($bean);
129 146
        foreach ($this->bean->getMethods() as $method) {
130 146
            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 146
                $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 146
                    $name === "setlocation"
142 146
                    || $name === "settasktype"
143 146
                    || ('addtask' === $name
144 2
                    && $this->isContainer()
145 2
                    && count($method->getParameters()) === 1
146 2
                        && Task::class === $method->getParameters()[0])
147
                ) {
148 117
                    continue;
149
                }
150
151 146
                if ($name === "addtext") {
152 14
                    $this->methodAddText = $method;
153 146
                } 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
                        throw new BuildException(
164
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() must take exactly one parameter."
165
                        );
166
                    }
167
168 1
                    $this->slotListeners[$name] = $method;
169 146
                } elseif (strpos($name, "set") === 0 && count($method->getParameters()) === 1) {
170 139
                    $this->attributeSetters[$name] = $method;
171 146
                } elseif (strpos($name, "create") === 0) {
172 50
                    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 49
                    if ($method->hasReturnType()) {
179 5
                        $this->nestedTypes[$name] = $method->getReturnType();
180
                    } else {
181 45
                        preg_match('/@return[\s]+([\w]+)/', $method->getDocComment(), $matches);
182 45
                        if (!empty($matches[1]) && class_exists($matches[1], false)) {
183 33
                            $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 30
                            $this->nestedTypes[$name] = $this->getPropertyName($name, "create");
188
                        }
189
                    }
190
191 49
                    $this->nestedCreators[$name] = $method;
192 146
                } elseif (strpos($name, "addconfigured") === 0) {
193
                    // *must* use class hints if using addConfigured ...
194
195
                    // 1 param only
196 2
                    $params = $method->getParameters();
197
198 2
                    if (count($params) < 1) {
199
                        throw new BuildException(
200
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() must take at least one parameter."
201
                        );
202
                    }
203
204 2
                    if (count($params) > 1) {
205
                        $this->warn(
206
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() takes more than one parameter. (IH only uses the first)"
207
                        );
208
                    }
209
210
                    /** @var \ReflectionType $hint */
211 2
                    $classname = (($hint = $params[0]->getType()) && !$hint->isBuiltin()) ? $hint->getName() : null;
212
213 2
                    if ($classname === null) {
214 1
                        throw new BuildException(
215 1
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() method MUST use a class hint to indicate the class type of parameter."
216
                        );
217
                    }
218
219 1
                    $this->nestedTypes[$name] = $classname;
220
221 1
                    $this->nestedStorers[$name] = $method;
222 146
                } elseif (strpos($name, "add") === 0) {
223
                    // *must* use class hints if using add ...
224
225
                    // 1 param only
226 51
                    $params = $method->getParameters();
227 51
                    if (count($params) < 1) {
228
                        throw new BuildException(
229
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() must take at least one parameter."
230
                        );
231
                    }
232
233 51
                    if (count($params) > 1) {
234
                        $this->warn(
235
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() takes more than one parameter. (IH only uses the first)"
236
                        );
237
                    }
238
239
                    /** @var \ReflectionType $hint */
240 51
                    $classname = (($hint = $params[0]->getType()) && !$hint->isBuiltin()) ? $hint->getName() : null;
241
242
                    // we don't use the classname here, but we need to make sure it exists before
243
                    // we later try to instantiate a non-existent class
244 51
                    if ($classname === null) {
245 1
                        throw new BuildException(
246 1
                            $method->getDeclaringClass()->getName() . "::" . $method->getName() . "() method MUST use a class hint to indicate the class type of parameter."
247
                        );
248
                    }
249
250 50
                    $this->nestedCreators[$name] = $method;
251
                }
252
            } // if $method->isPublic()
253
        } // foreach
254 145
    }
255
256
    /**
257
     * Indicates whether the introspected class is a task container, supporting arbitrary nested tasks/types.
258
     *
259
     * @return bool true if the introspected class is a container; false otherwise.
260
     */
261 2
    public function isContainer()
262
    {
263 2
        return $this->bean->implementsInterface(TaskContainer::class);
264
    }
265
266
    /**
267
     * Sets the named attribute.
268
     *
269
     * @param  Project $project
270
     * @param  object $element
271
     * @param  string $attributeName
272
     * @param  mixed $value
273
     * @throws BuildException
274
     */
275 684
    public function setAttribute(Project $project, $element, $attributeName, &$value)
276
    {
277
        // we want to check whether the value we are setting looks like
278
        // a slot-listener variable:  %{task.current_file}
279
        //
280
        // slot-listener variables are not like properties, in that they cannot be mixed with
281
        // other text values.  The reason for this disparity is that properties are only
282
        // set when first constructing objects from XML, whereas slot-listeners are always dynamic.
283
        //
284
        // This is made possible by PHP5 (objects automatically passed by reference) and PHP's loose
285
        // typing.
286 684
        if (StringHelper::isSlotVar($value)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression StringHelper::isSlotVar($value) of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
287
            $as = "setlistening" . strtolower($attributeName);
288
289
            if (!isset($this->slotListeners[$as])) {
290
                $msg = $this->getElementName(
291
                    $project,
292
                    $element
293
                ) . " doesn't support a slot-listening '$attributeName' attribute.";
294
                throw new BuildException($msg);
295
            }
296
297
            $method = $this->slotListeners[$as];
298
299
            $key = StringHelper::slotVar($value);
300
            $value = Register::getSlot(
301
                $key
302
            ); // returns a RegisterSlot object which will hold current value of that register (accessible using getValue())
303
        } else {
304
            // Traditional value options
305
306 684
            $as = "set" . strtolower($attributeName);
307
308 684
            if (!isset($this->attributeSetters[$as])) {
309 5
                if ($element instanceof DynamicAttribute) {
310 2
                    $element->setDynamicAttribute($attributeName, (string) $value);
311 2
                    return;
312
                }
313 3
                $msg = $this->getElementName($project, $element) . " doesn't support the '$attributeName' attribute.";
314 3
                throw new BuildException($msg);
315
            }
316
317 684
            $method = $this->attributeSetters[$as];
318
319 684
            if ($as == "setrefid") {
320 20
                $value = new Reference($project, $value);
321
            } else {
322 684
                $params = $method->getParameters();
323
324
                /** @var \ReflectionType $hint */
325 684
                $reflectedAttr = ($hint = $params[0]->getType()) ? $hint->getName() : null;
326
327
                // value is a string representation of a boolean type,
328
                // convert it to primitive
329 684
                if ($reflectedAttr === 'bool' || ($reflectedAttr !== 'string' && StringHelper::isBoolean($value))) {
330 267
                    $value = StringHelper::booleanValue($value);
331
                }
332
333
                // there should only be one param; we'll just assume ....
334 684
                if ($reflectedAttr !== null) {
335 573
                    switch (strtolower($reflectedAttr)) {
336 573
                        case "phingfile":
337 294
                            $value = $project->resolveFile($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type boolean; however, parameter $fileName of Project::resolveFile() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

337
                            $value = $project->resolveFile(/** @scrutinizer ignore-type */ $value);
Loading history...
338 294
                            break;
339 503
                        case "path":
340 15
                            $value = new Path($project, $value);
341 15
                            break;
342 489
                        case "reference":
343
                            $value = new Reference($project, $value);
344
                            break;
345
                        // any other object params we want to support should go here ...
346
                    }
347
                } // if hint !== null
348
            } // if not setrefid
349
        } // if is slot-listener
350
351
        try {
352 684
            $project->log(
353 684
                "    -calling setter " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
354 684
                Project::MSG_DEBUG
355
            );
356 684
            $method->invoke($element, $value);
357 9
        } catch (Exception $exc) {
358 9
            throw new BuildException($exc->getMessage(), $exc);
359
        }
360 683
    }
361
362
    /**
363
     * Adds PCDATA areas.
364
     *
365
     * @param  Project $project
366
     * @param  string $element
367
     * @param  string $text
368
     * @throws BuildException
369
     */
370 69
    public function addText(Project $project, $element, $text)
371
    {
372 69
        if ($this->methodAddText === null) {
373 1
            $msg = $this->getElementName($project, $element) . " doesn't support nested text data.";
0 ignored issues
show
Bug introduced by
$element of type string is incompatible with the type object expected by parameter $element of IntrospectionHelper::getElementName(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

373
            $msg = $this->getElementName($project, /** @scrutinizer ignore-type */ $element) . " doesn't support nested text data.";
Loading history...
374 1
            throw new BuildException($msg);
375
        }
376
        try {
377 69
            $method = $this->methodAddText;
378 69
            $method->invoke($element, $text);
379
        } catch (Exception $exc) {
380
            throw new BuildException($exc->getMessage(), $exc);
381
        }
382 69
    }
383
384
    /**
385
     * Creates a named nested element.
386
     *
387
     * Valid creators can be in the form createFoo() or addFoo(Bar).
388
     *
389
     * @param  Project $project
390
     * @param  object $element Object the XML tag is child of.
391
     *                              Often a task object.
392
     * @param  string $elementName XML tag name
393
     * @return object         Returns the nested element.
394
     * @throws BuildException
395
     */
396 422
    public function createElement(Project $project, $element, $elementName)
397
    {
398 422
        $addMethod = "add" . strtolower($elementName);
399 422
        $createMethod = "create" . strtolower($elementName);
400 422
        $nestedElement = null;
401
402 422
        if (isset($this->nestedCreators[$createMethod])) {
403 358
            $method = $this->nestedCreators[$createMethod];
404
            try { // try to invoke the creator method on object
405 358
                $project->log(
406 358
                    "    -calling creator " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
407 358
                    Project::MSG_DEBUG
408
                );
409 358
                $nestedElement = $method->invoke($element);
410 1
            } catch (Exception $exc) {
411 1
                throw new BuildException($exc->getMessage(), $exc);
412
            }
413 331
        } elseif (isset($this->nestedCreators[$addMethod])) {
414 328
            $method = $this->nestedCreators[$addMethod];
415
416
            // project components must use class hints to support the add methods
417
418
            try { // try to invoke the adder method on object
419 328
                $project->log(
420 328
                    "    -calling adder " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
421 328
                    Project::MSG_DEBUG
422
                );
423
                // we've already assured that correct num of params
424
                // exist and that method is using class hints
425 328
                $params = $method->getParameters();
426
427
                /** @var \ReflectionType $hint */
428 328
                $classname = (($hint = $params[0]->getType()) && !$hint->isBuiltin()) ? $hint->getName() : null;
429
430
                // create a new instance of the object and add it via $addMethod
431 328
                $clazz = new ReflectionClass($classname);
432 328
                if ($clazz->getConstructor() !== null && $clazz->getConstructor()->getNumberOfRequiredParameters() >= 1) {
433 4
                    $nestedElement = new $classname(Phing::getCurrentProject() ?? $project);
434
                } else {
435 328
                    $nestedElement = new $classname();
436
                }
437
438 328
                if ($nestedElement instanceof Task && $element instanceof Task) {
439 1
                    $nestedElement->setOwningTarget($element->getOwningTarget());
440
                }
441
442 328
                $method->invoke($element, $nestedElement);
443 2
            } catch (Exception $exc) {
444 2
                throw new BuildException($exc->getMessage(), $exc);
445
            }
446 4
        } elseif ($this->bean->implementsInterface("CustomChildCreator")) {
447 3
            $method = $this->bean->getMethod('customChildCreator');
448
449
            try {
450 3
                $nestedElement = $method->invoke($element, strtolower($elementName), $project);
451
            } catch (Exception $exc) {
452
                throw new BuildException($exc->getMessage(), $exc);
453
            }
454
        } else {
455
            //try the add method for the element's parent class
456 1
            $typedefs = $project->getDataTypeDefinitions();
457 1
            if (isset($typedefs[$elementName])) {
458
                $elementClass = Phing::import($typedefs[$elementName]);
459
                $parentClass = get_parent_class($elementClass);
460
                $addMethod = 'add' . strtolower($parentClass);
461
462
                if (isset($this->nestedCreators[$addMethod])) {
463
                    $method = $this->nestedCreators[$addMethod];
464
                    try {
465
                        $project->log(
466
                            "    -calling parent adder "
467
                            . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
468
                            Project::MSG_DEBUG
469
                        );
470
                        $nestedElement = new $elementClass();
471
                        $method->invoke($element, $nestedElement);
472
                    } catch (Exception $exc) {
473
                        throw new BuildException($exc->getMessage(), $exc);
474
                    }
475
                }
476
            }
477 1
            if ($nestedElement === null) {
478 1
                $msg = $this->getElementName($project, $element) . " doesn't support the '$elementName' creator/adder.";
479 1
                throw new BuildException($msg);
480
            }
481
        }
482
483 422
        if ($nestedElement instanceof ProjectComponent) {
484 378
            $nestedElement->setProject($project);
485
        }
486
487 422
        return $nestedElement;
488
    }
489
490
    /**
491
     * Creates a named nested element.
492
     *
493
     * @param  Project $project
494
     * @param  string $element
495
     * @param  string $child
496
     * @param  string|null $elementName
497
     * @return void
498
     * @throws BuildException
499
     */
500
    public function storeElement($project, $element, $child, $elementName = null)
501
    {
502
        if ($elementName === null) {
503
            return;
504
        }
505
506
        $storer = "addconfigured" . strtolower($elementName);
507
508
        if (isset($this->nestedStorers[$storer])) {
509
            $method = $this->nestedStorers[$storer];
510
511
            try {
512
                $project->log(
513
                    "    -calling storer " . $method->getDeclaringClass()->getName() . "::" . $method->getName() . "()",
514
                    Project::MSG_DEBUG
515
                );
516
                $method->invoke($element, $child);
517
            } catch (Exception $exc) {
518
                throw new BuildException($exc->getMessage(), $exc);
519
            }
520
        }
521
    }
522
523
    /**
524
     * Does the introspected class support PCDATA?
525
     *
526
     * @return boolean
527
     */
528 1
    public function supportsCharacters()
529
    {
530 1
        return ($this->methodAddText !== null);
531
    }
532
533
    /**
534
     * Return all attribues supported by the introspected class.
535
     *
536
     * @return string[]
537
     */
538
    public function getAttributes()
539
    {
540
        $attribs = [];
541
        foreach (array_keys($this->attributeSetters) as $setter) {
542
            $attribs[] = $this->getPropertyName($setter, "set");
543
        }
544
545
        return $attribs;
546
    }
547
548
    /**
549
     * Return all nested elements supported by the introspected class.
550
     *
551
     * @return string[]
552
     */
553
    public function getNestedElements()
554
    {
555
        return $this->nestedTypes;
556
    }
557
558
    /**
559
     * Get the name for an element.
560
     * When possible the full classnam (phing.tasks.system.PropertyTask) will
561
     * be returned.  If not available (loaded in taskdefs or typedefs) then the
562
     * XML element name will be returned.
563
     *
564
     * @param  Project $project
565
     * @param  object $element The Task or type element.
566
     * @return string  Fully qualified class name of element when possible.
567
     */
568 5
    public function getElementName(Project $project, $element)
569
    {
570 5
        $taskdefs = $project->getTaskDefinitions();
571 5
        $typedefs = $project->getDataTypeDefinitions();
572
573
        // check if class of element is registered with project (tasks & types)
574
        // most element types don't have a getTag() method
575 5
        $elClass = get_class($element);
576
577 5
        if (!in_array('getTag', get_class_methods($elClass))) {
578
            // loop through taskdefs and typesdefs and see if the class name
579
            // matches (case-insensitive) any of the classes in there
580 5
            foreach (array_merge($taskdefs, $typedefs) as $elName => $class) {
581 4
                if (0 === strcasecmp($elClass, StringHelper::unqualify($class))) {
582 4
                    return $class;
583
                }
584
            }
585
586 1
            return "$elClass (unknown)";
587
        }
588
589
// ->getTag() method does exist, so use it
590
        $elName = $element->getTag();
591
        if (isset($taskdefs[$elName])) {
592
            return $taskdefs[$elName];
593
        }
594
595
        if (isset($typedefs[$elName])) {
596
            return $typedefs[$elName];
597
        }
598
599
        return "$elName (unknown)";
600
    }
601
602
    /**
603
     * Extract the name of a property from a method name - subtracting  a given prefix.
604
     *
605
     * @param  string $methodName
606
     * @param  string $prefix
607
     * @return string
608
     */
609 30
    public function getPropertyName($methodName, $prefix)
610
    {
611 30
        $start = strlen($prefix);
612
613 30
        return strtolower(substr($methodName, $start));
614
    }
615
616
    /**
617
     * Prints warning message to screen if -debug was used.
618
     *
619
     * @param string $msg
620
     */
621
    public function warn($msg)
622
    {
623
        if (Phing::getMsgOutputLevel() === Project::MSG_DEBUG) {
624
            print("[IntrospectionHelper] " . $msg . "\n");
625
        }
626
    }
627
}
628