Completed
Push — master ( ac1d29...5581e0 )
by Roeland
15:06 queued 10s
created

QBMapper::insertOrUpdate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
/**
4
 * @copyright 2018, Roeland Jago Douma <[email protected]>
5
 *
6
 * @author Roeland Jago Douma <[email protected]>
7
 *
8
 * @license GNU AGPL version 3 or any later version
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU Affero General Public License as
12
 * published by the Free Software Foundation, either version 3 of the
13
 * License, or (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU Affero General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU Affero General Public License
21
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
22
 *
23
 */
24
25
namespace OCP\AppFramework\Db;
26
27
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
28
use OCP\DB\QueryBuilder\IQueryBuilder;
29
use OCP\IDBConnection;
30
31
/**
32
 * Simple parent class for inheriting your data access layer from. This class
33
 * may be subject to change in the future
34
 *
35
 * @since 14.0.0
36
 */
37
abstract class QBMapper {
38
39
	/** @var string */
40
	protected $tableName;
41
42
	/** @var string */
43
	protected $entityClass;
44
45
	/** @var IDBConnection */
46
	protected $db;
47
48
	/**
49
	 * @param IDBConnection $db Instance of the Db abstraction layer
50
	 * @param string $tableName the name of the table. set this to allow entity
51
	 * @param string $entityClass the name of the entity that the sql should be
52
	 * mapped to queries without using sql
53
	 * @since 14.0.0
54
	 */
55 View Code Duplication
	public function __construct(IDBConnection $db, string $tableName, string $entityClass=null){
56
		$this->db = $db;
57
		$this->tableName = $tableName;
58
59
		// if not given set the entity name to the class without the mapper part
60
		// cache it here for later use since reflection is slow
61
		if($entityClass === null) {
62
			$this->entityClass = str_replace('Mapper', '', \get_class($this));
63
		} else {
64
			$this->entityClass = $entityClass;
65
		}
66
	}
67
68
69
	/**
70
	 * @return string the table name
71
	 * @since 14.0.0
72
	 */
73
	public function getTableName(): string {
74
		return $this->tableName;
75
	}
76
77
78
	/**
79
	 * Deletes an entity from the table
80
	 * @param Entity $entity the entity that should be deleted
81
	 * @return Entity the deleted entity
82
	 * @since 14.0.0
83
	 */
84 View Code Duplication
	public function delete(Entity $entity): Entity {
85
		$qb = $this->db->getQueryBuilder();
86
87
		$qb->delete($this->tableName)
88
			->where(
89
				$qb->expr()->eq('id', $qb->createNamedParameter($entity->getId()))
90
			);
91
		$qb->execute();
92
		return $entity;
93
	}
94
95
96
	/**
97
	 * Creates a new entry in the db from an entity
98
	 * @param Entity $entity the entity that should be created
99
	 * @return Entity the saved entity with the set id
100
	 * @since 14.0.0
101
	 * @suppress SqlInjectionChecker
102
	 */
103
	public function insert(Entity $entity): Entity {
104
		// get updated fields to save, fields have to be set using a setter to
105
		// be saved
106
		$properties = $entity->getUpdatedFields();
107
108
		$qb = $this->db->getQueryBuilder();
109
		$qb->insert($this->tableName);
110
111
		// build the fields
112 View Code Duplication
		foreach($properties as $property => $updated) {
113
			$column = $entity->propertyToColumn($property);
114
			$getter = 'get' . ucfirst($property);
115
			$value = $entity->$getter();
116
117
			$qb->setValue($column, $qb->createNamedParameter($value));
118
		}
119
120
		$qb->execute();
121
122
		$entity->setId((int) $qb->getLastInsertId());
123
124
		return $entity;
125
	}
126
127
	/**
128
	 * Tries to creates a new entry in the db from an entity and
129
	 * updates an existing entry if duplicate keys are detected
130
	 * by the database
131
	 *
132
	 * @param Entity $entity the entity that should be created/updated
133
	 * @return Entity the saved entity with the (new) id
134
	 * @throws \InvalidArgumentException if entity has no id
135
	 * @since 15.0.0
136
	 * @suppress SqlInjectionChecker
137
	 */
138
	public function insertOrUpdate(Entity $entity): Entity {
139
		try {
140
			return $this->insert($entity);
141
		} catch (UniqueConstraintViolationException $ex) {
0 ignored issues
show
Bug introduced by
The class Doctrine\DBAL\Exception\...raintViolationException does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
142
			return $this->update($entity);
143
		}
144
	}
145
146
	/**
147
	 * Updates an entry in the db from an entity
148
	 * @throws \InvalidArgumentException if entity has no id
149
	 * @param Entity $entity the entity that should be created
150
	 * @return Entity the saved entity with the set id
151
	 * @since 14.0.0
152
	 * @suppress SqlInjectionChecker
153
	 */
154
	public function update(Entity $entity): Entity {
155
		// if entity wasn't changed it makes no sense to run a db query
156
		$properties = $entity->getUpdatedFields();
157
		if(\count($properties) === 0) {
158
			return $entity;
159
		}
160
161
		// entity needs an id
162
		$id = $entity->getId();
163
		if($id === null){
164
			throw new \InvalidArgumentException(
165
				'Entity which should be updated has no id');
166
		}
167
168
		// get updated fields to save, fields have to be set using a setter to
169
		// be saved
170
		// do not update the id field
171
		unset($properties['id']);
172
173
		$qb = $this->db->getQueryBuilder();
174
		$qb->update($this->tableName);
175
176
		// build the fields
177 View Code Duplication
		foreach($properties as $property => $updated) {
178
			$column = $entity->propertyToColumn($property);
179
			$getter = 'get' . ucfirst($property);
180
			$value = $entity->$getter();
181
182
			$qb->set($column, $qb->createNamedParameter($value));
183
		}
184
185
		$qb->where(
186
			$qb->expr()->eq('id', $qb->createNamedParameter($id))
187
		);
188
		$qb->execute();
189
190
		return $entity;
191
	}
192
193
	/**
194
	 * Returns an db result and throws exceptions when there are more or less
195
	 * results
196
	 *
197
	 * @see findEntity
198
	 *
199
	 * @param IQueryBuilder $query
200
	 * @throws DoesNotExistException if the item does not exist
201
	 * @throws MultipleObjectsReturnedException if more than one item exist
202
	 * @return array the result as row
203
	 * @since 14.0.0
204
	 */
205
	protected function findOneQuery(IQueryBuilder $query): array {
206
		$cursor = $query->execute();
207
208
		$row = $cursor->fetch();
209
		if($row === false) {
210
			$cursor->closeCursor();
211
			$msg = $this->buildDebugMessage(
212
				'Did expect one result but found none when executing', $query
213
			);
214
			throw new DoesNotExistException($msg);
215
		}
216
217
		$row2 = $cursor->fetch();
218
		$cursor->closeCursor();
219
		if($row2 !== false ) {
220
			$msg = $this->buildDebugMessage(
221
				'Did not expect more than one result when executing', $query
222
			);
223
			throw new MultipleObjectsReturnedException($msg);
224
		}
225
226
		return $row;
227
	}
228
229
	/**
230
	 * @param string $msg
231
	 * @param IQueryBuilder $sql
232
	 * @return string
233
	 * @since 14.0.0
234
	 */
235
	private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
236
		return $msg .
237
			': query "' . $sql->getSQL() . '"; ';
238
	}
