Completed
Push — master ( 185b88...fe8b2f )
by Marco
24s
created

Instantiator   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 185
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Test Coverage

Coverage 95.08%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 21
c 7
b 0
f 0
lcom 1
cbo 1
dl 0
loc 185
ccs 58
cts 61
cp 0.9508
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A instantiate() 0 14 3
A buildAndCacheFromFactory() 0 11 2
A getReflectionClass() 0 14 3
A checkIfUnSerializationIsSupported() 0 20 2
A attemptInstantiationViaUnSerialization() 0 10 2
A isInstantiableViaReflection() 0 4 2
A hasInternalAncestors() 0 10 3
A isSafeToClone() 0 4 2
A buildFactory() 0 21 2
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license. For more information, see
17
 * <http://www.doctrine-project.org>.
18
 */
19
20
namespace Doctrine\Instantiator;
21
22
use Doctrine\Instantiator\Exception\InvalidArgumentException;
23
use Doctrine\Instantiator\Exception\UnexpectedValueException;
24
use Exception;
25
use ReflectionClass;
26
27
/**
28
 * {@inheritDoc}
29
 *
30
 * @author Marco Pivetta <[email protected]>
31
 */
32
final class Instantiator implements InstantiatorInterface
33
{
34
    /**
35
     * Markers used internally by PHP to define whether {@see \unserialize} should invoke
36
     * the method {@see \Serializable::unserialize()} when dealing with classes implementing
37
     * the {@see \Serializable} interface.
38
     */
39
    const SERIALIZATION_FORMAT_USE_UNSERIALIZER   = 'C';
40
    const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
41
42
    /**
43
     * @var \callable[] used to instantiate specific classes, indexed by class name
44
     */
45
    private static $cachedInstantiators = [];
46
47
    /**
48
     * @var object[] of objects that can directly be cloned, indexed by class name
49
     */
50
    private static $cachedCloneables = [];
51
52
    /**
53
     * {@inheritDoc}
54
     */
55 40
    public function instantiate($className)
56
    {
57 40
        if (isset(self::$cachedCloneables[$className])) {
58 10
            return clone self::$cachedCloneables[$className];
59
        }
60
61 31
        if (isset(self::$cachedInstantiators[$className])) {
62 9
            $factory = self::$cachedInstantiators[$className];
63
64 9
            return $factory();
65
        }
66
67 22
        return $this->buildAndCacheFromFactory($className);
68
    }
69
70
    /**
71
     * Builds the requested object and caches it in static properties for performance
72
     *
73
     * @return object
74
     */
75 22
    private function buildAndCacheFromFactory(string $className)
76
    {
77 22
        $factory  = self::$cachedInstantiators[$className] = $this->buildFactory($className);
78 17
        $instance = $factory();
79
80 17
        if ($this->isSafeToClone(new ReflectionClass($instance))) {
81 10
            self::$cachedCloneables[$className] = clone $instance;
82
        }
83
84 17
        return $instance;
85
    }
86
87
    /**
88
     * Builds a callable capable of instantiating the given $className without
89
     * invoking its constructor.
90
     *
91
     * @throws InvalidArgumentException
92
     * @throws UnexpectedValueException
93
     * @throws \ReflectionException
94
     */
95 22
    private function buildFactory(string $className) : callable
96
    {
97 22
        $reflectionClass = $this->getReflectionClass($className);
98
99 18
        if ($this->isInstantiableViaReflection($reflectionClass)) {
100 16
            return [$reflectionClass, 'newInstanceWithoutConstructor'];
101
        }
102
103 2
        $serializedString = sprintf(
104 2
            '%s:%d:"%s":0:{}',
105 2
            self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
106 2
            strlen($className),
107 2
            $className
108
        );
109
110 2
        $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
111
112 2
        return function () use ($serializedString) {
113 2
            return unserialize($serializedString);
114 1
        };
115
    }
116
117
    /**
118
     * @param string $className
119
     *
120
     * @return ReflectionClass
121
     *
122
     * @throws InvalidArgumentException
123
     * @throws \ReflectionException
124
     */
125 22
    private function getReflectionClass($className) : ReflectionClass
126
    {
127 22
        if (! class_exists($className)) {
128 3
            throw InvalidArgumentException::fromNonExistingClass($className);
129
        }
130
131 19
        $reflection = new ReflectionClass($className);
132
133 19
        if ($reflection->isAbstract()) {
134 1
            throw InvalidArgumentException::fromAbstractClass($reflection);
135
        }
136
137 18
        return $reflection;
138
    }
139
140
    /**
141
     * @param ReflectionClass $reflectionClass
142
     * @param string          $serializedString
143
     *
144
     * @throws UnexpectedValueException
145
     *
146
     * @return void
147
     */
148
    private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, $serializedString) : void
149
    {
150 2
        set_error_handler(function ($code, $message, $file, $line) use ($reflectionClass, & $error) : void {
151 1
            $error = UnexpectedValueException::fromUncleanUnSerialization(
152 1
                $reflectionClass,
153 1
                $message,
154 1
                $code,
155 1
                $file,
156 1
                $line
157
            );
158 2
        });
159
160 2
        $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
161
162 2
        restore_error_handler();
163
164 2
        if ($error) {
165 1
            throw $error;
166
        }
167 1
    }
168
169
    /**
170
     * @param ReflectionClass $reflectionClass
171
     * @param string          $serializedString
172
     *
173
     * @throws UnexpectedValueException
174
     *
175
     * @return void
176
     */
177 2
    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, $serializedString) : void
178
    {
179
        try {
180 2
            unserialize($serializedString);
181
        } catch (Exception $exception) {
182
            restore_error_handler();
183
184
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
185
        }
186 2
    }
187
188 18
    private function isInstantiableViaReflection(ReflectionClass $reflectionClass) : bool
189
    {
190 18
        return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
191
    }
192
193
    /**
194
     * Verifies whether the given class is to be considered internal
195
     */
196 18
    private function hasInternalAncestors(ReflectionClass $reflectionClass) : bool
197
    {
198
        do {
199 18
            if ($reflectionClass->isInternal()) {
200 13
                return true;
201
            }
202 13
        } while ($reflectionClass = $reflectionClass->getParentClass());
203
204 5
        return false;
205
    }
206
207
    /**
208
     * Checks if a class is cloneable
209
     *
210
     * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
211
     */
212 17
    private function isSafeToClone(ReflectionClass $reflection) : bool
213
    {
214 17
        return $reflection->isCloneable() && ! $reflection->hasMethod('__clone');
215
    }
216
}
217