Passed
Push — dev ( ad1734...704976 )
by 世昌
02:19
created

FileLogger::clearContent()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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
        $fromR = fopen($from, 'r');
168
        $toR = fopen($to, 'w+');
169
        flock($toR, LOCK_EX);
0 ignored issues
show
Bug introduced by
It seems like $toR 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

169
        flock(/** @scrutinizer ignore-type */ $toR, LOCK_EX);
Loading history...
170
        // 复制内容
171
        stream_copy_to_stream($fromR, $toR);
0 ignored issues
show
Bug introduced by
It seems like $toR 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

171
        stream_copy_to_stream($fromR, /** @scrutinizer ignore-type */ $toR);
Loading history...
Bug introduced by
It seems like $fromR can also be of type false; however, parameter $source 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

171
        stream_copy_to_stream(/** @scrutinizer ignore-type */ $fromR, $toR);
Loading history...
172
        flock($toR, LOCK_UN);
173
        fclose($toR);
0 ignored issues
show
Bug introduced by
It seems like $toR 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

173
        fclose(/** @scrutinizer ignore-type */ $toR);
Loading history...
174
        fclose($fromR);
175
        // 清空内容
176
        $this->clearContent($from);
177
    }
178
179
    /**
180
     * 清空内容
181
     * @param string $path
182
     */
183
    private function clearContent(string $path)
184
    {
185
        fclose(fopen($path, 'w'));
0 ignored issues
show
Bug introduced by
It seems like fopen($path, 'w') 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

185
        fclose(/** @scrutinizer ignore-type */ fopen($path, 'w'));
Loading history...
186
    }
187
188
    /**
189
     * 获取压缩
190
     *
191
     * @param string $path
192
     * @return ZipArchive|null
193
     */
194
    private function getZipArchive(string $path)
195
    {
196
        if (class_exists('ZipArchive')) {
197
            $zip = new ZipArchive;
198
            $res = $zip->open($path, ZipArchive::CREATE);
199
            if ($res === true) {
200
                return $zip;
201
            }
202
        }
203
        return null;
204
    }
205
206
    /**
207
     * 检查日志文件大小
208
     *
209
     * @return boolean
210
     */
211
    private function checkSize(): bool
212
    {
213
        $logFile = $this->latest;
214
        if (file_exists($logFile)) {
215
            if (filesize($logFile) > $this->getConfig('max-file-size')) {
216
                return true;
217
            }
218
        }
219
        return false;
220
    }
221
222
223
    /**
224
     * @param string $level
225
     * @param string $message
226
     * @param array $context
227
     * @return mixed|void
228
     * @throws FileLoggerException
229
     */
230
    public function log($level, string $message, array $context = [])
231
    {
232
        if (LogLevel::compare($level, $this->getConfig('log-level')) >= 0) {
233
            $replace = [];
234
            $message = $this->interpolate($message, $context);
235
            $replace['%level%'] = $level;
236
            $replace['%message%'] = $message;
237
            $write = strtr($this->getConfig('log-format'), $replace);
238
            fwrite($this->getAvailableWrite(), $write . PHP_EOL);
239
        }
240
    }
241
242
243
    /**
244
     * 将临时文件写入最后日志
245
     */
246
    private function rollLatest()
247
    {
248
        if (isset($this->latest)) {
249
            $latest = fopen($this->latest, 'a+');
250
            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

250
            if (flock(/** @scrutinizer ignore-type */ $latest, LOCK_EX)) {
Loading history...
251
                rewind($this->temp);
252
                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

252
                stream_copy_to_stream($this->temp, /** @scrutinizer ignore-type */ $latest);
Loading history...
253
                flock($latest, LOCK_UN);
254
                if (file_exists($this->tempName)) {
255
                    unlink($this->tempName);
256
                }
257
            }
258
            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

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