Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.43% covered (success)
96.43%
54 / 56
83.33% covered (success)
83.33%
10 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
DeveloperToolbar
96.43% covered (success)
96.43%
54 / 56
83.33% covered (success)
83.33%
10 / 12
24
0.00% covered (danger)
0.00%
0 / 1
 __construct
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 getInstance
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRequestHash
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDataByHash
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 generateRequestHash
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 addData
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setDisabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDisabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dump
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getDumps
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __destruct
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
5.01
1<?php
2
3namespace Miniframe\Toolbar\Service;
4
5use Miniframe\Core\Config;
6use Miniframe\Core\Request;
7use RuntimeException;
8
9class DeveloperToolbar
10{
11    /**
12     * Path for the log files.
13     *
14     * @var string
15     */
16    private $logPath;
17    /**
18     * Path to the log file for the current request.
19     *
20     * @var string
21     */
22    private $logFile;
23    /**
24     * Hash for the current request.
25     *
26     * @var string
27     */
28    private $requestHash;
29    /**
30     * Data that is logged for the current request.
31     *
32     * @var array
33     */
34    private $logData;
35    /**
36     * Set to true to disable logging for this request
37     *
38     * @var bool
39     */
40    private $disabled = false;
41
42    /**
43     * Log retention in seconds (14400s = 4h)
44     */
45    private const LOG_RETENTION = 14400;
46
47    /**
48     * Reference to a DeveloperToolbar instance
49     *
50     * @var DeveloperToolbar|null
51     */
52    private static $developerToolbar;
53
54    /**
55     * Initiates the Developer Toolbar service
56     *
57     * @param Request $request Reference to the Request object.
58     * @param string  $logPath Path in which log files are stored.
59     */
60    public function __construct(Request $request, string $logPath)
61    {
62        // Store basic data
63        $this->logData['rusage']['start'] = getrusage();
64        $this->logData['request'] = $request;
65        $this->logData['requestHeaders'] = function_exists('getallheaders') ? getallheaders() : array();
66        $this->logData['dumps'] = array();
67
68        // Defines the log file
69        $this->requestHash = $this->generateRequestHash();
70        $this->logPath = $logPath;
71        if (!is_dir($this->logPath)) {
72            mkdir($this->logPath, 0777, true);
73        }
74        $this->logFile = rtrim($logPath, '/') . '/' . $this->requestHash . '.debug';
75
76        // Catches all kind of errors
77        $this->logData['errors'] = array();
78        set_error_handler(function (int $errno, string $message, string $file, int $line): bool {
79            $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
80            array_shift($backtrace); // Remove the error handler itself
81            $table = [
82                E_ERROR => 'E_ERROR',
83                E_WARNING => 'E_WARNING',
84                E_PARSE => 'E_PARSE',
85                E_NOTICE => 'E_NOTICE',
86                E_CORE_ERROR => 'E_CORE_ERROR',
87                E_CORE_WARNING => 'E_CORE_WARNING',
88                E_COMPILE_ERROR => 'E_COMPILE_ERROR',
89                E_COMPILE_WARNING => 'E_COMPILE_WARNING',
90                E_USER_ERROR => 'E_USER_ERROR',
91                E_USER_WARNING => 'E_USER_WARNING',
92                E_USER_NOTICE => 'E_USER_NOTICE',
93                E_STRICT => 'E_STRICT',
94                E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
95                E_DEPRECATED => 'E_DEPRECATED',
96                E_USER_DEPRECATED => 'E_USER_DEPRECATED',
97            ];
98            $this->logData['errors'][] = array(
99                'errno'   => $errno,
100                'errtype' => $table[$errno] ?? 'Unknown',
101                'message' => $message,
102                'file'    => $file,
103                'line'    => $line,
104                'backtrace' => $backtrace,
105            );
106            return true; // Error catched/logged, don't show it in the page itself.
107        }, E_ALL);
108
109        // Register for static calls (mainly dump())
110        self::$developerToolbar = $this;
111    }
112
113    /**
114     * Returns a reference to a DeveloperToolbar instance
115     *
116     * @return DeveloperToolbar|null
117     */
118    public static function getInstance(): ?DeveloperToolbar
119    {
120        return self::$developerToolbar;
121    }
122
123    /**
124     * Returns the request hash
125     *
126     * @return string
127     */
128    public function getRequestHash(): string
129    {
130        return $this->requestHash;
131    }
132
133    /**
134     * Fetched logged data by hash.
135     *
136     * @param string $hash The hash.
137     *
138     * @return array|null
139     */
140    public function getDataByHash(string $hash): ?array
141    {
142        if (!preg_match('/^[a-zA-Z0-9_\-.]+$/', $hash)) {
143            return null;
144        }
145        $logFile = $this->logPath . '/' . $hash . '.debug';
146        if (!file_exists($logFile)) {
147            return null;
148        }
149        return unserialize(file_get_contents($logFile));
150    }
151
152    /**
153     * Generates a somewhat random/unique string that matches /^[a-zA-Z0-9_\-.]+$/
154     *
155     * @return string
156     */
157    private function generateRequestHash(): string
158    {
159        $binary = '';
160        foreach (str_split(date('YmdHis') . substr(explode(' ', microtime())[0], 2), 2) as $char) {
161            $binary .= chr($char);
162        }
163        for ($iterator = 0; $iterator < 4; ++$iterator) {
164            $binary .= chr(rand(0x00, 0xff));
165        }
166        return str_replace(['+', '/', '='], ['_', '-', '.'], base64_encode($binary));
167    }
168
169    /**
170     * Adds data to the Developer Toolbar log
171     *
172     * @param string $key  Key of the data.
173     * @param mixed  $data Serializable log data.
174     *
175     * @return void
176     */
177    public function addData(string $key, $data): void
178    {
179        $this->logData[$key] = $data;
180    }
181
182    /**
183     * Disables or enables logging.
184     *
185     * @param boolean $isDisabled The disabled state.
186     *
187     * @return void
188     */
189    public function setDisabled(bool $isDisabled): void
190    {
191        $this->disabled = $isDisabled;
192    }
193
194    /**
195     * Return the disabled state.
196     *
197     * @return boolean
198     */
199    public function isDisabled(): bool
200    {
201        return $this->disabled;
202    }
203
204    /**
205     * Dumps a variable
206     *
207     * @param mixed $variable The variable to dump.
208     *
209     * @return void
210     */
211    public function dump($variable): void
212    {
213        // Where is the dump() called?
214        $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
215        $backtraceIndex = 0;
216        if (
217            isset($backtrace[$backtraceIndex]['file'])
218            && realpath($backtrace[$backtraceIndex]['file']) == realpath(__DIR__ . '/../functions.php')
219        ) {
220            $backtraceIndex = 1;
221        }
222        $data = [
223            'file' => $backtrace[$backtraceIndex]['file'] ?? null,
224            'line' => $backtrace[$backtraceIndex]['line'] ?? null,
225            'data' => $variable,
226        ];
227
228        $this->logData['dumps'][] = $data;
229    }
230
231    /**
232     * Returns all dumps
233     *
234     * @return array
235     */
236    public function getDumps(): array
237    {
238        return $this->logData['dumps'] ?? [];
239    }
240
241    /**
242     * Returns all errors
243     *
244     * @return array
245     */
246    public function getErrors(): array
247    {
248        return $this->logData['errors'] ?? [];
249    }
250
251    /**
252     * Store the log data and cleans up old logs
253     */
254    public function __destruct()
255    {
256        if ($this->disabled) {
257            return;
258        }
259
260        $this->logData['rusage']['end'] = getrusage();
261        $this->logData['memoryPeak']['real'] = memory_get_peak_usage(true);
262        $this->logData['memoryPeak']['emalloc'] = memory_get_peak_usage();
263        file_put_contents($this->logFile, serialize($this->logData));
264        if (!file_exists($this->logFile)) {
265            throw new \RuntimeException('Can\'t write to ' . $this->logFile);
266        }
267
268        // Clean up old files
269        $files = glob($this->logPath . '/*.debug');
270        foreach ($files as $file) {
271            if (filemtime($file) < time() - self::LOG_RETENTION) {
272                unlink($file);
273            }
274        }
275    }
276}