BusinessLayer   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 89
c 0
b 0
f 0
dl 0
loc 347
rs 8.64
wmc 47

25 Methods

Rating   Name   Duplication   Size   Complexity  
A findAllAdvanced() 0 20 5
A findAllIds() 0 5 3
A findAllRated() 0 2 1
A findAllIdsAndNames() 0 16 4
A findAllIdsByParentIds() 0 5 2
A setStarred() 0 5 2
A unsetStarred() 0 5 2
A findAllUsers() 0 2 1
A findOrDefault() 0 5 2
A delete() 0 3 1
A count() 0 2 1
A findById() 0 21 5
A findAllStarred() 0 2 1
A setRating() 0 8 2
A find() 0 7 3
A maxId() 0 2 1
A findAll() 0 4 1
A findAllByName() 0 7 2
A deleteAll() 0 2 1
A findAllStarredIds() 0 2 1
A latestInsertTime() 0 2 1
A deleteById() 0 3 2
A exists() 0 2 1
A latestUpdateTime() 0 2 1
A __construct() 0 2 1

How to fix   Complexity   

Complex Class

Complex classes like BusinessLayer often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use BusinessLayer, and based on these observations, apply Extract Interface, too.

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