Completed
Push — master ( 3190f6...102f9d )
by Sam
48s queued 18s
created

SSViewer_DataPresenter   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 425
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 150
dl 0
loc 425
rs 3.36
c 0
b 0
f 0
wmc 63

13 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
B getPropertiesFromProvider() 0 45 8
A cacheIteratorProperties() 0 10 2
A cacheGlobalProperties() 0 9 2
A getObj() 0 7 2
A processTemplateOverride() 0 20 5
A getInjectedValue() 0 27 5
A castValue() 0 13 6
A popScope() 0 10 2
B __call() 0 24 8
A pushScope() 0 17 3
B getValueSource() 0 50 10
B obj() 0 31 7

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
namespace SilverStripe\View;
4
5
use InvalidArgumentException;
6
use SilverStripe\Core\ClassInfo;
7
use SilverStripe\ORM\FieldType\DBField;
8
9
/**
10
 * This extends SSViewer_Scope to mix in data on top of what the item provides. This can be "global"
11
 * data that is scope-independant (like BaseURL), or type-specific data that is layered on top cross-cut like
12
 * (like $FirstLast etc).
13
 *
14
 * It's separate from SSViewer_Scope to keep that fairly complex code as clean as possible.
15
 */
16
class SSViewer_DataPresenter extends SSViewer_Scope
0 ignored issues
show
Bug introduced by
The type SilverStripe\View\SSViewer_Scope was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
17
{
18
    /**
19
     * List of global property providers
20
     *
21
     * @internal
22
     * @var TemplateGlobalProvider[]|null
23
     */
24
    private static $globalProperties = null;
25
26
    /**
27
     * List of global iterator providers
28
     *
29
     * @internal
30
     * @var TemplateIteratorProvider[]|null
31
     */
32
    private static $iteratorProperties = null;
33
34
    /**
35
     * Overlay variables. Take precedence over anything from the current scope
36
     *
37
     * @var array|null
38
     */
39
    protected $overlay;
40
41
    /**
42
     * Flag for whether overlay should be preserved when pushing a new scope
43
     *
44
     * @see SSViewer_DataPresenter::pushScope()
45
     * @var bool
46
     */
47
    protected $preserveOverlay = false;
48
49
    /**
50
     * Underlay variables. Concede precedence to overlay variables or anything from the current scope
51
     *
52
     * @var array
53
     */
54
    protected $underlay;
55
56
    /**
57
     * @var object $item
58
     * @var array $overlay
59
     * @var array $underlay
60
     * @var SSViewer_Scope $inheritedScope
61
     */
62
    public function __construct(
63
        $item,
64
        array $overlay = null,
65
        array $underlay = null,
66
        SSViewer_Scope $inheritedScope = null
67
    ) {
68
        parent::__construct($item, $inheritedScope);
69
70
        $this->overlay = $overlay ?: [];
71
        $this->underlay = $underlay ?: [];
72
73
        $this->cacheGlobalProperties();
74
        $this->cacheIteratorProperties();
75
    }
76
77
    /**
78
     * Build cache of global properties
79
     */
80
    protected function cacheGlobalProperties()
81
    {
82
        if (self::$globalProperties !== null) {
83
            return;
84
        }
85
86
        self::$globalProperties = $this->getPropertiesFromProvider(
87
            TemplateGlobalProvider::class,
88
            'get_template_global_variables'
89
        );
90
    }
91
92
    /**
93
     * Build cache of global iterator properties
94
     */
95
    protected function cacheIteratorProperties()
96
    {
97
        if (self::$iteratorProperties !== null) {
98
            return;
99
        }
100
101
        self::$iteratorProperties = $this->getPropertiesFromProvider(
102
            TemplateIteratorProvider::class,
103
            'get_template_iterator_variables',
104
            true // Call non-statically
105
        );
106
    }
107
108
    /**
109
     * @var string $interfaceToQuery
110
     * @var string $variableMethod
111
     * @var boolean $createObject
112
     * @return array
113
     */
114
    protected function getPropertiesFromProvider($interfaceToQuery, $variableMethod, $createObject = false)
115
    {
116
        $methods = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $methods is dead and can be removed.
Loading history...
117
118
        $implementors = ClassInfo::implementorsOf($interfaceToQuery);
119
        if ($implementors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $implementors 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...
120
            foreach ($implementors as $implementor) {
121
                // Create a new instance of the object for method calls
122
                if ($createObject) {
123
                    $implementor = new $implementor();
124
                    $exposedVariables = $implementor->$variableMethod();
125
                } else {
126
                    $exposedVariables = $implementor::$variableMethod();
127
                }
128
129
                foreach ($exposedVariables as $varName => $details) {
130
                    if (!is_array($details)) {
131
                        $details = [
132
                            'method' => $details,
133
                            'casting' => ViewableData::config()->uninherited('default_cast')
134
                        ];
135
                    }
136
137
                    // If just a value (and not a key => value pair), use method name for both key and value
138
                    if (is_numeric($varName)) {
139
                        $varName = $details['method'];
140
                    }
141
142
                    // Add in a reference to the implementing class (might be a string class name or an instance)
143
                    $details['implementor'] = $implementor;
144
145
                    // And a callable array
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
146
                    if (isset($details['method'])) {
147
                        $details['callable'] = [$implementor, $details['method']];
148
                    }
149
150
                    // Save with both uppercase & lowercase first letter, so either works
151
                    $lcFirst = strtolower($varName[0]) . substr($varName, 1);
152
                    $result[$lcFirst] = $details;
153
                    $result[ucfirst($varName)] = $details;
154
                }
155
            }
156
        }
157
158
        return $result;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
159
    }
160
161
    /**
162
     * Look up injected value - it may be part of an "overlay" (arguments passed to <% include %>),
163
     * set on the current item, part of an "underlay" ($Layout or $Content), or an iterator/global property
164
     *
165
     * @param string $property Name of property
166
     * @param array $params
167
     * @param bool $cast If true, an object is always returned even if not an object.
168
     * @return array|null
169
     */
170
    public function getInjectedValue($property, array $params, $cast = true)
171
    {
172
        // Get source for this value
173
        $result = $this->getValueSource($property);
174
        if (!array_key_exists('source', $result)) {
175
            return null;
176
        }
177
178
        // Look up the value - either from a callable, or from a directly provided value
179
        $source = $result['source'];
180
        $res = [];
181
        if (isset($source['callable'])) {
182
            $res['value'] = $source['callable'](...$params);
183
        } elseif (array_key_exists('value', $source)) {
184
            $res['value'] = $source['value'];
185
        } else {
186
            throw new InvalidArgumentException(
187
                "Injected property $property does't have a value or callable value source provided"
188
            );
189
        }
190
191
        // If we want to provide a casted object, look up what type object to use
192
        if ($cast) {
193
            $res['obj'] = $this->castValue($res['value'], $source);
194
        }
195
196
        return $res;
197
    }
198
199
    /**
200
     * Store the current overlay (as it doesn't directly apply to the new scope
201
     * that's being pushed). We want to store the overlay against the next item
202
     * "up" in the stack (hence upIndex), rather than the current item, because
203
     * SSViewer_Scope::obj() has already been called and pushed the new item to
204
     * the stack by this point
205
     *
206
     * @return SSViewer_Scope
207
     */
208
    public function pushScope()
209
    {
210
        $scope = parent::pushScope();
211
        $upIndex = $this->getUpIndex() ?: 0;
212
213
        $itemStack = $this->getItemStack();
214
        $itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY] = $this->overlay;
215
        $this->setItemStack($itemStack);
216
217
        // Remove the overlay when we're changing to a new scope, as values in
218
        // that scope take priority. The exceptions that set this flag are $Up
219
        // and $Top as they require that the new scope inherits the overlay
220
        if (!$this->preserveOverlay) {
221
            $this->overlay = [];
222
        }
223
224
        return $scope;
225
    }
