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