Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 148 | Details | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php declare(strict_types=1);
2
 
3
/*
4
 * This file is part of the Monolog package.
5
 *
6
 * (c) Jordi Boggiano <j.boggiano@seld.be>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
 
12
namespace Monolog\Handler;
13
 
14
use Monolog\Formatter\FormatterInterface;
15
use Monolog\Formatter\LineFormatter;
16
use Monolog\Utils;
621 lars 17
use Monolog\Logger;
148 lars 18
 
19
use function count;
20
use function headers_list;
21
use function stripos;
22
use function trigger_error;
23
 
24
use const E_USER_DEPRECATED;
25
 
26
/**
27
 * Handler sending logs to browser's javascript console with no browser extension required
28
 *
29
 * @author Olivier Poitrey <rs@dailymotion.com>
30
 *
31
 * @phpstan-import-type FormattedRecord from AbstractProcessingHandler
32
 */
33
class BrowserConsoleHandler extends AbstractProcessingHandler
34
{
35
    /** @var bool */
36
    protected static $initialized = false;
37
    /** @var FormattedRecord[] */
38
    protected static $records = [];
39
 
40
    protected const FORMAT_HTML = 'html';
41
    protected const FORMAT_JS = 'js';
42
    protected const FORMAT_UNKNOWN = 'unknown';
43
 
44
    /**
45
     * {@inheritDoc}
46
     *
47
     * Formatted output may contain some formatting markers to be transferred to `console.log` using the %c format.
48
     *
49
     * Example of formatted string:
50
     *
51
     *     You can do [[blue text]]{color: blue} or [[green background]]{background-color: green; color: white}
52
     */
53
    protected function getDefaultFormatter(): FormatterInterface
54
    {
55
        return new LineFormatter('[[%channel%]]{macro: autolabel} [[%level_name%]]{font-weight: bold} %message%');
56
    }
57
 
58
    /**
59
     * {@inheritDoc}
60
     */
61
    protected function write(array $record): void
62
    {
63
        // Accumulate records
64
        static::$records[] = $record;
65
 
66
        // Register shutdown handler if not already done
67
        if (!static::$initialized) {
68
            static::$initialized = true;
69
            $this->registerShutdownFunction();
70
        }
71
    }
72
 
73
    /**
74
     * Convert records to javascript console commands and send it to the browser.
75
     * This method is automatically called on PHP shutdown if output is HTML or Javascript.
76
     */
77
    public static function send(): void
78
    {
79
        $format = static::getResponseFormat();
80
        if ($format === self::FORMAT_UNKNOWN) {
81
            return;
82
        }
83
 
84
        if (count(static::$records)) {
85
            if ($format === self::FORMAT_HTML) {
86
                static::writeOutput('<script>' . static::generateScript() . '</script>');
87
            } elseif ($format === self::FORMAT_JS) {
88
                static::writeOutput(static::generateScript());
89
            }
90
            static::resetStatic();
91
        }
92
    }
93
 
94
    public function close(): void
95
    {
96
        self::resetStatic();
97
    }
98
 
99
    public function reset()
100
    {
101
        parent::reset();
102
 
103
        self::resetStatic();
104
    }
105
 
106
    /**
107
     * Forget all logged records
108
     */
109
    public static function resetStatic(): void
110
    {
111
        static::$records = [];
112
    }
113
 
114
    /**
115
     * Wrapper for register_shutdown_function to allow overriding
116
     */
117
    protected function registerShutdownFunction(): void
118
    {
119
        if (PHP_SAPI !== 'cli') {
120
            register_shutdown_function(['Monolog\Handler\BrowserConsoleHandler', 'send']);
121
        }
122
    }
123
 
124
    /**
125
     * Wrapper for echo to allow overriding
126
     */
127
    protected static function writeOutput(string $str): void
128
    {
129
        echo $str;
130
    }
131
 
132
    /**
133
     * Checks the format of the response
134
     *
135
     * If Content-Type is set to application/javascript or text/javascript -> js
136
     * If Content-Type is set to text/html, or is unset -> html
137
     * If Content-Type is anything else -> unknown
138
     *
139
     * @return string One of 'js', 'html' or 'unknown'
140
     * @phpstan-return self::FORMAT_*
141
     */
142
    protected static function getResponseFormat(): string
143
    {
144
        // Check content type
145
        foreach (headers_list() as $header) {
146
            if (stripos($header, 'content-type:') === 0) {
147
                return static::getResponseFormatFromContentType($header);
148
            }
149
        }
150
 
151
        return self::FORMAT_HTML;
152
    }
153
 
154
    /**
155
     * @return string One of 'js', 'html' or 'unknown'
156
     * @phpstan-return self::FORMAT_*
157
     */
158
    protected static function getResponseFormatFromContentType(string $contentType): string
159
    {
160
        // This handler only works with HTML and javascript outputs
161
        // text/javascript is obsolete in favour of application/javascript, but still used
162
        if (stripos($contentType, 'application/javascript') !== false || stripos($contentType, 'text/javascript') !== false) {
163
            return self::FORMAT_JS;
164
        }
165
 
166
        if (stripos($contentType, 'text/html') !== false) {
167
            return self::FORMAT_HTML;
168
        }
169
 
170
        return self::FORMAT_UNKNOWN;
171
    }
172
 
173
    private static function generateScript(): string
174
    {
175
        $script = [];
176
        foreach (static::$records as $record) {
177
            $context = static::dump('Context', $record['context']);
178
            $extra = static::dump('Extra', $record['extra']);
179
 
180
            if (empty($context) && empty($extra)) {
621 lars 181
                $script[] = static::call_array(static::getConsoleMethodForLevel($record['level']), static::handleStyles($record['formatted']));
148 lars 182
            } else {
183
                $script = array_merge(
184
                    $script,
185
                    [static::call_array('groupCollapsed', static::handleStyles($record['formatted']))],
186
                    $context,
187
                    $extra,
188
                    [static::call('groupEnd')]
189
                );
190
            }
191
        }
192
 
193
        return "(function (c) {if (c && c.groupCollapsed) {\n" . implode("\n", $script) . "\n}})(console);";
194
    }
195
 
621 lars 196
    private static function getConsoleMethodForLevel(int $level): string
197
    {
198
        return [
199
            Logger::DEBUG => 'debug',
200
            Logger::INFO => 'info',
201
            Logger::NOTICE => 'info',
202
            Logger::WARNING => 'warn',
203
            Logger::ERROR => 'error',
204
            Logger::CRITICAL => 'error',
205
            Logger::ALERT => 'error',
206
            Logger::EMERGENCY => 'error',
207
        ][$level] ?? 'log';
208
    }
209
 
148 lars 210
    /**
211
     * @return string[]
212
     */
213
    private static function handleStyles(string $formatted): array
214
    {
215
        $args = [];
216
        $format = '%c' . $formatted;
217
        preg_match_all('/\[\[(.*?)\]\]\{([^}]*)\}/s', $format, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
218
 
219
        foreach (array_reverse($matches) as $match) {
220
            $args[] = '"font-weight: normal"';
221
            $args[] = static::quote(static::handleCustomStyles($match[2][0], $match[1][0]));
222
 
223
            $pos = $match[0][1];
224
            $format = Utils::substr($format, 0, $pos) . '%c' . $match[1][0] . '%c' . Utils::substr($format, $pos + strlen($match[0][0]));
225
        }
226
 
227
        $args[] = static::quote('font-weight: normal');
228
        $args[] = static::quote($format);
229
 
230
        return array_reverse($args);
231
    }
232
 
233
    private static function handleCustomStyles(string $style, string $string): string
234
    {
235
        static $colors = ['blue', 'green', 'red', 'magenta', 'orange', 'black', 'grey'];
236
        static $labels = [];
237
 
238
        $style = preg_replace_callback('/macro\s*:(.*?)(?:;|$)/', function (array $m) use ($string, &$colors, &$labels) {
239
            if (trim($m[1]) === 'autolabel') {
240
                // Format the string as a label with consistent auto assigned background color
241
                if (!isset($labels[$string])) {
242
                    $labels[$string] = $colors[count($labels) % count($colors)];
243
                }
244
                $color = $labels[$string];
245
 
246
                return "background-color: $color; color: white; border-radius: 3px; padding: 0 2px 0 2px";
247
            }
248
 
249
            return $m[1];
250
        }, $style);
251
 
252
        if (null === $style) {
253
            $pcreErrorCode = preg_last_error();
254
            throw new \RuntimeException('Failed to run preg_replace_callback: ' . $pcreErrorCode . ' / ' . Utils::pcreLastErrorMessage($pcreErrorCode));
255
        }
256
 
257
        return $style;
258
    }
259
 
260
    /**
261
     * @param  mixed[] $dict
262
     * @return mixed[]
263
     */
264
    private static function dump(string $title, array $dict): array
265
    {
266
        $script = [];
267
        $dict = array_filter($dict);
268
        if (empty($dict)) {
269
            return $script;
270
        }
271
        $script[] = static::call('log', static::quote('%c%s'), static::quote('font-weight: bold'), static::quote($title));
272
        foreach ($dict as $key => $value) {
273
            $value = json_encode($value);
274
            if (empty($value)) {
275
                $value = static::quote('');
276
            }
277
            $script[] = static::call('log', static::quote('%s: %o'), static::quote((string) $key), $value);
278
        }
279
 
280
        return $script;
281
    }
282
 
283
    private static function quote(string $arg): string
284
    {
285
        return '"' . addcslashes($arg, "\"\n\\") . '"';
286
    }
287
 
288
    /**
289
     * @param mixed $args
290
     */
291
    private static function call(...$args): string
292
    {
293
        $method = array_shift($args);
294
        if (!is_string($method)) {
295
            throw new \UnexpectedValueException('Expected the first arg to be a string, got: '.var_export($method, true));
296
        }
297
 
298
        return static::call_array($method, $args);
299
    }
300
 
301
    /**
302
     * @param mixed[] $args
303
     */
304
    private static function call_array(string $method, array $args): string
305
    {
306
        return 'c.' . $method . '(' . implode(', ', $args) . ');';
307
    }
308
}