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

CronManager::getTasks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
21
/**
22
 * Class CronManager
23
 * @package Quantum\Libraries\Cron
24
 */
25
class CronManager
26
{
27
    /**
28
     * Loaded tasks
29
     * @var array<string, CronTaskInterface>
30
     */
31
    private $tasks = [];
32
33
    /**
34
     * Cron directory path
35
     * @var string
36
     */
37
    private $cronDirectory;
38
39
    /**
40
     * Execution statistics
41
     * @var array
42
     */
43
    private $stats = [
44
        'total' => 0,
45
        'executed' => 0,
46
        'skipped' => 0,
47
        'failed' => 0,
48
        'locked' => 0,
49
    ];
50
51
    /**
52
     * CronManager constructor
53
     * @param string|null $cronDirectory
54
     */
55
    public function __construct(?string $cronDirectory = null)
56
    {
57
        $configuredPath = $cronDirectory ?? cron_config('path');
58
        $this->cronDirectory = $configuredPath ?: $this->getDefaultCronDirectory();
59
    }
60
61
    /**
62
     * Load tasks from cron directory
63
     * @return void
64
     * @throws CronException
65
     */
66
    public function loadTasks(): void
67
    {
68
        if (!fs()->isDirectory($this->cronDirectory)) {
69
            if ($this->cronDirectory !== $this->getDefaultCronDirectory()) {
70
                throw CronException::cronDirectoryNotFound($this->cronDirectory);
71
            }
72
            return;
73
        }
74
75
        $files = fs()->glob($this->cronDirectory . DS . '*.php') ?: [];
76
77
        foreach ($files as $file) {
78
            $this->loadTaskFromFile($file);
79
        }
80
81
        $this->stats['total'] = count($this->tasks);
82
    }
83
84
    /**
85
     * Load a single task from file
86
     * @param string $file
87
     * @return void
88
     * @throws CronException
89
     */
90
    private function loadTaskFromFile(string $file): void
91
    {
92
        $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

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