Search by

jardiscore / kernel

jardis

Infrastructure kernel for PHP — an immutable DomainKernel holding wired-up cache, logger, event dispatcher, HTTP client, database, mailer and filesystem, plus an optional ENV-driven bootstrap. Jardis-generated domain code reaches it through a single interface.

v2.4.1 2026-08-30 17:55 UTC

This package is auto-updated.

Last update: 2026-09-01 02:38:07 UTC


README

Build Status Latest Version License: MIT PHP Version PHPStan Level PSR-12 Coverage PSR-11 PSR-14 PSR-16 PSR-18

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) plus an optional ENV-driven packer (BuildDomainKernelFromEnv) — you inject the kernel into the generated domain facade's constructor, nothing more.

Why Jardis Kernel?

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.

  • DomainKernelInterface in, done. Every generated {Domain}Context and BC facade takes the DomainKernel 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 DomainKernel without a logger is a perfectly valid DomainKernel; the domain checks and acts accordingly.
  • ClassVersion built in. Versioned classes via namespace injection is a Support-package concern (jardissupport/classversion); the DomainKernel 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. BuildDomainKernelFromEnv packs a DomainKernel from cascading .env files if you want that; build your own DomainKernel directly if you don't.

Installation

composer require jardiscore/kernel

Quickstart

1. Build a DomainKernel

The simplest DomainKernel — no services at all:

use JardisCore\Kernel\DomainKernel;

$kernel = new DomainKernel(projectRoot: __DIR__);

A DomainKernel with a database connection — plain PDO is enough:

$kernel = new DomainKernel(
    projectRoot: __DIR__,
    connection: new PDO('mysql:host=localhost;dbname=shop', 'root', ''),
);

2. Hand it to the generated domain facade

Jardis-generated domains take the DomainKernel 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 ContextResponseDomainResponse 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 DomainKernel these generated classes consume.

3. Or: pack the DomainKernel from ENV

For projects that want zero manual service wiring, BuildDomainKernelFromEnv assembles a DomainKernel from one .env in the project root (template: docs/.env.example). It takes the project root (the git-clone target) — there is no config/ layer: every configuration value of a Jardis project lives exactly once, in that one file.

use JardisCore\Kernel\Bootstrap\BuildDomainKernelFromEnv;

$packer = new BuildDomainKernelFromEnv();
$kernel = $packer(__DIR__);   // reads <projectRoot>/.env (+ cascade)

$ecommerce = new Ecommerce($kernel);

The cascade is DotEnv's usual one: .env -> .env.local -> .env.{APP_ENV}, plus any load()/load?() include you write yourself. A project root without a .env is simply "nothing configured yet" — every service degrades to null, nothing throws, and no directory is created.

The process environment always wins. Since jardissupport/dotenv 1.4.0 a key already set in the process environment beats the file value (12-factor III), so the same image runs with a .env on a developer machine and with environment: entries in production without a code change.

No file at all? Hand the packer the configuration as a string:

$kernel = $packer(__DIR__, $secretsManagerPayload);   // .env-formatted string
$kernel = $packer(__DIR__, '');                       // everything from the environment

The string mode is exclusive against the file: <projectRoot>/.env is not read, not even per key — the empty string is a valid input meaning "the process environment is the whole configuration". The project root still matters in string mode: the support/secret.key fallback resolves against it. KEY_FILE= values are read from disk only when the path is absolute (jardissupport/dotenv >= 1.6); a relative _FILE value stays a plain string — COMPOSE_FILE, NGINX_INDEX_FILE and the like are names, not secret mounts.

Encrypted values (secret(...))

The encryption key is looked up in exactly two places, in this order:

  1. APP_SECRET_KEY in the process environment — the master key belongs there and never in a .env file. A key written into the file would have to be read by the very load it is supposed to unlock, and it would sit in the kernel's env() array in plaintext.
  2. <projectRoot>/support/secret.key — the file fallback, for setups that mount a key rather than export it.

