Completed
Push — master ( 5df59f...c0d6d9 )
by James Ekow Abaka
02:05
created

Driver::quoteQueryIdentifiers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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