226
227
    /**
228
     * Now that we're going to jump up an item in the item stack, we need to
229
     * restore the overlay that was previously stored against the next item "up"
230
     * in the stack from the current one
231
     *
232
     * @return SSViewer_Scope
233
     */
234
    public function popScope()
235
    {
236
        $upIndex = $this->getUpIndex();
237
238
        if ($upIndex !== null) {
239
            $itemStack = $this->getItemStack();
240
            $this->overlay = $itemStack[$upIndex][SSViewer_Scope::ITEM_OVERLAY];
241
        }
242
243
        return parent::popScope();
244
    }
245
246
    /**
247
     * $Up and $Top need to restore the overlay from the parent and top-level
248
     * scope respectively.
249
     *
250
     * @param string $name
251
     * @param array $arguments
252
     * @param bool $cache
253
     * @param string $cacheName
254
     * @return $this
255
     */
256
    public function obj($name, $arguments = [], $cache = false, $cacheName = null)
257
    {
258
        $overlayIndex = false;
259
260
        switch ($name) {
261
            case 'Up':
262
                $upIndex = $this->getUpIndex();
263
                if ($upIndex === null) {
264
                    throw new \LogicException('Up called when we\'re already at the top of the scope');
265
                }
266
                $overlayIndex = $upIndex; // Parent scope
267
                $this->preserveOverlay = true; // Preserve overlay
268
                break;
269
            case 'Top':
270
                $overlayIndex = 0; // Top-level scope
271
                $this->preserveOverlay = true; // Preserve overlay
272
                break;
273
            default:
274
                $this->preserveOverlay = false;
275
                break;
276
        }
277
278
        if ($overlayIndex !== false) {
279
            $itemStack = $this->getItemStack();
280
            if (!$this->overlay && isset($itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY])) {
281
                $this->overlay = $itemStack[$overlayIndex][SSViewer_Scope::ITEM_OVERLAY];
282
            }
283
        }
284
285
        parent::obj($name, $arguments, $cache, $cacheName);
286
        return $this;
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292
    public function getObj($name, $arguments = [], $cache = false, $cacheName = null)
293
    {
294
        $result = $this->getInjectedValue($name, (array)$arguments);
295
        if ($result) {
296
            return $result['obj'];
297
        }
298
        return parent::getObj($name, $arguments, $cache, $cacheName);
299
    }
