Completed
Pull Request — master (#470)
by Claus
01:32
created

StandardVariableProvider   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 300
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 300
rs 6
c 0
b 0
f 0
wmc 55
lcom 1
cbo 0

18 Methods

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

How to fix   Complexity   

Complex Class

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
declare(strict_types=1);
3
namespace TYPO3Fluid\Fluid\Core\Variables;
4
5
/*
6
 * This file belongs to the package "TYPO3 Fluid".
7
 * See LICENSE.txt that was shipped with this package.
8
 */
9
10
/**
11
 * Class StandardVariableProvider
12
 */
13
class StandardVariableProvider implements VariableProviderInterface
14
{
15
    const ACCESSOR_ARRAY = 'array';
16
    const ACCESSOR_GETTER = 'getter';
17
    const ACCESSOR_ASSERTER = 'asserter';
18
    const ACCESSOR_PUBLICPROPERTY = 'public';
19
20
    /**
21
     * Variables stored in context
22
     *
23
     * @var mixed
24
     */
25
    protected $variables = [];
26
27
    /**
28
     * Variables, if any, with which to initialize this
29
     * VariableProvider.
30
     *
31
     * @param array $variables
32
     */
33
    public function __construct(array $variables = [])
34
    {
35
        $this->variables = $variables;
36
    }
37
38
    /**
39
     * @param array $variables
40
     * @return VariableProviderInterface
41
     */
42
    public function getScopeCopy(array $variables): VariableProviderInterface
43
    {
44
        if (!isset($variables['settings']) && isset($this->variables['settings'])) {
45
            $variables['settings'] = $this->variables['settings'];
46
        }
47
        return new static($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): void
59
    {
60
        $this->variables = $source;
61
    }
62
63
    /**
64
     * @return mixed
65
     */
66
    public function getSource()
67
    {
68
        return $this->variables;
69
    }
70
71
    /**
72
     * Get every variable provisioned by the VariableProvider
73
     * implementing the interface. Must return an array or
74
     * ArrayAccess instance!
75
     *
76
     * @return array
77
     */
78
    public function getAll(): array
79
    {
80
        return $this->variables;
81
    }
82
83
    /**
84
     * Add a variable to the context
85
     *
86
     * @param string $identifier Identifier of the variable to add
87
     * @param mixed $value The variable's value
88
     * @return void
89
     */
90
    public function add(string $identifier, $value): void
91
    {
92
        $this->variables[$identifier] = $value;
93
    }
94
95
    /**
96
     * Get a variable from the context. Throws exception if variable is not found in context.
97
     *
98
     * If "_all" is given as identifier, all variables are returned in an array,
99
     * if one of the other reserved variables are given, their appropriate value
100
     * they're representing is returned.
101
     *
102
     * @param string $identifier
103
     * @return mixed The variable value identified by $identifier
104
     */
105
    public function get(string $identifier)
106
    {
107
        return $this->getByPath($identifier);
108
    }
109
110
    /**
111
     * Get a variable by dotted path expression, retrieving the
112
     * variable from nested arrays/objects one segment at a time.
113
     * If the second variable is passed, it is expected to contain
114
     * extraction method names (constants from VariableExtractor)
115
     * which indicate how each value is extracted.
116
     *
117
     * @param string $path
118
     * @param array $accessors Optional list of accessors (see class constants)
119
     * @return mixed
120
     */
121
    public function getByPath(string $path, array $accessors = [])
122
    {
123
        $subject = $this->variables;
124
        foreach (explode('.', $path) as $index => $pathSegment) {
125
            $accessor = isset($accessors[$index]) ? $accessors[$index] : null;
126
            $subject = $this->extractSingleValue($subject, $pathSegment, $accessor);
127
            if ($subject === null) {
128
                break;
129
            }
130
        }
131
        return $subject;
132
    }
133
134
    /**
135
     * Remove a variable from context. Throws exception if variable is not found in context.
136
     *
137
     * @param string $identifier The identifier to remove
138
     * @return void
139
     */
140
    public function remove(string $identifier): void
141
    {
142
        unset($this->variables[$identifier]);
143
    }
144
145
    /**
146
     * Returns an array of all identifiers available in the context.
147
     *
148
     * @return array Array of identifier strings
149
     */
150
    public function getAllIdentifiers(): array
151
    {
152
        return array_keys($this->variables);
153
    }
154
155
    /**
156
     * Checks if this property exists in the VariableContainer.
157
     *
158
     * @param string $identifier
159
     * @return boolean TRUE if $identifier exists, FALSE otherwise
160
     */
161
    public function exists(string $identifier): bool
162
    {
163
        return isset($this->variables[$identifier]);
164
    }
165
166
    /**
167
     * @param string $propertyPath
168
     * @return array
169
     */
170
    public function getAccessorsForPath(string $propertyPath): array
171
    {
172
        $subject = $this->variables;
173
        $accessors = [];
174
        $propertyPathSegments = explode('.', $propertyPath);
175
        foreach ($propertyPathSegments as $pathSegment) {
176
            $accessor = $this->detectAccessor($subject, $pathSegment);
177
            if ($accessor === null) {
178
                // Note: this may include cases of sub-variable references. When such
179
                // a reference is encountered the accessor chain is stopped and new
180
                // accessors will be detected for the sub-variable and all following
181
                // path segments since the variable is now fully dynamic.
182
                break;
183
            }
184
            $accessors[] = $accessor;
185
            $subject = $this->extractSingleValue($subject, $pathSegment);
186
        }
187
        return $accessors;
188
    }
189
190
    /**
191
     * Extracts a single value from an array or object.
192
     *
193
     * @param mixed $subject
194
     * @param string $propertyName
195
     * @param string|null $accessor
196
     * @return mixed
197
     */
198
    protected function extractSingleValue($subject, string $propertyName, ?string $accessor = null)
199
    {
200
        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...
201
            $accessor = $this->detectAccessor($subject, $propertyName);
202
        }
203
        return $this->extractWithAccessor($subject, $propertyName, $accessor);
204
    }
205
206
    /**
207
     * Returns TRUE if the data type of $subject is potentially compatible
208
     * with the $accessor.
209
     *
210
     * @param mixed $subject
211
     * @param string $propertyName
212
     * @param string $accessor
213
     * @return boolean
214
     */
215
    protected function canExtractWithAccessor($subject, string $propertyName, string $accessor): bool
216
    {
217
        $class = is_object($subject) ? get_class($subject) : false;
218
        if ($accessor === self::ACCESSOR_ARRAY) {
219
            return (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)));
220
        } elseif ($accessor === self::ACCESSOR_GETTER) {
221
            return ($class !== false && method_exists($subject, 'get' . ucfirst($propertyName)));
222
        } elseif ($accessor === self::ACCESSOR_ASSERTER) {
223
            return ($class !== false && $this->isExtractableThroughAsserter($subject, $propertyName));
224
        } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY) {
225
            return ($class !== false && isset($subject->$propertyName));
226
        }
