PropertyHelper   F
last analyzed

Complexity

Total Complexity 67

Size/Duplication

Total Lines 559
Duplicated Lines 0 %

Test Coverage

Coverage 83.33%

Importance

Changes 1
Bugs 1 Features 0
Metric Value
wmc 67
eloc 155
c 1
b 1
f 0
dl 0
loc 559
ccs 145
cts 174
cp 0.8333
rs 3.04

19 Methods

Rating   Name   Duplication   Size   Complexity  
A getPropertyHook() 0 17 6
B parsePropertyString() 0 30 7
B getProperty() 0 23 7
A setPropertyHook() 0 4 2
A copyInheritedProperties() 0 5 3
A setNext() 0 3 1
A setProject() 0 3 1
A copyUserProperties() 0 5 3
A getPropertyHelper() 0 15 2
A setUserProperty() 0 14 4
A getInheritedProperties() 0 3 1
A getNext() 0 3 1
A getUserProperties() 0 3 1
A getUserProperty() 0 11 3
A getProperties() 0 3 1
A setNewProperty() 0 17 5
A setInheritedProperty() 0 20 4
B setProperty() 0 34 7
B replaceProperties() 0 58 8

How to fix   Complexity   

Complex Class

Complex classes like PropertyHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PropertyHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing;
22
23
use Phing\Exception\BuildException;
24
use Phing\Type\PropertyValue;
25
use Phing\Util\StringHelper;
26
27
/**
28
 * Component creation and configuration.
29
 *
30
 * @author Siad Ardroumli <[email protected]>
31
 */
