doctrine/inflector · error · RuntimeException

preg_replace returned null for value "%s"

Error message

preg_replace returned null for value "%s"

What it means

tableize() converts CamelCase to snake_case via preg_replace with the 'u' (UTF-8) modifier on the pattern '~(?<=\w)([A-Z])~u' (src/Inflector.php:234). PCRE refuses to process a subject that is not valid UTF-8, makes preg_replace return null, and the library throws RuntimeException instead of returning a broken table name. So this error almost always means the string you passed contains byte sequences that are not valid UTF-8 — legacy single-byte encodings, binary data, or a multibyte character cut in half.

Source

Thrown at src/Inflector.php:237

    /** @var WordInflector */
    private $pluralizer;

    public function __construct(WordInflector $singularizer, WordInflector $pluralizer)
    {
        $this->singularizer = $singularizer;
        $this->pluralizer   = $pluralizer;
    }

    /**
     * Converts a word into the format for a Doctrine table name. Converts 'ModelName' to 'model_name'.
     */
    public function tableize(string $word): string
    {
        $tableized = preg_replace('~(?<=\\w)([A-Z])~u', '_$1', $word);

        if ($tableized === null) {
            throw new RuntimeException(sprintf(
                'preg_replace returned null for value "%s"',
                $word
            ));
        }

        return mb_strtolower($tableized);
    }

    /**
     * Converts a word into the format for a Doctrine class name. Converts 'table_name' to 'TableName'.
     */
    public function classify(string $word): string
    {
        return str_replace([' ', '_', '-'], '', ucwords($word, ' _-'));
    }

    /**
     * Camelizes a word. This uses the classify() method and turns the first character to lowercase.

View on GitHub (pinned to 288d99b85c)

Solutions

  1. Find where the non-UTF-8 bytes enter (the exception message includes the offending value) and fix the source: database charset, file encoding, or upstream API output.
  2. Sanitize at the boundary before calling tableize(): if (!mb_check_encoding($word, 'UTF-8')) { $word = mb_convert_encoding($word, 'UTF-8', 'Windows-1252'); } using the real source encoding.
  3. Replace substr() with mb_substr() (or mb_strcut()) anywhere multibyte strings can be truncated.
  4. For MySQL, connect with utf8mb4 (charset in the DSN or SET NAMES utf8mb4) so reads are valid UTF-8.
  5. If the cause is unclear, reproduce with the same value and call preg_last_error_msg() right after the failing preg_* call to confirm PREG_BAD_UTF8_ERROR.

Example fix

// before
$table = $inflector->tableize($rawName); // $rawName is Windows-1252 or truncated UTF-8 -> RuntimeException

// after
if (! mb_check_encoding($rawName, 'UTF-8')) {
    $rawName = mb_convert_encoding($rawName, 'UTF-8', 'Windows-1252'); // adjust to the actual source encoding
}
$table = $inflector->tableize($rawName);
Defensive patterns

Strategy: validation

Validate before calling

use Doctrine\Inflector\Inflector;

if (! mb_check_encoding($word, 'UTF-8')) {
    $word = mb_convert_encoding($word, 'UTF-8', 'Windows-1252'); // use the actual source encoding
}

$table = $inflector->tableize($word);

Type guard

function isValidUtf8(string $value): bool
{
    return mb_check_encoding($value, 'UTF-8');
}

Try / catch

use RuntimeException;

try {
    $table = $inflector->tableize($word);
} catch (RuntimeException $e) {
    if (str_contains($e->getMessage(), 'preg_replace returned null')) {
        // input was not valid UTF-8: re-encode once and retry, or reject at this boundary
        $table = $inflector->tableize(mb_convert_encoding($word, 'UTF-8', 'Windows-1252'));
    } else {
        throw $e;
    }
}

Prevention

When it happens

Trigger: Calling $inflector->tableize($word) where $word is not valid UTF-8: text read from a latin1/Windows-1252 database column or CSV/Excel file, scraped filenames or external payloads, or a UTF-8 string truncated mid-character by substr() instead of mb_substr(). The 'u' modifier makes PCRE raise PREG_BAD_UTF8_ERROR, preg_replace returns null, and the null check at src/Inflector.php:236 throws.

Common situations: MySQL connection not set to utf8mb4 so retrieved text arrives as latin1; imports of Windows-1252 files; user-uploaded filenames passed through tableize(); strings sliced with substr() on multibyte content; data pipelines where an upstream system encodes to ISO-8859-1. Often appears after a data migration or a change of data source, and surfaces deep inside ORM/console tooling that tableizes entity or class names.

Related errors


AI-assisted analysis of doctrine/inflector@288d99b85c (2026-08-21). Data as JSON: /api/errors/e5083125373eb3af. Report an issue: GitHub.