Completed
Pull Request — master (#53)
by Farhad
01:54
created

Instantiator   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 187
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 96.72%

Importance

Changes 0
Metric Value
wmc 21
lcom 1
cbo 1
dl 0
loc 187
ccs 59
cts 61
cp 0.9672
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A instantiate() 0 14 3
A buildAndCacheFromFactory() 0 11 2
A buildFactory() 0 21 2
A getReflectionClass() 0 14 3
A checkIfUnSerializationIsSupported() 0 22 2
A attemptInstantiationViaUnSerialization() 0 8 2
A isInstantiableViaReflection() 0 4 2
A hasInternalAncestors() 0 12 3
A isSafeToClone() 0 4 2
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
     * @return ReflectionClass
113
     *
114
     * @throws InvalidArgumentException
115
     * @throws ReflectionException
116
     */
117 22
    private function getReflectionClass($className) : ReflectionClass
118
    {
119 22
        if (! class_exists($className)) {
120 3
            throw InvalidArgumentException::fromNonExistingClass($className);
121
        }
122
123 19
        $reflection = new ReflectionClass($className);
124
125 19
        if ($reflection->isAbstract()) {
126 1
            throw InvalidArgumentException::fromAbstractClass($reflection);
127
        }
128
129 18
        return $reflection;
130
    }
131
132
    /**
133
     * @param ReflectionClass $reflectionClass
134
     * @param string $serializedString
135
     *
136
     * @throws UnexpectedValueException
137
     */
138 2
    private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString) : void
139
    {
140
        set_error_handler(static function ($code, $message, $file, $line) use ($reflectionClass, & $error) : void {
141 1
            $error = UnexpectedValueException::fromUncleanUnSerialization(
142 1
                $reflectionClass,
143 1
                $message,
144 1
                $code,
145 1
                $file,
146 1
                $line
147
            );
148 2
        });
149
150
        try {
151 2
            $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
152 2
        } finally {
153 2
            restore_error_handler();
154
        }
155
156 2
        if ($error) {
157 1
            throw $error;
158
        }
159 1
    }
160
161
    /**
162
     * @param ReflectionClass $reflectionClass
163
     * @param string $serializedString
164
     *
165
     * @throws UnexpectedValueException
166
     */
167 2
    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString) : void
168
    {
169
        try {
170 2
            unserialize($serializedString);
171
        } catch (Exception $exception) {
172
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
173
        }
174 2
    }
175
176 18
    private function isInstantiableViaReflection(ReflectionClass $reflectionClass) : bool
177
    {
178 18
        return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
179
    }
180
181
    /**
182
     * Verifies whether the given class is to be considered internal
183
     */
184 18
    private function hasInternalAncestors(ReflectionClass $reflectionClass) : bool
185
    {
186
        do {
187 18
            if ($reflectionClass->isInternal()) {
188 13
                return true;
189
            }
190
191 13
            $reflectionClass = $reflectionClass->getParentClass();
192 13
        } while ($reflectionClass);
193
194 5
        return false;
195
    }
196
197
    /**
198
     * Checks if a class is cloneable
199
     *
200
     * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
201
     */
202 17
    private function isSafeToClone(ReflectionClass $reflection) : bool
203
    {
204 17
        return $reflection->isCloneable() && ! $reflection->hasMethod('__clone');
205
    }
206
}
207