Completed
Push — dev ( 490e75...7168b9 )
by James Ekow Abaka
02:24
created

Driver   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 360
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 360
ccs 100
cts 111
cp 0.9009
rs 9.0399
c 0
b 0
f 0

24 Methods

Rating   Name   Duplication   Size   Complexity  
getDriverName() 0 1 ?
quoteIdentifier() 0 1 ?
A __construct() 0 4 1
A connect() 0 13 4
A __destruct() 0 4 1
A disconnect() 0 5 1
A getDefaultSchema() 0 4 1
A quote() 0 4 1
A fetchRows() 0 9 2
A quotedQuery() 0 4 1
A expand() 0 19 5
A describe() 0 4 1
A describeTable() 0 12 2
A beginTransaction() 0 6 2
A commit() 0 6 2
A rollback() 0 5 1
A getPDO() 0 7 2
A getDescriptor() 0 8 2
A getLastInsertId() 0 4 1
A setDefaultSchema() 0 4 1
A setCleanDefaults() 0 4 1
A quoteQueryIdentifiers() 0 8 1
A prepareQuery() 0 18 5
A query() 0 21 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
namespace ntentan\atiaa;
4
5
use ntentan\atiaa\exceptions\DatabaseDriverException;
6
use ntentan\atiaa\exceptions\ConnectionException;
7
use Psr\Log\LoggerInterface;
8
9
/**
10
 * A driver class for connecting to a specific database platform.
11
 * The Driver class is the main wrapper for atiaa. The driver class contains
12
 * an instance of PDO with which it performs its operations. Aside from wrapping
13
 * around PDO it also provides methods which makes it possible to quote strings
14
 * and identifiers in a platform independent fashion. The driver class is
15
 * responsible for loading the descriptors which are used for describing the
16
 * database schemas.
17
 */
