Passed
Push — master ( aae225...dd614b )
by Théo
07:41
created

ConsoleLogger::log()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 40
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 22
nc 3
nop 3
dl 0
loc 40
rs 9.568
c 1
b 0
f 0
1
<?php
2
/**
3
 * This code is licensed under the BSD 3-Clause License.
4
 *
5
 * Copyright (c) 2017, Maks Rafalko
6
 * All rights reserved.
7
 *
8
 * Redistribution and use in source and binary forms, with or without
9
 * modification, are permitted provided that the following conditions are met:
10
 *
11
 * * Redistributions of source code must retain the above copyright notice, this
12
 *   list of conditions and the following disclaimer.
13
 *
14
 * * Redistributions in binary form must reproduce the above copyright notice,
15
 *   this list of conditions and the following disclaimer in the documentation
16
 *   and/or other materials provided with the distribution.
17
 *
18
 * * Neither the name of the copyright holder nor the names of its
19
 *   contributors may be used to endorse or promote products derived from
20
 *   this software without specific prior written permission.
21
 *
22
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
 */
33
34
declare(strict_types=1);
35
36
namespace Infection\Logger;
37
38
use DateTime;
39
use DateTimeInterface;
40
use function get_class;
41
use Infection\Console\IO;
42
use function is_object;
43
use function is_scalar;
44
use function method_exists;
45
use Psr\Log\AbstractLogger;
46
use Psr\Log\LogLevel;
47
use function Safe\sprintf;
48
use function strpos;
49
use function strtr;
50
use Symfony\Component\Console\Output\OutputInterface;
51
use Webmozart\Assert\Assert;
52
53
/**
54
 * @internal
55
 */
56
final class ConsoleLogger extends AbstractLogger
57
{
58
    private const INFO = 'info';
59
    private const ERROR = 'error';
60
61
    private const VERBOSITY_LEVEL_MAP = [
62
        LogLevel::EMERGENCY => OutputInterface::VERBOSITY_NORMAL,
63
        LogLevel::ALERT => OutputInterface::VERBOSITY_NORMAL,
64
        LogLevel::CRITICAL => OutputInterface::VERBOSITY_NORMAL,
65
        LogLevel::ERROR => OutputInterface::VERBOSITY_NORMAL,
66
        LogLevel::WARNING => OutputInterface::VERBOSITY_NORMAL,
67
        LogLevel::NOTICE => OutputInterface::VERBOSITY_NORMAL,
68
        LogLevel::INFO => OutputInterface::VERBOSITY_VERBOSE,
69
        LogLevel::DEBUG => OutputInterface::VERBOSITY_DEBUG,
70
    ];
71
72
    private const FORMAT_LEVEL_MAP = [
73
        LogLevel::EMERGENCY => self::ERROR,
74
        LogLevel::ALERT => self::ERROR,
75
        LogLevel::CRITICAL => self::ERROR,
76
        LogLevel::ERROR => self::ERROR,
77
        LogLevel::WARNING => self::INFO,
78
        LogLevel::NOTICE => self::INFO,
79
        LogLevel::INFO => self::INFO,
80
        LogLevel::DEBUG => self::INFO,
81
    ];
82
83
    private const IO_MAP = [
84
        LogLevel::ERROR => 'error',
85
        LogLevel::WARNING => 'warning',
86
        LogLevel::NOTICE => 'note',
87
    ];
88
89
    private $io;
90
91
    public function __construct(IO $io)
92
    {
93
        $this->io = $io;
94
    }
95
96
    /**
97
     * @param string $level
98
     * @param string $message
99
     * @param mixed[] $context
100
     */
101
    public function log($level, $message, array $context = []): void
102
    {
103
        Assert::keyExists(
104
            self::VERBOSITY_LEVEL_MAP,
105
            $level,
106
            'The log level %s does not exist'
107
        );
108
109
        $output = $this->io->getOutput();
110
111
        // The if condition check isn't necessary per se – it's the same one that $output will do
112
        // internally anyway. We only do it for efficiency here as the message formatting is
113
        // relatively expensive
114
        if ($output->getVerbosity() < self::VERBOSITY_LEVEL_MAP[$level]) {
115
            return;
116
        }
117
118
        $interpolatedMessage = $this->interpolate($message, $context);
119
120
        if (!isset($context['block'])) {
121
            $output->writeln(
122
                sprintf(
123
                    '<%1$s>[%2$s] %3$s</%1$s>',
124
                    self::FORMAT_LEVEL_MAP[$level],
125
                    $level,
126
                    $interpolatedMessage
127
                ),
128
                self::VERBOSITY_LEVEL_MAP[$level]
129
            );
130
131
            return;
132
        }
133
134
        Assert::keyExists(
135
            self::IO_MAP,
136
            $level,
137
            'The log level "%s" does not exist for the IO mapping'
138
        );
139
140
        $this->io->{self::IO_MAP[$level]}($interpolatedMessage);
141
    }
142
143
    /**
144
     * Interpolates context values into the message placeholders.
145
     *
146
     * @param mixed[] $context
147
     *
148
     * @author PHP Framework Interoperability Group
149
     */
150
    private function interpolate(string $message, array $context): string
151
    {
152
        if (strpos($message, '{') === false) {
153
            return $message;
154
        }
155
156
        $replacements = [];
157
158
        foreach ($context as $key => $val) {
159
            if ($val === null
160
                || is_scalar($val)
161
                || (is_object($val) && method_exists($val, '__toString'))
162
            ) {
163
                $replacements["{{$key}}"] = $val;
164
            } elseif ($val instanceof DateTimeInterface) {
165
                $replacements["{{$key}}"] = $val->format(DateTime::RFC3339);
166
            } elseif (is_object($val)) {
167
                $replacements["{{$key}}"] = '[object ' . get_class($val) . ']';
168
            } else {
169
                $replacements["{{$key}}"] = '[' . gettype($val) . ']';
170
            }
171
        }
172
173
        return strtr($message, $replacements);
174
    }
175
}
176