Passed
Push — master ( 8a2369...384d23 )
by Tom
02:08
created

Database::__construct()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 10
nc 8
nop 4
dl 0
loc 17
rs 8.8571
c 0
b 0
f 0
1
<?php
2
namespace Maphper\DataSource;
3
class Database implements \Maphper\DataSource {
4
	const EDIT_STRUCTURE = 1;
5
	const EDIT_INDEX = 2;
6
	const EDIT_OPTIMISE = 4;
7
8
	private $table;
9
	private $cache = [];
10
	private $primaryKey;
11
	private $fields = '*';
12
	private $defaultSort;
13
	private $resultCache = [];
14
	private $alterDb = false;
15
	private $adapter;
16
	private $crudBuilder;
17
18
	public function __construct($db, $table, $primaryKey = 'id', array $options = []) {
19
		$this->options = new DatabaseOptions($db, $options);
0 ignored issues
show
Bug Best Practice introduced by
The property options does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
20
		$this->adapter = $this->options->getAdapter();
21
22
		$this->table = $table;
23
		$this->primaryKey = is_array($primaryKey) ? $primaryKey : [$primaryKey];
24
25
		$this->crudBuilder = new \Maphper\Lib\CrudBuilder();
26
		$this->selectBuilder = new \Maphper\Lib\SelectBuilder();
0 ignored issues
show
Bug Best Practice introduced by
The property selectBuilder does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
27
28
		$this->fields = implode(',', array_map([$this->adapter, 'quote'], (array) $this->options->read('fields')));
29
30
		$this->defaultSort = $this->options->read('defaultSort') !== false ? $this->options->read('defaultSort')  : implode(', ', $this->primaryKey);
31
32
		$this->alterDb = $this->options->getEditMode();
33
34
		if (self::EDIT_OPTIMISE & $this->alterDb && rand(0,500) == 1) $this->adapter->optimiseColumns($table);
35
	}
36
37
	public function getPrimaryKey() {
38
		return $this->primaryKey;
39
	}
40
41
	public function deleteById($id) {
42
		$this->adapter->query($this->crudBuilder->delete($this->table, [$this->primaryKey[0] . ' = :id'], [':id' => $id], 1));
43
		unset($this->cache[$id]);
44
	}
45
46
	public function findById($id) {
47
		if (!isset($this->cache[$id])) {
48
			try {
49
				$result = $this->adapter->query($this->selectBuilder->select($this->table, [$this->getPrimaryKey()[0] . ' = :id'], [':id' => $id], ['limit' => 1]));
50
			}
51
			catch (\Exception $e) {
52
				$this->errors[] = $e;
0 ignored issues
show
Bug Best Practice introduced by
The property errors does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
53
			}
54
55
			if (isset($result[0])) 	$this->cache[$id] = $result[0];
56
			else return null;
57
		}
58
		return $this->cache[$id];
59
	}
60
61
	public function findAggregate($function, $field, $group = null, array $criteria = [], array $options = []) {
62
		//Cannot count/sum/max multiple fields, pick the first one. This should only come into play when trying to count() a mapper with multiple primary keys
63
		if (is_array($field)) $field = $field[0];
64
		$query = $this->selectBuilder->createSql($criteria, \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND);
65
66
		try {
67
			$this->addIndex(array_keys($query['args']));
68
			$this->addIndex(explode(',', $group));
69
			$result = $this->adapter->query($this->selectBuilder->aggregate($this->table, $function, $field, $query['sql'], $query['args'], $group));
70
71
			if (isset($result[0]) && $group == null) return $result[0]->val;
72
			else if ($group != null) {
73
				$ret = [];
74
				foreach ($result as $res) $ret[$res->$field] = $res->val;
75
				return $ret;
76
			}
77
			else return 0;
78
		}
79
		catch (\Exception $e) {
80
			$this->errors[] = $e;
0 ignored issues
show
Bug Best Practice introduced by
The property errors does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
81
			return $group ? [] : 0;
82
		}
83
	}
84
85
	private function addIndex($args) {
86
		if (self::EDIT_INDEX & $this->alterDb) $this->adapter->addIndex($this->table, $args);
87
	}
88
89
	public function findByField(array $fields, $options = []) {
90
		$cacheId = md5(serialize(func_get_args()));
91
		if (!isset($this->resultCache[$cacheId])) {
92
			$query = $this->selectBuilder->createSql($fields, \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND);
93
94
			if (!isset($options['order'])) $options['order'] = $this->defaultSort;
95
96
			$query['sql'] = array_filter($query['sql']);
97
98
			try {
99
				$this->resultCache[$cacheId] = $this->adapter->query($this->selectBuilder->select($this->table, $query['sql'], $query['args'], $options));
100
				$this->addIndex(array_keys($query['args']));
101
				$this->addIndex(explode(',', $options['order']));
102
			}
103
			catch (\Exception $e) {
104
				$this->errors[] = $e;
0 ignored issues
show
Bug Best Practice introduced by
The property errors does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
105
				$this->resultCache[$cacheId] = [];
106
			}
107
		}
108
		return $this->resultCache[$cacheId];
109
	}
110
111
	public function deleteByField(array $fields, array $options = [], $mode = null) {
112
		if ($mode == null) $mode = \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND;
113
		if (isset($options['limit']) != null) $limit = ' LIMIT ' . $options['limit'];
114
		else $limit = '';
115
116
		$query = $this->selectBuilder->createSql($fields, $mode);
117
        $query['sql'] = array_filter($query['sql']);
118
		$this->adapter->query($this->crudBuilder->delete($this->table, $query['sql'], $query['args'], $limit));
119
		$this->addIndex(array_keys($query['args']));
120
121
		//Clear the cache
122
		$this->cache = [];
123
		$this->resultCache = [];
124
	}
125
126
	public function save($data, $tryagain = true) {
127
		$tryagain = $tryagain && self::EDIT_STRUCTURE & $this->alterDb;
128
		$new = false;
129
		foreach ($this->primaryKey as $k) {
130
			if (empty($data->$k)) {
131
				$data->$k = null;
132
				$new = true;
133
			}
134
		}
135
136
		try {
137
			$result = $this->insert($this->table, $this->primaryKey, $data);
138
139
			//If there was an error but PDO is silent, trigger the catch block anyway
140
			if ($result->errorCode() !== '00000') throw new \Exception('Could not insert into ' . $this->table);
141
142
			if ($tryagain === false && $result->rowCount() === 0) {
143
144
				$updateWhere = $this->crudBuilder->update($this->table, $this->primaryKey, $data);
145
146
				$matched = $this->findByField($updateWhere->getArgs());
147
148
				if (count($matched) == 0) throw new \InvalidArgumentException('Record inserted into table ' . $this->table . ' fails table constraints');
149
 			}
150
			
151
		}
152
		catch (\Exception $e) {
153
			if ($tryagain) {
154
				$this->adapter->alterDatabase($this->table, $this->primaryKey, $data);
155
				$this->save($data, false);
156
			}
157
			else throw $e;
158
		}
159
		//TODO: This will error if the primary key is a private field
160
		if ($new && count($this->primaryKey) == 1) $data->{$this->primaryKey[0]} = $this->adapter->lastInsertId();
161
		//Something has changed, clear any cached results as they may now be incorrect
162
		$this->resultCache = [];
163
		$pkValue = $data->{$this->primaryKey[0]};
164
		if (isset($this->cache[$pkValue])) $this->cache[$pkValue] = (object) array_merge((array)$this->cache[$pkValue], (array)$data);
165
		else $this->cache[$pkValue] = $data;
166
	}
167
168
	private function insert($table, array $primaryKey, $data) {
169
		$error = 0;
170
		try {
171
			$result = $this->adapter->query($this->crudBuilder->insert($table, $data));
172
		}
173
		catch (\Exception $e) {
174
			$error = 1;
175
		}
176
177
 		if ($error || $result->errorCode() !== '00000') {
178
 			$result = $this->adapter->query($this->crudBuilder->update($table, $primaryKey, $data));
179
 		}
180
181
		return $result;
182
	}
183
}
184