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