Store-agnostic, reified, immutable typed data-mapper contracts for PHP, powered by xphp monomorphization (real generics with declaration-site variance).

Maintainers

Package info

github.com/xphp-kanon/core

Language:Makefile

pkg:composer/xphp-kanon/core

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-07-25 17:53 UTC

This package is not auto-updated.

Last update: 2026-07-26 17:48:12 UTC


README

Store-agnostic, reified, immutable typed data-mapper contracts for PHP, built on xphp monomorphization — real generics with declaration-site variance, compiled to concrete PHP classes.

This is the foundation of the Kanon data mapper: the contracts a store adapter implements. It owns no store dependency — only xphp-lang/collections for ImmutableList/OrderedCollection — and it is the ecosystem's first cross-package .xphp consumer.

What it is

The read/write contracts, the query value object, and the one shared behavior:

Type Description
ReadRepository<out T> The read contract: find(int|string): ?T, all(): OrderedCollection<T>, findBy(Criteria): OrderedCollection<T>, count(Criteria): int. Covariant.
WriteRepository<in T> The write contract: save(T): void, delete(T): void. Contravariant.
Repository<T> The full read+write contract — extends ReadRepository<T>, WriteRepository<T>. Invariant.
Hydrator<out T> Turns one raw record (array<string, mixed>) into one entity T. Covariant.
RecordMapping collect::<T>(iterable $records, Hydrator<T>): ImmutableList<T> — hydrate a stream of records into a typed immutable list.
Query\Criteria An immutable, store-agnostic query: equality filters + ordering + a limit/offset window.
Query\Order One ordering term (field, direction) of a Criteria.

A store adapter (a future xphp-kanon/sql built on Doctrine DBAL, later a document adapter) implements Repository/Hydrator and translates Criteria to its store; a consumer that needs only reads or only writes depends on the narrower ReadRepository/WriteRepository. Absence is ?T, failure is a native exception, and list results are immutable.

What it is NOT

  • Not the mapper itself — that is this core plus a store adapter. This package has no store.
  • Not an ORM — no identity map, no unit of work, no change tracking, no lazy associations. That tier is a separate, future package.
  • No functional layer — absence is ?T and failure is an exception, not Option/Result/Either.
  • Not a unified query languageCriteria is the common subset every store expresses the same way (equality/order/window); richer, store-specific queries belong to an adapter.

Variance

ReadRepository and Hydrator are covariant in the entity type — a ReadRepository<Book> is usable wherever a ReadRepository<Product> is expected (with Book extends Product):

// A ReadRepository<Book> flows into a position typed for the supertype Product,
// and its read methods are called through the widened binding.
function firstName(ReadRepository::<Product> $repo): ?string
{
    return $repo->all()->first()?->name;
}

List-valued reads return the OrderedCollection<T> interface (not a concrete list) precisely so this upcast stays sound at runtime — an interface's specializations carry the covariant edge that a concrete container return does not.

WriteRepository is the mirror image — contravariant (in T): a WriteRepository<Product> is usable wherever a WriteRepository<Book> is expected (a writer of the supertype accepts the subtype). Combining both, Repository<T> is invariant. See docs/caveats.md, docs/adr/0001-store-agnostic-read-contracts.md, and docs/adr/0006-write-tier.md.

Using it

The package ships .xphp templates; concrete classes are generated when you compile your own code (which supplies the concrete entity types) together with these contracts. Point your xphp.json at your installed xphp packages via include, so the compiler pulls in these contracts and collections. The vendor/*/* glob discovers every installed xphp package (this core and collections) automatically:

{
    "sources": ["src"],
    "target": "build",
    "cache": "cache",
    "include": ["vendor/*/*"]
}

Construction and static generics are turbofish-required — the type argument is explicit, not inferred from the call's arguments:

$books = RecordMapping::collect::<Article>($conn->fetchAllAssociative($sql), $articleHydrator);

See the worked example in examples/Demo.xphp. Locally:

make build        # compile the contract templates
make demo         # compile + run the example
make test/unit    # behaviour tests, including covariant-upcast assertions
make ci           # check + build + tests + PHPStan (level 9) + mutation (100% MSI) + demo

Caveats

Turbofish-required construction, read-only scope, Criteria as store-agnostic data, and the requirement that entity types be authored as .xphp in the compiled source set. See docs/caveats.md. Design decisions are recorded under docs/adr/.

License

MIT.