Completed
Pull Request — master (#4)
by James Ekow Abaka
02:32 queued 01:18
created

Driver::getDriverName()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 1
ccs 0
cts 0
cp 0
nc 1
1
<?php
2
3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2014-2018 James Ekow Abaka Ainooson
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
namespace ntentan\atiaa;
28
29
use ntentan\atiaa\exceptions\ConnectionException;
30
use ntentan\atiaa\exceptions\DatabaseDriverException;
31
use Psr\Log\LoggerInterface;
32
33
/**
34
 * A driver class for connecting to a specific database platform.
35
 * The Driver class is the main wrapper for atiaa. The driver class contains
36
 * an instance of PDO with which it performs its operations. Aside from wrapping
37
 * around PDO it also provides methods which makes it possible to quote strings
38
 * and identifiers in a platform independent fashion. The driver class is
39
 * responsible for loading the descriptors which are used for describing the
40
 * database schemas.
41
 */
42
abstract class Driver
43
{
44
    /**
45
     * The internal PDO connection that is wrapped by this driver.
46
     *
47
     * @var \PDO
48
     */
49
    private $pdo;
50
51
    /**
52
     * A logger for logging queries during debug.
53
     *
54
     * @var LoggerInterface
55
     */
56
    private $logger;
57
58
    /**
59
     * The default schema used in the connection.
60
     *
61
     * @var string
62
     */
63
    protected $defaultSchema;
64
65
    /**
66
     * The connection parameters with which this connection was established.
67
     *
68
     * @var array
69
     */
70
    protected $config;
71
72
    /**
73
     * An instance of the descriptor used internally.
74
     *
75
     * @var \ntentan\atiaa\Descriptor
76
     */
77
    private $descriptor;
78
    private static $transactionCount = 0;
79
80
    /**
81
     * Creates a new instance of the Atiaa driver. This class is usually initiated
82
     * through the \ntentan\atiaa\Atiaa::getConnection() method. For example
83
     * to create a new instance of a connection to a mysql database.
84
     *
85
     * ````php
86
     * use ntentan\atiaa\Driver;
87
     *
88
     * \\ This automatically insitatiates the driver class
89
     * $driver = Driver::getConnection(
90
     *     array(
91
     *         'driver' => 'mysql',
92
     *         'user' => 'root',
93
     *         'password' => 'rootpassy',
94
     *         'host' => 'localhost',
95
     *         'dbname' => 'somedb'
96
     *     )
97
     * );
98
     *
99
     * var_dump($driver->query("SELECT * FROM some_table");
100
     * var_dump($driver->describe());
101
     * ````
102
     *
103
     * @param array <string> $config The configuration with which to connect to the database.
104
     */
105 31
    public function __construct(array $config)
106
    {
107 31
        $this->config = $config;
108 31
    }
109
110 31
    public function connect()
111
    {
112 31
        $username = isset($this->config['user']) ? $this->config['user'] : null;
113 31
        $password = isset($this->config['password']) ? $this->config['password'] : null;
114
115
        try {
116 31
            $this->pdo = new \PDO($this->getDriverName().':'.$this->expand($this->config), $username, $password);
117 30
            $this->pdo->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
118 30
            $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
119 30
            $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
120 1
        } catch (\PDOException $e) {
121 1
            throw new ConnectionException("PDO failed to connect: {$e->getMessage()}");
122
        }
123 30
    }
124
125 19
    public function __destruct()
126
    {
127 19
        $this->disconnect();
128 19
    }
129
130
    /**
131
     * Close a connection to the database server.
132
     */
133 19
    public function disconnect()
134
    {
135 19
        $this->pdo = null;
136 19
        $this->pdo = new NullConnection();
137 19
    }
138
139
    /**
140
     * Get the default schema of the current connection.
141
     *
142
     * @return string
143
     */
144 10
    public function getDefaultSchema()
145
    {
146 10
        return $this->defaultSchema;
147
    }
148
149
    /**
150
     * Use the PDO driver to quote a string.
151
     *
152
     * @param type $string
153
     *
154
     * @throws ConnectionException
155
     *
156
     * @return string
157
     */
158 3
    public function quote($string)
159
    {
160 3
        return $this->getPDO()->quote($string);
161
    }
162
163
    /**
164
     * @param $statement
165
     *
166
     * @return mixed
167
     */
168 27
    private function fetchRows($statement)
169
    {
170
        try {
171 27
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
172
173 27
            return $rows;
174
        } catch (\PDOException $e) {
175
            // Skip any exceptions from fetching rows
176
        }
177
    }
178
179 21
    private function prepareQuery($query, $bindData)
180
    {
181 21
        $statement = $this->pdo->prepare($query);
182 21
        foreach ($bindData as $key => $value) {
183 21
            switch (gettype($value)) {
184 21
                case 'integer':
185
                    $type = \PDO::PARAM_INT;
186
                    break;
187 21
                case 'boolean':
188
                    $type = \PDO::PARAM_BOOL;
189
                    break;
190
                default:
191 21
                    $type = \PDO::PARAM_STR;
192 21
                    break;
193
            }
194 21
            $statement->bindValue(is_numeric($key) ? $key + 1 : $key, $value, $type);
195
        }
196
197 21
        return $statement;
198
    }
199
200
    /**
201
     * Pepare and execute a query, while binding data at the same time. Prevents
202
     * the writing of repetitive prepare and execute statements. This method
203
     * returns an array which contains the results of the query that was
204
     * executed. For queries which do not return any results a null is returned.
205
     *
206
     * @todo Add a parameter to cache prepared statements so they can be reused easily.
207
     *
208
     * @param string $query    The query to be executed quoted in PDO style
209
     * @param array  $bindData
210
     *
211
     * @throws DatabaseDriverException
212
     *
213
     * @return array <mixed>
214
     */
215 29
    public function query($query, $bindData = [])
216
    {
217
        try {
218 29
            if (empty($bindData)) {
219 25
                $statement = $this->pdo->query($query);
220
            } else {
221 21
                $statement = $this->prepareQuery($query, $bindData);
222 27
                $statement->execute();
223
            }
224 6
        } catch (\PDOException $e) {
225 3
            $boundData = json_encode($bindData);
226
227 3
            throw new DatabaseDriverException("{$e->getMessage()} [$query] [BOUND DATA:$boundData]");
228
        }
229 27
        if ($this->logger) {
230
            $this->logger->debug($query, $bindData);
231
        }
232 27
        $rows = $this->fetchRows($statement);
233 27
        $statement->closeCursor();
234
235 27
        return $rows;
236
    }
237
238
    /**
239
     * Runs a query but ensures that all identifiers are properly quoted by calling
240
     * the Driver::quoteQueryIdentifiers method on the query before executing it.
241
     *
242
     * @param string $query
243
     * @param bool   $bindData
244
     *
245
     * @throws DatabaseDriverException
246
     *
247
     * @return array <mixed>
248
     */
249 15
    public function quotedQuery($query, $bindData = [])
250
    {
251 15
        return $this->query($this->quoteQueryIdentifiers($query), $bindData);
0 ignored issues
show
Bug introduced by
It seems like $bindData defined by parameter $bindData on line 249 can also be of type boolean; however, ntentan\atiaa\Driver::query() does only seem to accept array, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
252
    }
253
254
    /**
255
     * Expands the configuration array into a format that can easily be passed
256
     * to PDO.
257
     *
258
     * @param array $params The query parameters
259
     *
260
     * @return string
261
     */
262 31
    private function expand($params)
263
    {
264 31
        unset($params['driver']);
265 31
        if (isset($params['file'])) {
266 31
            if ($params['file'] != '') {
267 20
                return $params['file'];
268
            }
269
        }
270
271 11
        $equated = [];
272 11
        foreach ($params as $key => $value) {
273 11
            if ($value == '') {
274 11
                continue;
275
            } else {
276 11
                $equated[] = "$key=$value";
277
            }
278
        }
279
280 11
        return implode(';', $equated);
281
    }
282
283
    /**
284
     * This method provides a system independent way of quoting identifiers in
285
     * queries. By default all identifiers can be quoted with double quotes (").
286
     * When a query quoted with double quotes is passed through this method the
287
     * output generated has the double quotes replaced with the quoting character
288
     * of the target database platform.
289
     *
290
     * @param string $query
291
     *
292
     * @return string
293
     */
294 18
    public function quoteQueryIdentifiers($query)
295
    {
296 18
        return preg_replace_callback(
297
            '/\"([a-zA-Z\_ ]*)\"/', function ($matches) {
298 18
                return $this->quoteIdentifier($matches[1]);
299 18
            }, $query
300
        );
301
    }
302
303
    /**
304
     * Returns an array description of the schema represented by the connection.
305
     * The description returns contains information about `tables`, `columns`, `keys`,
306
     * `constraints`, `views` and `indices`.
307
     *
308
     * @return array<mixed>
0 ignored issues
show
Documentation introduced by
Should the return type not be array<string,array>?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
309
     */
310 6
    public function describe()
311
    {
312 6
        return $this->getDescriptor()->describe();
313
    }
314
315
    /**
316
     * Returns the description of a database table as an associative array.
317
     *
318
     * @param string $table
319
     *
320
     * @return array<mixed>
321
     */
322 6
    public function describeTable($table)
323
    {
324 6
        $table = explode('.', $table);
325 6
        if (count($table) > 1) {
326
            $schema = $table[0];
327
            $table = $table[1];
328
        } else {
329 6
            $schema = $this->getDefaultSchema();
330 6
            $table = $table[0];
331
        }
332
333 6
        return $this->getDescriptor()->describeTables($schema, [$table], true);
334
    }
335
336
    /**
337
     * A wrapper arround PDO's beginTransaction method which uses a static reference
338
     * counter to implement nested transactions.
339
     */
340 6
    public function beginTransaction()
341
    {
342 6
        if (self::$transactionCount++ === 0) {
343 6
            $this->pdo->beginTransaction();
344
        }
345 6
    }
346
347
    /**
348
     * A wrapper around PDO's commit transaction method which uses a static reference
349
     * counter to implement nested transactions.
350
     */
351 3
    public function commit()
352
    {
353 3
        if (--self::$transactionCount === 0) {
354 3
            $this->pdo->commit();
355
        }
356 3
    }
357
358
    /**
359
     * A wrapper around PDO's rollback transaction methd which rolls back all
360
     * activities performed since the first call to begin transaction.
361
     * Unfortunately, transactions cannot be rolled back in a nested fashion.
362
     */
363 3
    public function rollback()
364
    {
365 3
        $this->pdo->rollBack();
366 3
        self::$transactionCount = 0;
367 3
    }
368
369
    /**
370
     * Return the underlying PDO object.
371
     *
372
     * @throws ConnectionException
373
     *
374
     * @return \PDO
375
     */
376 3
    public function getPDO()
377
    {
378 3
        if ($this->pdo === null) {
379
            throw new ConnectionException('A connection has not been established. Please call the connect() method.');
380
        }
381
382 3
        return $this->pdo;
383
    }
384
385
    /**
386
     * Returns an instance of a descriptor for a given driver.
387
     *
388
     * @return \ntentan\atiaa\Descriptor
389
     */
390 12
    private function getDescriptor()
391
    {
392 12
        if (!is_object($this->descriptor)) {
393 12
            $descriptorClass = '\\ntentan\\atiaa\\descriptors\\'.ucfirst($this->config['driver']).'Descriptor';
394 12
            $this->descriptor = new $descriptorClass($this);
395
        }
396
397 12
        return $this->descriptor;
398
    }
399
400
    /**
401
     * A wrapper around PDO's lastInsertId() method.
402
     *
403
     * @return mixed
404
     */
405
    public function getLastInsertId()
406
    {
407
        return $this->pdo->lastInsertId();
408
    }
409
410
    /**
411
     * Specify the default schema to use in cases where a schema is not provided
412
     * as part of the table reference.
413
     *
414
     * @param string $defaultSchema
415
     */
416
    public function setDefaultSchema($defaultSchema)
417
    {
418
        $this->defaultSchema = $defaultSchema;
419
    }
420
421
    abstract protected function getDriverName();
0 ignored issues
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
422
423
    abstract public function quoteIdentifier($identifier);
0 ignored issues
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
424
425 3
    public function setCleanDefaults($cleanDefaults)
426
    {
427 3
        $this->getDescriptor()->setCleanDefaults($cleanDefaults);
428 3
    }
429
}
430