Completed
Push — master ( 5f3c84...0c19f1 )
by Ondřej
03:04
created

ConnConfig::getTxConfig()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 0
1
<?php
2
declare(strict_types=1);
3
4
namespace Ivory\Connection\Config;
5
6
use Ivory\Connection\ConnectionControl;
7
use Ivory\Connection\IObservableTransactionControl;
8
use Ivory\Connection\IStatementExecution;
9
use Ivory\Connection\TxConfig;
10
use Ivory\Exception\UnsupportedException;
11
use Ivory\Result\IQueryResult;
12
use Ivory\Value\Quantity;
13
14
/**
15
 * The standard implementation of runtime database configuration manager.
16
 *
17
 * {@inheritdoc}
18
 *
19
 * Besides the interface-specified methods, this implementation exposes the database configuration parameters as dynamic
20
 * ("overloaded") properties. The value currently in effect is always returned. For accessing properties with non-word
21
 * characters in their names (e.g., when using
22
 * {@link http://www.postgresql.org/docs/9.4/static/runtime-config-custom.html customized options}), the standard PHP
23
 * syntax `$config->{'some.prop'}` may be employed, or the {@link get()} method may simply be called.
24
 *
25
 * The implementation of read operations is lazy - no database query is made until actually needed. Some values are
26
 * gathered using the {@link pg_parameter_status()} function (these get cached internally by the PHP driver). Others are
27
 * directly queried and are not cached. For caching these, see the {@link CachingConnConfig} implementation.
28
 *
29
 * Data types of non-standard settings are fetched once and cached for the whole {@link ConnConfig} object lifetime
30
 * ({@link ConnConfig::flushCache()} may, of course, be used for flushing it in any case). Note that
31
 * {@link http://www.postgresql.org/docs/9.4/static/runtime-config-custom.html customized options} are always of type
32
 * `text`.
33
 *
34
 * As for the {@link IObservableConnConfig} implementation, any changes of configuration parameters must be done through
35
 * some of the methods offered by this class. Bypassing them (i.e., setting a parameter value in a raw query, or within
36
 * a database function) prevents the observers from being notified about the parameter value changes. In such cases, the
37
 * {@link ConnConfig::notifyPropertyChange()} or {@link ConnConfig::notifyPropertiesReset()} has to be called manually
38
 * for the new parameter value to be retrieved again.
39
 */
