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
- Pass a ReaderInterface implementation as an INSTANCE: ->addReader(new CustomReader()).
- Or pass the class-string of a class implementing AdapterInterface that has a static create() (e.g. EnvConstAdapter::class, ArrayAdapter::class).
- Verify before calling: class_exists($s) && is_subclass_of($s, AdapterInterface::class).
- Check the namespace matches the installed major version (v5 adapters live in Dotenv\Repository\Adapter\).
- 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
- Instance => ReaderInterface; class-string => AdapterInterface. Never mix the two forms.
- A class-string adapter must expose public static create() returning a PhpOption\Option.
- After upgrading phpdotdotenv, grep for old adapter namespaces before wiring the builder.
- Let DI inject typed instances instead of config strings when possible.
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
- Expected name to be a non-empty string.
- Failed to parse dotenv file. %s
- Illegal character encoding [%s] specified.
- Conversion from encoding [%s] failed.
- At least one environment file path must be provided.
AI-assisted analysis of vlucas/phpdotenv@416df70283 (2026-08-21).
Data as JSON: /api/errors/70df334028f32fc7.
Report an issue: GitHub.