With neither, a secret(...) value cannot be resolved. It is then not passed on as a cipher: the packer throws JardisCore\Kernel\Exception\InvalidEnvConfigurationException naming the ENV key (never the value) before any adapter is built. The key source is independent of the value source — a key file applies to string input too.

BuildDomainKernelFromEnv wires nine services (cache, logger, event dispatcher + listener registry, HTTP client, DB connection, mailer, filesystem, messaging) from DB_* / CACHE_* / LOG_* / HTTP_* / MAIL_* / REDIS_* / MESSAGING_* / MESSAGING_DB_* / KAFKA_* / RABBITMQ_* ENV keys — see docs/.env.example for the full key reference, grouped into the eight blocks (app, database, redis, cache, logger, http, mail, messaging) the tooling reads. The resulting packed DomainKernel exposes twelve accessors in total (the nine services above, plus projectRoot(), env(), and container(), which are not ENV-wired services — see the accessor table below). projectRoot() returns the project root passed to the packer, which is also the directory its .env is read from. Every adapter it can use (jardisadapter/cache, jardisadapter/dbconnection, jardisadapter/eventdispatcher, jardisadapter/filesystem, jardisadapter/http, jardisadapter/logger, jardisadapter/mailer, jardisadapter/messaging) is a composer suggest — not installed, or not configured, means that accessor stays null on the packed DomainKernel. Nothing throws for a missing optional service.

DomainKernel — the DomainKernel

$kernel = new DomainKernel(
    projectRoot: '/path/to/project',    // 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, file values only
    messaging: $messagingService,       // ?MessagingServiceInterface
);
Method Return
projectRoot() string — root of the project the kernel serves; multiple domains in one project share it
env(string $key) mixed — case-insensitive; private ENV only, no global fallback
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
messaging() ?MessagingServiceInterfaceMESSAGING_TRANSPORT=kafka | rabbitmq | redis | database; database is broker-less and reuses the writer PDO from DB_*

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 DomainKernel, event routing simply stays inactive — no error.

Multi-Domain Service Sharing (explicit)

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__);   // project root, reads <projectRoot>/.env

$ecommerce = new Ecommerce($kernel);   // same DomainKernel instance
$billing   = new Billing($kernel);     // same DomainKernel instance -> same connection, cache, ...

A domain that needs its own services builds its own DomainKernel from its own project root instead of sharing one — one stack, one technical environment per DomainKernel (ein-stack-eine-technische-umgebung); two databases means two stacks, not two config directories inside one:

$billingKernel = (new BuildDomainKernelFromEnv())('/path/to/billing-project');
$billing = new Billing($billingKernel);

Advanced: ConnectionPool (optional)

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.example, block database):

use JardisAdapter\DbConnection\ConnectionPool;
use JardisAdapter\DbConnection\Factory\ConnectionFactory;

$factory = new ConnectionFactory();

