Completed
Push — master ( 533b50...d4d97e )
by
unknown
44:27 queued 26:27
created

ObjectAccess::getGettableProperties()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 4
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
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 super cow powers should be used, fetch value through reflection
49
     * - if public getter method exists, call it.
50
     * - if the target object is an instance of ArrayAccess, it gets the property
51
     * on it if it exists.
52
     * - if public property exists, return the value of it.
53
     * - else, throw exception
54
     *
55
     * @param mixed $subject Object or array to get the property from
56
     * @param string $propertyName name of the property to retrieve
57
     * @param bool $forceDirectAccess directly access property using reflection(!)
58
     *
59
     * @throws \InvalidArgumentException in case $subject was not an object or $propertyName was not a string
60
     * @throws Exception\PropertyNotAccessibleException
61
     * @return mixed Value of the property
62
     */
63
    public static function getProperty($subject, string $propertyName, bool $forceDirectAccess = false)
64
    {
65
        if (!is_object($subject) && !is_array($subject)) {
66
            throw new \InvalidArgumentException(
67
                '$subject must be an object or array, ' . gettype($subject) . ' given.',
68
                1237301367
69
            );
70
        }
71
72
        return self::getPropertyInternal($subject, $propertyName, $forceDirectAccess);
73
    }
74
75
    /**
76
     * Gets a property of a given object or array.
77
     * This is an internal method that does only limited type checking for performance reasons.
78
     * 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.
79
     *
80
     * @see getProperty()
81
     *
82
     * @param mixed $subject Object or array to get the property from
83
     * @param string $propertyName name of the property to retrieve
84
     * @param bool $forceDirectAccess directly access property using reflection(!)
85
     *
86
     * @throws Exception\PropertyNotAccessibleException
87
     * @return mixed Value of the property
88
     * @internal
89
     */
90
    public static function getPropertyInternal($subject, string $propertyName, bool $forceDirectAccess = false)
91
    {
92
        if ($forceDirectAccess === true) {
93
            trigger_error('Argument $forceDirectAccess will be removed in TYPO3 11.0', E_USER_DEPRECATED);
94
        }
95
96
        if (!$forceDirectAccess && ($subject instanceof \SplObjectStorage || $subject instanceof ObjectStorage)) {
97
            $subject = iterator_to_array(clone $subject, false);
98
        }
99
100
        $propertyPath = new PropertyPath($propertyName);
101
102
        if ($subject instanceof \ArrayAccess) {
103
            $accessor = self::createAccessor();
104
105
            // Check if $subject is an instance of \ArrayAccess and therefore maybe has actual accessible properties.
106
            if ($accessor->isReadable($subject, $propertyPath)) {
107
                return $accessor->getValue($subject, $propertyPath);
108
            }
109
110
            // Use array style property path for instances of \ArrayAccess
111
            // https://symfony.com/doc/current/components/property_access.html#reading-from-arrays
112
113
            $propertyPath = self::convertToArrayPropertyPath($propertyPath);
114
        }
115
116
        if (is_object($subject)) {
117
            return self::getObjectPropertyValue($subject, $propertyPath, $forceDirectAccess);
118
        }
119
120
        if (is_array($subject)) {
121
            return self::getArrayIndexValue($subject, self::convertToArrayPropertyPath($propertyPath));
122
        }
123
124
        return null;
125
    }
126
127
    /**
128
     * Gets a property path from a given object or array.
129
     *
130
     * If propertyPath is "bla.blubb", then we first call getProperty($object, 'bla'),
131
     * and on the resulting object we call getProperty(..., 'blubb')
132
     *
133
     * For arrays the keys are checked likewise.
134
     *
135
     * @param mixed $subject Object or array to get the property path from
136
     * @param string $propertyPath
137
     *
138
     * @return mixed Value of the property
139
     */
140
    public static function getPropertyPath($subject, string $propertyPath)
141
    {
142
        try {
143
            foreach (new PropertyPath($propertyPath) as $pathSegment) {
144
                $subject = self::getPropertyInternal($subject, $pathSegment);
145
            }
146
        } catch (PropertyNotAccessibleException $error) {
147
            return null;
148
        }
149
        return $subject;
150
    }
151
152
    /**
153
     * Set a property for a given object.
154
     * Tries to set the property the following ways:
155
     * - if target is an array, set value
156
     * - if super cow powers should be used, set value through reflection
157
     * - if public setter method exists, call it.
158
     * - if public property exists, set it directly.
159
     * - if the target object is an instance of ArrayAccess, it sets the property
160
     * on it without checking if it existed.
161
     * - else, return FALSE
162
     *
163
     * @param mixed $subject The target object or array
164
     * @param string $propertyName Name of the property to set
165
     * @param mixed $propertyValue Value of the property
166
     * @param bool $forceDirectAccess directly access property using reflection(!)
167
     *
168
     * @throws \InvalidArgumentException in case $object was not an object or $propertyName was not a string
169
     * @return bool TRUE if the property could be set, FALSE otherwise
170
     */
