TaskLogger::setMinLevel()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 3
ccs 3
cts 3
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Todd Burry <[email protected]>
4
 * @copyright 2009-2015 Vanilla Forums Inc.
5
 * @license MIT
6
 */
7
8
namespace Garden\Cli;
9
10
use Psr\Log\InvalidArgumentException;
11
use Psr\Log\LoggerInterface;
12
use Psr\Log\LoggerTrait;
13
use Psr\Log\LogLevel;
14
15
/**
16
 * A helper class to format CLI output in a log-style format.
17
 */
18
class TaskLogger implements LoggerInterface {
19
    use LoggerTrait;
20
21
    const FIELD_INDENT = '_indent';
22
    const FIELD_TIME = '_time';
23
    const FIELD_BEGIN = '_begin';
24
    const FIELD_END = '_end';
25
    const FIELD_DURATION = '_duration';
26
    const FIELD_LEVEL = '_level';
27
28
    private static $levels = [
29
        LogLevel::DEBUG,
30
        LogLevel::INFO,
31
        LogLevel::NOTICE,
32
        LogLevel::WARNING,
33
        LogLevel::ERROR,
34
        LogLevel::CRITICAL,
35
        LogLevel::ALERT,
36
        LogLevel::EMERGENCY,
37
    ];
38
    /**
39
     * @var int The minimum level deep to output.
40
     */
41
    private $minLevel = LogLevel::INFO;
42
    /**
43
     * @var array An array of currently running tasks.
44
     */
45
    private $taskStack = [];
46
    /**
47
     * @var LoggerInterface The logger to ultimately log the information to.
48
     */
49
    private $logger;
50
51
    /**
52
     * TaskLogger constructor.
53
     *
54
     * @param LoggerInterface $logger The logger to ultimately log the information to.
55
     * @param string $minLevel The minimum error level that will be logged. One of the **LogLevel** constants.
56
     */
57 36
    public function __construct(LoggerInterface $logger = null, $minLevel = LogLevel::INFO) {
58 36
        if ($logger === null) {
59
            $logger = new StreamLogger();
60
        }
61 36
        $this->logger = $logger;
62 36
        $this->setMinLevel($minLevel);
63 36
    }
64
65
    /**
66
     * Output a debug message that designates the beginning of a task.
67
     *
68
     * @param string $message The message to output.
69
     * @param array $context Context variables to pass to the message.
70
     * @return $this
71
     */
72 3
    public function beginDebug(string $message, array $context = []) {
73 3
        return $this->begin(LogLevel::DEBUG, $message, $context);
74
    }
75
76
    /**
77
     * Output a message that designates the beginning of a task.
78
     *
79
     * @param string $level
80
     * @param string $message The message to output.
81
     * @param array $context The log context.
82
     * @return $this
83
     */
84 18
    public function begin(string $level, string $message, array $context = []) {
85 18
        $output = $this->compareLevel($level, $this->getMinLevel()) >= 0;
86 17
        $context = [self::FIELD_BEGIN => true] + $context + [self::FIELD_TIME => microtime(true)];
87 17
        $task = [$level, $message, $context, $output];
88
89 17
        if ($output) {
90 15
            $this->log($level, $message, $context);
91
        }
92
93 17
        array_push($this->taskStack, $task);
94
95 17
        return $this;
96
    }
97
98
    /**
99
     * Compare two log levels.
100
     *
101
     * @param string $a The first log level to compare.
102
     * @param string $b The second log level to compare.
103
     * @return int Returns -1, 0, or 1.
104
     */
105 35
    private function compareLevel(string $a, string $b): int {
106 35
        $i = array_search($a, static::$levels);
0 ignored issues
show
Bug introduced by
Since $levels is declared private, accessing it with static will lead to errors in possible sub-classes; you can either use self, or increase the visibility of $levels to at least protected.
Loading history...
107 35
        if ($i === false) {
108 3
            throw new InvalidArgumentException("Log level is invalid: $a", 500);
109
        }
110
111 32
        return $i <=> array_search($b, static::$levels);
112
    }
113
114
    /**
115
     * Get the maxLevel.
116
     *
117
     * @return string Returns the maxLevel.
118
     */
119 35
    public function getMinLevel(): string {
120 35
        return $this->minLevel;
121
    }
122
123
    /**
124
     * @param string $minLevel
125
     * @return $this
126
     */
127 36
    public function setMinLevel(string $minLevel) {
128 36
        $this->minLevel = $minLevel;
0 ignored issues
show
Documentation Bug introduced by
The property $minLevel was declared of type integer, but $minLevel is of type string. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
129 36
        return $this;
130
    }
131
132
    /**
133
     * Logs with an arbitrary level.
134
     *
135
     * @param mixed $level
136
     * @param string $message
137
     * @param array $context
138
     *
139
     * @return void
140
     */
141 32
    public function log($level, $message, array $context = []) {
142 32
        if ($this->compareLevel($level, $this->getMinLevel()) >= 0) {
143 30
            $this->outputTaskStack();
144
145
            // Increase the level begin tasks if this level is higher.
146 30
            foreach ($this->taskStack as &$task) {
147 4
                if ($this->compareLevel($level, $task[0]) > 0) {
148 4
                    $task[0] = $level;
149
                }
150
            }
151
152 30
            $this->logInternal($level, $message, [self::FIELD_INDENT => $this->currentIndent()] + $context);
153
        }
154 31
    }
155
156
    /**
157
     * Output the task stack.
158
     */
159 30
    private function outputTaskStack() {
160 30
        foreach ($this->taskStack as $indent => &$task) {
161 4
            list($taskLevel, $taskMessage, $taskContext, $taskOutput) = $task;
162 4
            if (!$taskOutput) {
163 2
                $this->logInternal($taskLevel, $taskMessage, [self::FIELD_INDENT => $indent] + $taskContext);
164
165 4
                $task[3] = true; // mark task as outputted
166
            }
167
        }
168 30
    }
169
170
    /**
171
     * Internal log implementation with less error checking.
172
     *
173
     * @param string $level The log level.
174
     * @param string $message The log message.
175
     * @param array $context The log context.
176
     */
177 31
    private function logInternal(string $level, string $message, array $context = []) {
178 31
        $context = $context + [self::FIELD_INDENT => $this->currentIndent(), self::FIELD_TIME => microtime(true)];
179 31
        $this->logger->log($level, $message, $context);
180 31
    }
181
182
    /**
183
     * Get the current depth of tasks.
184
     *
185
     * @return int Returns the current level.
186
     */
187 32
    private function currentIndent() {
188 32
        return count($this->taskStack);
189
    }
190
191
    /**
192
     * Output an info message that designates the beginning of a task.
193
     *
194
     * @param string $message The message to output.
195
     * @param array $context Context variables to pass to the message.
196
     * @return $this
197
     */
198 2
    public function beginInfo(string $message, array $context = []) {
199 2
        return $this->begin(LogLevel::INFO, $message, $context);
200
    }
201
202
    /**
203
     * Output a notice message that designates the beginning of a task.
204
     *
205
     * @param string $message The message to output.
206
     * @param array $context Context variables to pass to the message.
207
     * @return $this
208
     */
209 2
    public function beginNotice(string $message, array $context = []) {
210 2
        return $this->begin(LogLevel::NOTICE, $message, $context);
211
    }
212
213
    /**
214
     * Output a warning message that designates the beginning of a task.
215
     *
216
     * @param string $message The message to output.
217
     * @param array $context Context variables to pass to the message.
218
     * @return $this
219
     */
220 1
    public function beginWarning(string $message, array $context = []) {
221 1
        return $this->begin(LogLevel::WARNING, $message, $context);
222
    }
223
224
    /**
225
     * Output an error message that designates the beginning of a task.
226
     *
227
     * @param string $message The message to output.
228
     * @param array $context Context variables to pass to the message.
229
     * @return $this
230
     */
231 1
    public function beginError(string $message, array $context = []) {
232 1
        return $this->begin(LogLevel::ERROR, $message, $context);
233
    }
234
235
    /**
236
     * Output a critical message that designates the beginning of a task.
237
     *
238
     * @param string $message The message to output.
239
     * @param array $context Context variables to pass to the message.
240
     * @return $this
241
     */
242 1
    public function beginCritical(string $message, array $context = []) {
243 1
        return $this->begin(LogLevel::CRITICAL, $message, $context);
244
    }
245
246
    /**
247
     * Output an alert message that designates the beginning of a task.
248
     *
249
     * @param string $message The message to output.
250
     * @param array $context Context variables to pass to the message.
251
     * @return $this
252
     */
253 1
    public function beginAlert(string $message, array $context = []) {
254 1
        return $this->begin(LogLevel::ALERT, $message, $context);
255
    }
256
257
    /**
258
     * Output an emergency message that designates the beginning of a task.
259
     *
260
     * @param string $message The message to output.
261
     * @param array $context Context variables to pass to the message.
262
     * @return $this
263
     */
264 1
    public function beginEmergency(string $message, array $context = []) {
265 1
        return $this->begin(LogLevel::EMERGENCY, $message, $context);
266
    }
267
268
    /**
269
     * Output a message that ends a task with an HTTP status code.
270
     *
271
     * This method is useful if you are making a call to an external API as a task. You can end the task by passing the
272
     * response code to this message.
273
     *
274
     * @param int $httpStatus The HTTP status code that represents the completion of a task.
275
     * @return $this
276
     */
277 4
    public function endHttpStatus(int $httpStatus) {
278 4
        $statusStr = sprintf('%03d', $httpStatus);
279
280 4
        if ($httpStatus == 0 || $httpStatus >= 500) {
281 2
            $this->end($statusStr, [self::FIELD_LEVEL => LogLevel::CRITICAL]);
282 2
        } elseif ($httpStatus >= 400) {
283 1
            $this->endError($statusStr);
284
        } else {
285 1
            $this->end($statusStr);
286
        }
287
288 4
        return $this;
289
    }
290
291
    /**
292
     * Output a message that designates a task being completed.
293
     *
294
     * @param string $message The message to output.
295
     * @param array $context Context for the log. There are a few special fields that can be given with the context.
296
     *
297
     * - **TaskLogger::FIELD_TIME**: Specify a specific timestamp for the log.
298
     * - **TaskLogger::FIELD_LEVEL**: Override the level of the end log. Otherwise the level of the beg log is used.
299
     *
300
     * @return $this Returns `$this` for fluent calls.
301
     */
302 11
    public function end(string $message = '', array $context = []) {
303 11
        $context = [self::FIELD_INDENT => $this->currentIndent() - 1, self::FIELD_END => true] + $context + [self::FIELD_TIME => microtime(true)];
304 11
        $level = $context[self::FIELD_LEVEL] ?? null;
305
306
        // Find the task we are finishing.
307 11
        $task = end($this->taskStack);
308 11
        if ($task !== false) {
309 9
            list($taskLevel, $taskMessage, $taskContext, $taskOutput) = $task;
310 9
            $context[self::FIELD_DURATION] = $context[self::FIELD_TIME] - $taskContext[self::FIELD_TIME];
311 9
            $level = $level ?: $taskLevel;
312
        } else {
313 2
            trigger_error('Called TaskLogger::end() without calling TaskFormatter::begin()', E_USER_NOTICE);
314
        }
315 11
        $level = $level ?: LogLevel::INFO;
316
317 11
        $output = $this->compareLevel($level, $this->getMinLevel()) >= 0;
318 10
        if ($output) {
319 10
            $this->logInternal($level, $message, $context);
320
        }
321 10
        array_pop($this->taskStack);
322
323 10
        return $this;
324
    }
325
326
    /**
327
     * Output a message that represents a task being completed in an error.
328
     *
329
     * When formatting is turned on, error messages are output in red.
330
     *
331
     * @param string $message The message to output.
332
     * @param array $context The log context.
333
     * @return $this
334
     */
335 1
    public function endError(string $message, array $context = []) {
336 1
        return $this->end($message, [self::FIELD_LEVEL => LogLevel::ERROR] + $context);
337
    }
338
}
339