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