Completed
Push — dev ( 7937c3...19c9e2 )
by James Ekow Abaka
21:26
created

Driver   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 390
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 88.29%

Importance

Changes 0
Metric Value
wmc 43
lcom 1
cbo 4
dl 0
loc 390
ccs 98
cts 111
cp 0.8829
rs 8.96
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A connect() 0 14 4
A __destruct() 0 4 1
A disconnect() 0 5 1
A getDefaultSchema() 0 4 1
A quote() 0 4 1
A fetchRows() 0 10 2
A quotedQuery() 0 4 1
A expand() 0 20 5
A quoteQueryIdentifiers() 0 8 1
A describe() 0 4 1
A beginTransaction() 0 6 2
A commit() 0 6 2
A describeTable() 0 13 2
A prepareQuery() 0 19 5
A query() 0 23 4
A rollback() 0 7 2
A getPDO() 0 8 2
A getDescriptor() 0 9 2
A getLastInsertId() 0 4 1
A setDefaultSchema() 0 4 1
getDriverName() 0 1 ?
quoteIdentifier() 0 1 ?
A setCleanDefaults() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Driver often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Driver, and based on these observations, apply Extract Interface, too.

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 32
    public function __construct(array $config)
106
    {
107 32
        $this->config = $config;
108 32
    }
109
110 32
    public function connect()
111
    {
112 32
        $username = isset($this->config['user']) ? $this->config['user'] : null;
113 32
        $password = isset($this->config['password']) ? $this->config['password'] : null;
114
115
        try {
116 32
            $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 2
        } catch (\PDOException $e) {
121 2
            throw new ConnectionException("PDO failed to connect: {$e->getMessage()}");
122
        }
123 30
    }
124
125 20
    public function __destruct()
126
    {
127 20
        $this->disconnect();
128 20
    }
129
130
    /**
131
     * Close a connection to the database server.
132
     */
133 20
    public function disconnect()
134
    {
135 20
        $this->pdo = null;
136 20
        $this->pdo = new NullConnection();
137 20
    }
138
139
    /**
140
     * Get the default schema of the current connection.
141
     *
142
     * @return string
143
     */
144 5
    public function getDefaultSchema()
145
    {
146 5
        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 24
    private function fetchRows($statement)
169
    {
170
        try {
171 24
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
172
173 24
            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 21
                case 'boolean': // casts to boolean seems unstable
186
                    $type = \PDO::PARAM_INT;
187
                    break;
188
                default:
189 21
                    $type = \PDO::PARAM_STR;
190 21
                    break;
191
            }
192
            // Bind values while adjusting numerical indices to start from 1
193 21
            $statement->bindValue(is_numeric($key) ? $key + 1 : $key, $value, $type);
194
        }
195
196 21
        return $statement;
197
    }
198
199
    /**
200
     * Pepare and execute a query, while binding data at the same time. Prevents
201
     * the writing of repetitive prepare and execute statements. This method
202
     * returns an array which contains the results of the query that was
203
     * executed. For queries which do not return any results a null is returned.
204
     *
205
     * @todo Add a parameter to cache prepared statements so they can be reused easily.
206
     *
207
     * @param string $query    The query to be executed quoted in PDO style
208
     * @param array  $bindData
209
     *
210
     * @throws DatabaseDriverException
211
     *
212
     * @return array <mixed>
213
     */
214 28
    public function query($query, $bindData = [])
215
    {
216
        try {
217 28
            if (empty($bindData)) {
218 20
                $statement = $this->pdo->query($query);
219
            } else {
220 21
                $statement = $this->prepareQuery($query, $bindData);
221 21
                $statement->execute();
222 24
                $statement->errorCode();
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 24
        if ($this->logger) {
230
            $this->logger->debug($query, $bindData);
231
        }
232 24
        $rows = $this->fetchRows($statement);
233 24
        $statement->closeCursor();
234
235 24
        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 32
    private function expand($params)
263
    {
264 32
        unset($params['driver']);
265 32
        if (isset($params['file'])) {
266 32
            if ($params['file'] != '') {
267 10
                return $params['file'];
268
            }
269
        }
270
271 22
        $equated = [];
272 22
        foreach ($params as $key => $value) {
273 22
            if ($value == '') {
274 22
                continue;
275
            } else {
276 22
                $equated[] = "$key=$value";
277
            }
278
        }
279
280 22
        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
        if(self::$transactionCount) {
366 3
            $this->pdo->rollBack();
367 3
            self::$transactionCount = 0;
368
        }
369
    }
370
371
    /**
372
     * Return the underlying PDO object.
373
     *
374
     * @throws ConnectionException
375
     *
376 3
     * @return \PDO
377
     */
378 3
    public function getPDO()
379
    {
380
        if ($this->pdo === null) {
381
            throw new ConnectionException('A connection has not been established. Please call the connect() method.');
382 3
        }
383
384
        return $this->pdo;
385
    }
386
387
    /**
388
     * Returns an instance of a descriptor for a given driver.
389
     *
390 12
     * @return \ntentan\atiaa\Descriptor
391
     */
392 12
    private function getDescriptor()
393 12
    {
394 12
        if (!is_object($this->descriptor)) {
395
            $descriptorClass = '\\ntentan\\atiaa\\descriptors\\'.ucfirst($this->config['driver']).'Descriptor';
396
            $this->descriptor = new $descriptorClass($this);
397 12
        }
398
399
        return $this->descriptor;
400
    }
401
402
    /**
403
     * A wrapper around PDO's lastInsertId() method.
404
     *
405
     * @return mixed
406
     */
407
    public function getLastInsertId()
408
    {
409
        return $this->pdo->lastInsertId();
410
    }
411
412
    /**
413
     * Specify the default schema to use in cases where a schema is not provided
414
     * as part of the table reference.
415
     *
416
     * @param string $defaultSchema
417
     */
418
    public function setDefaultSchema($defaultSchema)
419
    {
420
        $this->defaultSchema = $defaultSchema;
421
    }
422
423
    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...
424
425 3
    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...
426
427 3
    public function setCleanDefaults($cleanDefaults)
428 3
    {
429
        $this->getDescriptor()->setCleanDefaults($cleanDefaults);
430
    }
431
}
432