Completed
Pull Request — master (#425)
by Claus
02:33
created

StandardVariableProvider   F

Complexity

Total Complexity 82

Size/Duplication

Total Lines 442
Duplicated Lines 21.95 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 97
loc 442
rs 2
c 0
b 0
f 0
wmc 82
lcom 1
cbo 0

25 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A getScopeCopy() 0 8 3
A setSource() 0 11 2
A getSource() 0 4 1
A getAll() 0 4 1
C add() 0 46 14
A assertSubjectIsArrayOrObject() 0 14 3
A get() 0 4 1
A getByPath() 0 12 4
A remove() 0 6 2
A getAllIdentifiers() 0 4 1
A exists() 0 4 1
A __sleep() 0 4 1
A offsetSet() 0 4 1
A offsetUnset() 0 4 1
A offsetExists() 0 4 1
A offsetGet() 0 4 1
A getAccessorsForPath() 19 19 3
A resolveSubVariableReferences() 11 11 3
A extractSingleValue() 7 7 3
B canExtractWithAccessor() 14 14 11
B extractWithAccessor() 17 17 11
B detectAccessor() 21 21 8
A isExtractableThroughAsserter() 0 5 2
A extractThroughAsserter() 8 8 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like StandardVariableProvider 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 StandardVariableProvider, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace TYPO3Fluid\Fluid\Core\Variables;
3
4
/*
5
 * This file belongs to the package "TYPO3 Fluid".
6
 * See LICENSE.txt that was shipped with this package.
7
 */
8
9
/**
10
 * Class StandardVariableProvider
11
 */
12
class StandardVariableProvider implements VariableProviderInterface
13
{
14
    const ACCESSOR_ARRAY = 'array';
15
    const ACCESSOR_GETTER = 'getter';
16
    const ACCESSOR_ASSERTER = 'asserter';
17
    const ACCESSOR_PUBLICPROPERTY = 'public';
18
19
    /**
20
     * Variables stored in context
21
     *
22
     * @var mixed
23
     */
24
    protected $variables = [];
25
26
    /**
27
     * Variables, if any, with which to initialize this
28
     * VariableProvider.
29
     *
30
     * @param array $variables
31
     */
32
    public function __construct(array $variables = [])
33
    {
34
        $this->setSource($variables);
35
    }
36
37
    /**
38
     * @param array|\ArrayAccess $variables
39
     * @return VariableProviderInterface
40
     */
41
    public function getScopeCopy($variables)
42
    {
43
        if (!array_key_exists('settings', $variables) && array_key_exists('settings', $this->variables)) {
44
            $variables['settings'] = $this->variables['settings'];
45
        }
46
        $className = get_class($this);
47
        return new $className($variables);
48
    }
49
50
    /**
51
     * Set the source data used by this VariableProvider. The
52
     * source can be any type, but the type must of course be
53
     * supported by the VariableProvider itself.
54
     *
55
     * @param mixed $source
56
     * @return void
57
     */
58
    public function setSource($source)
59
    {
60
        // Rather than assign $this->variables = $source we iterate in order to make sure that
61
        // the logic within add() which is capable of storing nested variables, is used. In other
62
        // words: $source can contain dotted-path keys which become a nested array structure or
63
        // become overrides for values on objects.
64
        $this->variables = [];
65
        foreach ($source as $key => $value) {
66
            $this->add($key, $value);
67
        }
68
    }
69
70
    /**
71
     * @return mixed
72
     */
73
    public function getSource()
74
    {
75
        return $this->variables;
76
    }
77
78
    /**
79
     * Get every variable provisioned by the VariableProvider
80
     * implementing the interface. Must return an array or
81
     * ArrayAccess instance!
82
     *
83
     * @return array|\ArrayAccess
84
     */
85
    public function getAll()
86
    {
87
        return $this->variables;
88
    }
89
90
    /**
91
     * Add a variable to the context
92
     *
93
     * @param string $identifier Identifier of the variable to add
94
     * @param mixed $value The variable's value
95
     * @return void
96
     * @api
97
     */
98
    public function add($identifier, $value)
99
    {
100
        if (strpos($identifier, '.') === false) {
101
            $this->variables[$identifier] = $value;
102
        } else {
103
            $parts = explode('.', $identifier);
104
            $root = array_shift($parts);
105
            if (!isset($this->variables[$root])) {
106
                $this->variables[$root] = [];
107
            }
108
            $subject = &$this->variables[$root];
109
            $propertyName = array_pop($parts);
110
            $iterated = [$root];
111
112
            $this->assertSubjectIsArrayOrObject($subject, $iterated, $identifier);
113
114
            foreach ($parts as $part) {
115
                $iterated[] = $part;
116
                if (is_array($subject) || $subject instanceof \ArrayAccess || $subject instanceof \ArrayObject) {
117
                    if (!isset($subject[$part])) {
118
                        $subject[$part] = [];
119
                    }
120
                    $subject = &$subject[$part];
121
                } elseif (is_object($subject)) {
122
                    $subject = $this->extractSingleValue($subject, $part);
123
                } else {
124
                    $subject = null;
125
                }
126
127
                $this->assertSubjectIsArrayOrObject($subject, $iterated, $identifier);
128
            }
129
130
            // Assign the value on the $subject that is now a reference (either to somewhere in $this->variables
131
            // or itself an object that is by nature a reference).
132
            if (is_array($subject) || $subject instanceof \ArrayAccess || $subject instanceof \ArrayObject) {
133
                $subject[$propertyName] = $value;
134
            } elseif (is_object($subject)) {
135
                $setterMethodName = 'set' . ucfirst($propertyName);
136
                if (method_exists($subject, $setterMethodName)) {
137
                    $subject->$setterMethodName($value);
138
                } else {
139
                    $subject->$propertyName = $value;
140
                }
141
            }
142
        }
143
    }
144
145
    protected function assertSubjectIsArrayOrObject($subject, array $segmentsUntilSubject, $originalPathToSet)
146
    {
147
        if (!(is_array($subject) || is_object($subject))) {
148
            throw new \UnexpectedValueException(
149
                sprintf(
150
                    'Variable in path "%s" is scalar and is not the last segment in the full path "%s". ' .
151
                    'Refusing to coerce value of parent segment - cannot assign variable.',
152
                    implode('.', $segmentsUntilSubject),
153
                    $originalPathToSet
154
                ),
155
                1546878798
156
            );
157
        }
158
    }
159
160
    /**
161
     * Get a variable from the context. Throws exception if variable is not found in context.
162
     *
163
     * If "_all" is given as identifier, all variables are returned in an array,
164
     * if one of the other reserved variables are given, their appropriate value
165
     * they're representing is returned.
166
     *
167
     * @param string $identifier
168
     * @return mixed The variable value identified by $identifier
169
     * @api
170
     */
171
    public function get($identifier)
172
    {
173
        return $this->getByPath($identifier);
174
    }
175
176
    /**
177
     * Get a variable by dotted path expression, retrieving the
178
     * variable from nested arrays/objects one segment at a time.
179
     * If the second variable is passed, it is expected to contain
180
     * extraction method names (constants from VariableExtractor)
181
     * which indicate how each value is extracted.
182
     *
183
     * @param string $path
184
     * @param array $accessors Optional list of accessors (see class constants)
185
     * @return mixed
186
     */
187
    public function getByPath($path, array $accessors = [])
188
    {
189
        $subject = $this->variables;
190
        foreach (explode('.', $this->resolveSubVariableReferences($path)) as $index => $pathSegment) {
191
            $accessor = isset($accessors[$index]) ? $accessors[$index] : null;
192
            $subject = $this->extractSingleValue($subject, $pathSegment, $accessor);
193
            if ($subject === null) {
194
                break;
195
            }
196
        }
197
        return $subject;
198
    }
199
200
    /**
201
     * Remove a variable from context. Throws exception if variable is not found in context.
202
     *
203
     * @param string $identifier The identifier to remove
204
     * @return void
205
     * @api
206
     */
207
    public function remove($identifier)
208
    {
209
        if (array_key_exists($identifier, $this->variables)) {
210
            unset($this->variables[$identifier]);
211
        }
212
    }
213
214
    /**
215
     * Returns an array of all identifiers available in the context.
216
     *
217
     * @return array Array of identifier strings
218
     */
219
    public function getAllIdentifiers()
220
    {
221
        return array_keys($this->variables);
222
    }
223
224
    /**
225
     * Checks if this property exists in the VariableContainer.
226
     *
227
     * @param string $identifier
228
     * @return boolean TRUE if $identifier exists, FALSE otherwise
229
     * @api
230
     */
231
    public function exists($identifier)
232
    {
233
        return array_key_exists($identifier, $this->variables);
234
    }
235
236
    /**
237
     * Clean up for serializing.
238
     *
239
     * @return string[]
240
     */
241
    public function __sleep()
242
    {
243
        return ['variables'];
244
    }
245
246
    /**
247
     * Adds a variable to the context.
248
     *
249
     * @param string $identifier Identifier of the variable to add
250
     * @param mixed $value The variable's value
251
     * @return void
252
     */
253
    public function offsetSet($identifier, $value)
254
    {
255
        $this->add($identifier, $value);
256
    }
257
258
    /**
259
     * Remove a variable from context. Throws exception if variable is not found in context.
260
     *
261
     * @param string $identifier The identifier to remove
262
     * @return void
263
     */
264
    public function offsetUnset($identifier)
265
    {
266
        $this->remove($identifier);
267
    }
268
269
    /**
270
     * Checks if this property exists in the VariableContainer.
271
     *
272
     * @param string $identifier
273
     * @return boolean TRUE if $identifier exists, FALSE otherwise
274
     */
275
    public function offsetExists($identifier)
276
    {
277
        return $this->exists($identifier);
278
    }
279
280
    /**
281
     * Get a variable from the context. Throws exception if variable is not found in context.
282
     *
283
     * @param string $identifier
284
     * @return mixed The variable identified by $identifier
285
     */
286
    public function offsetGet($identifier)
287
    {
288
        return $this->get($identifier);
289
    }
290
291
    /**
292
     * @param string $propertyPath
293
     * @return array
294
     */
295 View Code Duplication
    public function getAccessorsForPath($propertyPath)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
296
    {
297
        $subject = $this->variables;
298
        $accessors = [];
299
        $propertyPathSegments = explode('.', $propertyPath);
300
        foreach ($propertyPathSegments as $index => $pathSegment) {
301
            $accessor = $this->detectAccessor($subject, $pathSegment);
302
            if ($accessor === null) {
303
                // Note: this may include cases of sub-variable references. When such
304
                // a reference is encountered the accessor chain is stopped and new
305
                // accessors will be detected for the sub-variable and all following
306
                // path segments since the variable is now fully dynamic.
307
                break;
308
            }
309
            $accessors[] = $accessor;
310
            $subject = $this->extractSingleValue($subject, $pathSegment);
311
        }
312
        return $accessors;
313
    }
314
315
    /**
316
     * @param string $propertyPath
317
     * @return string
318
     */
319 View Code Duplication
    protected function resolveSubVariableReferences($propertyPath)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
320
    {
321
        if (strpos($propertyPath, '{') !== false) {
322
            preg_match_all('/(\{.*\})/', $propertyPath, $matches);
323
            foreach ($matches[1] as $match) {
324
                $subPropertyPath = substr($match, 1, -1);
325
                $propertyPath = str_replace($match, $this->getByPath($subPropertyPath), $propertyPath);
326
            }
327
        }
328
        return $propertyPath;
329
    }
330
331
    /**
332
     * Extracts a single value from an array or object.
333
     *
334
     * @param mixed $subject
335
     * @param string $propertyName
336
     * @param string|null $accessor
337
     * @return mixed
338
     */
339 View Code Duplication
    protected function extractSingleValue($subject, $propertyName, $accessor = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
340
    {
341
        if (!$accessor || !$this->canExtractWithAccessor($subject, $propertyName, $accessor)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $accessor of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

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

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
342
            $accessor = $this->detectAccessor($subject, $propertyName);
343
        }
344
        return $this->extractWithAccessor($subject, $propertyName, $accessor);
345
    }
346
347
    /**
348
     * Returns TRUE if the data type of $subject is potentially compatible
349
     * with the $accessor.
350
     *
351
     * @param mixed $subject
352
     * @param string $propertyName
353
     * @param string $accessor
354
     * @return boolean
355
     */
356 View Code Duplication
    protected function canExtractWithAccessor($subject, $propertyName, $accessor)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
357
    {
358
        $class = is_object($subject) ? get_class($subject) : false;
359
        if ($accessor === self::ACCESSOR_ARRAY) {
360
            return (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)));
361
        } elseif ($accessor === self::ACCESSOR_GETTER) {
362
            return ($class !== false && method_exists($subject, 'get' . ucfirst($propertyName)));
363
        } elseif ($accessor === self::ACCESSOR_ASSERTER) {
364
            return ($class !== false && $this->isExtractableThroughAsserter($subject, $propertyName));
365
        } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY) {
366
            return ($class !== false && property_exists($subject, $propertyName));
367
        }
368
        return false;
369
    }
