Part of Jardis — the Domain-Driven Design platform for PHP. You model your domain; Jardis generates the production-ready hexagonal code (DTOs, Command/Query handlers, repositories, persistence). This package is the runtime the generated code runs on.
The Application-side offer for Jardis-generated domains. One immutable
infrastructure holder (DomainKernel, the "Koffer") plus an optional
ENV-driven packer (BuildDomainKernelFromEnv) — you inject the kernel into
the generated domain facade's constructor, nothing more.
Generated Jardis domains are hexagonal all the way down: they depend only on
jardissupport/contracts interfaces, never on a concrete implementation. This
package is one way to satisfy those interfaces at runtime — a minimal,
adapter-agnostic infrastructure bundle.
DomainKernelInterfacein, done. Every generated{Domain}Contextand BC facade takes the Koffer via constructor injection — nothing else to wire.- Plain PDO works. Pass a PDO, get going. Need connection pooling later?
Swap in a ConnectionPool. Same
dbConnection()accessor. - Every service is optional. Nullable by design — a Koffer without a logger is a perfectly valid Koffer; the domain checks and acts accordingly.
- ClassVersion built in. Versioned classes via namespace injection is a
Support-package concern (
jardissupport/classversion); the Koffer just carries a container that resolves it. - Immutable kernel. Once built, nothing changes. Safe for application servers, workers, long-running processes.
- ENV wiring is optional, not assumed.
BuildDomainKernelFromEnvpacks a Koffer from cascading.envfiles if you want that; build your ownDomainKerneldirectly if you don't.
composer require jardiscore/kernelThe simplest Koffer — no services at all:
use JardisCore\Kernel\DomainKernel;
$kernel = new DomainKernel(domainRoot: __DIR__);A Koffer with a database connection — plain PDO is enough:
$kernel = new DomainKernel(
domainRoot: __DIR__,
connection: new PDO('mysql:host=localhost;dbname=shop', 'root', ''),
);Jardis-generated domains take the Koffer via constructor injection — nothing
extends DomainApp anymore (Kernel-Entkopplung, see "Constitutional Note"
below):
$ecommerce = new Ecommerce($kernel); // {Domain} facade, generated by Jardis
$response = $ecommerce->order()->placeOrder(['customer' => 'Acme', 'total' => 99.90]);
$response->isSuccess(); // true
$response->getData(); // ['PlaceOrder' => ['orderId' => 42]]
$response->getEvents(); // ['PlaceOrder' => [OrderPlaced {...}]]The generated {Domain}Context base class (the Naht, handle()/context())
and the ContextResponse → DomainResponse response pipeline are themselves
part of what Jardis generates per domain — see the platform-implementation
skill / docs.jardis.io for the generated-code contract. This package only
provides the Koffer these generated classes consume.
For projects that want zero manual service wiring, BuildDomainKernelFromEnv
assembles a Koffer from a cascading .env tree (templates:
docs/env-examples/):
use JardisCore\Kernel\Bootstrap\BuildDomainKernelFromEnv;
$packer = new BuildDomainKernelFromEnv();
$kernel = $packer(__DIR__ . '/config'); // reads config/.env (+ cascade)
$ecommerce = new Ecommerce($kernel);BuildDomainKernelFromEnv wires eight services (cache, logger, event
dispatcher + listener registry, HTTP client, DB connection, mailer,
filesystem) from DB_* / CACHE_* / LOG_* / HTTP_* / MAIL_* /
REDIS_* ENV keys — see docs/env-examples/README.md
for the full key reference. The resulting packed DomainKernel exposes
eleven accessors in total (the eight services above, plus domainRoot(),
env(), and container(), which are not ENV-wired services — see the
accessor table below). Every adapter it can use (jardisadapter/cache,
jardisadapter/dbconnection, jardisadapter/eventdispatcher,
jardisadapter/filesystem, jardisadapter/http, jardisadapter/logger,
jardisadapter/mailer) is a composer suggest — not installed, or not
configured, means that accessor stays null on the packed Koffer. Nothing
throws for a missing optional service.
$kernel = new DomainKernel(
domainRoot: '/path/to/config', // required
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
);| Method | Return |
|---|---|
domainRoot() |
string |
env(string $key) |
mixed — case-insensitive; private ENV > $_ENV |
container() |
Factory — always wraps the injected container |
cache() |
?CacheInterface |
logger() |
?LoggerInterface |
eventDispatcher() |
?EventDispatcherInterface |
eventListenerRegistry() |
?EventListenerRegistryInterface — paired with eventDispatcher(); same underlying provider instance (D3) |
httpClient() |
?ClientInterface |
dbConnection() |
ConnectionPoolInterface|PDO|null |
mailer() |
?MailerInterface |
filesystem() |
?FilesystemServiceInterface |
DomainKernel builds nothing and reads no ENV itself — it is a pure,
immutable consumer. All ENV/service-assembly is Bootstrap\BuildDomainKernelFromEnv's
job (or your own equivalent).
eventListenerRegistry() exists so a generated {Agg}EventRouter can
register itself on the domain facade's constructor without any Application
wiring: a fresh build carries new routers automatically. Without a registry in
the Koffer, event routing simply stays inactive — no error.
There is no static registry anymore (Kernel-Entkopplung removed the
first-write-wins ServiceRegistry, G11) — sharing services across domains is
now an explicit choice, not implicit global state:
$kernel = (new BuildDomainKernelFromEnv())(__DIR__ . '/config');
$ecommerce = new Ecommerce($kernel); // same Koffer instance
$billing = new Billing($kernel); // same Koffer instance -> same connection, cache, ...A domain that needs its own services builds its own Koffer instead of sharing one:
$billingKernel = (new BuildDomainKernelFromEnv())(__DIR__ . '/config-billing');
$billing = new Billing($billingKernel);For application servers and read replicas, install jardisadapter/dbconnection
and pass a ConnectionPool instead of plain PDO — either directly, or let
BuildDomainKernelFromEnv build one from DB_READER{N}_HOST ENV keys (see
docs/env-examples/.env.database.example):
use JardisAdapter\DbConnection\ConnectionPool;
use JardisAdapter\DbConnection\Factory\ConnectionFactory;
$factory = new ConnectionFactory();
$kernel = new DomainKernel(
domainRoot: __DIR__,
connection: new ConnectionPool(
writer: $factory->mysql('primary', 'user', 'pass', 'shop'),
readers: [
$factory->mysql('replica1', 'user', 'pass', 'shop'),
$factory->mysql('replica2', 'user', 'pass', 'shop'),
],
),
);ConnectionPool provides lifecycle management, health checks, round-robin
load balancing, and automatic writer fallback when no readers are available.
Everything downstream ($kernel->dbConnection()) doesn't change.
BuildDomainKernelFromEnv Bootstrap-Packer (optional). ENV -> Koffer.
├── Handler\BuildConnectionFromEnv mysql | pgsql | sqlite (+ pool)
├── Handler\BuildRedisFromEnv shared fan-out (-> cache + logger)
├── Handler\ExtractPdoFromConnection feeds the cache "db" layer
├── Handler\BuildCacheFromEnv memory | apcu | redis | db
├── Handler\BuildLoggerFromEnv file | console | slack | ... (+redis)
├── Handler\BuildEventListenerProviderFromEnv shared provider (D3)
├── Handler\BuildEventDispatcherFromProvider wraps the same provider
├── Handler\BuildHttpClientFromEnv
├── Handler\BuildMailerFromEnv
├── Handler\BuildFilesystemFromEnv
├── Data\CacheLayer memory | apcu | redis | db (4 cases)
└── Data\LogHandler file | console | errorlog | syslog |
browserconsole | redis | slack | teams |
loki | webhook | null (11 cases)
DomainKernel Immutable. Constructor injection only.
├── env(key) Case-insensitive. Private > $_ENV
├── container() Always Factory. Wraps external container.
├── cache() ?CacheInterface
├── logger() ?LoggerInterface
├── eventDispatcher() ?EventDispatcherInterface
├── eventListenerRegistry() ?EventListenerRegistryInterface (paired with eventDispatcher, D3)
├── httpClient() ?ClientInterface
├── dbConnection() ConnectionPoolInterface | PDO | null
├── mailer() ?MailerInterface
└── filesystem() ?FilesystemServiceInterface
Everything downstream of the Koffer — the generated {Domain}Context Naht
(handle()/context()), resource()/payload()/version()/result(), and
the ContextResponse → DomainResponseTransformer → DomainResponse
pipeline — is generated per domain by Jardis itself (Kernel-Entkopplung: the
generated domain is JardisCore-free; it imports only
jardissupport/contracts). See the platform-implementation skill for that
generated-code contract.
As of the Kernel-Entkopplung redesign, jardiscore/kernel sits outside the hexagonal
inner rings — it is Application-layer, not Domain-layer. Concretely:
- The Koffer core (
DomainKernel+ the contract interfaces it implements) stays adapter-free: onlyjardissupport/contracts+ PSR interfaces. - The
Bootstrap\sub-namespace legitimately imports concrete adapter packages (jardisadapter/*) — that is Application wiring, not Domain code, and Application code is allowed to depend on concrete infrastructure. - Generated Jardis domains never import anything under
Bootstrap\— they only ever seeDomainKernelInterface.
This mirrors the project-wide rule ("Composition over Inheritance", flat extends only inside generated code) applied one layer up: the Application composes the Koffer from adapters; the Domain composes its behaviour from the Koffer.
Included dependencies:
| Package | Purpose |
|---|---|
jardissupport/contracts |
Interface contracts (DomainKernelInterface, EventListenerRegistryInterface, etc.) |
jardissupport/classversion |
Versioned class resolution via namespace injection |
jardissupport/factory |
PSR-11 Container + class instantiation |
jardissupport/dotenv |
Cascading .env loading — used by BuildDomainKernelFromEnv |
Optional (composer suggest, used by Bootstrap\BuildDomainKernelFromEnv):
| Package | Purpose |
|---|---|
jardisadapter/cache |
Multi-layer caching (Memory, APCu, Redis, Database) |
jardisadapter/dbconnection |
ConnectionPool with read/write splitting, health checks, load balancing |
jardisadapter/eventdispatcher |
PSR-14 event dispatching + listener registry |
jardisadapter/filesystem |
Local and S3 filesystem abstraction |
jardisadapter/http |
PSR-18 HTTP client with handler pipeline |
jardisadapter/logger |
PSR-3 logger with file/console/network/queue handlers |
jardisadapter/mailer |
SMTP mailer with STARTTLS, AUTH, HTML/text, attachments |
Full documentation, guides, and API reference:
Jardis is open source under the MIT License. Free for any purpose — commercial or non-commercial.
Jardis — Development with Passion Built by Headgent Development
This package ships with a skill for Claude Code, Cursor, Continue, and Aider. Install it in your consuming project:
composer require --dev jardis/dev-skillsMore details: https://docs.jardis.io/en/skills