Completed
Push — master ( 1d3657...1c70eb )
by James Ekow Abaka
04:29
created

Driver::prepareQuery()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 18
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5.583

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 10
cts 14
cp 0.7143
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 15
nc 4
nop 2
crap 5.583
1
<?php
2
3
namespace ntentan\atiaa;
4
5
use ntentan\atiaa\exceptions\DatabaseDriverException;
6
use ntentan\panie\Container;
7
use ntentan\panie\exceptions\ResolutionException;
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
     * The internal PDO connection that is wrapped by this driver.
22
     * @var \PDO
23
     */
24
    private $pdo;
25
    
26
    private $logger;
27
28
    /**
29
     * The default schema used in the connection.
30
     * @var string
31
     */
32
    protected $defaultSchema;
33
34
    /**
35
     * The connection parameters with which this connection was established.
36
     * @var array
37
     */
38
    protected $config;
39
40
    /**
41
     * An instance of the descriptor used internally.
42
     * @var \ntentan\atiaa\Descriptor
43
     */
44
    private $descriptor;
45
    private static $transactionCount = 0;
46
47
    /**
48
     * Creates a new instance of the Atiaa driver. This class is usually initiated
49
     * through the \ntentan\atiaa\Atiaa::getConnection() method. For example
50
     * to create a new instance of a connection to a mysql database.
51
     * 
52
     * ````php
53
     * use ntentan\atiaa\Driver;
54
     * 
55
     * \\ This automatically insitatiates the driver class
56
     * $driver = Driver::getConnection(
57
     *     array(
58
     *         'driver' => 'mysql',
59
     *         'user' => 'root',
60
     *         'password' => 'rootpassy',
61
     *         'host' => 'localhost',
62
     *         'dbname' => 'somedb'
63
     *     )
64
     * );
65
     * 
66
     * var_dump($driver->query("SELECT * FROM some_table");
67
     * var_dump($driver->describe());
68
     * ````
69
     * 
70
     * @param array<string> $config The configuration with which to connect to the database.
71
     */
72 23
    public function __construct(Container $container, $config = null) {
73 23
        $this->config = $config;
0 ignored issues
show
Documentation Bug introduced by
It seems like $config can be null. However, the property $config is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

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

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
74 23
        $username = isset($this->config['user']) ? $this->config['user'] : null;
75 23
        $password = isset($this->config['password']) ? $this->config['password'] : null;
76
        
77
        try{
78 23
            $this->logger = $container->resolve(QueryLogger::class);
79 23
        } catch (ResolutionException $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
80
            
81
        }
82
83
        try {
84 23
            $this->pdo = new \PDO(
85 23
                $this->getDriverName() . ":" . $this->expand($this->config), $username, $password
86
            );
87 21
            $this->pdo->setAttribute(\PDO::ATTR_STRINGIFY_FETCHES, false);
88 21
            $this->pdo->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);
89 21
            $this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
90 2
        } catch (\PDOException $e) {
91 2
            throw new DatabaseDriverException("PDO failed to connect: {$e->getMessage()}", $e);
92
        }
93 21
    }
94
95 24
    public function __destruct() {
96 24
        $this->disconnect();
97 24
    }
98
99
    /**
100
     * Close a connection to the database server.
101
     */
102 24
    public function disconnect() {
103 24
        $this->pdo = null;
104 24
        $this->pdo = new NullConnection();
105 24
    }
106
107
    /**
108
     * Get the default schema of the current connection.
109
     * @return string
110
     */
111 6
    public function getDefaultSchema() {
112 6
        return $this->defaultSchema;
113
    }
114
115
    /**
116
     * Use the PDO driver to quote a string.
117
     * @param type $string
118
     * @return string
119
     */
120 2
    public function quote($string) {
121 2
        return $this->pdo->quote($string);
122
    }
123
124
    /**
125
     * 
126
     * @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...
127
     * @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...
128
     */
129 15
    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...
130
        try {
131 15
            $rows = $statement->fetchAll(\PDO::FETCH_ASSOC);
132 15
            return $rows;
133
        } catch (\PDOException $e) {
134
            // Skip any exceptions from fetching rows
135
        }
136
    }
137
    
