vlucas/phpdotenv · error · InvalidArgumentException

Expected either an instance of %s or a class-string implemen

Error message

Expected either an instance of %s or a class-string implementing %s

What it means

RepositoryBuilder::addReader() (src/Repository/RepositoryBuilder.php:144) accepts exactly two kinds of argument: an instance of ReaderInterface, or a class-string naming a class that implements AdapterInterface (verified via class_exists + ReflectionClass::implementsInterface, src/Repository/RepositoryBuilder.php:120). Anything else — an object implementing neither, a string that is not an adapter class (including interface names or non-existent classes), or a scalar — throws InvalidArgumentException. When a class-string passes the check, the builder later calls its static create(); if it is unsupported it is silently skipped, not an error.

Source

Thrown at src/Repository/RepositoryBuilder.php:144

        return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class);
    }

    /**
     * Creates a repository builder with the given reader added.
     *
     * Accepts either a reader instance, or a class-string for an adapter. If
     * the adapter is not supported, then we silently skip adding it.
     *
     * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader
     *
     * @throws \InvalidArgumentException
     *
     * @return \Dotenv\Repository\RepositoryBuilder
     */
    public function addReader($reader)
    {
        if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) {
            throw new InvalidArgumentException(
                \sprintf(
                    'Expected either an instance of %s or a class-string implementing %s',
                    ReaderInterface::class,
                    AdapterInterface::class
                )
            );
        }

        $optional = Some::create($reader)->flatMap(static function ($reader) {
            return \is_string($reader) ? $reader::create() : Some::create($reader);
        });

        $readers = \array_merge($this->readers, \iterator_to_array($optional));

        return new self($readers, $this->writers, $this->immutable, $this->allowList);
    }

    /**

View on GitHub (pinned to 416df70283)

Solutions

  1. Pass a ReaderInterface implementation as an INSTANCE: ->addReader(new CustomReader()).
  2. Or pass the class-string of a class implementing AdapterInterface that has a static create() (e.g. EnvConstAdapter::class, ArrayAdapter::class).
  3. Verify before calling: class_exists($s) && is_subclass_of($s, AdapterInterface::class).
  4. Check the namespace matches the installed major version (v5 adapters live in Dotenv\Repository\Adapter\).
  5. If you implement your own adapter class-string, give it a public static create(): Option — otherwise $reader::create() fatals even after the guard passes.

Example fix

// before
$builder = RepositoryBuilder::createWithDefaultAdapters()
    ->addReader(\App\Env\CustomReader::class); // implements only ReaderInterface -> throws

// after
$builder = RepositoryBuilder::createWithDefaultAdapters()
    ->addReader(new \App\Env\CustomReader());
Defensive patterns

Strategy: type-guard

Type guard

use Dotenv\Repository\Adapter\AdapterInterface;
use Dotenv\Repository\Adapter\ReaderInterface;

function isAcceptableReader(ReaderInterface|string $candidate): bool
{
    if ($candidate instanceof ReaderInterface) {
        return true;
    }

    return is_string($candidate)
        && class_exists($candidate)
        && is_subclass_of($candidate, AdapterInterface::class);
}

Prevention

When it happens

Trigger: RepositoryBuilder::createWithDefaultAdapters()->addReader(new SomeAdapter()) where SomeAdapter implements neither ReaderInterface nor AdapterInterface; addReader('App\Env\CustomReader') where that class implements only ReaderInterface (a string must be an AdapterInterface class — pass a ReaderInterface as an instance instead); addReader(ReaderInterface::class) (an interface, so class_exists fails); addReader('Dotenv\Adapter\EnvConstAdapter') (old v2/v3 namespace string that no longer exists).

Common situations: Upgrading from phpdotenv 2/3/4 where adapters were registered as differently-named class-strings; wiring a custom reader and passing its class name out of habit; copy-pasted snippets from outdated docs/bundles.

Related errors


AI-assisted analysis of vlucas/phpdotenv@416df70283 (2026-08-21). Data as JSON: /api/errors/70df334028f32fc7. Report an issue: GitHub.