Completed
Push — master ( 258711...eb0d45 )
by
unknown
16:48
created

ObjectAccess   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 363
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 105
dl 0
loc 363
rs 8.4
c 0
b 0
f 0
wmc 50

14 Methods

Rating   Name   Duplication   Size   Complexity  
B getGettablePropertyNames() 0 46 9
A getSettablePropertyNames() 0 25 5
A getPropertyPath() 0 10 3
A getObjectPropertyValue() 0 9 2
A setProperty() 0 16 5
A getArrayIndexValue() 0 3 1
A createAccessor() 0 8 2
A isPropertySettable() 0 9 4
A isPropertyGettable() 0 11 5
B getPropertyInternal() 0 31 7
A getGettableProperties() 0 7 2
A wrap() 0 3 1
A convertToArrayPropertyPath() 0 7 1
A getProperty() 0 9 3

How to fix   Complexity   

Complex Class

Complex classes like ObjectAccess often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ObjectAccess, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
 * It is free software; you can redistribute it and/or modify it under
9
 * the terms of the GNU General Public License, either version 2
10
 * of the License, or any later version.
11
 *
12
 * For the full copyright and license information, please read the
13
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
 * The TYPO3 project - inspiring people to share!
16
 */
17
18
namespace TYPO3\CMS\Extbase\Reflection;
19
20
use Symfony\Component\PropertyAccess\PropertyAccess;
21
use Symfony\Component\PropertyAccess\PropertyAccessor;
22
use Symfony\Component\PropertyAccess\PropertyPath;
23
use TYPO3\CMS\Core\Utility\GeneralUtility;
24
use TYPO3\CMS\Core\Utility\StringUtility;
25
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
26
use TYPO3\CMS\Extbase\Reflection\Exception\PropertyNotAccessibleException;
27
28
/**
29
 * Provides methods to call appropriate getter/setter on an object given the
30
 * property name. It does this following these rules:
31
 * - if the target object is an instance of ArrayAccess, it gets/sets the property
32
 * - if public getter/setter method exists, call it.
33
 * - if public property exists, return/set the value of it.
34
 * - else, throw exception
35
 * @internal only to be used within Extbase, not part of TYPO3 Core API.
36
 */
37
class ObjectAccess
38
{
39
    /**
40
     * @var PropertyAccessor
41
     */
42
    private static $propertyAccessor;
43
44
    /**
45
     * Get a property of a given object.
46
     * Tries to get the property the following ways:
47
     * - if the target is an array, and has this property, we call it.
48
     * - if public getter method exists, call it.
49
     * - if the target object is an instance of ArrayAccess, it gets the property
50
     * on it if it exists.
51
     * - if public property exists, return the value of it.
52
     * - else, throw exception
53
     *
54
     * @param mixed $subject Object or array to get the property from
55
     * @param string $propertyName name of the property to retrieve
56
     *
57
     * @throws \InvalidArgumentException in case $subject was not an object or $propertyName was not a string
58
     * @throws Exception\PropertyNotAccessibleException
59
     * @return mixed Value of the property
60
     */
61
    public static function getProperty($subject, string $propertyName)
62
    {
63
        if (!is_object($subject) && !is_array($subject)) {
64
            throw new \InvalidArgumentException(
65
                '$subject must be an object or array, ' . gettype($subject) . ' given.',
66
                1237301367
67
            );
68
        }
69
        return self::getPropertyInternal($subject, $propertyName);
70
    }
71
72
    /**
73
     * Gets a property of a given object or array.
74
     * This is an internal method that does only limited type checking for performance reasons.
75
     * If you can't make sure that $subject is either of type array or object and $propertyName of type string you should use getProperty() instead.
76
     *
77
     * @see getProperty()
78
     *
79
     * @param mixed $subject Object or array to get the property from
80
     * @param string $propertyName name of the property to retrieve
81
     *
82
     * @throws Exception\PropertyNotAccessibleException
83
     * @return mixed Value of the property
84
     * @internal
85
     */
86
    public static function getPropertyInternal($subject, string $propertyName)
87
    {
88
        if ($subject instanceof \SplObjectStorage || $subject instanceof ObjectStorage) {
89
            $subject = iterator_to_array(clone $subject, false);
90
        }
91
92
        $propertyPath = new PropertyPath($propertyName);
93
94
        if ($subject instanceof \ArrayAccess) {
95
            $accessor = self::createAccessor();
96
97
            // Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties.
98
            if ($accessor->isReadable($subject, $propertyPath)) {
99
                return $accessor->getValue($subject, $propertyPath);
100
            }
101
102
            // Use array style property path for instances of \ArrayAccess
103
            // https://symfony.com/doc/current/components/property_access.html#reading-from-arrays
104
105
            $propertyPath = self::convertToArrayPropertyPath($propertyPath);
106
        }
107
108
        if (is_object($subject)) {
109
            return self::getObjectPropertyValue($subject, $propertyPath);
110
        }
111
112
        if (is_array($subject)) {
113
            return self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath));
114
        }
