Completed
Push — develop ( d28428...d28428 )
by Simon
02:20
created

BaseConnection   C

Complexity

Total Complexity 67

Size/Duplication

Total Lines 437
Duplicated Lines 18.99 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 8
Bugs 1 Features 2
Metric Value
wmc 67
c 8
b 1
f 2
lcom 1
cbo 13
dl 83
loc 437
rs 5.2808

31 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 3
B connect() 0 30 3
A disconnect() 0 4 1
A isConnected() 0 3 1
A select() 0 3 1
A insert() 0 3 1
A update() 0 3 1
A replace() 0 3 1
A delete() 0 3 1
A prepare() 0 18 3
B query() 0 36 5
A execute() 0 4 1
A getAll() 14 14 2
A getAssoc() 0 16 3
A getAssocMulti() 0 20 4
A getRow() 14 14 2
A getCol() 15 15 2
A getOne() 14 14 2
A begin() 0 12 2
A commit() 13 13 3
A rollback() 13 13 3
A inTransaction() 0 3 2
A insertId() 0 5 2
A quote() 0 4 1
A quoteIdentifier() 0 17 3
A rawExec() 0 12 2
A dump() 0 4 2
A bindParams() 0 17 4
A getResult() 0 13 1
A getOption() 0 3 2
A setCharacterSet() 0 15 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like BaseConnection often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseConnection, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of Yolk - Gamer Network's PHP Framework.
4
 *
5
 * Copyright (c) 2014 Gamer Network Ltd.
6
 * 
7
 * Distributed under the MIT License, a copy of which is available in the
8
 * LICENSE file that was bundled with this package, or online at:
9
 * https://github.com/gamernetwork/yolk-database
10
 */
11
12
namespace yolk\database;
13
14
use yolk\contracts\database\DatabaseConnection;
15
use yolk\contracts\profiler\ProfilerAwareTrait;
16
use yolk\contracts\profiler\ProfilerAware;
17
use yolk\contracts\support\Dumpable;
18
19
use yolk\database\exceptions\DatabaseException;
20
use yolk\database\exceptions\ConnectionException;
21
use yolk\database\exceptions\NotConnectedException;
22
use yolk\database\exceptions\QueryException;
23
use yolk\database\exceptions\TransactionException;
24
25
/**
26
 * A wrapper for PDO that provides some handy extra functions and streamlines everything else.
27
 */
28
abstract class BaseConnection implements DatabaseConnection, ProfilerAware, Dumpable {
29
30
	use ProfilerAwareTrait;
31
32
	/**
33
	 * Connection details.
34
	 * @var DSN
35
	 */
36
	protected $dsn = null;
37
38
	/**
39
	 * Underlying PDO object.
40
	 * @var \PDO
41
	 */
42
	protected $pdo = null;
43
44
	/**
45
	 * Prepared statement cache.
46
	 * @var array
47
	 */
48
	protected $statements = [];
49
50
	/**
51
	 * Create a new database connection.
52
	 *
53
	 * @param DSN $dsn a DSN instance describing the database connection details
54
	 */
55
	public function __construct( DSN $dsn ) {
56
57
		$this->dsn = $dsn;
58
59
		// check for PDO extension
60
		if( !extension_loaded('pdo') )
61
			throw new DatabaseException('The PDO extension is required but the extension is not loaded');
62
63
		// check the PDO driver is available
64
		elseif( !in_array($this->dsn->type, \PDO::getAvailableDrivers()) )
65
			throw new DatabaseException("The {$this->dsn->type} PDO driver is not currently installed");
66
67
	}
68
69
	public function connect() {
70
71
		if( $this->pdo instanceof \PDO )
72
			return true;
73
74
		try {
75
76
			$this->pdo = new \PDO(
77
				$this->dsn->getConnectionString(),
78
				$this->dsn->user,
79
				$this->dsn->pass,
80
				$this->dsn->options
81
			);
82
83
			$this->pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC);
84
			$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);           // always use exceptions
85
86
			$this->setCharacterSet(
87
				$this->getOption('charset', 'UTF8'),
88
				$this->getOption('collation')
89
			);
90
91
		}
