Passed
Push — dev ( 704976...a02dd0 )
by 世昌
02:38
created

FileLogger   B

Complexity

Total Complexity 45

Size/Duplication

Total Lines 307
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 121
c 7
b 0
f 0
dl 0
loc 307
rs 8.8
wmc 45

16 Methods

Rating   Name   Duplication   Size   Complexity  
A getAvailableWrite() 0 7 2
A set() 0 6 1
A getDefaultConfig() 0 10 1
A __construct() 0 4 1
A prepareWrite() 0 12 2
B packLogFile() 0 33 8
A removePackFiles() 0 8 4
A rollLatest() 0 16 4
A clearContent() 0 8 2
A write() 0 7 2
A log() 0 9 2
A getZipArchive() 0 10 3
A shutdown() 0 6 2
A checkSize() 0 9 3
A interpolate() 0 12 5
A safeMoveKeep() 0 14 3

How to fix   Complexity   

Complex Class

Complex classes like FileLogger often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileLogger, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace suda\framework\debug\log\logger;
4
5
use ZipArchive;
6
use RecursiveIteratorIterator;
7
use RecursiveDirectoryIterator;
8
use suda\framework\debug\ConfigTrait;
9
use suda\framework\debug\log\LogLevel;
10
use suda\framework\debug\ConfigInterface;
11
use suda\framework\filesystem\FileSystem;
12
use suda\framework\debug\log\AbstractLogger;
13
use suda\framework\debug\log\logger\exception\FileLoggerException;
14
15
/**
16
 * Class FileLogger
17
 * @package suda\framework\debug\log\logger
18
 */
19
class FileLogger extends AbstractLogger implements ConfigInterface
20
{
21
    use ConfigTrait;
22
23
    /**
24
     * 文件
25
     *
26
     * @var resource
27
     */
28
    protected $temp;
29
30
    /**
31
     * 临时文件名
32
     *
33
     * @var string
34
     */
35
    protected $tempName;
36
37
    /**
38
     * 移除文件
39
     *
40
     * @var array
41
     */
42
    protected $removeFiles = [];
43
44
    /**
45
     * 最后的日志
46
     *
47
     * @var string
48
     */
49
    protected $latest;
50
51
    /**
52
     * 构建文件日志
53
     *
54
     * @param array $config
55
     */
56
    public function __construct(array $config = [])
57
    {
58
        $this->set($config);
59
        register_shutdown_function([$this, 'shutdown']);
60
    }
61
62
    /**
63
     * 设置配置
64
     * @param array $config
65
     */
66
    public function set(array $config)
67
    {
68
        $this->applyConfig($config);
69
        FileSystem::make($this->getConfig('save-path'));
70
        FileSystem::make($this->getConfig('save-dump-path'));
71
        FileSystem::make($this->getConfig('save-zip-path'));
72
    }
73
74
    /**
75
     * @return resource
76
     * @throws FileLoggerException
77
     */
78
    public function getAvailableWrite()
79
    {
80
        if (is_resource($this->temp)) {
81
            return $this->temp;
82
        }
83
        $this->prepareWrite();
84
        return $this->temp;
85
    }
86
87
    /**
88
     * @throws FileLoggerException
89
     */
90
    private function prepareWrite()
91
    {
92
        $unique = substr(md5(uniqid()), 0, 8);
93
        $save = $this->getConfig('save-path');
94
        $this->tempName = $save . '/' . date('YmdHis') . '.' . $unique . '.log';
95
        $temp = fopen($this->tempName, 'w+');
96
        if ($temp !== false) {
97
            $this->temp = $temp;
98
        } else {
99
            throw new FileLoggerException(__METHOD__ . ':' . sprintf('cannot create log file'));
100
        }
101
        $this->latest = $save . '/' . $this->getConfig('file-name');
102
    }
103
104
    /**
105
     * @return array
106
     */
107
    public function getDefaultConfig(): array
108
    {
109
        return [
110
            'save-path' => './logs',
111
            'save-zip-path' => './logs/zip',
112
            'save-dump-path' => './logs/dump',
113
            'max-file-size' => 2097152,
114
            'file-name' => 'latest.log',
115
            'log-level' => 'debug',
116
            'log-format' => '[%level%] %message%',
117
        ];
118
    }
119
120
    /**
121
     * 打包文件
122
     */
123
    private function packLogFile()
124
    {
125
        $logFile = $this->latest;
126
        $path = preg_replace(
127
            '/[\\\\]+/',
128
            '/',
129
            $this->getConfig('save-zip-path') . '/' . date('Y-m-d') . '.zip'
130
        );
131
        $zip = $this->getZipArchive($path);
132
        if ($zip !== null) {
133
            $add = $zip->addFile($logFile, date('Y-m-d') . '-' . $zip->numFiles . '.log');
134
            if (is_dir($this->getConfig('save-dump-path'))) {
135
                $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(
136
                    $this->getConfig('save-dump-path'),
137
                    RecursiveDirectoryIterator::SKIP_DOTS
138
                ));
139
                foreach ($it as $dumpLog) {
140
                    if ($zip->addFile($dumpLog, 'dump/' . basename($dumpLog))) {
141
                        array_push($this->removeFiles, $dumpLog);
142
                    }
143
                }
144
            }
145
            $zip->close();
146
            if ($add) {
147
                $this->clearContent($logFile);
148
            }
149
        } else {
150
            if (is_file($logFile) && file_exists($logFile)) {
151
                $this->safeMoveKeep(
152
                    $logFile,
153
                    $this->getConfig('save-path')
154
                    . '/' . date('Y-m-d')
155
                    . '-' . substr(md5(uniqid()), 0, 8). '.log'
156
                );
157
            }
158
        }
159
    }