171
    public static function setProperty(&$subject, string $propertyName, $propertyValue, bool $forceDirectAccess = false): bool
172
    {
173
        if ($forceDirectAccess === true) {
174
            trigger_error('Argument $forceDirectAccess will be removed in TYPO3 11.0', E_USER_DEPRECATED);
175
        }
176
177
        if (is_array($subject) || ($subject instanceof \ArrayAccess && !$forceDirectAccess)) {
178
            $subject[$propertyName] = $propertyValue;
179
            return true;
180
        }
181
        if (!is_object($subject)) {
182
            throw new \InvalidArgumentException('subject must be an object or array, ' . gettype($subject) . ' given.', 1237301368);
183
        }
184
185
        $accessor = self::createAccessor();
186
        if ($accessor->isWritable($subject, $propertyName)) {
187
            $accessor->setValue($subject, $propertyName, $propertyValue);
188
            return true;
189
        }
190
191
        if ($forceDirectAccess) {
192
            if (property_exists($subject, $propertyName)) {
193
                $propertyReflection = new \ReflectionProperty($subject, $propertyName);
194
                $propertyReflection->setAccessible(true);
195
                $propertyReflection->setValue($subject, $propertyValue);
196
            } else {
197
                $subject->{$propertyName} = $propertyValue;
198
            }
199
200
            return true;
201
        }
202
203
        return false;
204
    }
205
206
    /**
207
     * Returns an array of properties which can be get with the getProperty()
208
     * method.
209
     * Includes the following properties:
210
     * - which can be get through a public getter method.
211
     * - public properties which can be directly get.
212
     *
213
     * @param object $object Object to receive property names for
214
     *
215
     * @return array Array of all gettable property names
216
     * @throws Exception\UnknownClassException
217
     */
218
    public static function getGettablePropertyNames(object $object): array
219
    {
220
        if ($object instanceof \stdClass) {
221
            $properties = array_keys((array)$object);
222
            sort($properties);
223
            return $properties;
224
        }
225
226
        $classSchema = GeneralUtility::makeInstance(ReflectionService::class)
227
            ->getClassSchema($object);
228
229
        $accessor = self::createAccessor();
230
        $propertyNames = array_keys($classSchema->getProperties());
231
        $accessiblePropertyNames = array_filter($propertyNames, function ($propertyName) use ($accessor, $object) {
232
            return $accessor->isReadable($object, $propertyName);
233
        });
234
235
        foreach ($classSchema->getMethods() as $methodName => $methodDefinition) {
236
            if (!$methodDefinition->isPublic()) {
237
                continue;
238
            }
239
240
            foreach ($methodDefinition->getParameters() as $methodParam) {
241
                if (!$methodParam->isOptional()) {
242
                    continue 2;
243
                }
244
            }
245
246
            if (StringUtility::beginsWith($methodName, 'get')) {
247
                $accessiblePropertyNames[] = lcfirst(substr($methodName, 3));
248
                continue;
249
            }
250
251
            if (StringUtility::beginsWith($methodName, 'has')) {
252
                $accessiblePropertyNames[] = lcfirst(substr($methodName, 3));
253
                continue;
254
            }
255
256
            if (StringUtility::beginsWith($methodName, 'is')) {
257
                $accessiblePropertyNames[] = lcfirst(substr($methodName, 2));
258
            }
259
        }
260
261
        $accessiblePropertyNames = array_unique($accessiblePropertyNames);
262
        sort($accessiblePropertyNames);
263
        return $accessiblePropertyNames;
264
    }
265
266
    /**
267
     * Returns an array of properties which can be set with the setProperty()
268
     * method.
269
     * Includes the following properties:
270
     * - which can be set through a public setter method.
271
     * - public properties which can be directly set.
272
     *
273
     * @param object $object Object to receive property names for
274
     *
275
     * @throws \InvalidArgumentException
276
     * @return array Array of all settable property names
277
     */
278
    public static function getSettablePropertyNames(object $object): array
279
    {
280
        $accessor = self::createAccessor();
281
282
        if ($object instanceof \stdClass || $object instanceof \ArrayAccess) {
283
            $propertyNames = array_keys((array)$object);
284
        } else {
285
            $classSchema = GeneralUtility::makeInstance(ReflectionService::class)->getClassSchema($object);
286
287
            $propertyNames = array_filter(array_keys($classSchema->getProperties()), function ($methodName) use ($accessor, $object) {
288
                return $accessor->isWritable($object, $methodName);
289
            });
290
291
            $setters = array_filter(array_keys($classSchema->getMethods()), function ($methodName) use ($object) {
292
                return StringUtility::beginsWith($methodName, 'set') && is_callable([$object, $methodName]);
293
            });
294
295
            foreach ($setters as $setter) {
296
                $propertyNames[] = lcfirst(substr($setter, 3));
297
            }
298
        }
299
300
        $propertyNames = array_unique($propertyNames);
301
        sort($propertyNames);
302
        return $propertyNames;
303
    }
