Passed
Push — master ( 6fb4a1...59560a )
by Ondřej
01:50
created

ConnConfig.php$0 ➔ getServerMajorVersionNumber()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 3
c 0
b 0
f 0
cc 1
rs 10
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Connection\Config;
4
5
use Ivory\Connection\ConnectionControl;
6
use Ivory\Connection\IObservableTransactionControl;
7
use Ivory\Connection\IStatementExecution;
8
use Ivory\Exception\UnsupportedException;
9
use Ivory\Value\Quantity;
10
11
/**
12
 * The standard implementation of runtime database configuration manager.
13
 *
14
 * {@inheritdoc}
15
 *
16
 * Besides the interface-specified methods, this implementation exposes the database configuration parameters as dynamic
17
 * ("overloaded") properties. The value currently in effect is always returned. For accessing properties with non-word
18
 * characters in their names (e.g., when using
19
 * {@link http://www.postgresql.org/docs/9.4/static/runtime-config-custom.html customized options}), the standard PHP
20
 * syntax `$config->{'some.prop'}` may be employed, or the {@link get()} method may simply be called.
21
 *
22
 * The implementation of read operations is lazy - no database query is made until actually needed. Some values are
23
 * gathered using the {@link pg_parameter_status()} function (these get cached internally by the PHP driver). Others are
24
 * directly queried and are not cached. For caching these, see the {@link CachingConnConfig} implementation.
25
 *
26
 * Data types of non-standard settings are fetched once and cached for the whole {@link ConnConfig} object lifetime
27
 * ({@link ConnConfig::flushCache()} may, of course, be used for flushing it in any case). Note that
28
 * {@link http://www.postgresql.org/docs/9.4/static/runtime-config-custom.html customized options} are always of type
29
 * `text`.
30
 *
31
 * As for the {@link IObservableConnConfig} implementation, any changes of configuration parameters must be done through
32
 * some of the methods offered by this class. Bypassing them (i.e., setting a parameter value in a raw query, or within
33
 * a database function) prevents the observers from being notified about the parameter value changes. In such cases, the
34
 * {@link ConnConfig::notifyPropertyChange()} or {@link ConnConfig::notifyPropertiesReset()} has to be called manually
35
 * for the new parameter value to be retrieved again.
36
 */
