Passed
Push — master ( 22417d...b806b4 )
by Pauli
18:16
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
use \OCA\Music\Utility\Util;
23
24
/**
25
 * Common base class for data access classes of the Music app
26
 */
27
abstract class BaseMapper extends Mapper {
28
	const SQL_DATE_FORMAT = 'Y-m-d H:i:s.v';
29
30
	protected $nameColumn;
31
32
	public function __construct(IDBConnection $db, string $tableName, string $entityClass, string $nameColumn) {
33
		parent::__construct($db, $tableName, $entityClass);
34
		$this->nameColumn = $nameColumn;
35
	}
36
37
	/**
38
	 * Create an empty object of the entity class bound to this mapper
39
	 */
40
	public function createEntity() : Entity {
41
		return new $this->entityClass();
42
	}
43
44
	/**
45
	 * Find a single entity by id and user_id
46
	 * @throws DoesNotExistException if the entity does not exist
47
	 * @throws MultipleObjectsReturnedException if more than one entity exists
48
	 */
49
	public function find(int $id, string $userId) : Entity {
50
		$sql = $this->selectUserEntities("`{$this->getTableName()}`.`id` = ?");
51
		return $this->findEntity($sql, [$userId, $id]);
52
	}
53
54
	/**
55
	 * Find all entities matching the given IDs. Specifying the owning user is optional.
56
	 * @param integer[] $ids  IDs of the entities to be found
57
	 * @param string|null $userId
58
	 * @return Entity[]
59
	 */
60
	public function findById(array $ids, string $userId=null) : array {
61
		$count = \count($ids);
62
		$condition = "`{$this->getTableName()}`.`id` IN ". $this->questionMarks($count);
63
64
		if (empty($userId)) {
65
			$sql = $this->selectEntities($condition);
66
		} else {
67
			$sql = $this->selectUserEntities($condition);
68
			$ids = \array_merge([$userId], $ids);
69
		}
70
71
		return $this->findEntities($sql, $ids);
72
	}
73
74
	/**
75
	 * Find all user's entities
76
	 * @param string|null $createdMin Optional minimum `created` timestamp.
77
	 * @param string|null $createdMax Optional maximum `created` timestamp.
78
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
79
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
80
	 * @return Entity[]
81
	 */
82
	public function findAll(string $userId, int $sortBy=SortBy::None, int $limit=null, int $offset=null,
83
							?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
84
		$sorting = $this->formatSortingClause($sortBy);
85
		[$condition, $params] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
86
		$sql = $this->selectUserEntities($condition, $sorting);
87
		\array_unshift($params, $userId);
88
		return $this->findEntities($sql, $params, $limit, $offset);
89
	}
90
91
	/**
92
	 * Find all user's entities matching the given name
93
	 * @param string|null $createdMin Optional minimum `created` timestamp.
94
	 * @param string|null $createdMax Optional maximum `created` timestamp.
95
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
96
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
97
	 * @return Entity[]
98
	 */
99
	public function findAllByName(
100
		?string $name, string $userId, bool $fuzzy=false, int $limit=null, int $offset=null,
101
		?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
102
103
		$nameCol = "`{$this->getTableName()}`.`{$this->nameColumn}`";
104
		if ($name === null) {
105
			$condition = "$nameCol IS NULL";
106
			$params = [$userId];
107
		} elseif ($fuzzy) {
108
			$condition = "LOWER($nameCol) LIKE LOWER(?)";
109
			$params = [$userId, self::prepareFuzzySearchString($name)];
110
		} else {
111
			$condition = "LOWER($nameCol) = LOWER(?)";
112
			$params = [$userId, $name];
113
		}
114
115
		[$timestampConds, $timestampParams] = $this->formatTimestampConditions($createdMin, $createdMax, $updatedMin, $updatedMax);
116
		if (!empty($timestampConds)) {
117
			$condition .= ' AND ' . $timestampConds;
118
			$params = \array_merge($params, $timestampParams);
119
		}
120
121
		$sql = $this->selectUserEntities($condition, "ORDER BY LOWER($nameCol)");
122
123
		return $this->findEntities($sql, $params, $limit, $offset);
124
	}
125
126
	/**
127
	 * Find all user's starred entities
128
	 * @return Entity[]
129
	 */
130
	public function findAllStarred(string $userId, int $limit=null, int $offset=null) : array {
131
		$sql = $this->selectUserEntities(
132
				"`{$this->getTableName()}`.`starred` IS NOT NULL",
133
				"ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)");
134
		return $this->findEntities($sql, [$userId], $limit, $offset);
135
	}
136
137
	/**
138
	 * Find IDs of all user's entities of this kind
139
	 * @return int[]
140
	 */
141
	public function findAllIds(string $userId) : array {
142
		$sql = "SELECT `id` FROM `{$this->getTableName()}` WHERE `user_id` = ?";
143
		$result = $this->execute($sql, [$userId]);
144
145
		return \array_map('intval', $result->fetchAll(\PDO::FETCH_COLUMN));
146
	}
147
148
	/**
149
	 * Find IDs of all users owning any entities of this mapper
150
	 * @return string[]
151
	 */
152
	public function findAllUsers() : array {
153
		$sql = "SELECT DISTINCT(`user_id`) FROM `{$this->getTableName()}`";
154
		$result = $this->execute($sql);
155
156
		return $result->fetchAll(\PDO::FETCH_COLUMN);
157
	}
158
159
	/**
160
	 * Delete all entities with given IDs without specifying the user
161
	 * @param integer[] $ids  IDs of the entities to be deleted
162
	 */
163
	public function deleteById(array $ids) : void {
164
		$count = \count($ids);
165
		if ($count === 0) {
166
			return;
167
		}
168
		$this->deleteByCond('`id` IN ' . $this->questionMarks($count), $ids);
169
	}
170
171
	/**
172
	 * Delete all entities matching the given SQL condition
173
	 * @param string $condition SQL 'WHERE' condition (without the keyword 'WHERE')
174
	 * @param array $params SQL parameters for the condition
175
	 */
176
	protected function deleteByCond(string $condition, array $params) : void {
177
		$sql = "DELETE FROM `{$this->getTableName()}` WHERE ". $condition;
178
		$this->execute($sql, $params);
179
	}
180
181
	/**
182
	 * Delete all entities of the given user
183
	 */
184
	public function deleteAll(string $userId) : void {
185
		$sql = "DELETE FROM `{$this->getTableName()}` WHERE `user_id` = ?";
186
		$this->execute($sql, [$userId]);
187
	}
188
189
	/**
190
	 * Tests if entity with given ID and user ID exists in the database
191
	 */
192
	public function exists(int $id, string $userId) : bool {
193
		$sql = "SELECT 1 FROM `{$this->getTableName()}` WHERE `id` = ? AND `user_id` = ?";
194
		$result = $this->execute($sql, [$id, $userId]);
195
		return $result->rowCount() > 0;
196
	}
197
198
	/**
199
	 * Count all entities of a user
200
	 */
201
	public function count(string $userId) : int {
202
		$sql = "SELECT COUNT(*) AS count FROM `{$this->getTableName()}` WHERE `user_id` = ?";
203
		$result = $this->execute($sql, [$userId]);
204
		$row = $result->fetch();
205
		return \intval($row['count']);
206
	}
207
208
	/**
209
	 * {@inheritDoc}
210
	 * @see \OCP\AppFramework\Db\Mapper::insert()
211
	 */
212
	public function insert(Entity $entity) : Entity {
213
		$now = new \DateTime();
214
		$nowStr = $now->format(self::SQL_DATE_FORMAT);
215
		$entity->setCreated($nowStr);
216
		$entity->setUpdated($nowStr);
217
218
		try {
219
			return parent::insert($entity);
220
		} catch (\Doctrine\DBAL\Exception\UniqueConstraintViolationException $e) {
221
			throw new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e);
222
		} 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...
223
			// Nextcloud 21+
224
			if ($e->getReason() == \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
225
				throw new UniqueConstraintViolationException($e->getMessage(), $e->getCode(), $e);
226
			} else {
227
				throw $e;
228
			}
229
		}
