1<?php
2
3namespace Doctrine\Instantiator;
4
5use Doctrine\Instantiator\Exception\InvalidArgumentException;
6use Doctrine\Instantiator\Exception\UnexpectedValueException;
7use Exception;
8use ReflectionClass;
9use ReflectionException;
10use function class_exists;
11use function restore_error_handler;
12use function set_error_handler;
13use function sprintf;
14use function strlen;
15use function unserialize;
16
17/**
18 * {@inheritDoc}
19 */
20final 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    public function instantiate($className)
48    {
49        if (isset(self::$cachedCloneables[$className])) {
50            return clone self::$cachedCloneables[$className];
51        }
52
53        if (isset(self::$cachedInstantiators[$className])) {
54            $factory = self::$cachedInstantiators[$className];
55
56            return $factory();
57        }
58
59        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    private function buildAndCacheFromFactory(string $className)
68    {
69        $factory  = self::$cachedInstantiators[$className] = $this->buildFactory($className);
70        $instance = $factory();
71
72        if ($this->isSafeToClone(new ReflectionClass($instance))) {
73            self::$cachedCloneables[$className] = clone $instance;
74        }
75
76        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    private function buildFactory(string $className) : callable
88    {
89        $reflectionClass = $this->getReflectionClass($className);
90
91        if ($this->isInstantiableViaReflection($reflectionClass)) {
92            return [$reflectionClass, 'newInstanceWithoutConstructor'];
93        }
94
95        $serializedString = sprintf(
96            '%s:%d:"%s":0:{}',
97            self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
98            strlen($className),
99            $className
100        );
101
102        $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
103
104        return static function () use ($serializedString) {
105            return unserialize($serializedString);
106        };
107    }
108
109    /**
110     * @param string $className
111     *
112     * @throws InvalidArgumentException
113     * @throws ReflectionException
114     */
115    private function getReflectionClass($className) : ReflectionClass
116    {
117        if (! class_exists($className)) {
118            throw InvalidArgumentException::fromNonExistingClass($className);
119        }
120
121        $reflection = new ReflectionClass($className);
122
123        if ($reflection->isAbstract()) {
124            throw InvalidArgumentException::fromAbstractClass($reflection);
125        }
126
127        return $reflection;
128    }
129
130    /**
131     * @throws UnexpectedValueException
132     */
133    private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString) : void
134    {
135        set_error_handler(static function ($code, $message, $file, $line) use ($reflectionClass, & $error) : void {
136            $error = UnexpectedValueException::fromUncleanUnSerialization(
137                $reflectionClass,
138                $message,
139                $code,
140                $file,
141                $line
142            );
143        });
144
145        try {
146            $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
147        } finally {
148            restore_error_handler();
149        }
150
151        if ($error) {
152            throw $error;
153        }
154    }
155
156    /**
157     * @throws UnexpectedValueException
158     */
159    private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString) : void
160    {
161        try {
162            unserialize($serializedString);
163        } catch (Exception $exception) {
164            throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
165        }
166    }
167
168    private function isInstantiableViaReflection(ReflectionClass $reflectionClass) : bool
169    {
170        return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
171    }
172
173    /**
174     * Verifies whether the given class is to be considered internal
175     */
176    private function hasInternalAncestors(ReflectionClass $reflectionClass) : bool
177    {
178        do {
179            if ($reflectionClass->isInternal()) {
180                return true;
181            }
182
183            $reflectionClass = $reflectionClass->getParentClass();
184        } while ($reflectionClass);
185
186        return false;
187    }
188
189    /**
190     * Checks if a class is cloneable
191     *
192     * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
193     */
194    private function isSafeToClone(ReflectionClass $reflection) : bool
195    {
196        return $reflection->isCloneable() && ! $reflection->hasMethod('__clone');
197    }
198}
199