Passed
Pull Request — master (#408)
by
unknown
03:22
created

CronManager   A

Complexity

Total Complexity 27

Size/Duplication

Total Lines 216
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 71
c 1
b 0
f 0
dl 0
loc 216
rs 10
wmc 27

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A loadTaskFromFile() 0 13 3
A loadTasks() 0 16 5
A runDueTasks() 0 13 3
A createTaskFromArray() 0 10 2
A getDefaultCronDirectory() 0 3 1
A getStats() 0 3 1
A runTask() 0 28 5
A getTasks() 0 3 1
A runTaskByName() 0 9 2
A log() 0 7 2
1
<?php
2
3
/**
4
 * Quantum PHP Framework
5
 *
6
 * An open source software development framework for PHP
7
 *
8
 * @package Quantum
9
 * @author Arman Ag. <[email protected]>
10
 * @copyright Copyright (c) 2018 Softberg LLC (https://softberg.org)
11
 * @link http://quantum.softberg.org/
12
 * @since 3.0.0
13
 */
14
15
namespace Quantum\Libraries\Cron;
16
17
use Quantum\Libraries\Cron\Contracts\CronTaskInterface;
18
use Quantum\Libraries\Logger\Factories\LoggerFactory;
19
use Quantum\Libraries\Cron\Exceptions\CronException;
20
use Quantum\Libraries\Logger\Logger;
21
22
/**
23
 * Class CronManager
24
 * @package Quantum\Libraries\Cron
25
 */
26
class CronManager
27
{
28
    /**
29
     * Loaded tasks
30
     * @var array<string, CronTaskInterface>
31
     */
32
    private $tasks = [];
33
34
    /**
35
     * Cron directory path
36
     * @var string
37
     */
38
    private $cronDirectory;
39
40
    /**
41
     * Execution statistics
42
     * @var array
43
     */
44
    private $stats = [
45
        'total' => 0,
46
        'executed' => 0,
47
        'skipped' => 0,
48
        'failed' => 0,
49
        'locked' => 0,
50
    ];
51
52
    /**
53
     * CronManager constructor
54
     * @param string|null $cronDirectory
55
     */
56
    public function __construct(?string $cronDirectory = null)
57
    {
58
        $configuredPath = $cronDirectory ?? cron_config('path');
59
        $this->cronDirectory = $configuredPath ?: $this->getDefaultCronDirectory();
60
    }
61
62
    /**
63
     * Load tasks from cron directory
64
     * @return void
65
     * @throws CronException
66
     */
67
    public function loadTasks(): void
68
    {
69
        if (!fs()->isDirectory($this->cronDirectory)) {
70
            if ($this->cronDirectory !== $this->getDefaultCronDirectory()) {
71
                throw CronException::cronDirectoryNotFound($this->cronDirectory);
72
            }
73
            return;
74
        }
75
76
        $files = fs()->glob($this->cronDirectory . DS . '*.php') ?: [];
77
78
        foreach ($files as $file) {
79
            $this->loadTaskFromFile($file);
80
        }
81
82
        $this->stats['total'] = count($this->tasks);
83
    }
84
85
    /**
86
     * Load a single task from file
87
     * @param string $file
88
     * @return void
89
     * @throws CronException
90
     */
91
    private function loadTaskFromFile(string $file): void
92
    {
93
        $task = fs()->require($file);
0 ignored issues
show
Unused Code introduced by
The call to Quantum\Libraries\Storage\FileSystem::require() has too many arguments starting with $file. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

93
        $task = fs()->/** @scrutinizer ignore-call */ require($file);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
94
95
        if (is_array($task)) {
96
            $task = $this->createTaskFromArray($task);
97
        }
98
99
        if (!$task instanceof CronTaskInterface) {
100
            throw CronException::invalidTaskFile($file);
101
        }
102
103
        $this->tasks[$task->getName()] = $task;
104
    }
105
106
    /**
107
     * Create task from array definition
108
     * @param array $definition
109
     * @return CronTask
110
     * @throws CronException
111
     */
112
    private function createTaskFromArray(array $definition): CronTask
113
    {
114
        if (!isset($definition['name'], $definition['expression'], $definition['callback'])) {
115
            throw new CronException('Task definition must contain name, expression, and callback');
116
        }
117
118
        return new CronTask(
119
            $definition['name'],
120
            $definition['expression'],
121
            $definition['callback']
122
        );
123
    }
124
125
    /**
126
     * Run all due tasks
127
     * @param bool $force Ignore locks
128
     * @return array Statistics
129
     */
130
    public function runDueTasks(bool $force = false): array
131
    {
132
        $this->loadTasks();
133
134
        foreach ($this->tasks as $task) {
135
            if ($task->shouldRun()) {
136
                $this->runTask($task, $force);
137
            } else {
138
                $this->stats['skipped']++;
139
            }
140
        }
141
142
        return $this->stats;
143
    }
144
145
    /**
146
     * Run a specific task by name
147
     * @param string $taskName
148
     * @param bool $force Ignore locks
149
     * @return void
150
     * @throws CronException
151
     */
152
    public function runTaskByName(string $taskName, bool $force = false): void
153
    {
154
        $this->loadTasks();
155
156
        if (!isset($this->tasks[$taskName])) {
157
            throw CronException::taskNotFound($taskName);
158
        }
159
160
        $this->runTask($this->tasks[$taskName], $force);
161
    }
162
163
    /**
164
     * Run a single task
165
     * @param CronTaskInterface $task
166
     * @param bool $force Ignore locks
167
     * @return void
168
     */
169
    private function runTask(CronTaskInterface $task, bool $force = false): void
170
    {
171
        $lock = new CronLock($task->getName());
172
173
        if (!$force && !$lock->acquire()) {
174
            $this->stats['locked']++;
175
            $this->log('warning', "Task \"{$task->getName()}\" skipped: locked");
176
            return;
177
        }
178
179
        $startTime = microtime(true);
180
        $this->log('info', "Task \"{$task->getName()}\" started");
181
182
        try {
183
            $task->handle();
184
            $duration = round(microtime(true) - $startTime, 2);
185
            $this->stats['executed']++;
186
            $this->log('info', "Task \"{$task->getName()}\" completed in {$duration}s");
187
        } catch (\Throwable $e) {
188
            $this->stats['failed']++;
189
            $this->log('error', "Task \"{$task->getName()}\" failed: " . $e->getMessage(), [
190
                'exception' => get_class($e),
191
                'file' => $e->getFile(),
192
                'line' => $e->getLine(),
193
            ]);
194
        } finally {
195
            if (!$force) {
196
                $lock->release();
197
            }
198
        }
199
    }
200
201
    /**
202
     * Get all loaded tasks
203
     * @return array<string, CronTaskInterface>
204
     */
205
    public function getTasks(): array
206
    {
207
        return $this->tasks;
208
    }
209
210
    /**
211
     * Get execution statistics
212
     * @return array
213
     */
214
    public function getStats(): array
215
    {
216
        return $this->stats;
217
    }
218
219
    /**
220
     * Get default cron directory
221
     * @return string
222
     */
223
    private function getDefaultCronDirectory(): string
224
    {
225
        return base_dir() . DS . 'cron';
226
    }
227
228
    /**
229
     * Log a message
230
     * @param string $level
231
     * @param string $message
232
     * @param array $context
233
     * @return void
234
     */
235
    private function log(string $level, string $message, array $context = []): void
236
    {
237
        try {
238
            $logger = LoggerFactory::get(Logger::SINGLE);
239
            $logger->log($level, '[CRON] ' . $message, $context);
240
        } catch (\Throwable $exception) {
241
            error_log(sprintf('[CRON] [%s] %s', strtoupper($level), $message));
242
        }
243
    }
244
}
245