Passed
Push — master ( 4e74cd...0361f1 )
by Pauli
02:51
created

BaseMapper::findAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 5
c 2
b 0
f 1
dl 0
loc 7
rs 10
cc 1
nc 1
nop 8

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
		$sorting = $this->formatSortingClause($sortBy);
84
		[$condition, $params] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
85
		$sql = $this->selectUserEntities($condition, $sorting);
86
		\array_unshift($params, $userId);
87
		return $this->findEntities($sql, $params, $limit, $offset);
88
	}
89
90
	/**
91
	 * Find all user's entities matching the given name
92
	 * @param string|null $createdMin Optional minimum `created` timestamp.
93
	 * @param string|null $createdMax Optional maximum `created` timestamp.
94
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
95
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
96
	 * @return Entity[]
97
	 */
98
	public function findAllByName(
99
		?string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null,
100
		?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
101
102
		$nameCol = "`{$this->getTableName()}`.`{$this->nameColumn}`";
103
		if ($name === null) {
104
			$condition = "$nameCol IS NULL";
105
			$params = [$userId];
106
		} elseif ($fuzzy) {
107
			$condition = "LOWER($nameCol) LIKE LOWER(?)";
108
			$params = [$userId, "%$name%"];
109
		} else {
110
			$condition = "$nameCol = ?";
111
			$params = [$userId, $name];
112
		}
113
114
		[$timestampConds, $timestampParams] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
115
		if (!empty($timestampConds)) {
116
			$condition .= ' AND ' . $timestampConds;
117
			$params = \array_merge($params, $timestampParams);
118
		}
119
120
		$sql = $this->selectUserEntities($condition, "ORDER BY LOWER($nameCol)");
121
122
		return $this->findEntities($sql, $params, $limit, $offset);
123
	}
124
125
	/**
126
	 * Find all user's starred entities
127
	 * @return Entity[]
128
	 */
129
	public function findAllStarred(string $userId, int $limit=null, int $offset=null) : array {
130
		$sql = $this->selectUserEntities(
131
				"`{$this->getTableName()}`.`starred` IS NOT NULL",
132
				"ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)");
133
		return $this->findEntities($sql, [$userId], $limit, $offset);
134
	}
135
136
	/**
137
	 * Find IDs of all user's entities of this kind
138
	 * @return int[]
139
	 */
140
	public function findAllIds(string $userId) : array {
141
		$sql = "SELECT `id` FROM `{$this->getTableName()}` WHERE `user_id` = ?";
142
		$result = $this->execute($sql, [$userId]);
143
144
		return \array_map('intval', $result->fetchAll(\PDO::FETCH_COLUMN));
145
	}
146
147
	/**
148
	 * Find IDs of all users owning any entities of this mapper
149
	 * @return string[]
150
	 */
151
	public function findAllUsers() : array {
152
		$sql = "SELECT DISTINCT(`user_id`) FROM `{$this->getTableName()}`";
153
		$result = $this->execute($sql);
154
155
		return $result->fetchAll(\PDO::FETCH_COLUMN);
156
	}
157
158
	/**
159
	 * Delete all entities with given IDs without specifying the user
160
	 * @param integer[] $ids  IDs of the entities to be deleted
161
	 */
162
	public function deleteById(array $ids) : void {
163
		$count = \count($ids);
164
		if ($count === 0) {
165
			return;
166
		}
167
		$this->deleteByCond('`id` IN ' . $this->questionMarks($count), $ids);
168
	}
169
170
	/**
171
	 * Delete all entities matching the given SQL condition
172
	 * @param string $condition SQL 'WHERE' condition (without the keyword 'WHERE')
173
	 * @param array $params SQL parameters for the condition
174
	 */
175
	protected function deleteByCond(string $condition, array $params) : void {
176
		$sql = "DELETE FROM `{$this->getTableName()}` WHERE ". $condition;
177
		$this->execute($sql, $params);
178
	}