227
        return false;
228
    }
229
230
    /**
231
     * @param mixed $subject
232
     * @param string $propertyName
233
     * @param string|null $accessor
234
     * @return mixed
235
     */
236
    protected function extractWithAccessor($subject, string $propertyName, ?string $accessor)
237
    {
238
        if (($accessor === self::ACCESSOR_ARRAY && is_array($subject) && isset($subject[$propertyName]))
239
            || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))
240
        ) {
241
            return $subject[$propertyName];
242
        } elseif (is_object($subject)) {
243
            if ($accessor === self::ACCESSOR_GETTER) {
244
                return call_user_func_array([$subject, 'get' . ucfirst($propertyName)], []);
245
            } elseif ($accessor === self::ACCESSOR_ASSERTER) {
246
                return $this->extractThroughAsserter($subject, $propertyName);
247
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && isset($subject->$propertyName)) {
248
                return $subject->$propertyName;
249
            }
250
        }
251
        return null;
252
    }
253
254
    /**
255
     * Detect which type of accessor to use when extracting
256
     * $propertyName from $subject.
257
     *
258
     * @param mixed $subject
259
     * @param string $propertyName
260
     * @return string|null
261
     */
262
    protected function detectAccessor($subject, string $propertyName): ?string
263
    {
264
        if (is_array($subject) || $subject instanceof \ArrayAccess) {
265
            return self::ACCESSOR_ARRAY;
266
        }
267
        if (is_object($subject)) {
268
            $upperCasePropertyName = ucfirst($propertyName);
269
            $getter = 'get' . $upperCasePropertyName;
270
            if (is_callable([$subject, $getter])) {
271
                return self::ACCESSOR_GETTER;
272
            }
273
            if ($this->isExtractableThroughAsserter($subject, $propertyName)) {
274
                return self::ACCESSOR_ASSERTER;
275
            }
276
            if (property_exists($subject, $propertyName)) {
277
                return self::ACCESSOR_PUBLICPROPERTY;
278
            }
279
        }
280
281
        return null;
282
    }
283
284
    /**
285
     * Tests whether a property can be extracted through `is*` or `has*` methods.
286
     *
287
     * @param mixed $subject
288
     * @param string $propertyName
289
     * @return bool
290
     */
291
    protected function isExtractableThroughAsserter($subject, string $propertyName): bool
292
    {
293
        return method_exists($subject, 'is' . ucfirst($propertyName))
294
            || method_exists($subject, 'has' . ucfirst($propertyName));
295
    }
296
297
    /**
298
     * Extracts a property through `is*` or `has*` methods.
299
     *
300
     * @param object $subject
301
     * @param string $propertyName
302
     * @return mixed
303
     */
304
    protected function extractThroughAsserter(object $subject, string $propertyName)
305
    {
306
        if (method_exists($subject, 'is' . ucfirst($propertyName))) {
307
            return call_user_func_array([$subject, 'is' . ucfirst($propertyName)], []);
308
        }
309
310
        return call_user_func_array([$subject, 'has' . ucfirst($propertyName)], []);
311
    }
312
}
313