Passed
Push — master ( 36e64a...0adb6c )
by Richard
01:36
created

Database::save()   A

Complexity

Conditions 4
Paths 7

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 11
nc 7
nop 2
dl 0
loc 20
rs 9.2
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 $options;
10
	private $cache = [];
11
	private $primaryKey;
12
	private $fields = '*';
13
	private $defaultSort;
14
	private $resultCache = [];
15
	private $alterDb = false;
16
	private $adapter;
17
	private $crudBuilder;
18
    private $selectBuilder;
19
20
	public function __construct($db, $table, $primaryKey = 'id', array $options = []) {
21
		$this->options = new DatabaseOptions($db, $options);
22
		$this->adapter = $this->options->getAdapter();
23
24
		$this->table = $table;
25
		$this->primaryKey = is_array($primaryKey) ? $primaryKey : [$primaryKey];
26
27
		$this->crudBuilder = new \Maphper\Lib\CrudBuilder();
28
		$this->selectBuilder = new \Maphper\Lib\SelectBuilder();
29
30
		$this->fields = implode(',', array_map([$this->adapter, 'quote'], (array) $this->options->read('fields')));
31
32
		$this->defaultSort = $this->options->read('defaultSort') !== false ? $this->options->read('defaultSort')  : implode(', ', $this->primaryKey);
33
34
		$this->alterDb = $this->options->getEditMode();
35
36
		$this->optimizeColumns();
37
	}
38
39
    private function optimizeColumns() {
40
        if (self::EDIT_OPTIMISE & $this->alterDb && rand(0,500) == 1) $this->adapter->optimiseColumns($this->table);
41
    }
42
43
	public function getPrimaryKey() {
44
		return $this->primaryKey;
45
	}
46
47
	public function deleteById($id) {
48
		$this->adapter->query($this->crudBuilder->delete($this->table, [$this->primaryKey[0] . ' = :id'], [':id' => $id], 1));
49
		unset($this->cache[$id]);
50
	}
51
52
	public function findById($id) {
53
		if (!isset($this->cache[$id])) {
54
			try {
55
				$result = $this->adapter->query($this->selectBuilder->select($this->table, [$this->getPrimaryKey()[0] . ' = :id'], [':id' => $id], ['limit' => 1]));
56
			}
57
			catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
58
			}
59
60
			if (isset($result[0])) 	$this->cache[$id] = $result[0];
61
			else return null;
62
		}
63
		return $this->cache[$id];
64
	}
65
66
	public function findAggregate($function, $field, $group = null, array $criteria = [], array $options = []) {
67
		//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
68
		if (is_array($field)) $field = $field[0];
69
		$query = $this->selectBuilder->createSql($criteria, \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND);
70
71
		try {
72
			$this->addIndex(array_keys($query['args']));
73
			$this->addIndex(explode(',', $group));
74
			$result = $this->adapter->query($this->selectBuilder->aggregate($this->table, $function, $field, $query['sql'], $query['args'], $group));
75
76
			return $this->determineAggregateResult($result, $group);
77
		}
78
		catch (\Exception $e) {
79
			return $group ? [] : 0;
80
		}
81
	}
82
83
    private function determineAggregateResult($result, $group) {
84
        if (isset($result[0]) && $group == null) return $result[0]->val;
85
        else if ($group != null) {
86
            $ret = [];
87
            foreach ($result as $res) $ret[$res->$field] = $res->val;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $field seems to be never defined.
Loading history...
88
            return $ret;
89
        }
90
        else return 0;
91
    }
92
93
	private function addIndex($args) {
94
		if (self::EDIT_INDEX & $this->alterDb) $this->adapter->addIndex($this->table, $args);
95
	}