40
class ConnConfig implements IObservableConnConfig
41
{
42
    private $connCtl;
43
    private $stmtExec;
44
    private $txCtl;
45
    private $watcher;
46
47
    private $typeCache = null;
48
    /** @var string[]|null */
49
    private $effectiveSearchPathCache = null;
50
    /** @var IConfigObserver[][] map: parameter name or <tt>''</tt> => list of observers registered for changes of the
51
     *                             parameter (or any parameter for <tt>''</tt> (empty string) entry) */
52
    private $observers = [];
53
54
55
    public function __construct(
56
        ConnectionControl $connCtl,
57
        IStatementExecution $stmtExec,
58
        IObservableTransactionControl $txCtl
59
    ) {
60
        $this->connCtl = $connCtl;
61
        $this->stmtExec = $stmtExec;
62
        $this->txCtl = $txCtl;
63
64
        $this->watcher = new ConnConfigTransactionWatcher($this);
65
        $txCtl->addObserver($this->watcher);
66
    }
67
68
69
    /**
70
     * Alias for {@link ConnConfig::get().
71
     *
72
     * @param string $propertyName name of a configuration option
73
     * @return bool|float|int|Quantity|string|null current value of the requested option, or <tt>null</tt> if no such
74
     *                                               option has been defined yet
75
     */
76
    public function __get($propertyName)
77
    {
78
        return $this->get($propertyName);
79
    }
80
81
    /**
82
     * Alias for {@link ConnConfig::defined()}.
83
     *
84
     * @param string $propertyName name of a configuration option
85
     * @return bool whether the option is defined
86
     */
87
    public function __isset($propertyName)
88
    {
89
        return $this->defined($propertyName);
90
    }
91
92
    /**
93
     * Alias for {@link ConnConfig::setForSession()}.
94
     *
95
     * @param string $propertyName name of a configuration option
96
     * @param bool|string|int|float|Quantity $value the new value, or {@link ConnConfig::DEFAULT_VALUE} to use the
97
     *                                                option's default
98
     */
99
    public function __set($propertyName, $value)
100
    {
101
        $this->setForSession($propertyName, $value);
102
    }
103
104
    public function flushCache(): void
105
    {
106
        $this->typeCache = null;
107
    }
108
109
    public function get(string $propertyName)
110
    {
111
        if (self::isCustomOption($propertyName)) {
112
            return $this->getCustomOptionValue($propertyName);
113
        }
114
115
        static $pgParStatusRecognized = [
116
            ConfigParam::IS_SUPERUSER => true,
117
            ConfigParam::SESSION_AUTHORIZATION => true,
118
            ConfigParam::APPLICATION_NAME => true,
119
            ConfigParam::DATE_STYLE => true,
120
            ConfigParam::INTERVAL_STYLE => true,
121
            ConfigParam::TIME_ZONE => true,
122
            ConfigParam::CLIENT_ENCODING => true,
123
            ConfigParam::STANDARD_CONFORMING_STRINGS => true,
124
            ConfigParam::INTEGER_DATETIMES => true,
125
            ConfigParam::SERVER_ENCODING => true,
126
            ConfigParam::SERVER_VERSION => true,
127
        ];
128
        if (isset($pgParStatusRecognized[$propertyName])) {
129
            $connHandler = $this->connCtl->requireConnection();
130
            $val = pg_parameter_status($connHandler, $propertyName);
131
            if ($val !== false) {
132
                $type = ConfigParam::TYPEMAP[$propertyName];
133
                assert($type !== null);
134
                return ConfigParamType::createValue($type, $val);
135
            }
136
        }
137
138
        if ($propertyName == ConfigParam::MONEY_DEC_SEP) {
139
            return $this->getMoneyDecimalSeparator();
140
        }
141
142
        // determine the type
143
        $type = null;
144
        // try exact match first, for performance reasons; hopefully, indexing the type map will not be needed
145
        if (array_key_exists($propertyName, ConfigParam::TYPEMAP)) {
146
            $type = ConfigParam::TYPEMAP[$propertyName];
147
        } else {
148
            // okay, try to search for case-insensitive matches
149
            if ($this->typeCache === null) {
150
                $this->typeCache = array_change_key_case(ConfigParam::TYPEMAP, CASE_LOWER);
151
            }
152
            $lowerPropertyName = strtolower($propertyName);
153
            if (isset($this->typeCache[$lowerPropertyName])) {
154
                $type = $this->typeCache[$lowerPropertyName];
155
            }
156
        }
157
158
        $connHandler = $this->connCtl->requireConnection();
159
160
        if ($type === null) { // type unknown, try to look in the catalog for server configuration
161
            $query = 'SELECT setting, vartype, unit FROM pg_catalog.pg_settings WHERE name ILIKE $1';
162
            $res = pg_query_params($connHandler, $query, [$propertyName]);
163
            if ($res !== false || pg_num_rows($res) > 0) {
164
                $row = pg_fetch_assoc($res);
165
                try {
166
                    $type = ConfigParamType::detectType($row['vartype'], $row['setting'], $row['unit']);
167
                    $this->typeCache[$propertyName] = $type;
168
                    $this->typeCache[strtolower($propertyName)] = $type;
169
                    return ConfigParamType::createValue($type, $row['setting'], $row['unit']);
170
                } catch (UnsupportedException $e) {
171
                    throw new UnsupportedException("Unsupported type of configuration parameter '$propertyName'");
172
                }
173
            }
174
        }
175
176
        if ($type === null) {
177
            /* As the last resort, treat the value as a string. Note that the pg_settings view might not contain all,
178
             * e.g., "session_authorization" - which is recognized by pg_parameter_status() and is probably just an
179
             * exception which gets created automatically for the connection, but who knows...
180
             */
181
            $type = ConfigParamType::STRING;
182
        }
183
184
        $res = pg_query_params($connHandler, 'SELECT pg_catalog.current_setting($1)', [$propertyName]);
185
        if ($res !== false) {
186
            $val = pg_fetch_result($res, 0, 0);
187
            return ConfigParamType::createValue($type, $val);
188
        } else {
189
            return null;
190
        }
191
    }
192
193
    private function getCustomOptionValue(string $customOptionName): ?string
194
    {
195
        /* The custom option might not be recognized by PostgreSQL yet and an exception might be thrown, which would
196
         * break the current transaction (if any). Hence the savepoint.
197
         * Besides, custom options are always of type string, no need to even worry about the type.
198
         */
199
        $connHandler = $this->connCtl->requireConnection();
200
        $inTx = $this->txCtl->inTransaction();
201
        $savepoint = false;
202
        $needsRollback = false;
203
        try {
204
            if ($inTx) {
205
                $spRes = @pg_query($connHandler, 'SAVEPOINT _ivory_customized_option');
206
                if ($spRes === false) {
207
                    throw new \RuntimeException('Error retrieving a custom option value.');
208
                }
209
                $savepoint = true;
210
            }
211
            $res = @pg_query_params($connHandler, 'SELECT pg_catalog.current_setting($1)', [$customOptionName]);
212
            if ($res !== false) {
213
                return pg_fetch_result($res, 0, 0);
214
            } else {
215
                $needsRollback = true;
0 ignored issues
show
Unused Code introduced by
$needsRollback is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
216
                return null;
217
            }
218
        } finally {
219
            // anything might have been thrown, which must not break the (savepoint-release savepoint) pair
220
            if ($savepoint) {
221
                if ($needsRollback) {
222
                    $rbRes = @pg_query($connHandler, 'ROLLBACK TO SAVEPOINT _ivory_customized_option');
223
                    if ($rbRes === false) {
224
                        throw new \RuntimeException(
225
                            'Error restoring the transaction status - it stayed in the aborted state.'
226
                        );
227
                    }
228
                }
229
230
                $spRes = @pg_query($connHandler, 'RELEASE SAVEPOINT _ivory_customized_option');
231
                if ($spRes === false) {
232
                    throw new \RuntimeException(
233
                        'Error restoring the transaction status - the savepoint probably stayed defined.'
234
                    );
235
                }
236
            }
237
        }
238
    }
239
240
    /**
241
     * @param string $propertyName
242
     * @return bool whether the requested property is a custom option
243
     */
244
    private static function isCustomOption(string $propertyName): bool
245
    {
246
        // see http://www.postgresql.org/docs/9.4/static/runtime-config-custom.html
247
        return (strpos($propertyName, '.') !== false);
248
    }
249
250
    public function defined(string $propertyName): bool
251
    {
252
        return ($this->get($propertyName) !== null);
253
    }
254
255
    public function setForTransaction(string $propertyName, $value): void
256
    {
257
        if (!$this->txCtl->inTransaction()) {
258
            return; // setting the option would have no effect as the implicit transaction would end immediately
259
        }
260
261
        $connHandler = $this->connCtl->requireConnection();
262
        pg_query_params($connHandler, 'SELECT pg_catalog.set_config($1, $2, TRUE)', [$propertyName, $value]);
263
264
        $this->watcher->handleSetForTransaction($propertyName);
265
        $this->notifyPropertyChange($propertyName, $value);
266
    }
267
268
    public function setForSession(string $propertyName, $value): void
269
    {
270
        $connHandler = $this->connCtl->requireConnection();
271
        pg_query_params($connHandler, 'SELECT pg_catalog.set_config($1, $2, FALSE)', [$propertyName, $value]);
272
273
        $this->watcher->handleSetForSession($propertyName);
274
        $this->notifyPropertyChange($propertyName, $value);
275
    }
276
277
    public function resetAll(): void
278
    {
279
        $connHandler = $this->connCtl->requireConnection();
280
        pg_query($connHandler, 'RESET ALL');
281
282
        $this->watcher->handleResetAll();
283
        $this->notifyPropertiesReset();
284
    }
285
286
    public function getEffectiveSearchPath(): array
287
    {
288
        if ($this->effectiveSearchPathCache === null) {
289
            $this->initEffectiveSearchPathCache();
290
            $refresher = function () {
291
                $this->initEffectiveSearchPathCache();
292
            };
293
            $this->addObserver(
294
                new class($refresher) implements IConfigObserver
295
                {
296
                    private $refresher;
297
298
                    public function __construct($refresher)
299
                    {
300
                        $this->refresher = $refresher;
301
                    }
302
303
                    public function handlePropertyChange(string $propertyName, $newValue): void
304
                    {
305
                        call_user_func($this->refresher);
306
                    }
307
308
                    public function handlePropertiesReset(IConnConfig $connConfig): void
309
                    {
310
                        call_user_func($this->refresher);
311
                    }
312
                },
313
                ConfigParam::SEARCH_PATH
314
            );
315
        }
316
317
        return $this->effectiveSearchPathCache;
318
    }
319
320
    private function initEffectiveSearchPathCache(): void
321
    {
322
        $connHandler = $this->connCtl->requireConnection();
323
        $res = pg_query($connHandler, 'SELECT unnest(pg_catalog.current_schemas(TRUE))');
324
        if ($res !== false) {
325
            $this->effectiveSearchPathCache = [];
326
            while (($row = pg_fetch_row($res)) !== false) {
327
                $this->effectiveSearchPathCache[] = $row[0];
328
            }
329
        }
330
    }
331
332
    public function getMoneyDecimalSeparator(): string
333
    {
334
        /** @var IQueryResult $r */
335
        $r = $this->stmtExec->rawQuery('SELECT 1.2::money::text');
336
        $v = $r->value();
337
        if (preg_match('~1(\D*)2~', $v, $m)) {
338
            return $m[1];
339
        } else {
340
            return '';
341
        }
342
    }
343
344
345
    //region IObservableConnConfig
346
347
    public function addObserver(IConfigObserver $observer, $parameterName = null): void
348
    {
349
        if (is_array($parameterName)) {
350
            foreach ($parameterName as $pn) {
351
                $this->addObserverImpl($observer, $pn);
352
            }
353
        } else {
354
            $this->addObserverImpl($observer, (string)$parameterName); // null is represented by an empty string
355
        }
356
    }
357
358
    private function addObserverImpl(IConfigObserver $observer, string $parameterName): void
359
    {
360
        if (!isset($this->observers[$parameterName])) {
361
            $this->observers[$parameterName] = [];
362
        }
363
364
        $this->observers[$parameterName][] = $observer;
365
    }
366
367
    public function removeObserver(IConfigObserver $observer): void
368
    {
369
        foreach ($this->observers as $k => $obsList) {
370
            foreach ($obsList as $i => $obs) {
371
                if ($obs === $observer) {
372
                    unset($this->observers[$k][$i]);
373
                }
374
            }
375
        }
376
    }
377
378
    public function removeAllObservers(): void
379
    {
380
        $this->observers = [];
381
    }
382
383
    public function notifyPropertyChange(string $parameterName, $newValue = null): void
384
    {
385
        if ($newValue === null) {
386
            $newValue = $this->get($parameterName);
387
        }
388
389
        foreach ([$parameterName, null] as $k) {
390
            if (isset($this->observers[$k])) {
391
                foreach ($this->observers[$k] as $obs) {
392
                    $obs->handlePropertyChange($parameterName, $newValue);
393
                }
394
            }
395
        }
396
397
        if ($parameterName == ConfigParam::LC_MONETARY) {
398
            $this->notifyPropertyChange(ConfigParam::MONEY_DEC_SEP);
399
        }
400
    }
401
402
    public function notifyPropertiesReset(): void
403
    {
404
        /** @var IConfigObserver[] $obsSet */
405
        $obsSet = [];
406
        foreach ($this->observers as $k => $obsList) {
407
            foreach ($obsList as $obs) {
408
                $obsSet[spl_object_hash($obs)] = $obs;
409
            }
410
        }
411
        foreach ($obsSet as $obs) {
412
            $obs->handlePropertiesReset($this);
413
        }
414
    }
415
416
    //endregion
417
}
418