jardissupport / contracts
Consolidated contract interfaces for all Jardis packages — Kernel, Data, Repository, DbQuery, Messaging, Validation, Workflow, and more
Requires
- php: >=8.2
- ext-pdo: *
- 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)
- phpstan/phpstan: 2.2.3
- phpunit/phpunit: 10.5.63
- squizlabs/php_codesniffer: 3.13.6
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v2.11.0
- v2.10.0
- v2.9.1
- v2.9.0
- v2.8.0
- v2.7.0
- v2.6.0
- v2.5.0
- v2.4.0
- v2.3.1
- v2.3.0
- v2.2.0
- v2.1.0
- v2.0.5
- v2.0.4
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.1.1
- v1.1.0
- v1.0.2
- v1.0.1
- v1.0.0
- dev-develop
- dev-feature/261015_auth-service-contracts
- dev-feature/261014_identity-contracts
- dev-fix/261014_ship-claude-skills
This package is auto-updated.
Last update: 2026-10-15 06:32:38 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 part of the open-source foundation that generated code runs on.
Consolidated ports and adapters contracts for all Jardis packages — the interface layer that keeps hexagonal architecture honest across the entire ecosystem.
Overview
This package provides all interface contracts for the Jardis ecosystem in a single location. It replaces the previously separate jardisport/* packages.
| Namespace | Contracts | Purpose |
|---|---|---|
Auth |
23 | UserLookup, CredentialRecord, SessionClaim (login lookup + session claim names), AuthResult, TenantAwareAuthResult, SessionAwareAuthResult, RequestAttribute (subject/subject-id/tenant/session attribute names), Authenticator, Credential(+Type), Guard, HashedToken, PasswordHasher, Session, TokenStore(+Type); since 2.11: AuthServices, SessionManager, TokenIssuer, TokenRedeemer, SessionResult, IssuedToken, RoleAwareAuthResult |
ClassVersion |
2 | Versioned class resolution |
Connection |
1 | Generic connection abstraction |
Data |
3 | Hydration, Identity, FieldMapper |
DbConnection |
4 | ConnectionPool, DbConnection, DatabaseConfig, Savepoint (additive: nested transaction brackets) |
DbQuery |
17 | Query Builder, Conditions, Joins, Expressions, Results |
DotEnv |
1 | Environment variable loading |
EventListener |
1 | EventListenerRegistry (paired with PSR-14 EventDispatcher) |
Filesystem |
6 | Filesystem (Reader/Writer/Service), FileInfo + Exception |
Identity |
12 | Deprecated since 2.11 — User/Tenant/Membership/Group/Invitation entity contracts, UserStatus (enum), and the six store ports (User, Tenant, Membership, Group, GroupMember, Invitation) |
Kernel |
6 | DomainKernel, ContextResponse, DomainResponse, EventScope, ResponseStatus, GeneratedContextInterface (marker) |
Mailer |
4 | Mailer, MailMessage, MailTransport + Exception |
Messaging |
10 | Publisher, Consumer, MessageHandler + Exceptions |
Repository |
6 | Generic CRUD, PkStrategy, Exceptions (Persist, RecordNotFound, UniqueViolation, TransientConflict) |
Scheduling |
5 | Constraint, CronExpression, Schedule(+Violation), ScheduledTask |
Secret |
2 | Secret resolution + Exception |
Validation |
4 | Validator, ValueValidator, MissingValueValidator (marker), ValidationResult (errors + kinds tree: missing|invalid; toFields() flat findings) |
Workflow |
8 | Workflow engine + orchestration (Workflow, Builder, NodeBuilder, Config, Context, Result, Chain, AggregateResponse) — 7 named transitions: onSuccess, onFail, onTimeout, onSkip, onCancel, onEvent, onExit |
115 contracts across 18 domains.
Installation
composer require jardissupport/contracts
Namespace
JardisSupport\Contract\
├── Auth\
├── ClassVersion\
├── Connection\
├── Data\
├── DbConnection\
├── DbQuery\
├── DotEnv\
├── EventListener\
├── Filesystem\
├── Identity\
├── Kernel\
├── Mailer\
├── Messaging\
│ └── Exception\
├── Repository\
│ ├── Exception\
│ └── PrimaryKey\
├── Scheduling\
├── Secret\
├── Validation\
└── Workflow\
Kernel — v2 additions (Kernel-Entkopplung)
Three additions to Kernel\ that let a generated domain drop its compile-time
dependency on jardiscore/kernel's base classes while keeping the same
runtime vocabulary.
ResponseStatus enum
JardisSupport\Contract\Kernel\ResponseStatus — an int-backed enum of
domain-neutral response status codes (Success = 200, Created = 201,
NoContent = 204, ValidationError = 400, Unauthorized = 401,
Forbidden = 403, NotFound = 404, MethodNotAllowed = 405, Conflict = 409,
RuleViolation = 422, InternalError = 500), ported 1:1 from jardiscore/kernel's
JardisCore\Kernel\Response\ResponseStatus. Generated domains import it from
here instead of from jardiscore/kernel.
DomainKernelInterface::eventListenerRegistry()
The Kernel interface gained an 11th accessor:
eventListenerRegistry(): ?EventListenerRegistryInterface. It is paired with
eventDispatcher() — both are typically backed by the same underlying
provider instance, one side for dispatching (PSR-14), one side for
registering listeners. Generated {Agg}EventRouter scaffolds use this
accessor to self-register on the domain facade's constructor. Nullable like
every other service on the interface: without a registry, event routing
simply stays inactive.
GeneratedContextInterface marker
JardisSupport\Contract\Kernel\GeneratedContextInterface is an empty marker
interface. Every generated {Domain}Context implements it, so the resolve
path recognizes generated context classes via
is_subclass_of($resolved, GeneratedContextInterface::class) — independent
of domain boundaries (cross-domain services included) and without requiring
a shared package base class to extend.
It took over the detection role of BoundedContextInterface, which was
removed from this package in the Kernel-Entkopplung (2026-07) together with
the jardiscore/kernel BoundedContext base class it referred to. Generated
contexts implement this marker instead of extending a package class.
Kernel — v2.0.0 (env-konfiguration)
Two changes to DomainKernelInterface, released together as v2.0.0:
domainRoot()→projectRoot()(BREAKING). The accessor is renamed and now names what it always carried in practice: the project root (the git-clone target), from which the<projectRoot>/config/envconvention derives — not a config path.messaging(): ?MessagingServiceInterface(new, twelfth accessor). Publish/consume across the transports ofjardisadapter/messaging(Kafka, RabbitMQ, Redis). Nullable like every other service accessor: without a configured transport, messaging simply stays inactive.
Repository exceptions — UniqueViolationException (v2.5.0)
Repository\Exception\UniqueViolationException (subtype of PersistException) tells the caller which unique
constraint failed:
| Getter | Type | Meaning |
|---|---|---|
getConstraint() |
?string |
Constraint/index name, null if the driver does not expose it |
getColumns() |
list<string> |
Violated columns. [] = unknown (driver/dialect delivers no columns) — never a partial list |
getTable() |
?string |
Table of the violated constraint, null if unknown |
The constructor is (message, constraint, previous, columns = [], table = null) — the former three-argument call is
unchanged (additive, minor).
try { $repository->insert($row); } catch (UniqueViolationException $e) { $columns = $e->getColumns(); // e.g. ['email'] — [] when unknown $table = $e->getTable(); // e.g. 'users' — null when unknown }
TransientConflictException (v2.6.0)
Repository\Exception\TransientConflictException (subtype of PersistException) marks a transient concurrency
conflict of the database — snapshot/serialization conflict or deadlock. The statement was rejected, not wrong:
repeating the whole unit of work in a new transaction can succeed. Without this type a caller cannot tell such a
conflict from a technical failure.
| Member | Type | Meaning |
|---|---|---|
getDriverCode() |
?string |
Driver error code / SQLSTATE as text, null if the driver does not expose it. Examples (not exhaustive): '1020' (MariaDB "record has changed since last read"), '1213' (deadlock), '40001' (serialization failure), '40P01' (PostgreSQL deadlock) |
getPrevious() |
?Throwable |
The original driver exception |
The constructor is (message = '', driverCode = null, previous = null). Purely additive (minor): every existing
catch (PersistException) still catches it — put the more specific catch first.
try { $repository->update($row); } catch (TransientConflictException $e) { $code = $e->getDriverCode(); // e.g. '1020' — null when unknown // retry the unit of work in a new transaction, or answer "conflict" } catch (PersistException $e) { // technical failure }
Validation — ValidationResult::toFields() (v2.7.0)
ValidationResult flattens its error tree into a list of findings, so a caller does not re-implement the walk over
getErrors() + getKinds():
$fields = $result->toFields(); // list<array{field: string, reason: string, message: string}> $fields = $result->toFields('order'); // top-level segment "order" carries no path prefix
| Member | Meaning |
|---|---|
field |
Dot path of the string keys down to the message. List indices (int keys) add no segment of their own |
reason |
ValidationResult::REASON_MISSING ('missing') exactly when the validator kind is KIND_MISSING, otherwise REASON_INVALID ('invalid'). A finding without a kind is invalid; there is no text fallback |
message |
The validator text as delivered — may be '' |
$root |
A top-level segment equal to $root is stripped from the path (aggregate root); other top-level segments stay in it. null strips nothing |
// errors: ['order' => ['email' => ['Required'], 'line' => ['qty' => ['Too small'], 0 => 'x']]] // kinds: ['order' => ['email' => ['missing'], 'line' => ['qty' => ['invalid'], 0 => 'invalid']]] $result->toFields(); // [['field' => 'order.email', 'reason' => 'missing', 'message' => 'Required'], // ['field' => 'order.line.qty', 'reason' => 'invalid', 'message' => 'Too small'], // ['field' => 'order.line', 'reason' => 'invalid', 'message' => 'x']]
Answer texts and message keys stay with the caller: the package delivers path, reason and validator text only. A
caller that needs a default text for an empty message (or a messageKey per reason) maps reason itself.
Purely additive (minor).
Auth — token session: RequestAttribute and TenantAwareAuthResultInterface (v2.8.0)
Additive, no breaking change. A transport layer that resolves an access token to a session stores the result under two request attributes whose names are defined exactly once here:
use JardisSupport\Contract\Auth\RequestAttribute; use JardisSupport\Contract\Auth\TenantAwareAuthResultInterface; RequestAttribute::SUBJECT; // 'jardisSubject' - authenticated subject identifier RequestAttribute::TENANT; // 'jardisTenant' - tenant of the session (string|null) if ($result instanceof TenantAwareAuthResultInterface) { $tenant = $result->getTenant(); // ?string }
TenantAwareAuthResultInterface extends AuthResultInterface adds getTenant(): ?string.
AuthResultInterface itself is unchanged, so existing implementers stay valid. jardissupport/auth
v1.2.0 ships the implementation (TokenAuthenticator).
Auth — subject id and session reference: SessionAwareAuthResultInterface (v2.9.0)
Additive, no breaking change. A token session resolves to more than the qualified subject:
consumers no longer split user:42 themselves and can revoke exactly the session they were
authenticated with.
use JardisSupport\Contract\Auth\RequestAttribute; use JardisSupport\Contract\Auth\SessionAwareAuthResultInterface; RequestAttribute::SUBJECT_ID; // 'jardisSubjectId' - bare subject identifier ('42' from 'user:42') RequestAttribute::SESSION; // 'jardisSession' - session reference (hash of the access token) if ($result instanceof SessionAwareAuthResultInterface) { $id = $result->getSubjectId(); // ?string $reference = $result->getSessionReference(); // ?string }
SessionAwareAuthResultInterface extends TenantAwareAuthResultInterface adds getSubjectId(): ?string
and getSessionReference(): ?string; existing implementers stay valid. jardissupport/auth v1.3.0
ships the implementation (TokenAuthenticator, SessionManager::invalidateByReference()).
Auth — login lookup and session claims: UserLookupInterface, CredentialRecordInterface, SessionClaim (v2.10.0)
Additive, no breaking change. A password login resolves an identifier to credential data through a port, and the session claim names are defined once.
use JardisSupport\Contract\Auth\CredentialRecordInterface; use JardisSupport\Contract\Auth\SessionClaim; use JardisSupport\Contract\Auth\UserLookupInterface; SessionClaim::TENANT; // 'tenant' - tenant of the session SessionClaim::ROLE; // 'role' - role of the subject within the tenant /** @var UserLookupInterface $lookup */ $record = $lookup->lookup('anna@example.com'); // ?CredentialRecordInterface if ($record !== null) { $hash = $record->getPasswordHash(); // string $subject = $record->getSubject(); // 'user:42' (format type:id) $claims = $record->getClaims(); // array<string, mixed>, e.g. [SessionClaim::TENANT => 't1'] }
jardissupport/identity v1.0 and jardissupport/auth v1.4 ship the implementations.
Identity - users, tenants, memberships, groups, invitations (v2.10.0) — DEPRECATED since 2.11.0
Deprecated (2.11.0): Identity is a generic subdomain; it is replaced by the Builder blueprint "Access". All 12
Identity\*contracts carry@deprecated 2.11.0. Nothing is removed — removal is reserved for the next major.
Additive, new namespace JardisSupport\Contract\Identity. Entity contracts are read-only views;
the password hash is deliberately not part of UserInterface (only UserStoreInterface::passwordHashOf()
and CredentialRecordInterface expose it). Invitations hold the token hash only.
use JardisSupport\Contract\Identity\InvitationStoreInterface; use JardisSupport\Contract\Identity\UserStatus; use JardisSupport\Contract\Identity\UserStoreInterface; /** @var UserStoreInterface $users */ $users->updateStatus($id, UserStatus::Disabled); $hash = $users->passwordHashOf($id); // ?string /** @var InvitationStoreInterface $invitations */ $accepted = $invitations->markAccepted($invitationId, new DateTimeImmutable()); // false if already accepted or revoked
Ports: UserStoreInterface, TenantStoreInterface, MembershipStoreInterface, GroupStoreInterface,
GroupMemberStoreInterface, InvitationStoreInterface. jardissupport/identity v1.0 ships the implementation.
Auth services (2.11)
Additive. A generated process node reaches auth through DomainKernelInterface::auth(): ?AuthServicesInterface
(null if the app wires no auth) without knowing a concrete class. The contracts carry no data classes;
domain events of the session lifecycle stay internal to the implementing package. Subjects are qualified
strings type:id.
| Contract | Purpose |
|---|---|
AuthServicesInterface |
Four accessors: sessions(), tokenIssuer(), tokenRedeemer(), hasher() |
SessionManagerInterface |
create(string $subject, array $claims = []), refresh(string $refreshToken) (both return SessionResultInterface), invalidateByReference(string $sessionReference), invalidateAll(string $subject) |
SessionResultInterface |
getAccessToken(), getRefreshToken(), getSession(): SessionInterface |
TokenIssuerInterface |
issue(TokenType $type, string $subject, int $ttlSeconds, array $claims = []): IssuedTokenInterface |
IssuedTokenInterface |
getPlain() (hand out once), getHashed(): HashedTokenInterface |
TokenRedeemerInterface |
peek(string $plain, TokenType $expected) (find + validate, no revocation), redeem(...) (validate + revoke once) |
RoleAwareAuthResultInterface |
extends AuthResultInterface, adds getRole(): ?string (claim SessionClaim::ROLE) |
Also new: TokenType::Invitation ('invitation') and RequestAttribute::ROLE ('jardisRole', set only when the
access token carries the role claim). DomainKernelInterface::auth() is a new method on a published interface:
kernel implementors must add it.
use JardisSupport\Contract\Auth\TokenType; $auth = $kernel->auth(); // ?AuthServicesInterface $issued = $auth?->tokenIssuer()->issue(TokenType::Invitation, 'user:42', 86400); $token = $auth?->tokenRedeemer()->peek($issued->getPlain(), TokenType::Invitation); // no revocation
Design Principles
- One package, all contracts — single dependency for the entire Jardis ecosystem
- Only PSR standards at the Kernel level — PSR-3, PSR-11, PSR-14, PSR-16, PSR-18 + PDO
- No implementation code — interfaces, enums, value objects and exception classes only
- Hexagonal Architecture — contracts define ports, implementations live in adapter/support packages
Migration from jardisport/*
Replace namespace imports:
// Before use JardisPort\Kernel\DomainKernelInterface; use JardisPort\DbQuery\DbQueryBuilderInterface; // After use JardisSupport\Contract\Kernel\DomainKernelInterface; use JardisSupport\Contract\DbQuery\DbQueryBuilderInterface;
Replace composer dependency:
// Before "require": { "jardisport/kernel": "^1.0", "jardisport/dbquery": "^1.0" } // After "require": { "jardissupport/contracts": "^1.0" }
Related Packages
| Package | Role |
|---|---|
jardiscore/kernel |
DomainKernel (the DomainKernel) — implements the Kernel\* contracts, provides the 11 infrastructure accessors |
jardiscore/app |
HTTP-Delivery layer — FastRoute router behind its own interface, PSR-15 middleware pipeline, DomainResponse → PSR-7 mapper, DomainKernel bootstrap bridge |
Generated {Domain}Context |
Domain Layer (Builder output) — implements Kernel\GeneratedContextInterface, consumes DomainKernelInterface via resource() |
jardissupport/* |
Support package implementations |
jardisadapter/* |
Adapter implementations for external systems |
Documentation
Full documentation, guides, and API reference:
docs.jardis.io/en/support/contracts
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