Completed
Push — master ( c52910...66a65c )
by lyu
02:53 queued 47s
created

LogManager::driver()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
192
        }
193
194
        return new Monolog($this->parseChannel($config), $handlers);
195
    }
196
197
    /**
198
     * Create an instance of the single file log driver.
199
     *
200
     * @param array $config
201
     *
202
     * @return \Psr\Log\LoggerInterface
203
     */
204
    protected function createSingleDriver(array $config)
205
    {
206
        return new Monolog($this->parseChannel($config), [
207
            $this->prepareHandler(
208
                new StreamHandler($config['path'], $this->level($config))
209
            ),
210
        ]);
211
    }
212
213
    /**
214
     * Create an instance of the daily file log driver.
215
     *
216
     * @param array $config
217
     *
218
     * @return \Psr\Log\LoggerInterface
219
     */
220 View Code Duplication
    protected function createDailyDriver(array $config)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
221
    {
222
        return new Monolog($this->parseChannel($config), [
223
            $this->prepareHandler(new RotatingFileHandler(
224
                $config['path'], $config['days'] ?? 7, $this->level($config)
225
            )),
226
        ]);
227
    }
228
229
    /**
230
     * Create an instance of the Slack log driver.
231
     *
232
     * @param array $config
233
     *
234
     * @return \Psr\Log\LoggerInterface
235
     */
236
    protected function createSlackDriver(array $config)
237
    {
238
        return new Monolog($this->parseChannel($config), [
239
            $this->prepareHandler(new SlackWebhookHandler(
240
                $config['url'],
241
                $config['channel'] ?? null,
242
                $config['username'] ?? 'EasyWeChat',
243
                $config['attachment'] ?? true,
244
                $config['emoji'] ?? ':boom:',
245
                $config['short'] ?? false,
246
                $config['context'] ?? true,
247
                $this->level($config)
248
            )),
249
        ]);
250
    }
251
252
    /**
253
     * Create an instance of the syslog log driver.
254
     *
255
     * @param array $config
256
     *
257
     * @return \Psr\Log\LoggerInterface
258
     */
259 View Code Duplication
    protected function createSyslogDriver(array $config)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
260
    {
261
        return new Monolog($this->parseChannel($config), [
262
            $this->prepareHandler(new SyslogHandler(
263
                    'EasyWeChat', $config['facility'] ?? LOG_USER, $this->level($config))
264
            ),
265
        ]);
266
    }
267
268
    /**
269
     * Create an instance of the "error log" log driver.
270
     *
271
     * @param array $config
272
     *
273
     * @return \Psr\Log\LoggerInterface
274
     */
275 View Code Duplication
    protected function createErrorlogDriver(array $config)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
276
    {
277
        return new Monolog($this->parseChannel($config), [
278
            $this->prepareHandler(new ErrorLogHandler(
279
                    $config['type'] ?? ErrorLogHandler::OPERATING_SYSTEM, $this->level($config))
280
            ),
281
        ]);
282
    }
283
284
    /**
285
     * Prepare the handlers for usage by Monolog.
286
     *
287
     * @param array $handlers
288
     *
289
     * @return array
290
     */
291
    protected function prepareHandlers(array $handlers)
292
    {
293
        foreach ($handlers as $key => $handler) {
294
            $handlers[$key] = $this->prepareHandler($handler);
295
        }
296
297
        return $handlers;
298
    }
299
300
    /**
301
     * Prepare the handler for usage by Monolog.
302
     *
303
     * @param \Monolog\Handler\HandlerInterface $handler
304
     *
305
     * @return \Monolog\Handler\HandlerInterface
306
     */
307
    protected function prepareHandler(HandlerInterface $handler)
308
    {
309
        return $handler->setFormatter($this->formatter());
310
    }
311
312
    /**
313
     * Get a Monolog formatter instance.
314
     *
315
     * @return \Monolog\Formatter\FormatterInterface
316
     */
317
    protected function formatter()
318
    {
319
        $formatter = new LineFormatter(null, null, true, true);
320
        $formatter->includeStacktraces();
321
322
        return $formatter;
323
    }
324
325
    /**
326
     * Extract the log channel from the given configuration.
327
     *
328
     * @param array $config
329
     *
330
     * @return string
331
     */
332
    protected function parseChannel(array $config)
333
    {
334
        return $config['name'] ?? null;
335
    }
336
337
    /**
338
     * Parse the string level into a Monolog constant.
339
     *
340
     * @param array $config
341
     *
342
     * @return int
343
     *
344
     * @throws \InvalidArgumentException
345
     */