138 19
    private function prepareQuery($query, $bindData) {
139 19
        $statement = $this->pdo->prepare($query);
140 16
        foreach($bindData as $key => $value) {
141 15
            switch(gettype($value)) {
142 15
                case "integer": 
143
                    $type = \PDO::PARAM_INT;
144
                    break;
145 15
                case "boolean": 
146
                    $type = \PDO::PARAM_BOOL;
147
                    break;
148
                default: 
149 15
                    $type = \PDO::PARAM_STR;
150 15
                    break;
151
            }
152 15
            $statement->bindValue(is_numeric($key) ? $key + 1: $key, $value, $type);
153
        }
154 16
        return $statement;
155
    }
156
157
    /**
158
     * Pepare and execute a query, while binding data at the same time. Prevents
159
     * the writing of repetitive prepare and execute statements. This method
160
     * returns an array which contains the results of the query that was
161
     * executed. For queries which do not return any results a null is returned.
162
     * 
163
     * @todo Add a parameter to cache prepared statements so they can be reused easily.
164
     * 
165
     * @param string $query The query to be executed quoted in PDO style
166
     * @param false|array<mixed> $bindData The data to be bound to the query object.
167
     * @return array<mixed>
168
     */
169 19
    public function query($query, $bindData = []) {
170
        try {
171 19
            if (is_array($bindData)) {
172 19
                $statement = $this->prepareQuery($query, $bindData);
173 16
                $statement->execute();
174
            } else {
175 15
                $statement = $this->pdo->query($query);
176
            }
177 4
        } catch (\PDOException $e) {
178 2
            $boundData = json_encode($bindData);
179 2
            throw new DatabaseDriverException("{$e->getMessage()} [$query] [BOUND DATA:$boundData]");
180
        }
181 15
        if($this->logger) {
182
            $this->logger->debug($query, $bindData);
183
        }
184 15
        $rows = $this->fetchRows($statement);
185 15
        $statement->closeCursor();
186 15
        return $rows;
187
    }
188
189
    /**
190
     * Runs a query but ensures that all identifiers are properly quoted by calling
191
     * the Driver::quoteQueryIdentifiers method on the query before executing it.
192
     * 
193
     * @param string $query
194
     * @param false|array<mixed> $bindData
195
     * @return array<mixed>
196
     */
197 11
    public function quotedQuery($query, $bindData = false) {
198 11
        return $this->query($this->quoteQueryIdentifiers($query), $bindData);
199
    }
200
201
    /**
202
     * Expands the configuration array into a format that can easily be passed
203
     * to PDO.
204
     * 
205
     * @param array $params The query parameters
206
     * @return string
207
     */
208 23
    private function expand($params) {
209 23
        unset($params['driver']);
210 23
        if (isset($params['file'])) {
211 23
            if ($params['file'] != '') {
212
                return $params['file'];
213
            }
214
        }
215
216 23
        $equated = array();
217 23
        foreach ($params as $key => $value) {
218 23
            if ($value == '') {
219 23
                continue;
220
            } else {
221 23
                $equated[] = "$key=$value";
222
            }
223
        }
224 23
        return implode(';', $equated);
225
    }
226
227
    /**
228
     * This method provides a system independent way of quoting identifiers in
229
     * queries. By default all identifiers can be quoted with double quotes (").
230
     * When a query quoted with double quotes is passed through this method the
231
     * output generated has the double quotes replaced with the quoting character
232
     * of the target database platform.
233
     * 
234
     * @param string $query
235
     * @return string
236
     */
237 13
    public function quoteQueryIdentifiers($query) {
238 13
        return preg_replace_callback(
239 13
                '/\"([a-zA-Z\_ ]*)\"/', function($matches) {
240 13
            return $this->quoteIdentifier($matches[1]);
241 13
        }, $query
242
        );
243
    }
244
245
    /**
246
     * Returns an array description of the schema represented by the connection.
247
     * The description returns contains information about `tables`, `columns`, `keys`,
248
     * `constraints`, `views` and `indices`.
249
     * 
250
     * @return array<mixed>
251
     */
252 4
    public function describe() {
253 4
        return $this->getDescriptor()->describe();
254
    }
255
256
    /**
257
     * Returns the description of a database table as an associative array.
258
     * 
259
     * @param string $table
260
     * @return array<mixed>
261
     */
