• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In
You are now the owner of this repo.

daycry / jobs / 25509260188

07 May 2026 03:36PM UTC coverage: 75.053%. First build
25509260188

push

github

daycry
feat(v2.0): opt-in API with immutable JobDefinition + lease-based QueueBackend

Introduces the new Daycry\Jobs\V2\ namespace alongside the existing v1 API
so adopters can migrate incrementally. Nothing in v1 changes: legacy Job
builders, QueueInterface and WorkerInterface keep working unchanged.

- JobDefinition: readonly value object replacing the mutable Job builder;
  withXxx() copies for safe sharing across enqueue sites; fromLegacyJob()
  bridges from the v1 mutable builder.
- JobLease + QueueBackend: unified enqueue/fetch/ack/nack/abandon contract
  that replaces the QueueInterface+WorkerInterface split, removing the
  per-instance hidden state in legacy backends.
- LegacyWorkerAdapter wraps any v1 backend so worker code can already use
  the new lease API without touching the underlying implementation.
- TypedJobHandler base class rehydrates payloads to a declared DTO via
  reflection so handlers receive typed objects instead of mixed.
- docs/V2_MIGRATION.md documents the four-step adoption path and the
  proposed deprecation timeline (v1.2 -> v1.3 native -> v2.0 -> v3.0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

118 of 144 new or added lines in 4 files covered. (81.94%)

2139 of 2850 relevant lines covered (75.05%)

12.03 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.77
/src/V2/JobDefinition.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Queues.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Jobs\V2;
15

16
use DateTimeImmutable;
17
use DateTimeInterface;
18
use Daycry\Jobs\Job;
19
use ReflectionException;
20
use ReflectionProperty;
21

22
/**
23
 * Immutable description of a job at definition time.
24
 *
25
 * Compared to the v1 mutable {@see \Daycry\Jobs\Job} builder, JobDefinition is a value
26
 * object: every withXxx() helper returns a new instance, so the same definition can be
27
 * shared across enqueue sites without spooky action at a distance.
28
 *
29
 * The v2 design splits the responsibilities that previously lived on Job into three
30
 * objects:
31
 *  - JobDefinition: what the job IS (handler key, payload, scheduling, retry policy).
32
 *  - JobEnvelope (existing v1 class, reused): how the definition travels through queues.
33
 *  - JobRuntime (future): the in-flight, mutable state during execution (attempts, output).
34
 *
35
 * Adoption is gradual: callers can keep using the v1 Job builder; v2 components accept
36
 * either input via {@see fromLegacyJob()} until the legacy API is removed in a future
37
 * major release.
38
 */
39
final class JobDefinition
40
{
41
    /**
42
     * @param string                 $handler        Handler key (must exist in Config\Jobs::$jobs).
43
     * @param mixed                  $payload        Arbitrary payload passed to the handler.
44
     * @param string|null            $name           Friendly name for logs/metrics; defaults to handler:hash(payload).
45
     * @param string|null            $queue          Optional explicit queue; null means "use first configured queue".
46
     * @param int                    $priority       Higher = sooner (backend-dependent).
47
     * @param int|null               $maxRetries     Null = no retries; integers = retry up to N times before DLQ.
48
     * @param int|null               $timeout        Per-attempt soft timeout in seconds; null = use config defaultTimeout.
49
     * @param DateTimeImmutable|null $scheduledAt    Earliest run time (UTC); null = run as soon as possible.
50
     * @param bool                   $singleInstance Lock with a runtime cache flag to prevent concurrent runs.
51
     * @param list<string>           $environments   Restrict execution to these CI4 environments; empty = no restriction.
52
     * @param list<string>           $dependsOn      Job names that must succeed first within the same scheduler run.
53
     * @param string                 $cronExpression Cron schedule used by Scheduler; defaults to every minute.
54
     * @param array<string, mixed>   $meta           Free-form metadata propagated to the envelope.
55
     */
56
    public function __construct(
57
        public readonly string $handler,
58
        public readonly mixed $payload,
59
        public readonly ?string $name = null,
60
        public readonly ?string $queue = null,
61
        public readonly int $priority = 5,
62
        public readonly ?int $maxRetries = 0,
63
        public readonly ?int $timeout = null,
64
        public readonly ?DateTimeImmutable $scheduledAt = null,
65
        public readonly bool $singleInstance = false,
66
        public readonly array $environments = [],
67
        public readonly array $dependsOn = [],
68
        public readonly string $cronExpression = '* * * * *',
69
        public readonly array $meta = [],
70
    ) {
71
    }
5✔
72

73
    public function withName(string $name): self
74
    {
75
        return $this->copy(['name' => $name]);
1✔
76
    }
77

78
    public function withQueue(?string $queue): self
79
    {
80
        return $this->copy(['queue' => $queue]);
1✔
81
    }
82

83
    public function withPriority(int $priority): self
84
    {
85
        return $this->copy(['priority' => $priority]);
1✔
86
    }
87

88
    public function withMaxRetries(?int $maxRetries): self
89
    {
NEW
90
        return $this->copy(['maxRetries' => $maxRetries]);
×
91
    }
92

93
    public function withTimeout(?int $timeout): self
94
    {
NEW
95
        return $this->copy(['timeout' => $timeout]);
×
96
    }
97

98
    public function withScheduledAt(?DateTimeImmutable $when): self
99
    {
100
        return $this->copy(['scheduledAt' => $when]);
1✔
101
    }
102

103
    public function withSingleInstance(bool $singleInstance = true): self
104
    {
NEW
105
        return $this->copy(['singleInstance' => $singleInstance]);
×
106
    }
107

108
    /**
109
     * @param list<string> $environments
110
     */
111
    public function withEnvironments(array $environments): self
112
    {
NEW
113
        return $this->copy(['environments' => $environments]);
×
114
    }
115

116
    /**
117
     * @param list<string> $dependsOn
118
     */
119
    public function withDependsOn(array $dependsOn): self
120
    {
NEW
121
        return $this->copy(['dependsOn' => $dependsOn]);
×
122
    }
123

124
    public function withCronExpression(string $expression): self
125
    {
NEW
126
        return $this->copy(['cronExpression' => $expression]);
×
127
    }
128

129
    /**
130
     * @param array<string, mixed> $meta
131
     */
132
    public function withMeta(array $meta): self
133
    {
NEW
134
        return $this->copy(['meta' => $meta]);
×
135
    }
136

137
    /**
138
     * Bridge from the v1 mutable Job builder. Reads only public/declared state — does
139
     * NOT carry callbacks or middleware (those remain v1-only until full v2 migration).
140
     */
141
    public static function fromLegacyJob(Job $job): self
142
    {
143
        $schedule = null;
1✔
144

145
        try {
146
            $reflected = new ReflectionProperty($job, 'schedule');
1✔
147

148
            /** @var DateTimeInterface|null $raw */
149
            $raw = $reflected->isInitialized($job) ? $reflected->getValue($job) : null;
1✔
150
            if ($raw instanceof DateTimeInterface) {
1✔
151
                $schedule = DateTimeImmutable::createFromInterface($raw);
1✔
152
            }
NEW
153
        } catch (ReflectionException) {
×
154
            // No schedule on the legacy Job — fine, leave as null.
155
        }
156

157
        $priority = 5;
1✔
158
        if (method_exists($job, 'getPriority')) {
1✔
NEW
159
            $rawPriority = $job->getPriority();
×
NEW
160
            $priority    = is_int($rawPriority) ? $rawPriority : 5;
×
161
        }
162

163
        // EnvironmentTrait::getEnvironments() returns the array carried by Job; the trait
164
        // is mixed into Job in src/Job.php so the method is always present here.
165
        $environments = array_values($job->getEnvironments());
1✔
166

167
        return new self(
1✔
168
            handler: $job->getJob(),
1✔
169
            payload: $job->getPayload(),
1✔
170
            name: $job->getName(),
1✔
171
            queue: $job->getQueue(),
1✔
172
            priority: $priority,
1✔
173
            maxRetries: $job->getMaxRetries(),
1✔
174
            timeout: $job->getTimeout(),
1✔
175
            scheduledAt: $schedule,
1✔
176
            singleInstance: $job->isSingleInstance(),
1✔
177
            environments: $environments,
1✔
178
            dependsOn: array_values($job->getDependsOn() ?? []),
1✔
179
            cronExpression: $job->getExpression(),
1✔
180
        );
1✔
181
    }
182

183
    /**
184
     * @param array<string, mixed> $changes
185
     */
186
    private function copy(array $changes): self
187
    {
188
        return new self(
2✔
189
            handler: $changes['handler'] ?? $this->handler,
2✔
190
            payload: array_key_exists('payload', $changes) ? $changes['payload'] : $this->payload,
2✔
191
            name: array_key_exists('name', $changes) ? $changes['name'] : $this->name,
2✔
192
            queue: array_key_exists('queue', $changes) ? $changes['queue'] : $this->queue,
2✔
193
            priority: $changes['priority'] ?? $this->priority,
2✔
194
            maxRetries: array_key_exists('maxRetries', $changes) ? $changes['maxRetries'] : $this->maxRetries,
2✔
195
            timeout: array_key_exists('timeout', $changes) ? $changes['timeout'] : $this->timeout,
2✔
196
            scheduledAt: array_key_exists('scheduledAt', $changes) ? $changes['scheduledAt'] : $this->scheduledAt,
2✔
197
            singleInstance: $changes['singleInstance'] ?? $this->singleInstance,
2✔
198
            environments: $changes['environments'] ?? $this->environments,
2✔
199
            dependsOn: $changes['dependsOn'] ?? $this->dependsOn,
2✔
200
            cronExpression: $changes['cronExpression'] ?? $this->cronExpression,
2✔
201
            meta: $changes['meta'] ?? $this->meta,
2✔
202
        );
2✔
203
    }
204
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc