Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.11% covered (success)
99.11%
111 / 112
96.15% covered (success)
96.15%
25 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
File
99.11% covered (success)
99.11%
111 / 112
96.15% covered (success)
96.15%
25 / 26
68
0.00% covered (danger)
0.00%
0 / 1
 __construct
83.33% covered (success)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 create
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFolder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 folder
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStart
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getEnd
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getStatus
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 push
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 pop
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 hasJobs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasFailedJob
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getFailedJob
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 hasFailedJobs
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFailedJobs
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 clearFailed
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 clear
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 schedule
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getTasks
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getTask
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 updateTask
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 removeTask
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getTaskCount
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasTasks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 clearTasks
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getFolders
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 getFiles
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2/**
3 * Pop PHP Framework (http://www.popphp.org/)
4 *
5 * @link       https://github.com/popphp/popphp-framework
6 * @author     Nick Sagona, III <dev@nolainteractive.com>
7 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
8 * @license    http://www.popphp.org/license     New BSD License
9 */
10
11/**
12 * @namespace
13 */
14namespace Pop\Queue\Adapter;
15
16use Pop\Queue\Process\AbstractJob;
17use Pop\Queue\Process\Task;
18
19/**
20 * File adapter class
21 *
22 * @category   Pop
23 * @package    Pop\Queue
24 * @author     Nick Sagona, III <dev@nolainteractive.com>
25 * @copyright  Copyright (c) 2009-2024 NOLA Interactive, LLC. (http://www.nolainteractive.com)
26 * @license    http://www.popphp.org/license     New BSD License
27 * @version    2.0.0
28 */
29class File extends AbstractTaskAdapter
30{
31
32    /**
33     * Folder
34     * @var ?string
35     */
36    protected ?string $folder = null;
37
38    /**
39     * Constructor
40     *
41     * Instantiate the file object
42     *
43     * @param  string  $folder
44     * @param  ?string $priority
45     * @throws Exception
46     */
47    public function __construct(string $folder, ?string $priority = null)
48    {
49        if (!file_exists($folder)) {
50            throw new Exception("Error: The folder '" . $folder . "' does not exist.");
51        }
52        if (!is_writable($folder)) {
53            throw new Exception("Error: The folder '" . $folder . "' is not writable.");
54        }
55
56        $this->folder = $folder;
57        parent::__construct($priority);
58    }
59
60    /**
61     * Create file adapter
62     *
63     * @param  string  $folder
64     * @param  ?string $priority
65     * @throws Exception
66     * @return File
67     */
68    public static function create(string $folder, ?string $priority = null): File
69    {
70        return new self($folder, $priority);
71    }
72
73    /**
74     * Get folder
75     *
76     * @return ?string
77     */
78    public function getFolder(): ?string
79    {
80        return $this->folder;
81    }
82
83    /**
84     * Get folder (alias)
85     *
86     * @return ?string
87     */
88    public function folder(): ?string
89    {
90        return $this->folder;
91    }
92
93    /**
94     * Get queue start index
95     *
96     * @return int
97     */
98    public function getStart(): int
99    {
100        $folders = $this->getFolders($this->folder);
101        return $folders[0] ?? 0;
102    }
103
104    /**
105     * Get queue end index
106     *
107     * @return int
108     */
109    public function getEnd(): int
110    {
111        $folders = $this->getFolders($this->folder);
112        return (!empty($folders)) ? end($folders) : 0;
113    }
114
115    /**
116     * Get queue job status
117     *
118     * @param  int $index
119     * @return int
120     */
121    public function getStatus(int $index): int
122    {
123        return (file_exists($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status')) ?
124            file_get_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status') : 0;
125    }
126
127    /**
128     * Push job on to queue
129     *
130     * @param  AbstractJob $job
131     * @return File
132     */
133    public function push(AbstractJob $job): File
134    {
135        $status = 1;
136        $index  = ($this->getEnd() + 1);
137
138        if ($job->hasFailed()) {
139            $status = 2;
140            if ($this->isFilo()) {
141                $index = ($this->getStart() - 1);
142            }
143        }
144
145        if ($job->isValid()) {
146            if (!file_exists($this->folder . DIRECTORY_SEPARATOR . $index)) {
147                mkdir($this->folder . DIRECTORY_SEPARATOR . $index);
148            }
149            file_put_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'payload', serialize(clone $job));
150            file_put_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status', $status);
151        }
152
153        return $this;
154    }
155
156    /**
157     * Pop job off of queue
158     *
159     * @return ?AbstractJob
160     */
161    public function pop(): ?AbstractJob
162    {
163        $job    = false;
164        $index  = ($this->isFifo()) ? $this->getStart() : $this->getEnd();
165        $status = $this->getStatus($index);
166
167        if ($status != 0) {
168            file_put_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status', 0);
169            if (file_exists($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'payload')) {
170                $job = file_get_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'payload');
171                unlink($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'payload');
172                if (file_exists($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status')) {
173                    unlink($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status');
174                }
175                rmdir($this->folder . DIRECTORY_SEPARATOR . $index);
176            }
177        }
178
179        return ($job !== false) ? unserialize($job) : null;
180    }
181
182    /**
183     * Check if adapter has jobs
184     *
185     * @return bool
186     */
187    public function hasJobs(): bool
188    {
189        return !empty($this->getFolders($this->folder));
190    }
191
192    /**
193     * Check if adapter has failed job
194     *
195     * @param  int $index
196     * @return bool
197     */
198    public function hasFailedJob(int $index): bool
199    {
200        return (file_exists($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status') &&
201            (file_get_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'status') == 2));
202    }
203
204    /**
205     * Get failed job
206     *
207     * @param  int $index
208     * @param  bool $unserialize
209     * @return mixed
210     */
211    public function getFailedJob(int $index, bool $unserialize = true): mixed
212    {
213        if (($this->hasFailedJob($index)) &&
214            file_exists($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'payload')) {
215            $payload = file_get_contents($this->folder . DIRECTORY_SEPARATOR . $index . DIRECTORY_SEPARATOR . 'payload');
216            return ($unserialize) ? unserialize($payload) : $payload;
217        } else {
218            return null;
219        }
220    }
221
222    /**
223     * Check if adapter has failed jobs
224     *
225     * @return bool
226     */
227    public function hasFailedJobs(): bool
228    {
229        return (count($this->getFailedJobs(false)) > 0);
230    }
231
232    /**
233     * Get adapter failed jobs
234     *
235     * @param  bool $unserialize
236     * @return array
237     */
238    public function getFailedJobs(bool $unserialize = true): array
239    {
240        $folders = $this->getFolders($this->folder);
241        $failed  = [];
242
243        foreach ($folders as $index) {
244            if ($this->hasFailedJob($index)) {
245                $failed[$index] = $this->getFailedJob($index);
246            }
247        }
248
249        return $failed;
250    }
251
252    /**
253     * Clear failed jobs out of the queue
254     *
255     * @return File
256     */
257    public function clearFailed(): File
258    {
259        $failed = $this->getFailedJobs(false);
260
261        foreach ($failed as $folder => $failedJob) {
262            if (file_exists($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'payload')) {
263                unlink($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'payload');
264            }
265            if (file_exists($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'status')) {
266                unlink($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'status');
267            }
268            rmdir($this->folder . DIRECTORY_SEPARATOR . $folder);
269        }
270        return $this;
271    }
272
273    /**
274     * Clear jobs out of queue
275     *
276     * @return File
277     */
278    public function clear(): File
279    {
280        $files   = $this->getFiles($this->folder);
281        $folders = $this->getFolders($this->folder);
282
283        foreach ($files as $file) {
284            if (file_exists($this->folder . DIRECTORY_SEPARATOR . $file)) {
285                unlink($this->folder . DIRECTORY_SEPARATOR . $file);
286            }
287        }
288
289        foreach ($folders as $folder) {
290            if (file_exists($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'payload')) {
291                unlink($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'payload');
292            }
293            if (file_exists($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'status')) {
294                unlink($this->folder . DIRECTORY_SEPARATOR . $folder . DIRECTORY_SEPARATOR . 'status');
295            }
296            rmdir($this->folder . DIRECTORY_SEPARATOR . $folder);
297        }
298
299        return $this;
300    }
301
302    /**
303     * Schedule job with queue
304     *
305     * @param  Task $task
306     * @return File
307     */
308    public function schedule(Task $task): File
309    {
310        if ($task->isValid()) {
311            file_put_contents(
312                $this->folder . DIRECTORY_SEPARATOR . 'task-' . $task->getJobId(), serialize(clone $task)
313            );
314        }
315        return $this;
316    }
317
318    /**
319     * Get scheduled tasks
320     *
321     * @return array
322     */
323    public function getTasks(): array
324    {
325        $files = $this->getFiles($this->folder);
326        $tasks = [];
327
328        foreach ($files as $file) {
329            if (str_starts_with($file, 'task-')) {
330                $tasks[] = substr($file, 5);
331            }
332        }
333
334        return $tasks;
335    }
336
337    /**
338     * Get scheduled task
339     *
340     * @param  string $taskId
341     * @return ?Task
342     */
343    public function getTask(string $taskId): ?Task
344    {
345        return (file_exists($this->folder . DIRECTORY_SEPARATOR . 'task-' . $taskId)) ?
346            unserialize(file_get_contents($this->folder . DIRECTORY_SEPARATOR . 'task-' . $taskId)) : null;
347    }
348
349    /**
350     * Update scheduled task
351     *
352     * @param  Task $task
353     * @return File
354     */
355    public function updateTask(Task $task): File
356    {
357        if ($task->isValid()) {
358            file_put_contents(
359                $this->folder . DIRECTORY_SEPARATOR . 'task-' . $task->getJobId(), serialize(clone $task)
360            );
361        } else {
362            $this->removeTask($task->getJobId());
363        }
364
365        return $this;
366    }
367
368    /**
369     * Remove scheduled task
370     *
371     * @param  string $taskId
372     * @return File
373     */
374    public function removeTask(string $taskId): File
375    {
376        if (file_exists($this->folder . DIRECTORY_SEPARATOR . 'task-' . $taskId)) {
377            unlink($this->folder . DIRECTORY_SEPARATOR . 'task-' . $taskId);
378        }
379        return $this;
380    }
381
382    /**
383     * Get scheduled tasks count
384     *
385     * @return int
386     */
387    public function getTaskCount(): int
388    {
389        return count($this->getTasks());
390    }
391
392    /**
393     * Has scheduled tasks
394     *
395     * @return bool
396     */
397    public function hasTasks(): bool
398    {
399        return ($this->getTaskCount() > 0);
400    }
401
402    /**
403     * Clear all scheduled task
404     *
405     * @return File
406     */
407    public function clearTasks(): File
408    {
409        $tasks = $this->getTasks();
410
411        foreach ($tasks as $taskId) {
412            $this->removeTask($taskId);
413        }
414
415        return $this;
416    }
417
418    /**
419     * Get folders
420     *
421     * @param  string $folder
422     * @return array
423     */
424    public function getFolders(string $folder): array
425    {
426        if (is_dir($folder)) {
427            return array_values(array_filter(scandir($folder), function($value) use ($folder) {
428                return (($value != '.') && ($value != '..') && ($value != '.empty') && is_dir($folder . '/' . $value));
429            }));
430        } else {
431            return [];
432        }
433    }
434
435    /**
436     * Get files from folder
437     *
438     * @param  string $folder
439     * @return array
440     */
441    public function getFiles(string $folder): array
442    {
443        if (is_dir($folder)) {
444            return array_values(array_filter(scandir($folder), function($value) use ($folder) {
445                return (($value != '.') && ($value != '..') && ($value != '.empty') && !is_dir($folder . '/' . $value));
446            }));
447        } else {
448            return [];
449        }
450    }
451
452}