Completed
Push — master ( 8520af...dcbbb9 )
by Marco
8s
created

Instantiator   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 179
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 95.16%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 21
c 7
b 0
f 0
lcom 1
cbo 1
dl 0
loc 179
ccs 59
cts 62
cp 0.9516
rs 10

9 Methods

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