sebastianbergmann/phpunit · error · SubscriberTypeAlreadyRegisteredException

Subscriber type "%s" already registered

Error message

Subscriber type "%s" already registered

What it means

TypeMap refuses double registration: addMapping() throws SubscriberTypeAlreadyRegisteredException when the subscriber interface is already a key in the private mapping array. The map is append-only through the public API, so this usually means seeding logic ran twice or custom mappings were re-added on top of the Facade's defaults.

Source

Thrown at src/Event/TypeMap.php:165

        if (!in_array(Event::class, class_implements($eventClass), true)) {
            throw new InvalidEventException(
                sprintf(
                    'Event "%s" does not implement Event interface',
                    $eventClass,
                ),
            );
        }
    }

    /**
     * @param class-string $subscriberInterface
     *
     * @throws SubscriberTypeAlreadyRegisteredException
     */
    private function ensureSubscriberWasNotAlreadyRegistered(string $subscriberInterface): void
    {
        if (array_key_exists($subscriberInterface, $this->mapping)) {
            throw new SubscriberTypeAlreadyRegisteredException(
                sprintf(
                    'Subscriber type "%s" already registered',
                    $subscriberInterface,
                ),
            );
        }
    }

    /**
     * @param class-string $eventClass
     *
     * @throws EventAlreadyAssignedException
     */
    private function ensureEventWasNotAlreadyAssigned(string $eventClass): void
    {
        if (in_array($eventClass, $this->mapping, true)) {
            throw new EventAlreadyAssignedException(
                sprintf(

View on GitHub (pinned to f123cdb2a2)

Solutions

  1. Track which interfaces you have registered (e.g. a static set keyed by interface name) and skip duplicates before calling addMapping().
  2. Deduplicate the configuration driving the registration loop so each interface appears once.
  3. Do not re-add built-in subscriber interfaces; Facade::registerDefaultTypes() already claims them.

Example fix

// before
foreach ($config->subscriberMappings() as $interface => $event) {
    $typeMap->addMapping($interface, $event); // second pass throws
}

// after
$registered = [];
foreach ($config->subscriberMappings() as $interface => $event) {
    if (isset($registered[$interface])) { continue; }
    $typeMap->addMapping($interface, $event);
    $registered[$interface] = true;
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
    $typeMap->addMapping($subscriberInterface, $eventClass);
} catch (PHPUnit\Event\SubscriberTypeAlreadyRegisteredException $e) {
    // Idempotent bootstrap: the interface is already mapped, nothing to do
}

Prevention

When it happens

Trigger: Calling addMapping(MyInterface::class, ...) twice on the same TypeMap; a custom runner that merges its own mappings over a map already populated by Facade::registerDefaultTypes(); a registration loop iterating the same configuration twice.

Common situations: Custom bootstrap code that registers mappings once per loaded extension but is executed for multiple test suites in one process; copy-pasted registration blocks; duplicated extension entries in phpunit.xml.

Related errors


AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23). Data as JSON: /api/errors/49e74bf979a10b8c. Report an issue: GitHub.