Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.30% covered (success)
91.30%
42 / 46
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
FileLogger
91.30% covered (success)
91.30%
42 / 46
71.43% covered (warning)
71.43%
5 / 7
12.09
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 toSafeFileNameHeader
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 toHeaderDate
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 cleanupCore
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
3.71
 cleanup
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 getLogFilePath
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 logImpl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace PeServer\Core\Log;
6
7use PeServer\Core\Code;
8use PeServer\Core\Collection\Arr;
9use PeServer\Core\IO\Directory;
10use PeServer\Core\IO\File;
11use PeServer\Core\IO\IOUtility;
12use PeServer\Core\IO\Path;
13use PeServer\Core\Log\LoggerBase;
14use PeServer\Core\Log\Logging;
15use PeServer\Core\Log\LogOptions;
16use PeServer\Core\Text;
17use PeServer\Core\Throws\Throws;
18
19/**
20 * ファイルロガー。
21 */
22class FileLogger extends LoggerBase
23{
24    #region variable
25
26    /**
27     * 出力ディレクトリパス。
28     */
29    private readonly string $directoryPath;
30    /**
31     * ファイル書式名
32     *
33     * @phpstan-var literal-string
34     */
35    private readonly string $baseFileName;
36
37    /**
38     * 破棄済みファイルパターン。
39     *
40     * @var string[]
41     */
42    private static array $cleanupFilePatterns = [];
43
44    #endregion
45
46    /**
47     * 生成。
48     *
49     * @param LogOptions $options
50     */
51    public function __construct(Logging $logging, LogOptions $options)
52    {
53        parent::__construct($logging, $options);
54
55        $directoryPath = $this->options->configuration['directory'] ?? Text::EMPTY;
56        Throws::throwIfNullOrWhiteSpace($directoryPath);
57        $this->directoryPath = $directoryPath;
58
59        $baseFileName = Code::toLiteralString($this->options->configuration['name'] ?? Text::EMPTY);
60        Throws::throwIfNullOrWhiteSpace($baseFileName);
61        $this->baseFileName = $baseFileName;
62
63        /** @phpstan-var non-negative-int */
64        $count = $this->options->configuration['count'] ?? 0;
65        Throws::throwIf(0 <= $count); //@phpstan-ignore-line [DOCTYPE]
66        $this->cleanup($count);
67    }
68
69    #region function
70
71    private function toSafeFileNameHeader(): string
72    {
73        $trimHeader = Text::trim($this->options->header, '/\\');
74        return Text::replace($trimHeader, ['/', '\\', '*', '|', '<', '>', '?', ':'], '_');
75    }
76
77    protected function toHeaderDate(bool $isCleanup): string
78    {
79        return $isCleanup
80            ? '*'
81            : date('Ymd');
82    }
83
84    /**
85     * 破棄処理内部実装。
86     *
87     * @param int $maxCount
88     * @phpstan-param non-negative-int $maxCount
89     * @param string $filePattern
90     */
91    private function cleanupCore(int $maxCount, string $filePattern): void
92    {
93        $logFiles = Directory::find($this->directoryPath, $filePattern);
94        $logCount = Arr::getCount($logFiles);
95        if ($logCount <= $maxCount) {
96            return;
97        }
98
99        $length = $logCount - $maxCount;
100        for ($i = 0; $i < $length; $i++) {
101            File::removeFile($logFiles[$i]);
102        }
103    }
104
105    /**
106     * 破棄処理。
107     *
108     * @param int $maxCount
109     * @phpstan-param non-negative-int $maxCount
110     */
111    private function cleanup(int $maxCount): void
112    {
113        if (!$maxCount) {
114            return;
115        }
116
117        $filePattern = Text::replaceMap(
118            $this->baseFileName,
119            [
120                'HEADER' => $this->toSafeFileNameHeader(),
121                'DATE' => $this->toHeaderDate(true),
122            ]
123        );
124        if (!Arr::containsValue(self::$cleanupFilePatterns, $filePattern)) {
125            self::$cleanupFilePatterns[] = $filePattern;
126            $this->cleanupCore($maxCount, $filePattern);
127        }
128    }
129
130    private function getLogFilePath(): string
131    {
132        $fileName = Text::replaceMap(
133            $this->baseFileName,
134            [
135                'HEADER' => $this->toSafeFileNameHeader(),
136                'DATE' => $this->toHeaderDate(false),
137            ]
138        );
139
140        return Path::combine($this->directoryPath, $fileName);
141    }
142
143    #endregion
144
145    #region LoggerBase
146
147    protected function logImpl(int $level, int $traceIndex, $message, ...$parameters): void
148    {
149        Directory::createDirectoryIfNotExists($this->directoryPath);
150
151        $logMessage = $this->format($level, $traceIndex + 1, $message, ...$parameters);
152
153        $filePath = $this->getLogFilePath();
154        //error_logが制限されている場合はこっちを使用する→: file_put_contents($filePath, $logMessage . PHP_EOL, FILE_APPEND | LOCK_EX);
155        error_log($logMessage . PHP_EOL, 3, $filePath);
156    }
157
158    #endregion
159}