Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
10 / 10
CRAP
100.00% covered (success)
100.00%
1 / 1
MydbEnvironment
100.00% covered (success)
100.00%
32 / 32
100.00% covered (success)
100.00%
10 / 10
18
100.00% covered (success)
100.00%
1 / 1
 gc_collect_cycles
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 restore_error_handler
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_error_handler
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setMysqlndNetReadTimeout
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 error_reporting
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ignore_user_abort
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ini_set
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 endSignalsTrap
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 startSignalsTrap
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getNullErrorHandler
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
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 sql\MydbException\EnvironmentException;
19use function count;
20use function error_reporting;
21use function gc_collect_cycles;
22use function gc_enabled;
23use function ignore_user_abort;
24use function ini_set;
25use function pcntl_signal;
26use function pcntl_signal_dispatch;
27use function pcntl_signal_get_handler;
28use function restore_error_handler;
29use function set_error_handler;
30use const E_ALL;
31use const E_STRICT;
32use const SIG_DFL;
33use const SIGHUP;
34use const SIGINT;
35use const SIGTERM;
36
37/**
38 * @author Sergei Shilko <contact@sshilko.com>
39 * @license https://opensource.org/licenses/mit-license.php MIT
40 * @see https://github.com/sshilko/php-sql-mydb
41 */
42class MydbEnvironment implements MydbEnvironmentInterface
43{
44
45    /**
46     * @var array<int>
47     */
48    protected array $knownSignals = [SIGTERM, SIGINT, SIGHUP];
49
50    /**
51     * Any signals that were trapped during custom signal handler
52     *
53     * @var array<int>
54     */
55    protected array $trappedSignals = [];
56
57    /**
58     * Backup of signal handlers
59     * Original signal handler, which replaced by custon trap
60     * @psalm-var array<int, int|resource|string>
61     */
62    protected array $trappedHandlers = [];
63
64    /**
65     * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
66     * @SuppressWarnings("camelCase")
67     * @see https://www.php.net/manual/en/function.gc-collect-cycles
68     */
69    public function gc_collect_cycles(): void
70    {
71        if (!gc_enabled()) {
72            // @codeCoverageIgnoreStart
73            return;
74            // @codeCoverageIgnoreEnd
75        }
76
77        gc_collect_cycles();
78    }
79
80    /**
81     * Restore previous PHP error handler
82     *
83     * @see https://www.php.net/manual/en/function.restore-error-handler.php
84     * @SuppressWarnings("camelCase")
85     * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
86     */
87    public function restore_error_handler(): void
88    {
89        restore_error_handler();
90    }
91
92    /**
93     * Set custom PHP error handler
94     *
95     * @param callable|null $callback
96     * @see https://www.php.net/manual/en/function.set-error-handler
97     * @SuppressWarnings("camelCase")
98     * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
99     */
100    public function set_error_handler(?callable $callback = null, int $error_levels = E_ALL|E_STRICT): void
101    {
102        /** @var callable(int, string, string=, int=, array<array-key, mixed>=):bool|null $newHandler */
103        $newHandler = $callback ?? $this->getNullErrorHandler();
104
105        set_error_handler($newHandler, $error_levels);
106    }
107
108    /**
109     * Set mysqlnd.net_read_timeout php ini value
110     *
111     * mysqlnd and the MySQL Client Library, libmysqlclient use different networking APIs.
112     * mysqlnd uses PHP streams, whereas libmysqlclient uses its own wrapper around the operating level network calls.
113     * PHP, by default, sets a read timeout of 60s for streams.
114     * This is set via php.ini, default_socket_timeout.
115     * This default applies to all streams that set no other timeout value.
116     * mysqlnd does not set any other value and therefore connections of long running queries can be disconnected
117     * after default_socket_timeout seconds resulting in an error message 2006 - MySQL Server has gone away.
118     * The MySQL Client Library sets a default timeout of 24 * 3600 seconds (1 day)
119     * and waits for other timeouts to occur, such as TCP/IP timeouts. mysqlnd now uses the same very long timeout.
120     * The value is configurable through a new php.ini setting: mysqlnd.net_read_timeout.
121     *
122     * mysqlnd.net_read_timeout gets used by any extension (ext/mysql, ext/mysqli, PDO_MySQL) that uses mysqlnd.
123     * mysqlnd tells PHP Streams to use mysqlnd.net_read_timeout.
124     * Please note that there may be subtle differences between MYSQL_OPT_READ_TIMEOUT from the MySQL Client Library
125     * and PHP Streams, for example MYSQL_OPT_READ_TIMEOUT is documented to work only for TCP/IP connections and,
126     * prior to MySQL 5.1.2, only for Windows. PHP streams may not have this limitation.
127     *
128     * @throws \sql\MydbException\EnvironmentException
129     * @see https://www.php.net/manual/en/mysqlnd.config.php
130     */
131    public function setMysqlndNetReadTimeout(string $timeoutSeconds): bool
132    {
133        return (bool) $this->ini_set('mysqlnd.net_read_timeout', $timeoutSeconds);
134    }
135
136    /**
137     * Sets which PHP errors are reported
138     *
139     * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
140     * @SuppressWarnings("camelCase")
141     * @see https://www.php.net/manual/en/function.error-reporting
142     */
143    public function error_reporting(int $level): int
144    {
145        return error_reporting($level);
146    }
147
148    /**
149     * Set whether a client disconnect should abort script execution (does not affect CLI)
150     *
151     * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
152     * @SuppressWarnings("camelCase")
153     * @see https://www.php.net/manual/en/function.ignore-user-abort
154     */
155    public function ignore_user_abort(): int
156    {
157        return ignore_user_abort();
158    }
159
160    /**
161     * Sets the value of a configuration option
162     *
163     * @phpcs:disable PSR1.Methods.CamelCapsMethodName.NotCamelCaps
164     * @SuppressWarnings("camelCase")
165     * @throws \sql\MydbException\EnvironmentException
166     * @see https://www.php.net/manual/en/function.ini-set
167     */
168    public function ini_set(string $key, string $value): string
169    {
170        $result = ini_set($key, $value);
171        if (false === $result) {
172            // @codeCoverageIgnoreStart
173            throw new EnvironmentException();
174            // @codeCoverageIgnoreEnd
175        }
176
177        return $result;
178    }
179
180    /**
181     * Disable custom signal handler
182     *
183     * @see https://wiki.php.net/rfc/async_signals
184     * @see https://blog.pascal-martin.fr/post/php71-en-other-new-things/
185     * @see https://www.php.net/manual/en/function.pcntl-signal
186     *
187     * @return array<int>|null array of trapped signals
188     * @throws \sql\MydbException\EnvironmentException
189     */
190    public function endSignalsTrap(): ?array
191    {
192        /**
193         * Process signals
194         */
195        if (!pcntl_signal_dispatch()) {
196            // @codeCoverageIgnoreStart
197            throw new EnvironmentException();
198            // @codeCoverageIgnoreEnd
199        }
200
201        $trappedSignals = $this->trappedSignals;
202        foreach ($this->knownSignals as $signalNumber) {
203            /**
204             * Reset signals to previous/default handler
205             */
206            $newHandler = $this->trappedHandlers[$signalNumber] ?? SIG_DFL;
207
208            /**
209             * @psalm-suppress PossiblyInvalidArgument
210             */
211            if (!pcntl_signal($signalNumber, $newHandler)) {
212                // @codeCoverageIgnoreStart
213                throw new EnvironmentException();
214                // @codeCoverageIgnoreEnd
215            }
216            unset($this->trappedHandlers[$signalNumber]);
217        }
218        $this->trappedSignals = [];
219
220        return count($trappedSignals) > 0
221            ? $trappedSignals
222            : null;
223    }
224
225    /**
226     * Enable custom signal handler
227     *
228     * @see https://wiki.php.net/rfc/async_signals
229     * @see https://blog.pascal-martin.fr/post/php71-en-other-new-things/
230     * @see https://www.php.net/manual/en/function.pcntl-signal
231     * @throws \sql\MydbException\EnvironmentException
232     */
233    public function startSignalsTrap(): void
234    {
235        $this->trappedSignals = [];
236
237        $signalHandler = function (int $signalNumber): void {
238            $this->trappedSignals[] = $signalNumber;
239        };
240
241        foreach ($this->knownSignals as $signalNumber) {
242            $originalNandler = pcntl_signal_get_handler($signalNumber);
243            $this->trappedHandlers[$signalNumber] = $originalNandler;
244
245            if (!pcntl_signal($signalNumber, $signalHandler)) {
246                // @codeCoverageIgnoreStart
247                throw new EnvironmentException();
248                // @codeCoverageIgnoreEnd
249            }
250        }
251    }
252
253    /**
254     * Error handler that does nothing and does not chain
255     * @see https://www.php.net/manual/en/function.set-error-handler
256     */
257    protected function getNullErrorHandler(): callable
258    {
259        return static function (): bool {
260            return true;
261        };
262    }
263}