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

VariableExtractor::resolveSubVariableReferences()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 2
dl 0
loc 11
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_ASSERTER_HAS = 'asserterHas';
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->isExctractableThroughAsserter($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 call_user_func_array([$subject, 'is' . ucfirst($propertyName)], []);
188
            } elseif ($accessor === self::ACCESSOR_ASSERTER_HAS) {
189
                return call_user_func_array([$subject, 'has' . ucfirst($propertyName)], []);
190
            } elseif ($accessor === self::ACCESSOR_PUBLICPROPERTY && property_exists($subject, $propertyName)) {
191
                return $subject->$propertyName;
192
            }
193
        }
194
        return null;
195
    }
196
197
    /**
198
     * Detect which type of accessor to use when extracting
199
     * $propertyName from $subject.
200
     *
201
     * @param mixed $subject
202
     * @param string $propertyName
203
     * @return string|NULL
204
     */
205
    protected function detectAccessor($subject, $propertyName)
206
    {
207
        if (is_array($subject) || ($subject instanceof \ArrayAccess && $subject->offsetExists($propertyName))) {
208
            return self::ACCESSOR_ARRAY;
209
        }
210
        if (is_object($subject)) {
211
            $upperCasePropertyName = ucfirst($propertyName);
212
            $getter = 'get' . $upperCasePropertyName;
213
            $asserter = 'is' . $upperCasePropertyName;
214
            $asserterHas = 'has' . $upperCasePropertyName;
215
            if (method_exists($subject, $getter)) {
216
                return self::ACCESSOR_GETTER;
217
            }
218
            if (method_exists($subject, $asserter)) {
219
                return self::ACCESSOR_ASSERTER;
220
            }
221
            if (method_exists($subject, $asserterHas)) {
222
                return self::ACCESSOR_ASSERTER_HAS;
223
            }
224
            if (property_exists($subject, $propertyName)) {
225
                return self::ACCESSOR_PUBLICPROPERTY;
226
            }
227
        }
228
229
        return null;
230
    }
231
232
    /**
233
     * @param mixed $subject
234
     * @param string $propertyName
235
     * @return bool
236
     */
237
    protected function isExctractableThroughAsserter($subject, $propertyName)
238
    {
239
        return method_exists($subject, 'is' . ucfirst($propertyName))
240
            || method_exists($subject, 'has' . ucfirst($propertyName));
241
    }
242
}
243