Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
15 / 15
CRAP
100.00% covered (success)
100.00%
1 / 1
MydbLogger
100.00% covered (success)
100.00%
61 / 61
100.00% covered (success)
100.00%
15 / 15
42
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 __destruct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 error
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 log
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 warning
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 emergency
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 alert
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 critical
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 notice
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 info
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 debug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 checkStreamResource
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
7
 writeOutput
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
9
 fwrite
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 formatter
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * This file is part of the sshilko/php-sql-mydb package.
4 *
5 * (c) Sergei Shilko <contact@sshilko.com>
6 *
7 * MIT License
8 *
9 * For the full copyright and license information, please view the LICENSE
10 * file that was distributed with this source code.
11 * @license https://opensource.org/licenses/mit-license.php MIT
12 */
13
14declare(strict_types = 1);
15
16namespace sql;
17
18use Psr\Log\LoggerInterface;
19use Psr\Log\LogLevel;
20use sql\MydbException\LoggerException;
21use function clearstatcache;
22use function count;
23use function fclose;
24use function feof;
25use function fflush;
26use function fwrite;
27use function is_resource;
28use function is_scalar;
29use function restore_error_handler;
30use function set_error_handler;
31use function stream_get_meta_data;
32use function strlen;
33use function strtr;
34use function substr;
35use function var_export;
36use const PHP_EOL;
37use const STDERR;
38use const STDOUT;
39
40/**
41 * Implementation of PSR-3 Logger that will output to STDERR & STDOUT
42 *
43 * @author Sergei Shilko <contact@sshilko.com>
44 * @license https://opensource.org/licenses/mit-license.php MIT
45 * @see https://github.com/sshilko/php-sql-mydb
46 * @see https://www.php-fig.org/psr/psr-3/
47 */
48class MydbLogger implements LoggerInterface
49{
50    protected const IO_WRITE_ATTEMPTS = 3;
51
52    /**
53     * Opened resource, STDOUT
54     * @see https://www.php.net/manual/en/features.commandline.io-streams.php
55     *
56     * @var resource
57     */
58    protected $stdout;
59
60    /**
61     * Opened resource, STDERR
62     * @see https://www.php.net/manual/en/features.commandline.io-streams.php
63     *
64     * @var resource
65     */
66    protected $stderr;
67
68    /**
69     * End of line delimiter
70     */
71    protected string $stdeol = PHP_EOL;
72
73    /**
74     * @param resource $stdout
75     * @param resource $stderr
76     * @psalm-suppress MissingParamType
77     * @throws \sql\MydbException\LoggerException
78     */
79    public function __construct($stdout = STDOUT, $stderr = STDERR, string $stdeol = PHP_EOL)
80    {
81        /**
82         * @psalm-suppress DocblockTypeContradiction
83         */
84        if (!is_resource($stdout) || !is_resource($stderr)) {
85            throw new LoggerException();
86        }
87
88        $this->stdout = $stdout;
89        $this->stderr = $stderr;
90        $this->stdeol = $stdeol;
91    }
92
93    public function __destruct()
94    {
95        /**
96         * @psalm-suppress RedundantConditionGivenDocblockType
97         */
98        if (is_resource($this->stdout)) {
99            fflush($this->stdout);
100            fclose($this->stdout);
101        }
102
103        /**
104         * @psalm-suppress RedundantConditionGivenDocblockType
105         */
106        if (is_resource($this->stderr)) {
107            fflush($this->stderr);
108            fclose($this->stderr);
109        }
110        clearstatcache();
111    }
112
113    /**
114     * @throws \sql\MydbException\LoggerException
115     * @param array|string $message
116     */
117    public function error($message, array $context = []): void
118    {
119        if ([] !== $message && '' !== $message) {
120            $this->writeOutput($this->stderr, static::formatter($message) . $this->stdeol);
121        }
122
123        if (!count($context)) {
124            return;
125        }
126
127        $this->writeOutput($this->stderr, static::formatter($context) . $this->stdeol);
128    }
129
130    /**
131     * @param mixed $level
132     * @param array|string $message
133     * @throws \sql\MydbException\LoggerException
134     * @phpcs:disable SlevomatCodingStandard.Functions.UnusedParameter.UnusedParameter
135     */
136    public function log($level, $message, array $context = []): void
137    {
138        if ([] !== $message && '' !== $message) {
139            $this->writeOutput($this->stdout, static::formatter($message) . $this->stdeol);
140        }
141
142        if (!count($context)) {
143            return;
144        }
145
146        $this->writeOutput($this->stdout, static::formatter($context) . $this->stdeol);
147    }
148
149    /**
150     * @param array|string $message
151     * @throws \sql\MydbException\LoggerException
152     */
153    public function warning($message, array $context = []): void
154    {
155        $this->error($message, $context);
156    }
157
158    /**
159     * @param array|string $message
160     * @throws \sql\MydbException\LoggerException
161     */
162    public function emergency($message, array $context = []): void
163    {
164        $this->error($message, $context);
165    }
166
167    /**
168     * @param array|string $message
169     * @throws \sql\MydbException\LoggerException
170     */
171    public function alert($message, array $context = []): void
172    {
173        $this->error($message, $context);
174    }
175
176    /**
177     * @param array|string $message
178     * @throws \sql\MydbException\LoggerException
179     */
180    public function critical($message, array $context = []): void
181    {
182        $this->error($message, $context);
183    }
184
185    /**
186     * @param array|string $message
187     * @throws \sql\MydbException\LoggerException
188     */
189    public function notice($message, array $context = []): void
190    {
191        $this->log(LogLevel::NOTICE, $message, $context);
192    }
193
194    /**
195     * @param array|string $message
196     * @throws \sql\MydbException\LoggerException
197     */
198    public function info($message, array $context = []): void
199    {
200        $this->log(LogLevel::INFO, $message, $context);
201    }
202
203    /**
204     * @param array|string $message
205     * @throws \sql\MydbException\LoggerException
206     */
207    public function debug($message, array $context = []): void
208    {
209        $this->log(LogLevel::DEBUG, $message, $context);
210    }
211
212    /**
213     * @param resource $stream &fs.file.pointer;
214     * @link https://php.net/manual/en/function.fwrite.php
215     * @throws \sql\MydbException\LoggerException
216     */
217    protected function checkStreamResource($stream): void
218    {
219        /**
220         * is_resource checks whether resource was closed with i.e. fclose()
221         * @psalm-suppress DocblockTypeContradiction
222         * @psalm-suppress RedundantConditionGivenDocblockType
223         */
224        if (false === is_resource($stream)) {
225            throw new LoggerException('Stream resource is not valid or already closed');
226        }
227
228        $info = stream_get_meta_data($stream);
229
230        if ($info['timed_out'] || $info['eof'] || feof($stream)) {
231            throw new LoggerException();
232        }
233
234        if ('' !== $info['mode'] && strtr($info['mode'], 'waxc+', '.....') === $info['mode']) {
235            throw new LoggerException('Stream resource is not opened in write mode');
236        }
237    }
238
239    /**
240     * @param resource $stream &fs.file.pointer;
241     * @link https://php.net/manual/en/function.fwrite.php
242     * @throws \sql\MydbException\LoggerException
243     * @psalm-suppress MissingParamType
244     * @phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
245     */
246    protected function writeOutput($stream, string $data = ''): void
247    {
248        $this->checkStreamResource($stream);
249
250        $tries = self::IO_WRITE_ATTEMPTS;
251        $len = strlen($data);
252
253        /** @phan-suppress-next-line PhanNoopConstant */
254        for ($written = 0; $written < $len; true) {
255            $chunk = substr($data, $written);
256            if ('' === $chunk) {
257                // @codeCoverageIgnoreStart
258                throw new LoggerException();
259                // @codeCoverageIgnoreEnd
260            }
261
262            $writeResult = $this->fwrite($stream, $chunk);
263
264            if (null === $writeResult || feof($stream)) {
265                // @codeCoverageIgnoreStart
266                throw new LoggerException();
267                // @codeCoverageIgnoreEnd
268            }
269
270            if (false === fflush($stream)) {
271                // @codeCoverageIgnoreStart
272                throw new LoggerException();
273                // @codeCoverageIgnoreEnd
274            }
275
276            $written += $writeResult;
277
278            if ($written < $len) {
279                // @codeCoverageIgnoreStart
280                throw new LoggerException();
281                // @codeCoverageIgnoreEnd
282            }
283
284            if (0 === $writeResult) {
285                // @codeCoverageIgnoreStart
286                --$tries;
287                // @codeCoverageIgnoreEnd
288            }
289
290            if ($tries <= 0) {
291                // @codeCoverageIgnoreStart
292                throw new LoggerException();
293                // @codeCoverageIgnoreEnd
294            }
295        }
296    }
297
298    /**
299     * @param resource $stream
300     */
301    protected function fwrite($stream, string $data): ?int
302    {
303        $error = null;
304
305        /**
306         * @psalm-suppress InvalidArgument
307         */
308        set_error_handler(
309            static function ($_, string $errstr) use (&$error): bool {
310                // @codeCoverageIgnoreStart
311                $error = $errstr;
312
313                return true;
314                // @codeCoverageIgnoreEnd
315            }
316        );
317
318        $sent = fwrite($stream, $data);
319
320        restore_error_handler();
321
322        if (null !== $error || false === $sent) {
323            // @codeCoverageIgnoreStart
324            return null;
325            // @codeCoverageIgnoreEnd
326        }
327
328        return $sent;
329    }
330
331    /**
332     * @param string|array $var
333     */
334    protected static function formatter($var): string
335    {
336        if (is_scalar($var)) {
337            return $var;
338        }
339
340        return var_export($var, true);
341    }
342}