Completed
Push — master ( 3911cb...b4ba01 )
by Mathias
01:45
created

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
     * Internal array of ['className' => ['getFoo', 'getBar'], ...]
28
     * storing all names of getter methods for a given class.
29
     *
30
     * @var array
31
     */
32
    protected static $gettersByClassName = [];
33
34
    /**
35
     * Variables, if any, with which to initialize this
36
     * VariableProvider.
37
     *
38
     * @param array $variables
39
     */
40
    public function __construct(array $variables = [])
41
    {
42
        $this->variables = $variables;
43
    }
44
45
    /**
46
     * @param array|\ArrayAccess $variables
47
     * @return VariableProviderInterface
48
     */
49
    public function getScopeCopy($variables)
50
    {
51
        if (!array_key_exists('settings', $variables) && array_key_exists('settings', $this->variables)) {
52
            $variables['settings'] = $this->variables['settings'];
53
        }
54
        $className = get_class($this);
55
        return new $className($variables);
56
    }
57
58
    /**
59
     * Set the source data used by this VariableProvider. The
60
     * source can be any type, but the type must of course be
61
     * supported by the VariableProvider itself.
62
     *
63
     * @param mixed $source
64
     * @return void
65
     */
66
    public function setSource($source)
67
    {
68
        $this->variables = $source;
69
    }
70
71
    /**
72
     * @return mixed
73
     */
74
    public function getSource()
75
    {
76
        return $this->variables;
77
    }
78
79
    /**
80
     * Get every variable provisioned by the VariableProvider
81
     * implementing the interface. Must return an array or
82
     * ArrayAccess instance!
83
     *
84
     * @return array|\ArrayAccess
85
     */
86
    public function getAll()
87
    {
88
        return $this->variables;
89
    }
90
91
    /**
92
     * Add a variable to the context
93
     *
94
     * @param string $identifier Identifier of the variable to add
95
     * @param mixed $value The variable's value
96
     * @return void
97
     * @api
98
     */
99
    public function add($identifier, $value)
100
    {
101
        $this->variables[$identifier] = $value;
102
    }
103
104
    /**
105
     * Get a variable from the context. Throws exception if variable is not found in context.
106
     *
107
     * If "_all" is given as identifier, all variables are returned in an array,
108
     * if one of the other reserved variables are given, their appropriate value
109
     * they're representing is returned.
110
     *
111
     * @param string $identifier
112
     * @return mixed The variable value identified by $identifier
113
     * @api
114
     */
115
    public function get($identifier)
116
    {
117
        return $this->getByPath($identifier);
118
    }
119
120
    /**
121
     * Get a variable by dotted path expression, retrieving the
122
     * variable from nested arrays/objects one segment at a time.
123
     * If the second variable is passed, it is expected to contain
124
     * extraction method names (constants from VariableExtractor)
125
     * which indicate how each value is extracted.
126
     *
127
     * @param string $path
128
     * @param array $accessors Optional list of accessors (see class constants)
129
     * @return mixed
130
     */
131
    public function getByPath($path, array $accessors = [])
132
    {
133
        $subject = $this->variables;
134
        foreach (explode('.', $this->resolveSubVariableReferences($path)) as $index => $pathSegment) {
135
            $accessor = isset($accessors[$index]) ? $accessors[$index] : null;
136
            $subject = $this->extractSingleValue($subject, $pathSegment, $accessor);
137
            if ($subject === null) {
138
                break;
139
            }
140
        }
141
        return $subject;
142
    }
143
144
    /**
145
     * Remove a variable from context. Throws exception if variable is not found in context.
146
     *
147
     * @param string $identifier The identifier to remove
148
     * @return void
149
     * @api
150
     */
151
    public function remove($identifier)
152
    {
153
        if (array_key_exists($identifier, $this->variables)) {
154
            unset($this->variables[$identifier]);
155
        }
156
    }
157
158
    /**
159
     * Returns an array of all identifiers available in the context.
160
     *
161
     * @return array Array of identifier strings
162
     */
163
    public function getAllIdentifiers()
164
    {
165
        return array_keys($this->variables);
166
    }
167
168
    /**
169
     * Checks if this property exists in the VariableContainer.
170
     *
171
     * @param string $identifier
172
     * @return boolean TRUE if $identifier exists, FALSE otherwise
173
     * @api
174
     */
175
    public function exists($identifier)
176
    {
177
        return array_key_exists($identifier, $this->variables);
178
    }
179
180
    /**
181
     * Clean up for serializing.
182
     *
183
     * @return string[]
184
     */