230
	}
231
232
	/**
233
	 * {@inheritDoc}
234
	 * @see \OCP\AppFramework\Db\Mapper::update()
235
	 */
236
	public function update(Entity $entity) : Entity {
237
		$now = new \DateTime();
238
		$entity->setUpdated($now->format(self::SQL_DATE_FORMAT));
239
		return parent::update($entity);
240
	}
241
242
	/**
243
	 * Insert an entity, or if an entity with the same identity already exists,
244
	 * update the existing entity.
245
	 * Note: The functions insertOrUpate and updateOrInsert get the exactly same thing done. The only difference is
246
	 * that the former is optimized for cases where the entity doens't exist and the latter for cases where it does exist.
247
	 * @return Entity The inserted or updated entity, containing also the id field
248
	 */
249
	public function insertOrUpdate(Entity $entity) : Entity {
250
		try {
251
			return $this->insert($entity);
252
		} catch (UniqueConstraintViolationException $ex) {
253
			$existingEntity = $this->findUniqueEntity($entity);
254
			if (self::entityNeedsUpdate($existingEntity, $entity)) {
255
				$entity->setId($existingEntity->getId());
256
				$entity->setCreated($existingEntity->getCreated());
257
				return $this->update($entity);
258
			} else {
259
				return $existingEntity;
260
			}
261
		}
262
	}