179
180
	/**
181
	 * Delete all entities of the given user
182
	 */
183
	public function deleteAll(string $userId) : void {
184
		$sql = "DELETE FROM `{$this->getTableName()}` WHERE `user_id` = ?";
185
		$this->execute($sql, [$userId]);
186
	}
187
188
	/**
189
	 * Tests if entity with given ID and user ID exists in the database
190
	 */
191
	public function exists(int $id, string $userId) : bool {
192
		$sql = "SELECT 1 FROM `{$this->getTableName()}` WHERE `id` = ? AND `user_id` = ?";
193
		$result = $this->execute($sql, [$id, $userId]);
194
		return $result->rowCount() > 0;
195
	}
196
197
	/**
198
	 * Count all entities of a user
199
	 */
200
	public function count(string $userId) : int {
201
		$sql = "SELECT COUNT(*) AS count FROM `{$this->getTableName()}` WHERE `user_id` = ?";
202
		$result = $this->execute($sql, [$userId]);
203
		$row = $result->fetch();
204
		return \intval($row['count']);
205
	}
206
207
	/**
208
	 * {@inheritDoc}
209
	 * @see \OCP\AppFramework\Db\Mapper::insert()
210
	 */
211
	public function insert(Entity $entity) : Entity {
212
		$now = new \DateTime();
213
		$nowStr = $now->format(self::SQL_DATE_FORMAT);
214
		$entity->setCreated($nowStr);
215
		$entity->setUpdated($nowStr);
216
217
		try {
218
			return parent::insert($entity);
219
		} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
220
			throw new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e);
221
		} 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...
222
			// Nextcloud 21
223
			if ($e->getReason() == \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
224
				throw new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e);
225
			} else {
226
				throw $e;
227
			}
228
		}
229
	}
230
231
	/**
232
	 * {@inheritDoc}
233
	 * @see \OCP\AppFramework\Db\Mapper::update()
234
	 */
235
	public function update(Entity $entity) : Entity {
236
		$now = new \DateTime();
237
		$entity->setUpdated($now->format(self::SQL_DATE_FORMAT));
238
		return parent::update($entity);
239
	}
240
241
	/**
242
	 * Insert an entity, or if an entity with the same identity already exists,
243
	 * update the existing entity.
244
	 * @return Entity The inserted or updated entity, containing also the id field
245
	 */
246
	public function insertOrUpdate(Entity $entity) : Entity {
247
		try {
248
			return $this->insert($entity);
249
		} catch (UniqueConstraintViolationException $ex) {
250
			$existingEntity = $this->findUniqueEntity($entity);
251
			$entity->setId($existingEntity->getId());
252
			$entity->setCreated($existingEntity->getCreated());
253
			return $this->update($entity);
254
		}
255
	}
256
257
	/**
258
	 * Set the "starred" column of the given entities
259
	 * @param \DateTime|null $date
260
	 * @param integer[] $ids
261
	 * @param string $userId
262
	 * @return int number of modified entities
263
	 */
264
	public function setStarredDate(?\DateTime $date, array $ids, string $userId) : int {
265
		$count = \count($ids);
266
		if (!empty($date)) {
267
			$date = $date->format(self::SQL_DATE_FORMAT);
268
		}
269
270
		$sql = "UPDATE `{$this->getTableName()}` SET `starred` = ?
271
				WHERE `id` IN {$this->questionMarks($count)} AND `user_id` = ?";
272
		$params = \array_merge([$date], $ids, [$userId]);
273
		return $this->execute($sql, $params)->rowCount();
274
	}
275
276
	public function latestInsertTime(string $userId) : ?\DateTime {
277
		$sql = "SELECT MAX(`{$this->getTableName()}`.`created`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
278
		$result = $this->execute($sql, [$userId]);
279
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
280
281
		return ($createdTime === null) ? null : new \DateTime($createdTime);
282
	}
