Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
13 / 13
93.75% covered (success)
93.75%
15 / 16
58.33% covered (warning)
58.33%
7 / 12
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Log
100.00% covered (success)
100.00%
13 / 13
93.75% covered (success)
93.75%
15 / 16
58.33% covered (warning)
58.33%
7 / 12
100.00% covered (success)
100.00%
4 / 4
12.63
100.00% covered (success)
100.00%
1 / 1
 write
100.00% covered (success)
100.00%
3 / 3
87.50% covered (warning)
87.50%
7 / 8
50.00% covered (danger)
50.00%
3 / 6
100.00% covered (success)
100.00%
1 / 1
4.12
 attempt
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 request
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 replacements
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
4 / 4
33.33% covered (danger)
33.33%
1 / 3
100.00% covered (success)
100.00%
1 / 1
3.19
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Core\Error;
6
7use Psr\Http\Message\ServerRequestInterface as Request;
8use Psr\Log\LoggerInterface as Logger;
9use Throwable;
10
11/**
12 * Writes the records of the app and its error handler, each about an
13 * exception. Without a logger, or when the logger fails, a record goes to
14 * PHP's error log, with the exception's trace and previous exceptions: the
15 * failures they report were caught, so PHP would never report them, and a
16 * logger that cannot write must not keep a request from being answered.
17 *
18 * @internal
19 */
20final class Log
21{
22    /** @param array<string, string|int> $values Placeholder values */
23    public static function write(
24        ?Logger $logger,
25        string|int $level,
26        string $message,
27        Throwable $exception,
28        array $values = [],
29    ): void {
30        if ($logger !== null && self::attempt($logger, $level, $message, $exception, $values)) {
31            return;
32        }
33
34        error_log(strtr($message, self::replacements($values)) . ': ' . (string) $exception);
35    }
36
37    /**
38     * Logs the record and tells whether that worked. A failing logger is
39     * reported to PHP's error log.
40     *
41     * @param array<string, string|int> $values Placeholder values
42     */
43    public static function attempt(
44        Logger $logger,
45        string|int $level,
46        string $message,
47        Throwable $exception,
48        array $values = [],
49    ): bool {
50        try {
51            $logger->log($level, $message, ['exception' => $exception, ...$values]);
52
53            return true;
54        } catch (Throwable $e) {
55            error_log('Logging failed: ' . (string) $e);
56
57            return false;
58        }
59    }
60
61    /**
62     * The request's method and path, for the placeholders of a record. The
63     * path only: query strings can carry tokens.
64     *
65     * @return array{method: string, path: string}
66     */
67    public static function request(Request $request): array
68    {
69        return ['method' => $request->getMethod(), 'path' => $request->getUri()->getPath()];
70    }
71
72    /**
73     * @param array<string, string|int> $values
74     * @return array<string, string>
75     */
76    private static function replacements(array $values): array
77    {
78        $replacements = [];
79
80        foreach ($values as $key => $value) {
81            $replacements['{' . $key . '}'] = (string) $value;
82        }
83
84        return $replacements;
85    }
86}