├── .phpunit-watcher.yml ├── .styleci.yml ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config ├── di.php └── params.php ├── infection.json.dist ├── psalm-8.3.xml ├── psalm.xml ├── rector.php └── src ├── FileRotator.php ├── FileRotatorInterface.php └── FileTarget.php /.phpunit-watcher.yml: -------------------------------------------------------------------------------- 1 | watch: 2 | directories: 3 | - src 4 | - tests 5 | fileMask: '*.php' 6 | notifications: 7 | passingTests: false 8 | failingTests: false 9 | phpunit: 10 | binaryPath: vendor/bin/phpunit 11 | timeout: 180 12 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr12 2 | risky: true 3 | 4 | version: 8.1 5 | 6 | finder: 7 | exclude: 8 | - docs 9 | - vendor 10 | 11 | enabled: 12 | - alpha_ordered_traits 13 | - array_indentation 14 | - array_push 15 | - combine_consecutive_issets 16 | - combine_consecutive_unsets 17 | - combine_nested_dirname 18 | - declare_strict_types 19 | - dir_constant 20 | - fully_qualified_strict_types 21 | - function_to_constant 22 | - hash_to_slash_comment 23 | - is_null 24 | - logical_operators 25 | - magic_constant_casing 26 | - magic_method_casing 27 | - method_separation 28 | - modernize_types_casting 29 | - native_function_casing 30 | - native_function_type_declaration_casing 31 | - no_alias_functions 32 | - no_empty_comment 33 | - no_empty_phpdoc 34 | - no_empty_statement 35 | - no_extra_block_blank_lines 36 | - no_short_bool_cast 37 | - no_superfluous_elseif 38 | - no_unneeded_control_parentheses 39 | - no_unneeded_curly_braces 40 | - no_unneeded_final_method 41 | - no_unset_cast 42 | - no_unused_imports 43 | - no_unused_lambda_imports 44 | - no_useless_else 45 | - no_useless_return 46 | - normalize_index_brace 47 | - php_unit_dedicate_assert 48 | - php_unit_dedicate_assert_internal_type 49 | - php_unit_expectation 50 | - php_unit_mock 51 | - php_unit_mock_short_will_return 52 | - php_unit_namespaced 53 | - php_unit_no_expectation_annotation 54 | - phpdoc_no_empty_return 55 | - phpdoc_no_useless_inheritdoc 56 | - phpdoc_order 57 | - phpdoc_property 58 | - phpdoc_scalar 59 | - phpdoc_singular_inheritdoc 60 | - phpdoc_trim 61 | - phpdoc_trim_consecutive_blank_line_separation 62 | - phpdoc_type_to_var 63 | - phpdoc_types 64 | - phpdoc_types_order 65 | - print_to_echo 66 | - regular_callable_call 67 | - return_assignment 68 | - self_accessor 69 | - self_static_accessor 70 | - set_type_to_cast 71 | - short_array_syntax 72 | - short_list_syntax 73 | - simplified_if_return 74 | - single_quote 75 | - standardize_not_equals 76 | - ternary_to_null_coalescing 77 | - trailing_comma_in_multiline_array 78 | - unalign_double_arrow 79 | - unalign_equals 80 | - empty_loop_body_braces 81 | - integer_literal_case 82 | - union_type_without_spaces 83 | 84 | disabled: 85 | - function_declaration 86 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Yii Logging Library - File Target Change Log 2 | 3 | ## 3.0.1 under development 4 | 5 | - Enh #63: Add 'categories', 'except' and 'exportInterval' setters to default config (@olegbaturin) 6 | - Bug #46: Replace rotate by rename to rotate by copy (@vjik) 7 | 8 | ## 3.0.0 February 17, 2023 9 | 10 | - Chg #53: Adapt configuration group names to Yii conventions (@vjik) 11 | 12 | ## 2.0.1 July 25, 2022 13 | 14 | - Bug #45: Fix definitions config (@rustamwin) 15 | 16 | ## 2.0.0 July 18, 2022 17 | 18 | - Enh #40: Add support for `yiisoft/files` of version `^2.0` (@DplusG) 19 | - Bug #38: Drop `rotateByCopy`, always rotate by rename (@DplusG) 20 | - Bug #43: Add `ext-zlib` to composer requirements (@DplusG) 21 | 22 | ## 1.1.0 May 23, 2022 23 | 24 | - Chg #36: Raise the minimum `yiisoft/log` version to `^2.0` and the minimum PHP version to 8.0 (@rustamwin) 25 | 26 | ## 1.0.4 August 26, 2021 27 | 28 | - Bug #35: Remove `Psr\Log\LoggerInterface` definition from configuration for using multiple targets to application (@devanych) 29 | 30 | ## 1.0.3 April 13, 2021 31 | 32 | - Chg: Adjust config for yiisoft/factory changes (@vjik, @samdark) 33 | 34 | ## 1.0.2 March 23, 2021 35 | 36 | - Chg: Adjust config for new config plugin (@samdark) 37 | 38 | ## 1.0.1 February 22, 2021 39 | 40 | - Chg #29: Replace the default maximum file size for file rotation to `10` megabytes in `params.php` (@devanych) 41 | 42 | ## 1.0.0 February 11, 2021 43 | 44 | Initial release. 45 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2008 by Yii Software () 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in 12 | the documentation and/or other materials provided with the 13 | distribution. 14 | * Neither the name of Yii Software nor the names of its 15 | contributors may be used to endorse or promote products derived 16 | from this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 21 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 22 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 23 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 24 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 25 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 27 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 28 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 29 | POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Yii 4 | 5 |

