Completed
Pull Request — master (#287)
by Claus
03:10
created

VariableExtractor::extractThroughAsserter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 2
dl 0
loc 8
rs 9.4285
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 VariableExtractor
11
 *
12
 * Extracts variables from arrays/objects by use
13
 * of array accessing and basic getter methods.
14
 */
15
class VariableExtractor
16
{
17
18
    const ACCESSOR_ARRAY = 'array';
19
    const ACCESSOR_GETTER = 'getter';
20
    const ACCESSOR_ASSERTER = 'asserter';
21
    const ACCESSOR_PUBLICPROPERTY = 'public';
22
23
    /**
24
     * Static interface for instanciating and extracting
25
     * in a single operation. Delegates to getByPath.
26
     *
27
     * @param mixed $subject
28
     * @param string $propertyPath
29
     * @param array $accessors
30
     * @return mixed
31
     */
32
    public static function extract($subject, $propertyPath, array $accessors = [])
33
    {
34
        $extractor = new self();
35
        return $extractor->getByPath($subject, $propertyPath, $accessors);
36
    }
37
38
    /**
39
     * Static interface for instanciating and extracting
40
     * accessors for each segment of the path.
41
     *
42
     * @param VariableProviderInterface $subject
43
     * @param string $propertyPath
44
     * @return mixed
45
     */
46
    public static function extractAccessors($subject, $propertyPath)
47
    {
48
        $extractor = new self();
49
        return $extractor->getAccessorsForPath($subject, $propertyPath);
50
    }
51
52
    /**
53
     * Extracts a variable by path, recursively, from the
54
     * subject pass in argument. This implementation supports
55
     * recursive variable references by using {} around sub-
56
     * references, e.g. "array.{index}" will first get the
57
     * "array" variable, then resolve the "index" variable
58
     * before using the value of "index" as name of the property
59
     * to return. So:
60
     *
61
     * $subject = array('foo' => array('bar' => 'baz'), 'key' => 'bar')
62
     * $propertyPath = 'foo.{key}';
63
     * $result = ...getByPath($subject, $propertyPath);
64
     * // $result value is "baz", because $subject['foo'][$subject['key']] = 'baz';
65
     *
66
     * @param mixed $subject
67
     * @param string $propertyPath
68
     * @param array $accessors
69
     * @return mixed
70
     */
71
    public function getByPath($subject, $propertyPath, array $accessors = [])
72
    {
73
        if ($subject instanceof StandardVariableProvider) {
74
            return $subject->getByPath($propertyPath, $accessors);
75
        }
76
77
        $propertyPath = $this->resolveSubVariableReferences($subject, $propertyPath);
78
        $propertyPathSegments = explode('.', $propertyPath);
79
        foreach ($propertyPathSegments as $index => $pathSegment) {
80
            $accessor = isset($accessors[$index]) ? $accessors[$index] : null;
81
            $subject = $this->extractSingleValue($subject, $pathSegment, $accessor);
82
            if ($subject === null) {
83
                break;
84
            }
85
        }
86
        return $subject;
87
    }
88
89
    /**
90
     * @param VariableProviderInterface $subject
91
     * @param string $propertyPath
92
     * @return array
93
     */
94
    public function getAccessorsForPath($subject, $propertyPath)
95
    {
96
        $accessors = [];
97
        $propertyPathSegments = explode('.', $propertyPath);
98
        foreach ($propertyPathSegments as $index => $pathSegment) {
99
            $accessor = $this->detectAccessor($subject, $pathSegment);
100
            if ($accessor === null) {
101
                // Note: this may include cases of sub-variable references. When such
102
                // a reference is encountered the accessor chain is stopped and new
103
                // accessors will be detected for the sub-variable and all following
104
                // path segments since the variable is now fully dynamic.
105
                break;
106
            }
107
            $accessors[] = $accessor;
108
            $subject = $this->extractSingleValue($subject, $pathSegment);
109
        }
110
        return $accessors;
111
    }
112
113
    /**
114
     * @param mixed $subject
115
     * @param string $propertyPath
116
     * @return string
117
     */
118
    protected function resolveSubVariableReferences($subject, $propertyPath)
119
    {
120
        if (strpos($propertyPath, '{') !== false) {
121
            preg_match_all('/(\{.*\})/', $propertyPath, $matches);
122
            foreach ($matches[1] as $match) {
123
                $subPropertyPath = substr($match, 1, -1);
124
                $propertyPath = str_replace($match, $this->getByPath($subject, $subPropertyPath), $propertyPath);
125
            }
126
        }
127
        return $propertyPath;
128
    }
129
130
    /**
131
     * Extracts a single value from an array or object.
132
     *
133
     * @param mixed $subject
134
     * @param string $propertyName
135
     * @param string|null $accessor
136
     * @return mixed
137
     */
138
    protected function extractSingleValue($subject, $propertyName, $accessor = null)
139
    {
140
        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...
141
            $accessor = $this->detectAccessor($subject, $propertyName);
142
        }
143
        return $this->extractWithAccessor($subject, $propertyName, $accessor);
144
    }
145
146
    /**
147
     * Returns TRUE if the data type of $subject is potentially compatible
148
     * with the $accessor.
149
     *
150
     * @param mixed $subject
151
     * @param string $propertyName
152
     * @param string $accessor
153
     * @return boolean
154
     */
155
    protected function canExtractWithAccessor($subject, $propertyName, $accessor)
156
    {
157
        $class = is_object($subject) ? get_class($subject) : false;
158
        if ($accessor === self::ACCESSOR_ARRAY) {
159
            return (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)));
160
        } elseif ($accessor === self::ACCESSOR_GETTER) {
161
            return ($class !== false && method_exists($subject, 'get' . ucfirst($propertyName)));
162
        } elseif ($accessor === self::ACCESSOR_ASSERTER) {
163
            return ($class !== false && $this->isExtractableThroughAsserter($subject, $propertyName));
164
        } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY) {
165
            return ($class !== false && property_exists($subject, $propertyName));
166
        }
