Completed
Push — master ( f42128...f70f0b )
by James Ekow Abaka
01:39
created

Driver::connect()   A

Complexity

Conditions 4
Paths 20

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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