GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.

Variables::expand()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * See class comment
4
 *
5
 * PHP Version 5
6
 *
7
 * @category Netresearch
8
 * @package  Netresearch\Kite
9
 * @author   Christian Opitz <[email protected]>
10
 * @license  http://www.netresearch.de Netresearch Copyright
11
 * @link     http://www.netresearch.de
12
 */
13
14
namespace Netresearch\Kite;
15
16
use Netresearch\Kite\Exception;
17
use Netresearch\Kite\ExpressionLanguage\ExpressionLanguage;
18
19
/**
20
 * Variable container class
21
 *
22
 * @category Netresearch
23
 * @package  Netresearch\Kite
24
 * @author   Christian Opitz <[email protected]>
25
 * @license  http://www.netresearch.de Netresearch Copyright
26
 * @link     http://www.netresearch.de
27
 */
28
class Variables implements \ArrayAccess
29
{
30
    /**
31
     * @var array
32
     */
33
    private $variables = array();
34
35
    /**
36
     * @var Variables
37
     */
38
    private $parent;
39
40
    /**
41
     * @var Variables[]
42
     */
43
    private $children = array();
44
45
    /**
46
     * Variables constructor.
47
     *
48
     * @param Variables|null $parent   Parent variables container
49
     * @param array          $defaults Array with default values
50
     */
51
    public function __construct(Variables $parent = null, array $defaults = array())
52
    {
53
        $this->parent = $parent === $this ? null : $parent;
54
        if ($this->parent) {
55
            $this->parent->children[] = $this;
56
        }
57
        $variableConfiguration = $this->configureVariables();
58
        foreach ($variableConfiguration as $variable => $config) {
59
            if (is_array($config)) {
60
                if (!array_key_exists($variable, $defaults) && (!array_key_exists('required', $config) || !$config['required'])) {
61
                    $defaults[$variable] = array_key_exists('default', $config) ? $config['default'] : null;
62
                }
63
            } elseif ($config === null) {
64
                $defaults[$variable] = null;
65
            } elseif (!is_numeric($variable) && $config !== false) {
66
                throw new Exception('Invalid variable configuration');
67
            }
68
        }
69
        $this->variables += $defaults;
70
        $this->variables['_variableConfiguration'] = $variableConfiguration;
71
    }
72
73
    /**
74
     * Clone the children with this step as well, cause otherwise they'll lose
75
     * connection to parent
76
     *
77
     * @return void
78
     */
79
    function __clone()
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
80
    {
81
        $children = array();
82
        foreach ($this->children as $child) {
83
            $children[] = $clone = clone $child;
84
            $clone->parent = $this;
85
        }
86
        $this->children = $children;
87
    }
88
89
    /**
90
     * Bind to another parent
91
     *
92
     * @param Variables $newParent The new parent
93
     *
94
     * @return void
95
     */
96
    public function bindTo(Variables $newParent)
97
    {
98
        if ($this->parent) {
99
            foreach ($this->parent->children as $i => $child) {
100
                if ($child === $this) {
101
                    unset($this->parent->children[$i]);
102
                    break;
103
                }
104
            }
105
        }
106
        $this->parent = $newParent;
107
        $this->parent->children[] = $this;
108
    }
109
110
    /**
111
     * Get the parent variables object, if any
112
     *
113
     * @return Variables
114
     */
115
    public function getParent()
116
    {
117
        return $this->parent;
118
    }
119
120
    /**
121
     * Provide an array of variable configurations:
122
     * - Keys are the variable names
123
     * - Values can be
124
     * -- an array with the configuration - following keys are interpreted:
125
     * --- required (defaults to false)
126
     * --- default (defaults to null, ignored when required)
127
     * -- null: The default value will be set to null
128
     * -- false: No default value will be set
129
     *
130
     * Also you can add a "--" with numeric key, to state the boundaries of
131
     * current and parent variables configuration
132
     *
133
     * In either case variables in this configuration will always be fetched
134
     * from and saved to the very scope of this variables object (this)
135
     *
136
     * @return array
137
     */
138
    protected function configureVariables()
139
    {
140
        return array();
141
    }
142
143
    /**
144
     * Get a variable from this very object (unexpanded)
145
     *
146
     * @param mixed $offset Variable name
147
     *
148
     * @internal Required set/get/has/remove in order to access entries of Variables
149
     *           without looking up parents.
150
     *           You can however override this to do your own logic on plain offsets
151
     *           (no dot paths as in set/get/has/remove).
152
     *
153
     * @return mixed
154
     */
155
    public function &offsetGet($offset)
156
    {
157
        return $this->variables[$offset];
158
    }
159
160
    /**
161
     * Determine if a variable is available on this very object
162
     *
163
     * @param mixed $offset Variable name
164
     *
165
     * @internal See {@see Variables::offsetGet()}
166
     *
167
     * @return boolean
168
     */
169
    public function offsetExists($offset)
170
    {
171
        return array_key_exists($offset, $this->variables);
172
    }
173
174
    /**
175
     * Set a variable on this very object
176
     *
177
     * @param mixed $offset Variable name
178
     * @param mixed $value  The value
179
     *
180
     * @internal See {@see Variables::offsetGet()}
181
     *
182
     * @return void
183
     */
184
    public function offsetSet($offset, $value)
185
    {
186
        $this->variables[$offset] = $value;
187
    }
188
189
    /**
190
     * Unset a variable from this very object
191
     *
192
     * @param mixed $offset Variable name
193
     *
194
     * @internal See {@see Variables::offsetGet()}
195
     *
196
     * @return mixed
197
     */
198
    public function offsetUnset($offset)
199
    {
200
        unset($this->variables[$offset]);
201
    }
202
203
204
    /**
205
     * Find the first context that contains the first part of the variable
206
     * (this will always return current context - configured variables as well)
207
     *
208
     * @param array $variableParts The path split by dot
209
     *
210
     * @return Variables
211
     */
212
    private function findContext(&$variableParts)
213
    {
214
        if (!$variableParts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $variableParts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
215
            return $this;
216
        }
217
        if (array_key_exists($variableParts[0], $this->variables['_variableConfiguration'])) {
218
            // Configured variables always belong to the current scope
219
            return $this;
220
        }
221
        if ($variableParts[0] === 'this') {
222
            array_shift($variableParts);
223
        } elseif ($variableParts[0] === 'parent') {
224
            if (!$this->parent) {
225
                throw new Exception('No parent object available');
226
            }
227
            array_shift($variableParts);
228
            return $this->parent;
229
        } else {
230
            // Look up this and all parents, if they have the variable
231
            $parent = $this;
232
            do {
233
                if ($parent->offsetExists($variableParts[0])) {
234
                    return $parent;
235
                }
236
            } while ($parent = $parent->parent);
237
        }
238
        return $this;
239
    }
240
241
    /**
242
     * Get an expanded variable by it's path (second argument can be a default value)
243
     *
244
     * @param string $name The variable name
245
     *
246
     * @final This method is not to be overwritten as logic is to complex to deal
247
     *        with overrides because callee detection might be introduced later on
248
     *        and the expansion logic of dot path's is mostly internal.
249
     *        If you need to intercept the variable handling rather override
250
     *        offsetSet, offsetGet, offsetExists or offsetUnset.
251
     *
252
     * @return mixed
253
     */
254
    final public function get($name)
255
    {
256
        $parts = explode('.', $name);
257
258
        $value = $this->findContext($parts);
259
        while ($parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
260
            $part = array_shift($parts);
261
            if ($this->arrayKeyExists($value, $part)) {
262
                $value = $this->expand($value[$part]);
263
            } elseif ($this->propertyExists($value, $part)) {
264
                $value = $this->expand($value->$part);
265
            } else {
266
                if (func_num_args() > 1) {
267
                    return func_get_arg(1);
268
                } else {
269
                    throw new Exception\MissingVariableException('Missing variable ' . $name);
270
                }
271
            }
272
            if ($parts && $value instanceof self && $value !== $this) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
273
                try {
274
                    $args = func_get_args();
275
                    $args[0] = implode('.', $parts);
276
                    return call_user_func_array(array($value, 'get'), $args);
277
                } catch (Exception\MissingVariableException $e) {
278
                    throw new Exception\MissingVariableException('Missing variable ' . $name);
279
                }
280
            }
281
        }
282
283
        return $value;
284
    }
