Completed
Push — master ( b4ba01...ae2d3d )
by Mathias
02:11
created

StandardVariableProvider::populateGettersByClassName()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 9
rs 9.9666
c 0
b 0
f 0
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->variables = $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
        $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|\ArrayAccess
77
     */
78
    public function getAll()
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
     * @api
90
     */
91
    public function add($identifier, $value)
92
    {
93
        $this->variables[$identifier] = $value;
94
    }
95
96
    /**
97
     * Get a variable from the context. Throws exception if variable is not found in context.
98
     *
99
     * If "_all" is given as identifier, all variables are returned in an array,
100
     * if one of the other reserved variables are given, their appropriate value
101
     * they're representing is returned.
102
     *
103
     * @param string $identifier
104
     * @return mixed The variable value identified by $identifier
105
     * @api
106
     */
107
    public function get($identifier)
108
    {
109
        return $this->getByPath($identifier);
110
    }
111
112
    /**
113
     * Get a variable by dotted path expression, retrieving the
114
     * variable from nested arrays/objects one segment at a time.
115
     * If the second variable is passed, it is expected to contain
116
     * extraction method names (constants from VariableExtractor)
117
     * which indicate how each value is extracted.
118
     *
119
     * @param string $path
120
     * @param array $accessors Optional list of accessors (see class constants)
121
     * @return mixed
122
     */
123
    public function getByPath($path, array $accessors = [])
124
    {
125
        $subject = $this->variables;
126
        foreach (explode('.', $this->resolveSubVariableReferences($path)) as $index => $pathSegment) {
127
            $accessor = isset($accessors[$index]) ? $accessors[$index] : null;
128
            $subject = $this->extractSingleValue($subject, $pathSegment, $accessor);
129
            if ($subject === null) {
130
                break;
131
            }
132
        }
133
        return $subject;
134
    }
135
136
    /**
137
     * Remove a variable from context. Throws exception if variable is not found in context.
138
     *
139
     * @param string $identifier The identifier to remove
140
     * @return void
141
     * @api
142
     */
143
    public function remove($identifier)
144
    {
145
        if (array_key_exists($identifier, $this->variables)) {
146
            unset($this->variables[$identifier]);
147
        }
148
    }
149
150
    /**
151
     * Returns an array of all identifiers available in the context.
152
     *
153
     * @return array Array of identifier strings
154
     */
155
    public function getAllIdentifiers()
156
    {
157
        return array_keys($this->variables);
158
    }
159
160
    /**
161
     * Checks if this property exists in the VariableContainer.
162
     *
163
     * @param string $identifier
164
     * @return boolean TRUE if $identifier exists, FALSE otherwise
165
     * @api
166
     */
167
    public function exists($identifier)
168
    {
169
        return array_key_exists($identifier, $this->variables);
170
    }
171
172
    /**
173
     * Clean up for serializing.
174
     *
175
     * @return string[]
176
     */
177
    public function __sleep()
178
    {
179
        return ['variables'];
180
    }
181
182
    /**
183
     * Adds a variable to the context.
184
     *
185
     * @param string $identifier Identifier of the variable to add
186
     * @param mixed $value The variable's value
187
     * @return void
188
     */
189
    public function offsetSet($identifier, $value)
190
    {
191
        $this->add($identifier, $value);
192
    }
193
194
    /**
195
     * Remove a variable from context. Throws exception if variable is not found in context.
196
     *
197
     * @param string $identifier The identifier to remove
198
     * @return void
199
     */
200
    public function offsetUnset($identifier)
201
    {
202
        $this->remove($identifier);
203
    }
204
205
    /**
206
     * Checks if this property exists in the VariableContainer.
207
     *
208
     * @param string $identifier
209
     * @return boolean TRUE if $identifier exists, FALSE otherwise
210
     */
211
    public function offsetExists($identifier)
212
    {
213
        return $this->exists($identifier);
214
    }
215
216
    /**
217
     * Get a variable from the context. Throws exception if variable is not found in context.
218
     *
219
     * @param string $identifier
220
     * @return mixed The variable identified by $identifier
221
     */
222
    public function offsetGet($identifier)
223
    {
224
        return $this->get($identifier);
225
    }
226
227
    /**
228
     * @param string $propertyPath
229
     * @return array
230
     */
231 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...
232
    {
233
        $subject = $this->variables;
234
        $accessors = [];
235
        $propertyPathSegments = explode('.', $propertyPath);
236
        foreach ($propertyPathSegments as $index => $pathSegment) {
237
            $accessor = $this->detectAccessor($subject, $pathSegment);
238
            if ($accessor === null) {
239
                // Note: this may include cases of sub-variable references. When such
240
                // a reference is encountered the accessor chain is stopped and new
241
                // accessors will be detected for the sub-variable and all following
242
                // path segments since the variable is now fully dynamic.
243
                break;
244
            }
245
            $accessors[] = $accessor;
246
            $subject = $this->extractSingleValue($subject, $pathSegment);
247
        }
248
        return $accessors;
249
    }
