summaryrefslogtreecommitdiffstats
path: root/vendor/ipl/sql/src/Connection.php
blob: de84c72096681ae9fe61b7b3a591cded8050beaf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
<?php

namespace ipl\Sql;

use BadMethodCallException;
use Exception;
use InvalidArgumentException;
use ipl\Sql\Contract\Adapter;
use ipl\Sql\Contract\Quoter;
use ipl\Stdlib\Plugins;
use PDO;
use PDOStatement;

/**
 * Connection to a SQL database using the native PDO for database access
 */
class Connection implements Quoter
{
    use Plugins;

    /** @var Config */
    protected $config;

    /** @var ?PDO */
    protected $pdo;

    /** @var QueryBuilder */
    protected $queryBuilder;

    /** @var Adapter */
    protected $adapter;

    /**
     * Create a new database connection using the given config for initialising the options for the connection
     *
     * {@link init()} is called after construction.
     *
     * @param Config|iterable $config
     *
     * @throws InvalidArgumentException If there's no adapter for the given database available
     */
    public function __construct($config)
    {
        $config = $config instanceof Config ? $config : new Config($config);

        $this->addPluginLoader('adapter', __NAMESPACE__ . '\\Adapter');

        $adapter = $this->loadPlugin('adapter', $config->db);

        if (! $adapter) {
            throw new InvalidArgumentException("Can't load database adapter for '{$config->db}'.");
        }

        $this->adapter = new $adapter();
        $this->config = $config;

        $this->init();
    }

    /**
     * Proxy PDO method calls
     *
     * @param string $name      The name of the PDO method to call
     * @param array  $arguments Arguments for the method to call
     *
     * @return mixed
     *
     * @throws BadMethodCallException If the called method does not exist
     *
     */
    public function __call($name, array $arguments)
    {
        $this->connect();

        if (! method_exists($this->pdo, $name)) {
            $class = get_class($this);
            $message = "Call to undefined method $class::$name";

            throw new BadMethodCallException($message);
        }

        return call_user_func_array([$this->pdo, $name], $arguments);
    }

    /**
     * Initialise the database connection
     *
     * If you have to adjust the connection after construction, override this method.
     */
    public function init()
    {
    }

    /**
     * Get the database adapter
     *
     * @return Adapter
     */
    public function getAdapter()
    {
        return $this->adapter;
    }

    /**
     * Get the connection configuration
     *
     * @return Config
     */
    public function getConfig()
    {
        return $this->config;
    }

    /**
     * Get the query builder for the database connection
     *
     * @return QueryBuilder
     */
    public function getQueryBuilder()
    {
        if ($this->queryBuilder === null) {
            $this->queryBuilder = new QueryBuilder($this->adapter);
        }

        return $this->queryBuilder;
    }

    /**
     * Create and return the PDO instance
     *
     * This method is called via {@link connect()} to establish a database connection.
     * If the default PDO needs to be adjusted for a certain DBMS, override this method.
     *
     * @return PDO
     */
    protected function createPdoAdapter()
    {
        $adapter = $this->getAdapter();

        $config = $this->getConfig();

        return new PDO(
            $adapter->getDsn($config),
            $config->username,
            $config->password,
            $adapter->getOptions($config)
        );
    }

    /**
     * Connect to the database, if not already connected
     *
     * @return $this
     */
    public function connect()
    {
        if ($this->pdo !== null) {
            return $this;
        }

        $this->pdo = $this->createPdoAdapter();

        if (! empty($this->config->charset)) {
            $this->exec(sprintf('SET NAMES %s', $this->pdo->quote($this->config->charset)));
        }

        $this->adapter->setClientTimezone($this);

        return $this;
    }

    /**
     * Disconnect from the database
     *
     * @return $this
     */
    public function disconnect()
    {
        $this->pdo = null;

        return $this;
    }

    /**
     * Check whether the connection to the database is still available
     *
     * @param bool $reconnect Whether to automatically reconnect
     *
     * @return bool
     */
    public function ping($reconnect = true)
    {
        try {
            $this->query('SELECT 1')->closeCursor();
        } catch (Exception $e) {
            if (! $reconnect) {
                return false;
            }

            $this->disconnect();

            return $this->ping(false);
        }

        return true;
    }

