Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.50% covered (success)
87.50%
14 / 16
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractRegistry
87.50% covered (success)
87.50%
14 / 16
60.00% covered (warning)
60.00%
3 / 5
11.24
0.00% covered (danger)
0.00%
0 / 1
 isExpired
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 encode
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 decode
75.00% covered (success)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
4.25
 prune
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 purgeUndecodable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Queue\Registry;
16
17/**
18 * Registry abstract class
19 *
20 * @category   Pop
21 * @package    Pop\Queue
22 * @author     Nick Sagona, III <nick@popphp.org>
23 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
24 * @license    https://www.popphp.org/license     New BSD License
25 * @version    3.0.0
26 */
27abstract class AbstractRegistry implements RegistryInterface
28{
29
30    /**
31     * Whether a record's heartbeat is older than the given number of
32     * seconds. Shared by every backend's prune() so the cutoff rule can't
33     * drift between them.
34     *
35     * @param  WorkerRecord $record
36     * @param  int          $olderThanSeconds
37     * @return bool
38     */
39    protected function isExpired(WorkerRecord $record, int $olderThanSeconds): bool
40    {
41        return ((time() - $record->getLastSeenAt()) > $olderThanSeconds);
42    }
43
44    /**
45     * Encode a record for storage
46     *
47     * JSON, deliberately - registry records are plain scalars and arrays,
48     * so this carries none of the unserialize() object-injection surface
49     * that job payloads do.
50     *
51     * @param  WorkerRecord $record
52     * @return string
53     * @throws Exception
54     */
55    protected function encode(WorkerRecord $record): string
56    {
57        // JSON_INVALID_UTF8_SUBSTITUTE so a record with a stray invalid byte in
58        // an operator-supplied name degrades to replacement characters rather
59        // than vanishing: losing sight of a worker is worse than an ugly name.
60        // The false-check still earns its place under declare(strict_types=1):
61        // returning false from a ": string" method now raises a TypeError, but
62        // one that names neither the record nor the encoding fault. Checking
63        // here trades that for a message carrying json_last_error_msg().
64        $payload = json_encode($record->toArray(), JSON_INVALID_UTF8_SUBSTITUTE);
65
66        if ($payload === false) {
67            throw new Exception('Error: Unable to encode the worker record: ' . json_last_error_msg());
68        }
69
70        return $payload;
71    }
72
73    /**
74     * Decode a stored record, or null if the payload is unusable
75     *
76     * @param  ?string $payload
77     * @return ?WorkerRecord
78     */
79    protected function decode(?string $payload): ?WorkerRecord
80    {
81        if (empty($payload)) {
82            return null;
83        }
84
85        $data = json_decode($payload, true);
86
87        return (is_array($data) && !empty($data['id'])) ? WorkerRecord::fromArray($data) : null;
88    }
89
90    /**
91     * Remove every record whose heartbeat is older than the given number of
92     * seconds, plus any stored entry that can no longer be decoded, returning
93     * how many were removed in total.
94     *
95     * Concrete here rather than per-backend: the expiry half is expressible
96     * purely in terms of all()/delete()/isExpired(), and four byte-identical
97     * copies would be four places for the rule to drift.
98     *
99     * @param  int $olderThanSeconds
100     * @return int
101     */
102    public function prune(int $olderThanSeconds): int
103    {
104        $removed = 0;
105
106        foreach ($this->all() as $id => $record) {
107            if ($this->isExpired($record, $olderThanSeconds)) {
108                $this->delete($id);
109                $removed++;
110            }
111        }
112
113        return $removed + $this->purgeUndecodable();
114    }
115
116    /**
117     * Remove stored entries that can no longer be decoded into a WorkerRecord,
118     * returning how many were removed.
119     *
120     * These are invisible to all() by design (it skips them so one corrupt
121     * entry cannot derail enumeration), which would otherwise make them
122     * permanently unreapable - a truncated write or a bad payload would leak
123     * for the lifetime of the store. Backends with real storage override this;
124     * the default is a no-op for backends that cannot hold an undecodable
125     * entry.
126     *
127     * @return int
128     */
129    protected function purgeUndecodable(): int
130    {
131        return 0;
132    }
133
134}