115
116
        return null;
117
    }
118
119
    /**
120
     * Gets a property path from a given object or array.
121
     *
122
     * If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'),
123
     * and on the resulting object we call getProperty(..., 'blubb')
124
     *
125
     * For arrays the keys are checked likewise.
126
     *
127
     * @param mixed $subject Object or array to get the property path from
128
     * @param string $propertyPath
129
     *
130
     * @return mixed Value of the property
131
     */
132
    public static function getPropertyPath($subject, string $propertyPath)
133
    {
134
        try {
135
            foreach (new PropertyPath($propertyPath) as $pathSegment) {
136
                $subject = self::getPropertyInternal($subject, $pathSegment);
137
            }
138
        } catch (PropertyNotAccessibleException $error) {
139
            return null;
140
        }
141
        return $subject;
142
    }
143
144
    /**
145
     * Set a property for a given object.
146
     * Tries to set the property the following ways:
147
     * - if target is an array, set value
148
     * - if super cow powers should be used, set value through reflection
149
     * - if public setter method exists, call it.
150
     * - if public property exists, set it directly.
151
     * - if the target object is an instance of ArrayAccess, it sets the property
152
     * on it without checking if it existed.
153
     * - else, return FALSE
154
     *
155
     * @param mixed $subject The target object or array
156
     * @param string $propertyName Name of the property to set
157
     * @param mixed $propertyValue Value of the property
158
     *
159
     * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string
160
     * @return bool TRUE if the property could be set, FALSE otherwise
161
     */
162
    public static function setProperty(&$subject, string $propertyName, $propertyValue): bool
163
    {
164
        if (is_array($subject) || $subject instanceof \ArrayAccess) {
165
            $subject[$propertyName] = $propertyValue;
166
            return true;
167
        }
168
        if (!is_object($subject)) {
169
            throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368);
170
        }
171
172
        $accessor = self::createAccessor();
173
        if ($accessor->isWritable($subject, $propertyName)) {
174
            $accessor->setValue($subject, $propertyName, $propertyValue);
175
            return true;
176
        }
177
        return false;
178
    }
179
180
    /**
181
     * Returns an array of properties which can be get with the getProperty()
182
     * method.
183
     * Includes the following properties:
184
     * - which can be get through a public getter method.
185
     * - public properties which can be directly get.
186
     *
187
     * @param object $object Object to receive property names for
188
     *
189
     * @return array Array of all gettable property names
190
     * @throws Exception\UnknownClassException
191
     */
192
    public static function getGettablePropertyNames(object $object): array
193
    {
194
        if ($object instanceof \stdClass) {
195
            $properties = array_keys((array)$object);
196
            sort($properties);
197
            return $properties;
198
        }
199
200
        $classSchema = GeneralUtility::makeInstance(ReflectionService::class)
201
            ->getClassSchema($object);
202
203
        $accessor = self::createAccessor();
204
        $propertyNames = array_keys($classSchema->getProperties());
205
        $accessiblePropertyNames = array_filter($propertyNames, function ($propertyName) use ($accessor, $object) {
206
            return $accessor->isReadable($object, $propertyName);
207
        });
208
209
        foreach ($classSchema->getMethods() as $methodName => $methodDefinition) {
210
            if (!$methodDefinition->isPublic()) {
211
                continue;
212
            }
213
214
            foreach ($methodDefinition->getParameters() as $methodParam) {
215
                if (!$methodParam->isOptional()) {
216
                    continue 2;
217
                }
218
            }
219
220
            if (StringUtility::beginsWith($methodName, 'get')) {
221
                $accessiblePropertyNames[] = lcfirst(substr($methodName, 3));
222
                continue;
223
            }
224
225
            if (StringUtility::beginsWith($methodName, 'has')) {
226
                $accessiblePropertyNames[] = lcfirst(substr($methodName, 3));
227
                continue;
228
            }
229
230
            if (StringUtility::beginsWith($methodName, 'is')) {
231
                $accessiblePropertyNames[] = lcfirst(substr($methodName, 2));
232
            }
233
        }
234
235
        $accessiblePropertyNames = array_unique($accessiblePropertyNames);
236
        sort($accessiblePropertyNames);
237
        return $accessiblePropertyNames;
238
    }
239
240
    /**
241
     * Returns an array of properties which can be set with the setProperty()
242
     * method.
243
     * Includes the following properties:
244
     * - which can be set through a public setter method.
245
     * - public properties which can be directly set.
246
     *
247
     * @param object $object Object to receive property names for
248
     *
249
     * @throws \InvalidArgumentException
250
     * @return array Array of all settable property names
251
     */