250
251
    /**
252
     * @param string $propertyPath
253
     * @return string
254
     */
255 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...
256
    {
257
        if (strpos($propertyPath, '{') !== false) {
258
            preg_match_all('/(\{.*\})/', $propertyPath, $matches);
259
            foreach ($matches[1] as $match) {
260
                $subPropertyPath = substr($match, 1, -1);
261
                $propertyPath = str_replace($match, $this->getByPath($subPropertyPath), $propertyPath);
262
            }
263
        }
264
        return $propertyPath;
265
    }
266
267
    /**
268
     * Extracts a single value from an array or object.
269
     *
270
     * @param mixed $subject
271
     * @param string $propertyName
272
     * @param string|null $accessor
273
     * @return mixed
274
     */
275 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...
276
    {
277
        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...
278
            $accessor = $this->detectAccessor($subject, $propertyName);
279
        }
280
        return $this->extractWithAccessor($subject, $propertyName, $accessor);
281
    }
282
283
    /**
284
     * Returns TRUE if the data type of $subject is potentially compatible
285
     * with the $accessor.
286
     *
287
     * @param mixed $subject
288
     * @param string $propertyName
289
     * @param string $accessor
290
     * @return boolean
291
     */
292 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...
293
    {
294
        $class = is_object($subject) ? get_class($subject) : false;
295
        if ($accessor === self::ACCESSOR_ARRAY) {
296
            return (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)));
297
        } elseif ($accessor === self::ACCESSOR_GETTER) {
298
            return ($class !== false && method_exists($subject, 'get' . ucfirst($propertyName)));
299
        } elseif ($accessor === self::ACCESSOR_ASSERTER) {
300
            return ($class !== false && $this->isExtractableThroughAsserter($subject, $propertyName));
301
        } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY) {
302
            return ($class !== false && property_exists($subject, $propertyName));
303
        }
304
        return false;
305
    }
306
307
    /**
308
     * @param mixed $subject
309
     * @param string $propertyName
310
     * @param string $accessor
311
     * @return mixed
312
     */
313 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...
314
    {
315
        if ($accessor === self::ACCESSOR_ARRAY && is_array($subject) && array_key_exists($propertyName, $subject)
316
            || $subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)
317
        ) {
318
            return $subject[$propertyName];
319
        } elseif (is_object($subject)) {
320
            if ($accessor === self::ACCESSOR_GETTER) {
321
                return call_user_func_array([$subject, 'get' . ucfirst($propertyName)], []);
322
            } elseif ($accessor === self::ACCESSOR_ASSERTER) {
323
                return $this->extractThroughAsserter($subject, $propertyName);
324
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && property_exists($subject, $propertyName)) {
325
                return $subject->$propertyName;
326
            }
327
        }
328
        return null;
329
    }
330
331
    /**
332
     * Detect which type of accessor to use when extracting
333
     * $propertyName from $subject.
334
     *
335
     * @param mixed $subject
336
     * @param string $propertyName
337
     * @return string|NULL
338
     */
339 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...
340
    {
341
        if (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))) {
342
            return self::ACCESSOR_ARRAY;
343
        }
344
        if (is_object($subject)) {
345
            $upperCasePropertyName = ucfirst($propertyName);
346
            $getter = 'get' . $upperCasePropertyName;
347
            if (method_exists($subject, $getter)) {
348
                return self::ACCESSOR_GETTER;
349
            }
350
            if ($this->isExtractableThroughAsserter($subject, $propertyName)) {
351
                return self::ACCESSOR_ASSERTER;
352
            }
353
            if (property_exists($subject, $propertyName)) {
354
                return self::ACCESSOR_PUBLICPROPERTY;
355
            }
356
        }
357
358
        return null;
359
    }
360
361
    /**
362
     * Tests whether a property can be extracted through `is*` or `has*` methods.
363
     *
364
     * @param mixed $subject
365
     * @param string $propertyName
366
     * @return bool
367
     */
368
    protected function isExtractableThroughAsserter($subject, $propertyName)
369
    {
370
        return method_exists($subject, 'is' . ucfirst($propertyName))
371
            || method_exists($subject, 'has' . ucfirst($propertyName));
372
    }
373
374
    /**
375
     * Extracts a property through `is*` or `has*` methods.
376
     *
377
     * @param object $subject
378
     * @param string $propertyName
379
     * @return mixed
380
     */
381 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...
382
    {
383
        if (method_exists($subject, 'is' . ucfirst($propertyName))) {
384
            return call_user_func_array([$subject, 'is' . ucfirst($propertyName)], []);
385
        }
386
387
        return call_user_func_array([$subject, 'has' . ucfirst($propertyName)], []);
388
    }
389
}
390