Yii Logging Library - File Target

6 |
7 |

8 | 9 | [![Latest Stable Version](https://poser.pugx.org/yiisoft/log-target-file/v/stable.png)](https://packagist.org/packages/yiisoft/log-target-file) 10 | [![Total Downloads](https://poser.pugx.org/yiisoft/log-target-file/downloads.png)](https://packagist.org/packages/yiisoft/log-target-file) 11 | [![Build status](https://github.com/yiisoft/log-target-file/workflows/build/badge.svg)](https://github.com/yiisoft/log-target-file/actions?query=workflow%3Abuild) 12 | [![Code coverage](https://codecov.io/gh/yiisoft/log-target-file/graph/badge.svg?token=OWRDVB01EH)](https://codecov.io/gh/yiisoft/log-target-file) 13 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fyiisoft%2Flog-target-file%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/yiisoft/log-target-file/master) 14 | [![static analysis](https://github.com/yiisoft/log-target-file/workflows/static%20analysis/badge.svg)](https://github.com/yiisoft/log-target-file/actions?query=workflow%3A%22static+analysis%22) 15 | [![type-coverage](https://shepherd.dev/github/yiisoft/log-target-file/coverage.svg)](https://shepherd.dev/github/yiisoft/log-target-file) 16 | 17 | This package provides the File target for the [yiisoft/log](https://github.com/yiisoft/log). The target: 18 | 19 | - records log messages in a file 20 | - allows you to configure log files rotation 21 | - provides the ability to compress rotated log files 22 | 23 | ## Requirements 24 | 25 | - PHP 8.0 or higher. 26 | 27 | ## Installation 28 | 29 | The package could be installed with [Composer](https://getcomposer.org): 30 | 31 | ```shell 32 | composer require yiisoft/log-target-file 33 | ``` 34 | 35 | ## General usage 36 | 37 | Creating a rotator: 38 | 39 | ```php 40 | $rotator = new \Yiisoft\Log\Target\File\FileRotator( 41 | $maxFileSize, 42 | $maxFiles, 43 | $fileMode, 44 | $compressRotatedFiles 45 | ); 46 | ``` 47 | 48 | - `$maxFileSize (int)` - The maximum file size, in kilo-bytes. Defaults to `10240`, meaning 10MB. 49 | - `$maxFiles (int)` - The number of files used for rotation. Defaults to `5`. 50 | - `$fileMode (int|null)` - The permission to be set for newly created files. Defaults to `null`. 51 | - `$compressRotatedFiles (bool)` - Whether to compress rotated files with gzip. Defaults to `false`. 52 | 53 | Creating a target: 54 | 55 | ```php 56 | $fileTarget = new \Yiisoft\Log\Target\File\FileTarget( 57 | $logFile, 58 | $rotator, 59 | $dirMode, 60 | $fileMode 61 | ); 62 | ``` 63 | 64 | - `$logFile (string)` - The log file path. Defaults to `/tmp/app.log`. 65 | - `$rotator (\Yiisoft\Log\Target\File\FileRotatorInterface|null)` - Defaults to `null`, 66 | which means that log files will not be rotated. 67 | - `$dirMode (int)` - The permission to be set for newly created directories. Defaults to `0775`. 68 | - `$fileMode (int|null)` - The permission to be set for newly created log files. Defaults to `null`. 69 | 70 | Creating a logger: 71 | 72 | ```php 73 | $logger = new \Yiisoft\Log\Logger([$fileTarget]); 74 | ``` 75 | 76 | For use in the [Yii framework](https://www.yiiframework.com/), see the configuration files: 77 | - [`config/di.php`](https://github.com/yiisoft/log-target-file/blob/master/config/di.php) 78 | - [`config/params.php`](https://github.com/yiisoft/log-target-file/blob/master/config/params.php) 79 | 80 | ## Documentation 81 | 82 | For a description of using the logger, see the [yiisoft/log](https://github.com/yiisoft/log) package. 83 | 84 | - [Yii guide to logging](https://github.com/yiisoft/docs/blob/master/guide/en/runtime/logging.md) 85 | - [Internals](docs/internals.md) 86 | 87 | If you need help or have a question, the [Yii Forum](https://forum.yiiframework.com/c/yii-3-0/63) is a good place for that. 88 | You may also check out other [Yii Community Resources](https://www.yiiframework.com/community). 89 | 90 | ## License 91 | 92 | The Yii Logging Library - File Target is free software. It is released under the terms of the BSD License. 93 | Please see [`LICENSE`](./LICENSE.md) for more information. 94 | 95 | Maintained by [Yii Software](https://www.yiiframework.com/). 96 | 97 | ## Support the project 98 | 99 | [![Open Collective](https://img.shields.io/badge/Open%20Collective-sponsor-7eadf1?logo=open%20collective&logoColor=7eadf1&labelColor=555555)](https://opencollective.com/yiisoft) 100 | 101 | ## Follow updates 102 | 103 | [![Official website](https://img.shields.io/badge/Powered_by-Yii_Framework-green.svg?style=flat)](https://www.yiiframework.com/) 104 | [![Twitter](https://img.shields.io/badge/twitter-follow-1DA1F2?logo=twitter&logoColor=1DA1F2&labelColor=555555?style=flat)](https://twitter.com/yiiframework) 105 | [![Telegram](https://img.shields.io/badge/telegram-join-1DA1F2?style=flat&logo=telegram)](https://t.me/yii3en) 106 | [![Facebook](https://img.shields.io/badge/facebook-join-1DA1F2?style=flat&logo=facebook&logoColor=ffffff)](https://www.facebook.com/groups/yiitalk) 107 | [![Slack](https://img.shields.io/badge/slack-join-1DA1F2?style=flat&logo=slack)](https://yiiframework.com/go/slack) 108 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yiisoft/log-target-file", 3 | "type": "library", 4 | "description": "Yii Logging Library - File Target", 5 | "keywords": [ 6 | "yii", 7 | "framework", 8 | "log", 9 | "logger" 10 | ], 11 | "homepage": "https://www.yiiframework.com/", 12 | "license": "BSD-3-Clause", 13 | "support": { 14 | "issues": "https://github.com/yiisoft/log-target-file/issues", 15 | "source": "https://github.com/yiisoft/log-target-file", 16 | "forum": "https://www.yiiframework.com/forum/", 17 | "wiki": "https://www.yiiframework.com/wiki/", 18 | "irc": "ircs://irc.libera.chat:6697/yii", 19 | "chat": "https://t.me/yii3en" 20 | }, 21 | "funding": [ 22 | { 23 | "type": "opencollective", 24 | "url": "https://opencollective.com/yiisoft" 25 | }, 26 | { 27 | "type": "github", 28 | "url": "https://github.com/sponsors/yiisoft" 29 | } 30 | ], 31 | "require": { 32 | "php": "^8.0", 33 | "ext-zlib": "*", 34 | "yiisoft/files": "^1.0|^2.0", 35 | "yiisoft/log": "^2.0" 36 | }, 37 | "require-dev": { 38 | "maglnet/composer-require-checker": "^4.2", 39 | "phpunit/phpunit": "^9.5", 40 | "rector/rector": "^2.0", 41 | "roave/infection-static-analysis-plugin": "^1.25", 42 | "spatie/phpunit-watcher": "^1.23", 43 | "vimeo/psalm": "^4.30|^5.24", 44 | "yiisoft/aliases": "^3.0", 45 | "yiisoft/di": "^1.0" 46 | }, 47 | "suggest": { 48 | "ext-zlib": "Enabling gzip compression of rotated files." 49 | }, 50 | "autoload": { 51 | "psr-4": { 52 | "Yiisoft\\Log\\Target\\File\\": "src" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "Yiisoft\\Log\\Target\\File\\Tests\\": "tests" 58 | } 59 | }, 60 | "extra": { 61 | "config-plugin-options": { 62 | "source-directory": "config" 63 | }, 64 | "config-plugin": { 65 | "di": "di.php", 66 | "params": "params.php" 67 | } 68 | }, 69 | "config": { 70 | "sort-packages": true, 71 | "allow-plugins": { 72 | "infection/extension-installer": true, 73 | "composer/package-versions-deprecated": true 74 | } 75 | }, 76 | "scripts": { 77 | "test": "phpunit --testdox --no-interaction", 78 | "test-watch": "phpunit-watcher watch" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /config/di.php: -------------------------------------------------------------------------------- 1 | [ 14 | 'class' => FileRotator::class, 15 | '__construct()' => [ 16 | $params['yiisoft/log-target-file']['fileRotator']['maxFileSize'], 17 | $params['yiisoft/log-target-file']['fileRotator']['maxFiles'], 18 | $params['yiisoft/log-target-file']['fileRotator']['fileMode'], 19 | $params['yiisoft/log-target-file']['fileRotator']['compressRotatedFiles'], 20 | ], 21 | ], 22 | 23 | FileTarget::class => static function (Aliases $aliases, FileRotatorInterface $fileRotator) use ($params) { 24 | $fileTarget = new FileTarget( 25 | $aliases->get($params['yiisoft/log-target-file']['fileTarget']['file']), 26 | $fileRotator, 27 | $params['yiisoft/log-target-file']['fileTarget']['dirMode'], 28 | $params['yiisoft/log-target-file']['fileTarget']['fileMode'], 29 | ); 30 | 31 | $fileTarget->setLevels($params['yiisoft/log-target-file']['fileTarget']['levels']); 32 | $fileTarget->setCategories($params['yiisoft/log-target-file']['fileTarget']['categories']); 33 | $fileTarget->setExcept($params['yiisoft/log-target-file']['fileTarget']['except']); 34 | $fileTarget->setExportInterval($params['yiisoft/log-target-file']['fileTarget']['exportInterval']); 35 | 36 | return $fileTarget; 37 | }, 38 | ]; 39 | -------------------------------------------------------------------------------- /config/params.php: -------------------------------------------------------------------------------- 1 | [ 9 | 'fileTarget' => [ 10 | 'file' => '@runtime/logs/app.log', 11 | 'levels' => [ 12 | LogLevel::EMERGENCY, 13 | LogLevel::ERROR, 14 | LogLevel::WARNING, 15 | LogLevel::INFO, 16 | LogLevel::DEBUG, 17 | ], 18 | 'categories' => [], 19 | 'except' => [], 20 | 'exportInterval' => 1000, 21 | 'dirMode' => 0755, 22 | 'fileMode' => null, 23 | ], 24 | 'fileRotator' => [ 25 | 'maxFileSize' => 10240, 26 | 'maxFiles' => 5, 27 | 'fileMode' => null, 28 | 'compressRotatedFiles' => false, 29 | ], 30 | ], 31 | ]; 32 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ] 6 | }, 7 | "logs": { 8 | "text": "php:\/\/stderr", 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /psalm-8.3.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /rector.php: -------------------------------------------------------------------------------- 1 | paths([ 11 | __DIR__ . '/src', 12 | __DIR__ . '/tests', 13 | ]); 14 | 15 | // register a single rule 16 | $rectorConfig->rule(InlineConstructorDefaultToPropertyRector::class); 17 | 18 | // define sets of rules 19 | $rectorConfig->sets([ 20 | LevelSetList::UP_TO_PHP_80, 21 | ]); 22 | }; 23 | -------------------------------------------------------------------------------- /src/FileRotator.php: -------------------------------------------------------------------------------- 1 | checkCannotBeLowerThanOne($maxFileSize, '$maxFileSize'); 78 | $this->checkCannotBeLowerThanOne($maxFiles, '$maxFiles'); 79 | 80 | $this->maxFileSize = $maxFileSize; 81 | $this->maxFiles = $maxFiles; 82 | 83 | if ($compressRotatedFiles && !extension_loaded('zlib')) { 84 | throw new RuntimeException(sprintf( 85 | 'The %s requires the PHP extension "ext-zlib" to compress rotated files.', 86 | self::class, 87 | )); 88 | } 89 | 90 | $this->compressRotatedFiles = $compressRotatedFiles; 91 | } 92 | 93 | public function rotateFile(string $file): void 94 | { 95 | for ($i = $this->maxFiles; $i >= 0; --$i) { 96 | // `$i === 0` is the original file 97 | $rotateFile = $file . ($i === 0 ? '' : '.' . $i); 98 | $newFile = $file . '.' . ($i + 1); 99 | 100 | if ($i === $this->maxFiles) { 101 | $this->safeRemove($this->compressRotatedFiles ? $rotateFile . self::COMPRESS_EXTENSION : $rotateFile); 102 | continue; 103 | } 104 | 105 | if ($this->compressRotatedFiles && is_file($rotateFile . self::COMPRESS_EXTENSION)) { 106 | $this->rotate($rotateFile . self::COMPRESS_EXTENSION, $newFile . self::COMPRESS_EXTENSION); 107 | continue; 108 | } 109 | 110 | if (!is_file($rotateFile)) { 111 | continue; 112 | } 113 | 114 | $this->rotate($rotateFile, $newFile); 115 | 116 | if ($i === 0) { 117 | $this->clear($rotateFile); 118 | } 119 | } 120 | } 121 | 122 | public function shouldRotateFile(string $file): bool 123 | { 124 | return file_exists($file) && @filesize($file) > ($this->maxFileSize * 1024); 125 | } 126 | 127 | /*** 128 | * Copy rotated file into new file. 129 | */ 130 | private function rotate(string $rotateFile, string $newFile): void 131 | { 132 | copy($rotateFile, $newFile); 133 | 134 | if ($this->compressRotatedFiles && !$this->isCompressed($newFile)) { 135 | $this->compress($newFile); 136 | $newFile .= self::COMPRESS_EXTENSION; 137 | } 138 | 139 | if ($this->fileMode !== null) { 140 | chmod($newFile, $this->fileMode); 141 | } 142 | } 143 | 144 | /** 145 | * Compresses a file with gzip and renames it by appending `.gz` to the file. 146 | */ 147 | private function compress(string $file): void 148 | { 149 | $filePointer = FileHelper::openFile($file, 'rb'); 150 | flock($filePointer, LOCK_SH); 151 | $gzFile = $file . self::COMPRESS_EXTENSION; 152 | $gzFilePointer = gzopen($gzFile, 'wb9'); 153 | 154 | while (!feof($filePointer)) { 155 | gzwrite($gzFilePointer, fread($filePointer, 8192)); 156 | } 157 | 158 | flock($filePointer, LOCK_UN); 159 | fclose($filePointer); 160 | gzclose($gzFilePointer); 161 | @unlink($file); 162 | } 163 | 164 | /*** 165 | * Clears the file without closing any other process open handles. 166 | * 167 | * @param string $file 168 | */ 169 | private function clear(string $file): void 170 | { 171 | $filePointer = FileHelper::openFile($file, 'ab'); 172 | 173 | flock($filePointer, LOCK_EX); 174 | ftruncate($filePointer, 0); 175 | flock($filePointer, LOCK_UN); 176 | fclose($filePointer); 177 | } 178 | 179 | /** 180 | * Checks the existence of file and removes it. 181 | */ 182 | private function safeRemove(string $file): void 183 | { 184 | if (is_file($file)) { 185 | @unlink($file); 186 | } 187 | } 188 | 189 | /** 190 | * Whether the file is compressed. 191 | */ 192 | private function isCompressed(string $file): bool 193 | { 194 | return substr($file, -3, 3) === self::COMPRESS_EXTENSION; 195 | } 196 | 197 | /** 198 | * Checks that the value cannot be lower than one. 199 | * 200 | * @param int $value The value to be checked. 201 | * @param string $argumentName The name of the argument to check. 202 | */ 203 | private function checkCannotBeLowerThanOne(int $value, string $argumentName): void 204 | { 205 | if ($value < 1) { 206 | throw new InvalidArgumentException(sprintf( 207 | 'The argument "%s" cannot be lower than 1.', 208 | $argumentName, 209 | )); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/FileRotatorInterface.php: -------------------------------------------------------------------------------- 1 | logFile); 63 | 64 | if (!file_exists($logPath)) { 65 | FileHelper::ensureDirectory($logPath, $this->dirMode); 66 | } 67 | 68 | $text = $this->formatMessages("\n"); 69 | $filePointer = FileHelper::openFile($this->logFile, 'ab'); 70 | flock($filePointer, LOCK_EX); 71 | 72 | if ($this->rotator !== null) { 73 | // clear stat cache to ensure getting the real current file size and not a cached one 74 | // this may result in rotating twice when cached file size is used on subsequent calls 75 | clearstatcache(); 76 | } 77 | 78 | if ($this->rotator !== null && $this->rotator->shouldRotateFile($this->logFile)) { 79 | flock($filePointer, LOCK_UN); 80 | fclose($filePointer); 81 | $this->rotator->rotateFile($this->logFile); 82 | $writeResult = file_put_contents($this->logFile, $text, FILE_APPEND | LOCK_EX); 83 | } else { 84 | $writeResult = fwrite($filePointer, $text); 85 | flock($filePointer, LOCK_UN); 86 | fclose($filePointer); 87 | } 88 | 89 | $this->checkWrittenResult($writeResult, $text); 90 | 91 | if ($this->fileMode !== null) { 92 | chmod($this->logFile, $this->fileMode); 93 | } 94 | } 95 | 96 | /** 97 | * Checks the written result. 98 | * 99 | * @param false|int $writeResult The number of bytes written to the file, or FALSE if an error occurs. 100 | * @param string $text The text written to the file. 101 | * 102 | * @throws RuntimeException For unable to export log through file. 103 | */ 104 | private function checkWrittenResult(false|int $writeResult, string $text): void 105 | { 106 | if ($writeResult === false) { 107 | throw new RuntimeException(sprintf( 108 | 'Unable to export log through file: %s', 109 | error_get_last()['message'] ?? '', 110 | )); 111 | } 112 | 113 | $textSize = strlen($text); 114 | 115 | if ($writeResult < $textSize) { 116 | throw new RuntimeException(sprintf( 117 | 'Unable to export whole log through file. Wrote %d out of %d bytes.', 118 | $writeResult, 119 | $textSize, 120 | )); 121 | } 122 | } 123 | } 124 | --------------------------------------------------------------------------------