php-alchemist/chrono-smith

A reusable, deterministic Scheduling Engine for PHP.

Maintainers

Package info

github.com/PHP-Alchemist/ChronoSmith

pkg:composer/php-alchemist/chrono-smith

Statistics

Installs: 2

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

v1.0.0-rc 2026-05-01 00:55 UTC

This package is auto-updated.

Last update: 2026-05-01 01:07:07 UTC


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, and rem.
  • 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-DD
  • i: interval, such as 1d, 1w, or 1m
  • a: optional anchor, such as dowFRI, dom30, or eom
  • r: monthly roll policy, back or forward
  • stick: monthly stickiness, dom or eom
  • mode: due semantics, on or by
  • n / rem: max and remaining occurrences
  • e: end date; no occurrences after this date
  • gr: grace period used by overdue derivation
  • ns: PHP-style namespace/class-name metadata, preserved exactly
  • meta64: 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(), and serialize()
  • 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/:

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