$kernel = new DomainKernel(
    projectRoot: __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.

When BuildDomainKernelFromEnv builds the pool, five optional DB_POOL_* keys tune its ConnectionPoolConfig (see docs/.env.example, block database): DB_POOL_VALIDATE_CONNECTIONS, DB_POOL_HEALTH_CHECK_CACHE_TTL, DB_POOL_HEALTH_CHECK_NEGATIVE_CACHE_TTL, DB_POOL_LOAD_BALANCING_STRATEGY (round-robin | random) and DB_POOL_STICKY_WRITER (stickyWriterDuringTransaction: reads inside an open writer transaction go to the writer). Setting none of them keeps the adapter defaults — the pool is built exactly as before; setting any builds an explicit config where only the set keys deviate. DB_POOL_STICKY_WRITER requires jardisadapter/dbconnection >= 1.1.0 — with an older version installed the pool build fails and falls back to a plain PDO attempt on DB_HOST, logged via error_log. An invalid DB_POOL_LOAD_BALANCING_STRATEGY, or that plain PDO fallback also failing, now throws InvalidEnvConfigurationException instead of degrading to null (R1 — see "Error Handling" below).

Error Handling (ENV Bootstrap)

BuildDomainKernelFromEnv and its Handlers follow four rules for every ENV key they read:

State Result
Key not set, or set to an empty value (KEY=) null — the service degrades gracefully
Key set to an unparsable/invalid value (e.g. HTTP_VERIFY_SSL=maybe, an unknown DB_POOL_LOAD_BALANCING_STRATEGY) throws JardisCore\Kernel\Exception\InvalidEnvConfigurationException
Key set, service configured, but unreachable (DB/Redis connection fails) throws InvalidEnvConfigurationException
Key holds an unresolvable secret(...) value (no APP_SECRET_KEY, no support/secret.key) throws InvalidEnvConfigurationException naming the KEY, never the value — before any adapter is built
An ENV key the packer does not recognize ignored

Boolean ENV keys (HTTP_VERIFY_SSL, DB_POOL_VALIDATE_CONNECTIONS, DB_POOL_STICKY_WRITER) go through one shared unit, Bootstrap\Handler\NormalizeEnvBool — DotEnv casts a literal true/false to bool and a bare 1/0 to int, never a raw string, so a $value === 'true' comparison silently misreads both (the bug NormalizeEnvBool replaces). Credential-shaped keys (*_PASSWORD, *_USER, *_SECRET, *_TOKEN) are registered as DotEnv raw keys before loading — they reach their handler as the literal string instead of a cast bool/int (DB_PASSWORD=false stays 'false', not bool(false)).

The one thing that is not a degradation is an unresolved secret(...) value. Passing a cipher on as if it were a password, a host or a token fails later, elsewhere, and unintelligibly — so the packer stops at boot, names the ENV key and points at the two key sources (APP_SECRET_KEY in the process environment, <projectRoot>/support/secret.key). The exception message never contains the value.

Architecture

BuildDomainKernelFromEnv        Bootstrap-Packer (optional). ENV -> DomainKernel.
    ├── 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
    ├── Handler\BuildMessagingFromEnv               kafka | rabbitmq | redis | database (R4)
    ├── Handler\NormalizeEnvBool                   shared ENV-to-bool unit (R1)
    ├── Handler\IsEnvValueUnset                    shared "is this key configured" check (R1)
    ├── Data\CredentialEnvKeySuffixes               *_PASSWORD | *_USER | *_SECRET | *_TOKEN (R1)
    ├── 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.
    ├── projectRoot()           Root of the project this kernel serves.
    ├── env(key)                Case-insensitive. File-only, no global fallback.
    ├── 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
    └── messaging()             ?MessagingServiceInterface

Everything downstream of the DomainKernel — the generated {Domain}Context Naht (handle()/context()), resource()/payload()/version()/result(), and the ContextResponseDomainResponseTransformerDomainResponse 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.

Constitutional Note (Kernel-Entkopplung D4)

As of the Kernel-Entkopplung redesign, jardiscore/kernel sits outside the hexagonal inner rings — it is Application-layer, not Domain-layer. Concretely:

  • The DomainKernel core (DomainKernel + the contract interfaces it implements) stays adapter-free: only jardissupport/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 see DomainKernelInterface.

This mirrors the project-wide rule ("Composition over Inheritance", flat extends only inside generated code) applied one layer up: the Application composes the DomainKernel from adapters; the Domain composes its behaviour from the DomainKernel.

Related Packages

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 (^1.5) Cascading .env loading — used by BuildDomainKernelFromEnv; ^1.5 for addRawKeys() (credential cast exemption), process-environment precedence (>= 1.4.0) and loadPrivateFromString()

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

Documentation

Full documentation, guides, and API reference:

docs.jardis.io/en/core/kernel

ENV key reference: docs/.env.example — the one configuration template, in eight blocks.

License

Jardis is open source under the MIT License. Free for any purpose — commercial or non-commercial.

Jardis — Development with Passion Built by Headgent Development

AI-Assisted 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-skills

More details: https://docs.jardis.io/en/skills