239
240
241
	/**
242
	 * Creates an entity from a row. Automatically determines the entity class
243
	 * from the current mapper name (MyEntityMapper -> MyEntity)
244
	 *
245
	 * @param array $row the row which should be converted to an entity
246
	 * @return Entity the entity
247
	 * @since 14.0.0
248
	 */
249
	protected function mapRowToEntity(array $row): Entity {
250
		return \call_user_func($this->entityClass .'::fromRow', $row);
251
	}
252
253
254
	/**
255
	 * Runs a sql query and returns an array of entities
256
	 *
257
	 * @param IQueryBuilder $query
258
	 * @return Entity[] all fetched entities
259
	 * @since 14.0.0
260
	 */
261
	protected function findEntities(IQueryBuilder $query): array {
262
		$cursor = $query->execute();
263
264
		$entities = [];
265
266
		while($row = $cursor->fetch()){
267
			$entities[] = $this->mapRowToEntity($row);
268
		}
269
270
		$cursor->closeCursor();
271
272
		return $entities;
273
	}
274
275
276
	/**
277
	 * Returns an db result and throws exceptions when there are more or less
278
	 * results
279
	 *
280
	 * @param IQueryBuilder $query
281
	 * @throws DoesNotExistException if the item does not exist
282
	 * @throws MultipleObjectsReturnedException if more than one item exist
283
	 * @return Entity the entity
284
	 * @since 14.0.0
285
	 */
286
	protected function findEntity(IQueryBuilder $query): Entity {
287
		return $this->mapRowToEntity($this->findOneQuery($query));
288
	}
289
290
}
291