18
abstract class Driver
19
{
20
21
    /**
22
     * The internal PDO connection that is wrapped by this driver.
23
     * @var \PDO
24
     */
25
    private $pdo;
26
27
    /**
28
     * A logger for logging queries during debug.
29
     * @var LoggerInterface
30
     */
31
    private $logger;
32
33
    /**
34
     * The default schema used in the connection.
35
     * @var string
36
     */
37
    protected $defaultSchema;
38
39
    /**
40
     * The connection parameters with which this connection was established.
41
     * @var array
42
     */
43
    protected $config;
44
45
    /**
46
     * An instance of the descriptor used internally.
47
     * @var \ntentan\atiaa\Descriptor
48
     */
49
    private $descriptor;
50
    private static $transactionCount = 0;
51
52
    /**
53
     * Creates a new instance of the Atiaa driver. This class is usually initiated
54
     * through the \ntentan\atiaa\Atiaa::getConnection() method. For example
55
     * to create a new instance of a connection to a mysql database.
56
     *
57
     * ````php
58
     * use ntentan\atiaa\Driver;
59
     *
60
     * \\ This automatically insitatiates the driver class
61
     * $driver = Driver::getConnection(
62
     *     array(
63
     *         'driver' => 'mysql',
64
     *         'user' => 'root',
65
     *         'password' => 'rootpassy',
66
     *         'host' => 'localhost',
67
     *         'dbname' => 'somedb'
68
     *     )
69
     * );
70
     *
71
     * var_dump($driver->query("SELECT * FROM some_table");
72
     * var_dump($driver->describe());
73
     * ````
74
     *
75
     * @param array <string> $config The configuration with which to connect to the database.
76
     */
77 33
    public function __construct(array $config)
78
    {
79 33
        $this->config = $config;
80 33
    }
81
82 33
    public function connect()
83
    {
84 33
        $username = isset($this->config['user']) ? $this->config['user'] : null;
85 33
        $password = isset($this->config['password']) ? $this->config['password'] : null;
86
        try {
87 33
            $this->pdo = new \PDO($this->getDriverName() . ":" . $this->expand($this->config), $username, $password);
88 31
            $this->pdo->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
89 31
            $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
90 31
            $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
91 2
        } catch (\PDOException $e) {
92 2
            throw new ConnectionException("PDO failed to connect: {$e->getMessage()}");
93
        }
94 31
    }
95
96 20
    public function __destruct()
97
    {
98 20
        $this->disconnect();
99 20
    }
100
101
    /**
102
     * Close a connection to the database server.
103
     */
104 20
    public function disconnect()
105
    {
106 20
        $this->pdo = null;
107 20
        $this->pdo = new NullConnection();
108 20
    }
109
110
    /**
111
     * Get the default schema of the current connection.
112
     * @return string
113
     */
114 11
    public function getDefaultSchema()
115
    {
116 11
        return $this->defaultSchema;
117
    }
118
119
    /**
120
     * Use the PDO driver to quote a string.
121
     * @param type $string
122
     * @return string
123
     * @throws ConnectionException
124
     */
125 3
    public function quote($string)
126
    {
127 3
        return $this->getPDO()->quote($string);
128
    }
129
130
    /**
131
     *
132
     * @param $statement
133
     * @return mixed
134
     */
135 25
    private function fetchRows($statement)
136
    {
137
        try {
138 25
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
139 25
            return $rows;
140
        } catch (\PDOException $e) {
141
            // Skip any exceptions from fetching rows
142
        }
143
    }
144
145 22
    private function prepareQuery($query, $bindData)
146
    {
147 22
        $statement = $this->pdo->prepare($query);
148 22
        foreach ($bindData as $key => $value) {
149 22
            switch (gettype($value)) {
150 22
                case "integer":
151 22
                case "boolean": // casts to boolean seems unstable
152
                    $type = \PDO::PARAM_INT;
153
                    break;
154
                default:
155 22
                    $type = \PDO::PARAM_STR;
156 22
                    break;
157
            }
158
            // Bind values while adjusting numerical indices to start from 1
159 22
            $statement->bindValue(is_numeric($key) ? $key + 1 : $key, $value, $type);
160
        }
161 22
        return $statement;
162
    }
163
164
    /**
165
     * Pepare and execute a query, while binding data at the same time. Prevents
166
     * the writing of repetitive prepare and execute statements. This method
167
     * returns an array which contains the results of the query that was
168
     * executed. For queries which do not return any results a null is returned.
169
     *
170
     * @todo Add a parameter to cache prepared statements so they can be reused easily.
171
     *
172
     * @param string $query The query to be executed quoted in PDO style
173
     * @param array $bindData
174
     * @return array <mixed>
175
     * @throws DatabaseDriverException
176
     */
177 29
    public function query($query, $bindData = [])
178
    {
179
        try {
180 29
            if (empty($bindData)) {
181 21
                $statement = $this->pdo->query($query);
182
            } else {
183 22
                $statement = $this->prepareQuery($query, $bindData);
184 22
                $statement->execute();
185 25
                $statement->errorCode();
186
            }
187 6
        } catch (\PDOException $e) {
188 3
            $boundData = json_encode($bindData);
189 3
            throw new DatabaseDriverException("{$e->getMessage()} [$query] [BOUND DATA:$boundData]");
190
        }
191 25
        if ($this->logger) {
192
            $this->logger->debug($query, $bindData);
193
        }
194 25
        $rows = $this->fetchRows($statement);
195 25
        $statement->closeCursor();
196 25
        return $rows;
197
    }
198
199
    /**
200
     * Runs a query but ensures that all identifiers are properly quoted by calling
201
     * the Driver::quoteQueryIdentifiers method on the query before executing it.
202
     *
203
     * @param string $query
204
     * @param bool $bindData
205
     * @return array <mixed>
206
     * @throws DatabaseDriverException
207
     */
208 16
    public function quotedQuery($query, $bindData = [])
209
    {
210 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 208 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...
211
    }
212
213
    /**
214
     * Expands the configuration array into a format that can easily be passed
215
     * to PDO.
216
     *
217
     * @param array $params The query parameters
218
     * @return string
219
     */
220 33
    private function expand($params)
221
    {
222 33
        unset($params['driver']);
223 33
        if (isset($params['file'])) {
224 33
            if ($params['file'] != '') {
225 10
                return $params['file'];
226
            }
227
        }
228
229 23
        $equated = array();
230 23
        foreach ($params as $key => $value) {
231 23
            if ($value == '') {
232 23
                continue;
233
            } else {
234 23
                $equated[] = "$key=$value";
235
            }
236
        }
237 23
        return implode(';', $equated);
238
    }
239
240
    /**
241
     * This method provides a system independent way of quoting identifiers in
242
     * queries. By default all identifiers can be quoted with double quotes (").
243
     * When a query quoted with double quotes is passed through this method the
244
     * output generated has the double quotes replaced with the quoting character
245
     * of the target database platform.
246
     *
247
     * @param string $query
248
     * @return string
249
     */
250 19
    public function quoteQueryIdentifiers($query)
251
    {
252 19
        return preg_replace_callback(
253
            '/\"([a-zA-Z\_ ]*)\"/', function ($matches) {
254 19
            return $this->quoteIdentifier($matches[1]);
255 19
        }, $query
256
        );
257
    }
258
259
    /**
260
     * Returns an array description of the schema represented by the connection.
261
     * The description returns contains information about `tables`, `columns`, `keys`,
262
     * `constraints`, `views` and `indices`.
263
     *
264
     * @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...
265
     */
266 6
    public function describe()
267
    {
268 6
        return $this->getDescriptor()->describe();
269
    }
270
271
    /**
272
     * Returns the description of a database table as an associative array.
273
     *
274
     * @param string $table
275
     * @return array<mixed>
276
     */
277 7
    public function describeTable($table)
278
    {
279 7
        $table = explode('.', $table);
280 7
        if (count($table) > 1) {
281 1
            $schema = $table[0];
282 1
            $table = $table[1];
283
        } else {
284 6
            $schema = $this->getDefaultSchema();
285 6
            $table = $table[0];
286
        }
287 7
        return $this->getDescriptor()->describeTables($schema, array($table), true);
288
    }
289
290
    /**
291
     * A wrapper arround PDO's beginTransaction method which uses a static reference
292
     * counter to implement nested transactions.
293
     */
294 6
    public function beginTransaction()
295
    {
296 6
        if (self::$transactionCount++ === 0) {
297 6
            $this->pdo->beginTransaction();
298
        }
299 6
    }
300
301
    /**
302
     * A wrapper around PDO's commit transaction method which uses a static reference
303
     * counter to implement nested transactions.
304
     */
305 3
    public function commit()
306
    {
307 3
        if (--self::$transactionCount === 0) {
308 3
            $this->pdo->commit();
309
        }
310 3
    }
311
312
    /**
313
     * A wrapper around PDO's rollback transaction methd which rolls back all
314
     * activities performed since the first call to begin transaction.
315
     * Unfortunately, transactions cannot be rolled back in a nested fashion.
316
     */
317 3
    public function rollback()
318
    {
319 3
        $this->pdo->rollBack();
320 3
        self::$transactionCount = 0;
321 3
    }
322
323
    /**
324
     * Return the underlying PDO object.
325
     * @return \PDO
326
     * @throws ConnectionException
327
     */
328 3
    public function getPDO()
329
    {
330 3
        if ($this->pdo === null) {
331
            throw new ConnectionException("A connection has not been established. Please call the connect() method.");
332
        }
333 3
        return $this->pdo;
334
    }
335
336
    /**
337
     * Returns an instance of a descriptor for a given driver.
338
     * @return \ntentan\atiaa\Descriptor
339
     */
340 13
    private function getDescriptor()
341
    {
342 13
        if (!is_object($this->descriptor)) {
343 13
            $descriptorClass = "\\ntentan\\atiaa\\descriptors\\" . ucfirst($this->config['driver']) . "Descriptor";
344 13
            $this->descriptor = new $descriptorClass($this);
345
        }
346 13
        return $this->descriptor;
347
    }
348
349
    /**
350
     * A wrapper around PDO's lastInsertId() method.
351
     * @return mixed
352
     */
353
    public function getLastInsertId()
354
    {
355
        return $this->pdo->lastInsertId();
356
    }
357
358
    /**
359
     * Specify the default schema to use in cases where a schema is not provided
360
     * as part of the table reference.
361
     * @param string $defaultSchema
362
     */
363
    public function setDefaultSchema($defaultSchema)
364
    {
365
        $this->defaultSchema = $defaultSchema;
366
    }
367
368
    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...
369
370
    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...
371
372 3
    public function setCleanDefaults($cleanDefaults)
373
    {
374 3
        $this->getDescriptor()->setCleanDefaults($cleanDefaults);
375 3
    }
376
377
}
378