Passed
Push — master ( 5fbab0...5feb54 )
by Paul
08:33
created

Console   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 302
Duplicated Lines 0 %

Test Coverage

Coverage 45.31%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 44
eloc 114
c 3
b 0
f 0
dl 0
loc 302
ccs 58
cts 128
cp 0.4531
rs 8.8798

22 Methods

Rating   Name   Duplication   Size   Complexity  
A normalizeValue() 0 8 3
A __toString() 0 3 1
A __call() 0 8 2
A clear() 0 4 1
A __construct() 0 4 1
A get() 0 4 1
A getRaw() 0 3 1
A canLogEntry() 0 7 3
A setLogFile() 0 21 5
A getLevel() 0 3 1
A size() 0 5 2
A log() 0 15 3
A humanLevel() 0 4 1
A getLevels() 0 4 1
A normalizeThrowableMessage() 0 6 2
A reset() 0 9 2
A interpolate() 0 11 4
A humanSize() 0 3 1
A once() 0 15 3
A logOnce() 0 14 3
A getMessageFromData() 0 5 2
A buildLogEntry() 0 7 1

How to fix   Complexity   

Complex Class

Complex classes like Console often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Console, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace GeminiLabs\SiteReviews\Modules;
4
5
use BadMethodCallException;
6
use DateTime;
7
use GeminiLabs\SiteReviews\Helper;
8
use GeminiLabs\SiteReviews\Helpers\Arr;
9
use GeminiLabs\SiteReviews\Helpers\Cast;
10
use GeminiLabs\SiteReviews\Helpers\Str;
11
use GeminiLabs\SiteReviews\Modules\Backtrace;
12
use ReflectionClass;
13
use Throwable;
14
15
/**
16
 * @method static debug($message, $context = [])
17
 * @method static info($message, $context = [])
18
 * @method static notice($message, $context = [])
19
 * @method static warning($message, $context = [])
20
 * @method static error($message, $context = [])
21
 * @method static critical($message, $context = [])
22
 * @method static alert($message, $context = [])
23
 * @method static emergency($message, $context = [])
24
 */
