Passed
Pull Request — master (#1729)
by
unknown
03:19
created

LogManager::extend()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 5
rs 10
ccs 3
cts 3
cp 1
crap 1
1
<?php
2
3
/*
4
 * This file is part of the overtrue/wechat.
5
 *
6
 * (c) overtrue <[email protected]>
7
 *
8
 * This source file is subject to the MIT license that is bundled
9
 * with this source code in the file LICENSE.
10
 */
11
12
namespace EasyWeChat\Kernel\Log;
13
14
use EasyWeChat\Kernel\ServiceContainer;
15
use Psr\Log\LoggerInterface;
16
use InvalidArgumentException;
17
use Monolog\Logger as Monolog;
18
use Monolog\Handler\StreamHandler;
19
use Monolog\Handler\SyslogHandler;
20
use Monolog\Formatter\LineFormatter;
21
use Monolog\Handler\ErrorLogHandler;
22
use Monolog\Handler\HandlerInterface;
23
use Monolog\Handler\RotatingFileHandler;
24
use Monolog\Handler\SlackWebhookHandler;
25
use Monolog\Handler\WhatFailureGroupHandler;
26
27
/**
28
 * Class LogManager.
29
 *
30
 * @author overtrue <[email protected]>
31
 */
32
class LogManager implements LoggerInterface
33
{
34
    /**
35
     * @var \EasyWeChat\Kernel\ServiceContainer
36
     */
37
    protected $app;
38
39
    /**
40
     * The array of resolved channels.
41
     *
42
     * @var array
43
     */
44
    protected $channels = [];
45
46
    /**
47
     * The registered custom driver creators.
48
     *
49
     * @var array
50
     */
51
    protected $customCreators = [];
52
53
    /**
54
     * The Log levels.
55
     *
56
     * @var array
57
     */
58
    protected $levels = [
59
        'debug' => Monolog::DEBUG,
60
        'info' => Monolog::INFO,
61
        'notice' => Monolog::NOTICE,
62
        'warning' => Monolog::WARNING,
63
        'error' => Monolog::ERROR,
64
        'critical' => Monolog::CRITICAL,
65
        'alert' => Monolog::ALERT,
66
        'emergency' => Monolog::EMERGENCY,
67
    ];
68
69
    /**
70
     * LogManager constructor.
71
     *
72
     * @param \EasyWeChat\Kernel\ServiceContainer $app
73
     */
74 21
    public function __construct(ServiceContainer $app)
75
    {
76 21
        $this->app = $app;
77 21
    }
78
79
    /**
80
     * Create a new, on-demand aggregate logger instance.
81
     *
82
     * @param array       $channels
83
     * @param string|null $channel
84
     *
85
     * @return \Psr\Log\LoggerInterface
86
     *
87
     * @throws \Exception
88
     */
89 1
    public function stack(array $channels, $channel = null)
90
    {
91 1
        return $this->createStackDriver(compact('channels', 'channel'));
92
    }
93
94
    /**
95
     * Get a log channel instance.
96
     *
97
     * @param string|null $channel
98
     *
99
     * @return mixed
100
     *
101
     * @throws \Exception
102
     */
103 2
    public function channel($channel = null)
104
    {
105 2
        return $this->driver($channel);
106
    }
107
108
    /**
109
     * Get a log driver instance.
110
     *
111
     * @param string|null $driver
112
     *
113
     * @return mixed
114
     *
115
     * @throws \Exception
116
     */
117 8
    public function driver($driver = null)
118
    {
119 8
        return $this->get($driver ?? $this->getDefaultDriver());
120
    }
121
122
    /**
123
     * Attempt to get the log from the local cache.
124
     *
125
     * @param string $name
126
     *
127
     * @return \Psr\Log\LoggerInterface
128
     *
129
     * @throws \Exception
130
     */
131 8
    protected function get($name)
132
    {
133
        try {
134 8
            return $this->channels[$name] ?? ($this->channels[$name] = $this->resolve($name));
135 4
        } catch (\Throwable $e) {
136 4
            $logger = $this->createEmergencyLogger();
137
138 4
            $logger->emergency('Unable to create configured logger. Using emergency logger.', [
139 4
                'exception' => $e,
140
            ]);
141
142 4
            return $logger;
143
        }
144
    }
145
146
    /**
147
     * Resolve the given log instance by name.
148
     *
149
     * @param string $name
150
     *
151
     * @return \Psr\Log\LoggerInterface
152
     *
153
     * @throws InvalidArgumentException
154
     */
155 8
    protected function resolve($name)
156
    {
157 8
        $config = $this->app['config']->get(\sprintf('log.channels.%s', $name));
158
159 8
        if (is_null($config)) {
160 2
            throw new InvalidArgumentException(\sprintf('Log [%s] is not defined.', $name));
161
        }
162
163 6
        if (isset($this->customCreators[$config['driver']])) {
164 1
            return $this->callCustomCreator($config);
165
        }
166
167 5
        $driverMethod = 'create' . ucfirst($config['driver']) . 'Driver';
168
169 5
        if (method_exists($this, $driverMethod)) {
170 4
            return $this->{$driverMethod}($config);
171
        }
172
173 1
        throw new InvalidArgumentException(\sprintf('Driver [%s] is not supported.', $config['driver']));
174
    }
175
176
    /**
177
     * Create an emergency log handler to avoid white screens of death.
178
     *
179
     * @return \Monolog\Logger
180
     *
181
     * @throws \Exception
182
     */
183 2
    protected function createEmergencyLogger()
184
    {
185 2
        return new Monolog('EasyWeChat', $this->prepareHandlers([new StreamHandler(
186 2
            \sys_get_temp_dir() . '/easywechat/easywechat.log',
187 2
            $this->level(['level' => 'debug'])
188
        )]));
189
    }
190
191
    /**
192
     * Call a custom driver creator.
193
     *
194
     * @param array $config
195
     *
196
     * @return mixed
197
     */
198 1
    protected function callCustomCreator(array $config)
199
    {
200 1
        return $this->customCreators[$config['driver']]($this->app, $config);
201
    }
202
203
    /**
204
     * Create an aggregate log driver instance.
205
     *
206
     * @param array $config
207
     *
208
     * @return \Monolog\Logger
209
     *
210
     * @throws \Exception
211
     */
212 2
    protected function createStackDriver(array $config)
213
    {
214 2
        $handlers = [];
215
216 2
        foreach ($config['channels'] ?? [] as $channel) {
217 2
            $handlers = \array_merge($handlers, $this->channel($channel)->getHandlers());
0 ignored issues
show
Bug introduced by
The method getHandlers() does not exist on Psr\Log\LoggerInterface. It seems like you code against a sub-type of Psr\Log\LoggerInterface such as EasyWeChat\Kernel\Log\LogManager or Monolog\Logger or Psr\Log\Test\TestLogger. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

217
            $handlers = \array_merge($handlers, $this->channel($channel)->/** @scrutinizer ignore-call */ getHandlers());
Loading history...
218
        }
219
220 2
        if ($config['ignore_exceptions'] ?? false) {
221
            $handlers = [new WhatFailureGroupHandler($handlers)];
222
        }
223
224 2
        return new Monolog($this->parseChannel($config), $handlers);
225
    }
226
227
    /**
228
     * Create an instance of the single file log driver.
229
     *
230
     * @param array $config
231
     *
232
     * @return \Psr\Log\LoggerInterface
233
     *
234
     * @throws \Exception
235
     */
236 2
    protected function createSingleDriver(array $config)
237
    {
238 2
        return new Monolog($this->parseChannel($config), [
239 2
            $this->prepareHandler(new StreamHandler(
240 2
                $config['path'],
241 1
                $this->level($config),
242 1
                $config['bubble'] ?? true,
243 1
                $config['permission'] ?? null,
244 1
                $config['locking'] ?? false
245
            ), $config),
246
        ]);
247
    }
248
249
    /**
250
     * Create an instance of the daily file log driver.
251
     *
252
     * @param array $config
253
     *
254
     * @return \Psr\Log\LoggerInterface
255
     */
256 1
    protected function createDailyDriver(array $config)
257
    {
258 1
        return new Monolog($this->parseChannel($config), [
259 1
            $this->prepareHandler(new RotatingFileHandler(
260 1
                $config['path'],
261 1
                $config['days'] ?? 7,
262 1
                $this->level($config),
263 1
                $config['bubble'] ?? true,
264 1
                $config['permission'] ?? null,
265 1
                $config['locking'] ?? false
266
            ), $config),
267
        ]);
268
    }
269
270
    /**
271
     * Create an instance of the Slack log driver.
272
     *
273
     * @param array $config
274
     *
275
     * @return \Psr\Log\LoggerInterface
276
     */
277 1
    protected function createSlackDriver(array $config)
278
    {
279 1
        return new Monolog($this->parseChannel($config), [
280 1
            $this->prepareHandler(new SlackWebhookHandler(
281 1
                $config['url'],
282 1
                $config['channel'] ?? null,
283 1
                $config['username'] ?? 'EasyWeChat',
284 1
                $config['attachment'] ?? true,
285 1
                $config['emoji'] ?? ':boom:',
286 1
                $config['short'] ?? false,
287 1
                $config['context'] ?? true,
288 1
                $this->level($config),
289 1
                $config['bubble'] ?? true,
290 1
                $config['exclude_fields'] ?? []
291
            ), $config),
292
        ]);
293
    }
294
295
    /**
296
     * Create an instance of the syslog log driver.
297
     *
298
     * @param array $config
299
     *
300
     * @return \Psr\Log\LoggerInterface
301
     */
302 1
    protected function createSyslogDriver(array $config)
303
    {
304 1
        return new Monolog($this->parseChannel($config), [
305 1
            $this->prepareHandler(new SyslogHandler(
306 1
                'EasyWeChat',
307 1
                $config['facility'] ?? LOG_USER,
308 1
                $this->level($config)
309
            ), $config),
310
        ]);
311
    }
312
313
    /**
314
     * Create an instance of the "error log" log driver.
315
     *
316
     * @param array $config
317
     *
318
     * @return \Psr\Log\LoggerInterface
319
     */
320 2
    protected function createErrorlogDriver(array $config)
321
    {
322 2
        return new Monolog($this->parseChannel($config), [
323 2
            $this->prepareHandler(
324 2
                new ErrorLogHandler(
325 2
                    $config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM,
326 2
                    $this->level($config)
327
                )
328
            ),
329
        ]);
330
    }
331
332
    /**
333
     * Create an instance of any handler available in Monolog.
334
     *
335
     * @param  array  $config
336
     * @return \Psr\Log\LoggerInterface
337
     *
338
     * @throws \InvalidArgumentException
339
     * @throws \Illuminate\Contracts\Container\BindingResolutionException
340
     */
341
    protected function createMonologDriver(array $config)
342
    {
343
        if (!is_a($config['handler'], HandlerInterface::class, true)) {
344
            throw new InvalidArgumentException(
345
                $config['handler'] . ' must be an instance of ' . HandlerInterface::class
346
            );
347
        }
348
349
        $with = array_merge(
350
            ['level' => $this->level($config)],
351
            $config['with'] ?? [],
352
            $config['handler_with'] ?? []
353
        );
354
355
        return new Monolog($this->parseChannel($config), [$this->prepareHandler(
356
            $this->app->make($config['handler'], $with),
0 ignored issues
show
Bug introduced by
The method make() does not exist on EasyWeChat\Kernel\ServiceContainer. It seems like you code against a sub-type of said class. However, the method does not exist in EasyWeChat\OfficialAccount\Application or EasyWeChat\Tests\Kernel\...ontainerForProviderTest or EasyWeChat\BasicService\Application or EasyWeChat\OpenPlatform\...cialAccount\Application. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

356
            $this->app->/** @scrutinizer ignore-call */ 
357
                        make($config['handler'], $with),
Loading history...
357
            $config
358
        )]);
359
    }
360
361
    /**
362
     * Prepare the handlers for usage by Monolog.
363
     *
364
     * @param array $handlers
365
     *
366
     * @return array
367
     */
368 2
    protected function prepareHandlers(array $handlers)
369
    {
370 2
        foreach ($handlers as $key => $handler) {
371 2
            $handlers[$key] = $this->prepareHandler($handler);
372
        }
373
374 2
        return $handlers;
375
    }
376
377
    /**
378
     * Prepare the handler for usage by Monolog.
379
     *
380
     * @param \Monolog\Handler\HandlerInterface $handler
381
     *
382
     * @return \Monolog\Handler\HandlerInterface
383
     */
384 3
    protected function prepareHandler(HandlerInterface $handler, array $config = [])
385
    {
386 3
        if (!isset($config['formatter'])) {
387 3
            $handler->setFormatter($this->formatter());
0 ignored issues
show
Bug introduced by
The method setFormatter() does not exist on Monolog\Handler\HandlerInterface. It seems like you code against a sub-type of said class. However, the method does not exist in Monolog\Handler\Handler or Monolog\Handler\AbstractHandler or Monolog\Handler\NullHandler or Monolog\Handler\NoopHandler. Are you sure you never get one of those? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

387
            $handler->/** @scrutinizer ignore-call */ 
388
                      setFormatter($this->formatter());
Loading history...
388
        } elseif ($config['formatter'] !== 'default') {
389
            $handler->setFormatter($this->app->make($config['formatter'], $config['formatter_with'] ?? []));
390
        }
391
392 3
        return $handler;
393
    }
394
395
    /**
396
     * Get a Monolog formatter instance.
397
     *
398
     * @return \Monolog\Formatter\FormatterInterface
399
     */
400 3
    protected function formatter()
401
    {
402 3
        $formatter = new LineFormatter(null, null, true, true);
403 3
        $formatter->includeStacktraces();
404
405 3
        return $formatter;
406
    }
407
408
    /**
409
     * Extract the log channel from the given configuration.
410
     *
411
     * @param array $config
412
     *
413
     * @return string
414
     */
415 2
    protected function parseChannel(array $config)
416
    {
417 2
        return $config['name'] ?? 'EasyWeChat';
418
    }
419
420
    /**
421
     * Parse the string level into a Monolog constant.
422
     *
423
     * @param array $config
424
     *
425
     * @return int
426
     *
427
     * @throws InvalidArgumentException
428
     */
429 4
    protected function level(array $config)
430
    {
431 4
        $level = $config['level'] ?? 'debug';
432
433 4
        if (isset($this->levels[$level])) {
434 3
            return $this->levels[$level];
435
        }
436
437 1
        throw new InvalidArgumentException('Invalid log level.');
438
    }
439
440
    /**
441
     * Get the default log driver name.
442
     *
443
     * @return string
444
     */
445 3
    public function getDefaultDriver()
446
    {
447 3
        return $this->app['config']['log.default'];
448
    }
449
450
    /**
451
     * Set the default log driver name.
452
     *
453
     * @param string $name
454
     */
455 1
    public function setDefaultDriver($name)
456
    {
457 1
        $this->app['config']['log.default'] = $name;
458 1
    }
459
460
    /**
461
     * Register a custom driver creator Closure.
462
     *
463
     * @param string   $driver
464
     * @param \Closure $callback
465
     *
466
     * @return $this
467
     */
468 1
    public function extend($driver, \Closure $callback)
469
    {
470 1
        $this->customCreators[$driver] = $callback->bindTo($this, $this);
471
472 1
        return $this;
473
    }
474
475
    /**
476
     * System is unusable.
477
     *
478
     * @param string $message
479
     * @param array  $context
480
     *
481
     * @return mixed
482
     *
483
     * @throws \Exception
484
     */
485 1
    public function emergency($message, array $context = [])
486
    {
487 1
        return $this->driver()->emergency($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->emergency($message, $context) targeting Psr\Log\LoggerInterface::emergency() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
488
    }
489
490
    /**
491
     * Action must be taken immediately.
492
     *
493
     * Example: Entire website down, database unavailable, etc. This should
494
     * trigger the SMS alerts and wake you up.
495
     *
496
     * @param string $message
497
     * @param array  $context
498
     *
499
     * @return mixed
500
     *
501
     * @throws \Exception
502
     */
503 1
    public function alert($message, array $context = [])
504
    {
505 1
        return $this->driver()->alert($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->alert($message, $context) targeting Psr\Log\LoggerInterface::alert() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
506
    }
507
508
    /**
509
     * Critical conditions.
510
     *
511
     * Example: Application component unavailable, unexpected exception.
512
     *
513
     * @param string $message
514
     * @param array  $context
515
     *
516
     * @return mixed
517
     *
518
     * @throws \Exception
519
     */
520 1
    public function critical($message, array $context = [])
521
    {
522 1
        return $this->driver()->critical($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->critical($message, $context) targeting Psr\Log\LoggerInterface::critical() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
523
    }
524
525
    /**
526
     * Runtime errors that do not require immediate action but should typically
527
     * be logged and monitored.
528
     *
529
     * @param string $message
530
     * @param array  $context
531
     *
532
     * @return mixed
533
     *
534
     * @throws \Exception
535
     */
536 1
    public function error($message, array $context = [])
537
    {
538 1
        return $this->driver()->error($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->error($message, $context) targeting Psr\Log\LoggerInterface::error() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
539
    }
540
541
    /**
542
     * Exceptional occurrences that are not errors.
543
     *
544
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
545
     * that are not necessarily wrong.
546
     *
547
     * @param string $message
548
     * @param array  $context
549
     *
550
     * @return mixed
551
     *
552
     * @throws \Exception
553
     */
554 1
    public function warning($message, array $context = [])
555
    {
556 1
        return $this->driver()->warning($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->warning($message, $context) targeting Psr\Log\LoggerInterface::warning() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
557
    }
558
559
    /**
560
     * Normal but significant events.
561
     *
562
     * @param string $message
563
     * @param array  $context
564
     *
565
     * @return mixed
566
     *
567
     * @throws \Exception
568
     */
569 1
    public function notice($message, array $context = [])
570
    {
571 1
        return $this->driver()->notice($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->notice($message, $context) targeting Psr\Log\LoggerInterface::notice() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
572
    }
573
574
    /**
575
     * Interesting events.
576
     *
577
     * Example: User logs in, SQL logs.
578
     *
579
     * @param string $message
580
     * @param array  $context
581
     *
582
     * @return mixed
583
     *
584
     * @throws \Exception
585
     */
586 1
    public function info($message, array $context = [])
587
    {
588 1
        return $this->driver()->info($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->info($message, $context) targeting Psr\Log\LoggerInterface::info() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
589
    }
590
591
    /**
592
     * Detailed debug information.
593
     *
594
     * @param string $message
595
     * @param array  $context
596
     *
597
     * @return mixed
598
     *
599
     * @throws \Exception
600
     */
601 2
    public function debug($message, array $context = [])
602
    {
603 2
        return $this->driver()->debug($message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->debug($message, $context) targeting Psr\Log\LoggerInterface::debug() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
604
    }
605
606
    /**
607
     * Logs with an arbitrary level.
608
     *
609
     * @param mixed  $level
610
     * @param string $message
611
     * @param array  $context
612
     *
613
     * @return mixed
614
     *
615
     * @throws \Exception
616
     */
617 1
    public function log($level, $message, array $context = [])
618
    {
619 1
        return $this->driver()->log($level, $message, $context);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->driver()->log($level, $message, $context) targeting Psr\Log\LoggerInterface::log() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
620
    }
621
622
    /**
623
     * Dynamically call the default driver instance.
624
     *
625
     * @param string $method
626
     * @param array  $parameters
627
     *
628
     * @return mixed
629
     *
630
     * @throws \Exception
631
     */
632 1
    public function __call($method, $parameters)
633
    {
634 1
        return $this->driver()->$method(...$parameters);
635
    }
636
}
637