283
284
	public function latestUpdateTime(string $userId) : ?\DateTime {
285
		$sql = "SELECT MAX(`{$this->getTableName()}`.`updated`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
286
		$result = $this->execute($sql, [$userId]);
287
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
288
289
		return ($createdTime === null) ? null : new \DateTime($createdTime);
290
	}
291
292
	/**
293
	 * helper creating a string like '(?,?,?)' with the specified number of elements
294
	 */
295
	protected function questionMarks(int $count) : string {
296
		$questionMarks = [];
297
		for ($i = 0; $i < $count; $i++) {
298
			$questionMarks[] = '?';
299
		}
300
		return '(' . \implode(',', $questionMarks) . ')';
301
	}
302
303
	/**
304
	 * Build a SQL SELECT statement which selects all entities of the given user,
305
	 * and optionally applies other conditions, too.
306
	 * This is built upon `selectEntities` which may be overridden by the derived class.
307
	 * @param string|null $condition Optional extra condition. This will get automatically
308
	 *                               prefixed with ' AND ', so don't include that.
309
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
310
	 *                               the conditions in the SQL statement
311
	 */
312
	protected function selectUserEntities(string $condition=null, string $extension=null) : string {
313
		$allConditions = "`{$this->getTableName()}`.`user_id` = ?";
314
315
		if (!empty($condition)) {
316
			$allConditions .= " AND $condition";
317
		}
318
319
		return $this->selectEntities($allConditions, $extension);
320
	}
321
322
	/**
323
	 * Build a SQL SELECT statement which selects all entities matching the given condition.
324
	 * The derived class may override this if necessary.
325
	 * @param string $condition This will get automatically prefixed with ' WHERE '
326
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
327
	 *                               the conditions in the SQL statement
328
	 */
329
	protected function selectEntities(string $condition, string $extension=null) : string {
330
		return "SELECT * FROM `{$this->getTableName()}` WHERE $condition $extension ";
331
	}
332
333
	/**
334
	 * @return array with two values: The SQL condition as string and the SQL parameters as string[]
335
	 */
336
	protected function formatTimestampConditions(?string $createdMin, ?string $createdMax, ?string $updatedMin, ?string $updatedMax) : array {
337
		$conditions = [];
338
		$params = [];
339
340
		if (!empty($createdMin)) {
341
			$conditions[] = "`{$this->getTableName()}`.`created` >= ?";
342
			$params[] = $createdMin;
343
		}
344
345
		if (!empty($createdMax)) {
346
			$conditions[] = "`{$this->getTableName()}`.`created` <= ?";
347
			$params[] = $createdMax;
348
		}
349
350
		if (!empty($updatedMin)) {
351
			$conditions[] = "`{$this->getTableName()}`.`updated` >= ?";
352
			$params[] = $updatedMin;
353
		}
354
355
		if (!empty($updatedMax)) {
356
			$conditions[] = "`{$this->getTableName()}`.`updated` <= ?";
357
			$params[] = $updatedMax;
358
		}
359
360
		return [\implode(' AND ', $conditions), $params];
361
	}
362
363
	/**
364
	 * Convert given sorting condition to an SQL clause. Derived class may overide this if necessary.
365
	 * @param int $sortBy One of the constants defined in the class SortBy
366
	 */
367
	protected function formatSortingClause(int $sortBy) : ?string {
368
		if ($sortBy == SortBy::Name) {
369
			return "ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)";
370
		} elseif ($sortBy == SortBy::Newest) {
371
			return "ORDER BY `{$this->getTableName()}`.`id` DESC"; // abuse the fact that IDs are ever-incrementing values
372
		} else {
373
			return null;
374
		}
375
	}
376
377
	/**
378
	 * Find an entity which has the same identity as the supplied entity.
379
	 * How the identity of the entity is defined, depends on the derived concrete class.
380
	 */
381
	abstract protected function findUniqueEntity(Entity $entity) : Entity;
382
}
383