92
		catch( \PDOException $e ) {
93
			throw new ConnectionException($e->getMessage(), $e->getCode(), $e);
94
		}
95
96
		return true;
97
98
	}
99
100
	public function disconnect() {
101
		$this->pdo = null;
102
		return true;
103
	}
104
105
	public function isConnected() {
106
		return $this->pdo instanceof \PDO;
107
	}
108
109
	public function select() {
110
		return new query\Select($this);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \yolk\database\query\Select($this); (yolk\database\query\Select) is incompatible with the return type declared by the interface yolk\contracts\database\DatabaseConnection::select of type yolk\contracts\database\...s\database\query\Select.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
111
	}
112
113
	public function insert() {
114
		return new query\Insert($this);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \yolk\database\query\Insert($this); (yolk\database\query\Insert) is incompatible with the return type declared by the interface yolk\contracts\database\DatabaseConnection::insert of type yolk\contracts\database\...s\database\query\Insert.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
115
	}
116
117
	public function update() {
118
		return new query\Update($this);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \yolk\database\query\Update($this); (yolk\database\query\Update) is incompatible with the return type declared by the interface yolk\contracts\database\DatabaseConnection::update of type yolk\contracts\database\...s\database\query\Update.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
119
	}
120
121
	public function replace() {
122
		return new query\Replace($this);
123
	}
124
125
	public function delete() {
126
		return new query\Delete($this);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return new \yolk\database\query\Delete($this); (yolk\database\query\Delete) is incompatible with the return type declared by the interface yolk\contracts\database\DatabaseConnection::delete of type yolk\contracts\database\...s\database\query\Delete.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
127
	}
128
129
	public function prepare( $statement ) {
130
131
		if( ! $statement instanceof \PDOStatement  ) {
132
133
			$this->connect();
134
135
			$statement = trim($statement);
136
137
			if( !isset($this->statements[$statement]) )
138
				$this->statements[$statement] = $this->pdo->prepare($statement);
139
140
			$statement = $this->statements[$statement];
141
142
		}
143
144
		return $statement;
145
146
	}
147
148
	public function query( $statement, $params = [] ) {
149
150
		$this->connect();
151
152
		// TODO: profiler start
153
		$this->profiler && $this->profiler->start('Query');
154
155
		try {
156
157
			$statement = $this->prepare($statement);
158
159
			// single parameters don't have to be passed in an array - do that here
160
			if( !is_array($params) )
161
				$params = array($params);
162
163
			$this->bindParams($statement, $params);
164
165
			$start = microtime(true);
166
			$statement->execute();
167
			$duration = microtime(true) - $start;
168
			
169
		}
170
		catch( \PDOException $e ) {
171
			throw new QueryException($e->getMessage(), $e->getCode(), $e);
172
		}
173
174
		// TODO: profiler stop + record
175
		if( $this->profiler ) {
176
			$this->profiler->stop('Query');
177
			// remove all whitespace at start of lines
178
			$this->profiler->query(preg_replace("/^\s*/m", "", trim($statement->queryString)), $params, $duration);
179
		}
180
181
		return $statement;
182
183
	}
184
185
	public function execute( $statement, $params = [] ) {
186
		$statement = $this->query($statement, $params);
187
		return $statement->rowCount();
188
	}
189
190 View Code Duplication
	public function getAll( $statement, $params = [], $expires = 0, $key = '' ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
191
		return $this->getResult(
192
			$statement,
193
			$params,
194
			$expires,
195
			$key,
196
			function( \PDOStatement $statement ) {
197
				$result = $statement->fetchAll();
198
				if( $result === false )
199
					$result = [];
200
				return $result;
201
			}
202
		);
203
	}
204
205
	public function getAssoc( $statement, $params = [], $expires = 0, $key = '' ) {
206
		return $this->getResult(
207
			$statement,
208
			$params,
209
			$expires,
210
			$key,
211
			function( \PDOStatement $statement ) {
212
				$result = [];
213
				while( $row = $statement->fetch() ) {
214
					$key = array_shift($row);
215
					$result[$key] = count($row) == 1 ? array_shift($row) : $row;
216
				}
217
				return $result;
218
			}
219
		);
220
	}
221
222
	public function getAssocMulti( $statement, $params = [], $expires = 0, $key = '' ) {
223
		return $this->getResult(
224
			$statement,
225
			$params,
226
			$expires,
227
			$key,
228
			function( \PDOStatement $statement ) {
229
				$result = [];
230
				while( $row = $statement->fetch() ) {
231
					$k1 = array_shift($row);
232
					$k2 = array_shift($row);
233
					$v  = count($row) == 1 ? array_shift($row) : $row;
234
					if( !isset($result[$k1]) )
235
						$result[$k1] = [];
236
					$result[$k1][$k2] = $v;
237
				}
238
				return $result;
239
			}
240
		);
241
	}
242
243 View Code Duplication
	public function getRow( $statement, $params = [], $expires = 0, $key = '' ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
244
		return $this->getResult(
245
			$statement,
246
			$params,
247
			$expires,
248
			$key,
249
			function( \PDOStatement $statement ) {
250
				$result = $statement->fetch();
251
				if( $result === false )
252
					$result = [];
253
				return $result;
254
			}
255
		);
256
	}
257
258 View Code Duplication
	public function getCol( $statement, $params = [], $expires = 0, $key = '' ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
259
		return $this->getResult(
260
			$statement,
261
			$params,
262
			$expires,
263
			$key,
264
			function( \PDOStatement $statement ) {
265
				$result = [];
266
				while( $row = $statement->fetch() ) {
267
					$result[] = array_shift($row);
268
				}
269
				return $result;
270
			}
271
		);
272
	}
273
274 View Code Duplication
	public function getOne( $statement, $params = [], $expires = 0, $key = '' ) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
275
		return $this->getResult(
276
			$statement,
277
			$params,
278
			$expires,
279
			$key,
280
			function( \PDOStatement $statement ) {
281
				$result = $statement->fetchColumn();
282
				if( $result === false )
283
					$result = null;
284
				return $result;
285
			}
286
		);
287
	}
288
289
	public function begin() {
290
291
		$this->connect();
292
293
		try {
294
			return $this->pdo->beginTransaction();
295
		}
296
		catch( \PDOException $e ) {
297
			throw new TransactionException($e->getMessage(), $e->getCode(), $e);
298
		}
299
300
	}
301
302 View Code Duplication
	public function commit() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
303
304
		if( !$this->isConnected() )
305
			throw new NotConnectedException();
306
307
		try {
308
			return $this->pdo->commit();
309
		}
310
		catch( \PDOException $e ) {
311
			throw new TransactionException($e->getMessage(), $e->getCode(), $e);
312
		}
313
314
	}
315
316 View Code Duplication
	public function rollback() {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
317
318
		if( !$this->isConnected() )
319
			throw new NotConnectedException();
320
321
		try {
322
			return $this->pdo->rollBack();
323
		}
324
		catch( \PDOException $e ) {
325
			throw new TransactionException($e->getMessage(), $e->getCode(), $e);
326
		}
327
328
	}
329
330
	public function inTransaction() {
331
		return $this->isConnected() ? $this->pdo->inTransaction() : false;
332
	}
333
334
	public function insertId( $name = '' ) {
335
		if( !$this->isConnected() )
336
			throw new NotConnectedException();
337
		return $this->pdo->lastInsertId($name);
338
	}
339
340
	public function quote( $value, $type = \PDO::PARAM_STR ) {
341
		$this->connect();
342
		return $this->pdo->quote($value, $type);
343
	}
