Instantiator::hasInternalAncestors()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 6
cts 6
cp 1
rs 9.8666
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php
2
3
namespace Doctrine\Instantiator;
4
5
use ArrayIterator;
6
use Doctrine\Instantiator\Exception\InvalidArgumentException;
7
use Doctrine\Instantiator\Exception\UnexpectedValueException;
8
use Exception;
9
use ReflectionClass;
10
use ReflectionException;
11
use Serializable;
12
use function class_exists;
13
use function is_subclass_of;
14
use function restore_error_handler;
15
use function set_error_handler;
16
use function sprintf;
17
use function strlen;
18
use function unserialize;
19
20
/**
21
 * {@inheritDoc}
22
 */
23
final class Instantiator implements InstantiatorInterface
24
{
25
    /**
26
     * Markers used internally by PHP to define whether {@see \unserialize} should invoke
27
     * the method {@see \Serializable::unserialize()} when dealing with classes implementing
28
     * the {@see \Serializable} interface.
29
     */
30
    public const SERIALIZATION_FORMAT_USE_UNSERIALIZER   = 'C';
31
    public const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
32
33
    /**
34
     * Used to instantiate specific classes, indexed by class name.
35
     *
36
     * @var callable[]
37
     */
38
    private static $cachedInstantiators = [];
39
40
    /**
41
     * Array of objects that can directly be cloned, indexed by class name.
42
     *
43
     * @var object[]
44
     */
45
    private static $cachedCloneables = [];
46
47
    /**
48
     * {@inheritDoc}
49
     */
50 42
    public function instantiate($className)
51
    {
52 42
        if (isset(self::$cachedCloneables[$className])) {
53 10
            return clone self::$cachedCloneables[$className];
54
        }
55
56 33
        if (isset(self::$cachedInstantiators[$className])) {
57 10
            $factory = self::$cachedInstantiators[$className];
58
59 10
            return $factory();
60
        }
61
62 23
        return $this->buildAndCacheFromFactory($className);
63
    }
64
65
    /**
66
     * Builds the requested object and caches it in static properties for performance
67
     *
68
     * @return object
69
     */
70 23
    private function buildAndCacheFromFactory(string $className)
71
    {
72 23
        $factory  = self::$cachedInstantiators[$className] = $this->buildFactory($className);
73 18
        $instance = $factory();
74
75 18
        if ($this->isSafeToClone(new ReflectionClass($instance))) {
76 10
            self::$cachedCloneables[$className] = clone $instance;
77
        }
78
79 18
        return $instance;
80
    }
81
82
    /**
83
     * Builds a callable capable of instantiating the given $className without
84
     * invoking its constructor.
85
     *
86
     * @throws InvalidArgumentException
87
     * @throws UnexpectedValueException
88
     * @throws ReflectionException
89
     */
90 23
    private function buildFactory(string $className) : callable
91
    {
92 23
        $reflectionClass = $this->getReflectionClass($className);
93
94 19
        if ($this->isInstantiableViaReflection($reflectionClass)) {
95 16
            return [$reflectionClass, 'newInstanceWithoutConstructor'];
96
        }
97
98 3
        $serializedString = sprintf(
99 3
            '%s:%d:"%s":0:{}',
100 3
            is_subclass_of($className, Serializable::class) ? self::SERIALIZATION_FORMAT_USE_UNSERIALIZER : self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
0 ignored issues
show
Bug introduced by
Due to PHP Bug #53727, is_subclass_of might return inconsistent results on some PHP versions if \Serializable::class can be an interface. If so, you could instead use ReflectionClass::implementsInterface.
Loading history...
101 3
            strlen($className),
102 3
            $className
103
        );
104
105 3
        $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
106
107
        return static function () use ($serializedString) {
108 4
            return unserialize($serializedString);
109 2
        };
110
    }
111
112
    /**
113
     * @throws InvalidArgumentException
114
     * @throws ReflectionException
115
     */
116 23
    private function getReflectionClass(string $className) : ReflectionClass
117
    {
118 23
        if (! class_exists($className)) {
119 3
            throw InvalidArgumentException::fromNonExistingClass($className);
120
        }
121
122 20
        $reflection = new ReflectionClass($className);
123
124 20
        if ($reflection->isAbstract()) {
125 1
            throw InvalidArgumentException::fromAbstractClass($reflection);
126
        }
127
128 19
        return $reflection;
129
    }
130
131
    /**
132
     * @throws UnexpectedValueException
133
     */
134 3
    private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString) : void
135
    {
136
        set_error_handler(static function (int $code, string $message, string $file, int $line) use ($reflectionClass, &$error) : bool {
137 1
            $error = UnexpectedValueException::fromUncleanUnSerialization(
138 1
                $reflectionClass,
139 1
                $message,
140 1
                $code,
141 1
                $file,
142 1
                $line
143
            );
144
145 1
            return true;
146 3
        });
147
148
        try {
149 3
            $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
150 3
        } finally {
151 3
            restore_error_handler();
152
        }
153
154 3
        if ($error) {
155 1
            throw $error;
156
        }
157 2
    }
158
159
    /**
160
     * @throws UnexpectedValueException
161
     */
162 3
    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString) : void
163
    {
164
        try {
165 3
            unserialize($serializedString);
166
        } catch (Exception $exception) {
167
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
168
        }
169 3
    }
170
171 19
    private function isInstantiableViaReflection(ReflectionClass $reflectionClass) : bool
172
    {
173 19
        return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
174
    }
175
176
    /**
177
     * Verifies whether the given class is to be considered internal
178
     */
179 19
    private function hasInternalAncestors(ReflectionClass $reflectionClass) : bool
180
    {
181
        do {
182 19
            if ($reflectionClass->isInternal()) {
183 14
                return true;
184
            }
185
186 14
            $reflectionClass = $reflectionClass->getParentClass();
187 14
        } while ($reflectionClass);
188
189 5
        return false;
190
    }
191
192
    /**
193
     * Checks if a class is cloneable
194
     *
195
     * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
196
     */
197 18
    private function isSafeToClone(ReflectionClass $reflection) : bool
198
    {
199 18
        return $reflection->isCloneable()
200 18
            && ! $reflection->hasMethod('__clone')
201 18
            && ! $reflection->isSubclassOf(ArrayIterator::class);
202
    }
203
}
204