jardiscore / kernel
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.
Package info
pkg:composer/jardiscore/kernel
Requires
- php: >=8.2
- ext-pdo: *
- jardissupport/classversion: ^1.0
- jardissupport/contracts: ^2.0
- jardissupport/dotenv: ^1.3
- jardissupport/factory: ^1.0
- psr/container: ^2.0
- psr/event-dispatcher: ^1.0
- psr/http-client: ^1.0
- psr/log: ^3.0
- psr/simple-cache: ^3.0
Requires (Dev)
- ext-amqp: *
- ext-rdkafka: *
- ext-redis: *
- jardis/dev-skills: ^1.0
- jardisadapter/cache: ^1.0
- jardisadapter/dbconnection: ^1.1
- jardisadapter/eventdispatcher: ^1.0
- jardisadapter/filesystem: ^1.0
- jardisadapter/http: ^1.0
- jardisadapter/logger: ^1.0
- jardisadapter/mailer: ^1.0
- jardisadapter/messaging: ^1.0
- jardissupport/secret: ^1.0
- phpstan/phpstan: 2.1.56
- phpunit/phpunit: 11.5.55
- squizlabs/php_codesniffer: 3.13.6
Suggests
- ext-amqp: For RabbitMQ messaging via the Bootstrap-Packer
- ext-rdkafka: For Kafka messaging via the Bootstrap-Packer
- ext-redis: For Redis-based caching, logging and messaging via the Bootstrap-Packer
- jardisadapter/cache: For multi-layer caching (Memory, Redis, Database) via the Bootstrap-Packer
- jardisadapter/dbconnection: For ConnectionPool with read/write splitting, health checks and load balancing
- jardisadapter/eventdispatcher: For PSR-14 event dispatching via the Bootstrap-Packer
- jardisadapter/filesystem: For local and S3 filesystem abstraction via the Bootstrap-Packer
- jardisadapter/http: For PSR-18 HTTP client with handler pipeline via the Bootstrap-Packer
- jardisadapter/logger: For log handlers (File, Console, Redis, Database, etc.) via the Bootstrap-Packer
- jardisadapter/mailer: For SMTP mailer with STARTTLS, AUTH, HTML/text, attachments via the Bootstrap-Packer
- jardisadapter/messaging: For Kafka/RabbitMQ/Redis messaging via the Bootstrap-Packer
- jardissupport/secret: For decrypting secret(...) ENV values via the Bootstrap-Packer when <projectRoot>/support/secret.key exists
This package is auto-updated.
Last update: 2026-08-23 16:54:20 UTC
README
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.
DomainKernelInterfacein, done. Every generated{Domain}Contextand 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.
BuildDomainKernelFromEnvpacks a DomainKernel from cascading.envfiles if you want that; build your ownDomainKerneldirectly 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 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 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 a cascading .env tree (templates:
docs/env-examples/). It takes the project root
(the git-clone target), not a config path — the convention is a fixed
config/env subdirectory, one project layout every Jardis project shares
(see the projekt-layout-konvention Wissensbasis entry):
use JardisCore\Kernel\Bootstrap\BuildDomainKernelFromEnv; $packer = new BuildDomainKernelFromEnv(); $kernel = $packer(__DIR__); // reads <projectRoot>/config/env (+ cascade) $ecommerce = new Ecommerce($kernel);
If <projectRoot>/config/env does not exist yet, the packer creates it
(mkdir, race-safe against a parallel fpm cold start) rather than throwing —
an empty or freshly created directory just means "nothing configured yet",
same as any other missing ENV key. The packer only throws when creating the
directory itself fails (permissions, read-only filesystem).
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_* / KAFKA_* / RABBITMQ_* ENV keys — see
docs/env-examples/README.md for the full key
reference. 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, not
the internal config/env path it reads 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() |
?MessagingServiceInterface |
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 config/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-examples/.env.database.example):
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-examples/.env.database.example):
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 |
| 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)).
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 (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 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.
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: 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 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.2) |
Cascading .env loading — used by BuildDomainKernelFromEnv; ^1.2 for addRawKeys() (credential cast exemption) |
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:
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