346
    protected function level(array $config)
347
    {
348
        $level = $config['level'] ?? 'debug';
349
350
        if (isset($this->levels[$level])) {
351
            return $this->levels[$level];
352
        }
353
354
        throw new \InvalidArgumentException('Invalid log level.');
355
    }
356
357
    /**
358
     * Get the default log driver name.
359
     *
360
     * @return string
361
     */
362
    public function getDefaultDriver()
363
    {
364
        return $this->app['config']['log.default'];
365
    }
366
367
    /**
368
     * Set the default log driver name.
369
     *
370
     * @param string $name
371
     */
372
    public function setDefaultDriver($name)
373
    {
374
        $this->app['config']['log.default'] = $name;
375
    }
376
377
    /**
378
     * Register a custom driver creator Closure.
379
     *
380
     * @param string   $driver
381
     * @param \Closure $callback
382
     *
383
     * @return $this
384
     */
385
    public function extend($driver, \Closure $callback)
386
    {
387
        $this->customCreators[$driver] = $callback->bindTo($this, $this);
388
389
        return $this;
390
    }
391
392
    /**
393
     * System is unusable.
394
     *
395
     * @param string $message
396
     * @param array  $context
397
     *
398
     * @return mixed
399
     */
400
    public function emergency($message, array $context = [])
401
    {
402
        return $this->driver()->emergency($message, $context);
403
    }
404
405
    /**
406
     * Action must be taken immediately.
407
     *
408
     * Example: Entire website down, database unavailable, etc. This should
409
     * trigger the SMS alerts and wake you up.
410
     *
411
     * @param string $message
412
     * @param array  $context
413
     *
414
     * @return mixed
415
     */
416
    public function alert($message, array $context = [])
417
    {
418
        return $this->driver()->alert($message, $context);
419
    }
420
421
    /**
422
     * Critical conditions.
423
     *
424
     * Example: Application component unavailable, unexpected exception.
425
     *
426
     * @param string $message
427
     * @param array  $context
428
     *
429
     * @return mixed
430
     */
431
    public function critical($message, array $context = [])
432
    {
433
        return $this->driver()->critical($message, $context);
434
    }
435
436
    /**
437
     * Runtime errors that do not require immediate action but should typically
438
     * be logged and monitored.
439
     *
440
     * @param string $message
441
     * @param array  $context
442
     *
443
     * @return mixed
444
     */
445
    public function error($message, array $context = [])
446
    {
447
        return $this->driver()->error($message, $context);
448
    }
449
450
    /**
451
     * Exceptional occurrences that are not errors.
452
     *
453
     * Example: Use of deprecated APIs, poor use of an API, undesirable things
454
     * that are not necessarily wrong.
455
     *
456
     * @param string $message
457
     * @param array  $context
458
     *
459
     * @return mixed
460
     */
461
    public function warning($message, array $context = [])
462
    {
463
        return $this->driver()->warning($message, $context);
464
    }
465
466
    /**
467
     * Normal but significant events.
468
     *
469
     * @param string $message
470
     * @param array  $context
471
     *
472
     * @return mixed
473
     */
474
    public function notice($message, array $context = [])
475
    {
476
        return $this->driver()->notice($message, $context);
477
    }
478
479
    /**
480
     * Interesting events.
481
     *
482
     * Example: User logs in, SQL logs.
483
     *
484
     * @param string $message
485
     * @param array  $context
486
     *
487
     * @return mixed
488
     */
489
    public function info($message, array $context = [])
490
    {
491
        return $this->driver()->info($message, $context);
492
    }
493
494
    /**
495
     * Detailed debug information.
496
     *
497
     * @param string $message
498
     * @param array  $context
499
     *
500
     * @return mixed
501
     */
502
    public function debug($message, array $context = [])
503
    {
504
        return $this->driver()->debug($message, $context);
505
    }
506
507
    /**
508
     * Logs with an arbitrary level.
509
     *
510
     * @param mixed  $level
511
     * @param string $message
512
     * @param array  $context
513
     *
514
     * @return mixed
515
     */
516
    public function log($level, $message, array $context = [])
517
    {
518
        return $this->driver()->log($level, $message, $context);
519
    }
520
521
    /**
522
     * Dynamically call the default driver instance.
523
     *
524
     * @param string $method
525
     * @param array  $parameters
526
     *
527
     * @return mixed
528
     */
529
    public function __call($method, $parameters)
530
    {
531
        return $this->driver()->$method(...$parameters);
532
    }
533
}
534