Completed
Push — master ( 6460ce...1729d3 )
by James Ekow Abaka
05:20
created

Driver::__construct()   A

Complexity

Conditions 4
Paths 20

Size

Total Lines 16
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4

Importance

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