Passed
Push — master ( 6b69e2...10553e )
by Pauli
10:08
created

BaseMapper::findAllByName()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 4
eloc 16
nc 6
nop 9
dl 0
loc 25
rs 9.7333
c 1
b 0
f 1

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