Passed
Push — master ( 1fce36...731973 )
by Pauli
10:48
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\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
	 * @return Entity The inserted or updated entity, containing also the id field
246
	 */
247
	public function insertOrUpdate(Entity $entity) : Entity {
248
		try {
249
			return $this->insert($entity);
250
		} catch (UniqueConstraintViolationException $ex) {
251
			$existingEntity = $this->findUniqueEntity($entity);
252
			$entity->setId($existingEntity->getId());
253
			$entity->setCreated($existingEntity->getCreated());
254
			return $this->update($entity);
255
		}
256
	}
257
258
	/**
259
	 * Set the "starred" column of the given entities
260
	 * @param \DateTime|null $date
261
	 * @param integer[] $ids
262
	 * @param string $userId
263
	 * @return int number of modified entities
264
	 */
265
	public function setStarredDate(?\DateTime $date, array $ids, string $userId) : int {
266
		$count = \count($ids);
267
		if (!empty($date)) {
268
			$date = $date->format(self::SQL_DATE_FORMAT);
269
		}
270
271
		$sql = "UPDATE `{$this->getTableName()}` SET `starred` = ?
272
				WHERE `id` IN {$this->questionMarks($count)} AND `user_id` = ?";
273
		$params = \array_merge([$date], $ids, [$userId]);
274
		return $this->execute($sql, $params)->rowCount();
275
	}
276
277
	public function latestInsertTime(string $userId) : ?\DateTime {
278
		$sql = "SELECT MAX(`{$this->getTableName()}`.`created`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
279
		$result = $this->execute($sql, [$userId]);
280
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
281
282
		return ($createdTime === null) ? null : new \DateTime($createdTime);
283
	}
284
285
	public function latestUpdateTime(string $userId) : ?\DateTime {
286
		$sql = "SELECT MAX(`{$this->getTableName()}`.`updated`) FROM `{$this->getTableName()}` WHERE `user_id` = ?";
287
		$result = $this->execute($sql, [$userId]);
288
		$createdTime = $result->fetch(\PDO::FETCH_COLUMN);
289
290
		return ($createdTime === null) ? null : new \DateTime($createdTime);
291
	}
292
293
	/**
294
	 * helper creating a string like '(?,?,?)' with the specified number of elements
295
	 */
296
	protected function questionMarks(int $count) : string {
297
		$questionMarks = [];
298
		for ($i = 0; $i < $count; $i++) {
299
			$questionMarks[] = '?';
300
		}
301
		return '(' . \implode(',', $questionMarks) . ')';
302
	}
303
304
	/**
305
	 * Build a SQL SELECT statement which selects all entities of the given user,
306
	 * and optionally applies other conditions, too.
307
	 * This is built upon `selectEntities` which may be overridden by the derived class.
308
	 * @param string|null $condition Optional extra condition. This will get automatically
309
	 *                               prefixed with ' AND ', so don't include that.
310
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
311
	 *                               the conditions in the SQL statement
312
	 */
313
	protected function selectUserEntities(string $condition=null, string $extension=null) : string {
314
		$allConditions = "`{$this->getTableName()}`.`user_id` = ?";
315
316
		if (!empty($condition)) {
317
			$allConditions .= " AND $condition";
318
		}
319
320
		return $this->selectEntities($allConditions, $extension);
321
	}
322
323
	/**
324
	 * Build a SQL SELECT statement which selects all entities matching the given condition.
325
	 * The derived class may override this if necessary.
326
	 * @param string $condition This will get automatically prefixed with ' WHERE '
327
	 * @param string|null $extension Any extension (e.g. ORDER BY, LIMIT) to be added after
328
	 *                               the conditions in the SQL statement
329
	 */
330
	protected function selectEntities(string $condition, string $extension=null) : string {
331
		return "SELECT * FROM `{$this->getTableName()}` WHERE $condition $extension ";
332
	}
333
334
	/**
335
	 * @return array with two values: The SQL condition as string and the SQL parameters as string[]
336
	 */
337
	protected function formatTimestampConditions(?string $createdMin, ?string $createdMax, ?string $updatedMin, ?string $updatedMax) : array {
338
		$conditions = [];
339
		$params = [];
340
341
		if (!empty($createdMin)) {
342
			$conditions[] = "`{$this->getTableName()}`.`created` >= ?";
343
			$params[] = $createdMin;
344
		}
345
346
		if (!empty($createdMax)) {
347
			$conditions[] = "`{$this->getTableName()}`.`created` <= ?";
348
			$params[] = $createdMax;
349
		}
350
351
		if (!empty($updatedMin)) {
352
			$conditions[] = "`{$this->getTableName()}`.`updated` >= ?";
353
			$params[] = $updatedMin;
354
		}
355
356
		if (!empty($updatedMax)) {
357
			$conditions[] = "`{$this->getTableName()}`.`updated` <= ?";
358
			$params[] = $updatedMax;
359
		}
360
361
		return [\implode(' AND ', $conditions), $params];
362
	}
363
364
	/**
365
	 * Convert given sorting condition to an SQL clause. Derived class may overide this if necessary.
366
	 * @param int $sortBy One of the constants defined in the class SortBy
367
	 */
368
	protected function formatSortingClause(int $sortBy) : ?string {
369
		if ($sortBy == SortBy::Name) {
370
			return "ORDER BY LOWER(`{$this->getTableName()}`.`{$this->nameColumn}`)";
371
		} elseif ($sortBy == SortBy::Newest) {
372
			return "ORDER BY `{$this->getTableName()}`.`id` DESC"; // abuse the fact that IDs are ever-incrementing values
373
		} else {
374
			return null;
375
		}
376
	}
377
378
	protected static function prepareFuzzySearchString(string $input) : string {
379
		// possibly multiparted query enclosed in quotation marks is handled as a single substing,
380
		// while the default interpretation of multipart string is that each of the parts can be found
381
		// separately as substing in the given order
382
		if (Util::startsWith($input, '"') && Util::endsWith($input, '"')) {
383
			// remove the quotation
384
			$pattern = \substr($input, 1, -1);
385
		} else {
386
			// split to parts by whitespace
387
			$parts = \preg_split('/\s+/', $input, -1, PREG_SPLIT_NO_EMPTY);
388
			// glue the parts back together with a wildcard charater
389
			$pattern = \implode('%', $parts);
390
		}
391
		return "%$pattern%";
392
	}
393
394
	/**
395
	 * Find an entity which has the same identity as the supplied entity.
396
	 * How the identity of the entity is defined, depends on the derived concrete class.
397
	 */
398
	abstract protected function findUniqueEntity(Entity $entity) : Entity;
399
}
400