285
286
    /**
287
     * Determine if a variable path is available
288
     *
289
     * @param string $name The variable name
290
     *
291
     * @final See {@see Variables::get()}
292
     *
293
     * @return boolean
294
     */
295
    final public function has($name)
296
    {
297
        $parts = explode('.', $name);
298
        $value = $this->findContext($parts);
299
        while ($parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
300
            $part = array_shift($parts);
301
            if ($this->arrayKeyExists($value, $part)) {
302
                $value = $this->expand($value[$part]);
303
            } elseif ($this->propertyExists($value, $part)) {
304
                $value = $this->expand($value->$part);
305
            } else {
306
                return false;
307
            }
308
            if ($parts && $value instanceof self && $value !== $this) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
309
                return $value->has(implode('.', $parts));
310
            }
311
        }
312
313
        return true;
314
    }
315
316
    /**
317
     * Set a variable by it's path
318
     *
319
     * @param string $name  The variable name
320
     * @param mixed  $value The value
321
     *
322
     * @final See {@see Variables::get()}
323
     *
324
     * @return $this
325
     */
326
    final public function set($name, $value)
327
    {
328
        $parts = explode('.', $name);
329
        if (in_array($name, array('this', 'parent'), true)) {
330
            throw new Exception('this and parent are variable names you may not override');
331
        }
332
333
        $finalPart = array_pop($parts);
334
        $parent = $this->findContext($parts);
335 View Code Duplication
        while ($parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
336
            $part = array_shift($parts);
337
            if ($this->arrayKeyExists($parent, $part)) {
338
                $parent = &$parent[$part];
339
            } elseif ($this->propertyExists($parent, $part)) {
340
                $parent = &$parent->$part;
341
            } else {
342
                $parent = $this->expand($parent);
343
                if (is_object($parent) || is_array($parent)) {
344
                    array_unshift($parts, $part);
345
                    continue;
346
                } else {
347
                    throw new Exception('Can not set values on primitives or null');
348
                }
349
            }
350
            if ($parts && $parent instanceof self && $parent !== $this) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
351
                $parent->set(implode('.', $parts), $value);
352
                return $this;
353
            }
354
        }
