Passed
Push — master ( e9743f...7b0700 )
by Richard
01:43
created

Database::insert()   B

Complexity

Conditions 5
Paths 8

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 8
nop 3
dl 0
loc 16
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
    private function getIfNew($data) {
127
        $new = false;
128
        foreach ($this->primaryKey as $k) {
129
            if (empty($data->$k)) {
130
                $data->$k = null;
131
                $new = true;
132
            }
133
        }
134
        return $new;
135
    }
136
137
	public function save($data, $tryagain = true) {
138
		$tryagain = $tryagain && self::EDIT_STRUCTURE & $this->alterDb;
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 ($tryagain) {
149
				$this->adapter->alterDatabase($this->table, $this->primaryKey, $data);
150
				$this->save($data, false);
151
			}
152
			else throw $e;
153
		}
154
155
		if ($new && count($this->primaryKey) == 1) $data->{$this->primaryKey[0]} = $this->adapter->lastInsertId();
156
		//Something has changed, clear any cached results as they may now be incorrect
157
		$this->resultCache = [];
158
		$this->updateCache($data);
159
	}
160
161
    private function checkIfUpdateWorked($data) {
162
        $updateWhere = $this->crudBuilder->update($this->table, $this->primaryKey, $data);
163
        $matched = $this->findByField($updateWhere->getArgs());
164
165
        if (count($matched) == 0) throw new \InvalidArgumentException('Record inserted into table ' . $this->table . ' fails table constraints');
166
    }
167
168
    private function updateCache($data) {
169
        $pkValue = $data->{$this->primaryKey[0]};
170
		if (isset($this->cache[$pkValue])) $this->cache[$pkValue] = (object) array_merge((array)$this->cache[$pkValue], (array)$data);
171
		else $this->cache[$pkValue] = $data;
172
    }
173
174
	private function insert($table, array $primaryKey, $data) {
175
		$error = 0;
176
		try {
177
			$result = $this->adapter->query($this->crudBuilder->insert($table, $data));
178
		}
179
		catch (\Exception $e) {
180
			$error = 1;
181
		}
182
183
 		if ($error || $result->errorCode() !== '00000') {
184
 			$result = $this->adapter->query($this->crudBuilder->update($table, $primaryKey, $data));
185
 		}
186
187
        if ($result->rowCount() === 0) $this->checkIfUpdateWorked($data);
188
189
		return $result;
190
	}
191
}
192