Completed
Pull Request — master (#26)
by Filip
03:36
created

Instantiator::checkIfUnSerializationIsSupported()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
ccs 13
cts 13
cp 1
rs 9.4285
cc 2
eloc 12
nc 2
nop 2
crap 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 Closure;
23
use Doctrine\Instantiator\Exception\InvalidArgumentException;
24
use Doctrine\Instantiator\Exception\UnexpectedValueException;
25
use Exception;
26
use ReflectionClass;
27
use Throwable;
28
29
/**
30
 * {@inheritDoc}
31
 *
32
 * @author Marco Pivetta <[email protected]>
33
 */
34
final class Instantiator implements InstantiatorInterface
35
{
36
    /**
37
     * Markers used internally by PHP to define whether {@see \unserialize} should invoke
38
     * the method {@see \Serializable::unserialize()} when dealing with classes implementing
39
     * the {@see \Serializable} interface.
40
     */
41
    const SERIALIZATION_FORMAT_USE_UNSERIALIZER   = 'C';
42
    const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
43
44
    /**
45
     * @var \Closure[] of {@see \Closure} instances used to instantiate specific classes
46
     */
47
    private static $cachedInstantiators = array();
48
49
    /**
50
     * @var object[] of objects that can directly be cloned
51
     */
52
    private static $cachedCloneables = array();
53
54
    /**
55
     * {@inheritDoc}
56
     */
57 40
    public function instantiate($className)
58
    {
59 40
        if (isset(self::$cachedCloneables[$className])) {
60 10
            return clone self::$cachedCloneables[$className];
61
        }
62
63 31
        if (isset(self::$cachedInstantiators[$className])) {
64 9
            $factory = self::$cachedInstantiators[$className];
65
66 9
            return $factory();
67
        }
68
69 22
        return $this->buildAndCacheFromFactory($className);
70
    }
71
72
    /**
73
     * Builds the requested object and caches it in static properties for performance
74
     *
75
     * @param string $className
76
     *
77
     * @return object
78
     */
79 22
    private function buildAndCacheFromFactory($className)
80
    {
81 22
        $factory  = self::$cachedInstantiators[$className] = $this->buildFactory($className);
82 17
        $instance = $factory();
83
84 17
        if ($this->isSafeToClone(new ReflectionClass($instance))) {
85 10
            self::$cachedCloneables[$className] = clone $instance;
86 10
        }
87
88 17
        return $instance;
89
    }
90
91
    /**
92
     * Builds a {@see \Closure} capable of instantiating the given $className without
93
     * invoking its constructor.
94
     *
95
     * @param string $className
96
     *
97
     * @return Closure
98
     */
99 24
    private function buildFactory($className)
100
    {
101 22
        $reflectionClass = $this->getReflectionClass($className);
102
103 18
        if ($this->isInstantiableViaReflection($reflectionClass)) {
104
            return function () use ($reflectionClass) {
105 24
                return $reflectionClass->newInstanceWithoutConstructor();
106 16
            };
107
        }
108
109 2
        $serializedString = sprintf(
110 2
            '%s:%d:"%s":0:{}',
111 2
            $this->getSerializationFormat($reflectionClass),
112 2
            strlen($className),
113
            $className
114 2
        );
115
116 2
        $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
117
118
        return function () use ($serializedString) {
119 2
            return unserialize($serializedString);
120 1
        };
121
    }
122
123
    /**
124
     * @param string $className
125
     *
126
     * @return ReflectionClass
127
     *
128
     * @throws InvalidArgumentException
129
     */
130 22
    private function getReflectionClass($className)
131
    {
132 22
        if (! class_exists($className)) {
133 3
            throw InvalidArgumentException::fromNonExistingClass($className);
134
        }
135
136 19
        $reflection = new ReflectionClass($className);
137
138 19
        if ($reflection->isAbstract()) {
139 1
            throw InvalidArgumentException::fromAbstractClass($reflection);
140
        }
141
142 18
        return $reflection;
143
    }
144
145
    /**
146
     * @param ReflectionClass $reflectionClass
147
     * @param string          $serializedString
148
     *
149
     * @throws UnexpectedValueException
150
     *
151
     * @return void
152
     */
153
    private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, $serializedString)
154
    {
155 2
        set_error_handler(function ($code, $message, $file, $line) use ($reflectionClass, & $error) {
156 1
            $error = UnexpectedValueException::fromUncleanUnSerialization(
157 1
                $reflectionClass,
158 1
                $message,
159 1
                $code,
160 1
                $file,
161
                $line
162 1
            );
163 2
        });
164
165 2
        $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
166
167 2
        restore_error_handler();
168
169 2
        if ($error) {
170 1
            throw $error;
171
        }
172 1
    }
173
174
    /**
175
     * @param ReflectionClass $reflectionClass
176
     * @param string          $serializedString
177
     *
178
     * @throws UnexpectedValueException
179
     *
180
     * @return void
181
     */
182 2
    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, $serializedString)
