Passed
Push — master ( 5b283a...d58ab5 )
by Richard
01:35
created

Database::insert()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 8
nc 4
nop 3
dl 0
loc 14
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->selectQuery($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->selectQuery($this->selectBuilder->aggregate($this->table, $function, $field, $query['sql'], $query['args'], $group));
75
76
			return $this->determineAggregateResult($result, $group, $field);
77
		}
78
		catch (\Exception $e) {
79
			return $group ? [] : 0;
80
		}
81
	}
82
83
    private function determineAggregateResult($result, $group, $field) {
84
        if ($group != null) {
85
            $ret = [];
86
            foreach ($result as $res) $ret[$res->$field] = $res->val;
87
            return $ret;
88
        }
89
        else if (isset($result[0])) return $result[0]->val;
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
			try {
105
				$this->resultCache[$cacheId] = $this->selectQuery($this->selectBuilder->select($this->table, $query['sql'], $query['args'], $options));
106
				$this->addIndex(array_keys($query['args']));
107
				$this->addIndex(explode(',', $options['order']));
108
			}
109
			catch (\Exception $e) {
110
				$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...
111
				$this->resultCache[$cacheId] = [];
112
			}
113
		}
114
		return $this->resultCache[$cacheId];
115
	}
116
117
	public function deleteByField(array $fields, array $options = []) {
118
		$query = $this->selectBuilder->createSql($fields, \Maphper\Maphper::FIND_EXACT | \Maphper\Maphper::FIND_AND);
119
		$this->adapter->query($this->crudBuilder->delete($this->table, $query['sql'], $query['args'], $options['limit'], null, $options['order']));
120
		$this->addIndex(array_keys($query['args']));
121
122
		//Clear the cache
123
		$this->cache = [];
124
		$this->resultCache = [];
125
	}
126
127
    private function getIfNew($data) {
128
        $new = false;
129
        foreach ($this->primaryKey as $k) {
130
            if (empty($data->$k)) {
131
                $data->$k = null;
132
                $new = true;
133
            }
134
        }
135
        return $new;
136
    }
137
138
	public function save($data, $tryagain = true) {
139
        $new = $this->getIfNew($data);
140
141
		try {
142
            $result = $this->insert($this->table, $this->primaryKey, $data);
143
144
			//If there was an error but PDO is silent, trigger the catch block anyway
145
			if ($result->errorCode() !== '00000') throw new \Exception('Could not insert into ' . $this->table);
146
		}
147
		catch (\Exception $e) {
148
			if (!$this->getTryAgain($tryagain)) throw $e;
149
150
			$this->adapter->alterDatabase($this->table, $this->primaryKey, $data);
151
			$this->save($data, false);
152
		}
153
154
		$this->updatePK($data, $new);
155
		//Something has changed, clear any cached results as they may now be incorrect
156
		$this->resultCache = [];
157
		$this->updateCache($data);
158
	}
159
160
    private function getTryAgain($tryagain) {
161
        return $tryagain && self::EDIT_STRUCTURE & $this->alterDb;
162
    }
163
164
    private function updatePK($data, $new) {
165
        if ($new && count($this->primaryKey) == 1) $data->{$this->primaryKey[0]} = $this->adapter->lastInsertId();
166
    }
167
168
    private function checkIfUpdateWorked($data) {
169
        $updateWhere = $this->crudBuilder->update($this->table, $this->primaryKey, $data);
170
        $matched = $this->findByField($updateWhere->getArgs());
171
172
        if (count($matched) == 0) throw new \InvalidArgumentException('Record inserted into table ' . $this->table . ' fails table constraints');
173
    }
174
175
    private function updateCache($data) {
176
        $pkValue = $data->{$this->primaryKey[0]};
177
		if (isset($this->cache[$pkValue])) $this->cache[$pkValue] = (object) array_merge((array)$this->cache[$pkValue], (array)$data);
178
		else $this->cache[$pkValue] = $data;
179
    }
180
181
	private function insert($table, array $primaryKey, $data) {
182
		$error = 0;
183
		try {
184
			$result = $this->adapter->query($this->crudBuilder->insert($table, $data));
185
		}
186
		catch (\Exception $e) {
187
			$error = 1;
188
		}
189
190
 		if ($error || $result->errorCode() !== '00000') {
191
            $result = $this->tryUpdate($table, $primaryKey, $data);
192
        }
193
194
		return $result;
195
	}
196
197
    private function tryUpdate($table, array $primaryKey, $data) {
198
        $result = $this->adapter->query($this->crudBuilder->update($table, $primaryKey, $data));
199
        if ($result->rowCount() === 0) $this->checkIfUpdateWorked($data);
200
201
        return $result;
202
    }
203
204
    private function selectQuery(\Maphper\Lib\Query $query) {
205
        return $this->adapter->query($query)->fetchAll(\PDO::FETCH_OBJ);
206
    }
207
}
208