Passed
Push — feature/786_podcasts ( 7b8be7...af6910 )
by Pauli
02:22
created

BaseMapper   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 337
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 2
Metric Value
eloc 126
dl 0
loc 337
rs 8.96
c 2
b 0
f 2
wmc 43

23 Methods

Rating   Name   Duplication   Size   Complexity  
A findById() 0 12 2
A createEntity() 0 2 1
A __construct() 0 3 1
A find() 0 3 1
A findAllStarred() 0 5 1
A findAll() 0 13 3
A findAllByName() 0 25 4
A latestUpdateTime() 0 6 2
A exists() 0 4 1
A selectEntities() 0 2 1
A findAllIds() 0 5 1
A selectUserEntities() 0 8 2
A formatTimestampConditions() 0 25 5
A questionMarks() 0 6 2
A insertOrUpdate() 0 8 2
A deleteByCond() 0 3 1
A latestInsertTime() 0 6 2
A insert() 0 16 4
A deleteById() 0 6 2
A count() 0 5 1
A deleteAll() 0 3 1
A setStarredDate() 0 10 2
A update() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like BaseMapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BaseMapper, and based on these observations, apply Extract Interface, too.

1
<?php declare(strict_types=1);
2
3
/**
4
 * ownCloud - Music app
5
 *
6
 * This file is licensed under the Affero General Public License version 3 or
7
 * later. See the COPYING file.
8
 *
9
 * @author Pauli Järvinen <[email protected]>
10
 * @copyright Pauli Järvinen 2016 - 2021
11
 */
12
13
namespace OCA\Music\Db;
14
15
use \OCP\AppFramework\Db\DoesNotExistException;
16
use \OCP\AppFramework\Db\Entity;
17
use \OCP\AppFramework\Db\Mapper;
18
use \OCP\AppFramework\Db\MultipleObjectsReturnedException;
19
use \OCP\IDBConnection;
20
21
use \OCA\Music\AppFramework\Db\UniqueConstraintViolationException;
22
23
/**
24
 * Common base class for data access classes of the Music app
25
 */
26
abstract class BaseMapper extends Mapper {
27
	const SQL_DATE_FORMAT = 'Y-m-d H:i:s.v';
28
29
	protected $nameColumn;
30
31
	public function __construct(IDBConnection $db, string $tableName, string $entityClass, string $nameColumn) {
32
		parent::__construct($db, $tableName, $entityClass);
33
		$this->nameColumn = $nameColumn;
34
	}
35
36
	/**
37
	 * Create an empty object of the entity class bound to this mapper
38
	 */
39
	public function createEntity() : Entity {
40
		return new $this->entityClass();
41
	}
42
43
	/**
44
	 * Find a single entity by id and user_id
45
	 * @throws DoesNotExistException if the entity does not exist
46
	 * @throws MultipleObjectsReturnedException if more than one entity exists
47
	 */
48
	public function find(int $id, string $userId) : Entity {
49
		$sql = $this->selectUserEntities("`{$this->getTableName()}`.`id` = ?");
50
		return $this->findEntity($sql, [$userId, $id]);
51
	}
52
53
	/**
54
	 * Find all entities matching the given IDs. Specifying the owning user is optional.
55
	 * @param integer[] $ids  IDs of the entities to be found
56
	 * @param string|null $userId
57
	 * @return Entity[]
58
	 */
59
	public function findById(array $ids, string $userId=null) : array {
60
		$count = \count($ids);
61
		$condition = "`{$this->getTableName()}`.`id` IN ". $this->questionMarks($count);
62
63
		if (empty($userId)) {
64
			$sql = $this->selectEntities($condition);
65
		} else {
66
			$sql = $this->selectUserEntities($condition);
67
			$ids = \array_merge([$userId], $ids);
68
		}
69
70
		return $this->findEntities($sql, $ids);
71
	}
72
73
	/**
74
	 * Find all user's entities
75
	 * @param string|null $createdMin Optional minimum `created` timestamp.
76
	 * @param string|null $createdMax Optional maximum `created` timestamp.
77
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
78
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
79
	 * @return Entity[]
80
	 */
81
	public function findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null,
82
							?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
83
		if ($sortBy == SortBy::Name) {
84
			$sorting = "ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)";
85
		} elseif ($sortBy == SortBy::Newest) {
86
			$sorting = "ORDER BY `{$this->getTableName()}`.`id` DESC"; // abuse the fact that IDs are ever-incrementing values
87
		} else {
88
			$sorting = null;
89
		}
90
		[$condition, $params] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
91
		$sql = $this->selectUserEntities($condition, $sorting);
92
		\array_unshift($params, $userId);
93
		return $this->findEntities($sql, $params, $limit, $offset);
94
	}
95
96
	/**
97
	 * Find all user's entities matching the given name
98
	 * @param string|null $createdMin Optional minimum `created` timestamp.
99
	 * @param string|null $createdMax Optional maximum `created` timestamp.
100
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
101
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
102
	 * @return Entity[]
103
	 */