252
    public static function getSettablePropertyNames(object $object): array
253
    {
254
        $accessor = self::createAccessor();
255
256
        if ($object instanceof \stdClass || $object instanceof \ArrayAccess) {
257
            $propertyNames = array_keys((array)$object);
258
        } else {
259
            $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object);
260
261
            $propertyNames = array_filter(array_keys($classSchema->getProperties()), function ($methodName) use ($accessor, $object) {
262
                return $accessor->isWritable($object, $methodName);
263
            });
264
265
            $setters = array_filter(array_keys($classSchema->getMethods()), function ($methodName) use ($object) {
266
                return StringUtility::beginsWith($methodName, 'set') && is_callable([$object, $methodName]);
267
            });
268
269
            foreach ($setters as $setter) {
270
                $propertyNames[] = lcfirst(substr($setter, 3));
271
            }
272
        }
273
274
        $propertyNames = array_unique($propertyNames);
275
        sort($propertyNames);
276
        return $propertyNames;
277
    }
278
279
    /**
280
     * Tells if the value of the specified property can be set by this Object Accessor.
281
     *
282
     * @param object $object Object containing the property
283
     * @param string $propertyName Name of the property to check
284
     *
285
     * @throws \InvalidArgumentException
286
     * @return bool
287
     */
288
    public static function isPropertySettable(object $object, $propertyName): bool
289
    {
290
        if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
291
            return true;
292
        }
293
        if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
294
            return true;
295
        }
296
        return is_callable([$object, 'set' . ucfirst($propertyName)]);
297
    }
298
299
    /**
300
     * Tells if the value of the specified property can be retrieved by this Object Accessor.
301
     *
302
     * @param object $object Object containing the property
303
     * @param string $propertyName Name of the property to check
304
     *
305
     * @throws \InvalidArgumentException
306
     * @return bool
307
     */
308
    public static function isPropertyGettable($object, $propertyName): bool
309
    {
310
        if (($object instanceof \ArrayAccess) && !$object->offsetExists($propertyName)) {
311
            return false;
312
        }
313
314
        if (is_array($object) || $object instanceof \ArrayAccess) {
315
            $propertyName = self::wrap($propertyName);
316
        }
317
318
        return self::createAccessor()->isReadable($object, $propertyName);
319
    }
320
321
    /**
322
     * Get all properties (names and their current values) of the current
323
     * $object that are accessible through this class.
324
     *
325
     * @param object $object Object to get all properties from.
326
     *
327
     * @throws \InvalidArgumentException
328
     * @return array Associative array of all properties.
329
     * @todo What to do with ArrayAccess
330
     */
331
    public static function getGettableProperties(object $object): array
332
    {
333
        $properties = [];
334
        foreach (self::getGettablePropertyNames($object) as $propertyName) {
335
            $properties[$propertyName] = self::getPropertyInternal($object, $propertyName);
336
        }
337
        return $properties;
338
    }
339
340
    /**
341
     * @return PropertyAccessor
342
     */
343
    private static function createAccessor(): PropertyAccessor
344
    {
345
        if (static::$propertyAccessor === null) {
0 ignored issues
show
Bug introduced by
Since $propertyAccessor is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $propertyAccessor to at least protected.
Loading history...
346
            static::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
347
                ->getPropertyAccessor();
348
        }
349
350
        return static::$propertyAccessor;
351
    }
352
353
    /**
354
     * @param object $subject
355
     * @param PropertyPath $propertyPath
356
     * @return mixed
357
     * @throws Exception\PropertyNotAccessibleException
358
     */
359
    private static function getObjectPropertyValue(object $subject, PropertyPath $propertyPath)
360
    {
361
        $accessor = self::createAccessor();
362
363
        if ($accessor->isReadable($subject, $propertyPath)) {
364
            return $accessor->getValue($subject, $propertyPath);
365
        }
366
367
        throw new PropertyNotAccessibleException('The property "' . (string)$propertyPath . '" on the subject does not exist.', 1476109666);
368
    }
369
370
    /**
371
     * @param array $subject
372
     * @param PropertyPath $propertyPath
373
     * @return mixed
374
     */
375
    private static function getArrayIndexValue(array $subject, PropertyPath $propertyPath)
376
    {
377
        return self::createAccessor()->getValue($subject, $propertyPath);
378
    }
379
380
    /**
381
     * @param PropertyPath $propertyPath
382
     * @return PropertyPath
383
     */
384
    private static function convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath
385
    {
386
        $segments = array_map(function ($segment) {
387
            return static::wrap($segment);
388
        }, $propertyPath->getElements());
389
390
        return new PropertyPath(implode('.', $segments));
391
    }
392
393
    /**
394
     * @param string $segment
395
     * @return string
396
     */
397
    private static function wrap(string $segment): string
398
    {
399
        return '[' . $segment . ']';
400
    }
401
}
402