370
371
    /**
372
     * @param mixed $subject
373
     * @param string $propertyName
374
     * @param string $accessor
375
     * @return mixed
376
     */
377 View Code Duplication
    protected function extractWithAccessor(&$subject, $propertyName, $accessor)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
378
    {
379
        if ($accessor === self::ACCESSOR_ARRAY && is_array($subject) && array_key_exists($propertyName, $subject)
380
            || $subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)
381
        ) {
382
            return $subject[$propertyName];
383
        } elseif (is_object($subject)) {
384
            if ($accessor === self::ACCESSOR_GETTER) {
385
                return call_user_func_array([$subject, 'get' . ucfirst($propertyName)], []);
386
            } elseif ($accessor === self::ACCESSOR_ASSERTER) {
387
                return $this->extractThroughAsserter($subject, $propertyName);
388
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && property_exists($subject, $propertyName)) {
389
                return $subject->$propertyName;
390
            }
391
        }
392
        return null;
393
    }
394
395
    /**
396
     * Detect which type of accessor to use when extracting
397
     * $propertyName from $subject.
398
     *
399
     * @param mixed $subject
400
     * @param string $propertyName
401
     * @return string|NULL
402
     */
403 View Code Duplication
    protected function detectAccessor($subject, $propertyName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
404
    {
405
        if (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))) {
406
            return self::ACCESSOR_ARRAY;
407
        }