104
	public function findAllByName(
105
		?string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null,
106
		?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
107
108
		$nameCol = "`{$this->getTableName()}`.`{$this->nameColumn}`";
109
		if ($name === null) {
110
			$condition = "$nameCol IS NULL";
111
			$params = [$userId];
112
		} elseif ($fuzzy) {
113
			$condition = "LOWER($nameCol) LIKE LOWER(?)";
114
			$params = [$userId, "%$name%"];
115
		} else {
116
			$condition = "$nameCol = ?";
117
			$params = [$userId, $name];
118
		}
119
120
		[$timestampConds, $timestampParams] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
121
		if (!empty($timestampConds)) {
122
			$condition .= ' AND ' . $timestampConds;
123
			$params = \array_merge($params, $timestampParams);
124
		}
125
126
		$sql = $this->selectUserEntities($condition, "ORDER BY LOWER($nameCol)");
127
128
		return $this->findEntities($sql, $params, $limit, $offset);
129
	}
130
131
	/**
132
	 * Find all user's starred entities
133
	 * @return Entity[]
134
	 */
135
	public function findAllStarred(string $userId, int $limit=null, int $offset=null) : array {
136
		$sql = $this->selectUserEntities(
137
				"`{$this->getTableName()}`.`starred` IS NOT NULL",
138
				"ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)");
139
		return $this->findEntities($sql, [$userId], $limit, $offset);
140
	}
141
142
	/**
143
	 * Find IDs of all user's entities of this kind
144
	 * @return int[]
145
	 */
146
	public function findAllIds(string $userId) : array {
147
		$sql = "SELECT `id` FROM `{$this->getTableName()}` WHERE `user_id` = ?";
148
		$result = $this->execute($sql, [$userId]);
149
150
		return \array_map('intval', $result->fetchAll(\PDO::FETCH_COLUMN));
151
	}
152
153
	/**
154
	 * Delete all entities with given IDs without specifying the user
155
	 * @param integer[] $ids  IDs of the entities to be deleted
156
	 */
157
	public function deleteById(array $ids) : void {
158
		$count = \count($ids);
159
		if ($count === 0) {
160
			return;
161
		}
162
		$this->deleteByCond('`id` IN ' . $this->questionMarks($count), $ids);
163
	}
164
165
	/**
166
	 * Delete all entities matching the given SQL condition
167
	 * @param string $condition SQL 'WHERE' condition (without the keyword 'WHERE')
168
	 * @param array $params SQL parameters for the condition
169
	 */
170
	protected function deleteByCond(string $condition, array $params) : void {
171
		$sql = "DELETE FROM `{$this->getTableName()}` WHERE ". $condition;
172
		$this->execute($sql, $params);
173
	}
174
175
	/**
176
	 * Delete all entities of the given user
177
	 */
178
	public function deleteAll(string $userId) : void {
179
		$sql = "DELETE FROM `{$this->getTableName()}` WHERE `user_id` = ?";
180
		$this->execute($sql, [$userId]);
181
	}
182
183
	/**
184
	 * Tests if entity with given ID and user ID exists in the database
185
	 */
186
	public function exists(int $id, string $userId) : bool {
187
		$sql = "SELECT 1 FROM `{$this->getTableName()}` WHERE `id` = ? AND `user_id` = ?";
188
		$result = $this->execute($sql, [$id, $userId]);
189
		return $result->rowCount() > 0;
190
	}
191
192
	/**
193
	 * Count all entities of a user
194
	 */
195
	public function count(string $userId) : int {
196
		$sql = "SELECT COUNT(*) AS count FROM `{$this->getTableName()}` WHERE `user_id` = ?";
197
		$result = $this->execute($sql, [$userId]);
198
		$row = $result->fetch();
199
		return \intval($row['count']);
200
	}
201
202
	/**
203
	 * {@inheritDoc}
204
	 * @see \OCP\AppFramework\Db\Mapper::insert()
205
	 */
206
	public function insert(Entity $entity) : Entity {
207
		$now = new \DateTime();
208
		$nowStr = $now->format(self::SQL_DATE_FORMAT);
209
		$entity->setCreated($nowStr);
210
		$entity->setUpdated($nowStr);
211
212
		try {
213
			return parent::insert($entity);
214
		} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
215
			throw new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e);
216
		} catch (\OCP\DB\Exception $e) {
0 ignored issues
show
Bug introduced by
The type OCP\DB\Exception was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
217
			// Nextcloud 21
218
			if ($e->getReason() == \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
219
				throw new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e);
220
			} else {
221
				throw $e;
222
			}
223
		}
224
	}
225
226
	/**
227
	 * {@inheritDoc}
228
	 * @see \OCP\AppFramework\Db\Mapper::update()
229
	 */
230
	public function update(Entity $entity) : Entity {
231
		$now = new \DateTime();
232
		$entity->setUpdated($now->format(self::SQL_DATE_FORMAT));
233
		return parent::update($entity);
234
	}
