Completed
Push — master ( 39c580...8c8c1a )
by Marco
12s
created

attemptInstantiationViaUnSerialization()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

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