Passed
Push — master ( 7b42d0...d80bc5 )
by Pauli
04:15 queued 21s
created

BusinessLayer::findAllIdsAndNames()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
c 0
b 0
f 0
nc 4
nop 11
dl 0
loc 16
rs 9.9332

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
 * ownCloud
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Alessandro Cosentino <[email protected]>
9
 * @author Bernhard Posselt <[email protected]>
10
 * @author Pauli Järvinen <[email protected]>
11
 * @copyright Alessandro Cosentino 2012
12
 * @copyright Bernhard Posselt 2012, 2014
13
 * @copyright Pauli Järvinen 2017 - 2025
14
 */
15
16
namespace OCA\Music\AppFramework\BusinessLayer;
17
18
use OCA\Music\Db\BaseMapper;
19
use OCA\Music\Db\Entity;
20
use OCA\Music\Db\MatchMode;
21
use OCA\Music\Db\SortBy;
22
use OCA\Music\Utility\ArrayUtil;
23
use OCA\Music\Utility\Random;
24
25
use OCP\AppFramework\Db\DoesNotExistException;
26
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
27
use OCP\IL10N;
28
29
/**
30
 * @phpstan-template EntityType of Entity
31
 */
32
abstract class BusinessLayer {
33
	/** @phpstan-var BaseMapper<EntityType> */
34
	protected BaseMapper $mapper;
35
36
	// Some SQLite installations can't handle more than 999 query args. Remember that `user_id` takes one slot in most queries.
37
	public const MAX_SQL_ARGS = 999;
38
39
	/**
40
	 * @phpstan-param BaseMapper<EntityType> $mapper
41
	 */
42
	public function __construct(BaseMapper $mapper) {
43
		$this->mapper = $mapper;
44
	}
45
46
	/**
47
	 * Delete an entity
48
	 * @param int $id the id of the entity
49
	 * @param string $userId the name of the user for security reasons
50
	 * @throws BusinessLayerException if the entity does not exist or more than one entity exists
51
	 * @phpstan-return EntityType
52
	 */
53
	public function delete(int $id, string $userId) : Entity {
54
		$entity = $this->find($id, $userId);
55
		return $this->mapper->delete($entity);
56
	}
57
58
	/**
59
	 * Deletes entities without specifying the owning user.
60
	 * This should never be called directly from the HTML API, but only in case
61
	 * we can actually trust the passed IDs (e.g. file deleted hook).
62
	 * @param array $ids the ids of the entities which should be deleted
63
	 */
64
	public function deleteById(array $ids) : void {
65
		if (\count($ids) > 0) {
66
			$this->mapper->deleteById($ids);
67
		}
68
	}
69
70
	/**
71
	 * Delete all entities of the given user
72
	 */
73
	public function deleteAll(string $userId) : void {
74
		$this->mapper->deleteAll($userId);
75
	}
76
77
	/**
78
	 * Finds an entity by id
79
	 * @param int $id the id of the entity
80
	 * @param string $userId the name of the user for security reasons
81
	 * @throws BusinessLayerException if the entity does not exist or more than one entity exists
82
	 * @phpstan-return EntityType
83
	 */
84
	public function find(int $id, string $userId) : Entity {
85
		try {
86
			return $this->mapper->find($id, $userId);
87
		} catch (DoesNotExistException $ex) {
88
			throw new BusinessLayerException($ex->getMessage());
89
		} catch (MultipleObjectsReturnedException $ex) {
90
			throw new BusinessLayerException($ex->getMessage());
91
		}
92
	}
93
94
	/**
95
	 * Finds an entity by id, or returns an empty entity instance if the requested one is not found
96
	 * @param int $id the id of the entity
97
	 * @param string $userId the name of the user for security reasons
98
	 * @phpstan-return EntityType
99
	 */
100
	public function findOrDefault(int $id, string $userId) : Entity {
101
		try {
102
			return $this->find($id, $userId);
103
		} catch (BusinessLayerException $ex) {
104
			return $this->mapper->createEntity();
105
		}
106
	}
107
108
	/**
109
	 * Find all entities matching the given IDs.
110
	 * Specifying the user is optional; if omitted, the caller should make sure that
111
	 * user's data is not leaked to unauthorized users.
112
	 * @param integer[] $ids  IDs of the entities to be found
113
	 * @param string|null $userId
114
	 * @param bool $preserveOrder If true, then the result will be in the same order as @a $ids
115
	 * @return Entity[]
116
	 * @phpstan-return EntityType[]
117
	 */
118
	public function findById(array $ids, ?string $userId=null, bool $preserveOrder=false) : array {
119
		$entities = [];
120
		if (\count($ids) > 0) {
121
			// don't use more than 999 SQL args in one query since that may be a problem for SQLite
122
			$idChunks = \array_chunk($ids, 998);
123
			foreach ($idChunks as $idChunk) {
124
				$entities = \array_merge($entities, $this->mapper->findById($idChunk, $userId));
125
			}
126
		}
127
128
		if ($preserveOrder) {
129
			$lut = ArrayUtil::createIdLookupTable($entities);
130
			$result = [];
131
			foreach ($ids as $id) {
132
				$result[] = $lut[$id];
133
			}
134
		} else {
135
			$result = $entities;
136
		}
137
138
		return $result;
139
	}
140
141
	/**
142
	 * Finds all entities
143
	 * @param string $userId the name of the user
144
	 * @param integer $sortBy sort order of the result set
145
	 * @param integer|null $limit
146
	 * @param integer|null $offset
147
	 * @param string|null $createdMin Optional minimum `created` timestamp.
148
	 * @param string|null $createdMax Optional maximum `created` timestamp.
149
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
150
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
151
	 * @return Entity[]
152
	 * @phpstan-return EntityType[]
153
	 */
154
	public function findAll(
155
			string $userId, int $sortBy=SortBy::Name, ?int $limit=null, ?int $offset=null,
156
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
157
		return $this->mapper->findAll($userId, $sortBy, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
158
	}
159
160
	/**
161
	 * Return all entities with name matching the search criteria
162
	 * @param string|null $createdMin Optional minimum `created` timestamp.
163
	 * @param string|null $createdMax Optional maximum `created` timestamp.
164
	 * @param string|null $updatedMin Optional minimum `updated` timestamp.
165
	 * @param string|null $updatedMax Optional maximum `updated` timestamp.
166
	 * @return Entity[]
167
	 * @phpstan-return EntityType[]
168
	 */
169
	public function findAllByName(
170
			?string $name, string $userId, int $matchMode=MatchMode::Exact, ?int $limit=null, ?int $offset=null,
171
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null) : array {
172
		if ($name !== null) {
173
			$name = \trim($name);
174
		}
175
		return $this->mapper->findAllByName($name, $userId, $matchMode, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax);
176
	}
177
178
	/**
179
	 * Find all starred entities
180
	 * @return Entity[]
181
	 * @phpstan-return EntityType[]
182
	 */
183
	public function findAllStarred(string $userId, ?int $limit=null, ?int $offset=null) : array {
184
		return $this->mapper->findAllStarred($userId, $limit, $offset);
185
	}
186
187
	/**
188
	 * Find IDSs of all starred entities
189
	 * @return int[]
190
	 */
191
	public function findAllStarredIds(string $userId) : array {
192
		return $this->mapper->findAllStarredIds($userId);
193
	}
194
195
	/**
196
	 * Find all entities with user-given rating 1-5
197
	 * @return Entity[]
198
	 * @phpstan-return EntityType[]
199
	 */
200
	public function findAllRated(string $userId, ?int $limit=null, ?int $offset=null) : array {
201
		return $this->mapper->findAllRated($userId, $limit, $offset);
202
	}
203
204
	/**
205
	 * Find all entities matching multiple criteria, as needed for the Ampache API method `advanced_search`
206
	 * @param string $conjunction Operator to use between the rules, either 'and' or 'or'
207
	 * @param array $rules Array of arrays: [['rule' => string, 'operator' => string, 'input' => string], ...]
208
	 * 				Here, 'rule' has dozens of possible values depending on the business layer in question,
209
	 * 				(see https://ampache.org/api/api-advanced-search#available-search-rules, alias names not supported here),
210
	 * 				'operator' is one of 
211
	 * 				['contain', 'notcontain', 'start', 'end', 'is', 'isnot', 'sounds', 'notsounds', 'regexp', 'notregexp',
212
	 * 				 '>=', '<=', '=', '!=', '>', '<', 'before', 'after', 'true', 'false', 'equal', 'ne', 'limit'],
213
	 * 				'input' is the right side value of the 'operator' (disregarded for the operators 'true' and 'false')
214
	 * @param Random $random When the randomization utility is passed, the result set will be in random order (still supporting proper paging).
215
	 * 						 In this case, the argument $sortBy is ignored.
216
	 * @return Entity[]
217
	 * @phpstan-return EntityType[]
218
	 */
219
	public function findAllAdvanced(
220
			string $conjunction, array $rules, string $userId, int $sortBy=SortBy::Name,
221
			?Random $random=null, ?int $limit=null, ?int $offset=null) : array {
222
223
		if ($conjunction !== 'and' && $conjunction !== 'or') {
224
			throw new BusinessLayerException("Bad conjunction '$conjunction'");
225
		}
226
		try {
227
			if ($random !== null) {
228
				// in case the random order is requested, the limit/offset handling happens after the DB query
229
				$entities = $this->mapper->findAllAdvanced($conjunction, $rules, $userId, SortBy::Name);
230
				$indices = $random->getIndices(\count($entities), $offset, $limit, $userId, 'adv_search_'.$this->mapper->unprefixedTableName());
231
				$entities = ArrayUtil::multiGet($entities, $indices);
232
			} else {
233
				$entities = $this->mapper->findAllAdvanced($conjunction, $rules, $userId, $sortBy, $limit, $offset);
234
			}
235
			return $entities;
236
		} catch (\Exception $e) {
237
			// catch everything as many kinds of DB exceptions are possible on various cloud versions
238
			throw new BusinessLayerException($e->getMessage());
239
		}
240
	}
241
242
	/**
243
	 * Find IDs of all user's entities of this kind.
244
	 * Optionally, limit to given IDs which may be used to check the validity of those IDs.
245
	 * @return int[]
246
	 */
247
	public function findAllIds(string $userId, ?array $ids = null) : array {
248
		if ($ids === null || \count($ids) > 0) {
249
			return $this->mapper->findAllIds($userId, $ids);
250
		} else {
251
			return [];
252
		}
253
	}
254
255
	/**
256
	 * Find all entity IDs grouped by the given parent entity IDs. Not applicable on all entity types.
257
	 * @param int[] $parentIds
258
	 * @return array<int, int[]> like [parentId => childIds[]]; some parents may have an empty array of children
259
	 * @throws BusinessLayerException if the entity type handled by this business layer doesn't have a parent relation
260
	 */
261
	public function findAllIdsByParentIds(string $userId, array $parentIds) : ?array {
262
		try {
263
			return $this->mapper->findAllIdsByParentIds($userId, $parentIds);
264
		} catch (\DomainException $ex) {
265
			throw new BusinessLayerException($ex->getMessage());
266
		}
267
	}
268
269
	/**
270
	 * Find all IDs and names of user's entities of this kind.
271
	 * Optionally, limit results based on a parent entity (not applicable for all entity types) or update/insert times or name
272
	 * @param bool $excludeChildless Exclude entities having no child-entities if applicable for this business layer (eg. artists without albums)
273
	 * @return array of arrays like ['id' => string, 'name' => string]
274
	 */
275
	public function findAllIdsAndNames(string $userId, IL10N $l10n, ?int $parentId=null, ?int $limit=null, ?int $offset=null,
276
			?string $createdMin=null, ?string $createdMax=null, ?string $updatedMin=null, ?string $updatedMax=null,
277
			bool $excludeChildless=false, ?string $name=null) : array {
278
		try {
279
			$idsAndNames = $this->mapper->findAllIdsAndNames(
280
				$userId, $parentId, $limit, $offset, $createdMin, $createdMax, $updatedMin, $updatedMax, $excludeChildless, $name);
281
		} catch (\DomainException $ex) {
282
			throw new BusinessLayerException($ex->getMessage());
283
		}
284
		foreach ($idsAndNames as &$idAndName) {
285
			if (empty($idAndName['name'])) {
286
				$emptyEntity = $this->mapper->createEntity();
287
				$idAndName['name'] = $emptyEntity->getNameString($l10n);
288
			}
289
		}
290
		return $idsAndNames;
291
	}
292
293
	/**
294
	 * Find IDs of all users owning any entities of this business layer
295
	 * @return string[]
296
	 */
297
	public function findAllUsers() : array {
298
		return $this->mapper->findAllUsers();
299
	}
300
301
	/**
302
	 * Set the given entities as "starred" on this date
303
	 * @param int[] $ids
304
	 * @param string $userId
305
	 * @return int number of modified entities
306
	 */
307
	public function setStarred(array $ids, string $userId) : int {
308
		if (\count($ids) > 0) {
309
			return $this->mapper->setStarredDate(new \DateTime(), $ids, $userId);
310
		} else {
311
			return 0;
312
		}
313
	}
314
315
	/**
316
	 * Remove the "starred" status of the given entities
317
	 * @param integer[] $ids
318
	 * @param string $userId
319
	 * @return int number of modified entities
320
	 */
321
	public function unsetStarred(array $ids, string $userId) : int {
322
		if (\count($ids) > 0) {
323
			return $this->mapper->setStarredDate(null, $ids, $userId);
324
		} else {
325
			return 0;
326
		}
327
	}
328
329
	/**
330
	 * Set rating for the entity by id
331
	 * @throws BusinessLayerException if the entity does not exist or more than one entity exists
332
	 * @throws \BadMethodCallException if the entity type of this business layer doesn't support rating
333
	 * @phpstan-return EntityType
334
	 */
335
	public function setRating(int $id, int $rating, string $userId) : Entity {
336
		$entity = $this->find($id, $userId);
337
		if (\property_exists($entity, 'rating')) {
338
			// Scrutinizer and PHPStan don't understand the connection between the property 'rating' and the method 'setRating'
339
			$entity->/** @scrutinizer ignore-call */setRating($rating); // @phpstan-ignore method.notFound
340
			return $this->mapper->update($entity);
341
		} else {
342
			throw new \BadMethodCallException('rating not supported on the entity type ' . \get_class($entity));
343
		}
344
	}
345
346
	/**
347
	 * Tests if entity with given ID and user ID exists in the database
348
	 */
349
	public function exists(int $id, string $userId) : bool {
350
		return $this->mapper->exists($id, $userId);
351
	}
352
353
	/**
354
	 * Get the number of entities
355
	 */
356
	public function count(string $userId) : int {
357
		return $this->mapper->count($userId);
358
	}
359
360
	/**
361
	 * Get the largest entity ID of the user
362
	 */
363
	public function maxId(string $userId) : ?int {
364
		return $this->mapper->maxId($userId);
365
	}
366
367
	/**
368
	 * Get the timestamp of the latest insert operation on the entity type in question
369
	 */
370
	public function latestInsertTime(string $userId) : \DateTime {
371
		return $this->mapper->latestInsertTime($userId) ?? new \DateTime('1970-01-01');
372
	}
373
374
	/**
375
	 * Get the timestamp of the latest update operation on the entity type in question
376
	 */
377
	public function latestUpdateTime(string $userId) : \DateTime {
378
		return $this->mapper->latestUpdateTime($userId) ?? new \DateTime('1970-01-01');
379
	}
380
}
381