    /**
     * Fetch and return all result rows as sequential array
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute.
     * @param array         $values Values to bind to the statement
     *
     * @return array
     */
    public function fetchAll($stmt, array $values = null)
    {
        return $this->prepexec($stmt, $values)
            ->fetchAll();
    }

    /**
     * Fetch and return the first column of all result rows as sequential array
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute.
     * @param array         $values Values to bind to the statement
     *
     * @return array
     */
    public function fetchCol($stmt, array $values = null)
    {
        return $this->prepexec($stmt, $values)
            ->fetchAll(PDO::FETCH_COLUMN, 0);
    }

    /**
     * Fetch and return the first row of the result rows
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute.
     * @param array         $values Values to bind to the statement
     *
     * @return array
     */
    public function fetchOne($stmt, array $values = null)
    {
        return $this->prepexec($stmt, $values)
            ->fetch();
    }

    /**
     * Alias of {@link fetchOne()}
     */
    public function fetchRow($stmt, array $values = null)
    {
        return $this->prepexec($stmt, $values)
            ->fetch();
    }

    /**
     * Fetch and return all result rows as an array of key-value pairs
     *
     * First column is the key and the second column is the value.
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute.
     * @param array         $values Values to bind to the statement
     *
     * @return array
     */
    public function fetchPairs($stmt, array $values = null)
    {
        return $this->prepexec($stmt, $values)
            ->fetchAll(PDO::FETCH_KEY_PAIR);
    }

    /**
     * Fetch and return the first column of the first result row
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute.
     * @param array         $values Values to bind to the statement
     *
     * @return string
     */
    public function fetchScalar($stmt, array $values = null)
    {
        return $this->prepexec($stmt, $values)
            ->fetchColumn(0);
    }

    /**
     * Yield each result row
     *
     * `Connection::yieldAll(Select|string $stmt [[, array $values], int $fetchMode [, mixed ...$fetchModeOptions]])`
     *
     * @param Select|string $stmt    The SQL statement to prepare and execute.
     * @param mixed         ...$args Values to bind to the statement, fetch mode for the statement, fetch mode options
     *
     * @return \Generator
     */
    public function yieldAll($stmt, ...$args)
    {
        $values = null;

        if (! empty($args)) {
            if (is_array($args[0])) {
                $values = array_shift($args);
            }
        }

        $fetchMode = null;

        if (! empty($args)) {
            $fetchMode = array_shift($args);

            switch ($fetchMode) {
                case PDO::FETCH_KEY_PAIR:
                    foreach ($this->yieldPairs($stmt, $values) as $key => $value) {
                        yield $key => $value;
                    }

                    return;
                case PDO::FETCH_COLUMN:
                    if (empty($args)) {
                        $args[] = 0;
                    }

                    break;
            }
        }

        $sth = $this->prepexec($stmt, $values);

        if ($fetchMode !== null) {
            $sth->setFetchMode($fetchMode, ...$args);
        }

        foreach ($sth as $key => $row) {
            yield $key => $row;
        }
    }

    /**
     * Yield the first column of each result row
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute
     * @param array         $values Values to bind to the statement
     *
     * @return \Generator
     */
    public function yieldCol($stmt, array $values = null)
    {
        $sth = $this->prepexec($stmt, $values);

        $sth->setFetchMode(PDO::FETCH_COLUMN, 0);

        foreach ($sth as $key => $row) {
            yield $key => $row;
        }
    }

    /**
     * Yield key-value pairs with the first column as key and the second column as value for each result row
     *
     * @param Select|string $stmt   The SQL statement to prepare and execute
     * @param array         $values Values to bind to the statement
     *
     * @return \Generator
     */
    public function yieldPairs($stmt, array $values = null)
    {
        $sth = $this->prepexec($stmt, $values);

        $sth->setFetchMode(PDO::FETCH_NUM);

        foreach ($sth as $row) {
            list($key, $value) = $row;

            yield $key => $value;
        }
    }