160
161
    /**
162
     * @param string $from
163
     * @param string $to
164
     */
165
    private function safeMoveKeep(string $from, string $to)
166
    {
167
        $fromFile = fopen($from, 'r');
168
        $toFile = fopen($to, 'w+');
169
        if ($fromFile !== false  && $toFile !== false) {
170
            flock($toFile, LOCK_EX);
171
            // 复制内容
172
            stream_copy_to_stream($fromFile, $toFile);
173
            flock($toFile, LOCK_UN);
174
            fclose($toFile);
175
            fclose($fromFile);
176
        }
177
        // 清空内容
178
        $this->clearContent($from);
179
    }
180
181
    /**
182
     * 清空内容
183
     * @param string $path
184
     * @return bool
185
     */
186
    private function clearContent(string $path)
187
    {
188
        $file = fopen($path, 'w');
189
        if ($file) {
0 ignored issues
show
introduced by
$file is of type false|resource, thus it always evaluated to false.
Loading history...
190
            fclose($file);
191
            return true;
192
        }
193
        return false;
194
    }
195
196
    /**
197
     * 获取压缩
198
     *
199
     * @param string $path
200
     * @return ZipArchive|null
201
     */
202
    private function getZipArchive(string $path)
203
    {
204
        if (class_exists('ZipArchive')) {
205
            $zip = new ZipArchive;
206
            $res = $zip->open($path, ZipArchive::CREATE);
207
            if ($res === true) {
208
                return $zip;
209
            }
210
        }
211
        return null;
212
    }
213
214
    /**
215
     * 检查日志文件大小
216
     *
217
     * @return boolean
218
     */
219
    private function checkSize(): bool
220
    {
221
        $logFile = $this->latest;
222
        if (file_exists($logFile)) {
223
            if (filesize($logFile) > $this->getConfig('max-file-size')) {
224
                return true;
225
            }
226
        }
227
        return false;
228
    }
229
230
231
    /**
232
     * @param string $level
233
     * @param string $message
234
     * @param array $context
235
     * @return mixed|void
236
     * @throws FileLoggerException
237
     */
238
    public function log($level, string $message, array $context = [])
239
    {
240
        if (LogLevel::compare($level, $this->getConfig('log-level')) >= 0) {
241
            $replace = [];
242
            $message = $this->interpolate($message, $context);
243
            $replace['%level%'] = $level;
244
            $replace['%message%'] = $message;
245
            $write = strtr($this->getConfig('log-format'), $replace);
246
            fwrite($this->getAvailableWrite(), $write . PHP_EOL);
247
        }
248
    }
249
250
251
    /**
252
     * 将临时文件写入最后日志
253
     */
254
    private function rollLatest()
255
    {
256
        if (isset($this->latest)) {
257
            $latest = fopen($this->latest, 'a+');
258
            if (flock($latest, LOCK_EX)) {
0 ignored issues
show
Bug introduced by
It seems like $latest can also be of type false; however, parameter $handle of flock() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

258
            if (flock(/** @scrutinizer ignore-type */ $latest, LOCK_EX)) {
Loading history...
259
                rewind($this->temp);
260
                stream_copy_to_stream($this->temp, $latest);
0 ignored issues
show
Bug introduced by
It seems like $latest can also be of type false; however, parameter $dest of stream_copy_to_stream() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

260
                stream_copy_to_stream($this->temp, /** @scrutinizer ignore-type */ $latest);
Loading history...
261
                flock($latest, LOCK_UN);
262
                if (file_exists($this->tempName)) {
263
                    unlink($this->tempName);
264
                }
265
            }
266
            fclose($latest);
0 ignored issues
show
Bug introduced by
It seems like $latest can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

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

266
            fclose(/** @scrutinizer ignore-type */ $latest);
Loading history...
267
            fclose($this->temp);
268
            $this->temp = null;
269
            $this->tempName = null;
270
        }
271
    }
272
273
    /**
274
     * 删除已经压缩的文件
275
     */
276
    private function removePackFiles()
277
    {
278
        foreach ($this->removeFiles as $file) {
279
            if (is_file($file) && file_exists($file)) {
280
                unlink($file);
281
            }
282
        }
283
        $this->removeFiles = [];
284
    }
285
286
    /**
287
     * 即时写入日志
288
     */
289
    public function write()
290
    {
291
        if ($this->checkSize()) {
292
            $this->packLogFile();
293
        }
294
        $this->rollLatest();
295
        $this->removePackFiles();
296
    }
297
298
    /**
299
     * 程序关闭时调用
300
     */
301
    public function shutdown()
302
    {
303
        if (function_exists('fastcgi_finish_request')) {
304
            fastcgi_finish_request();
305
        }
306
        $this->write();
307
    }
308
309
    /**
310
     * @param string $message
311
     * @param array $context
312
     * @return string
313
     */
314
    public function interpolate(string $message, array $context)
315
    {
316
        $replace = [];
317
        foreach ($context as $key => $val) {
318
            if (is_bool($val)) {
319
                $val = $val ? 'true' : 'false';
320
            } elseif (null === $val) {
321
                $val = 'null';
322
            }
323
            $replace['{' . $key . '}'] = $val;
324
        }
325
        return strtr($message, $replace);
326
    }
327
}
328