cordis-php / cordis
A strictly typed, YAML-composed plugin runtime for PHP 8.2+.
Requires
- php: ^8.2
- symfony/yaml: ^7.3 || ^8.0
Requires (Dev)
- laravel/pint: ^1.21
- pestphp/pest: ^3.8
- phpstan/phpstan: ^2.1
README
Cordis PHP is a strictly typed PHP 8.2+ runtime for building applications from reversible plugins and YAML composition. It takes the durable parts of Cordis:
- plugin instances own a scope and clean up in reverse order;
- services can appear, disappear, and be replaced while consumers restart;
- a loader reconciles a YAML entry tree instead of treating configuration as a one-time bootstrap script;
- patches compose a base configuration with deployment or user layers; and
- expressions are structured data, never executable PHP.
It is deliberately synchronous. PHP processes usually own the event loop at a higher layer, so a plugin's lifecycle is deterministic and can be embedded in CLI, worker, web, or framework hosts without imposing an async runtime.
Install
composer require cordis-php/cordis
Minimal composition
<?php declare(strict_types=1); use CordisPhp\Plugin\PluginRegistry; use CordisPhp\Runtime\Runtime; $plugins = new PluginRegistry(); $plugins->registerClass('clock', App\ClockPlugin::class); $runtime = new Runtime($plugins); $loader = $runtime->yaml(__DIR__ . '/app.yml'); $loader->reload();
# app.yml - id: clock name: clock config: timezone: UTC
Plugins implement CordisPhp\Contract\Plugin. They receive a child
Context, can provide services, subscribe to events, and return an optional
cleanup closure. Every registration is automatically tied to the plugin's
scope.
YAML grammar
Each entry has an id and either a plugin name or a group of child
entries. config and disabled are the only expression-capable fields.
- id: app group: - id: storage name: storage.sqlite config: path: $expr: env: APP_DB default: var/app.sqlite - id: api name: api.http inject: [storage]
The safe expression vocabulary is small and explicit:
{ $expr: { env: NAME, default: value } }{ $expr: { service: service-name } }{ $expr: { coalesce: [value-or-expression, ...] } }{ $expr: { concat: [value-or-expression, ...] } }
No YAML value is evaluated as PHP. A !expr YAML tag is also accepted and
uses the same structured payload.
The runtime envelope is closed and has a CI/editor-friendly YAML Schema:
YAML composition guide and
schema. Plugin config remains
opaque to that envelope schema and is validated by its registered plugin.
Layering and reload
YamlRuntimeLoader::reload($patches) reads and validates a complete document,
applies id-targeted patches, and reconciles the live plugin tree. A patch can
replace a row's fields or append entries to the root or a group:
- id: api config: port: 8080 - insert: - id: metrics name: telemetry.metrics
reloadIfChanged() is the host-friendly hot-reload primitive: a CLI watcher,
RoadRunner worker, or framework development server decides when to call it.
Lifecycle changes settle synchronously, and provider services are unpublished
before provider cleanup runs so dependents stop first. Long-lived hosts must
still call reload at a quiescent safe point: Cordis cannot invalidate a service
reference already held by application code that is still executing.
Quality gates
composer install composer qa composer examples ys -f resources/schema/composition.schema.yaml examples/03-yaml-live-reload/composition.initial.yaml
The Pest suite covers lifetimes, service replacement, dependency pending and restart, event modes, YAML validation, expression safety, patches, groups, health snapshots, isolation, interception, and live reconciliation. Pull requests run the suite on PHP 8.2 through 8.5, with Symfony YAML 7 and Symfony YAML 8 on the PHP versions supported by each line.
Releases
The package is published through Packagist from the
public GitHub repository. Release versions are owned by
ops/version/current.yaml, and v<version> tags
drive the checked GitHub Release workflow. Contributors preparing a release
can inspect the complete procedure with:
just ops version check just ops release gate
Packagist derives Composer versions from the VCS tags, so the Composer manifest intentionally does not duplicate the release version.
Repository operations
The repository carries a versioned, self-describing operations catalogue under
ops/. It keeps local developer and agent workflows explicit
without replacing Composer's PHP tooling:
just # discover the local command surface just validate # verify the versioned operations catalogue just check # run its derived fast quality lane just ci # run the CI-shaped local workflow just ops release status # reach any capability directly
Each capability declares its command surface, required tools, path ownership, and any agent skill. The control validator checks those contracts, and the repository-wide check and test lanes are derived from the manifests rather than maintained as a second hand-written list.
Operator and agent skills
The solution-facing skill catalogue maps real Cordis use
cases to focused procedures for design, agent harnesses, plugin development,
YAML configuration, refactoring, operations, troubleshooting, and verification.
They are repository-local durable instructions for operators and coding agents;
they complement the repository-maintenance skills stored inside ops/.
Runnable examples
The examples directory contains nine self-checking CLI programs. They show scoped cleanup, dynamic dependency restart, YAML reloads, event pipelines, runtime observability, service isolation, configuration validation, deployment-owned service swaps, and local service interception using only this library's public API.
Run all of them with:
composer examples