    /**
     * Prepare and execute the given statement
     *
     * @param Delete|Insert|Select|Update|string $stmt   The SQL statement to prepare and execute
     * @param string|array                       $values Values to bind to the statement, if any
     *
     * @return PDOStatement
     */
    public function prepexec($stmt, $values = null)
    {
        if ($values !== null && ! is_array($values)) {
            $values = [$values];
        }

        if (is_object($stmt)) {
            list($stmt, $values) = $this->getQueryBuilder()->assemble($stmt);
        }

        $this->connect();

        $sth = $this->pdo->prepare($stmt);
        $sth->execute($values);

        return $sth;
    }

    /**
     * Prepare and execute the given Select query
     *
     * @param Select $select
     *
     * @return PDOStatement
     */
    public function select(Select $select)
    {
        list($stmt, $values) = $this->getQueryBuilder()->assembleSelect($select);

        return $this->prepexec($stmt, $values);
    }

    /**
     * Insert a table row with the specified data
     *
     * @param string   $table The table to insert data into. The table specification must be in
     *                        one of the following formats: 'table' or 'schema.table'
     * @param iterable $data  Row data in terms of column-value pairs
     *
     * @return PDOStatement
     *
     * @throws InvalidArgumentException If data type is invalid
     */
    public function insert($table, $data)
    {
        $insert = (new Insert())
            ->into($table)
            ->values($data);

        return $this->prepexec($insert);
    }

    /**
     * Update table rows with the specified data, optionally based on a given condition
     *
     * @param string|array $table     The table to update. The table specification must be in one of
     *                                the following formats:
     *                                'table', 'table alias', ['alias' => 'table']
     * @param iterable     $data      The columns to update in terms of column-value pairs
     * @param mixed        $condition The WHERE condition
     * @param string       $operator  The operator to combine multiple conditions with,
     *                                if the condition is in the array format
     *
     * @return PDOStatement
     *
     * @throws InvalidArgumentException If data type is invalid
     */
    public function update($table, $data, $condition = null, $operator = Sql::ALL)
    {
        $update = (new Update())
            ->table($table)
            ->set($data);

        if ($condition !== null) {
            $update->where($condition, $operator);
        }

        return $this->prepexec($update);
    }

    /**
     * Delete table rows, optionally based on a given condition
     *
     * @param string|array $table     The table to delete data from. The table specification must be in one of the
     *                                following formats: 'table', 'table alias', ['alias' => 'table']
     * @param mixed        $condition The WHERE condition
     * @param string       $operator  The operator to combine multiple conditions with,
     *                                if the condition is in the array format
     *
     * @return PDOStatement
     */
    public function delete($table, $condition = null, $operator = Sql::ALL)
    {
        $delete = (new Delete())
            ->from($table);

        if ($condition !== null) {
            $delete->where($condition, $operator);
        }

        return $this->prepexec($delete);
    }

    /**
     * Begin a transaction
     *
     * @return bool Whether the transaction was started successfully
     */
    public function beginTransaction()
    {
        $this->connect();

        return $this->pdo->beginTransaction();
    }

    /**
     * Commit a transaction
     *
     * @return bool Whether the transaction was committed successfully
     */
    public function commitTransaction()
    {
        return $this->pdo->commit();
    }

    /**
     * Roll back a transaction
     *
     * @return bool Whether the transaction was rolled back successfully
     */
    public function rollBackTransaction()
    {
        return $this->pdo->rollBack();
    }

    /**
     * Run the given callback in a transaction
     *
     * @param callable $callback The callback to run in a transaction.
     *                           This connection instance is passed as parameter to the callback
     *
     * @return  mixed The return value of the callback
     *
     * @throws  Exception If an error occurs when running the callback
     */
    public function transaction(callable $callback)
    {
        $this->beginTransaction();

        try {
            $result = call_user_func($callback, $this);
            $this->commitTransaction();
        } catch (Exception $e) {
            $this->rollBackTransaction();

            throw $e;
        }

        return $result;
    }

    public function quoteIdentifier($identifier)
    {
        return $this->getAdapter()->quoteIdentifier($identifier);
    }
}