PHPackages                             jardissupport/scheduling - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. jardissupport/scheduling

ActiveLibrary

jardissupport/scheduling
========================

Cron expression parsing and task scheduling with fluent API and timezone support

v1.0.0(1mo ago)00proprietaryPHPPHP &gt;=8.2CI passing

Since Apr 5Pushed 1mo agoCompare

[ Source](https://github.com/jardisSupport/scheduling)[ Packagist](https://packagist.org/packages/jardissupport/scheduling)[ Docs](https://github.com/jardisSupport/scheduling)[ RSS](/packages/jardissupport-scheduling/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (1)Dependencies (4)Versions (3)Used By (0)

Jardis Scheduling
=================

[](#jardis-scheduling)

[![Build Status](https://github.com/jardisSupport/scheduling/actions/workflows/ci.yml/badge.svg)](https://github.com/jardisSupport/scheduling/actions/workflows/ci.yml/badge.svg)[![License: PolyForm Shield](https://camo.githubusercontent.com/d8fb46c82be4c5312bf3e372ac734dfdf6a8b328e9c2b2856af671adbb0600a5/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d506f6c79466f726d253230536869656c642d626c75652e737667)](LICENSE.md)[![PHP Version](https://camo.githubusercontent.com/a68b290dcc313d698dc138a1111aa83eee2f143605449d7e8b5416ea6f88558f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048502d253345253344382e322d3737374242342e737667)](https://www.php.net/)[![PHPStan Level](https://camo.githubusercontent.com/c51bda247654363d3e30bc352674dd761a9557803a14af0226eb411d6dc0006b/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f5048505374616e2d4c6576656c253230382d627269676874677265656e2e737667)](phpstan.neon)[![PSR-12](https://camo.githubusercontent.com/34b10db0caa29bacd49bda5c437a8de95385f036f3230b31fa605326e18da22c/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f436f64652532305374796c652d5053522d2d31322d626c75652e737667)](phpcs.xml)

> Part of the **[Jardis Business Platform](https://jardis.io)** — Enterprise-grade PHP components for Domain-Driven Design

**Scheduling rules as code.** Cron expression parsing and task scheduling with a fluent API — defines *when* tasks should run without executing them. No I/O, no persistence, no external dependencies. Pure PHP time logic.

---

Why This Package?
-----------------

[](#why-this-package)

- **Two ways to define timing** — Cron syntax (`*/5 9-17 * * 1-5`) for power users, fluent helpers (`->dailyAt('08:00')`) for everyone
- **Constraints beyond cron** — time windows, weekdays, environments, callable conditions
- **Tags &amp; priority** — categorize tasks, filter by tag, execute in priority order
- **Overlap guard** — mark tasks that must not run concurrently
- **Human-readable descriptions** — `describe()` turns cron syntax into readable text
- **Schedule validation** — detect duplicate names, conflicting constraints before runtime
- **Timezone-aware** — expressions evaluate against any timezone, regardless of server time
- **Testable** — pass any `DateTimeInterface` to `isDue()`, no system clock dependency
- **Zero dependencies** — pure PHP, no framework, no cron daemon required

---

Installation
------------

[](#installation)

```
composer require jardissupport/scheduling
```

---

Quick Start
-----------

[](#quick-start)

### Define a Schedule

[](#define-a-schedule)

```
use JardisSupport\Scheduling\Schedule;

$schedule = Schedule::create()
    ->task('cleanup:expired')
        ->dailyAt('03:00')
        ->description('Remove expired records')
        ->tag('maintenance')
        ->priority(10)
    ->task('sync:inventory')
        ->everyFiveMinutes()
        ->between('08:00', '18:00')
        ->weekdays()
        ->tag('sync', 'erp')
        ->withoutOverlapping()
    ->task('report:monthly')
        ->monthlyOn(1, '07:00')
        ->timezone('Europe/Berlin')
        ->tag('reports');
```

### Get Due Tasks

[](#get-due-tasks)

```
$now = new DateTimeImmutable();

// All due tasks (sorted by priority, highest first)
foreach ($schedule->dueNow($now) as $task) {
    echo $task->name();        // 'cleanup:expired'
    echo $task->description(); // 'Remove expired records'
    // Dispatch however you want — command bus, queue, subprocess
}

// Filter by tags
$syncTasks = $schedule->dueNow($now, ['sync']);
```

---

Cron Expression Parser
----------------------

[](#cron-expression-parser)

Standalone cron parsing — usable without the Schedule API:

```
use JardisSupport\Scheduling\CronExpression;

$cron = CronExpression::parse('*/5 9-17 * * 1-5');

$cron->isDue($now);              // true/false
$cron->nextRun($now);            // next matching DateTimeInterface
$cron->nextRuns($now, 5);        // next 5 matching times
$cron->previousRun($now);        // last matching DateTimeInterface
$cron->describe();               // 'Every 5 minutes', 'Daily at 09:30', etc.
```

### Supported Syntax

[](#supported-syntax)

FeatureExampleStandard 5-field`30 8 * * *`Ranges`0 9-17 * * *`Lists`0,15,30,45 * * * *`Steps`*/5 * * * *`Combined`1-10/3 * * * *`Seconds (6-field)`*/30 * * * * *`Year (7-field)`0 0 3 1 1 * 2027`Predefined`@daily`, `@hourly`, `@weekly`, `@monthly`, `@yearly`### Timezone Support

[](#timezone-support)

```
$cron = CronExpression::parse('0 8 * * *', new DateTimeZone('Europe/Berlin'));

// Evaluates against Berlin time, regardless of server timezone
$cron->isDue(new DateTimeImmutable('now', new DateTimeZone('UTC')));
```

---

Fluent Time Helpers
-------------------

[](#fluent-time-helpers)

No cron syntax required — readable method names that generate the right expressions:

MethodEquivalent`everyMinute()``* * * * *``everyFiveMinutes()``*/5 * * * *``everyFifteenMinutes()``*/15 * * * *``everyThirtyMinutes()``*/30 * * * *``hourly()``0 * * * *``hourlyAt(30)``30 * * * *``daily()``0 0 * * *``dailyAt('08:00')``0 8 * * *``weekly()``0 0 * * 0``weeklyOn(5, '14:00')``0 14 * * 5``monthly()``0 0 1 * *``monthlyOn(25, '06:00')``0 6 25 * *``yearly()``0 0 1 1 *``cron('...')`Direct expression---

Constraints
-----------

[](#constraints)

Additional restrictions beyond the cron expression — all composable:

### Time Windows

[](#time-windows)

```
->task('api:sync')
    ->everyFiveMinutes()
    ->between('08:00', '20:00')       // only during this window

->task('db:optimize')
    ->daily()
    ->unlessBetween('09:00', '17:00') // not during business hours
```

### Day Restrictions

[](#day-restrictions)

```
->task('erp:sync')
    ->hourly()
    ->weekdays()                      // Mon-Fri only

->task('backup:full')
    ->dailyAt('01:00')
    ->weekends()                      // Sat-Sun only

->task('supplier:import')
    ->dailyAt('06:00')
    ->days(2, 4)                      // Tue and Thu only (0=Sun, 6=Sat)
```

### Callable Conditions

[](#callable-conditions)

```
->task('beta:sync')
    ->everyFiveMinutes()
    ->when(fn() => $features->isEnabled('new-sync'))   // only if true

->task('cache:warmup')
    ->everyMinute()
    ->skip(fn() => $maintenance->isActive())           // skip if true
```

### Environment Restriction

[](#environment-restriction)

```
$schedule = Schedule::create('production')  // pass current environment
    ->task('monitor:uptime')
        ->everyMinute()
        ->environments('production', 'staging');
```

---

Tags, Priority &amp; Overlap Guard
----------------------------------

[](#tags-priority--overlap-guard)

### Tags

[](#tags)

Categorize tasks and filter by tag when querying:

```
->task('email:digest')
    ->dailyAt('08:00')
    ->tag('email', 'notifications')

// Query filtered
$schedule->dueNow($now, ['email']);     // only tasks tagged 'email'
$schedule->allTasks(['notifications']); // only tasks tagged 'notifications'
```

Tags use OR-semantics — a task matches if it has *any* of the requested tags.

### Priority

[](#priority)

Higher priority tasks are returned first:

```
->task('critical:alerts')
    ->everyMinute()
    ->priority(100)

->task('low:cleanup')
    ->everyMinute()
    ->priority(1)

// dueNow() and allTasks() return tasks sorted by priority (descending)
```

### Overlap Guard

[](#overlap-guard)

Mark tasks that should not run concurrently:

```
->task('import:large')
    ->everyFiveMinutes()
    ->withoutOverlapping()

// Check in your runner:
if (!$task->allowsOverlapping()) {
    // Acquire lock before executing
}
```

---

Human-Readable Descriptions
---------------------------

[](#human-readable-descriptions)

```
CronExpression::parse('* * * * *')->describe();      // 'Every minute'
CronExpression::parse('*/5 * * * *')->describe();     // 'Every 5 minutes'
CronExpression::parse('30 9 * * *')->describe();      // 'Daily at 09:30'
CronExpression::parse('0 9 * * 1')->describe();       // 'Weekly on Monday at 09:00'
CronExpression::parse('0 6 1 * *')->describe();       // 'Monthly on day 1 at 06:00'
CronExpression::parse('0 9-17 * * 1-5')->describe();  // 'Custom schedule'
```

---

Previous Run
------------

[](#previous-run)

Find the most recent time a cron expression would have matched:

```
$cron = CronExpression::parse('0 8 * * *');
$previous = $cron->previousRun(new DateTimeImmutable('2026-04-05 10:00:00'));
// 2026-04-05 08:00:00
```

---

Schedule Validation
-------------------

[](#schedule-validation)

Detect configuration problems before runtime:

```
$violations = $schedule->validate();

foreach ($violations as $violation) {
    echo $violation->severity;  // 'error' or 'warning'
    echo $violation->taskName;
    echo $violation->message;
}
```

CheckSeverityEmpty schedule (no tasks)warningDuplicate task nameserrorConflicting weekdays + weekends constraintswarning---

Inspecting the Schedule
-----------------------

[](#inspecting-the-schedule)

```
// All registered tasks (sorted by priority)
foreach ($schedule->allTasks() as $task) {
    echo $task->name();
    echo $task->description();
    echo $task->expression()->describe();
    echo $task->nextRun(new DateTimeImmutable())->format('Y-m-d H:i');
    echo $task->priority();
    echo $task->allowsOverlapping() ? 'yes' : 'no';
    echo implode(', ', $task->tags());
}

// Filter by tags
$emailTasks = $schedule->allTasks(['email']);
```

---

Error Handling
--------------

[](#error-handling)

ExceptionWhen`InvalidCronExpressionException`Unparseable cron syntax`InvalidScheduleException`Task without name, missing expression, invalid time format```
use JardisSupport\Scheduling\Exception\InvalidCronExpressionException;

try {
    CronExpression::parse('invalid');
} catch (InvalidCronExpressionException $e) {
    // "Invalid cron expression: "invalid" (Expected 5-7 fields, got 1)"
}
```

---

Architecture
------------

[](#architecture)

The user sees `Schedule` + fluent API. Internally, each concern is its own class:

```
Schedule (Orchestrator)
  │
  │  Fluent API: task() returns TaskBuilder
  │  Query: dueNow($now, $tags), allTasks($tags), validate()
  │
  ├── TaskBuilder (Internal)
  │   └── Fluent methods �� builds ScheduledTask
  │
  ├── ScheduledTask (Value Object)
  │   ├── name, description, tags, priority, overlapping
  │   ├── CronExpression
  │   ├── Constraints[]
  │   └── isDue(): expression.isDue() && all constraints satisfied
  │
  ├── CronExpression (Orchestrator)
  │   ├── parse() → field arrays (null = wildcard)
  │   ├── isDue() → compare fields against DateTime
  │   ├── nextRun() → iterate forward until match
  │   ├── previousRun() → iterate backward until match
  │   └── describe() → human-readable description
  │
  ├── Constraints (ConstraintInterface)
  │   ├── TimeWindow         between/unlessBetween
  │   ├── DayOfWeek          weekdays/weekends/days
  │   ├── CallableCondition  when/skip
  │   └── EnvironmentMatch   environments
  │
  └── ValidateSchedule → list

```

### What This Package Does NOT Do

[](#what-this-package-does-not-do)

- **No task execution** — no process spawning, no workers, no daemons
- **No persistence** — no database, no last-run tracking
- **No overlap prevention** — no locking (the flag is advisory for the runner)
- **No queue integration** — no message dispatch
- **No retry/error handling** — that's the runner's job

The runner calls `dueNow()` and decides what to do with the results.

---

Jardis Foundation Integration
-----------------------------

[](#jardis-foundation-integration)

Scheduling is a **support package** — no Foundation handler, no ENV configuration. The schedule is defined programmatically in your application layer:

```
// In your BoundedContext or Application Service:
$schedule = Schedule::create()
    ->task('order:cleanup')->dailyAt('03:00')
    ->task('invoice:generate')->monthlyOn(1, '06:00');

// Runner (CLI Command, Cron Job):
foreach ($schedule->dueNow(new DateTimeImmutable()) as $task) {
    $this->commandBus->dispatch($task->name());
}
```

---

Development
-----------

[](#development)

```
cp .env.example .env    # One-time setup
make install             # Install dependencies
make phpunit             # Run tests
make phpstan             # Static analysis (Level 8)
make phpcs               # Coding standards (PSR-12)
```

---

License
-------

[](#license)

[PolyForm Shield License 1.0.0](LICENSE.md) — free for all use including commercial. Only restriction: don't build a competing framework.

###  Health Score

38

—

LowBetter than 85% of packages

Maintenance91

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

46d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/e07a1b668e9e01ee6d1b85de7b3be1c2513f68aae9494b2011d1592104d5daa0?d=identicon)[jardis](/maintainers/jardis)

---

Top Contributors

[![Headgent](https://avatars.githubusercontent.com/u/245725954?v=4)](https://github.com/Headgent "Headgent (1 commits)")

---

Tags

crontimertaskDomain Driven DesignschedulingHeadgentjardis

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StylePHP\_CodeSniffer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/jardissupport-scheduling/health.svg)

```
[![Health](https://phpackages.com/badges/jardissupport-scheduling/health.svg)](https://phpackages.com/packages/jardissupport-scheduling)
```

###  Alternatives

[hhxsv5/laravel-s

🚀 LaravelS is an out-of-the-box adapter between Laravel/Lumen and Swoole.

3.9k676.0k10](/packages/hhxsv5-laravel-s)[jmose/command-scheduler-bundle

This Symfony bundle will allow you to schedule all your commands just like UNIX crontab

3361.4M1](/packages/jmose-command-scheduler-bundle)[omnilight/yii2-scheduling

Scheduling extension for Yii2 framework

3151.0M7](/packages/omnilight-yii2-scheduling)[orisai/scheduler

Cron job scheduler - with locks, parallelism and more

4037.1k4](/packages/orisai-scheduler)[laravel-admin-ext/scheduling

Task scheduling extension for laravel-admin

93247.1k6](/packages/laravel-admin-ext-scheduling)[rewieer/taskschedulerbundle

Task Scheduler with CRON for Symfony

63242.1k](/packages/rewieer-taskschedulerbundle)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
