Completed
Push — master ( 8a3ba3...5e5262 )
by James Ekow Abaka
12s
created

Driver   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 90.09%

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 4
dl 0
loc 388
ccs 100
cts 111
cp 0.9009
rs 9.0399
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 describeTable() 0 13 2
A beginTransaction() 0 6 2
A commit() 0 6 2
A rollback() 0 5 1
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
A prepareQuery() 0 19 5
A query() 0 23 4

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 31
            $this->pdo->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
118 31
            $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
119 31
            $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 31
    }
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 16
    public function getDefaultSchema()
145
    {
146 16
        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 28
    private function fetchRows($statement)
169
    {
170
        try {
171 28
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
172
173 28
            return $rows;
174
        } catch (\PDOException $e) {
175
            // Skip any exceptions from fetching rows
176
        }
177
    }
178
179 22
    private function prepareQuery($query, $bindData)
180
    {
181 22
        $statement = $this->pdo->prepare($query);
182 22
        foreach ($bindData as $key => $value) {
183 22
            switch (gettype($value)) {
184 22
                case 'integer':
185 22
                case 'boolean': // casts to boolean seems unstable
186
                    $type = \PDO::PARAM_INT;
187
                    break;
188
                default:
189 22
                    $type = \PDO::PARAM_STR;
190 22
                    break;
191
            }
192
            // Bind values while adjusting numerical indices to start from 1
193 22
            $statement->bindValue(is_numeric($key) ? $key + 1 : $key, $value, $type);
194
        }
195
196 22
        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 30
    public function query($query, $bindData = [])
215
    {
216
        try {
217 30
            if (empty($bindData)) {
218 26
                $statement = $this->pdo->query($query);
219
            } else {
220 22
                $statement = $this->prepareQuery($query, $bindData);
221 22
                $statement->execute();
222 28
                $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 28
        if ($this->logger) {
230
            $this->logger->debug($query, $bindData);
231
        }
232 28
        $rows = $this->fetchRows($statement);
233 28
        $statement->closeCursor();
234
235 28
        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 16
    public function quotedQuery($query, $bindData = [])
250
    {
251 16
        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 20
                return $params['file'];
268
            }
269
        }
270
271 12
        $equated = [];
272 12
        foreach ($params as $key => $value) {
273 12
            if ($value == '') {
274 12
                continue;
275
            } else {
276 12
                $equated[] = "$key=$value";
277
            }
278
        }
279
280 12
        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 19
    public function quoteQueryIdentifiers($query)
295
    {
296 19
        return preg_replace_callback(
297
            '/\"([a-zA-Z\_ ]*)\"/', function ($matches) {
298 19
                return $this->quoteIdentifier($matches[1]);
299 19
            }, $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 7
    public function describeTable($table)
323
    {
324 7
        $table = explode('.', $table);
325 7
        if (count($table) > 1) {
326 1
            $schema = $table[0];
327 1
            $table = $table[1];
328
        } else {
329 6
            $schema = $this->getDefaultSchema();
330 6
            $table = $table[0];
331
        }
332
333 7
        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 13
    private function getDescriptor()
391
    {
392 13
        if (!is_object($this->descriptor)) {
393 13
            $descriptorClass = '\\ntentan\\atiaa\\descriptors\\'.ucfirst($this->config['driver']).'Descriptor';
394 13
            $this->descriptor = new $descriptorClass($this);
395
        }
396
397 13
        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