Completed
Push — master ( 979f96...b68297 )
by James Ekow Abaka
05:14
created

Driver::getConnection()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 14
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5.2742

Importance

Changes 6
Bugs 0 Features 1
Metric Value
c 6
b 0
f 1
dl 0
loc 14
ccs 7
cts 9
cp 0.7778
rs 8.8571
cc 5
eloc 10
nc 7
nop 1
crap 5.2742
1
<?php
2
3
namespace ntentan\atiaa;
4
5
use ntentan\config\Config;
6
use ntentan\atiaa\exceptions\DatabaseDriverException;
7
8
/**
9
 * A driver class for connecting to a specific database platform.
10
 * The Driver class is the main wrapper for atiaa. The driver class contains
11
 * an instance of PDO with which it performs its operations. Aside from wrapping
12
 * around PDO it also provides methods which makes it possible to quote strings
13
 * and identifiers in a platform independent fashion. The driver class is
14
 * responsible for loading the descriptors which are used for describing the
15
 * database schemas.
16
 */
17
abstract class Driver
18
{
19
20
    /**
21
     * The internal PDO connection that is wrapped by this driver.
22
     * @var \PDO
23
     */
24
    private $pdo;
25
26
    /**
27
     * The default schema used in the connection.
28
     * @var string
29
     */
30
    protected $defaultSchema;
31
32
    /**
33
     * The connection parameters with which this connection was established.
34
     * @var array
35
     */
36
    protected $config;
37
38
    /**
39
     * An instance of the descriptor used internally.
40
     * @var \ntentan\atiaa\Descriptor
41
     */
42
    private $descriptor;
43
    
44
    private static $transactionCount = 0;
45
46
    /**
47
     * Creates a new instance of the Atiaa driver. This class is usually initiated
48
     * through the \ntentan\atiaa\Atiaa::getConnection() method. For example
49
     * to create a new instance of a connection to a mysql database.
50
     * 
51
     * ````php
52
     * use ntentan\atiaa\Driver;
53
     * 
54
     * \\ This automatically insitatiates the driver class
55
     * $driver = Driver::getConnection(
56
     *     array(
57
     *         'driver' => 'mysql',
58
     *         'user' => 'root',
59
     *         'password' => 'rootpassy',
60
     *         'host' => 'localhost',
61
     *         'dbname' => 'somedb'
62
     *     )
63
     * );
64
     * 
65
     * var_dump($driver->query("SELECT * FROM some_table");
66
     * var_dump($driver->describe());
67
     * ````
68 33
     * 
69
     * @param array<string> $config The configuration with which to connect to the database.
70 33
     */
71 33
    public function __construct($config = null)
72 33
    {
73
        $this->config = $config ? $config : Config::get('ntentan:db');
74 33
        $username = isset($this->config['user']) ? $this->config['user'] : null;
75
        $password = isset($this->config['password']) ? $this->config['password'] : null;
76 33
77 33
        unset($config['driver']);
78 33
79 31
        try{
80 31
            $this->pdo = new \PDO(
81 31
                $this->getDriverName() . ":" . $this->expand($this->config), $username, $password
82 31
            );
83
            $this->pdo->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
84 18
            $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
85
            $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
86 18
        }
87 18
        catch (\PDOException $e)
88
        {
89
            throw new DatabaseDriverException("PDO failed to connect: {$e->getMessage()}", $e);
90
        }
91
    }
92 28
93
    public function __destruct()
94 28
    {
95 28
        $this->disconnect();
96 28
    }
97
98
    /**
99
     * Close a connection to the database server.
100
     */
101
    public function disconnect()
102 16
    {
103
        $this->pdo = null;
104 16
        $this->pdo = new NullConnection();
0 ignored issues
show
Documentation Bug introduced by
It seems like new \ntentan\atiaa\NullConnection() of type object<ntentan\atiaa\NullConnection> is incompatible with the declared type object<PDO> of property $pdo.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
105
    }
106
107
    /**
108
     * Get the default schema of the current connection.
109
     * @return string
110
     */
111
    public function getDefaultSchema()
112 3
    {
113
        return $this->defaultSchema;
114 3
    }
115
116
    /**
117
     * Use the PDO driver to quote a string.
118
     * @param type $string
119
     * @return string
120
     */
121
    public function quote($string)
122 25
    {
123
        return $this->pdo->quote($string);
124
    }
125 25
126 25
    /**
127
     * 
128
     * @param boolean $status
0 ignored issues
show
Bug introduced by
There is no parameter named $status. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
129
     * @param \PDOStatement  $result 
0 ignored issues
show
Bug introduced by
There is no parameter named $result. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
130
     */
131
    private function fetchRows($statement)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
132
    {
133
        try {
134
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
135
            return $rows;
136
        } catch (\PDOException $e) {
137
            // Skip any exceptions from fetching rows
138
        }
139
    }
140
141
    /**
142
     * Pepare and execute a query, while binding data at the same time. Prevents
143
     * the writing of repetitive prepare and execute statements. This method
144 29
     * returns an array which contains the results of the query that was
145
     * executed. For queries which do not return any results a null is returned.
146
     * 
147 29
     * @todo Add a parameter to cache prepared statements so they can be reused easily.
148 22
     * 
149 22
     * @param string $query The query to be executed quoted in PDO style
150 22
     * @param false|array<mixed> $bindData The data to be bound to the query object.
151 21
     * @return array<mixed>
152
     */
153 29
    public function query($query, $bindData = false)
154 3
    {
155 3
        try {
156
            if (is_array($bindData)) {
157 25
                $statement = $this->pdo->prepare($query);
158 25
                $statement->execute($bindData);
159 25
            } else {
160
                $statement = $this->pdo->query($query);
161
            }
162
        } catch (\PDOException $e) {
163
            $boundData = json_encode($bindData);
164
            throw new DatabaseDriverException("{$e->getMessage()} [$query] [BOUND DATA:$boundData]");
165
        }
166
        $rows = $this->fetchRows($statement);
167
        $statement->closeCursor();
168
        return $rows;
169
    }
170 16
171
    /**
172 16
     * Runs a query but ensures that all identifiers are properly quoted by calling
173
     * the Driver::quoteQueryIdentifiers method on the query before executing it.
174
     * 
175
     * @param string $query
176
     * @param false|array<mixed> $bindData
177
     * @return array<mixed>
178
     */
179
    public function quotedQuery($query, $bindData = false)
180
    {
181
        return $this->query($this->quoteQueryIdentifiers($query), $bindData);
182 33
    }
183
184 33
    /**
185 33
     * Expands the configuration array into a format that can easily be passed
186 10
     * to PDO.
187
     * 
188 23
     * @param array $params The query parameters
189
     * @return string
190 23
     */
191 23
    private function expand($params)
192 23
    {
193 23
        if (isset($params['file'])) {
194
            if($params['file'] != '') {
195 23
                return $params['file'];
196
            }
197 23
        } 
198 23
        
199
        $equated = array();
200
        foreach ($params as $key => $value) {
201
            if ($value == '') {
202
                continue;
203
            } else {
204
                $equated[] = "$key=$value";
205
            }
206
        }
207
        return implode(';', $equated);
208
    }
209
210
    /**
211 19
     * This method provides a system independent way of quoting identifiers in
212
     * queries. By default all identifiers can be quoted with double quotes (").
213 19
     * When a query quoted with double quotes is passed through this method the
214 19
     * output generated has the double quotes replaced with the quoting character
215 19
     * of the target database platform.
216 19
     * 
217 19
     * @param string $query
218 19
     * @return string
219
     */
220
    public function quoteQueryIdentifiers($query)
221
    {
222
        return preg_replace_callback(
223
            '/\"([a-zA-Z\_ ]*)\"/', 
224
            function($matches) {
225
                return $this->quoteIdentifier($matches[1]);
226
            }, $query
227
        );
228 6
    }
229
230 6
    /**
231
     * Returns an array description of the schema represented by the connection.
232
     * The description returns contains information about `tables`, `columns`, `keys`,
233
     * `constraints`, `views` and `indices`.
234
     * 
235
     * @return array<mixed>
236
     */
237
    public function describe()
238
    {
239 7
        return $this->getDescriptor()->describe();
240
    }
241 7
242 7
    /**
243 1
     * Returns the description of a database table as an associative array.
244 1
     * 
245 1
     * @param type $table
246 6
     * @return array<mixed>
247 6
     */
248
    public function describeTable($table)
249 7
    {
250
        $table = explode('.', $table);
251
        if (count($table) > 1) {
252
            $schema = $table[0];
253
            $table = $table[1];
254
        } else {
255
            $schema = $this->getDefaultSchema();
256 6
            $table = $table[0];
257
        }
258 6
        return $this->getDescriptor()->describeTables($schema, array($table), true);
259 6
    }
260 6
261 6
    /**
262
     * A wrapper arround PDO's beginTransaction method which uses a static reference
263
     * counter to implement nested transactions.
264
     */
265
    public function beginTransaction()
266
    {
267 3
        if(self::$transactionCount++ === 0) {
268
            $this->pdo->beginTransaction();
269 3
        }
270 3
    }
271 3
272 3
    /**
273
     * A wrapper around PDO's commit transaction method which uses a static reference
274
     * counter to implement nested transactions.
275
     */
276
    public function commit()
277
    {
278
        if(--self::$transactionCount === 0) {
279 3
            $this->pdo->commit();
280
        }
281 3
    }
282 3
283 3
    /**
284
     * A wrapper around PDO's rollback transaction methd which rolls back all
285
     * activities performed since the first call to begin transaction. 
286
     * Unfortunately, transactions cannot be rolled back in a nested fashion.
287
     */
288
    public function rollback()
289 3
    {
290
        $this->pdo->rollBack();
291 3
        self::$transactionCount = 0;
292
    }
293
294
    /**
295
     * Return the underlying PDO object.
296
     * @return \PDO
297
     */
298 13
    public function getPDO()
299
    {
300 13
        return $this->pdo;
301 13
    }
302 13
303 13
    /**
304 13
     * Returns an instance of a descriptor for a given driver.
305
     * @return \atiaa\Descriptor
306
     */
307
    private function getDescriptor()
308
    {
309
        if (!is_object($this->descriptor)) {
310
            $descriptorClass = "\\ntentan\\atiaa\\descriptors\\" . ucfirst($this->config['driver']) . "Descriptor";
311
            $this->descriptor = new $descriptorClass($this);
312
        }
313
        return $this->descriptor;
314
    }
315
316
    /**
317
     * A wrapper around PDO's lastInsertId() method.
318
     * @return mixed
319
     */
320
    public function getLastInsertId()
321
    {
322
        return $this->pdo->lastInsertId();
323
    }
324
325
    /**
326
     * Specify the default schema to use in cases where a schema is not provided
327
     * as part of the table reference.
328
     * @param string $defaultSchema
329
     */
330
    public function setDefaultSchema($defaultSchema)
331
    {
332
        $this->defaultSchema = $defaultSchema;
333
    }
334
335
    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...
336
337
    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...
338
339
    /**
340
     * Returns a new instance of a driver based on the connection parameters 
341
     * passed to the method. The connection parameters are passed through an
342
     * associative array with the following keys.
343
     * 
344
     * driver 
345
     * : The name of the driver to use for the database connection. Supported
346
     *   drivers are `mysql` and `postgresql`. This parameter is required for
347
     *   all connections.
348
     * 
349
     * user
350
     * : The username to use for the database connection on platforms that 
351
     *   support it.
352
     * 
353
     * password
354
     * : The password associated to the user specified in the connection.
355
     * 
356
     * host
357 33
     * : The host name of the database server.
358
     * 
359 33
     * dbname
360
     * : The name of the default database to use after the connection to the 
361 33
     *   database is established.
362
     * 
363
     * @param array $config 
364
     * @return \ntentan\atiaa\Driver
365 33
     */
366 33
    /*public static function getConnection($config)
0 ignored issues
show
Unused Code Comprehensibility introduced by
57% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
367 2
    {
368 2
        if (is_string($config) && file_exists($config)) {
369
            require $config;
370
        } else if ($config['driver'] == '') {
371
            throw new DatabaseDriverException("Please specify a name for your database driver.");
372 3
        }
373
        try {
374 3
            $class = "\\ntentan\\atiaa\\drivers\\" . ucfirst($config['driver']) . "Driver";
375 3
            return new $class($config);
376
        } catch (\PDOException $e) {
377
            throw new DatabaseDriverException("PDO failed to connect: {$e->getMessage()}", $e);
378
        }
379
    }*/
380
    
381
    public function setCleanDefaults($cleanDefaults)
382
    {
383
        $this->getDescriptor()->setCleanDefaults($cleanDefaults);
384
    }
385
}
386