185
    public function __sleep()
186
    {
187
        return ['variables'];
188
    }
189
190
    /**
191
     * Adds a variable to the context.
192
     *
193
     * @param string $identifier Identifier of the variable to add
194
     * @param mixed $value The variable's value
195
     * @return void
196
     */
197
    public function offsetSet($identifier, $value)
198
    {
199
        $this->add($identifier, $value);
200
    }
201
202
    /**
203
     * Remove a variable from context. Throws exception if variable is not found in context.
204
     *
205
     * @param string $identifier The identifier to remove
206
     * @return void
207
     */
208
    public function offsetUnset($identifier)
209
    {
210
        $this->remove($identifier);
211
    }
212
213
    /**
214
     * Checks if this property exists in the VariableContainer.
215
     *
216
     * @param string $identifier
217
     * @return boolean TRUE if $identifier exists, FALSE otherwise
218
     */
219
    public function offsetExists($identifier)
220
    {
221
        return $this->exists($identifier);
222
    }
223
224
    /**
225
     * Get a variable from the context. Throws exception if variable is not found in context.
226
     *
227
     * @param string $identifier
228
     * @return mixed The variable identified by $identifier
229
     */
230
    public function offsetGet($identifier)
231
    {
232
        return $this->get($identifier);
233
    }
234
235
    /**
236
     * @param string $propertyPath
237
     * @return array
238
     */
239 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...
240
    {
241
        $subject = $this->variables;
242
        $accessors = [];
243
        $propertyPathSegments = explode('.', $propertyPath);
244
        foreach ($propertyPathSegments as $index => $pathSegment) {
245
            $accessor = $this->detectAccessor($subject, $pathSegment);
246
            if ($accessor === null) {
247
                // Note: this may include cases of sub-variable references. When such
248
                // a reference is encountered the accessor chain is stopped and new
249
                // accessors will be detected for the sub-variable and all following
250
                // path segments since the variable is now fully dynamic.
251
                break;
252
            }
253
            $accessors[] = $accessor;
254
            $subject = $this->extractSingleValue($subject, $pathSegment);
255
        }
256
        return $accessors;
257
    }
258
259
    /**
260
     * @param string $propertyPath
261
     * @return string
262
     */
263 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...
264
    {
265
        if (strpos($propertyPath, '{') !== false) {
266
            preg_match_all('/(\{.*\})/', $propertyPath, $matches);
267
            foreach ($matches[1] as $match) {
268
                $subPropertyPath = substr($match, 1, -1);
269
                $propertyPath = str_replace($match, $this->getByPath($subPropertyPath), $propertyPath);
270
            }
271
        }
272
        return $propertyPath;
273
    }
274
275
    /**
276
     * Extracts a single value from an array or object.
277
     *
278
     * @param mixed $subject
279
     * @param string $propertyName
280
     * @param string|null $accessor
281
     * @return mixed
282
     */
283 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...
284
    {
285
        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...
286
            $accessor = $this->detectAccessor($subject, $propertyName);
287
        }
288
        return $this->extractWithAccessor($subject, $propertyName, $accessor);
289
    }
290
291
    /**
292
     * Returns TRUE if the data type of $subject is potentially compatible
293
     * with the $accessor.
294
     *
295
     * @param mixed $subject
296
     * @param string $propertyName
297
     * @param string $accessor
298
     * @return boolean
299
     */
300
    protected function canExtractWithAccessor($subject, $propertyName, $accessor)
301
    {
302
        if ($accessor === self::ACCESSOR_ARRAY) {
303
            return (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)));
304
        }
305
        if (!is_object($subject)) {
306
            // The subject is not an object, none of the next accessors will apply so we return false immediately.
307
            return false;
308
        }
309
        $className = $this->populateGettersByClassName($subject);
310
        if ($accessor === self::ACCESSOR_GETTER) {
311
            return static::$gettersByClassName[$className]['get' . ucfirst($propertyName)] ?? false;
312
        } elseif ($accessor === self::ACCESSOR_ASSERTER) {
313
            return ($this->isExtractableThroughAsserter($subject, $propertyName));
314
        } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY) {
315
            return (property_exists($subject, $propertyName));
316
        }
317
        return false;
318
    }
319
320
    /**
321
     * @param mixed $subject
322
     * @param string $propertyName
323
     * @param string $accessor
324
     * @return mixed
325
     */
