Passed
Push — master ( d68b8d...e5c614 )
by Siad
10:45
created

PropertyHelper::replaceProperties()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 58
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 8.017

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 30
c 1
b 0
f 0
nc 7
nop 2
dl 0
loc 58
ccs 29
cts 31
cp 0.9355
crap 8.017
rs 8.1954

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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