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