Download the PHP package nemanjajojic/tick without Composer
On this page you can find all versions of the php package nemanjajojic/tick. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.
Download nemanjajojic/tick
More information about nemanjajojic/tick
Files in nemanjajojic/tick
Package tick
Short Description Tick — a PHP task scheduler. One tick per cron minute. No daemon, no framework, no surprises.
License MIT
Informations about the package tick
Tick
Tick is a PHP task scheduler. One tick per cron minute. No daemon, no framework, no surprises.
A modern, framework-agnostic PHP 8.2+ task scheduler library with strict typing, PSR-3 / PSR-20 interfaces, optional Symfony Console command classes, and first-class support for locking, retry policies, and runtime constraints. Designed for Slim, Mezzio, and DevOps-oriented teams who want explicit control over their scheduled tasks without framework coupling or hidden globals.
Architecture
For an architectural overview of Tick's components and their runtime interactions, see docs/architecture.md.
Design Decisions
- Pure library, no daemon, no framework coupling: The scheduler is invoked externally (typically via cron). No long-running process, no framework-specific integrations.
- PSR-3 / PSR-20 interfaces only: Logging and time are abstracted via standard interfaces, enabling easy testing and integration.
- Pluggable behavior via small, single-purpose interfaces: Tick ships
LockInterface(FileLock),TaskInterface(CallableTask,RawTask),RetryPolicyInterface(5 built-in policies), andTaskDispatcherInterface(default, dry-run, timing decorator). Every concern that users may need to swap or decorate is an interface — bring your own Redis lock, queue-pushing task, or custom retry policy without forking the library. - Fluent builder with once-only guards: Calling
cron()/ sugar methods,withoutOverlapping(),retry(), or constraint methods more than once throwsInvalidScheduleException. This prevents accidental misconfiguration. - Name is supplied at registration, not inside the task: Tasks are reusable; the name is a property of the registration, not of the task object.
Boundary: What the Library Ships vs. What You Provide
The library ships:
Schedulerclass with three explicit registration methods:call(),invoke(),raw()- Two built-in task adapters (
CallableTask,RawTask) plus the openTaskInterfacecontract - Cron expression parsing (via
dragonmantank/cron-expression) - File-based locking (
FileLock) withflock()semantics - Retry policies (
NoRetryPolicy,ConstantBackoffRetryPolicy,LinearBackoffRetryPolicy,JitteredExponentialBackoffRetryPolicy,DecorrelatedJitterRetryPolicy) - Runtime constraints (
when(),skip()) - Dry-run dispatcher and timing decorator
- Four optional Symfony Console command classes (
RunCommand,ListTasksCommand,RunTaskCommand,UpcomingCommand)
You provide:
- Composition root that builds the
Schedulerand registers tasks - Console wiring — if you want the CLI commands, you instantiate
Symfony\Component\Console\Applicationand add the four command classes yourself (no binary or autodiscovery is shipped) - Container setup (PHP-DI, Pimple, or manual wiring) — only if you use containers
- Task implementations (your business logic, as closures or
TaskInterfaceobjects) - Crontab entry to invoke the scheduler every minute
- Custom
LockInterfaceif you need Redis/database locks
Installation
Core (library only)
This gives you the Scheduler, all task adapters, dispatchers, locks, and retry policies. Zero framework dependencies. Use this if you call $scheduler->run() directly from your own application (Slim middleware, a Laravel command, a cron-invoked PHP script, etc.).
With the optional Console commands
This makes the four command classes (RunCommand, ListTasksCommand, RunTaskCommand, UpcomingCommand) usable. Tick does not ship a binary or an Application factory — you instantiate Symfony\Component\Console\Application and add the commands yourself. See Pattern 2 below for a complete bin/ script. symfony/console is a suggested dependency, not a hard one. The CLI is convenience, not the product; you can use the entire scheduler without it.
Requirements
- PHP 8.2+
- Composer 2
Quick Start
The Scheduler provides three explicit registration methods. Each requires a name as the first argument.
Usage Patterns
There are three integration patterns, from least to most CLI tooling. All three build a Scheduler in user code; Tick does not impose a bootstrap-file convention.
- Pure library — call
$scheduler->run()from your own PHP entry point. Nosymfony/console, no command classes. - Standalone Symfony Console application — install
symfony/console, add the three Tick command classes to your ownApplication, invoke via cron. - Framework command bus — register the three Tick command classes as services in your existing Symfony / Laravel / Mezzio app and let your framework's console front-controller run them.
Pattern 1: Pure library (no CLI, no Symfony)
The smallest possible deployment: a single PHP script that builds a Scheduler, registers tasks, and calls run(). Invoked once per minute from cron. No bootstrap indirection, no symfony/console, no framework.
Create cron/scheduler.php:
GenerateReports is any class implementing NemanjaJojic\Tick\Task\TaskInterface:
Crontab entry:
That is the entire integration. No symfony/console, no command classes, no CLI binary. This pattern suits:
- Small services where the scheduler is one of a few simple cron entries
- Projects that already use a non-Symfony framework (Slim, Mezzio, plain PHP) and don't want a second console
- Environments where pulling in additional dependencies is undesirable
- CI/CD pipelines where you want to drive a single tick from a shell script or test
Pattern 2: Standalone Symfony CLI with PHP-DI
When you want a proper CLI with tick:list, tick:run-task, and tick:upcoming for ops and debugging, install symfony/console, build your own Application, and add Tick's four command classes. There is no vendor/bin/tick and no --bootstrap option — you own the entry point and the wiring.
Create bin/my-scheduler:
Make the script executable:
Crontab entry:
This pattern suits:
- Slim / Mezzio / Laminas apps with a real DI container
- Teams that want
tick:listandtick:run-task <name>for ops and debugging - Multi-environment setups (
bin/my-scheduler-staging,bin/my-scheduler-prod) - Projects already invested in
symfony/consolefor other CLI tooling
Pattern 3: Framework command bus (Slim + PHP-DI)
Slim is an HTTP micro-framework and intentionally ships no console layer. The idiomatic way to "register Tick as services" in a Slim app is to reuse the same PHP-DI container Slim already uses for HTTP, and let the container build both the Scheduler and the three command classes. A thin bin/console entry point then turns container services into a Symfony Application. There is no Tick-specific bootstrap and no per-command construction in user code.
Define the services once in your existing PHP-DI definitions file (config/container.php or wherever Slim loads them):
Both your Slim public/index.php and your bin/console build the same container. The console entry point pulls the three commands out of the container and registers them on a Symfony Application:
Invoke through your project's single console:
This pattern suits teams who already have a Slim HTTP app, want one DI container for both web and CLI, and don't want a separate scheduler entry point. Substitute Mezzio (same PHP-DI / Aura.Di approach) or Laminas (Laminas\Cli) with equivalent service definitions.
CLI Reference
The five Console command classes Tick ships:
| Class | Name | Purpose |
|---|---|---|
RunCommand |
tick:run |
Run all due tasks (single tick). Cron-friendly. |
ListTasksCommand |
tick:list |
List registered task names, one per line. |
RunTaskCommand |
tick:run-task |
Force-run a single task by name. Honors lock and constraints. |
UpcomingCommand |
tick:upcoming |
Show upcoming task occurrences without executing anything. |
DetailCommand |
tick:detail |
Show full configuration for a single task plus the next N runs. |
For full usage, options, sample output, exit codes, and flock/systemd deployment recipes, see docs/cli.md.
Configuration Reference
The TaskBuilder returned by registration methods provides these configuration methods. Each method enforces a once-only contract: calling it twice throws InvalidScheduleException.
Schedule Methods
| Method | Cron Expression | Description |
|---|---|---|
cron(string $expression) |
Custom | Any valid cron expression |
everyNMinutes(int $minute) |
*/{minute} * * * * |
Every N minutes (1-59) |
hourly() |
0 * * * * |
At minute 0 of every hour |
hourlyAt(int $minute) |
{minute} * * * * |
At specified minute of every hour |
daily() |
0 0 * * * |
At midnight |
dailyAt(string $time) |
{minute} {hour} * * * |
At specified time (HH:MM format) |
weekly() |
0 0 * * 0 |
At midnight on Sunday |
weeklyOn(int $day, string $time) |
{minute} {hour} * * {day} |
At specified time on specified day (0=Sunday) |
monthly() |
0 0 1 * * |
At midnight on the 1st |
monthlyOn(int $day, string $time) |
{minute} {hour} {day} * * |
At specified time on specified day of month |
yearly() |
0 0 1 1 * |
At midnight on January 1st |
Behavior Methods
| Method | Description |
|---|---|
withoutOverlapping(int $ttlSeconds = 3600) |
Prevent concurrent execution via lock |
retry(RetryPolicyInterface $policy) |
Configure retry behavior on failure |
when(Closure $predicate) |
Run only if predicate returns true |
skip(Closure $predicate) |
Skip if predicate returns true |
describe(string $description) |
Attach a human-readable description (max 500 chars, surfaced by tick:detail) |
Task Descriptions
describe() attaches free-form documentation to a task. The description is stored on the RegisteredTask value object and surfaced by the tick:detail console command (both table and json formats). It does not affect scheduling or execution.
Validation rules (enforced at build time, throw InvalidScheduleException):
- Cannot be empty or whitespace-only.
- Maximum 500 characters (measured with
mb_strlen, UTF-8 safe). - May not contain control characters; newlines (
\n,\r) and other ASCII control bytes are rejected to keep table rendering and log lines clean. Tabs (\t) are permitted. - May be called at most once per task.
Overlap Protection (Locking)
Use withoutOverlapping() to prevent concurrent execution of the same task:
The lock key is the task name. The lock is held for the entire dispatch duration (including retry attempts and backoff sleeps). If the scheduler ticks again while a previous dispatch is still running, the subsequent tick observes LOCK_BUSY and is skipped.
The FileLock implementation uses kernel-level flock() with LOCK_EX | LOCK_NB for non-blocking acquisition. Locks are automatically released when the process exits.
Disabling Overlap Protection
For dev/local environments where overlap protection is undesirable, or for tests that exercise tasks declared with withoutOverlapping() without needing a writable filesystem, pass an inline no-op LockInterface implementation:
Do NOT use a no-op lock in production multi-tick deployments — overlap protection is silently disabled.
Custom Lock Implementations
Implement LockInterface for Redis, database, or other backends:
Retry Policies
NoRetryPolicy (Default)
Executes the task once. If it fails, the failure is recorded without retrying. This is the default if retry() is not called.
ConstantBackoffRetryPolicy
Retries with a constant delay between attempts:
LinearBackoffRetryPolicy
Retries with linearly increasing delays, optionally capped. See AWS Architecture Blog: Exponential Backoff And Jitter for background on retry-delay strategies:
JitteredExponentialBackoffRetryPolicy
AWS "full jitter" strategy. Spreads retries across the time window to avoid thundering-herd contention against a recovering downstream:
DecorrelatedJitterRetryPolicy
AWS "decorrelated jitter" strategy. Produces good spread while avoiding the long-tail variance of full jitter. Recommended default for retry storms against shared infrastructure:
Custom Retry Policy
Implement RetryPolicyInterface:
Constraints (when / skip)
Use when() and skip() to gate a due task behind a runtime predicate. The predicate is evaluated on every tick where the task's schedule matches.
when() — Feature Flags and Environment Gating
skip() — Maintenance Windows
Predicate Contract
- Constraints fire before lock acquisition (a skipped tick does not consume the lock)
- Constraint evaluation is performed by the dispatcher; the scheduler delegates entirely
- If the predicate returns
false, the dispatcher returnsDispatchResult::skippedByConstraint()(logged atinfolevel when wrapped byLoggingTaskDispatcherDecorator) - If the predicate throws, the dispatcher returns
DispatchResult::skippedByConstraint($exception)(logged aterrorlevel when wrapped); the task is skipped - Each builder accepts exactly one constraint; calling
when()afterskip()(or either twice) throwsInvalidScheduleException
Observability
Tick has no built-in logger. To get structured PSR-3 logs of every dispatch outcome, wrap your dispatcher with LoggingTaskDispatcherDecorator:
The decorator emits one log entry per dispatch and catches any Throwable raised by the inner dispatcher (logged at critical), returning a synthetic DispatchResult::failed() so the loop continues.
Log levels:
| Outcome | Level | Message |
|---|---|---|
COMPLETED |
info |
Tick: task completed. |
FAILED |
error |
Tick: task failed. |
LOCK_BUSY |
info |
Tick: task skipped (lock not acquired). |
DRY_RUN |
info |
Tick: would run. |
SKIPPED_BY_CONSTRAINT (predicate returned false) |
info |
Tick: task skipped (constraint). |
SKIPPED_BY_CONSTRAINT (predicate threw) |
error |
Tick: constraint predicate failed. |
| inner dispatcher threw | critical |
Tick: dispatcher threw; task skipped. |
Without the decorator, the scheduler is silent. Dispatcher exceptions propagate.
Dry Run
The scheduler delegates execution to a TaskDispatcherInterface. Two implementations ship with the library:
TaskDispatcher— acquires lock, runs retry policy, executes taskDryRunTaskDispatcher— returnsDispatchResult::dryRun()without executing anything
Pick the dispatcher at the composition root:
In dry-run mode, constraint predicates are still evaluated (their side effects will fire). This is deliberate: dry-run reports what the real run would actually do given current runtime state.
Composing Dispatchers
The TaskDispatcherInterface allows decorator composition. The library ships TimingTaskDispatcherDecorator for execution timing:
The timing decorator adds timing.durationMs to the DispatchResult::$logContext, which the LoggingTaskDispatcherDecorator includes in log entries when both decorators are chained.
Timezones
The Scheduler constructor takes a required DateTimeZone. Cron expressions are evaluated against wall-clock time in this timezone:
Multiple Timezones
Instantiate multiple Scheduler instances for different timezone cohorts:
Recommendation
Run servers in UTC, keep ClockInterface in UTC, and let the Scheduler timezone be the only zoned component. This eliminates ambiguity in logs and audit trails.
Development with Docker
The compose.yml provides a php service (PHP 8.2-cli-alpine).
Contributing
See CONTRIBUTING.md for the contribution workflow, quality gates, and coding standards.
License
MIT License. See LICENSE for details.