Rotation::runProcessor()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Cesargb\Log;
4
5
use Cesargb\Log\Compress\Gz;
6
use Cesargb\Log\Processors\RotativeProcessor;
7
use Exception;
8
9
class Rotation
10
{
11
    use Optionable;
12
    use ErrorHandler;
13
14
    private const COMPRESS_DEFAULT_LEVEL = null;
15
16
    private RotativeProcessor $processor;
17
18
    private bool $_compress = false;
19
    private ?int $_compressLevel = self::COMPRESS_DEFAULT_LEVEL;
20
21
    private int $_minSize = 0;
22
23
    private bool $_truncate = false;
24
25
    /**
26
     * @param mixed[] $options
27
     */
28
    public function __construct(array $options = [])
29
    {
30
        $this->processor = new RotativeProcessor();
31
32
        $this->methodsOptionables([
33
            'compress',
34
            'truncate',
35
            'minSize',
36
            'files',
37
            'then',
38
            'catch',
39
            'finally',
40
        ]);
41
42
        $this->options($options);
43
    }
44
45
    /**
46
     * Log files are rotated count times before being removed.
47
     */
48
    public function files(int $count): self
49
    {
50
        $this->processor->files($count);
51
52
        return $this;
53
    }
54
55
    /**
56
     * Old versions of log files are compressed.
57
     */
58
    public function compress(bool|int $level = true): self
59
    {
60
        $this->_compress = (bool)($level);
61
        $this->_compressLevel = is_numeric($level)
62
            ? $level
63
            : self::COMPRESS_DEFAULT_LEVEL;
64
65
        if ($this->_compress) {
66
            $this->processor->addExtension('gz');
67
        } else {
68
            $this->processor->removeExtension('gz');
69
        }
70
71
        return $this;
72
    }
73
74
    /**
75
     * Truncate the original log file in place after creating a copy, instead of
76
     * moving the old log file.
77
     *
78
     * It can be used when some program cannot be told to close its logfile and
79
     * thus might continue writing (appending) to the previous log file forever.
80
     */
81
    public function truncate(bool $truncate = true): self
82
    {
83
        $this->_truncate = $truncate;
84
85
        return $this;
86
    }
87
88
    /**
89
     * Log files are rotated when they grow bigger than size bytes.
90
     */
91
    public function minSize(int $bytes): self
92
    {
93
        $this->_minSize = $bytes;
94
95
        return $this;
96
    }
97
98
    /**
99
     * Rotate file.
100
     *
101
     * @return bool true if rotated was successful
102
     */
103
    public function rotate(string $filename): bool
104
    {
105
        $this->setFilename($filename);
106
107
        if (!$this->canRotate($filename)) {
108
            return false;
109
        }
110
111
        $fileTemporary = $this->_truncate
112
            ? $this->copyAndTruncate($filename)
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->copyAndTruncate($filename) targeting Cesargb\Log\Rotation::copyAndTruncate() 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...
113
            : $this->move($filename);
114
115
        if (is_null($fileTemporary)) {
116
            return false;
117
        }
118
119
        $fileTarget = $this->runProcessor(
120
            $filename,
121
            $fileTemporary
122
        );
123
124
        if (is_null($fileTarget)) {
125
            return false;
126
        }
127
128
        $fileTarget = $this->runCompress($fileTarget);
129
130
        $this->successful($filename, $fileTarget);
131
132
        return true;
133
    }
134
135
    /**
136
     * Run processor.
137
     */
138
    private function runProcessor(string $filenameSource, ?string $filenameTarget): ?string
139
    {
140
        $this->initProcessorFile($filenameSource);
141
142
        if (!$filenameTarget) {
143
            return null;
144
        }
145
146
        return $this->processor->handler($filenameTarget);
147
    }
148
149
    private function runCompress(string $filename): ?string
150
    {
151
        if (!$this->_compress) {
152
            return $filename;
153
        }
154
155
        $gz = new Gz();
156
157
        try {
158
            return $gz->handler($filename, $this->_compressLevel);
159
        } catch (Exception $error) {
160
            $this->exception($error);
161
162
            return null;
163
        }
164
    }
165
166
    /**
167
     * check if file need rotate.
168
     */
169
    private function canRotate(string $filename): bool
170
    {
171
        if (!file_exists($filename)) {
172
            $this->finished(sprintf('the file %s not exists.', $filename), $filename);
173
174
            return false;
175
        }
176
177
        if (!$this->fileIsValid($filename)) {
178
            $this->exception(
179
                new Exception(sprintf('the file %s not is valid.', $filename), 10)
180
            );
181
182
            return false;
183
        }
184
185
        return filesize($filename) > ($this->_minSize > 0 ? $this->_minSize : 0);
186
    }
187
188
    /**
189
     * Set original File to processor.
190
     */
191
    private function initProcessorFile(string $filename): void
192
    {
193
        $this->processor->setFilenameSource($filename);
194
    }
195
196
    /**
197
     * check if file is valid to rotate.
198
     */
199
    private function fileIsValid(string $filename): bool
200
    {
201
        return is_file($filename) && is_writable($filename);
202
    }
203
204
    /**
205
     * copy data to temp file and truncate.
206
     */
207
    private function copyAndTruncate(string $filename): ?string
208
    {
209
        clearstatcache();
210
211
        $filenameTarget = $this->getTempFilename(dirname($filename));
212
213
        if (!$filenameTarget) {
214
            return null;
215
        }
216
217
        $fd = $this->openFileWithLock($filename);
218
219
        if (!$fd) {
0 ignored issues
show
introduced by
$fd is of type null|resource, thus it always evaluated to false.
Loading history...
220
            return null;
221
        }
222
223
        if (!copy($filename, $filenameTarget)) {
224
            fclose($fd);
225
226
            $this->exception(
227
                new Exception(
228
                    sprintf('the file %s not can copy to temp file %s.', $filename, $filenameTarget),
229
                    22
230
                )
231
            );
232
233
            return null;
234
        }
235
236
        if (!ftruncate($fd, 0)) {
237
            fclose($fd);
238
239
            unlink($filenameTarget);
240
241
            $this->exception(
242
                new Exception(sprintf('the file %s not can truncate.', $filename), 23)
243
            );
244
245
            return null;
246
        }
247
248
        flock($fd, LOCK_UN);
249
250
        fflush($fd);
251
252
        fclose($fd);
253
254
        return $filenameTarget;
255
    }
256
257
    private function move(string $filename): ?string
258
    {
259
        clearstatcache();
260
261
        $filenameTarget = $this->getTempFilename(dirname($filename));
262
263
        if (!$filenameTarget) {
264
            return null;
265
        }
266
267
        if (!rename($filename, $filenameTarget)) {
268
            $this->exception(
269
                new Exception(
270
                    sprintf('the file %s not can move to temp file %s.', $filename, $filenameTarget),
271
                    22
272
                )
273
            );
274
275
            return null;
276
        }
277
278
        return $filenameTarget;
279
    }
280
281
    private function getTempFilename(string $path): ?string
282
    {
283
        $filename = tempnam($path, 'LOG');
284
285
        if ($filename === false) {
286
            $this->exception(
287
                new Exception(sprintf('the file %s not can create temp file.', $path), 19)
288
            );
289
290
            return null;
291
        }
292
293
        return $filename;
294
    }
295
296
    /**
297
     * @return null|resource
298
     */
299
    private function openFileWithLock(string $filename)
300
    {
301
        $fd = fopen($filename, 'r+');
302
303
        if ($fd === false) {
304
            $this->exception(
305
                new Exception(sprintf('the file %s not can open.', $filename), 20)
306
            );
307
308
            return null;
309
        }
310
311
        if (!flock($fd, LOCK_EX)) {
312
            fclose($fd);
313
314
            $this->exception(
315
                new Exception(sprintf('the file %s not can lock.', $filename), 21)
316
            );
317
318
            return null;
319
        }
320
321
        return $fd;
322
    }
323
}
324