yiisoft /
log-target-file
| 1 | <?php |
||||
| 2 | |||||
| 3 | declare(strict_types=1); |
||||
| 4 | |||||
| 5 | namespace Yiisoft\Log\Target\File; |
||||
| 6 | |||||
| 7 | use InvalidArgumentException; |
||||
| 8 | use RuntimeException; |
||||
| 9 | use Yiisoft\Files\FileHelper; |
||||
| 10 | |||||
| 11 | use function chmod; |
||||
| 12 | use function extension_loaded; |
||||
| 13 | use function fclose; |
||||
| 14 | use function feof; |
||||
| 15 | use function file_exists; |
||||
| 16 | use function filesize; |
||||
| 17 | use function flock; |
||||
| 18 | use function fread; |
||||
| 19 | use function ftruncate; |
||||
| 20 | use function is_file; |
||||
| 21 | use function rename; |
||||
| 22 | use function sprintf; |
||||
| 23 | use function substr; |
||||
| 24 | use function unlink; |
||||
| 25 | |||||
| 26 | use const LOCK_EX; |
||||
| 27 | use const LOCK_SH; |
||||
| 28 | use const LOCK_UN; |
||||
| 29 | |||||
| 30 | /** |
||||
| 31 | * FileRotator takes care of rotating files. |
||||
| 32 | * |
||||
| 33 | * If the size of the file exceeds {@see FileRotator::$maxFileSize} (in kilo-bytes), |
||||
| 34 | * a rotation will be performed, which renames the current file by suffixing the file name with '.1'. |
||||
| 35 | * |
||||
| 36 | * All existing files are moved backwards by one place, i.e., '.2' to '.3', '.1' to '.2', and so on. If compression |
||||
| 37 | * is enabled {@see FileRotator::$compressRotatedFiles}, the rotated files will be compressed into the '.gz' format. |
||||
| 38 | * The property {@see FileRotator::$maxFiles} specifies how many history files to keep. |
||||
| 39 | */ |
||||
| 40 | final class FileRotator implements FileRotatorInterface |
||||
| 41 | { |
||||
| 42 | /** |
||||
| 43 | * The extension of the compressed rotated files. |
||||
| 44 | */ |
||||
| 45 | private const COMPRESS_EXTENSION = '.gz'; |
||||
| 46 | |||||
| 47 | /** |
||||
| 48 | * @var int The maximum file size, in kilo-bytes. Defaults to 10240, meaning 10MB. |
||||
| 49 | */ |
||||
| 50 | private int $maxFileSize; |
||||
| 51 | |||||
| 52 | /** |
||||
| 53 | * @var int The number of files used for rotation. Defaults to 5. |
||||
| 54 | */ |
||||
| 55 | private int $maxFiles; |
||||
| 56 | |||||
| 57 | /** |
||||
| 58 | * @var bool Whether to compress rotated files with gzip. Defaults to `false`. |
||||
| 59 | * |
||||
| 60 | * If compression is enabled, the rotated files will be compressed into the '.gz' format. |
||||
| 61 | */ |
||||
| 62 | private bool $compressRotatedFiles; |
||||
| 63 | |||||
| 64 | /** |
||||
| 65 | * @param int $maxFileSize The maximum file size, in kilobytes. Defaults to 10240, meaning 10MB. |
||||
| 66 | * @param int $maxFiles The number of files used for rotation. Defaults to 5. |
||||
| 67 | * @param int|null $fileMode The permission to be set for newly created files. This value will be used by PHP |
||||
| 68 | * `chmod()` function. No umask will be applied. If not set, the permission will be determined by the current |
||||
| 69 | * environment. |
||||
| 70 | * @param bool $compressRotatedFiles Whether to compress rotated files with gzip. |
||||
| 71 | */ |
||||
| 72 | 10 | public function __construct( |
|||
| 73 | int $maxFileSize = 10240, |
||||
| 74 | int $maxFiles = 5, |
||||
| 75 | private ?int $fileMode = null, |
||||
| 76 | bool $compressRotatedFiles = false |
||||
| 77 | ) { |
||||
| 78 | 10 | $this->checkCannotBeLowerThanOne($maxFileSize, '$maxFileSize'); |
|||
| 79 | 9 | $this->checkCannotBeLowerThanOne($maxFiles, '$maxFiles'); |
|||
| 80 | |||||
| 81 | 8 | $this->maxFileSize = $maxFileSize; |
|||
| 82 | 8 | $this->maxFiles = $maxFiles; |
|||
| 83 | |||||
| 84 | 8 | if ($compressRotatedFiles && !extension_loaded('zlib')) { |
|||
| 85 | throw new RuntimeException(sprintf( |
||||
| 86 | 'The %s requires the PHP extension "ext-zlib" to compress rotated files.', |
||||
| 87 | self::class, |
||||
| 88 | )); |
||||
| 89 | } |
||||
| 90 | |||||
| 91 | 8 | $this->compressRotatedFiles = $compressRotatedFiles; |
|||
| 92 | } |
||||
| 93 | |||||
| 94 | 4 | public function rotateFile(string $file): void |
|||
| 95 | { |
||||
| 96 | 4 | for ($i = $this->maxFiles; $i >= 0; --$i) { |
|||
| 97 | // `$i === 0` is the original file |
||||
| 98 | 4 | $rotateFile = $file . ($i === 0 ? '' : '.' . $i); |
|||
| 99 | 4 | $newFile = $file . '.' . ($i + 1); |
|||
| 100 | |||||
| 101 | 4 | if ($i === $this->maxFiles) { |
|||
| 102 | 4 | $this->safeRemove($this->compressRotatedFiles ? $rotateFile . self::COMPRESS_EXTENSION : $rotateFile); |
|||
| 103 | 4 | continue; |
|||
| 104 | } |
||||
| 105 | |||||
| 106 | 4 | if ($this->compressRotatedFiles && is_file($rotateFile . self::COMPRESS_EXTENSION)) { |
|||
| 107 | 1 | $this->rotate($rotateFile . self::COMPRESS_EXTENSION, $newFile . self::COMPRESS_EXTENSION); |
|||
| 108 | 1 | continue; |
|||
| 109 | } |
||||
| 110 | |||||
| 111 | 4 | if (!is_file($rotateFile)) { |
|||
| 112 | 4 | continue; |
|||
| 113 | } |
||||
| 114 | |||||
| 115 | 4 | $this->rotate($rotateFile, $newFile); |
|||
| 116 | |||||
| 117 | 4 | if ($i === 0) { |
|||
| 118 | 4 | $this->clear($rotateFile); |
|||
| 119 | } |
||||
| 120 | } |
||||
| 121 | } |
||||
| 122 | |||||
| 123 | 7 | public function shouldRotateFile(string $file): bool |
|||
| 124 | { |
||||
| 125 | 7 | return file_exists($file) && @filesize($file) > ($this->maxFileSize * 1024); |
|||
| 126 | } |
||||
| 127 | |||||
| 128 | /*** |
||||
| 129 | * Renames rotated file into new file. |
||||
| 130 | * |
||||
| 131 | * @param string $rotateFile |
||||
| 132 | * @param string $newFile |
||||
| 133 | */ |
||||
| 134 | 4 | private function rotate(string $rotateFile, string $newFile): void |
|||
| 135 | { |
||||
| 136 | 4 | $this->safeRemove($newFile); |
|||
| 137 | 4 | rename($rotateFile, $newFile); |
|||
| 138 | |||||
| 139 | 4 | if ($this->compressRotatedFiles && !$this->isCompressed($newFile)) { |
|||
| 140 | 2 | $this->compress($newFile); |
|||
| 141 | 2 | $newFile .= self::COMPRESS_EXTENSION; |
|||
| 142 | } |
||||
| 143 | |||||
| 144 | 4 | if ($this->fileMode !== null) { |
|||
| 145 | 2 | chmod($newFile, $this->fileMode); |
|||
| 146 | } |
||||
| 147 | } |
||||
| 148 | |||||
| 149 | /** |
||||
| 150 | * Compresses a file with gzip and renames it by appending `.gz` to the file. |
||||
| 151 | */ |
||||
| 152 | 2 | private function compress(string $file): void |
|||
| 153 | { |
||||
| 154 | 2 | $filePointer = FileHelper::openFile($file, 'rb'); |
|||
| 155 | 2 | flock($filePointer, LOCK_SH); |
|||
| 156 | 2 | $gzFile = $file . self::COMPRESS_EXTENSION; |
|||
| 157 | 2 | $gzFilePointer = gzopen($gzFile, 'wb9'); |
|||
| 158 | |||||
| 159 | 2 | while (!feof($filePointer)) { |
|||
| 160 | 2 | gzwrite($gzFilePointer, fread($filePointer, 8192)); |
|||
| 161 | } |
||||
| 162 | |||||
| 163 | 2 | flock($filePointer, LOCK_UN); |
|||
| 164 | 2 | fclose($filePointer); |
|||
| 165 | 2 | gzclose($gzFilePointer); |
|||
| 166 | 2 | @unlink($file); |
|||
|
0 ignored issues
–
show
|
|||||
| 167 | } |
||||
| 168 | |||||
| 169 | /*** |
||||
| 170 | * Clears the file without closing any other process open handles. |
||||
| 171 | * |
||||
| 172 | * @param string $file |
||||
| 173 | */ |
||||
| 174 | 4 | private function clear(string $file): void |
|||
| 175 | { |
||||
| 176 | 4 | $filePointer = FileHelper::openFile($file, 'ab'); |
|||
| 177 | |||||
| 178 | 4 | flock($filePointer, LOCK_EX); |
|||
| 179 | 4 | ftruncate($filePointer, 0); |
|||
| 180 | 4 | flock($filePointer, LOCK_UN); |
|||
| 181 | 4 | fclose($filePointer); |
|||
| 182 | } |
||||
| 183 | |||||
| 184 | /** |
||||
| 185 | * Checks the existence of file and removes it. |
||||
| 186 | */ |
||||
| 187 | 4 | private function safeRemove(string $file): void |
|||
| 188 | { |
||||
| 189 | 4 | if (is_file($file)) { |
|||
| 190 | 2 | @unlink($file); |
|||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
unlink(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 191 | } |
||||
| 192 | } |
||||
| 193 | |||||
| 194 | /** |
||||
| 195 | * Whether the file is compressed. |
||||
| 196 | */ |
||||
| 197 | 2 | private function isCompressed(string $file): bool |
|||
| 198 | { |
||||
| 199 | 2 | return substr($file, -3, 3) === self::COMPRESS_EXTENSION; |
|||
| 200 | } |
||||
| 201 | |||||
| 202 | /** |
||||
| 203 | * Checks that the value cannot be lower than one. |
||||
| 204 | * |
||||
| 205 | * @param int $value The value to be checked. |
||||
| 206 | * @param string $argumentName The name of the argument to check. |
||||
| 207 | */ |
||||
| 208 | 10 | private function checkCannotBeLowerThanOne(int $value, string $argumentName): void |
|||
| 209 | { |
||||
| 210 | 10 | if ($value < 1) { |
|||
| 211 | 2 | throw new InvalidArgumentException(sprintf( |
|||
| 212 | 2 | 'The argument "%s" cannot be lower than 1.', |
|||
| 213 | 2 | $argumentName, |
|||
| 214 | 2 | )); |
|||
| 215 | } |
||||
| 216 | } |
||||
| 217 | } |
||||
| 218 |
If you suppress an error, we recommend checking for the error condition explicitly: