Completed
Push — master ( 6f557d...1a4e66 )
by James Ekow Abaka
02:04
created

Driver::quote()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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