167
        return false;
168
    }
169
170
    /**
171
     * @param mixed $subject
172
     * @param string $propertyName
173
     * @param string $accessor
174
     * @return mixed
175
     */
176
    protected function extractWithAccessor($subject, $propertyName, $accessor)
177
    {
178
        if ($accessor === self::ACCESSOR_ARRAY && is_array($subject) && array_key_exists($propertyName, $subject)
179
            || $subject instanceof \ArrayAccess && $subject->offsetExists($propertyName)
180
        ) {
181
            return $subject[$propertyName];
182
        } elseif (is_object($subject)) {
183
            if ($accessor === self::ACCESSOR_GETTER) {
184
                return call_user_func_array([$subject, 'get' . ucfirst($propertyName)], []);
185
            } elseif ($accessor === self::ACCESSOR_ASSERTER) {
186
                return $this->extractThroughAsserter($subject, $propertyName);
187
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && property_exists($subject, $propertyName)) {
188
                return $subject->$propertyName;
189
            }
190
        }
191
        return null;
192
    }
193
194
    /**
195
     * Detect which type of accessor to use when extracting
196
     * $propertyName from $subject.
197
     *
198
     * @param mixed $subject
199
     * @param string $propertyName
200
     * @return string|NULL
201
     */
202
    protected function detectAccessor($subject, $propertyName)
203
    {
204
        if (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))) {
205
            return self::ACCESSOR_ARRAY;
206
        }
207
        if (is_object($subject)) {
208
            $upperCasePropertyName = ucfirst($propertyName);
209
            $getter = 'get' . $upperCasePropertyName;
210
            if (method_exists($subject, $getter)) {
211
                return self::ACCESSOR_GETTER;
212
            }
213
            if ($this->isExtractableThroughAsserter($subject, $propertyName)) {
214
                return self::ACCESSOR_ASSERTER;
215
            }
216
            if (property_exists($subject, $propertyName)) {
217
                return self::ACCESSOR_PUBLICPROPERTY;
218
            }
219
        }
220
221
        return null;
222
    }
223
224
    /**
225
     * Tests whether a property can be extracted through `is*` or `has*` methods.
226
     *
227
     * @param mixed $subject
228
     * @param string $propertyName
229
     * @return bool
230
     */
231
    protected function isExtractableThroughAsserter($subject, $propertyName)
232
    {
233
        return method_exists($subject, 'is' . ucfirst($propertyName))
234
            || method_exists($subject, 'has' . ucfirst($propertyName));
235
    }
236
237
    /**
238
     * Extracts a property through `is*` or `has*` methods.
239
     *
240
     * @param object $subject
241
     * @param string $propertyName
242
     * @return mixed
243
     */
244
    protected function extractThroughAsserter($subject, $propertyName)
245
    {
246
        if (method_exists($subject, 'is' . ucfirst($propertyName))) {
247
            return call_user_func_array([$subject, 'is' . ucfirst($propertyName)], []);
248
        }
249
250
        return call_user_func_array([$subject, 'has' . ucfirst($propertyName)], []);
251
    }
252
}
253