Skip to content

Events

Gacela dispatches read-only lifecycle events as it boots, resolves services, reads config and manages caches. Listen to them for tracing, profiling, debugging or metrics — without touching your module code.

Zero-cost when nobody listens

Event dispatch is free when nothing listens. Every dispatch site first checks hasListeners() and skips building the event entirely when there are no listeners, so an app that registers none pays nothing — warm resolves are ~20% faster than before this optimisation landed in 1.18.0.

Registering listeners

Listeners are registered on GacelaConfig, in gacela.php or the Gacela::bootstrap() closure.

A generic listener — every event

php
registerGenericListener(callable $listener);
php
<?php # gacela.php

use Gacela\Framework\Event\GacelaEventInterface;

return function (GacelaConfig $config) {
  $config->registerGenericListener(
    function (GacelaEventInterface $event): void {
      error_log($event->toString());
    }
  );
};

A specific listener — one event type

php
registerSpecificListener(string $event, callable $listener);
php
<?php # gacela.php

use Gacela\Framework\Event\Bootstrap\GacelaBootstrapFinishedEvent;

return function (GacelaConfig $config) {
  $config->registerSpecificListener(
    GacelaBootstrapFinishedEvent::class,
    function (GacelaBootstrapFinishedEvent $event): void {
      error_log(sprintf('Bootstrap took %.2f ms', $event->durationMs()));
    }
  );
};

Every event implements GacelaEventInterface, which exposes toString(): string for logging. Concrete events add typed accessors — see the catalog below.

Lifecycle event catalog

The high-level events dispatched over a bootstrap, in the order you meet them. All were added in 1.18.0.

Gacela\Framework\Event\Bootstrap

EventDispatched whenAccessors
GacelaBootstrapStartedEventGacela::bootstrap() beginsappRootDir(): string
GacelaBootstrapFinishedEventbootstrap has finisheddurationMs(): float

Gacela\Framework\Event\Config

EventDispatched whenAccessors
ConfigInitializedEventthe merged configuration is assembledkeyCount(): int
ConfigKeyReadEventa config key is readkey(): string
ConfigKeyNotFoundEventa requested config key is missingkey(): string

Gacela\Framework\Event\Container

EventDispatched whenAccessors
BindingRegisteredEventa binding, alias or contextual binding is registeredid(): string
ServiceResolvedEventa service id is instantiated (once per id)id(): string

Gacela\Framework\Event\Provider

EventDispatched whenAccessors
ProviderRegisteredEventa module's Provider is registeredproviderClass(): string, moduleName(): string

Gacela\Framework\Event\Cache

EventDispatched whenAccessors
CacheClearedEventa cache file is removed (cache:clear)cacheFile(): string
CacheWarmedEventcache:warm finishesmoduleCount(): int, failedCount(): int

Recipes

Time the bootstrap

php
<?php # gacela.php

use Gacela\Framework\Event\Bootstrap\GacelaBootstrapFinishedEvent;

return function (GacelaConfig $config) {
  $config->registerSpecificListener(
    GacelaBootstrapFinishedEvent::class,
    fn (GacelaBootstrapFinishedEvent $e) => Metrics::timing('gacela.bootstrap_ms', $e->durationMs()),
  );
};

Log every resolved class

php
<?php # gacela.php

use Gacela\Framework\Event\ClassResolver\AbstractGacelaClassResolverEvent;
use Gacela\Framework\Event\GacelaEventInterface;

return function (GacelaConfig $config) {
  $config->registerGenericListener(function (GacelaEventInterface $event): void {
    if ($event instanceof AbstractGacelaClassResolverEvent) {
      error_log($event->toString());
    }
  });
};

Alert on missing config keys

php
<?php # gacela.php

use Gacela\Framework\Event\Config\ConfigKeyNotFoundEvent;

return function (GacelaConfig $config) {
  $config->registerSpecificListener(
    ConfigKeyNotFoundEvent::class,
    fn (ConfigKeyNotFoundEvent $e) => error_log("Missing config key: {$e->key()}"),
  );
};

Lower-level resolver & cache events

Beyond the lifecycle events above, Gacela dispatches fine-grained events during class resolution and cache bookkeeping. Reach for these when tracing why a class resolved the way it did. The class-resolution events share the AbstractGacelaClassResolverEvent base, so a single instanceof catches them all.

Gacela\Framework\Event\ClassResolver

  • AbstractGacelaClassResolverEvent (base type)
  • ResolvedClassCreatedEvent
  • ResolvedClassCachedEvent
  • ResolvedCreatedDefaultClassEvent
  • ResolvedClassTriedFromParentEvent

Gacela\Framework\Event\ClassResolver\ClassNameFinder

  • ClassNameValidCandidateFoundEvent
  • ClassNameInvalidCandidateFoundEvent
  • ClassNameCachedFoundEvent
  • ClassNameNotFoundEvent

Gacela\Framework\Event\ClassResolver\Cache

  • ClassNameCacheCachedEvent
  • ClassNamePhpCacheCreatedEvent
  • ClassNameInMemoryCacheCreatedEvent
  • CustomServicesCacheCachedEvent
  • CustomServicesPhpCacheCreatedEvent
  • CustomServicesInMemoryCacheCreatedEvent

Gacela\Framework\Event\ConfigReader

  • ReadPhpConfigEvent

Disabling events

Turn the whole system off — no listeners fire, and Gacela swaps in a no-op dispatcher:

php
<?php # gacela.php

return function (GacelaConfig $config) {
  $config->disableEventListeners();
};

Custom dispatcher

Gacela's default dispatcher implements EventDispatcherInterface:

php
interface EventDispatcherInterface
{
    public function dispatch(object $event): void;

    // Whether any listener would receive an event of the given class,
    // so hot-path dispatch sites can skip allocating the event.
    public function hasListeners(string $eventClass): bool;
}

Upgrading from < 1.18.0

hasListeners() was added to EventDispatcherInterface in 1.18.0. If you ship your own implementation of this interface, add the method — returning true keeps the previous always-dispatch behaviour.

See also

  • TestingGacelaTestCase records these events and turns them into assertions (assertServiceResolved(), assertBindingRegistered()).
  • Module Customization — where listeners fit among the other gacela.php hooks.
  • Bootstrap — the full GacelaConfig surface.