263
264
	/**
265
	 * Update an entity whose unique constraint fields match the given entity. If such entity is not found,
266
	 * a new entity is inserted.
267
	 * Note: The functions insertOrUpate and updateOrInsert get the exactly same thing done. The only difference is
268
	 * that the former is optimized for cases where the entity doens't exist and the latter for cases where it does exist.
269
	 * @return Entity The inserted or updated entity, containing also the id field
270
	 */
271
	public function updateOrInsert(Entity $entity) : Entity {
272
		try {
273
			$existingEntity = $this->findUniqueEntity($entity);
274
			if (self::entityNeedsUpdate($existingEntity, $entity)) {
275
				$entity->setId($existingEntity->getId());
276
				return $this->update($entity);
277
			} else {
278
				return $existingEntity;
279
			}
280
		} catch (DoesNotExistException $ex) {
281
			try {
282
				return $this->insert($entity);
283
			} catch (UniqueConstraintViolationException $ex) {
284
				// the conflicting entry didn't exist an eyeblink ago but now it does
285
				// => this is essentially a concurrent update and it is anyway non-deterministic, which
286
				//    update happens last; cancel this update
287
				return $this->findUniqueEntity($entity);
288
			}
289
		}
290
	}
291
292
	protected static function entityNeedsUpdate(Entity $oldData, Entity $newData) {
293
		$fields = $newData->getUpdatedFields();
294
		foreach ($fields as $field => $updated) {
295
			if ($oldData->$field != $newData->$field) {
296
				return true;
297
			}
298
		}
299
		return false;
300
	}
301
302
	/**
303
	 * Set the "starred" column of the given entities
304
	 * @param \DateTime|null $date
305
	 * @param integer[] $ids
306
	 * @param string $userId
307
	 * @return int number of modified entities
308
	 */
309
	public function setStarredDate(?\DateTime $date, array $ids, string $userId) : int {
310
		$count = \count($ids);
311
		if (!empty($date)) {
312
			$date = $date->format(self::SQL_DATE_FORMAT);
313
		}
314
315
		$sql = "UPDATE `{$this->getTableName()}` SET `starred` = ?
316
				WHERE `id` IN {$this->questionMarks($count)} AND `user_id` = ?";
317
		$params = \array_merge([$date], $ids, [$userId]);
318
		return $this->execute($sql, $params)->rowCount();
319
	}
320
321
	public function latestInsertTime(string $userId) : ?\DateTime {
322
		$sql = "SELECT MAX(`{$this->getTableName()}`.`created`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
323
		$result = $this->execute($sql, [$userId]);
324
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
325
326
		return ($createdTime === null) ? null : new \DateTime($createdTime);
327
	}
328
329
	public function latestUpdateTime(string $userId) : ?\DateTime {
330
		$sql = "SELECT MAX(`{$this->getTableName()}`.`updated`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
331
		$result = $this->execute($sql, [$userId]);
332
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
333
334
		return ($createdTime === null) ? null : new \DateTime($createdTime);
335
	}
336
337
	/**
338
	 * helper creating a string like '(?,?,?)' with the specified number of elements
339
	 */
