--- /dev/null
+<?php
+/*
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * This software consists of voluntary contributions made by many individuals
+ * and is licensed under the MIT license. For more information, see
+ * <http://www.doctrine-project.org>.
+ */
+
+namespace Doctrine\Instantiator;
+
+use Closure;
+use Doctrine\Instantiator\Exception\InvalidArgumentException;
+use Doctrine\Instantiator\Exception\UnexpectedValueException;
+use Exception;
+use ReflectionClass;
+
+/**
+ * {@inheritDoc}
+ *
+ * @author Marco Pivetta <ocramius@gmail.com>
+ */
+final class Instantiator implements InstantiatorInterface
+{
+ /**
+ * Markers used internally by PHP to define whether {@see \unserialize} should invoke
+ * the method {@see \Serializable::unserialize()} when dealing with classes implementing
+ * the {@see \Serializable} interface.
+ */
+ const SERIALIZATION_FORMAT_USE_UNSERIALIZER = 'C';
+ const SERIALIZATION_FORMAT_AVOID_UNSERIALIZER = 'O';
+
+ /**
+ * @var \Closure[] of {@see \Closure} instances used to instantiate specific classes
+ */
+ private static $cachedInstantiators = array();
+
+ /**
+ * @var object[] of objects that can directly be cloned
+ */
+ private static $cachedCloneables = array();
+
+ /**
+ * {@inheritDoc}
+ */
+ public function instantiate($className)
+ {
+ if (isset(self::$cachedCloneables[$className])) {
+ return clone self::$cachedCloneables[$className];
+ }
+
+ if (isset(self::$cachedInstantiators[$className])) {
+ $factory = self::$cachedInstantiators[$className];
+
+ return $factory();
+ }
+
+ return $this->buildAndCacheFromFactory($className);
+ }
+
+ /**
+ * Builds the requested object and caches it in static properties for performance
+ *
+ * @param string $className
+ *
+ * @return object
+ */
+ private function buildAndCacheFromFactory($className)
+ {
+ $factory = self::$cachedInstantiators[$className] = $this->buildFactory($className);
+ $instance = $factory();
+
+ if ($this->isSafeToClone(new ReflectionClass($instance))) {
+ self::$cachedCloneables[$className] = clone $instance;
+ }
+
+ return $instance;
+ }
+
+ /**
+ * Builds a {@see \Closure} capable of instantiating the given $className without
+ * invoking its constructor.
+ *
+ * @param string $className
+ *
+ * @return Closure
+ */
+ private function buildFactory($className)
+ {
+ $reflectionClass = $this->getReflectionClass($className);
+
+ if ($this->isInstantiableViaReflection($reflectionClass)) {
+ return function () use ($reflectionClass) {
+ return $reflectionClass->newInstanceWithoutConstructor();
+ };
+ }
+
+ $serializedString = sprintf(
+ '%s:%d:"%s":0:{}',
+ $this->getSerializationFormat($reflectionClass),
+ strlen($className),
+ $className
+ );
+
+ $this->checkIfUnSerializationIsSupported($reflectionClass, $serializedString);
+
+ return function () use ($serializedString) {
+ return unserialize($serializedString);
+ };
+ }
+
+ /**
+ * @param string $className
+ *
+ * @return ReflectionClass
+ *
+ * @throws InvalidArgumentException
+ */
+ private function getReflectionClass($className)
+ {
+ if (! class_exists($className)) {
+ throw InvalidArgumentException::fromNonExistingClass($className);
+ }
+
+ $reflection = new ReflectionClass($className);
+
+ if ($reflection->isAbstract()) {
+ throw InvalidArgumentException::fromAbstractClass($reflection);
+ }
+
+ return $reflection;
+ }
+
+ /**
+ * @param ReflectionClass $reflectionClass
+ * @param string $serializedString
+ *
+ * @throws UnexpectedValueException
+ *
+ * @return void
+ */
+ private function checkIfUnSerializationIsSupported(ReflectionClass $reflectionClass, $serializedString)
+ {
+ set_error_handler(function ($code, $message, $file, $line) use ($reflectionClass, & $error) {
+ $error = UnexpectedValueException::fromUncleanUnSerialization(
+ $reflectionClass,
+ $message,
+ $code,
+ $file,
+ $line
+ );
+ });
+
+ $this->attemptInstantiationViaUnSerialization($reflectionClass, $serializedString);
+
+ restore_error_handler();
+
+ if ($error) {
+ throw $error;
+ }
+ }
+
+ /**
+ * @param ReflectionClass $reflectionClass
+ * @param string $serializedString
+ *
+ * @throws UnexpectedValueException
+ *
+ * @return void
+ */
+ private function attemptInstantiationViaUnSerialization(ReflectionClass $reflectionClass, $serializedString)
+ {
+ try {
+ unserialize($serializedString);
+ } catch (Exception $exception) {
+ restore_error_handler();
+
+ throw UnexpectedValueException::fromSerializationTriggeredException($reflectionClass, $exception);
+ }
+ }
+
+ /**
+ * @param ReflectionClass $reflectionClass
+ *
+ * @return bool
+ */
+ private function isInstantiableViaReflection(ReflectionClass $reflectionClass)
+ {
+ if (\PHP_VERSION_ID >= 50600) {
+ return ! ($this->hasInternalAncestors($reflectionClass) && $reflectionClass->isFinal());
+ }
+
+ return \PHP_VERSION_ID >= 50400 && ! $this->hasInternalAncestors($reflectionClass);
+ }
+
+ /**
+ * Verifies whether the given class is to be considered internal
+ *
+ * @param ReflectionClass $reflectionClass
+ *
+ * @return bool
+ */
+ private function hasInternalAncestors(ReflectionClass $reflectionClass)
+ {
+ do {
+ if ($reflectionClass->isInternal()) {
+ return true;
+ }
+ } while ($reflectionClass = $reflectionClass->getParentClass());
+
+ return false;
+ }
+
+ /**
+ * Verifies if the given PHP version implements the `Serializable` interface serialization
+ * with an incompatible serialization format. If that's the case, use serialization marker
+ * "C" instead of "O".
+ *
+ * @link http://news.php.net/php.internals/74654
+ *
+ * @param ReflectionClass $reflectionClass
+ *
+ * @return string the serialization format marker, either self::SERIALIZATION_FORMAT_USE_UNSERIALIZER
+ * or self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER
+ */
+ private function getSerializationFormat(ReflectionClass $reflectionClass)
+ {
+ if ($this->isPhpVersionWithBrokenSerializationFormat()
+ && $reflectionClass->implementsInterface('Serializable')
+ ) {
+ return self::SERIALIZATION_FORMAT_USE_UNSERIALIZER;
+ }
+
+ return self::SERIALIZATION_FORMAT_AVOID_UNSERIALIZER;
+ }
+
+ /**
+ * Checks whether the current PHP runtime uses an incompatible serialization format
+ *
+ * @return bool
+ */
+ private function isPhpVersionWithBrokenSerializationFormat()
+ {
+ return PHP_VERSION_ID === 50429 || PHP_VERSION_ID === 50513;
+ }
+
+ /**
+ * Checks if a class is cloneable
+ *
+ * @param ReflectionClass $reflection
+ *
+ * @return bool
+ */
+ private function isSafeToClone(ReflectionClass $reflection)
+ {
+ if (method_exists($reflection, 'isCloneable') && ! $reflection->isCloneable()) {
+ return false;
+ }
+
+ // not cloneable if it implements `__clone`, as we want to avoid calling it
+ return ! $reflection->hasMethod('__clone');
+ }
+}