PHPackages                             georgeff/kernel - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Framework](/categories/framework)
4. /
5. georgeff/kernel

ActiveLibrary[Framework](/categories/framework)

georgeff/kernel
===============

A lightweight application kernel with service container bootstrapping, a module system, and lifecycle callbacks

2.0.0(2w ago)048413MITPHPPHP ^8.4CI passing

Since Feb 10Pushed 1mo agoCompare

[ Source](https://github.com/MikeGeorgeff/kernel)[ Packagist](https://packagist.org/packages/georgeff/kernel)[ RSS](/packages/georgeff-kernel/feed)WikiDiscussions main Synced 1w ago

READMEChangelogDependencies (37)Versions (19)Used By (13)

Kernel
======

[](#kernel)

[![CI](https://github.com/MikeGeorgeff/kernel/actions/workflows/ci.yml/badge.svg)](https://github.com/MikeGeorgeff/kernel/actions/workflows/ci.yml)[![Coverage Status](https://camo.githubusercontent.com/f4f13f5731e8d251ecee1b1203c9ea860ca097a695f97d36d577f0b35a3cfc88/68747470733a2f2f636f766572616c6c732e696f2f7265706f732f6769746875622f4d696b6547656f72676566662f6b65726e656c2f62616467652e7376673f6272616e63683d6d61696e)](https://coveralls.io/github/MikeGeorgeff/kernel?branch=main)[![Packagist Version](https://camo.githubusercontent.com/527c5f8bc6ae3693aa98d6e8b52c4605a0a29620f91ddbe4de910ff63fe4c4e8/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f67656f72676566662f6b65726e656c)](https://packagist.org/packages/georgeff/kernel)

A lightweight application kernel with service container bootstrapping, a module system, and lifecycle callbacks.

Installation
------------

[](#installation)

```
composer require georgeff/kernel
```

Usage
-----

[](#usage)

### Basic Bootstrapping

[](#basic-bootstrapping)

```
use Georgeff\Kernel\Environment\Production;
use Georgeff\Kernel\Kernel;

$kernel = new Kernel(new Production());

$kernel->define('logger', fn() => new FileLogger('/var/log/app.log'))->share();
$kernel->define('mailer', fn() => new SmtpMailer('localhost'));

$kernel->boot();

$container = $kernel->getContainer();
$logger = $container->get('logger');
```

### Environments

[](#environments)

`EnvironmentInterface` (`getValue(): string`, `is(string ...$values): bool`) replaces a fixed enum, so consumers can define their own environments. Five concrete environments ship with the package:

- `Environment\Production`
- `Environment\Staging`
- `Environment\Development`
- `Environment\Testing`
- `Environment\Local`

`Local` is for local development machines. `Development` is the remote dev/integration tier.

```
use Georgeff\Kernel\Environment\Local;
use Georgeff\Kernel\Kernel;

$kernel = new Kernel(new Local(), debug: true);

$kernel->getEnvironment()->getValue(); // 'local'
$kernel->getEnvironment()->is('local'); // true
$kernel->isDebug();                     // true
```

To add your own environment, implement `EnvironmentInterface` directly or extend `AbstractEnvironment` (which already implements `is()`):

```
use Georgeff\Kernel\Environment\AbstractEnvironment;

final class Canary extends AbstractEnvironment
{
    public function getValue(): string
    {
        return 'canary';
    }
}
```

#### EnvironmentResolver

[](#environmentresolver)

`Support\EnvironmentResolver` is an optional convenience for resolving an environment from a string (e.g. an `APP_ENV` value) without hand-writing a switch statement. It is not required — nothing in the kernel depends on it, and `new Kernel(new Production())` works with zero awareness it exists.

```
use Georgeff\Kernel\Support\EnvironmentResolver;
use Georgeff\Kernel\Support\Env;

$resolver = new EnvironmentResolver();
$resolver->register('canary', Canary::class);

$kernel = new Kernel($resolver->resolve(Env::get('APP_ENV', 'production')));
```

`register()` throws `EnvironmentException` if the class doesn't exist or doesn't implement `EnvironmentInterface`. `resolve()` throws `EnvironmentException` for an unregistered name.

### Service Definitions

[](#service-definitions)

`define()` registers a service definition and returns a `DefinitionInterface` for fluent configuration:

```
$kernel->define('db.connection', fn() => new PdoConnection($dsn, $user, $pass))
    ->share()
    ->alias(ConnectionInterface::class)
    ->tag('db.connections');
```

MethodDescription`share()`Register the service as a singleton`alias(string $alias)`Add a container alias`tag(string $tag)`Add a tagAll three return the same definition instance, so they can be chained in any order.

A definition is not shared by default — each resolution constructs a new instance. Reach for `share()` only when a service actually needs to stay a single instance across the process: something expensive to construct, or something stateful other services need to see the same instance of, like a logger or a database connection.

`define()` throws `DefinitionException` if the id is already defined — each id can only be claimed once. Use `override()` to intentionally replace an existing definition, or `defineFallback()` to register a definition that's only used if nothing else claims the id.

### Service Definition Fallbacks

[](#service-definition-fallbacks)

`defineFallback()` registers a definition that's only used if nothing else defines that id by the time the kernel boots — useful for a module that wants to provide a sensible default without forcing consumers to configure it, or without conflicting with a "real" definition another module provides:

```
$kernel->defineFallback('db.connection', fn() => new PdoConnection('sqlite::memory:'))->share();

// Some other module (or the application itself) defines the real thing:
$kernel->define('db.connection', fn() => new PdoConnection($dsn, $user, $pass))->share();

$kernel->boot();
// The real definition wins — the fallback is never used, regardless of which was registered first.
```

If nothing else defines the id, the fallback is used instead:

```
$kernel->defineFallback('db.connection', fn() => new PdoConnection('sqlite::memory:'))->share();

$kernel->boot();
// PdoConnection('sqlite::memory:') — nothing else claimed 'db.connection'.
```

Multiple modules can register a fallback for the same id without conflict. Unlike `define()`, `defineFallback()` never throws for a duplicate id — none of the callers has an opinion about which implementation wins, only that *something* ends up satisfying the id, so the last one registered silently wins if more than one is present and nothing else defines the id outright.

`defineFallback()` throws `KernelException` if called after boot.

### Definition Tags

[](#definition-tags)

Tags group service definitions under a shared label so they can be collected and resolved together:

```
$kernel->define(FirstMiddleware::class, fn() => new FirstMiddleware())->tag('http.middleware');
$kernel->define(SecondMiddleware::class, fn() => new SecondMiddleware())->tag('http.middleware');
```

Retrieve all services for a tag via `TagRegistryInterface` after boot:

```
use Georgeff\Kernel\DI\TagRegistryInterface;

$kernel->boot();

$registry   = $kernel->getContainer()->get(TagRegistryInterface::class);
$middleware = $registry->getTagged('http.middleware');
// [FirstMiddleware, SecondMiddleware] — resolved in registration order
```

`getTaggedIds(string $tag): string[]` returns the container ids for a tag without resolving them — used internally by [`resetShared()`'s tag-scoped reset](#resetting-services), and available directly if you need the ids without triggering resolution.

Tagging the same id with the same tag more than once is idempotent — `tag()` only adds the tag if it isn't already present.

### Service Decoration

[](#service-decoration)

`decorate()` wraps an existing service definition with a decorator. The decorator callable receives the resolved inner service and the container:

```
$kernel->define(LoggerInterface::class, fn() => new FileLogger('/var/log/app.log'))->share();

$kernel->decorate(
    LoggerInterface::class,
    fn(LoggerInterface $inner, ContainerInterface $c) => new TimestampLogger($inner),
);

$kernel->boot();

$logger = $kernel->getContainer()->get(LoggerInterface::class);
// TimestampLogger wrapping FileLogger
```

The decorated service automatically inherits the original's shared flag, aliases, and tags — existing consumers resolve the decorated version transparently. Multiple decorators on the same id stack — the first registered wraps the original, each subsequent decorator wraps the previous result.

`decorate()` can be called from a module's `register()` method to decorate a service contributed by another module. Because decoration is applied after all modules have registered, load order does not matter:

```
final class LoggingModule implements ModuleInterface
{
    public function register(KernelInterface $kernel): void
    {
        $kernel->decorate(
            CacheInterface::class,
            fn(CacheInterface $inner, ContainerInterface $c) => new LoggingCache(
                $inner,
                $c->get(LoggerInterface::class),
            ),
        );
    }
}
```

`decorate()` throws `KernelException` if called after boot. A `DefinitionException` is thrown at boot time if the target definition does not exist.

### Service Overrides

[](#service-overrides)

`override()` replaces an existing service definition outright — unlike `decorate()`, which wraps the original, `override()` swaps it for a completely different implementation. This is the tool for substituting fakes or test doubles for real services, especially ones registered by a module:

```
$kernel->define(QueueInterface::class, fn() => new RabbitMQQueue($config))->share();

$kernel->override(QueueInterface::class, fn() => new InMemoryQueue());

$kernel->boot();

$queue = $kernel->getContainer()->get(QueueInterface::class);
// InMemoryQueue
```

Because the override is applied in its own boot phase — after all modules have registered but before decoration and container registration — it always wins, even when it targets a service a module hasn't registered yet at the point `override()` is called:

```
$kernel->override(QueueInterface::class, fn() => new InMemoryQueue());

$kernel->addModule(new QueueModule()); // registers the real QueueInterface

$kernel->boot();
// InMemoryQueue wins - the override is applied after QueueModule::register() runs
```

Unlike `decorate()`, `override()` does not inherit the original definition's shared flag, aliases, or tags by default — the replacement may have entirely different requirements than what it's replacing. Configure the replacement explicitly via the returned `DefinitionInterface`:

```
$kernel->override(QueueInterface::class, fn() => new InMemoryQueue())->share();
```

Pass `preserve: true` to copy the original's shared flag, aliases, and tags onto the override instead:

```
$kernel->override(QueueInterface::class, fn() => new InMemoryQueue(), preserve: true);
// InMemoryQueue is shared, aliased, and tagged exactly like the original QueueInterface registration
```

`override()` throws `KernelException` if called after boot. A `DefinitionException` is thrown at boot time if the target definition does not exist — `override()` can only replace something, not create it. An `override()` can target an id that only exists because a `defineFallback()` backfilled it, since fallbacks resolve before overrides do.

### Modules

[](#modules)

Modules are self-contained units that contribute service definitions, configuration, and boot logic to the kernel.

#### Defining a Module

[](#defining-a-module)

Every module implements `ModuleInterface` with a single `register()` method:

```
use Georgeff\Kernel\KernelInterface;
use Georgeff\Kernel\Contract\ModuleInterface;

final class DatabaseModule implements ModuleInterface
{
    public function register(KernelInterface $kernel): void
    {
        $kernel->define('db.connection', fn() => new PdoConnection(getenv('DB_DSN')))
            ->share()
            ->alias(ConnectionInterface::class);
    }
}
```

#### Configuration

[](#configuration)

Modules that need to declare configuration implement `ConfigurableModuleInterface`. The returned array is merged from every configurable module and made available as `Config\ConfigInterface` in the container after boot:

```
use Georgeff\Kernel\Config\ConfigInterface;
use Georgeff\Kernel\Contract\ConfigurableModuleInterface;
use Georgeff\Kernel\Contract\EnvironmentInterface;
use Georgeff\Kernel\KernelInterface;

final class DatabaseModule implements ConfigurableModuleInterface
{
    public function register(KernelInterface $kernel): void
    {
        $kernel->define('db.connection', fn(ContainerInterface $c) => new PdoConnection(
            $c->get(ConfigInterface::class)->branch('db')->get('dsn'),
        ))->share();
    }

    public function config(EnvironmentInterface $env): array
    {
        return [
            'db' => [
                'dsn'  => getenv('DB_DSN') ?: 'sqlite::memory:',
                'host' => getenv('DB_HOST') ?: 'localhost',
            ],
        ];
    }
}
```

The `$env` parameter is available for structural differences — for example, swapping a real driver for an in-memory one in testing:

```
public function config(EnvironmentInterface $env): array
{
    return [
        'db' => [
            'dsn' => $env->is('testing') ? 'sqlite::memory:' : getenv('DB_DSN'),
        ],
    ];
}
```

Config from multiple modules is merged in registration order. Later definitions overwrite earlier ones for the same top-level key.

`Env::get()` is available for reading environment variables with automatic type coercion — useful in `config()` implementations:

```
use Georgeff\Kernel\Support\Env;

public function config(EnvironmentInterface $env): array
{
    return [
        'db' => [
            'dsn'  => Env::get('DB_DSN', 'sqlite::memory:'),
            'port' => Env::get('DB_PORT', '3306'),
            'log'  => Env::get('DB_LOG', false),
        ],
    ];
}
```

Coercion rules:

ValueResult`'true'`, `'TRUE'`, `'(true)'``true``'false'`, `'FALSE'`, `'(false)'``false``'null'`, `'NULL'`, `'(null)'``null`Valid JSON object or array`array`Anything elseraw `string`Numeric strings are intentionally left as strings — port numbers and similar values are most useful as strings.

#### Reading Config

[](#reading-config)

`Config\ConfigInterface` (resolved from the container) exposes `has()`, `get()`, `all()`, `isEmpty()`, and `branch()` for fluent traversal into nested array config values:

```
use Georgeff\Kernel\Config\ConfigInterface;

$config = $kernel->getContainer()->get(ConfigInterface::class);

$config->has('db');                    // true
$config->branch('db')->get('port');    // 5432
$config->branch('missing')->isEmpty(); // true — a missing key produces an empty (not null) branch
$config->all();                        // the full merged config as a plain array
```

`get(string $name, mixed $default = null)` checks `has()` first, so an explicitly-stored `null` value is returned as-is rather than falling back to `$default`. `branch()` throws `ConfigException` if the value at that key exists but isn't an array with non-numeric string keys — a scalar or a list value can't silently be treated as a nested config, so a config-shape mistake fails loud instead of quietly returning nothing further down the chain.

#### Module Boot

[](#module-boot)

Modules that need access to the built container implement `BootableModuleInterface`. `boot()` is called after the container is fully initialized:

```
use Georgeff\Kernel\KernelInterface;
use Georgeff\Kernel\Contract\BootableModuleInterface;
use Psr\Container\ContainerInterface;

final class MigrationModule implements BootableModuleInterface
{
    public function register(KernelInterface $kernel): void { /* ... */ }

    public function boot(ContainerInterface $container): void
    {
        $container->get(Migrator::class)->run();
    }
}
```

Because the container is already built when `boot()` is called, new service definitions cannot be added here — use `register()` for that.

If a module's `register()` or `boot()` throws anything other than a `KernelExceptionInterface` or a PSR-11 `ContainerExceptionInterface`, it's wrapped in a `ModuleException` with the original preserved as the previous exception, and identifies which module and which phase failed.

#### Registering Modules

[](#registering-modules)

Register modules on the kernel before booting:

```
$kernel = new Kernel(new Production());

$kernel
    ->addModule(new DatabaseModule())
    ->addModule(new CacheModule())
    ->addModule(new MigrationModule());

$kernel->boot();
```

Each module class may only be registered once. Registering the same class twice throws a `ModuleException`. `addModule()` also throws if called after boot has started.

`getModules(): list` returns the class names of every module added so far — available immediately, before `boot()` is even called:

```
$kernel->addModule(new DatabaseModule());

$kernel->getModules(); // [DatabaseModule::class]
```

#### Aggregate Modules

[](#aggregate-modules)

A module can compose and cascade-load other modules by implementing `AggregateModuleInterface` — useful for a package that wants a single `addModule()` call to pull in everything it needs, including conditionally based on the environment:

```
use Georgeff\Kernel\Contract\AggregateModuleInterface;
use Georgeff\Kernel\Contract\EnvironmentInterface;
use Georgeff\Kernel\Contract\ModuleInterface;
use Georgeff\Kernel\KernelInterface;

final class DatabaseModule implements AggregateModuleInterface
{
    public function register(KernelInterface $kernel): void
    {
        // register this module's own services, if any
    }

    public function modules(EnvironmentInterface $env): array
    {
        $modules = [new MigrationModule()];

        if (!$env->is('production')) {
            $modules[] = new DatabaseDebugModule();
        }

        return $modules;
    }
}
```

```
$kernel->addModule(new DatabaseModule());
// MigrationModule (and DatabaseDebugModule outside production) are loaded automatically.
```

Aggregate expansion is recursive — a module returned by `modules()` can itself be an aggregate. The same module-dedup guard used for `addModule()` protects against accidental cycles.

#### Boot Phase Order

[](#boot-phase-order)

When `boot()` is called, the kernel proceeds through these phases in order:

1. `onBooting` callbacks
2. **Module load** — aggregate modules are expanded; `config()` is called on all `ConfigurableModuleInterface` modules and the merged result becomes `Config\ConfigInterface`
3. **Module registration** — `register()` is called on all modules
4. **Service fallbacks** — pending `defineFallback()` definitions are added for any id that's still undefined
5. **Service overrides** — pending overrides are applied
6. **Service decoration** — pending decorators are applied
7. Container initialization
8. **Module boot** — `boot()` is called on all `BootableModuleInterface` modules
9. `onBooted` callbacks — if `enableGc()` was called, its cleanup runs here too, as just another `onBooted` callback (see [Garbage Collection](#garbage-collection))

### Lifecycle Callbacks

[](#lifecycle-callbacks)

The kernel provides four hooks for tapping into the boot and shutdown lifecycle. All callbacks receive the full `KernelInterface` instance and all hook methods return the kernel for fluent chaining.

#### Boot callbacks

[](#boot-callbacks)

`onBooting` runs before service definitions are registered with the container. Use it to add definitions dynamically or configure the kernel before boot:

```
$kernel->onBooting(function (KernelInterface $kernel) {
    $kernel->define('dynamic', fn() => new SomeService());
});
```

`onBooted` runs after boot completes. The container is available at this point:

```
$kernel->onBooted(function (KernelInterface $kernel) {
    $kernel->getContainer()->get('logger')->info('Kernel booted');
});
```

Both must be registered before `boot()` is called.

Boot callbacks fail fast: if a callback throws, the remaining callbacks in that hook are never called and the exception propagates immediately, aborting `boot()`. A thrown `KernelExceptionInterface` or PSR-11 `ContainerExceptionInterface` propagates as-is; anything else is wrapped in a `HookException` with the original preserved as the previous exception. This matches the sequential, order-dependent nature of booting — there's no reason to keep registering services once an earlier step has already failed.

#### Shutdown callbacks

[](#shutdown-callbacks)

`onShutdown` runs before the kernel is marked as shut down. `afterShutdown` runs after. Both can be registered any time before `shutdown()` is called — including after boot:

```
$kernel->onShutdown(function (KernelInterface $kernel) {
    // isShutdown() is still false here
});

$kernel->afterShutdown(function (KernelInterface $kernel) {
    // isShutdown() is true here
});
```

Shutdown callbacks behave differently from boot callbacks: every callback still runs even if an earlier one throws, since shutdown is cleanup — one broken hook (say, a failed cache flush) shouldn't prevent other unrelated cleanup (closing a DB connection, releasing a lock) from running. If any callbacks failed, a single `HookException` is thrown once every callback has had a chance to run, with a message aggregating every failure and the first failure preserved as the previous exception.

#### Resolution hooks

[](#resolution-hooks)

`onResolving` and `onResolved` tap into the container's own resolution lifecycle — a pre-resolution hook fired before a service is resolved and a post-resolution hook fired after:

```
$kernel->onResolving(function (string $id) {
    // about to resolve $id
});

$kernel->onResolved(function (string $id, mixed $resolved) {
    // $id was just resolved to $resolved via its factory
});
```

Both must be registered before `boot()` is called, and both throw `KernelException` if called after boot.

### Shutdown

[](#shutdown)

Call `shutdown()` to run the shutdown lifecycle. It is idempotent and a no-op if the kernel has not been booted:

```
$kernel->boot();

// handle a request, run a command, etc.

$kernel->shutdown();

$kernel->isShutdown(); // true
```

Shutdown runs in this order:

1. `onShutdown` callbacks
2. The container is released and resolved-service-reset tracking is cleared
3. Kernel marked as shut down (`isShutdown()` becomes `true`)
4. `afterShutdown` callbacks

If an `onShutdown` callback throws, none of the following steps run — the container isn't released, `isShutdown()` stays `false`, and `afterShutdown` callbacks don't run. Shutdown either completes in full or is treated as not having happened at all, so a caller that catches the failure still has a working kernel rather than a half-torn-down one.

After shutdown, `getContainer()` and `resetShared()` both throw `KernelException` — there's nothing left to resolve or reset.

### Garbage Collection

[](#garbage-collection)

`enableGc()` opts the kernel into releasing boot-only working state — `DefinitionRepository`'s, `ModuleLoader`'s, and `HookRepository`'s (`onBooting`/`onBooted` callbacks) — once `boot()` completes. Not every environment needs this, so it isn't automatic; a short-lived script has nothing to gain from it, while a long-running worker or daemon that keeps a `Kernel` instance alive indefinitely does:

```
$kernel = new Kernel(new Production());
$kernel->enableGc();
$kernel->boot();
```

`enableGc()` just registers the cleanup as a normal `onBooted()` callback internally, so it inherits that method's guard — it must be called before `boot()`, from anywhere with a reference to the kernel. That includes from inside a module's own `register()`, so a module can opt the whole kernel into cleanup on its own authority without the top-level bootstrap needing to know or coordinate:

```
final class SomeModule implements ModuleInterface
{
    public function register(KernelInterface $kernel): void
    {
        $kernel->enableGc();
    }
}
```

Calling `enableGc()` more than once (e.g. two modules both opting in) is safe — an internal flag makes it idempotent, so only one cleanup callback ever gets registered regardless of how many callers opt in. It still throws every time it's called after `boot()`, even if it was already successfully enabled beforehand. `onShutdown`/`afterShutdown` callbacks are always left uncleared regardless, since they haven't run yet at that point.

### Resetting Services

[](#resetting-services)

For long-running processes (workers, daemons) that want a clean slate between units of work, `resetShared()` resets every resolved shared service that implements `Contract\ResettableInterface` back to its original state:

```
use Georgeff\Kernel\Contract\ResettableInterface;

final class ConnectionPool implements ResettableInterface
{
    public function reset(): void
    {
        // clear internal state
    }
}

$kernel->define(ConnectionPool::class, fn() => new ConnectionPool())->share();
$kernel->boot();

$kernel->getContainer()->get(ConnectionPool::class);

$kernel->resetShared();
// ConnectionPool::reset() was called automatically — no manual tagging required.
```

Detection is automatic: any resolved, shared service implementing `ResettableInterface` is tracked the moment it's resolved through the container. `resetShared()` throws `KernelException` if called before boot.

A service's `reset()` is allowed to fail up to a threshold before `resetShared()` gives up on it entirely and throws `ServiceResetException`. The default threshold is 3, passed as an argument:

```
$kernel->resetShared(failureThreshold: 5);
```

An individual service can override the default by implementing `ThresholdAwareResettableInterface`:

```
use Georgeff\Kernel\Contract\ThresholdAwareResettableInterface;

final class FlakyCache implements ThresholdAwareResettableInterface
{
    public function reset(): void { /* ... */ }

    public function getFailureThreshold(): int
    {
        return 1; // give up after the very first failure
    }
}
```

Failures are tracked per container id (not per class, so the same class backing two different ids is tracked independently) and accumulate across separate calls to `resetShared()` until a successful reset clears that service's failure history.

A single `resetShared()` call runs every tracked service's `reset()` to completion, regardless of earlier failures — one service breaching its threshold does not stop the rest from being attempted. Once the full pass finishes, if one or more services breached their threshold during that call, a single `ServiceResetException` is thrown aggregating every breach: its message lists each failed service id alongside its exception message, and `getPrevious()` returns the first breach's original exception.

#### Scoping a reset by tag

[](#scoping-a-reset-by-tag)

Pass one or more tags to reset only the resettable services carrying at least one of them, instead of every tracked service:

```
$kernel->define(ConnectionPool::class, fn() => new ConnectionPool())->share()->tag('db');
$kernel->define(SessionCache::class, fn() => new SessionCache())->share()->tag('cache');

$kernel->boot();
$kernel->getContainer()->get(ConnectionPool::class);
$kernel->getContainer()->get(SessionCache::class);

$kernel->resetShared(3, 'db');
// Only ConnectionPool::reset() was called — SessionCache is untouched.

$kernel->resetShared(3, 'db', 'cache');
// Both are reset — a service tagged with more than one requested tag is still only reset once.
```

Calling `resetShared()` with no tags resets everything, same as before. A tag that doesn't match any currently-tracked resettable service — because nothing was tagged with it, or because the tagged services haven't been resolved yet — is a silent no-op rather than an error, so a mistyped tag name will not raise anything; double-check the tag string if a scoped reset doesn't appear to be doing anything.

### Custom Container Builder

[](#custom-container-builder)

The kernel uses a `Contract\ContainerBuilderInterface` to register definitions with the underlying container. `DI\ContainerBuilder`, backed by `georgeff/container`, is used by default. Provide your own to use a different container implementation:

```
$builder = new MyContainerBuilder();
$kernel = new Kernel(new Production(), $builder);
```

### Debug Mode

[](#debug-mode)

When debug mode is enabled, the kernel profiles the boot process and tracks service resolutions:

```
$kernel = new Kernel(new Development(), debug: true);
$kernel->boot();

$kernel->getStartTime(); // float (microtime)
$kernel->getDebugInfo(); // profiles + components (module/service-resolution/resetter data), merged by Profiler\Profiler
```

The `getDebugInfo()` array contains:

- `profiles.boot` — timing and memory usage for each boot phase
- `profiles.shutdown` — timing for the `shuttingDown` and `afterShutdown` phases; only present once `shutdown()` has actually been called
- `components.modules` — module loader state: which module classes were added and whether each phase has run; present as soon as debug mode is enabled, even before `boot()` is called
- `components.service.resolution` — which services have been resolved and which remain unresolved; only present once `boot()` has run. Each resolved entry reports `count` (number of times resolved), `duration` and `memory` (summed across every resolution), a `resolutions` list with the timing/memory breakdown of each individual resolution, and, for services implementing `DebuggableInterface`, a `debug.info` key holding the most recently resolved instance's own debug data. A shared service resolved more than once (repeat `get()` calls hitting the container's cache) is only counted once; a non-shared service resolved multiple times accumulates one entry in `resolutions` per resolution
- `components.service.resetter` — failure counts and logged exception messages per service id, for services that have failed a `reset()` at least once; present as soon as debug mode is enabled, even before `boot()` is called

Debuggable services registered against the profiler are nested under `components` specifically to keep that namespace separate from `profiles` — a registered name could otherwise collide with a profile name (e.g. a service registered as `boot`).

When debug is disabled, `getStartTime()` returns `null` and `getDebugInfo()` returns `[]`. `getStartTime()` also returns `null` in debug mode before `boot()` has been called.

#### DebuggableInterface

[](#debuggableinterface)

Services can implement `DebuggableInterface` to expose debug data. In debug mode, their `getDebugInfo()` output is collected automatically after each factory resolution and included in the kernel's debug info under `components.service.resolution.resolved..debug.info`:

```
use Georgeff\Kernel\Contract\DebuggableInterface;

final class ConnectionPool implements DebuggableInterface
{
    public function getDebugInfo(): array
    {
        return ['active' => $this->activeCount, 'idle' => $this->idleCount];
    }
}
```

`getDebugInfo()` is expected to return only scalars, `null`, and nested arrays of the same — see `DebuggableInterface`'s own docblock for the full contract. If something else slips through anyway, `components` is sanitized before being returned: objects are reduced to their class name, `Closure` instances to a `Closure#` reference string, enum cases to their backed value (or their case name for a non-backed enum), and resources to their resource type (e.g. `'stream'`). This is a defensive safety net so a misbehaving `getDebugInfo()` can't produce unprintable/unserializable debug output — it's not a substitute for returning meaningful data in the first place. `profiles` is never sanitized, since it's built entirely from this package's own internal timing data and can't contain anything unsafe.

### Exceptions

[](#exceptions)

Every exception this package throws implements `Exception\KernelExceptionInterface`, so callers can catch one type regardless of which specific exception was thrown:

```
use Georgeff\Kernel\Exception\KernelExceptionInterface;

try {
    $kernel->boot();
} catch (KernelExceptionInterface $e) {
    // KernelException, ModuleException, ConfigException, DefinitionException,
    // EnvironmentException, HookException, or ServiceResetException
}
```

`KernelException` (general kernel-state guards), `ModuleException` (module lifecycle), `ConfigException` (`Config::branch()`), `DefinitionException` (`DefinitionRepository` guards), `EnvironmentException` (`EnvironmentResolver`), `HookException` (a lifecycle callback failing — see [Lifecycle Callbacks](#lifecycle-callbacks)), and `ServiceResetException` (`resetShared()`) each provide static helpers via the shared `Exception\ThrowHelpers` trait:

```
use Georgeff\Kernel\Exception\KernelException;

// Always throws
KernelException::throw('Something went wrong');

// Throws if $condition is true
KernelException::throwIf($this->isBooted(), 'Kernel is already booted');

// Throws if $condition is false
KernelException::throwIfNot($this->isBooted(), 'Kernel has not been booted');
```

Each accepts an optional second (or third, for `throwIf`/`throwIfNot`) `$previous` throwable. These are primarily useful when authoring custom kernel subclasses or modules that need guard conditions consistent with the kernel's own error types.

### Extending the Kernel

[](#extending-the-kernel)

The `Kernel` class can be extended for specialized use cases such as HTTP or console kernels. `Contract\RunnableKernelInterface extends KernelInterface` is provided for kernels that serve as an application entry point:

```
use Georgeff\Kernel\Contract\RunnableKernelInterface;

class ConsoleKernel extends Kernel implements RunnableKernelInterface
{
    public function run(): int
    {
        $this->boot();

        // dispatch console command...

        return 0;
    }
}
```

`Kernel::$profiler` (`protected private(set) ?Profiler\Profiler`) uses PHP 8.4 asymmetric visibility: a subclass can read it — e.g. to inspect an active profile or register its own `DebuggableInterface` components — but only `Kernel` itself can assign to it.

Changelog
---------

[](#changelog)

See [CHANGELOG.md](CHANGELOG.md).

License
-------

[](#license)

MIT

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance92

Actively maintained with recent releases

Popularity16

Limited adoption so far

Community17

Small or concentrated contributor base

Maturity61

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~11 days

Total

16

Last Release

20d ago

Major Versions

1.x-dev → 2.0.02026-07-28

PHP version history (2 changes)1.0.0PHP ^8.2

2.0.0PHP ^8.4

### Community

Maintainers

![](https://www.gravatar.com/avatar/c8147ce1d901e9fa2ec95d6408e5862025211f9ae60131e41fb115a5a0916ce4?d=identicon)[georgeff](/maintainers/georgeff)

---

Top Contributors

[![MikeGeorgeff](https://avatars.githubusercontent.com/u/6169468?v=4)](https://github.com/MikeGeorgeff "MikeGeorgeff (34 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/georgeff-kernel/health.svg)

```
[![Health](https://phpackages.com/badges/georgeff-kernel/health.svg)](https://phpackages.com/packages/georgeff-kernel)
```

###  Alternatives

[symfony/symfony

The Symfony PHP framework

31.4k87.4M2.2k](/packages/symfony-symfony)[drupal/core-recommended

Locked core dependencies; require this project INSTEAD OF drupal/core.

6943.5M449](/packages/drupal-core-recommended)[typo3/cms

TYPO3 CMS is a free open source Content Management Framework initially created by Kasper Skaarhoj and licensed under GNU/GPL.

1.2k1.9M122](/packages/typo3-cms)[spiral/framework

Spiral, High-Performance PHP/Go Framework

2.1k2.3M72](/packages/spiral-framework)[typo3/cms-core

TYPO3 CMS Core

3313.6M5.6k](/packages/typo3-cms-core)[testo/testo

A lightweight PHP testing framework.

20319.6k179](/packages/testo-testo)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