262 5
    public function describeTable($table) {
263 5
        $table = explode('.', $table);
264 5
        if (count($table) > 1) {
265 1
            $schema = $table[0];
266 1
            $table = $table[1];
267
        } else {
268 4
            $schema = $this->getDefaultSchema();
269 4
            $table = $table[0];
270
        }
271 5
        return $this->getDescriptor()->describeTables($schema, array($table), true);
272
    }
273
274
    /**
275
     * A wrapper arround PDO's beginTransaction method which uses a static reference
276
     * counter to implement nested transactions.
277
     */
278 4
    public function beginTransaction() {
279 4
        if (self::$transactionCount++ === 0) {
280 4
            $this->pdo->beginTransaction();
281
        }
282 4
    }
283
284
    /**
285
     * A wrapper around PDO's commit transaction method which uses a static reference
286
     * counter to implement nested transactions.
287
     */
288 2
    public function commit() {
289 2
        if (--self::$transactionCount === 0) {
290 2
            $this->pdo->commit();
291
        }
292 2
    }
293
294
    /**
295
     * A wrapper around PDO's rollback transaction methd which rolls back all
296
     * activities performed since the first call to begin transaction. 
297
     * Unfortunately, transactions cannot be rolled back in a nested fashion.
298
     */
299 2
    public function rollback() {
300 2
        $this->pdo->rollBack();
301 2
        self::$transactionCount = 0;
302 2
    }
303
304
    /**
305
     * Return the underlying PDO object.
306
     * @return \PDO
307
     */
308 2
    public function getPDO() {
309 2
        return $this->pdo;
310
    }
311
312
    /**
313
     * Returns an instance of a descriptor for a given driver.
314
     * @return \atiaa\Descriptor
315
     */
316 9
    private function getDescriptor() {
317 9
        if (!is_object($this->descriptor)) {
318 9
            $descriptorClass = "\\ntentan\\atiaa\\descriptors\\" . ucfirst($this->config['driver']) . "Descriptor";
319 9
            $this->descriptor = new $descriptorClass($this);
320
        }
321 9
        return $this->descriptor;
322
    }
323
324
    /**
325
     * A wrapper around PDO's lastInsertId() method.
326
     * @return mixed
327
     */
328
    public function getLastInsertId() {
329
        return $this->pdo->lastInsertId();
330
    }
331
332
    /**
333
     * Specify the default schema to use in cases where a schema is not provided
334
     * as part of the table reference.
335
     * @param string $defaultSchema
336
     */
337
    public function setDefaultSchema($defaultSchema) {
338
        $this->defaultSchema = $defaultSchema;
339
    }
340
341
    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...
342
343
    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...
344
345
    /**
346
     * Returns a new instance of a driver based on the connection parameters 
347
     * passed to the method. The connection parameters are passed through an
348
     * associative array with the following keys.
349
     * 
350
     * driver 
351
     * : The name of the driver to use for the database connection. Supported
352
     *   drivers are `mysql` and `postgresql`. This parameter is required for
353
     *   all connections.
354
     * 
355
     * user
356
     * : The username to use for the database connection on platforms that 
357
     *   support it.
358
     * 
359
     * password
360
     * : The password associated to the user specified in the connection.
361
     * 
362
     * host
363
     * : The host name of the database server.
364
     * 
365
     * dbname
366
     * : The name of the default database to use after the connection to the 
367
     *   database is established.
368
     * 
369
     * @param array $config 
370
     * @return \ntentan\atiaa\Driver
371
     */
372
    /* 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...
373
      {
374
      if (is_string($config) && file_exists($config)) {
375
      require $config;
376
      } else if ($config['driver'] == '') {
377
      throw new DatabaseDriverException("Please specify a name for your database driver.");
378
      }
379
      try {
380
      $class = "\\ntentan\\atiaa\\drivers\\" . ucfirst($config['driver']) . "Driver";
381
      return new $class($config);
382
      } catch (\PDOException $e) {
383
      throw new DatabaseDriverException("PDO failed to connect: {$e->getMessage()}", $e);
384
      }
385
      } */
386
387 2
    public function setCleanDefaults($cleanDefaults) {
388 2
        $this->getDescriptor()->setCleanDefaults($cleanDefaults);
389 2
    }
390
391
}
392