96
97
	public function findByField(array $fields, $options = []) {
98
		$cacheId = md5(serialize(func_get_args()));
99
		if (!isset($this->resultCache[$cacheId])) {
100
			$query = $this->selectBuilder->createSql($fields, \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND);
101
102
			if (!isset($options['order'])) $options['order'] = $this->defaultSort;
103
104
			$query['sql'] = array_filter($query['sql']);
105
106
			try {
107
				$this->resultCache[$cacheId] = $this->adapter->query($this->selectBuilder->select($this->table, $query['sql'], $query['args'], $options));
108
				$this->addIndex(array_keys($query['args']));
109
				$this->addIndex(explode(',', $options['order']));
110
			}
111
			catch (\Exception $e) {
112
				$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...
113
				$this->resultCache[$cacheId] = [];
114
			}
115
		}
116
		return $this->resultCache[$cacheId];
117
	}
118
119
	public function deleteByField(array $fields, array $options = [], $mode = null) {
120
		if ($mode == null) $mode = \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND;
121
		if (isset($options['limit']) != null) $limit = ' LIMIT ' . $options['limit'];
122
		else $limit = '';
123
124
		$query = $this->selectBuilder->createSql($fields, $mode);
125
        $query['sql'] = array_filter($query['sql']);
126
		$this->adapter->query($this->crudBuilder->delete($this->table, $query['sql'], $query['args'], $limit));
127
		$this->addIndex(array_keys($query['args']));
128
129
		//Clear the cache
130
		$this->cache = [];
131
		$this->resultCache = [];
132
	}
133
134
    private function getIfNew($data) {
135
        $new = false;
136
        foreach ($this->primaryKey as $k) {
137
            if (empty($data->$k)) {
138
                $data->$k = null;
139
                $new = true;
140
            }
141
        }
142
        return $new;
143
    }
144
145
	public function save($data, $tryagain = true) {
146
        $new = $this->getIfNew($data);
147
148
		try {
149
            $result = $this->insert($this->table, $this->primaryKey, $data);
150
151
			//If there was an error but PDO is silent, trigger the catch block anyway
152
			if ($result->errorCode() !== '00000') throw new \Exception('Could not insert into ' . $this->table);
153
		}
154
		catch (\Exception $e) {
155
			if (!$this->getTryAgain($tryagain)) throw $e;
156
157
			$this->adapter->alterDatabase($this->table, $this->primaryKey, $data);
158
			$this->save($data, false);
159
		}
160
161
		$this->updatePK($data, $new);
162
		//Something has changed, clear any cached results as they may now be incorrect
163
		$this->resultCache = [];
164
		$this->updateCache($data);
165
	}
166
167
    private function getTryAgain($tryagain) {
168
        return $tryagain && self::EDIT_STRUCTURE & $this->alterDb;
169
    }
170
171
    private function updatePK($data, $new) {
172
        if ($new && count($this->primaryKey) == 1) $data->{$this->primaryKey[0]} = $this->adapter->lastInsertId();
173
    }
174
175
    private function checkIfUpdateWorked($data) {
176
        $updateWhere = $this->crudBuilder->update($this->table, $this->primaryKey, $data);
177
        $matched = $this->findByField($updateWhere->getArgs());
178
179
        if (count($matched) == 0) throw new \InvalidArgumentException('Record inserted into table ' . $this->table . ' fails table constraints');
180
    }
181
182
    private function updateCache($data) {
183
        $pkValue = $data->{$this->primaryKey[0]};
184
		if (isset($this->cache[$pkValue])) $this->cache[$pkValue] = (object) array_merge((array)$this->cache[$pkValue], (array)$data);
185
		else $this->cache[$pkValue] = $data;
186
    }
187
188
	private function insert($table, array $primaryKey, $data) {
189
		$error = 0;
190
		try {
191
			$result = $this->adapter->query($this->crudBuilder->insert($table, $data));
192
		}
193
		catch (\Exception $e) {
194
			$error = 1;
195
		}
196
197
 		if ($error || $result->errorCode() !== '00000') {
198
            $result = $this->tryUpdate($table, $primaryKey, $data);
199
        }
200
201
		return $result;
202
	}
203
204
    private function tryUpdate($table, array $primaryKey, $data) {
205
        $result = $this->adapter->query($this->crudBuilder->update($table, $primaryKey, $data));
206
        if ($result->rowCount() === 0) $this->checkIfUpdateWorked($data);
207
208
        return $result;
209
    }
210
}
211