Kernel
The Koffer for generated Jardis domains: an immutable service container plus an optional ENV packer.
Introduction
Every generated domain needs infrastructure: a database connection, cache, logger, event dispatcher and more. jardiscore/kernel bundles all of that into a Koffer (DomainKernel), an immutable container the domain takes in its constructor: new Ecommerce($kernel).
The package deliberately does little: it delivers the Koffer and an optional packer that builds it from .env. Nothing more. The Koffer builds nothing itself and reads no ENV; it is a pure, unchangeable consumer.
- DomainKernel — the Koffer: immutable, eleven services via constructor injection, implements
DomainKernelInterface - BuildDomainKernelFromEnv — optional packer:
__invoke(string $configPath): DomainKernel, composes ten handler closures and reads the.envcascade
Kernel decoupling (2026-07)
DomainApp, BoundedContext, ServiceRegistry and the response classes (ContextResponse, DomainResponse, DomainResponseTransformer) have been removed from this package. The Builder now generates them per domain as {Domain}Context (a 1:1 port of the former BoundedContext) and {Domain}\Response\*. ResponseStatus now lives in jardissupport/contracts. In new code, never reach for DomainApp, BoundedContext or ServiceRegistry. They no longer exist. The generated {Domain}Context provides the equivalent surface.
Installation
composer require jardiscore/kernelGitHub: jardisCore/kernel
Dependencies:
| Package | Purpose |
|---|---|
jardissupport/contracts | Interface contracts (PSR + Jardis), DomainKernelInterface, ResponseStatus, GeneratedContextInterface |
jardissupport/classversion | Versioned class resolution |
jardissupport/dotenv | Load the ENV cascade |
jardissupport/factory | PSR-11 container + reflection instantiation |
Optional (composer suggest, used by the packer, degrades to null when absent):
jardisadapter/{cache,dbconnection,eventdispatcher,filesystem,http,logger,mailer}, ext-redis
DomainKernel — the Koffer
Immutable service container: all services are injected in the constructor, nothing can be changed afterward. Safe for long-running processes like workers or servers.
use JardisCore\Kernel\DomainKernel;
$kernel = new DomainKernel(
domainRoot: '/path/to/config', // required, must not be empty
container: $factory, // ?ContainerInterface
cache: $cache, // ?CacheInterface
logger: $logger, // ?LoggerInterface
eventDispatcher: $dispatcher, // ?EventDispatcherInterface
eventListenerRegistry: $registry, // ?EventListenerRegistryInterface
httpClient: $client, // ?ClientInterface
connection: $pool, // ConnectionPoolInterface|PDO|null
mailer: $mailer, // ?MailerInterface
filesystem: $filesystemService, // ?FilesystemServiceInterface
env: ['db_host' => 'localhost'], // array — private ENV, takes precedence over $_ENV
);
$ecommerce = new Ecommerce($kernel); // generated domain facade — no extendsAll parameters except domainRoot are optional. domainRoot must not be empty, or the constructor throws. The Koffer is immutable after creation.
The eleven accessors
| Method | Return | Note |
|---|---|---|
domainRoot() | string | |
env(string $key) | mixed | case-insensitive; private ENV > $_ENV, stored lowercase |
container() | Factory | always wraps the injected container (not just ContainerInterface) |
cache() | ?CacheInterface | |
logger() | ?LoggerInterface | |
eventDispatcher() | ?EventDispatcherInterface | |
eventListenerRegistry() | ?EventListenerRegistryInterface | added with the Kernel decoupling — paired with eventDispatcher(), the same provider instance. A generated {Agg}EventRouter registers itself on it; without a registry, event routing simply stays inactive (no error) |
httpClient() | ?ClientInterface | |
dbConnection() | ConnectionPoolInterface|PDO|null | |
mailer() | ?MailerInterface | |
filesystem() | ?FilesystemServiceInterface |
container() always returns a Factory: if an external container is passed, it is embedded. Reflection-based instantiation is therefore always available.
Sharing across multiple domains is explicit
There is no static service registry anymore (ServiceRegistry has been removed). Anyone who wants to share services passes the same Koffer to multiple domains:
$kernel = (new BuildDomainKernelFromEnv())(__DIR__ . '/config');
$ecommerce = new Ecommerce($kernel); // same Koffer instance
$billing = new Billing($kernel); // same instance → same connection, cache, …A domain that needs its own services builds its own Koffer. There is no implicit fallback anymore.
BuildDomainKernelFromEnv — the ENV packer
Optional packer that assembles a Koffer from an .env cascade. An invokable class, not a static factory call.
use JardisCore\Kernel\Bootstrap\BuildDomainKernelFromEnv;
$packer = new BuildDomainKernelFromEnv();
$kernel = $packer(__DIR__ . '/config'); // reads config/.env (+ cascade) via DotEnv::loadPrivate()
$ecommerce = new Ecommerce($kernel);- One invokable class —
__invoke(string $configPath): DomainKernel.$configPathalso becomes thedomainRoot()of the packed Koffer. - ENV cascade via
JardisSupport\DotEnv\DotEnv::loadPrivate(), the sameload()/load?()cascade as any other Jardis configuration. Templates:docs/env-examples/. - Composes ten handler closures in the constructor (
(new Handler())->__invoke(...), Closure-Orchestrator, no logic of its own in the body):BuildConnectionFromEnv,ExtractPdoFromConnection,BuildRedisFromEnv,BuildCacheFromEnv,BuildLoggerFromEnv,BuildEventListenerProviderFromEnv,BuildEventDispatcherFromProvider,BuildHttpClientFromEnv,BuildMailerFromEnv,BuildFilesystemFromEnv, plus aloadEnvclosure. - Event dispatcher + registry are a pair: a
ListenerProvideris built and passed both aseventDispatcher()(wrapped) and aseventListenerRegistry()(unchanged). So a generated{Agg}EventRouteris visible to the dispatcher. - Every adapter is optional (
composer suggest): each handler closure degrades tonullvia aclass_exists()guard when its adapter is missing or its ENV is unconfigured. Nothing throws for a missing optional service. - Container wiring is out of scope: the packed Koffer uses the bare
Factoryas a fallback. Anyone who needs a custom PSR-11 container builds theDomainKerneldirectly instead of going through the packer.
The generated side (not part of this package)
Everything below the Koffer is generated per domain by the Builder, details in the platform workflow (Get Started):
{Domain}Context— the generated, hermetic base every BC/aggregate facade of the domain extends. Carries the Kernel seamhandle()/context()(nowprotected, family-internal) plusresource()/payload()/version()/result().implements JardisSupport\Contract\Kernel\GeneratedContextInterface: noextends BoundedContext, no package base class.{Domain}\Response\— the generated response trio (ContextResponse,DomainResponse,DomainResponseTransformer), ported 1:1 from the formersrc/Response/*.ResponseStatuslives injardissupport/contracts.- The domain facade (e.g.
Ecommerce) isfinal, holds only the Koffer (DomainKernelInterface $kernel) and registers every aggregate event router via$kernel->eventListenerRegistry().
Anyone extending or wiring generated code no longer works with this package, but with the generated {Domain}Context.
HTTP delivery
The Koffer is pure runtime. It knows nothing about HTTP. The request/response layer around it (FastRoute router, PSR-15 pipeline, DomainResponse → PSR-7 envelope) is delivered by App (jardiscore/app).
Architecture
BuildDomainKernelFromEnv (optional ENV packer)
↓ packs
DomainKernel — the Koffer (immutable, constructor injection, 11 accessors)
↓ new {Domain}($kernel)
{Domain}Context (generated — platform-implementation)
handle()/context() (Kernel seam, protected) · resource()/payload()/version()/result()
↓ result()
ContextResponse (generated) → DomainResponseTransformer (generated) → DomainResponse (generated)Everything below the DomainKernel line is generated per domain by the Builder, not provided by this package.
Dependency direction:
DomainKernel → PSR interfaces (from jardissupport/contracts)
→ DotEnv, Factory, ClassVersion (from jardissupport/*)
Bootstrap\ → jardisadapter/* (only allowed in the Bootstrap namespace)The Koffer core (DomainKernel + the contract interfaces) stays adapter-free: only jardissupport/contracts + PSR. Adapter imports are legitimate exclusively in the Bootstrap\ sub-namespace (application wiring, not domain code).