235
236
	/**
237
	 * Insert an entity, or if an entity with the same identity already exists,
238
	 * update the existing entity.
239
	 * @return Entity The inserted or updated entity, containing also the id field
240
	 */
241
	public function insertOrUpdate(Entity $entity) : Entity {
242
		try {
243
			return $this->insert($entity);
244
		} catch (UniqueConstraintViolationException $ex) {
245
			$existingEntity = $this->findUniqueEntity($entity);
246
			$entity->setId($existingEntity->getId());
247
			$entity->setCreated($existingEntity->getCreated());
248
			return $this->update($entity);
249
		}
250
	}
251
252
	/**
253
	 * Set the "starred" column of the given entities
254
	 * @param \DateTime|null $date
255
	 * @param integer[] $ids
256
	 * @param string $userId
257
	 * @return int number of modified entities
258
	 */
259
	public function setStarredDate(?\DateTime $date, array $ids, string $userId) : int {
260
		$count = \count($ids);
261
		if (!empty($date)) {
262
			$date = $date->format(self::SQL_DATE_FORMAT);
263
		}
264
265
		$sql = "UPDATE `{$this->getTableName()}` SET `starred` = ?
266
				WHERE `id` IN {$this->questionMarks($count)} AND `user_id` = ?";
267
		$params = \array_merge([$date], $ids, [$userId]);
268
		return $this->execute($sql, $params)->rowCount();
269
	}
270
271
	public function latestInsertTime(string $userId) : ?\DateTime {
272
		$sql = "SELECT MAX(`{$this->getTableName()}`.`created`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
273
		$result = $this->execute($sql, [$userId]);
274
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
275
276
		return ($createdTime === null) ? null : new \DateTime($createdTime);
277
	}
278
279
	public function latestUpdateTime(string $userId) : ?\DateTime {
280
		$sql = "SELECT MAX(`{$this->getTableName()}`.`updated`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
281
		$result = $this->execute($sql, [$userId]);
282
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
283
284
		return ($createdTime === null) ? null : new \DateTime($createdTime);
285
	}
286
287
	/**
288
	 * helper creating a string like '(?,?,?)' with the specified number of elements
289
	 */
290
	protected function questionMarks(int $count) : string {
291
		$questionMarks = [];
292
		for ($i = 0; $i < $count; $i++) {
293
			$questionMarks[] = '?';
294
		}
295
		return '(' . \implode(',', $questionMarks) . ')';
296
	}
297
298
	/**
299
	 * Build a SQL SELECT statement which selects all entities of the given user,
300
	 * and optionally applies other conditions, too.
301
	 * This is built upon `selectEntities` which may be overridden by the derived class.
302
	 * @param string|null $condition Optional extra condition. This will get automatically
303
	 *                               prefixed with ' AND ', so don't include that.
304
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
305
	 *                               the conditions in the SQL statement
306
	 */
307
	protected function selectUserEntities(string $condition=null, string $extension=null) : string {
308
		$allConditions = "`{$this->getTableName()}`.`user_id` = ?";
309
310
		if (!empty($condition)) {
311
			$allConditions .= " AND $condition";
312
		}
313
314
		return $this->selectEntities($allConditions, $extension);
315
	}
316
317
	/**
318
	 * Build a SQL SELECT statement which selects all entities matching the given condition.
319
	 * The derived class may override this if necessary.
320
	 * @param string $condition This will get automatically prefixed with ' WHERE '
321
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
322
	 *                               the conditions in the SQL statement
323
	 */
324
	protected function selectEntities(string $condition, string $extension=null) : string {
325
		return "SELECT * FROM `{$this->getTableName()}` WHERE $condition $extension ";
326
	}
327
328
	/**
329
	 * @return array with two values: The SQL condition as string and the SQL parameters as string[]
330
	 */
331
	protected function formatTimestampConditions(?string $createdMin, ?string $createdMax, ?string $updatedMin, ?string $updatedMax) : array {
332
		$conditions = [];
333
		$params = [];
334
		
335
		if (!empty($createdMin)) {
336
			$conditions[] = "`{$this->getTableName()}`.`created` >= ?";
337
			$params[] = $createdMin;
338
		}
339
		
340
		if (!empty($createdMax)) {
341
			$conditions[] = "`{$this->getTableName()}`.`created` <= ?";
342
			$params[] = $createdMax;
343
		}
344
		
345
		if (!empty($updatedMin)) {
346
			$conditions[] = "`{$this->getTableName()}`.`updated` >= ?";
347
			$params[] = $updatedMin;
348
		}
349
		
350
		if (!empty($updatedMax)) {
351
			$conditions[] = "`{$this->getTableName()}`.`updated` <= ?";
352
			$params[] = $updatedMax;
353
		}
354
		
355
		return [\implode(' AND ', $conditions), $params];
356
	}
357
358
	/**
359
	 * Find an entity which has the same identity as the supplied entity.
360
	 * How the identity of the entity is defined, depends on the derived concrete class.
361
	 */
362
	abstract protected function findUniqueEntity(Entity $entity) : Entity;
363
}
364