Completed
Pull Request — master (#59)
by Perez
02:11
created

Instantiator   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 181
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 96.77%

Importance

Changes 0
Metric Value
wmc 21
lcom 1
cbo 1
dl 0
loc 181
ccs 60
cts 62
cp 0.9677
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 24 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
     * @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 (int $code, string $message, string $file, int $line) use ($reflectionClass, & $error) : bool {
136 1
            $error = UnexpectedValueException::fromUncleanUnSerialization(
137 1
                $reflectionClass,
138 1
                $message,
139 1
                $code,
140 1
                $file,
141 1
                $line
142
            );
143
144 1
            return true;
145 2
        });
146
147
        try {
148 2
            $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
149 2
        } finally {
150 2
            restore_error_handler();
151
        }
152
153 2
        if ($error) {
154 1
            throw $error;
155
        }
156 1
    }
157
158
    /**
159
     * @throws UnexpectedValueException
160
     */
161 2
    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString) : void
162
    {
163
        try {
164 2
            unserialize($serializedString);
165
        } catch (Exception $exception) {
166
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
167
        }
168 2
    }
169
170 18
    private function isInstantiableViaReflection(ReflectionClass $reflectionClass) : bool
171
    {
172 18
        return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
173
    }
174
175
    /**
176
     * Verifies whether the given class is to be considered internal
177
     */
178 18
    private function hasInternalAncestors(ReflectionClass $reflectionClass) : bool
179
    {
180
        do {
181 18
            if ($reflectionClass->isInternal()) {
182 13
                return true;
183
            }
184
185 13
            $reflectionClass = $reflectionClass->getParentClass();
186 13
        } while ($reflectionClass);
187
188 5
        return false;
189
    }
190
191
    /**
192
     * Checks if a class is cloneable
193
     *
194
     * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
195
     */
196 17
    private function isSafeToClone(ReflectionClass $reflection) : bool
197
    {
198 17
        return $reflection->isCloneable() && ! $reflection->hasMethod('__clone');
199
    }
200
}
201