300
301
    /**
302
     * {@inheritdoc}
303
     */
304
    public function __call($name, $arguments)
305
    {
306
        // Extract the method name and parameters
307
        $property = $arguments[0];  // The name of the public function being called
308
309
        // The public function parameters in an array
310
        $params = (isset($arguments[1])) ? (array)$arguments[1] : [];
311
312
        $val = $this->getInjectedValue($property, $params);
313
        if ($val) {
314
            $obj = $val['obj'];
315
            if ($name === 'hasValue') {
316
                $result = ($obj instanceof ViewableData) ? $obj->exists() : (bool)$obj;
0 ignored issues
show
introduced by
$obj is always a sub-type of SilverStripe\View\ViewableData.
Loading history...
317
            } elseif (is_null($obj) || (is_scalar($obj) && !is_string($obj))) {
318
                $result = $obj; // Nulls and non-string scalars don't need casting
319
            } else {
320
                $result = $obj->forTemplate(); // XML_val
321
            }
322
323
            $this->resetLocalScope();
324
            return $result;
325
        }
326
327
        return parent::__call($name, $arguments);
328
    }
329
330
    /**
331
     * Evaluate a template override. Returns an array where the presence of
332
     * a 'value' key indiciates whether an override was successfully found,
333
     * as null is a valid override value
334
     *
335
     * @param string $property Name of override requested
336
     * @param array $overrides List of overrides available
337
     * @return array An array with a 'value' key if a value has been found, or empty if not
338
     */
339
    protected function processTemplateOverride($property, $overrides)
340
    {
341
        if (!array_key_exists($property, $overrides)) {
342
            return [];
343
        }
344
345
        // Detect override type
346
        $override = $overrides[$property];
347
348
        // Late-evaluate this value
349
        if (!is_string($override) && is_callable($override)) {
350
            $override = $override();
351
352
            // Late override may yet return null
353
            if (!isset($override)) {
354
                return [];
355
            }
356
        }
357
358
        return ['value' => $override];
359
    }
360
361
    /**
362
     * Determine source to use for getInjectedValue. Returns an array where the presence of
363
     * a 'source' key indiciates whether a value source was successfully found, as a source
364
     * may be a null value returned from an override
365
     *
366
     * @param string $property
367
     * @return array An array with a 'source' key if a value source has been found, or empty if not
368
     */
369
    protected function getValueSource($property)
370
    {
371
        // Check for a presenter-specific override
372
        $result = $this->processTemplateOverride($property, $this->overlay);
373
        if (array_key_exists('value', $result)) {
374
            return ['source' => $result];
375
        }
376
377
        // Check if the method to-be-called exists on the target object - if so, don't check any further
378
        // injection locations
379
        $on = $this->itemIterator ? $this->itemIterator->current() : $this->item;
380
        if (isset($on->$property) || method_exists($on, $property)) {
381
            return [];
382
        }
383
384
        // Check for a presenter-specific override
385
        $result = $this->processTemplateOverride($property, $this->underlay);
386
        if (array_key_exists('value', $result)) {
387
            return ['source' => $result];
388
        }
389
390
        // Then for iterator-specific overrides
391
        if (array_key_exists($property, self::$iteratorProperties)) {
0 ignored issues
show
Bug introduced by
It seems like self::iteratorProperties can also be of type null; however, parameter $search of array_key_exists() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

391
        if (array_key_exists($property, /** @scrutinizer ignore-type */ self::$iteratorProperties)) {
Loading history...
392
            $source = self::$iteratorProperties[$property];
393
            /** @var TemplateIteratorProvider $implementor */
394
            $implementor = $source['implementor'];
395
            if ($this->itemIterator) {
396
                // Set the current iterator position and total (the object instance is the first item in
397
                // the callable array)
0 ignored issues
show
Unused Code Comprehensibility introduced by
43% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
398
                $implementor->iteratorProperties(
399
                    $this->itemIterator->key(),
400
                    $this->itemIteratorTotal
401
                );
402
            } else {
403
                // If we don't actually have an iterator at the moment, act like a list of length 1
404
                $implementor->iteratorProperties(0, 1);
405
            }
406
407
            return ($source) ? ['source' => $source] : [];
408
        }
409
410
        // And finally for global overrides
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
411
        if (array_key_exists($property, self::$globalProperties)) {
412
            return [
413
                'source' => self::$globalProperties[$property] // get the method call
414
            ];
415
        }
416
417
        // No value
418
        return [];
419
    }
420
421
    /**
422
     * Ensure the value is cast safely
423
     *
424
     * @param mixed $value
425
     * @param array $source
426
     * @return DBField
427
     */
428
    protected function castValue($value, $source)
429
    {
430
        // If the value has already been cast, is null, or is a non-string scalar
431
        if (is_object($value) || is_null($value) || (is_scalar($value) && !is_string($value))) {
432
            return $value;
433
        }
434
435
        // Get provided or default cast
436
        $casting = empty($source['casting'])
437
            ? ViewableData::config()->uninherited('default_cast')
438
            : $source['casting'];
439
440
        return DBField::create_field($casting, $value);
441
    }
442
}
443