408
        if (is_object($subject)) {
409
            $upperCasePropertyName = ucfirst($propertyName);
410
            $getter = 'get' . $upperCasePropertyName;
411
            if (method_exists($subject, $getter)) {
412
                return self::ACCESSOR_GETTER;
413
            }
414
            if ($this->isExtractableThroughAsserter($subject, $propertyName)) {
415
                return self::ACCESSOR_ASSERTER;
416
            }
417
            if (property_exists($subject, $propertyName)) {
418
                return self::ACCESSOR_PUBLICPROPERTY;
419
            }
420
        }
421
422
        return null;
423
    }
424
425
    /**
426
     * Tests whether a property can be extracted through `is*` or `has*` methods.
427
     *
428
     * @param mixed $subject
429
     * @param string $propertyName
430
     * @return bool
431
     */
432
    protected function isExtractableThroughAsserter($subject, $propertyName)
433
    {
434
        return method_exists($subject, 'is' . ucfirst($propertyName))
435
            || method_exists($subject, 'has' . ucfirst($propertyName));
436
    }
437
438
    /**
439
     * Extracts a property through `is*` or `has*` methods.
440
     *
441
     * @param object $subject
442
     * @param string $propertyName
443
     * @return mixed
444
     */
445 View Code Duplication
    protected function extractThroughAsserter($subject, $propertyName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
446
    {
447
        if (method_exists($subject, 'is' . ucfirst($propertyName))) {
448
            return call_user_func_array([$subject, 'is' . ucfirst($propertyName)], []);
449
        }
450
451
        return call_user_func_array([$subject, 'has' . ucfirst($propertyName)], []);
452
    }
453
}
454