355
        $this->filterSpecialNames($finalPart, $value);
356
        if (is_array($parent) || $parent instanceof \ArrayAccess) {
357
            $parent[$finalPart] = $value;
358
        } elseif (is_object($parent)) {
359
            $parent->$finalPart = $value;
360
        }
361
        return $this;
362
    }
363
364
    /**
365
     * Unset a variable by it's path
366
     *
367
     * @param string $name Variable name
368
     *
369
     * @final See {@see Variables::get()}
370
     *
371
     * @return $this
372
     */
373
    final public function remove($name)
374
    {
375
        $parts = explode('.', $name);
376
        $finalPart = array_pop($parts);
377
        $parent = $this->findContext($parts);
378 View Code Duplication
        while ($parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
379
            $part = array_shift($parts);
380
            if ($this->arrayKeyExists($parent, $part)) {
381
                $parent = &$parent[$part];
382
            } elseif ($this->propertyExists($parent, $part)) {
383
                $parent = &$parent->$part;
384
            } else {
385
                $parent = $this->expand($parent);
386
                if (is_object($parent) || is_array($parent)) {
387
                    array_unshift($parts, $part);
388
                    continue;
389
                } else {
390
                    return $this;
391
                }
392
            }
393
            if ($parts && $parent instanceof self && $parent !== $this) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
394
                $parent->remove(implode('.', $parts));
395
                return $this;
396
            }
397
        }
