Completed
Push — master ( 67dbbd...ae466f )
by Marco
14s queued 11s
created

src/Doctrine/Instantiator/Instantiator.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
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