37
class ConnConfig implements IObservableConnConfig
38
{
39
    private $connCtl;
40
    private $stmtExec;
41
    private $txCtl;
42
    private $watcher;
43
44
    private $typeCache = null;
45
    /** @var string[]|null */
46
    private $effectiveSearchPathCache = null;
47
    /** @var IConfigObserver[][] map: parameter name or <tt>''</tt> => list of observers registered for changes of the
48
     *                             parameter (or any parameter for <tt>''</tt> (empty string) entry) */
49
    private $observers = [];
50
51
52
    public function __construct(
53
        ConnectionControl $connCtl,
54
        IStatementExecution $stmtExec,
55
        IObservableTransactionControl $txCtl
56
    ) {
57
        $this->connCtl = $connCtl;
58
        $this->stmtExec = $stmtExec;
59
        $this->txCtl = $txCtl;
60
61
        $this->watcher = new ConnConfigTransactionWatcher($this);
62
        $txCtl->addObserver($this->watcher);
63
    }
64
65
66
    /**
67
     * Alias for {@link ConnConfig::get().
68
     *
69
     * @param string $propertyName name of a configuration option
70
     * @return bool|float|int|Quantity|string|null current value of the requested option, or <tt>null</tt> if no such
71
     *                                               option has been defined yet
72
     */
73
    public function __get($propertyName)
74
    {
75
        return $this->get($propertyName);
76
    }
77
78
    /**
79
     * Alias for {@link ConnConfig::defined()}.
80
     *
81
     * @param string $propertyName name of a configuration option
82
     * @return bool whether the option is defined
83
     */
84
    public function __isset($propertyName)
85
    {
86
        return $this->defined($propertyName);
87
    }
88
89
    /**
90
     * Alias for {@link ConnConfig::setForSession()}.
91
     *
92
     * @param string $propertyName name of a configuration option
93
     * @param bool|string|int|float|Quantity $value the new value, or {@link ConnConfig::DEFAULT_VALUE} to use the
94
     *                                                option's default
95
     */
96
    public function __set($propertyName, $value)
97
    {
98
        $this->setForSession($propertyName, $value);
99
    }
100
101
    public function flushCache(): void
102
    {
103
        $this->typeCache = null;
104
    }
105
106
    public function get(string $propertyName)
107
    {
108
        if (self::isCustomOption($propertyName)) {
109
            return $this->getCustomOptionValue($propertyName);
110
        }
111
112
        static $pgParStatusRecognized = [
113
            ConfigParam::IS_SUPERUSER => true,
114
            ConfigParam::SESSION_AUTHORIZATION => true,
115
            ConfigParam::APPLICATION_NAME => true,
116
            ConfigParam::DATE_STYLE => true,
117
            ConfigParam::INTERVAL_STYLE => true,
118
            ConfigParam::TIME_ZONE => true,
119
            ConfigParam::CLIENT_ENCODING => true,
120
            ConfigParam::STANDARD_CONFORMING_STRINGS => true,
121
            ConfigParam::INTEGER_DATETIMES => true,
122
            ConfigParam::SERVER_ENCODING => true,
123
            ConfigParam::SERVER_VERSION => true,
124
            ConfigParam::SERVER_VERSION_NUM => true,
125
        ];
126
        if (isset($pgParStatusRecognized[$propertyName])) {
127
            $connHandler = $this->connCtl->requireConnection();
128
            $val = pg_parameter_status($connHandler, $propertyName);
129
            if ($val !== false) {
130
                $type = ConfigParam::TYPEMAP[$propertyName];
131
                assert($type !== null);
132
                return ConfigParamType::createValue($type, $val);
133
            }
134
        }
135
136
        if ($propertyName == ConfigParam::MONEY_DEC_SEP) {
137
            return $this->getMoneyDecimalSeparator();
138
        }
139
140
        // determine the type
141
        $type = null;
142
        // try exact match first, for performance reasons; hopefully, indexing the type map will not be needed
143
        if (array_key_exists($propertyName, ConfigParam::TYPEMAP)) {
144
            $type = ConfigParam::TYPEMAP[$propertyName];
145
        } else {
146
            // okay, try to search for case-insensitive matches
147
            if ($this->typeCache === null) {
148
                $this->typeCache = array_change_key_case(ConfigParam::TYPEMAP, CASE_LOWER);
149
            }
150
            $lowerPropertyName = strtolower($propertyName);
151
            if (isset($this->typeCache[$lowerPropertyName])) {
152
                $type = $this->typeCache[$lowerPropertyName];
153
            }
154
        }
155
156
        $connHandler = $this->connCtl->requireConnection();
157
158
        if ($type === null) { // type unknown, try to look in the catalog for server configuration
159
            $query = 'SELECT setting, vartype, unit FROM pg_catalog.pg_settings WHERE name ILIKE $1';
160
            $res = pg_query_params($connHandler, $query, [$propertyName]);
161
            if ($res !== false || pg_num_rows($res) > 0) {
162
                $row = pg_fetch_assoc($res);
163
                try {
164
                    $type = ConfigParamType::detectType($row['vartype'], $row['setting'], $row['unit']);
165
                    $this->typeCache[$propertyName] = $type;
166
                    $this->typeCache[strtolower($propertyName)] = $type;
167
                    return ConfigParamType::createValue($type, $row['setting'], $row['unit']);
168
                } catch (UnsupportedException $e) {
169
                    throw new UnsupportedException("Unsupported type of configuration parameter '$propertyName'");
170
                }
171
            }
172
        }
173
174
        if ($type === null) {
175
            /* As the last resort, treat the value as a string. Note that the pg_settings view might not contain all,
176
             * e.g., "session_authorization" - which is recognized by pg_parameter_status() and is probably just an
177
             * exception which gets created automatically for the connection, but who knows...
178
             */
179
            $type = ConfigParamType::STRING;
180
        }
181
182
        $res = pg_query_params($connHandler, 'SELECT pg_catalog.current_setting($1)', [$propertyName]);
183
        if ($res !== false) {
184
            $val = pg_fetch_result($res, 0, 0);
185
            return ConfigParamType::createValue($type, $val);
186
        } else {
187
            return null;
188
        }
189
    }
190
191
    private function getCustomOptionValue(string $customOptionName): ?string
192
    {
193
        /* The custom option might not be recognized by PostgreSQL yet and an exception might be thrown, which would
194
         * break the current transaction (if any). Hence the savepoint.
195
         * Besides, custom options are always of type string, no need to even worry about the type.
196
         */
197
        $connHandler = $this->connCtl->requireConnection();
198
        $inTx = $this->txCtl->inTransaction();
199
        $savepoint = false;
200
        $needsRollback = false;
201
        try {
202
            if ($inTx) {
203
                $spRes = @pg_query($connHandler, 'SAVEPOINT _ivory_customized_option');
204
                if ($spRes === false) {
205
                    throw new \RuntimeException('Error retrieving a custom option value.');
206
                }
207
                $savepoint = true;
208
            }
209
            $res = @pg_query_params($connHandler, 'SELECT pg_catalog.current_setting($1)', [$customOptionName]);
210
            if ($res !== false) {
211
                return pg_fetch_result($res, 0, 0);
212
            } else {
213
                $needsRollback = true;
214
                return null;
215
            }
216
        } finally {
217
            // anything might have been thrown, which must not break the (savepoint-release savepoint) pair
218
            if ($savepoint) {
219
                if ($needsRollback) {
220
                    $rbRes = @pg_query($connHandler, 'ROLLBACK TO SAVEPOINT _ivory_customized_option');
221
                    if ($rbRes === false) {
222
                        throw new \RuntimeException(
223
                            'Error restoring the transaction status - it stayed in the aborted state.'
224
                        );
225
                    }
226
                }
227
228
                $spRes = @pg_query($connHandler, 'RELEASE SAVEPOINT _ivory_customized_option');
229
                if ($spRes === false) {
230
                    throw new \RuntimeException(
231
                        'Error restoring the transaction status - the savepoint probably stayed defined.'
232
                    );
233
                }
234
            }
235
        }
236
    }
237
238
    /**
239
     * @param string $propertyName
240
     * @return bool whether the requested property is a custom option
241
     */
242
    private static function isCustomOption(string $propertyName): bool
243
    {
244
        // see http://www.postgresql.org/docs/9.4/static/runtime-config-custom.html
245
        return (strpos($propertyName, '.') !== false);
246
    }
247
248
    public function defined(string $propertyName): bool
249
    {
250
        return ($this->get($propertyName) !== null);
251
    }
252
253
    public function setForTransaction(string $propertyName, $value): void
254
    {
255
        if (!$this->txCtl->inTransaction()) {
256
            return; // setting the option would have no effect as the implicit transaction would end immediately
257
        }
258
259
        $connHandler = $this->connCtl->requireConnection();
260
        pg_query_params($connHandler, 'SELECT pg_catalog.set_config($1, $2, TRUE)', [$propertyName, $value]);
261
262
        $this->watcher->handleSetForTransaction($propertyName);
263
        $this->notifyPropertyChange($propertyName, $value);
264
    }
265
266
    public function setForSession(string $propertyName, $value): void
267
    {
268
        $connHandler = $this->connCtl->requireConnection();
269
        pg_query_params($connHandler, 'SELECT pg_catalog.set_config($1, $2, FALSE)', [$propertyName, $value]);
270
271
        $this->watcher->handleSetForSession($propertyName);
272
        $this->notifyPropertyChange($propertyName, $value);
273
    }
274
275
    public function resetAll(): void
276
    {
277
        $connHandler = $this->connCtl->requireConnection();
278
        pg_query($connHandler, 'RESET ALL');
279
280
        $this->watcher->handleResetAll();
281
        $this->notifyPropertiesReset();
282
    }
283
284
    public function getEffectiveSearchPath(): array
285
    {
286
        if ($this->effectiveSearchPathCache === null) {
287
            $this->initEffectiveSearchPathCache();
288
            $refresher = function () {
289
                $this->initEffectiveSearchPathCache();
290
            };
291
            $this->addObserver(
292
                new class($refresher) implements IConfigObserver
293
                {
294
                    private $refresher;
295
296
                    public function __construct($refresher)
297
                    {
298
                        $this->refresher = $refresher;
299
                    }
300
301
                    public function handlePropertyChange(string $propertyName, $newValue): void
302
                    {
303
                        call_user_func($this->refresher);
304
                    }
305
306
                    public function handlePropertiesReset(IConnConfig $connConfig): void
307
                    {
308
                        call_user_func($this->refresher);
309
                    }
310
                },
311
                ConfigParam::SEARCH_PATH
312
            );
313
        }
314
315
        return $this->effectiveSearchPathCache;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->effectiveSearchPathCache could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
316
    }
317
318
    private function initEffectiveSearchPathCache(): void
319
    {
320
        $connHandler = $this->connCtl->requireConnection();
321
        $res = pg_query($connHandler, 'SELECT unnest(pg_catalog.current_schemas(TRUE))');
322
        if ($res !== false) {
323
            $this->effectiveSearchPathCache = [];
324
            while (($row = pg_fetch_row($res)) !== false) {
325
                $this->effectiveSearchPathCache[] = $row[0];
326
            }
327
        }
328
    }
329
330
    public function getMoneyDecimalSeparator(): string
331
    {
332
        $r = $this->stmtExec->rawQuery('SELECT 1.2::money::text');
333
        $v = $r->value();
334
        if (preg_match('~1(\D*)2~', $v, $m)) {
335
            return $m[1];
336
        } else {
337
            return '';
338
        }
339
    }
340
341
    public function getServerVersionNumber(): int
342
    {
343
        return $this->get(ConfigParam::SERVER_VERSION_NUM);
344
    }
345
346
    public function getServerMajorVersionNumber(): int
347
    {
348
        return intdiv($this->getServerVersionNumber(), 10000);
349
    }
350
351
    //region IObservableConnConfig
352
353
    public function addObserver(IConfigObserver $observer, $parameterName = null): void
354
    {
355
        if (is_array($parameterName)) {
356
            foreach ($parameterName as $pn) {
357
                $this->addObserverImpl($observer, $pn);
358
            }
359
        } else {
360
            $this->addObserverImpl($observer, (string)$parameterName); // null is represented by an empty string
361
        }
362
    }
363
364
    private function addObserverImpl(IConfigObserver $observer, string $parameterName): void
365
    {
366
        if (!isset($this->observers[$parameterName])) {
367
            $this->observers[$parameterName] = [];
368
        }
369
370
        $this->observers[$parameterName][] = $observer;
371
    }
372
373
    public function removeObserver(IConfigObserver $observer): void
374
    {
375
        foreach ($this->observers as $k => $obsList) {
376
            foreach ($obsList as $i => $obs) {
377
                if ($obs === $observer) {
378
                    unset($this->observers[$k][$i]);
379
                }
380
            }
381
        }
382
    }
383
384
    public function removeAllObservers(): void
385
    {
386
        $this->observers = [];
387
    }
388
389
    public function notifyPropertyChange(string $parameterName, $newValue = null): void
390
    {
391
        if ($newValue === null) {
392
            $newValue = $this->get($parameterName);
393
        }
394
395
        foreach ([$parameterName, null] as $k) {
396
            if (isset($this->observers[$k])) {
397
                foreach ($this->observers[$k] as $obs) {
398
                    $obs->handlePropertyChange($parameterName, $newValue);
399
                }
400
            }
401
        }
402
403
        if ($parameterName == ConfigParam::LC_MONETARY) {
404
            $this->notifyPropertyChange(ConfigParam::MONEY_DEC_SEP);
405
        }
406
    }
407
408
    public function notifyPropertiesReset(): void
409
    {
410
        $obsSet = [];
411
        foreach ($this->observers as $k => $obsList) {
412
            foreach ($obsList as $obs) {
413
                $obsSet[spl_object_hash($obs)] = $obs;
414
            }
415
        }
416
        foreach ($obsSet as $obs) {
417
            assert($obs instanceof IConfigObserver);
418
            $obs->handlePropertiesReset($this);
419
        }
420
    }
421
422
    //endregion
423
}
424