FileLogger   A
last analyzed

Complexity

Total Complexity 29

Size/Duplication

Total Lines 260
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 68
c 2
b 0
f 1
dl 0
loc 260
rs 10
wmc 29

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
A open() 0 7 3
A __destruct() 0 4 2
A getTimestamp() 0 8 1
B handle() 0 34 7
A log() 0 7 1
A logMessage() 0 9 1
B exchangeProcess() 0 30 11
A cleanFileNames() 0 6 1
1
<?php
2
3
/**
4
 * Lenevor Framework
5
 *
6
 * LICENSE
7
 *
8
 * This source file is subject to the new BSD license that is bundled
9
 * with this package in the file license.md.
10
 * It is also available through the world-wide-web at this URL:
11
 * https://lenevor.com/license
12
 * If you did not receive a copy of the license and are unable to
13
 * obtain it through the world-wide-web, please send an email
14
 * to [email protected] so we can send you a copy immediately.
15
 *
16
 * @package     Lenevor
17
 * @subpackage  Base
18
 * @link        https://lenevor.com
19
 * @copyright   Copyright (c) 2019 - 2021 Alexander Campo <[email protected]>
20
 * @license     https://opensource.org/licenses/BSD-3-Clause New BSD license or see https://lenevor.com/license or see /license.md
21
 */
22
23
namespace Syscodes\Log\Handlers;
24
25
use DateTime;
26
use Throwable;
27
use Syscodes\Support\Chronos;
28
use Syscodes\Contracts\Log\Handler;
29
use Syscodes\Log\Exceptions\LogException;
30
use Syscodes\Log\Concerns\ParseLogEnvironment;
31
32
/**
33
 * The Lenevor Logger of errors.
34
 * 
35
 * @author Alexander Campo <[email protected]>
36
 */
