Completed
Pull Request — master (#287)
by Cedric
02:43
created

VariableExtractor::isExtractableThroughAsserter()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 3
nc 2
nop 2
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
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)) {
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...
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