RotatingFileHandler::getGlobPattern()   B
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 29
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 29
ccs 13
cts 13
cp 1
rs 8.8571
c 0
b 0
f 0
cc 2
eloc 15
nc 2
nop 1
crap 2
1
<?php
2
3
/**
4
 * AppserverIo\Logger\Handlers\RotatingFileHandler
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Tim Wagner <[email protected]>
15
 * @copyright 2015 TechDivision GmbH <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      http://github.com/appserver-io/logger
18
 * @link      http://www.appserver.io
19
 */
20
21
namespace AppserverIo\Logger\Handlers;
22
23
use Psr\Log\LogLevel;
24
use AppserverIo\Logger\LogMessageInterface;
25
26
/**
27
 * Handler implementation which allows for proper usage within a pthreads environment, as there
28
 * is a bug denying a call to a protected method within the same hierarchy if the classes are
29
 * already known by the parent thread context.
30
 *
31
 * The class got also extended to support file rotation based on a maximal file size.
32
 *
33
 * @author    Tim Wagner <[email protected]>
34
 * @copyright 2015 TechDivision GmbH <[email protected]>
35
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
36
 * @link      http://github.com/appserver-io/logger
37
 * @link      http://www.appserver.io
38
 */
39
class RotatingFileHandler extends CustomFileHandler
40
{
41
42
    /**
43
     * Placeholder for the "date" part of the file format
44
     *
45
     * @var string DATE_FORMAT_PLACEHOLDER
46
     */
47
    const DATE_FORMAT_PLACEHOLDER = '{date}';
48
49
    /**
50
     * Placeholder for the "original filename" part of the file format
51
     *
52
     * @var string FILENAME_FORMAT_PLACEHOLDER
53
     */
54
    const FILENAME_FORMAT_PLACEHOLDER = '{filename}';
55
56
    /**
57
     * The maximal possible file size (2Gb).
58
     * Limited as a precaution due to PHP integer type limitation on x86 systems
59
     *
60
     * @var integer MAX_FILE_SIZE
61
     */
62
    const MAX_FILE_SIZE = 2147483647;
63
64
    /**
65
     * Placeholder for the "size iterator" part of the file format
66
     *
67
     * @var string SIZE_FORMAT_PLACEHOLDER
68
     */
69
    const SIZE_FORMAT_PLACEHOLDER = '{sizeIterator}';
70
71
    /**
72
     * Format for the date shown within the rotated filename.
73
     * E.g. 'Y-m-d'
74
     *
75
     * @var string
76
     */
77
    protected $dateFormat;
78
79
    /**
80
     * Format of the filename after being rotated
81
     *
82
     * @var string
83
     */
84
    protected $filenameFormat;
85
86
    /**
87
     * Number of maximal files to keep.
88
     * Older files exceeding this limit will be deleted
89
     *
90
     * @var integer
91
     */
92
    protected $maxFiles;
93
94
    /**
95
     * Maximal size a log file might have after rotation gets triggered
96
     *
97
     * @var integer
98
     */
99
    protected $maxSize;
100
101
    /**
102
     * Whether or not a rotation has to take place at the next possible point in time
103
     *
104
     * @var boolean
105
     */
106
    protected $mustRotate;
107
108
    /**
109
     * Date at which the next rotation has to take place (if there are no size based rotations before)
110
     *
111
     * @var \DateTime
112
     */
113
    protected $nextRotationDate;
114
115
    /**
116
     * The original name of the file to rotate
117
     *
118
     * @var string
119
     */
120
    protected $originalLogFile;
121
122
    /**
123
     * Default constructor
124
     *
125
     * @param string       $logFile  Log file base name
126
     * @param integer      $logLevel The minimum logging level at which this handler will be triggered
127
     * @param integer      $maxFiles The maximal amount of files to keep (0 means unlimited)
128
     * @param integer|null $maxSize  The maximal size of a log file in byte (limited to a technical max of 2GB)
129
     */
130 8
    public function __construct($logFile, $logLevel = LogLevel::DEBUG, $maxFiles = 0, $maxSize = null)
131
    {
132
133
        // also construct the parent
134 8
        parent::__construct($logFile, $logLevel);
135
136
        // get the values passed via constructor
137 8
        $this->originalLogFile = $logFile;
138 8
        $this->maxFiles = (integer) $maxFiles;
139
140
        // also set the maximal size, but make sure we do not exceed the boundary
141 8
        if ($maxSize > RotatingFileHandler::MAX_FILE_SIZE || is_null($maxSize)) {
142 8
            $maxSize = RotatingFileHandler::MAX_FILE_SIZE;
143
        }
144
        // set the maximum size of log files
145 8
        $this->maxSize = (int) $maxSize;
146
147
        // preset the filename format
148 8
        $this->filenameFormat = self::FILENAME_FORMAT_PLACEHOLDER . '-' .
149 8
            self::DATE_FORMAT_PLACEHOLDER . '_' .
150 8
            self::SIZE_FORMAT_PLACEHOLDER;
151
152
        // set some default values
153 8
        $this->dateFormat = 'Y-m-d';
154 8
        $this->mustRotate = false;
155 8
        $this->nextRotationDate = new \DateTime('tomorrow');
156 8
    }
157
158
    /**
159
     * Will cleanup log files based on the value set for their maximal number
160
     *
161
     * @return void
162
     */
163 6
    protected function cleanupFiles()
164
    {
165
        // skip GC of old logs if files are unlimited
166 6
        if (0 === $this->maxFiles) {
167 5
            return;
168
        }
169
170 1
        $logFiles = glob($this->getGlobPattern());
171 1
        if ($this->maxFiles >= count($logFiles)) {
172
            // no files to remove
173 1
            return;
174
        }
175
176
        // Sorting the files by name to remove the older ones
177 1
        usort(
178 1
            $logFiles,
179 1
            function ($a, $b) {
180 1
                return strcmp($b, $a);
181 1
            }
182
        );
183
184
        // collect the files we have to archive and clean and prepare the archive's internal mapping
185 1
        $oldFiles = array();
186 1
        foreach (array_slice($logFiles, $this->maxFiles) as $oldFile) {
187 1
            $oldFiles[basename($oldFile)] = $oldFile;
188
        }
189
190
        // create an archive from the old files
191 1
        $dateTime = new \DateTime();
192 1
        $currentTime = $dateTime->format($this->getDateFormat());
193 1
        $phar = new \PharData($this->originalLogFile . $currentTime .  '.tar');
194 1
        $phar->buildFromIterator(new \ArrayIterator($oldFiles));
195
196
        // finally delete them as we got them in the archive
197 1
        foreach ($oldFiles as $oldFile) {
198 1
            if (is_writable($oldFile)) {
199 1
                unlink($oldFile);
200
            }
201
        }
202 1
    }
203
204
    /**
205
     * Will close the handler
206
     *
207
     * @return void
208
     */
209 6
    public function close()
210
    {
211
        // might do a rotation before closing
212 6
        if (true === $this->mustRotate) {
213 4
            $this->rotate();
214
        }
215 6
    }
216
217
    /**
218
     * Sets the next rotation date.
219
     *
220
     * @param \DateTime $nextRotationDate The next rotation date
221
     *
222
     * @return void
223
     */
224 1
    public function setNextRotationDate(\DateTime $nextRotationDate)
225
    {
226 1
        $this->nextRotationDate = $nextRotationDate;
227 1
    }
228
229
    /**
230
     * Will return the name of the file the next rotation will produce
231
     *
232
     * @return string
233
     */
234 4
    public function getRotatedFilename()
235
    {
236 4
        $fileInfo = pathinfo($this->logFile);
237 4
        $currentFilename = str_replace(
238
            array(
239 4
                self::FILENAME_FORMAT_PLACEHOLDER,
240 4
                self::DATE_FORMAT_PLACEHOLDER,
241 4
                self::SIZE_FORMAT_PLACEHOLDER
242
            ),
243
            array(
244 4
                $fileInfo['filename'],
245 4
                date($this->dateFormat),
246 4
                $this->getCurrentSizeIteration() // $this->currentSizeIteration
247
            ),
248 4
            $fileInfo['dirname'] . '/' . $this->filenameFormat
249
        );
250 4
        if (!empty($fileInfo['extension'])) {
251 4
            $currentFilename .= '.' . $fileInfo['extension'];
252
        }
253 4
        return $currentFilename;
254
    }
255
256
    /**
257
     * Will return the currently used iteration based on a file's size.
258
     *
259
     * @return integer The number of logfiles already exists
260
     */
261 4
    protected function getCurrentSizeIteration()
262
    {
263
264
        // load an iterator the current log files
265 4
        $logFiles = glob($this->getGlobPattern(date($this->dateFormat)));
266
267 4
        $fileCount = count($logFiles); // count the files
268 4
        if ($fileCount === 0) {
269 4
            return 1;
270
        } else {
271 3
            return count($logFiles) + 1;
272
        }
273
    }
274
275
    /**
276
     * Getter for the date format to store the logfiles under.
277
     *
278
     * @return string The date format to store the logfiles under
279
     */
280 5
    public function getDateFormat()
281
    {
282 5
        return $this->dateFormat;
283
    }
284
285
    /**
286
     * Getter for the file format to store the logfiles under.
287
     *
288
     * @return string The file format to store the logfiles under
289
     */
290 1
    public function getFilenameFormat()
291
    {
292 1
        return $this->filenameFormat;
293
    }
294
295
    /**
296
     * Will return a glob pattern with which log files belonging to the currently rotated file can be found
297
     *
298
     * @param string|\DateTime $dateSpecifier Might specify a specific date to search files for
299
     *
300
     * @return string
301
     */
302 5
    public function getGlobPattern($dateSpecifier = '*')
303
    {
304
305
        // load the file information
306 5
        $fileInfo = pathinfo($this->logFile);
307
308
        // create a glob expression to find all log files
309 5
        $glob = str_replace(
310
            array(
311 5
                self::FILENAME_FORMAT_PLACEHOLDER,
312 5
                self::DATE_FORMAT_PLACEHOLDER,
313 5
                self::SIZE_FORMAT_PLACEHOLDER
314
            ),
315
            array(
316 5
                $fileInfo['filename'],
317 5
                $dateSpecifier,
318 5
                '*'
319
            ),
320 5
            $fileInfo['dirname'] . '/' . $this->filenameFormat
321
        );
322
323
        // append the file extension if available
324 5
        if (empty($fileInfo['extension']) === false) {
325 5
            $glob .= '.' . $fileInfo['extension'];
326
        }
327
328
        // return the glob expression
329 5
        return $glob;
330
    }
331
332
    /**
333
     * Does the rotation of the log file which includes updating the currently used filename as well as cleaning up
334
     * the log directory
335
     *
336
     * @return void
337
     */
338 4
    protected function rotate()
339
    {
340
        // update filename
341 4
        rename($this->logFile, $this->getRotatedFilename());
342
343 4
        $this->nextRotationDate = new \DateTime('tomorrow');
344 4
        $this->mustRotate = false;
345 4
    }
346
347
    /**
348
     * Setter for the date format
349
     *
350
     * @param string $dateFormat Form that date will be shown in
351
     *
352
     * @return void
353
     */
354 1
    public function setDateFormat($dateFormat)
355
    {
356 1
        $this->dateFormat = $dateFormat;
357 1
        $this->close();
358 1
    }
359
360
    /**
361
     * Setter for the file format
362
     * If setting this please make use of the defined format placeholder constants
363
     *
364
     * @param string $filenameFormat New format to be used
365
     *
366
     * @return void
367
     */
368 1
    public function setFilenameFormat($filenameFormat)
369
    {
370 1
        $this->filenameFormat = $filenameFormat;
371 1
        $this->close();
372 1
    }
373
374
    /**
375
     * Handles the log message.
376
     *
377
     * @param \AppserverIo\Logger\LogMessageInterface $logMessage The message to be handled
378
     *
379
     * @return void
380
     */
381 6
    public function handle(LogMessageInterface $logMessage)
382
    {
383
384
        // do we have to rotate based on the current date or the file's size?
385 6
        if ($this->nextRotationDate < new \DateTime()) {
386 1
            $this->mustRotate = true;
387 1
            $this->close();
388 6
        } elseif (file_exists($this->logFile) && filesize($this->logFile) >= $this->maxSize) {
389 3
            $this->mustRotate = true;
390 3
            $this->close();
391
        }
392
393
        // let the parent class handle the log message
394 6
        parent::handle($logMessage);
395
396
        // cleanup the files we might not want
397 6
        $this->cleanupFiles();
398 6
    }
399
}
400