37
class FileLogger implements Handler
38
{
39
    use ParseLogEnvironment;
40
    
41
    /**
42
	 * The application implementation.
43
	 * 
44
	 * @var \Syscodes\Contracts\Core\Application $app
45
	 */
46
    protected $app;
47
48
    /**
49
     * Format of the timestamp for log files.
50
     * 
51
     * @var string $logDateFormat
52
     */
53
    protected $logDateFormat = 'Y-m-d H:i:s';
54
55
    /**
56
     * Extension to use for log files.
57
     * 
58
     * @var string $logExtension
59
     */
60
    protected $logFileExtension;
61
62
    /**
63
     * Path to the log file.
64
     * 
65
     * @var string $logFilePath
66
     */
67
    protected $logFilePath;
68
69
    /**
70
     * Octal notation for default permissions of the log file.
71
     * 
72
     * @var int $logFilePermissions
73
     */
74
    protected $logFilePermissions;
75
76
    /**
77
     * Caches instances of the handlers.
78
     * 
79
     * @var string $logHandler
80
     */
81
    protected $logHandler;
82
83
    /**
84
     * Write message in log file.
85
     * 
86
     * @var string $logMessage
87
     */
88
    protected $logMessage;
89
90
    /**
91
     * Constructor. The FileLogger class instance.
92
     * 
93
     * @param  array  $config
94
     * @param  \Syscodes\Contracts\Core\Application  $app
95
     * 
96
     * @return void
97
     */
98
    public function __construct(array $config = [], $app)
99
    {
100
        $this->app = $app;
101
102
        $this->logFilePath = $config['path'].DIRECTORY_SEPARATOR ?? STO_PATH.'logs'.DIRECTORY_SEPARATOR;
0 ignored issues
show
Bug introduced by
The constant Syscodes\Log\Handlers\STO_PATH was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
103
104
        $this->logFileExtension = empty($config['extension']) ? 'log' : $config['extension'];
105
        $this->logFileExtension = ltrim($this->logFileExtension, '.');
106
107
        $this->logFilePermissions = $config['permission'] ?? 0644;
108
    }
109
110
    /**
111
     * Destructor. Close.
112
     * 
113
     * @return bool
114
     */
115
    public function __destruct()
116
    {
117
        if ($this->logHandler) {
118
            fclose($this->logHandler);
0 ignored issues
show
Bug introduced by
$this->logHandler of type string is incompatible with the type resource expected by parameter $stream of fclose(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

118
            fclose(/** @scrutinizer ignore-type */ $this->logHandler);
Loading history...
119
        }
120
    }
121
    
122
    /**
123
     * Logs with an arbitrary level.
124
     * 
125
     * @param  mixed  $level
126
     * @param  string  $message
127
     * @param  array  $context
128
     * 
129
     * @return bool
130
     */
131
    public function log($level, $message, array $context = [])
132
    {
133
        $message = $this->exchangeProcess($message, $context);
134
135
        $this->handle($level, $message);
136
137
        return true;
138
    }
139
140
    /**
141
     * Replaces any placeholders in the message with variables
142
     * from the context.
143
     * 
144
     * @param  string  $message
145
     * @param  array  $context
146
     * 
147
     * @return mixed
148
     */
149
    protected function exchangeProcess($message, array $context = [])
150
    {
151
        if ( ! is_string($message)) {
0 ignored issues
show
introduced by
The condition is_string($message) is always true.
Loading history...
152
            return $message;
153
        }
154
        
155
        $replace = [];
156
        
157
        foreach ($context as $key => $value) {
158
            if ($key === 'exception' && $value instanceof Throwable) {
159
                $value = $value->getMessage() . ' ' . $this->cleanFileNames($value->getFile()) . ':' . $value->getLine();
160
                // Todo - sanitize input before writing to file?
161
                $replace["{{$key}}"] = $value;
162
            } elseif (null === $value || is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
163
                $replace["{{$key}}"] = $value;
164
            } elseif ($value instanceof DateTime) {
165
                $replace["{{$key}}"] = $value->format(DateTime::RFC3339);
166
            } elseif (is_object($value)) {
167
                $replace["{{$key}}"] = '[object '.get_class($value).']';
168
            } else {
169
                $replace["{{$key}}"] = '['.gettype($value).']';
170
            }
171
        }
172
        
173
        // Add special placeholders
174
        $replace['{postVars}'] = '$_POST: '.print_r($_POST, true);
0 ignored issues
show
Bug introduced by
Are you sure print_r($_POST, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

174
        $replace['{postVars}'] = '$_POST: './** @scrutinizer ignore-type */ print_r($_POST, true);
Loading history...
175
        $replace['{getVars}']  = '$_GET: '.print_r($_GET, true);
0 ignored issues
show
Bug introduced by
Are you sure print_r($_GET, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

175
        $replace['{getVars}']  = '$_GET: './** @scrutinizer ignore-type */ print_r($_GET, true);
Loading history...
176
        $replace['{env}']      = '['.$this->getLogEnvironment().']';
177
        
178
        return strtr($message, $replace);
179
    }
180
    /**
181
     * Cleans the paths of filenames by replacing APPPATH, SYSPATH
182
     * with the actual var. i.e.
183
     * 
184
     * /var/www/site/app/Http/Controllers/Home.php
185
     * 
186
     * becomes:
187
     * 
188
     * APPPATH/Http/Controllers/Home.php
189
     * 
190
     * @param  string  $file
191
     * 
192
     * @return string
193
     */
194
    protected function cleanFileNames(string $file)
195
    {
196
        $file = str_replace(APP_PATH, 'APPPATH/', $file);
197
        $file = str_replace(SYS_PATH, 'SYSPATH/', $file);
198
        
199
        return $file;
200
    }
201
202
    /**
203
     * Handles logging the message.
204
     * 
205
     * @param  string  $level
206
     * @param  string  $message
207
     * 
208
     * @return bool
209
     */
210
    public function handle($level, $message)
211
    {        
212
        $path = $this->logFilePath.'lenevor-'.date('Y-m-d').'.'.$this->logFileExtension;
213
214
        if ( ! is_file($path)) {
215
            $newFile = true;
0 ignored issues
show
Unused Code introduced by
The assignment to $newFile is dead and can be removed.
Loading history...
216
217
            if ($this->logFileExtension === 'php') {
218
                $this->logMessage .= "<?php // Log file was generated ?>\n\n";
219
            }
220
        }
221
222
        $this->open($path);
223
224
        $this->logMessage($level, $message);
225
        
226
        flock($this->logHandler, LOCK_EX);
0 ignored issues
show
Bug introduced by
It seems like $this->logHandler can also be of type false; however, parameter $stream of flock() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
        flock(/** @scrutinizer ignore-type */ $this->logHandler, LOCK_EX);
Loading history...
227
        
228
        for ($written = 0, $length = strlen($this->logMessage); $written < $length; $written += $result) {
229
            if (($result = fwrite($this->logHandler, substr($this->logMessage, $written))) === false) {
0 ignored issues
show
Bug introduced by
It seems like $this->logHandler can also be of type false; however, parameter $stream of fwrite() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

229
            if (($result = fwrite(/** @scrutinizer ignore-type */ $this->logHandler, substr($this->logMessage, $written))) === false) {
Loading history...
230
                // if we get this far, we'll never see this during travis-ci
231
                // @codeCoverageIgnoreStart
232
                break;
233
                // @codeCoverageIgnoreEnd
234
            }
235
        }
236
        
237
        flock($this->logHandler, LOCK_UN);
238
        
239
        if (isset($newfile) && $newfile === true) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newfile does not exist. Did you maybe mean $newFile?
Loading history...
240
            chmod($path, $this->logFilePermissions);
241
        }
242
        
243
        return is_int($result);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $result does not seem to be defined for all execution paths leading up to this point.
Loading history...
244
    }
245
246
    /**
247
     * Opens the current file.
248
     * 
249
     * @param  string  $path
250
     * 
251
     * @return bool
252
     */
253
    private function open($path)
254
    {
255
        if (false === $this->logHandler = is_resource($path) ? $path : @fopen($path, 'ab')) {
0 ignored issues
show
Documentation Bug introduced by
It seems like is_resource($path) ? $path : @fopen($path, 'ab') of type false or resource is incompatible with the declared type string of property $logHandler.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
introduced by
The condition is_resource($path) is always false.
Loading history...
256
            throw new LogException(sprintf('Unable to open "%s".', $path));
257
        }
258
259
        return true;
260
    }
261
262
    /**
263
     * Write message of log file.
264
     * 
265
     * @param  mixed  $level
266
     * @param  string  $messsage
267
     * 
268
     * @return $this
269
     */
270
    private function logMessage($level, $message)
271
    {
272
        $level = $this->getLogEnvironment().'.'.strtolower($level);
273
274
        $message = ucfirst($message);
275
276
        $this->logMessage .= "[{$this->getTimestamp()}] [{$level}] {$message}\n";
277
278
        return $this;
279
    }
280
281
    /**
282
     * Gets the correctly formatted Date/Time for the log entry.
283
     * 
284
     * PHP DateTime is dump, and you have to resort to trickery to get microseconds
285
     * to work correctly, so here it is.
286
     * 
287
     * @return string
288
     */
289
    private function getTimestamp()
290
    {
291
        $logDateFormat = $this->app['config']['logger.dateFormat'] ?? $this->logDateFormat;
292
        $originalTime  = microtime(true);
293
        $micro         = sprintf("%06d", ($originalTime - floor($originalTime)) * 1000000);
0 ignored issues
show
Bug introduced by
It seems like $originalTime can also be of type string; however, parameter $num of floor() does only seem to accept double|integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

293
        $micro         = sprintf("%06d", ($originalTime - floor(/** @scrutinizer ignore-type */ $originalTime)) * 1000000);
Loading history...
294
        $date          = new Chronos(date('Y-m-d H:i:s.'.$micro, $originalTime));
0 ignored issues
show
Bug introduced by
$originalTime of type double|string is incompatible with the type integer|null expected by parameter $timestamp of date(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

294
        $date          = new Chronos(date('Y-m-d H:i:s.'.$micro, /** @scrutinizer ignore-type */ $originalTime));
Loading history...
295
        
296
        return $date->format($logDateFormat);
297
    }   
298
}