php-alchemist / chrono-smith
A reusable, deterministic Scheduling Engine for PHP.
v1.0.0-rc
2026-05-01 00:55 UTC
Requires
- php: >=8.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpunit/phpunit: ^11.0
- povils/phpmnd: ^3.0
- squizlabs/php_codesniffer: ^3.0
- vimeo/psalm: ^6.0
README
ChronoSmith is a reusable, deterministic scheduling engine for PHP 8.4+. It models recurring obligations with a compact, versioned DSL (CS=1) and separates schedule intent from mutable schedule progress.
The engine is domain-agnostic: it computes schedule dates, records progress, and derives status such as overdue without knowing anything about billing, memberships, reminders, jobs, or any other application domain.
Installation
composer require php-alchemist/chrono-smith
Core Concepts
- Schedule Definition: Mostly immutable recurrence intent: start date, interval, anchor, roll policy, stickiness, due mode, end date, occurrence limits, timezone metadata, grace period, namespace, and opaque metadata.
- Schedule State: Mutable progress through obligations:
cursor,last,next, andrem. - Cursor Stability: Advancement is based on the scheduled due date (
cursor), not the actual occurrence date (last). Early or late completion does not cause anchor drift. - Derived Overdue Status: Overdue is never stored. It is derived at runtime with
now > next + grace. - Versioned Codec: DSL hydration and serialization are handled by versioned codecs using read-old/write-latest semantics.
DSL Example
[CS=1;ns=App\Scheduling\BillingScheduler;s=@2026-01-30;i=1m;a=dom30;r=back;stick=dom;cursor=@2026-01-30;next=@2026-02-28]
Important fields:
s: start date, written as@YYYY-MM-DDi: interval, such as1d,1w, or1ma: optional anchor, such asdowFRI,dom30, oreomr: monthly roll policy,backorforwardstick: monthly stickiness,domoreommode: due semantics,onorbyn/rem: max and remaining occurrencese: end date; no occurrences after this dategr: grace period used by overdue derivationns: PHP-style namespace/class-name metadata, preserved exactlymeta64: opaque base64url metadata
Quick Start
<?php require_once __DIR__ . '/vendor/autoload.php'; use PHPAlchemist\ChronoSmith\Codec\CodecRegistry; use PHPAlchemist\ChronoSmith\Codec\CS1Codec; use PHPAlchemist\ChronoSmith\Engine\AdvancementEngine; use PHPAlchemist\ChronoSmith\Engine\Scheduler; $registry = new CodecRegistry(); $registry->register(new CS1Codec()); $scheduler = new Scheduler($registry, new AdvancementEngine()); $schedule = $scheduler->hydrate( '[CS=1;ns=App\Scheduling\BillingScheduler;s=@2026-01-30;i=1m;a=dom30;r=back;stick=dom]', ); echo $scheduler->nextDue($schedule)?->format('Y-m-d'); // 2026-01-30 $schedule = $scheduler->advance($schedule); echo $schedule->cursor?->format('Y-m-d'); // 2026-01-30 echo $schedule->nextDueDate?->format('Y-m-d'); // 2026-02-28 $schedule = $scheduler->satisfyNext($schedule, new \DateTimeImmutable('2026-02-25')); echo $schedule->cursor?->format('Y-m-d'); // 2026-02-28 echo $schedule->lastActualOccurrence?->format('Y-m-d'); // 2026-02-25 echo $schedule->nextDueDate?->format('Y-m-d'); // 2026-03-30 $serialized = $scheduler->serialize($schedule);
Common Operations
Check Overdue Status
$schedule = $scheduler->hydrate('[CS=1;s=@2026-02-01;i=1m;next=@2026-02-01;gr=3d]'); $scheduler->isOverdue($schedule, new \DateTimeImmutable('2026-02-04')); // false $scheduler->isOverdue($schedule, new \DateTimeImmutable('2026-02-05')); // true
Stop After a Set Number of Occurrences
$schedule = $scheduler->hydrate('[CS=1;s=@2026-01-01;i=1w;a=dowTHU;n=3]'); $schedule = $scheduler->advance($schedule); // rem=2 $schedule = $scheduler->advance($schedule); // rem=1 $schedule = $scheduler->advance($schedule); // rem=0, next=null
Preserve Namespace and Metadata
$schedule = $scheduler->hydrate( '[CS=1;ns=App\Scheduling\BillingScheduler;s=@2026-01-01;i=1m;meta64=eyJwbGFuIjoicHJvIn0]', ); echo $schedule->namespace; // App\Scheduling\BillingScheduler echo $schedule->metadata; // {"plan":"pro"}
Features
- Daily, weekly, and monthly recurrence
- Weekday, day-of-month, and end-of-month anchors
- Monthly roll policies for invalid calendar dates
- Monthly stickiness for returning to a day-of-month or staying end-of-month
- Early and late completion without anchor drift
nextDue(),advance(),satisfyNext(),isOverdue(),hydrate(), andserialize()- End date and occurrence-count limits
- Grace-period based overdue derivation
- PHP-style namespace metadata preserved byte-for-byte
- Opaque base64url metadata via
meta64 - Unknown field preservation for forward compatibility
- Versioned codec registry
Examples
Executable examples live in examples/:
- Minimal daily schedule
- Monthly anchor rolls
- Satisfy without anchor drift
- Overdue with grace
- Namespaces, metadata, and unknown fields
- End date and occurrence limits
- Weekly
mode=by
Run one from the repository root:
php examples/03_satisfy_without_anchor_drift.php
Documentation
Development
composer validate --no-check-publish vendor/bin/phpunit --testdox vendor/bin/psalm --no-cache --threads=1
Requirements
- PHP 8.4+
- Composer
License
MIT