340
	protected function questionMarks(int $count) : string {
341
		$questionMarks = [];
342
		for ($i = 0; $i < $count; $i++) {
343
			$questionMarks[] = '?';
344
		}
345
		return '(' . \implode(',', $questionMarks) . ')';
346
	}
347
348
	/**
349
	 * Build a SQL SELECT statement which selects all entities of the given user,
350
	 * and optionally applies other conditions, too.
351
	 * This is built upon `selectEntities` which may be overridden by the derived class.
352
	 * @param string|null $condition Optional extra condition. This will get automatically
353
	 *                               prefixed with ' AND ', so don't include that.
354
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
355
	 *                               the conditions in the SQL statement
356
	 */
357
	protected function selectUserEntities(string $condition=null, string $extension=null) : string {
358
		$allConditions = "`{$this->getTableName()}`.`user_id` = ?";
359
360
		if (!empty($condition)) {
361
			$allConditions .= " AND $condition";
362
		}
363
364
		return $this->selectEntities($allConditions, $extension);
365
	}
366
367
	/**
368
	 * Build a SQL SELECT statement which selects all entities matching the given condition.
369
	 * The derived class may override this if necessary.
370
	 * @param string $condition This will get automatically prefixed with ' WHERE '
371
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
372
	 *                               the conditions in the SQL statement
373
	 */
374
	protected function selectEntities(string $condition, string $extension=null) : string {
375
		return "SELECT * FROM `{$this->getTableName()}` WHERE $condition $extension ";
376
	}
377
378
	/**
379
	 * @return array with two values: The SQL condition as string and the SQL parameters as string[]
380
	 */
381
	protected function formatTimestampConditions(?string $createdMin, ?string $createdMax, ?string $updatedMin, ?string $updatedMax) : array {
382
		$conditions = [];
383
		$params = [];
384
385
		if (!empty($createdMin)) {
386
			$conditions[] = "`{$this->getTableName()}`.`created` >= ?";
387
			$params[] = $createdMin;
388
		}
389
390
		if (!empty($createdMax)) {
391
			$conditions[] = "`{$this->getTableName()}`.`created` <= ?";
392
			$params[] = $createdMax;
393
		}
394
395
		if (!empty($updatedMin)) {
396
			$conditions[] = "`{$this->getTableName()}`.`updated` >= ?";
397
			$params[] = $updatedMin;
398
		}
399
400
		if (!empty($updatedMax)) {
401
			$conditions[] = "`{$this->getTableName()}`.`updated` <= ?";
402
			$params[] = $updatedMax;
403
		}
404
405
		return [\implode(' AND ', $conditions), $params];
406
	}
407
408
	/**
409
	 * Convert given sorting condition to an SQL clause. Derived class may overide this if necessary.
410
	 * @param int $sortBy One of the constants defined in the class SortBy
411
	 */
412
	protected function formatSortingClause(int $sortBy) : ?string {
413
		if ($sortBy == SortBy::Name) {
414
			return "ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)";
415
		} elseif ($sortBy == SortBy::Newest) {
416
			return "ORDER BY `{$this->getTableName()}`.`id` DESC"; // abuse the fact that IDs are ever-incrementing values
417
		} else {
418
			return null;
419
		}
420
	}
421
422
	protected static function prepareFuzzySearchString(string $input) : string {
423
		// possibly multiparted query enclosed in quotation marks is handled as a single substing,
424
		// while the default interpretation of multipart string is that each of the parts can be found
425
		// separately as substing in the given order
426
		if (Util::startsWith($input, '"') && Util::endsWith($input, '"')) {
427
			// remove the quotation
428
			$pattern = \substr($input, 1, -1);
429
		} else {
430
			// split to parts by whitespace
431
			$parts = \preg_split('/\s+/', $input, -1, PREG_SPLIT_NO_EMPTY);
432
			// glue the parts back together with a wildcard charater
433
			$pattern = \implode('%', $parts);
434
		}
435
		return "%$pattern%";
436
	}
437
438
	/**
439
	 * Find an entity which has the same identity as the supplied entity.
440
	 * How the identity of the entity is defined, depends on the derived concrete class.
441
	 */
442
	abstract protected function findUniqueEntity(Entity $entity) : Entity;
443
}
444