25
class Console
26
{
27
    const DEBUG = 0;      // Detailed debug information
28
    const INFO = 1;       // Interesting events
29
    const NOTICE = 2;     // Normal but significant events
30
    const WARNING = 4;    // Exceptional occurrences that are not errors
31
    const ERROR = 8;      // Runtime errors that do not require immediate action
32
    const CRITICAL = 16;  // Critical conditions
33
    const ALERT = 32;     // Action must be taken immediately
34
    const EMERGENCY = 64; // System is unusable
35
36
    protected $file;
37
    protected $log;
38
    protected $logOnceKey = 'glsr_log_once';
39
40 14
    public function __construct()
41
    {
42 14
        $this->setLogFile();
43 14
        $this->reset();
44 14
    }
45
46 14
    public function __call($method, $args)
47
    {
48 14
        $constant = 'static::'.strtoupper($method);
49 14
        if (defined($constant)) {
50 14
            $args = Arr::prepend($args, constant($constant));
51 14
            return call_user_func_array([$this, 'log'], array_slice($args, 0, 3));
52
        }
53
        throw new BadMethodCallException("Method [$method] does not exist.");
54
    }
55
56
    /**
57
     * @return string
58
     */
59
    public function __toString()
60
    {
61
        return $this->get();
62
    }
63
64
    /**
65
     * @return void
66
     */
67
    public function clear()
68
    {
69
        $this->log = '';
70
        file_put_contents($this->file, $this->log);
71
    }
72
73
    /**
74
     * @return string
75
     */
76
    public function get()
77
    {
78
        return esc_html(
79
            Helper::ifEmpty($this->log, _x('Console is empty', 'admin-text', 'site-reviews'))
80
        );
81
    }
82
83
    /**
84
     * @return string
85
     */
86
    public function getRaw()
87
    {
88
        return htmlspecialchars_decode($this->get(), ENT_QUOTES);
89
    }
90
91
    /**
92
     * @return int
93
     */
94
    public function getLevel()
95
    {
96
        return glsr()->filterInt('console/level', static::INFO);
97
    }
98
99
    /**
100
     * @return array
101
     */
102 14
    public function getLevels()
103
    {
104 14
        $constants = (new ReflectionClass(__CLASS__))->getConstants();
105 14
        return array_map('strtolower', array_flip($constants));
106
    }
107
108
    /**
109
     * @return string
110
     */
111
    public function humanLevel()
112
    {
113
        $level = $this->getLevel();
114
        return sprintf('%s (%d)', strtoupper(Arr::get($this->getLevels(), $level, 'unknown')), $level);
115
    }
116
117
    /**
118
     * @return string
119
     */
120
    public function humanSize()
121
    {
122
        return Str::replaceLast(' B', ' bytes', Cast::toString(size_format($this->size())));
123
    }
124
125
    /**
126
     * @param int $level
127
     * @param mixed $message
128
     * @param array $context
129
     * @param string $backtraceLine
130
     * @return static
131
     */
132 14
    public function log($level, $message, $context = [], $backtraceLine = '')
133
    {
134 14
        if (empty($backtraceLine)) {
135 14
            $backtraceLine = glsr(Backtrace::class)->line();
136
        }
137 14
        if ($this->canLogEntry($level, $backtraceLine)) {
138 14
            $levelName = Arr::get($this->getLevels(), $level);
139 14
            $backtraceLine = glsr(Backtrace::class)->normalizeLine($backtraceLine);
140 14
            $message = $this->interpolate($message, $context);
141 14
            $entry = $this->buildLogEntry($levelName, $message, $backtraceLine);
142 14
            file_put_contents($this->file, $entry.PHP_EOL, FILE_APPEND | LOCK_EX);
143 14
            apply_filters('console', $message, $levelName, $backtraceLine); // Show in Blackbar plugin if installed
144 14
            $this->reset();
145
        }
146 14
        return $this;
147
    }
148
149
    /**
150
     * @return void
151
     */
152
    public function logOnce()
153
    {
154
        $once = Arr::consolidate(glsr()->{$this->logOnceKey});
155
        $levels = $this->getLevels();
156
        foreach ($once as $entry) {
157
            $levelName = Arr::get($entry, 'level');
158
            if (in_array($levelName, $levels)) {
159
                $level = Arr::get(array_flip($levels), $levelName);
160
                $message = Arr::get($entry, 'message');
161
                $backtraceLine = Arr::get($entry, 'backtrace');
162
                $this->log($level, $message, [], $backtraceLine);
163
            }
164
        }
165
        glsr()->{$this->logOnceKey} = [];
166
    }
167
168
    /**
169
     * @param string $levelName
170
     * @param string $handle
171
     * @param mixed $data
172
     * @return void
173
     */
174
    public function once($levelName, $handle, $data)
175
    {
176
        $once = Arr::consolidate(glsr()->{$this->logOnceKey});
177
        $filtered = array_filter($once, function ($entry) use ($levelName, $handle) {
178
            return Arr::get($entry, 'level') == $levelName
179
                && Arr::get($entry, 'handle') == $handle;
180
        });
181
        if (empty($filtered)) {
182
            $once[] = [
183
                'backtrace' => glsr(Backtrace::class)->lineFromData($data),
184
                'handle' => $handle,
185
                'level' => $levelName,
186
                'message' => '[RECURRING] '.$this->getMessageFromData($data),
187
            ];
188
            glsr()->{$this->logOnceKey} = $once;
189
        }
190
    }
191
192
    /**
193
     * @return int
194
     */
195 14
    public function size()
196
    {
197 14
        return file_exists($this->file)
198 14
            ? filesize($this->file)
199 14
            : 0;
200
    }
201
202
    /**
203
     * @param string $levelName
204
     * @param mixed $message
205
     * @param string $backtraceLine
206
     * @return string
207
     */
208 14
    protected function buildLogEntry($levelName, $message, $backtraceLine = '')
209
    {
210 14
        return sprintf('[%s] %s [%s] %s',
211 14
            current_time('mysql'),
212 14
            strtoupper($levelName),
213
            $backtraceLine,
214 14
            esc_html($message)
215
        );
216
    }
217
218
    /**
219
     * @param int $level
220
     * @return bool
221
     */
222 14
    protected function canLogEntry($level, $backtraceLine)
223
    {
224 14
        $levelExists = array_key_exists($level, $this->getLevels());
225 14
        if (!Str::contains(glsr()->path(), $backtraceLine)) {
226 14
            return $levelExists; // ignore level restriction if triggered outside of the plugin
227
        }
228
        return $levelExists && $level >= $this->getLevel();
229
    }
230
231
    /**
232
     * @param mixed $data
233
     * @return string
234
     */
235
    protected function getMessageFromData($data)
236
    {
237
        return $data instanceof Throwable
238
            ? $this->normalizeThrowableMessage($data->getMessage())
239
            : print_r($data, true);
240
    }
241
242
    /**
243
     * Interpolates context values into the message placeholders.
244
     * @param mixed $message
245
     * @param array $context
246
     * @return string
247
     */
248 14
    protected function interpolate($message, $context = [])
249
    {
250 14
        $context = Arr::consolidate($context);
251 14
        if (!is_scalar($message) || empty($context)) {
252 14
            return print_r($message, true);
253
        }
254
        $replace = [];
255
        foreach ($context as $key => $value) {
256
            $replace['{'.$key.'}'] = $this->normalizeValue($value);
257
        }
258
        return strtr($message, $replace);
259
    }
260
261
    /**
262
     * @param string $message
263
     * @return string
264
     */
265
    protected function normalizeThrowableMessage($message)
266
    {
267
        $calledIn = strpos($message, ', called in');
268
        return false !== $calledIn
269
            ? substr($message, 0, $calledIn)
270
            : $message;
271
    }
272
273
    /**
274
     * @param mixed $value
275
     * @return string
276
     */
277
    protected function normalizeValue($value)
278
    {
279
        if ($value instanceof DateTime) {
280
            $value = $value->format('Y-m-d H:i:s');
281
        } elseif (!is_scalar($value)) {
282
            $value = json_encode($value);
283
        }
284
        return (string) $value;
285
    }
286
287
    /**
288
     * @return void
289
     */
290 14
    protected function reset()
291
    {
292 14
        if ($this->size() <= wp_convert_hr_to_bytes('512kb')) {
293 14
            return;
294
        }
295
        $this->clear();
296
        file_put_contents($this->file,
297
            $this->buildLogEntry(static::NOTICE,
298
                _x('Console was automatically cleared (512KB maximum size)', 'admin-text', 'site-reviews')
299
            )
300
        );
301
    }
302
303
    /**
304
     * @return void
305
     */
306 14
    protected function setLogFile()
307
    {
308 14
        require_once ABSPATH.WPINC.'/pluggable.php';
309 14
        $uploads = wp_upload_dir();
310 14
        if (!file_exists($uploads['basedir'])) {
311
            $uploads = wp_upload_dir(null, true, true); // maybe the site has been moved, so refresh the cached uploads path
312
        }
313 14
        $base = trailingslashit($uploads['basedir'].'/'.glsr()->id);
314 14
        $this->file = $base.'logs/'.sanitize_file_name('console-'.wp_hash(glsr()->id).'.log');
315
        $files = [
316 14
            $base.'index.php' => '<?php',
317 14
            $base.'logs/.htaccess' => 'deny from all',
318 14
            $base.'logs/index.php' => '<?php',
319 14
            $this->file => '',
320
        ];
321 14
        foreach ($files as $file => $contents) {
322 14
            if (wp_mkdir_p(dirname($file)) && !file_exists($file)) {
323 1
                file_put_contents($file, $contents);
324
            }
325
        }
326 14
        $this->log = file_get_contents($this->file);
327 14
    }
328
}
329