getgrav/grav · error · Pimple\Exception\FrozenServiceException

Cannot override frozen service "%s".

Error message

Cannot override frozen service "%s".

What it means

Pimple stores shared services as definitions and freezes them after their first resolution in Container::offsetGet(). Once frozen, the service has a fixed instantiated value; offsetSet() refuses any later assignment and throws FrozenServiceException to preserve object identity for code that already received the service.

Source

Thrown at system/src/Pimple/Container.php:107

     * Allowing any PHP callable leads to difficult to debug problems
     * as function names (strings) are callable (creating a function with
     * the same name as an existing parameter would break your container).
     *
     * @param string $id    The unique identifier for the parameter or object
     * @param mixed  $value The value of the parameter or a closure to define an object
     *
     * @return void
     *
     * @throws FrozenServiceException Prevent override of a frozen service
     */
    public function offsetSet(mixed $id, mixed $value): void
    {
        if (!is_string($id)) {
            throw new InvalidServiceIdentifierException($id);
        }

        if (isset($this->frozen[$id])) {
            throw new FrozenServiceException($id);
        }

        $this->values[$id] = $value;
        $this->keys[$id] = true;
    }

    /**
     * Gets a parameter or an object.
     *
     * @param string $id The unique identifier for the parameter or object
     *
     * @return mixed The value of the parameter or an object
     *
     * @throws UnknownIdentifierException If the identifier is not defined
     */
    public function offsetGet(mixed $id): mixed
    {
        if (!is_string($id)) {

View on GitHub (pinned to 6040efed04)

Solutions

  1. Move the override or service-provider registration before the first read of the service.
  2. Use $container->extend($id, $extension) before the service is resolved when you want to decorate the original definition rather than replace it.
  3. If replacement is intentional and you accept a new instance, call unset($container[$id]) first, then assign the new definition.
  4. Use $container->factory(...) when every access should create a fresh instance and freezing is not desired.
  5. Reorder plugin initialization so overrides happen during registration, not during a later hook.

Example fix

// before
$container['mailer'] = fn () => new Mailer('smtp-1');
$old = $container['mailer']; // freezes the shared service
$container['mailer'] = fn () => new Mailer('smtp-2'); // FrozenServiceException

// after
$container['mailer'] = fn () => new Mailer('smtp-1');
$container->extend('mailer', fn ($mailer, $c) => $mailer->withHost('smtp-2'));
$mailer = $container['mailer'];
Defensive patterns

Strategy: validation

Validate before calling

if (isset($container[$id]) && $container->initialized($id)) {
    throw new LogicException(sprintf('Cannot replace already resolved service "%s".', $id));
}
$container[$id] = $replacement;

Type guard

function isUnfrozenServiceId(Pimple\Container $container, string $id): bool
{
    return isset($container[$id]) && !$container->initialized($id);
}

Try / catch

try {
    $container[$id] = $replacement;
} catch (FrozenServiceException $e) {
    throw new LogicException(sprintf('Register replacement for "%s" before first resolution.', $id), 0, $e);
}

Prevention

When it happens

Trigger: Define $container['db'] as a closure, resolve it with $container['db'] or from a dependency of another service, then execute $container['db'] = $replacement. The second assignment reaches Container.php:106-108 and fails. The original resolution can also happen implicitly when another service or plugin fetches the ID first.

Common situations: A Grav plugin tries to replace $grav['twig'], $grav['config'], or another core service after boot has already resolved it; two plugins override the same service in an unpredictable order; test code fetches a service before replacing it.

Related errors


AI-assisted analysis of getgrav/grav@6040efed04 (2026-08-17). Data as JSON: /api/errors/bc4c82742593d554. Report an issue: GitHub.