304
305
    /**
306
     * Tells if the value of the specified property can be set by this Object Accessor.
307
     *
308
     * @param object $object Object containing the property
309
     * @param string $propertyName Name of the property to check
310
     *
311
     * @throws \InvalidArgumentException
312
     * @return bool
313
     */
314
    public static function isPropertySettable(object $object, $propertyName): bool
315
    {
316
        if ($object instanceof \stdClass && array_key_exists($propertyName, get_object_vars($object))) {
317
            return true;
318
        }
319
        if (array_key_exists($propertyName, get_class_vars(get_class($object)))) {
320
            return true;
321
        }
322
        return is_callable([$object, 'set' . ucfirst($propertyName)]);
323
    }
324
325
    /**
326
     * Tells if the value of the specified property can be retrieved by this Object Accessor.
327
     *
328
     * @param object $object Object containing the property
329
     * @param string $propertyName Name of the property to check
330
     *
331
     * @throws \InvalidArgumentException
332
     * @return bool
333
     */
334
    public static function isPropertyGettable($object, $propertyName): bool
335
    {
336
        if (($object instanceof \ArrayAccess) && !$object->offsetExists($propertyName)) {
337
            return false;
338
        }
339
340
        if (is_array($object) || $object instanceof \ArrayAccess) {
341
            $propertyName = self::wrap($propertyName);
342
        }
343
344
        return self::createAccessor()->isReadable($object, $propertyName);
345
    }
346
347
    /**
348
     * Get all properties (names and their current values) of the current
349
     * $object that are accessible through this class.
350
     *
351
     * @param object $object Object to get all properties from.
352
     *
353
     * @throws \InvalidArgumentException
354
     * @return array Associative array of all properties.
355
     * @todo What to do with ArrayAccess
356
     */
357
    public static function getGettableProperties(object $object): array
358
    {
359
        $properties = [];
360
        foreach (self::getGettablePropertyNames($object) as $propertyName) {
361
            $properties[$propertyName] = self::getPropertyInternal($object, $propertyName);
362
        }
363
        return $properties;
364
    }
365
366
    /**
367
     * @return PropertyAccessor
368
     */
369
    private static function createAccessor(): PropertyAccessor
370
    {
371
        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...
372
            static::$propertyAccessor = PropertyAccess::createPropertyAccessorBuilder()
373
                ->getPropertyAccessor();
374
        }
375
376
        return static::$propertyAccessor;
377
    }
378
379
    /**
380
     * @param object $subject
381
     * @param PropertyPath $propertyPath
382
     * @param bool $forceDirectAccess
383
     * @return mixed
384
     * @throws Exception\PropertyNotAccessibleException
385
     * @throws \ReflectionException
386
     */
387
    private static function getObjectPropertyValue(object $subject, PropertyPath $propertyPath, bool $forceDirectAccess)
388
    {
389
        $accessor = self::createAccessor();
390
391
        if ($accessor->isReadable($subject, $propertyPath)) {
392
            return $accessor->getValue($subject, $propertyPath);
393
        }
394
395
        $propertyName = (string)$propertyPath;
396
397
        if (!$forceDirectAccess) {
398
            throw new PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1476109666);
399
        }
400
401
        if (!property_exists($subject, $propertyName)) {
402
            throw new PropertyNotAccessibleException('The property "' . $propertyName . '" on the subject does not exist.', 1302855001);
403
        }
404
405
        $propertyReflection = new \ReflectionProperty($subject, $propertyName);
406
        $propertyReflection->setAccessible(true);
407
        return $propertyReflection->getValue($subject);
408
    }
409
410
    /**
411
     * @param array $subject
412
     * @param PropertyPath $propertyPath
413
     * @return mixed
414
     */
415
    private static function getArrayIndexValue(array $subject, PropertyPath $propertyPath)
416
    {
417
        return self::createAccessor()->getValue($subject, $propertyPath);
418
    }
419
420
    /**
421
     * @param PropertyPath $propertyPath
422
     * @return PropertyPath
423
     */
424
    private static function convertToArrayPropertyPath(PropertyPath $propertyPath): PropertyPath
425
    {
426
        $segments = array_map(function ($segment) {
427
            return static::wrap($segment);
428
        }, $propertyPath->getElements());
429
430
        return new PropertyPath(implode('.', $segments));
431
    }
432
433
    /**
434
     * @param string $segment
435
     * @return string
436
     */
437
    private static function wrap(string $segment): string
438
    {
439
        return '[' . $segment . ']';
440
    }
441
}
442