Passed
Push — main ( 346559...3ca6e5 )
by Siad
05:18
created

PropertyHelper::getPropertyHelper()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 2

Importance

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