398
399
        if ($this->arrayKeyExists($parent, $finalPart)) {
400
            unset($parent[$finalPart]);
401
        } elseif ($this->propertyExists($parent, $finalPart)) {
402
            unset($parent->$finalPart);
403
        }
404
405
        return $this;
406
    }
407
408
    /**
409
     * Determine if a variable is an array or accessible as array and has the key
410
     *
411
     * This method is required as isset returns false on existing keys with null
412
     * values and array_key_exists doesn't invoke {@see \ArrayAccess::offsetExists()}
413
     *
414
     * Use this BEFORE {@see Variables::propertyExists()} in according checks, as
415
     * it will match {@see Variables} objects as well. This will likely speed things
416
     * up a little but more importantly it will avoid that private or protected
417
     * properties are detected by {@see Variables::propertyExists()}.
418
     *
419
     * @param array|\ArrayAccess $array Array
420
     * @param string             $key   The key
421
     *
422
     * @return bool
423
     */
424
    private function arrayKeyExists($array, $key)
425
    {
426
        if (is_array($array)) {
427
            return array_key_exists($key, $array);
428
        } elseif ($array instanceof \ArrayAccess) {
429
            return $array->offsetExists($key);
430
        }
431
        return false;
432
    }
433
434
    /**
435
     * Determine if a variable is an object and has a property
436
     *
437
     * This method is required as isset returns false on existing properties with
438
     * null values and property_exists doesn't invoke {@see __isset()}
439
     *
440
     * @param object $object   The object
441
     * @param string $property The property
442
     *
443
     * @return bool
444
     */
445
    private function propertyExists($object, $property)
446
    {
447
        if (is_object($object)) {
448
            if (property_exists($object, $property)) {
449
                return true;
450
            }
451
            if (method_exists($object, '__isset') && $object->__isset($property)) {
452
                return true;
453
            }
454
        }
455
        return false;
456
    }
457
458
    /**
459
     * Set from array
460
     *
461
     * @param array $values Array with key value pairs
462
     *
463
     * @return $this
464
     */
465
    public function setFromArray($values)
466
    {
467
        foreach ($values as $key => $value) {
468
            $this->set($key, $value);
469
        }
470
        return $this;
471
    }
472
473
    /**
474
     * Expand expressions within the $value
475
     *
476
     * @param mixed $value The value
477
     *
478
     * @return string|mixed
479
     */
480
    public function expand($value)
481
    {
482
        static $expressionEngine;
483
        if (!$expressionEngine) {
484
            $expressionEngine = new ExpressionLanguage();
485
        }
486
        return $expressionEngine->evaluate($value, [ExpressionLanguage::VARIABLES_KEY => $this]);
487
    }
488
489
    /**
490
     * Handles special variable names
491
     *
492
     * @param string $key   Current part of the complete variable name
493
     * @param mixed  $value The value
494
     *
495
     * @return void
496
     */
497
    private function filterSpecialNames(&$key, &$value)
498
    {
499
        if ($key === 'node' && is_array($value)) {
500
            $key = 'nodes';
501
            $value = array($value);
502
        }
503
        if ($key === 'nodes') {
504
            $nodes = array();
505
            foreach ($value as $id => $options) {
506
                if ($options instanceof Node) {
507
                    $node = $options;
508
                } else {
509
                    $node = new Node($this);
510
                    $node->setFromArray($options);
511
                }
512
                $node->set('id', $id);
513
                $nodes[$id] = $node;
514
            }
515
            $value = $nodes;
516
        }
517
    }
518
}
519
?>
520