183
    {
184
        try {
185 2
            unserialize($serializedString);
186 2
        } catch (Exception $exception) {
187
            restore_error_handler();
188
189
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
190
        } catch (Throwable $exception) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Is this class maybe located in a folder that is not analyzed, or in a newer version of your dependencies than listed in your composer.lock/composer.json?
Loading history...
191
            restore_error_handler();
192
193
            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
194
        }
195 2
    }
196
197
    /**
198
     * @param ReflectionClass $reflectionClass
199
     *
200
     * @return bool
201
     */
202 18
    private function isInstantiableViaReflection(ReflectionClass $reflectionClass)
203
    {
204 18
        if (\PHP_VERSION_ID >= 50600) {
205 18
            return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
206
        }
207
208
        return \PHP_VERSION_ID >= 50400 && ! $this->hasInternalAncestors($reflectionClass);
209
    }
210
211
    /**
212
     * Verifies whether the given class is to be considered internal
213
     *
214
     * @param ReflectionClass $reflectionClass
215
     *
216
     * @return bool
217
     */
218 18
    private function hasInternalAncestors(ReflectionClass $reflectionClass)
219
    {
220
        do {
221 18
            if ($reflectionClass->isInternal()) {
222 13
                return true;
223
            }
224 13
        } while ($reflectionClass = $reflectionClass->getParentClass());
225
226 5
        return false;
227
    }
228
229
    /**
230
     * Verifies if the given PHP version implements the `Serializable` interface serialization
231
     * with an incompatible serialization format. If that's the case, use serialization marker
232
     * "C" instead of "O".
233
     *
234
     * @link http://news.php.net/php.internals/74654
235
     *
236
     * @param ReflectionClass $reflectionClass
237
     *
238
     * @return string the serialization format marker, either self::SERIALIZATION_FORMAT_USE_UNSERIALIZER
239
     *                or self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER
240
     */
241 2
    private function getSerializationFormat(ReflectionClass $reflectionClass)
242
    {
243 2
        if ($this->isPhpVersionWithBrokenSerializationFormat()
244 2
            && $reflectionClass->implementsInterface('Serializable')
245 2
        ) {
246
            return self::SERIALIZATION_FORMAT_USE_UNSERIALIZER;
247
        }
248
249 2
        return self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER;
250
    }
251
252
    /**
253
     * Checks whether the current PHP runtime uses an incompatible serialization format
254
     *
255
     * @return bool
256
     */
257 2
    private function isPhpVersionWithBrokenSerializationFormat()
258
    {
259 2
        return PHP_VERSION_ID === 50429 || PHP_VERSION_ID === 50513;
260
    }
261
262
    /**
263
     * Checks if a class is cloneable
264
     *
265
     * @param ReflectionClass $reflection
266
     *
267
     * @return bool
268
     */
269 17
    private function isSafeToClone(ReflectionClass $reflection)
270
    {
271 17
        if (method_exists($reflection, 'isCloneable') && ! $reflection->isCloneable()) {
272 6
            return false;
273
        }
274
275
        // not cloneable if it implements `__clone`, as we want to avoid calling it
276 11
        return ! $reflection->hasMethod('__clone');
277
    }
278
}
279