Passed
Push — master ( b0c53b...bb5d18 )
by Alexander
02:03
created

FileTarget::checkWrittenResult()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 5.3197

Importance

Changes 0
Metric Value
cc 3
eloc 10
nc 3
nop 2
dl 0
loc 16
ccs 4
cts 11
cp 0.3636
crap 5.3197
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Log\Target\File;
6
7
use RuntimeException;
8
use Yiisoft\Files\FileHelper;
9
use Yiisoft\Log\Target;
10
11
use function chmod;
12
use function clearstatcache;
13
use function dirname;
14
use function error_get_last;
15
use function fclose;
16
use function file_exists;
17
use function file_put_contents;
18
use function flock;
19
use function fwrite;
20
use function sprintf;
21
use function strlen;
22
23
use const FILE_APPEND;
24
use const LOCK_EX;
25
use const LOCK_UN;
26
27
/**
28
 * FileTarget records log messages in a file.
29
 *
30
 * The log file is specified via {@see FileTarget::$logFile}.
31
 *
32
 * If {@see FileRotator} is used and the size of the log file exceeds {@see FileRotator::$maxFileSize},
33
 * a rotation will be performed, which renames the current log file by suffixing the file name with '.1'.
34
 * All existing log files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on.
35
 * If compression is enabled {@see FileRotator::$compressRotatedFiles}, the rotated files will be compressed
36
 * into the '.gz' format. The property {@see FileRotator::$maxFiles} specifies how many history files to keep.
37
 */
38
final class FileTarget extends Target
39
{
40
    /**
41
     * @var string The log file path. If not set, it will use the "/tmp/app.log" file.
42
     * The directory containing the log files will be automatically created if not existing.
43
     */
44
    private string $logFile;
45
46
    /**
47
     * @var int The permission to be set for newly created directories.
48
     * This value will be used by PHP chmod() function. No umask will be applied.
49
     * Defaults to 0775, meaning the directory is read-writable by owner and group,
50
     * but read-only for other users.
51
     */
52
    private int $dirMode;
53
54
    /**
55
     * @var int|null The permission to be set for newly created log files.
56
     * This value will be used by PHP chmod() function. No umask will be applied.
57
     * If not set, the permission will be determined by the current environment.
58
     */
59
    private ?int $fileMode;
60
61
    private ?FileRotatorInterface $rotator;
62
63
    /**
64
     * @param string $logFile The log file path. If not set, it will use the "/tmp/app.log" file.
65
     * @param FileRotatorInterface|null $rotator The instance that takes care of rotating files.
66
     * @param int $dirMode The permission to be set for newly created directories.
67
     * @param int|null $fileMode The permission to be set for newly created log files.
68
     */
69 16
    public function __construct(
70
        string $logFile = '/tmp/app.log',
71
        FileRotatorInterface $rotator = null,
72
        int $dirMode = 0775,
73
        int $fileMode = null
74
    ) {
75 16
        $this->logFile = $logFile;
76 16
        $this->rotator = $rotator;
77 16
        $this->dirMode = $dirMode;
78 16
        $this->fileMode = $fileMode;
79 16
        parent::__construct();
80 16
    }
81
82 15
    protected function export(): void
83
    {
84 15
        $logPath = dirname($this->logFile);
85
86 15
        if (!file_exists($logPath)) {
87 2
            FileHelper::createDirectory($logPath, $this->dirMode);
88
        }
89
90 15
        $text = $this->formatMessages("\n");
91 15
        $filePointer = FileHelper::openFile($this->logFile, 'ab');
92 14
        flock($filePointer, LOCK_EX);
93
94 14
        if ($this->rotator !== null) {
95
            // clear stat cache to ensure getting the real current file size and not a cached one
96
            // this may result in rotating twice when cached file size is used on subsequent calls
97 12
            clearstatcache();
98
        }
99
100 14
        if ($this->rotator !== null && $this->rotator->shouldRotateFile($this->logFile)) {
101 8
            flock($filePointer, LOCK_UN);
102 8
            fclose($filePointer);
103 8
            $this->rotator->rotateFile($this->logFile);
104 8
            $writeResult = file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX);
105
        } else {
106 14
            $writeResult = fwrite($filePointer, $text);
107 14
            flock($filePointer, LOCK_UN);
108 14
            fclose($filePointer);
109
        }
110
111 14
        $this->checkWrittenResult($writeResult, $text);
112
113 14
        if ($this->fileMode !== null) {
114 2
            chmod($this->logFile, $this->fileMode);
115
        }
116 14
    }
117
118
    /**
119
     * Checks the written result.
120
     *
121
     * @param false|int $writeResult The number of bytes written to the file, or FALSE if an error occurs.
122
     * @param string $text The text written to the file.
123
     *
124
     * @throws RuntimeException For unable to export log through file.
125
     */
126 14
    private function checkWrittenResult($writeResult, string $text): void
127
    {
128 14
        if ($writeResult === false) {
129
            throw new RuntimeException(sprintf(
130
                'Unable to export log through file: %s',
131
                error_get_last()['message'] ?? '',
132
            ));
133
        }
134
135 14
        $textSize = strlen($text);
136
137 14
        if ($writeResult < $textSize) {
138
            throw new RuntimeException(sprintf(
139
                'Unable to export whole log through file. Wrote %d out of %d bytes.',
140
                $writeResult,
141
                $textSize,
142
            ));
143
        }
144 14
    }
145
}
146