doctrine/instantiator · error · Doctrine\Instantiator\Exception\UnexpectedValueException

An exception was raised while trying to instantiate an insta

Error message

An exception was raised while trying to instantiate an instance of "%s" via un-serialization

What it means

When a class cannot be created via ReflectionClass::newInstanceWithoutConstructor() (final classes with internal ancestors, src/Instantiator.php:213-216), the instantiator falls back to a synthetic serialized payload like 'C:12:"ClassName":0:{}' and probes unserialize() with it (checkIfUnSerializationIsSupported(), src/Instantiator.php:167-190). If that probe unserialize() throws an Exception, fromSerializationTriggeredException() wraps it with the class name and chains the original as previous. The real cause is always in getPrevious(): userland code that runs during unserialization (Serializable::unserialize(), __wakeup(), __unserialize()) threw on the empty probe payload.

Source

Thrown at src/Exception/UnexpectedValueException.php:29

use function sprintf;

/**
 * Exception for given parameters causing invalid/unexpected state on instantiation
 */
class UnexpectedValueException extends BaseUnexpectedValueException implements ExceptionInterface
{
    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    public static function fromSerializationTriggeredException(
        ReflectionClass $reflectionClass,
        Exception $exception,
    ): self {
        return new self(
            sprintf(
                'An exception was raised while trying to instantiate an instance of "%s" via un-serialization',
                $reflectionClass->getName(),
            ),
            0,
            $exception,
        );
    }

    /**
     * @phpstan-param ReflectionClass<T> $reflectionClass
     *
     * @template T of object
     */
    public static function fromUncleanUnSerialization(
        ReflectionClass $reflectionClass,
        string $errorString,
        int $errorCode,
        string $errorFile,
        int $errorLine,

View on GitHub (pinned to cbb879d6ee)

Solutions

  1. Catch the exception and inspect getPrevious() — it holds the original exception thrown inside unserialize(), which names the actual faulting code.
  2. Make Serializable::unserialize() / __wakeup() / __unserialize() tolerate the empty payload: return early on '' or empty arrays instead of throwing.
  3. Prefer the modern __serialize()/__unserialize() pair over the legacy Serializable interface when you control the class.
  4. If the throwing behavior cannot be changed, construct the object yourself (new $className(...) or ReflectionClass::newInstanceWithoutConstructor() when the class allows it) instead of using this instantiator for that type.

Example fix

// before
class Money implements \Serializable
{
    public function unserialize($data): void
    {
        if ($data === '') {
            throw new \InvalidArgumentException('empty payload'); // breaks the Instantiator probe
        }
        // ...
    }
}

// after: tolerate the empty payload used by the instantiation probe
public function unserialize($data): void
{
    if ($data === '') {
        return;
    }
    // ...
}
Defensive patterns

Strategy: try-catch

Validate before calling

$reflection = new ReflectionClass($className);

if ($reflection->isSubclassOf(Serializable::class)
    || $reflection->hasMethod('__wakeup')
    || $reflection->hasMethod('__unserialize')
) {
    // the unserialization probe will invoke this code with an empty payload:
    // make sure it cannot throw before relying on Instantiator
    $instance = $reflection->newInstanceWithoutConstructor();
} else {
    $instance = (new Instantiator())->instantiate($className);
}

Type guard

function isSafeForUnserializationProbe(string $className): bool
{
    $r = new ReflectionClass($className);

    return ! $r->isSubclassOf(Serializable::class)
        && ! $r->hasMethod('__wakeup')
        && ! $r->hasMethod('__unserialize');
}

Try / catch

use Doctrine\Instantiator\Exception\UnexpectedValueException;

try {
    $instance = $instantiator->instantiate($className);
} catch (UnexpectedValueException $e) {
    $rootCause = $e->getPrevious(); // the exception thrown inside unserialize()
    // log $rootCause for diagnosis, then fall back to explicit construction
    return new $className(...$constructorArgs);
}

Prevention

When it happens

Trigger: instantiate() hits the unserialization fallback path and the probe unserialize() throws instead of raising a PHP warning: typically a class implementing Serializable whose unserialize('') throws (empty-string validation, argument checks), or a __wakeup()/__unserialize() that throws when the object state is empty. The catch at src/Instantiator.php:203-204 converts the thrown Exception into this wrapper.

Common situations: Legacy classes implementing the old Serializable interface with strict payload validation that rejects the empty payload the library probes with; __wakeup() implementations that assert preconditions on properties the synthetic payload never sets; final classes extending internal SPL/PDO classes forced onto the unserialize path; code migrated from __wakeup to __unserialize that now throws where it previously warned.

Related errors


AI-assisted analysis of doctrine/instantiator@cbb879d6ee (2026-08-21). Data as JSON: /api/errors/ea0d612b5d85aa57. Report an issue: GitHub.