Instantiator.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <?php
  2. namespace Doctrine\Instantiator;
  3. use ArrayIterator;
  4. use Doctrine\Instantiator\Exception\ExceptionInterface;
  5. use Doctrine\Instantiator\Exception\InvalidArgumentException;
  6. use Doctrine\Instantiator\Exception\UnexpectedValueException;
  7. use Exception;
  8. use ReflectionClass;
  9. use ReflectionException;
  10. use Serializable;
  11. use function class_exists;
  12. use function enum_exists;
  13. use function is_subclass_of;
  14. use function restore_error_handler;
  15. use function set_error_handler;
  16. use function sprintf;
  17. use function strlen;
  18. use function unserialize;
  19. use const PHP_VERSION_ID;
  20. final 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. * @deprecated This constant will be private in 2.0
  28. */
  29. public const SERIALIZATION_FORMAT_USE_UNSERIALIZER = 'C';
  30. /** @deprecated This constant will be private in 2.0 */
  31. public const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
  32. /**
  33. * Used to instantiate specific classes, indexed by class name.
  34. *
  35. * @var callable[]
  36. */
  37. private static $cachedInstantiators = [];
  38. /**
  39. * Array of objects that can directly be cloned, indexed by class name.
  40. *
  41. * @var object[]
  42. */
  43. private static $cachedCloneables = [];
  44. /**
  45. * @param string $className
  46. * @phpstan-param class-string<T> $className
  47. *
  48. * @return object
  49. * @phpstan-return T
  50. *
  51. * @throws ExceptionInterface
  52. *
  53. * @template T of object
  54. */
  55. public function instantiate($className)
  56. {
  57. if (isset(self::$cachedCloneables[$className])) {
  58. /** @phpstan-var T */
  59. $cachedCloneable = self::$cachedCloneables[$className];
  60. return clone $cachedCloneable;
  61. }
  62. if (isset(self::$cachedInstantiators[$className])) {
  63. $factory = self::$cachedInstantiators[$className];
  64. return $factory();
  65. }
  66. return $this->buildAndCacheFromFactory($className);
  67. }
  68. /**
  69. * Builds the requested object and caches it in static properties for performance
  70. *
  71. * @phpstan-param class-string<T> $className
  72. *
  73. * @return object
  74. * @phpstan-return T
  75. *
  76. * @template T of object
  77. */
  78. private function buildAndCacheFromFactory(string $className)
  79. {
  80. $factory = self::$cachedInstantiators[$className] = $this->buildFactory($className);
  81. $instance = $factory();
  82. if ($this->isSafeToClone(new ReflectionClass($instance))) {
  83. self::$cachedCloneables[$className] = clone $instance;
  84. }
  85. return $instance;
  86. }
  87. /**
  88. * Builds a callable capable of instantiating the given $className without
  89. * invoking its constructor.
  90. *
  91. * @phpstan-param class-string<T> $className
  92. *
  93. * @phpstan-return callable(): T
  94. *
  95. * @throws InvalidArgumentException
  96. * @throws UnexpectedValueException
  97. * @throws ReflectionException
  98. *
  99. * @template T of object
  100. */
  101. private function buildFactory(string $className): callable
  102. {
  103. $reflectionClass = $this->getReflectionClass($className);
  104. if ($this->isInstantiableViaReflection($reflectionClass)) {
  105. return [$reflectionClass, 'newInstanceWithoutConstructor'];
  106. }
  107. $serializedString = sprintf(
  108. '%s:%d:"%s":0:{}',
  109. is_subclass_of($className, Serializable::class) ? self::SERIALIZATION_FORMAT_USE_UNSERIALIZER : self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER,
  110. strlen($className),
  111. $className
  112. );
  113. $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
  114. return static function () use ($serializedString) {
  115. return unserialize($serializedString);
  116. };
  117. }
  118. /**
  119. * @phpstan-param class-string<T> $className
  120. *
  121. * @phpstan-return ReflectionClass<T>
  122. *
  123. * @throws InvalidArgumentException
  124. * @throws ReflectionException
  125. *
  126. * @template T of object
  127. */
  128. private function getReflectionClass(string $className): ReflectionClass
  129. {
  130. if (! class_exists($className)) {
  131. throw InvalidArgumentException::fromNonExistingClass($className);
  132. }
  133. if (PHP_VERSION_ID >= 80100 && enum_exists($className, false)) {
  134. throw InvalidArgumentException::fromEnum($className);
  135. }
  136. $reflection = new ReflectionClass($className);
  137. if ($reflection->isAbstract()) {
  138. throw InvalidArgumentException::fromAbstractClass($reflection);
  139. }
  140. return $reflection;
  141. }
  142. /**
  143. * @phpstan-param ReflectionClass<T> $reflectionClass
  144. *
  145. * @throws UnexpectedValueException
  146. *
  147. * @template T of object
  148. */
  149. private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, string $serializedString): void
  150. {
  151. set_error_handler(static function (int $code, string $message, string $file, int $line) use ($reflectionClass, &$error): bool {
  152. $error = UnexpectedValueException::fromUncleanUnSerialization(
  153. $reflectionClass,
  154. $message,
  155. $code,
  156. $file,
  157. $line
  158. );
  159. return true;
  160. });
  161. try {
  162. $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
  163. } finally {
  164. restore_error_handler();
  165. }
  166. if ($error) {
  167. throw $error;
  168. }
  169. }
  170. /**
  171. * @phpstan-param ReflectionClass<T> $reflectionClass
  172. *
  173. * @throws UnexpectedValueException
  174. *
  175. * @template T of object
  176. */
  177. private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, string $serializedString): void
  178. {
  179. try {
  180. unserialize($serializedString);
  181. } catch (Exception $exception) {
  182. throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
  183. }
  184. }
  185. /**
  186. * @phpstan-param ReflectionClass<T> $reflectionClass
  187. *
  188. * @template T of object
  189. */
  190. private function isInstantiableViaReflection(ReflectionClass $reflectionClass): bool
  191. {
  192. return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
  193. }
  194. /**
  195. * Verifies whether the given class is to be considered internal
  196. *
  197. * @phpstan-param ReflectionClass<T> $reflectionClass
  198. *
  199. * @template T of object
  200. */
  201. private function hasInternalAncestors(ReflectionClass $reflectionClass): bool
  202. {
  203. do {
  204. if ($reflectionClass->isInternal()) {
  205. return true;
  206. }
  207. $reflectionClass = $reflectionClass->getParentClass();
  208. } while ($reflectionClass);
  209. return false;
  210. }
  211. /**
  212. * Checks if a class is cloneable
  213. *
  214. * Classes implementing `__clone` cannot be safely cloned, as that may cause side-effects.
  215. *
  216. * @phpstan-param ReflectionClass<T> $reflectionClass
  217. *
  218. * @template T of object
  219. */
  220. private function isSafeToClone(ReflectionClass $reflectionClass): bool
  221. {
  222. return $reflectionClass->isCloneable()
  223. && ! $reflectionClass->hasMethod('__clone')
  224. && ! $reflectionClass->isSubclassOf(ArrayIterator::class);
  225. }
  226. }