32
class PropertyHelper
33
{
34
    /**
35
     * @var Project
36
     */
37
    private $project;
38
39
    /**
40
     * @var null|PropertyHelper
41
     */
42
    private $next;
43
44
    /**
45
     * Project properties map (usually String to String).
46
     */
47
    private $properties = [];
48
49
    /**
50
     * Map of "user" properties (as created in the Ant task, for example).
51
     * Note that these key/value pairs are also always put into the
52
     * project properties, so only the project properties need to be queried.
53
     * Mapping is String to String.
54
     */
55
    private $userProperties = [];
56
57
    /**
58
     * Map of inherited "user" properties - that are those "user"
59
     * properties that have been created by tasks and not been set
60
     * from the command line or a GUI tool.
61
     * Mapping is String to String.
62
     */
63
    private $inheritedProperties = [];
64
65
    /**
66
     * There are 2 ways to hook into property handling:
67
     *  - you can replace the main PropertyHelper. The replacement is required
68
     * to support the same semantics (of course :-).
69
     *
70
     *  - you can chain a property helper capable of storing some properties.
71
     *  Again, you are required to respect the immutability semantics (at
72
     *  least for non-dynamic properties)
73
     *
74
     * @param PropertyHelper $next the next property helper in the chain
75
     */
76
    public function setNext(PropertyHelper $next): void
77
    {
78
        $this->next = $next;
79
    }
80
81
    /**
82
     * Get the next property helper in the chain.
83
     *
84
     * @return null|PropertyHelper the next property helper
85
     */
86 927
    public function getNext(): ?PropertyHelper
87
    {
88 927
        return $this->next;
89
    }
90
91
    /**
92
     * Factory method to create a property processor.
93
     * Users can provide their own or replace it using "ant.PropertyHelper"
94
     * reference. User tasks can also add themselves to the chain, and provide
95
     * dynamic properties.
96
     *
97
     * @param Project $project the project fro which the property helper is required
98
     *
99
     * @return PropertyHelper the project's property helper
100
     */
101 929
    public static function getPropertyHelper(Project $project): PropertyHelper
102
    {
103
        /**
104
         * @var PropertyHelper $helper
105
         */
106 929
        $helper = $project->getReference('phing.PropertyHelper');
107 929
        if (null !== $helper) {
108 908
            return $helper;
109
        }
110 929
        $helper = new self();
111 929
        $helper->setProject($project);
112
113 929
        $project->addReference('phing.PropertyHelper', $helper);
114
115 929
        return $helper;
116
    }
117
118
    // --------------------  Methods to override  --------------------
119
120
    /**
121
     * Sets a property. Any existing property of the same name
122
     * is overwritten, unless it is a user property. Will be called
123
     * from setProperty().
124
     *
125
     * If all helpers return false, the property will be saved in
126
     * the default properties table by setProperty.
127
     *
128
     * @param string $ns        the namespace that the property is in (currently
129
     *                          not used
130
     * @param string $name      The name of property to set.
131
     *                          Must not be
132
     *                          <code>null</code>.
133
     * @param string $value     The new value of the property.
134
     *                          Must not be <code>null</code>.
135
     * @param bool   $inherited true if this property is inherited (an [sub]ant[call] property)
136
     * @param bool   $user      true if this property is a user property
137
     * @param bool   $isNew     true is this is a new property
138
     *
139
     * @return bool true if this helper has stored the property, false if it
140
     *              couldn't. Each helper should delegate to the next one (unless it
141
     *              has a good reason not to).
142
     */
143 926
    public function setPropertyHook($ns, $name, $value, $inherited, $user, $isNew): bool
144
    {
145 926
        return null !== $this->getNext()
146 926
            && $this->getNext()->setPropertyHook($ns, $name, $value, $inherited, $user, $isNew);
147
    }
148
149
    /**
150
     * Get a property. If all hooks return null, the default
151
     * tables will be used.
152
     *
153
     * @param string $ns   namespace of the sought property
154
     * @param string $name name of the sought property
155
     * @param bool   $user true if this is a user property
156
     *
157
     * @return null|string the property, if returned by a hook, or null if none
158
     */
159 908
    public function getPropertyHook($ns, $name, $user): ?string
160
    {
161 908
        if (null !== $this->getNext()) {
162
            $o = $this->getNext()->getPropertyHook($ns, $name, $user);
163
            if (null !== $o) {
164
                return $o;
165
            }
166
        }
167
168 908
        if (null !== $this->project && StringHelper::startsWith('toString:', $name)) {
169 1
            $name = StringHelper::substring($name, strlen('toString:'));
170 1
            $v = $this->project->getReference($name);
171
172 1
            return (null === $v) ? null : (string) $v;
173
        }
174
175 908
        return null;
176
    }
177
178
    // -------------------- Optional methods   --------------------
179
    // You can override those methods if you want to optimize or
180
    // do advanced things (like support a special syntax).
181
    // The methods do not chain - you should use them when embedding ant
182
    // (by replacing the main helper)
183
184
    /**
185
     * Replaces <code>${xxx}</code> style constructions in the given value
186
     * with the string value of the corresponding data types.
187
     *
188
     * @param string   $value The string to be scanned for property references.
189
     *                        May be <code>null</code>, in which case this
190
     *                        method returns immediately with no effect.
191
     * @param string[] $keys  Mapping (String to String) of property names to their
192
     *                        values. If <code>null</code>, only project properties
193
     *                        will be used.
194
     *
195
     * @throws BuildException if the string contains an opening
196
     *                        <code>${</code> without a closing
197
     *                        <code>}</code>
198
     *
199
     * @return string the original string with the properties replaced, or
200
     *                <code>null</code> if the original string is <code>null</code>
201
     */
202 765
    public function replaceProperties(?string $value, ?array $keys): ?string
203
    {
204 765
        if (null === $value) {
205
            return null;
206
        }
207 765
        if (null === $keys) {
0 ignored issues
show
introduced by
The condition null === $keys is always false.
Loading history...
208 1
            $keys = $this->project->getProperties();
209
        }
210
        // Because we're not doing anything special (like multiple passes),
211
        // regex is the simplest / fastest.  PropertyTask, though, uses
212
        // the old parsePropertyString() method, since it has more stringent
213
        // requirements.
214
215 765
        $sb = $value;
216 765
        $iteration = 0;
217
        // loop to recursively replace tokens
218 765
        while (false !== strpos($sb, '${')) {
219 464
            $sb = preg_replace_callback(
220 464
                '/\$\{([^\$}]+)\}/',
221 464
                function ($matches) use ($keys) {
222 464
                    $propertyName = $matches[1];
223
224 464
                    $replacement = null;
225 464
                    if (array_key_exists($propertyName, $keys)) {
226 457
                        $replacement = $keys[$propertyName];
227
                    }
228
229 464
                    if (null === $replacement) {
230 11
                        $replacement = $this->getProperty(null, $propertyName);
231
                    }
232
233 464
                    if (null === $replacement) {
234 10
                        $this->project->log(
235 10
                            'Property ${' . $propertyName . '} has not been set.',
236 10
                            Project::MSG_VERBOSE
237 10
                        );
238
239 10
                        return $matches[0];
240
                    }
241
242 458
                    $this->project->log(
243 458
                        'Property ${' . $propertyName . '} => ' . (string) $replacement,
244 458
                        Project::MSG_VERBOSE
245 458
                    );
246
247 458
                    return $replacement;
248 464
                },
249 464
                $sb
250 464
            );
251
252
            // keep track of iterations so we can break out of otherwise infinite loops.
253 464
            ++$iteration;
254 464
            if (5 === $iteration) {
255 10
                return $sb;
256
            }
257
        }
258
259 764
        return $sb;
260
    }
261
262
    // -------------------- Default implementation  --------------------
263
    // Methods used to support the default behavior and provide backward
264
    // compatibility. Some will be deprecated, you should avoid calling them.
265
266
    /**
267
     * Default implementation of setProperty. Will be called from Project.
268
     *  This is the original 1.5 implementation, with calls to the hook
269
     *  added.
270
     *
271
     * @param string $ns      the namespace for the property (currently not used)
272
     * @param string $name    the name of the property
273
     * @param string $value   the value to set the property to
274
     * @param bool   $verbose if this is true output extra log messages
275
     *
276
     * @return bool true if the property is set
277
     */
278 926
    public function setProperty($ns, $name, $value, $verbose): bool
279
    {
280
        // user (CLI) properties take precedence
281 926
        if (isset($this->userProperties[$name])) {
282 25
            if ($verbose) {
283
                $this->project->log('Override ignored for user property ' . $name, Project::MSG_VERBOSE);
284
            }
285
286 25
            return false;
287
        }
288
289 926
        $done = $this->setPropertyHook($ns, $name, $value, false, false, false);
290 926
        if ($done) {
291
            return true;
292
        }
293
294 926
        if ($verbose && isset($this->properties[$name])) {
295 4
            $this->project->log(
296 4
                'Overriding previous definition of property ' . $name,
297 4
                Project::MSG_VERBOSE
298 4
            );
299
        }
300
301 926
        if ($verbose) {
302 260
            $this->project->log(
303 260
                'Setting project property: ' . $name . ' -> '
304 260
                . $value,
305 260
                Project::MSG_DEBUG
306 260
            );
307
        }
308 926
        $this->properties[$name] = $value;
309 926
        $this->project->addReference($name, new PropertyValue($value));
310
311 926
        return true;
312
    }
313
314
    /**
315
     * Sets a property if no value currently exists. If the property
316
     * exists already, a message is logged and the method returns with
317
     * no other effect.
318
     *
319
     * @param string $ns    the namespace for the property (currently not used)
320
     * @param string $name  the name of property to set
321
     * @param string $value the new value of the property
322
     */
323 557
    public function setNewProperty($ns, string $name, string $value): void
324
    {
325 557
        if (isset($this->properties[$name])) {
326 6
            $this->project->log('Override ignored for property ' . $name, Project::MSG_VERBOSE);
327
328 6
            return;
329
        }
330
331 555
        $done = $this->setPropertyHook($ns, $name, $value, false, false, true);
332 555
        if ($done) {
333
            return;
334
        }
335
336 555
        $this->project->log('Setting project property: ' . $name . ' -> ' . $value, Project::MSG_DEBUG);
337 555
        if (null !== $name && null !== $value) {
0 ignored issues
show
introduced by
The condition null !== $value is always true.
Loading history...
338 555
            $this->properties[$name] = $value;
339 555
            $this->project->addReference($name, new PropertyValue($value));
340
        }
341
    }
342
343
    /**
344
     * Sets a user property, which cannot be overwritten by
345
     * set/unset property calls. Any previous value is overwritten.
346
     *
347
     * @param string $ns    the namespace for the property (currently not used)
348
     * @param string $name  the name of property to set
349
     * @param string $value the new value of the property
350
     */
351 907
    public function setUserProperty($ns, string $name, string $value): void
352
    {
353 907
        if (null === $name || null === $value) {
0 ignored issues
show
introduced by
The condition null === $value is always false.
Loading history...
354
            return;
355
        }
356 907
        $this->project->log('Setting ro project property: ' . $name . ' -> ' . $value, Project::MSG_DEBUG);
357 907
        $this->userProperties[$name] = $value;
358
359 907
        $done = $this->setPropertyHook($ns, $name, $value, false, true, false);
360 907
        if ($done) {
361
            return;
362
        }
363 907
        $this->properties[$name] = $value;
364 907
        $this->project->addReference($name, new PropertyValue($value));
365
    }
366
367
    /**
368
     * Sets an inherited user property, which cannot be overwritten by set/unset
369
     * property calls. Any previous value is overwritten. Also marks
370
     * these properties as properties that have not come from the
371
     * command line.
372
     *
373
     * @param string $ns    the namespace for the property (currently not used)
374
     * @param string $name  the name of property to set
375
     * @param string $value the new value of the property
376
     */
377 24
    public function setInheritedProperty($ns, string $name, string $value): void
378
    {
379 24
        if (null === $name || null === $value) {
0 ignored issues
show
introduced by
The condition null === $value is always false.
Loading history...
380
            return;
381
        }
382 24
        $this->inheritedProperties[$name] = $value;
383
384 24
        $this->project->log(
385 24
            'Setting ro project property: ' . $name . ' -> '
386 24
            . $value,
387 24
            Project::MSG_DEBUG
388 24
        );
389 24
        $this->userProperties[$name] = $value;
390
391 24
        $done = $this->setPropertyHook($ns, $name, $value, true, false, false);
392 24
        if ($done) {
393
            return;
394
        }
395 24
        $this->properties[$name] = $value;
396 24
        $this->project->addReference($name, new PropertyValue($value));
397
    }
398
399
    // -------------------- Getting properties  --------------------
400
401
    /**
402
     * Returns the value of a property, if it is set.  You can override
403
     * this method in order to plug your own storage.
404
     *
405
     * @param string      $ns   the namespace for the property (currently not used)
406
     * @param null|string $name The name of the property.
407
     *                          May be <code>null</code>, in which case
408
     *                          the return value is also <code>null</code>.
409
     *
410
     * @return mixed the property value, or <code>null</code> for no match
411
     *               or if a <code>null</code> name is provided
412
     */
413 908
    public function getProperty($ns, ?string $name)
414
    {
415 908
        if (null === $name) {
416
            return null;
417
        }
418 908
        $o = $this->getPropertyHook($ns, $name, false);
419 908
        if (null !== $o) {
420 1
            return $o;
421
        }
422
423 908
        $found = $this->properties[$name] ?? null;
424
        // check to see if there are unresolved property references
425 908
        if (null !== $found && false !== strpos($found, '${')) {
426
            // attempt to resolve properties
427 1
            $found = $this->replaceProperties($found, null);
428 1
            if (StringHelper::startsWith('${', $found) && StringHelper::endsWith('}', $found)) {
429 1
                $found = null;
430
            }
431
            // save resolved value
432 1
            $this->properties[$name] = $found;
433
        }
434
435 908
        return $found;
436
    }
437
438
    /**
439
     * Returns the value of a user property, if it is set.
440
     *
441
     * @param string      $ns   the namespace for the property (currently not used)
442
     * @param null|string $name The name of the property.
443
     *                          May be <code>null</code>, in which case
444
     *                          the return value is also <code>null</code>.
445
     *
446
     * @return null|string the property value, or <code>null</code> for no match
447
     *                     or if a <code>null</code> name is provided
448
     */
449 13
    public function getUserProperty($ns, ?string $name): ?string
450
    {
451 13
        if (null === $name) {
452
            return null;
453
        }
454 13
        $o = $this->getPropertyHook($ns, $name, true);
455 13
        if (null !== $o) {
456
            return $o;
457
        }
458
459 13
        return $this->userProperties[$name] ?? null;
460
    }
461
462
    // -------------------- Access to property tables  --------------------
463
    // This is used to support ant call and similar tasks. It should be
464
    // deprecated, it is possible to use a better (more efficient)
465
    // mechanism to preserve the context.
466
467
    /**
468
     * Returns a copy of the properties table.
469
     *
470
     * @return array a hashtable containing all properties
471
     *               (including user properties)
472
     */
473 908
    public function getProperties(): array
474
    {
475 908
        return $this->properties;
476
    }
477
478
    /**
479
     * Returns a copy of the user property hashtable.
480
     *
481
     * @return array a hashtable containing just the user properties
482
     */
483
    public function getUserProperties(): array
484
    {
485
        return $this->userProperties;
486
    }
487
488
    public function getInheritedProperties(): array
489
    {
490
        return $this->inheritedProperties;
491
    }
492
493
    /**
494
     * Copies all user properties that have not been set on the
495
     * command line or a GUI tool from this instance to the Project
496
     * instance given as the argument.
497
     *
498
     * <p>To copy all "user" properties, you will also have to call
499
     * {@link #copyUserProperties copyUserProperties}.</p>
500
     *
501
     * @param Project $other the project to copy the properties to.  Must not be null.
502
     */
503
    public function copyInheritedProperties(Project $other): void
504
    {
505
        foreach ($this->inheritedProperties as $arg => $value) {
506
            if (null === $other->getUserProperty($arg)) {
507
                $other->setInheritedProperty($arg, (string) $this->inheritedProperties[$arg]);
508
            }
509
        }
510
    }
511
512
    /**
513
     * Copies all user properties that have been set on the command
514
     * line or a GUI tool from this instance to the Project instance
515
     * given as the argument.
516
     *
517
     * <p>To copy all "user" properties, you will also have to call
518
     * {@link #copyInheritedProperties copyInheritedProperties}.</p>
519
     *
520
     * @param Project $other the project to copy the properties to
521
     */
522 37
    public function copyUserProperties(Project $other): void
523
    {
524 37
        foreach ($this->userProperties as $arg => $value) {
525 37
            if (!isset($this->inheritedProperties[$arg])) {
526 37
                $other->setUserProperty($arg, $value);
527
            }
528
        }
529
    }
530
531
    /**
532
     * Parses a string containing <code>${xxx}</code> style property
533
     * references into two lists. The first list is a collection
534
     * of text fragments, while the other is a set of string property names.
535
     * <code>null</code> entries in the first list indicate a property
536
     * reference from the second list.
537
     *
538
     * It can be overridden with a more efficient or customized version.
539
     *
540
     * @param string $value        text to parse
541
     * @param array  $fragments    list to add text fragments to
542
     * @param array  $propertyRefs list to add property names to
543
     *
544
     * @throws BuildException if the string contains an opening
545
     *                        <code>${</code> without a closing
546
     *                        <code>}</code>
547
     */
548 21
    public function parsePropertyString(string $value, array &$fragments, array &$propertyRefs): void
549
    {
550 21
        $prev = 0;
551
552 21
        while (($pos = strpos($value, '$', $prev)) !== false) {
553 9
            if ($pos > $prev) {
554 7
                $fragments[] = StringHelper::substring($value, $prev, $pos - 1);
555
            }
556 9
            if ($pos === (strlen($value) - 1)) {
557
                $fragments[] = '$';
558
                $prev = $pos + 1;
559 9
            } elseif ('{' !== $value[$pos + 1]) {
560
                // the string positions were changed to value-1 to correct
561
                // a fatal error coming from function substring()
562
                $fragments[] = StringHelper::substring($value, $pos, $pos + 1);
563
                $prev = $pos + 2;
564
            } else {
565 9
                $endName = strpos($value, '}', $pos);
566 9
                if (false === $endName) {
567
                    throw new BuildException("Syntax error in property: {$value}");
568
                }
569 9
                $propertyName = StringHelper::substring($value, $pos + 2, $endName - 1);
570 9
                $fragments[] = null;
571 9
                $propertyRefs[] = $propertyName;
572 9
                $prev = $endName + 1;
573
            }
574
        }
575
576 21
        if ($prev < strlen($value)) {
577 21
            $fragments[] = StringHelper::substring($value, $prev);
578
        }
579
    }
580
581
    // --------------------  Hook management  --------------------
582
583
    /**
584
     * Set the project for which this helper is performing property resolution.
585
     *
586
     * @param Project $p the project instance
587
     */
588 929
    private function setProject(Project $p): void
589
    {
590 929
        $this->project = $p;
591
    }
592
}
593