Completed
Push — master ( c27d2d...a49ccc )
by Ondřej
03:50
created

ConnConfig::defined()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
251
    {
252
        if (!$this->txCtl->inTransaction()) {
253
            return; // setting the option would have no effect as the implicit transaction would end immediately
254
        }
255
256
        $connHandler = $this->connCtl->requireConnection();
257
        pg_query_params($connHandler, 'SELECT pg_catalog.set_config($1, $2, TRUE)', [$propertyName, $value]);
258
259
        $this->watcher->handleSetForTransaction($propertyName);
260
        $this->notifyPropertyChange($propertyName, $value);
261
    }
262
263 View Code Duplication
    public function setForSession(string $propertyName, $value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
264
    {
265
        $connHandler = $this->connCtl->requireConnection();
266
        pg_query_params($connHandler, 'SELECT pg_catalog.set_config($1, $2, FALSE)', [$propertyName, $value]);
267
268
        $this->watcher->handleSetForSession($propertyName);
269
        $this->notifyPropertyChange($propertyName, $value);
270
    }
271
272
    public function resetAll()
273
    {
274
        $connHandler = $this->connCtl->requireConnection();
275
        pg_query($connHandler, 'RESET ALL');
276
277
        $this->watcher->handleResetAll();
278
        $this->notifyPropertiesReset();
279
    }
280
281
    public function getTxConfig(): TxConfig
282
    {
283
        return self::createTxConfig(
284
            $this->transaction_isolation,
0 ignored issues
show
Documentation introduced by
The property transaction_isolation does not exist on object<Ivory\Connection\Config\ConnConfig>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
285
            $this->transaction_read_only,
0 ignored issues
show
Documentation introduced by
The property transaction_read_only does not exist on object<Ivory\Connection\Config\ConnConfig>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
286
            $this->transaction_deferrable
0 ignored issues
show
Documentation introduced by
The property transaction_deferrable does not exist on object<Ivory\Connection\Config\ConnConfig>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
287
        );
288
    }
289
290
    public function getDefaultTxConfig(): TxConfig
291
    {
292
        return self::createTxConfig(
293
            $this->get(ConfigParam::DEFAULT_TRANSACTION_ISOLATION),
294
            $this->get(ConfigParam::DEFAULT_TRANSACTION_READ_ONLY),
295
            $this->get(ConfigParam::DEFAULT_TRANSACTION_DEFERRABLE)
296
        );
297
    }
298
299
    private static function createTxConfig(string $isolationLevel, $readOnly, $deferrable)
300
    {
301
        $txConf = new TxConfig();
302
        $txConf->setReadOnly($readOnly);
303
        $txConf->setDeferrable($deferrable);
304
305
        static $isolationLevels = [
306
            'serializable' => TxConfig::ISOLATION_SERIALIZABLE,
307
            'repeatable read' => TxConfig::ISOLATION_REPEATABLE_READ,
308
            'read committed' => TxConfig::ISOLATION_READ_COMMITTED,
309
            'read uncommitted' => TxConfig::ISOLATION_READ_UNCOMMITTED,
310
        ];
311
        if (!isset($isolationLevels[$isolationLevel])) {
312
            throw new \InvalidArgumentException("Unrecognized transaction isolation level: '$isolationLevel'");
313
        }
314
        $txConf->setIsolationLevel($isolationLevels[$isolationLevel]);
315
316
        return $txConf;
317
    }
318
319
    public function getEffectiveSearchPath()
320
    {
321
        if ($this->effectiveSearchPathCache === null) {
322
            $this->initEffectiveSearchPathCache();
323
            $refresher = function () {
324
                $this->initEffectiveSearchPathCache();
325
            };
326
            $this->addObserver(
327
                new class($refresher) implements IConfigObserver
328
                {
329
                    private $refresher;
330
331
                    public function __construct($refresher)
332
                    {
333
                        $this->refresher = $refresher;
334
                    }
335
336
                    public function handlePropertyChange(string $propertyName, $newValue)
337
                    {
338
                        call_user_func($this->refresher);
339
                    }
340
341
                    public function handlePropertiesReset(IConnConfig $connConfig)
342
                    {
343
                        call_user_func($this->refresher);
344
                    }
345
                },
346
                ConfigParam::SEARCH_PATH
347
            );
348
        }
349
350
        return $this->effectiveSearchPathCache;
351
    }
352
353
    private function initEffectiveSearchPathCache()
354
    {
355
        $connHandler = $this->connCtl->requireConnection();
356
        $res = pg_query($connHandler, 'SELECT unnest(pg_catalog.current_schemas(TRUE))');
357
        if ($res !== false) {
358
            $this->effectiveSearchPathCache = [];
359
            while (($row = pg_fetch_row($res)) !== false) {
360
                $this->effectiveSearchPathCache[] = $row[0];
361
            }
362
        }
363
    }
364
365
    public function getMoneyDecimalSeparator(): string
366
    {
367
        /** @var IQueryResult $r */
368
        $r = $this->stmtExec->rawQuery('SELECT 1.2::money::text');
369
        $v = $r->value();
370
        if (preg_match('~1(\D*)2~', $v, $m)) {
371
            return $m[1];
372
        } else {
373
            return '';
374
        }
375
    }
376
377
378
    //region IObservableConnConfig
379
380
    public function addObserver(IConfigObserver $observer, $parameterName = null)
381
    {
382
        if (is_array($parameterName)) {
383
            foreach ($parameterName as $pn) {
384
                $this->addObserverImpl($observer, $pn);
385
            }
386
        } else {
387
            $this->addObserverImpl($observer, (string)$parameterName); // null is represented by an empty string
388
        }
389
    }
390
391
    private function addObserverImpl(IConfigObserver $observer, string $parameterName)
392
    {
393
        if (!isset($this->observers[$parameterName])) {
394
            $this->observers[$parameterName] = [];
395
        }
396
397
        $this->observers[$parameterName][] = $observer;
398
    }
399
400
    public function removeObserver(IConfigObserver $observer)
401
    {
402
        foreach ($this->observers as $k => $obsList) {
403
            foreach ($obsList as $i => $obs) {
404
                if ($obs === $observer) {
405
                    unset($this->observers[$k][$i]);
406
                }
407
            }
408
        }
409
    }
410
411
    public function removeAllObservers()
412
    {
413
        $this->observers = [];
414
    }
415
416
    public function notifyPropertyChange(string $parameterName, $newValue = null)
417
    {
418
        if ($newValue === null) {
419
            $newValue = $this->get($parameterName);
420
        }
421
422
        foreach ([$parameterName, null] as $k) {
423
            if (isset($this->observers[$k])) {
424
                foreach ($this->observers[$k] as $obs) {
425
                    $obs->handlePropertyChange($parameterName, $newValue);
426
                }
427
            }
428
        }
429
430
        if ($parameterName == ConfigParam::LC_MONETARY) {
431
            $this->notifyPropertyChange(ConfigParam::MONEY_DEC_SEP);
432
        }
433
    }
434
435
    public function notifyPropertiesReset()
436
    {
437
        /** @var IConfigObserver[] $obsSet */
438
        $obsSet = [];
439
        foreach ($this->observers as $k => $obsList) {
440
            foreach ($obsList as $obs) {
441
                $obsSet[spl_object_hash($obs)] = $obs;
442
            }
443
        }
444
        foreach ($obsSet as $obs) {
445
            $obs->handlePropertiesReset($this);
446
        }
447
    }
448
449
    //endregion
450
}
451