344
345
	public function quoteIdentifier( $name ) {
346
347
		$name = trim($name);
348
349
        if( $name == '*' )
350
            return $name;
351
352
		// ANSI-SQL (everything else) says to use double quotes to quote identifiers
353
        $char = '"';
354
355
		// MySQL uses backticks cos it's special
356
		if( $this->dsn->isMySQL() )
357
        	$char = '`';
358
359
		return $char. $name. $char;
360
361
	}
362
363
	/**
364
	 * Execute a raw SQL string and return the number of affected rows.
365
	 * Primarily used for DDL queries. Do not use this with:
366
	 * - Anything (data/parameters/etc) that comes from userland
367
	 * - Select queries - the answer will always be 0 as no rows are affected.
368
	 * - Everyday queries - use query() or execute()
369
	 * @param  string $sql  the SQL statement to exexcute
370
	 * @return integer   the number of rows affected by the statement
371
	 */
372
	public function rawExec( $sql ) {
373
374
		$this->connect();
375
376
		try {
377
			return $this->pdo->exec($sql);
378
		}
379
		catch( \PDOException $e ) {
380
			throw new QueryException($e->getMessage(), $e->getCode(), $e);
381
		}
382
383
	}
384
385
	public function dump( $dumper = '\\yolk\\debug\\TextDumper', $depth = 1 ) {
386
		if( $depth > 1 )
387
			return $this->dsn->toString();
388
	}
389
390
	/**
391
	 * Bind named and positional parameters to a PDOStatement.
392
	 * @param  PDOStatement $statement
393
	 * @param  array        $params
394
	 * @return void
395
	 */
396
	protected function bindParams( \PDOStatement $statement, array $params ) {
397
		foreach( $params as $name => $value ) {
398
399
			if( is_int($value) ) {
400
				$type = \PDO::PARAM_INT;
401
			}
402
			else {
403
				$type = \PDO::PARAM_STR;
404
			}
405
406
			// handle positional (?) and named (:name) parameters
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% 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...
407
			$name = is_numeric($name) ? (int) $name + 1 : ":{$name}";
408
409
			$statement->bindValue($name, $value, $type);
410
411
		}
412
	}
413
414
	/**
415
	 * Perform a select query and use a callback to extract a result.
416
	 * @param  \PDOStatement|string $statement   an existing PDOStatement object or a SQL string.
417
	 * @param  array $params        an array of parameters to pass into the query.
418
	 * @param  integer $expires     number of seconds to cache the result for if caching is enabled
419
	 * @param  string  $key         cache key used to store the result
420
	 * @param  \Closure $callback   function to yield a result from the executed statement
421
	 * @return array
422
	 */
423
	protected function getResult( $statement, $params, $expires, $key, \Closure $callback ) {
0 ignored issues
show
Unused Code introduced by
The parameter $expires is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $key is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
424
425
		// TODO: check cache
426
427
		$statement = $this->query($statement, $params);
428
429
		$result = $callback($statement);
430
431
		// TODO: store in cache
432
433
		return $result;
434
435
	}
436
437
	protected function getOption( $option, $default = null ) {
438
		return isset($this->dsn->options[$option]) ? $this->dsn->options[$option] : $default;
439
	}
440
441
	/**
442
	 * Make sure the connection is using the correct character set
443
	 * 
444
	 * @param string $charset   the character set to use for the connection
445
	 * @param string $collation the collation method to use for the connection
446
	 * @return self
447
	 */
448
	protected function setCharacterSet( $charset, $collation = '' ) {
449
450
		if( !$charset ) 
451
			throw new DatabaseException('No character set specified');
452
453
		$sql = 'SET NAMES '. $this->pdo->quote($charset);
454
455
		if( $collation )
456
			$sql .= ' COLLATE '. $this->pdo->quote($collation);
457
458
		$this->pdo->exec($sql);
459
460
		return $this;
461
462
	}
463
464
}
465
466
// EOF