Completed
Pull Request — master (#47)
by Michael
17:19 queued 11:46
created

Instantiator::instantiate()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

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