326 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...
327
    {
328
        if ($accessor === self::ACCESSOR_ARRAY && is_array($subject) && array_key_exists($propertyName, $subject)
329
            || $subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)
330
        ) {
331
            return $subject[$propertyName];
332
        } elseif (is_object($subject)) {
333
            if ($accessor === self::ACCESSOR_GETTER) {
334
                return call_user_func_array([$subject, 'get' . ucfirst($propertyName)], []);
335
            } elseif ($accessor === self::ACCESSOR_ASSERTER) {
336
                return $this->extractThroughAsserter($subject, $propertyName);
337
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && property_exists($subject, $propertyName)) {
338
                return $subject->$propertyName;
339
            }
340
        }
341
        return null;
342
    }
343
344
    /**
345
     * Detect which type of accessor to use when extracting
346
     * $propertyName from $subject.
347
     *
348
     * @param mixed $subject
349
     * @param string $propertyName
350
     * @return string|NULL
351
     */
352
    protected function detectAccessor($subject, $propertyName)
353
    {
354
        if (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))) {
355
            return self::ACCESSOR_ARRAY;
356
        }
357
        if (is_object($subject)) {
358
            $className = $this->populateGettersByClassName($subject);
359
            $upperCasePropertyName = ucfirst($propertyName);
360
            $getter = 'get' . $upperCasePropertyName;
361
            if (static::$gettersByClassName[$className][$getter] ?? false) {
362
                return self::ACCESSOR_GETTER;
363
            }
364
            if ($this->isExtractableThroughAsserter($subject, $propertyName)) {
365
                return self::ACCESSOR_ASSERTER;
366
            }
367
            if (property_exists($subject, $propertyName)) {
368
                return self::ACCESSOR_PUBLICPROPERTY;
369
            }
370
            if (method_exists($subject, '__call')) {
371
                return self::ACCESSOR_GETTER;
372
            }
373
        }
374
375
        return null;
376
    }
377
378
    /**
379
     * Tests whether a property can be extracted through `is*` or `has*` methods.
380
     *
381
     * @param mixed $subject
382
     * @param string $propertyName
383
     * @return bool
384
     */
385
    protected function isExtractableThroughAsserter($subject, $propertyName)
386
    {
387
        $className = $this->populateGettersByClassName($subject);
388
        $upperCasePropertyName = ucfirst($propertyName);
389
        return (bool) (static::$gettersByClassName[$className]['is' . $upperCasePropertyName] ?? static::$gettersByClassName[$className]['has' . $upperCasePropertyName] ?? false);
390
    }
391
392
    /**
393
     * Extracts a property through `is*` or `has*` methods.
394
     *
395
     * @param object $subject
396
     * @param string $propertyName
397
     * @return mixed
398
     */
399
    protected function extractThroughAsserter($subject, $propertyName)
400
    {
401
        $className = $this->populateGettersByClassName($subject);
402
        $upperCasePropertyName = ucfirst($propertyName);
403
        if (static::$gettersByClassName[$className]['is' . $upperCasePropertyName] ?? false) {
404
            return call_user_func_array([$subject, 'is' . $upperCasePropertyName], []);
405
        }
406
407
        return call_user_func_array([$subject, 'has' . $upperCasePropertyName], []);
408
    }
409
410
    /**
411
     * Studies an object and fills the static internal cache of getter method awareness.
412
     * The method must be called when detecting an accessor for an object and has a very
413
     * particular reason for existing:
414
     *
415
     * - method_exists() will return TRUE even for protected methods
416
     * - is_callable() will not, but is far too greedy and returns TRUE for *any* method
417
     *   name if __call is defined on the class.
418
     * - disregarding Reflection which is horribly slow, get_class_methods is the only
419
     *   method which returns a list of public methods.
420
     * - but repeatedly calling get_class_methods() *AND* in_array() is not a sensible
421
     *   solution.
422
     * - therefore, an internal cached array is built by class name which allows a null-
423
     *   coalesce expression to determine if a getter method exists *AND* is public *AND*
424
     *   is not going to be handled by __call.
425
     *
426
     * For ease of use (to avoid repeating get_class calls) the method returns the class
427
     * name of the instance as returned by get_class($instance).
428
     *
429
     * @param object $instance
430
     * @return string
431
     */
432
    protected function populateGettersByClassName($instance)
433
    {
434
        $className = get_class($instance);
435
        if (!isset(static::$gettersByClassName[$className])) {
436
            $methods = get_class_methods($instance);
437
            static::$gettersByClassName[$className] = array_combine($methods, $methods);
438
        }
439
        return $className;
440
    }
441
}
442