UserRepository   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 496
Duplicated Lines 0 %

Test Coverage

Coverage 11.59%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 278
c 4
b 0
f 0
dl 0
loc 496
ccs 24
cts 207
cp 0.1159
rs 7.92
wmc 51

31 Methods

Rating   Name   Duplication   Size   Complexity  
A createDefaultQueryBuilder() 0 8 1
A findOneByEmail() 0 7 1
A commit() 0 8 2
A beginTransaction() 0 3 1
A __construct() 0 2 1
A findAllActiveDirectoryUsersObjectGuid() 0 8 1
A findOneByUuid() 0 5 1
A findAll() 0 19 3
A findAllUuids() 0 13 2
A findAllNotInUsernamesList() 0 12 1
A findActiveDirectoryUserByObjectGuid() 0 3 1
A findAllStudentsWithoutParents() 0 16 1
A countUsers() 0 15 2
A removeDeletedUsers() 0 9 1
A findNextNonProvisionedUsers() 0 10 1
A findAllActiveDirectoryUsers() 0 8 1
A findParentUsersWithoutStudents() 0 16 1
C getPaginatedUsers() 0 88 11
A persist() 0 4 2
A findOneByUsername() 0 13 1
A findStudentsByGrade() 0 6 1
A remove() 0 4 2
A convertToActiveDirectory() 0 20 2
A findOneById() 0 5 1
A findUsersUpdatedAfter() 0 29 2
A findOneByExternalId() 0 5 1
A findAllExternalIdsByExternalIdList() 0 15 2
A rollBack() 0 2 1
A findGrades() 0 10 1
A findUsersByUsernames() 0 12 1
A convertToUser() 0 14 1

How to fix   Complexity   

Complex Class

Complex classes like UserRepository 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 UserRepository, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Repository;
4
5
use App\Entity\ActiveDirectoryUser;
6
use App\Entity\User;
7
use App\Entity\UserRole;
8
use App\Entity\UserType;
9
use DateTime;
10
use Doctrine\ORM\EntityManagerInterface;
11
use Doctrine\ORM\Query;
12
use Doctrine\ORM\QueryBuilder;
13
use Doctrine\ORM\Tools\Pagination\Paginator;
14
use Exception;
15
16
class UserRepository implements UserRepositoryInterface {
17
18
    private bool $isInTransaction = false;
19 14
20 14
    public function __construct(private EntityManagerInterface $em)
21 14
    {
22
    }
23
24
    public function beginTransaction(): void {
25
        $this->em->beginTransaction();
26
        $this->isInTransaction = true;
27
    }
28
29
    public function commit(): void {
30
        if(!$this->isInTransaction) {
31
            return;
32
        }
33
34
        $this->em->flush();
35
        $this->em->commit();
36
        $this->isInTransaction = false;
37
    }
38
39
    public function rollBack(): void {
40
        $this->em->rollback();
41
    }
42
43
    public function findAll(int $offset = 0, int $limit = null, bool $deleted = false): array {
44
        $qb = $this->em
45
            ->createQueryBuilder()
46
            ->select('u')
47
            ->from(User::class, 'u')
48
            ->orderBy('u.username', 'asc')
49
            ->setFirstResult($offset);
50
51
        if($deleted === true) {
52
            $qb->where($qb->expr()->isNotNull('u.deletedAt'));
53
        } else {
54
            $qb->where($qb->expr()->isNull('u.deletedAt'));
55
        }
56
57
        if($limit !== null) {
58
            $qb->setMaxResults($limit);
59
        }
60
61
        return $qb->getQuery()->getResult();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
62
    }
63
64
    public function findUsersByUsernames(array $usernames): array {
65
        $qb = $this->em->createQueryBuilder();
66
67
        $qb->select(['u', 'a', 'r', 't'])
68
            ->from(User::class, 'u')
69
            ->leftJoin('u.attributes', 'a')
70
            ->leftJoin('u.userRoles', 'r')
71
            ->leftJoin('u.type', 't')
72
            ->where('u.username IN (:usernames)')
73
            ->setParameter('usernames', $usernames);
74
75
        return $qb->getQuery()->getResult();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
76
    }
77
78
    public function findUsersUpdatedAfter(DateTime $dateTime, array $usernames = [ ]): array {
79
        $qb = $this->em
80
            ->createQueryBuilder();
81
82
        $qb->select(['DISTINCT u.username'])
83
            ->from(User::class, 'u')
84
            ->leftJoin('u.attributes', 'a')
85
            ->where(
86
                $qb->expr()->orX(
87
                    $qb->expr()->andX(
88
                        $qb->expr()->isNotNull('u.updatedAt'),
89
                        $qb->expr()->gt('u.updatedAt', ':datetime')
90
                    ),
91
                    $qb->expr()->andX(
92
                        $qb->expr()->isNotNull('a.updatedAt'),
93
                        $qb->expr()->gt('a.updatedAt', ':datetime')
94
                    )
95
                )
96
            )
97
            ->setParameter('datetime', $dateTime);
98
99
        if(count($usernames) > 0) {
100
            $qb->andWhere('u.username IN (:usernames)')
101
                ->setParameter('usernames', $usernames);
102
        }
103
104 1
        $usernames = $qb->getQuery()->getScalarResult();
105 1
106
        return $this->findUsersByUsernames($usernames);
107 1
    }
108 1
109 1
    public function findOneByUsername(string $username): ?User {
110 1
        $qb = $this->em->createQueryBuilder();
111 1
112 1
        $qb->select(['u', 'a', 'r', 't'])
113 1
            ->from(User::class, 'u')
114
            ->leftJoin('u.attributes', 'a')
115 1
            ->leftJoin('u.userRoles', 'r')
116
            ->leftJoin('u.type', 't')
117 1
            ->where('u.username = :username')
118
            ->setParameter('username', $username)
119
            ->setMaxResults(1);
120
121 1
        return $qb->getQuery()->getOneOrNullResult();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->getOneOrNullResult() could return the type integer which is incompatible with the type-hinted return App\Entity\User|null. Consider adding an additional type-check to rule them out.
Loading history...
122
    }
123
124
    private function createDefaultQueryBuilder(): QueryBuilder {
125
        return $this->em
126
            ->createQueryBuilder()
127
            ->select(['u', 'a', 'r', 't'])
128
            ->from(User::class, 'u')
129
            ->leftJoin('u.attributes', 'a')
130
            ->leftJoin('u.userRoles', 'r')
131
            ->leftJoin('u.type', 't');
132
    }
133
134
    /**
135
     * @inheritDoc
136
     */
137
    public function findOneByEmail(string $email): ?User {
138
        return $this->createDefaultQueryBuilder()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->createDefa...)->getOneOrNullResult() could return the type integer which is incompatible with the type-hinted return App\Entity\User|null. Consider adding an additional type-check to rule them out.
Loading history...
139
            ->where('u.email = :email')
140
            ->setParameter('email', $email)
141
            ->setMaxResults(1)
142
            ->getQuery()
143
            ->getOneOrNullResult();
144
    }
145
146 1
    public function findAllNotInUsernamesList(array $usernames, UserType $userType): array {
147 1
        $qb = $this->em->createQueryBuilder();
148 1
149 1
        $qb->select(['u'])
150
            ->from(User::class, 'u')
151
            ->leftJoin('u.type', 't')
152
            ->where('t.id = :type')
153
            ->andWhere($qb->expr()->notIn('u.username', ':usernames'))
154
            ->setParameter('usernames', $usernames)
155
            ->setParameter('type', $userType->getId());
156
157
        return $qb->getQuery()->getResult();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
158
    }
159
160
    public function persist(User $user): void {
161
        $this->em->persist($user);
162
        if($this->isInTransaction === false) {
163
            $this->em->flush();
164
        }
165
    }
166
167
    public function remove(User $user): void {
168
        $this->em->remove($user);
169
        if($this->isInTransaction === false) {
170
            $this->em->flush();
171
        }
172
    }
173
174
    /**
175
     * @inheritDoc
176
     */
177
    public function getPaginatedUsers(int $itemsPerPage, int &$page, ?UserType $type = null, ?UserRole $role = null, ?string $query = null, ?string $grade = null, bool $deleted = false, bool $onlyNotLinked = false): Paginator {
178
        $qb = $this->em
179
            ->createQueryBuilder()
180
            ->select('u')
181
            ->from(User::class, 'u')
182
            ->orderBy('u.username', 'asc');
183
184
        $qbInner = $this->em
185
            ->createQueryBuilder()
186
            ->select('DISTINCT uInner.id')
187
            ->from(User::class, 'uInner')
188
            ->leftJoin('uInner.userRoles', 'rInner')
189
            ->leftJoin('uInner.linkedStudents', 'sInner');
190
191
        if(!empty($grade)) {
192
            $qbInner
193
                ->andWhere(
194
                    $qbInner->expr()->orX(
195
                        'uInner.grade = :grade',
196
                        'sInner.grade = :grade'
197
                    )
198
                );
199
            $qb->setParameter('grade', $grade);
200
        }
201
202
        if(!empty($query)) {
203
            $qbInner
204
                ->andWhere(
205
                    $qb->expr()->orX(
206
                        'uInner.username LIKE :query',
207
                        'uInner.firstname LIKE :query',
208
                        'uInner.lastname LIKE :query',
209
                        'uInner.email LIKE :query'
210
                    )
211
                );
212
            $qb->setParameter('query', '%' . $query . '%');
213
        }
214
215
        if($type !== null) {
216
            $qbInner
217
                ->andWhere(
218
                    'u.type = :type'
219
                );
220
            $qb->setParameter('type', $type);
221
        }
222
223
        if($role !== null) {
224
            $qbInner->andWhere(
225
                'rInner.id = :role'
226
            );
227
            $qb->setParameter('role', $role);
228
        }
229
230
        if($deleted === true) {
231
            $qbInner->andWhere($qb->expr()->isNotNull('u.deletedAt'));
232
        } else {
233
            $qbInner->andWhere($qb->expr()->isNull('u.deletedAt'));
234
        }
235
236
        if(!is_numeric($page) || $page < 1) {
0 ignored issues
show
introduced by
The condition is_numeric($page) is always true.
Loading history...
237
            $page = 1;
238
        }
239
240
        $qb->where(
241
            $qb->expr()->in('u.id', $qbInner->getDQL())
242
        );
243
244
        if($type !== null && $type->getAlias() === 'student' && $onlyNotLinked === true) {
245
            $qb->andWhere(
246
                $qb->expr()->in('u.id',
247
                    $this->em->createQueryBuilder()
248
                        ->select('uStudentInner.id')
249
                        ->from(User::class, 'uStudentInner')
250
                        ->leftJoin('uStudentInner.parents', 'pStudentInner')
251
                        ->where('pStudentInner.id IS NULL')
252
                        ->getDQL()
253
                )
254
            );
255
        }
256
257
        $offset = ($page - 1) * $itemsPerPage;
258
259
        $paginator = new Paginator($qb);
260
        $paginator->getQuery()
261
            ->setMaxResults($itemsPerPage)
262
            ->setFirstResult($offset);
263
264
        return $paginator;
265
    }
266
267
268 5
    /**
269 5
     * @inheritDoc
270 5
     */
271 5
    public function findActiveDirectoryUserByObjectGuid(string $guid): ?ActiveDirectoryUser {
272 5
        return $this->em->getRepository(ActiveDirectoryUser::class)
273
            ->findOneBy(['objectGuid' => $guid]);
274
    }
275
276
    /**
277
     * @inheritDoc
278
     */
279
    public function findAllActiveDirectoryUsersObjectGuid(): array {
280
        return array_map(fn(array $item) => $item['objectGuid'],
281
            $this->em->createQueryBuilder()
282
                ->select('u.objectGuid')
283
                ->from(ActiveDirectoryUser::class, 'u')
284
                ->where('u.deletedAt IS NULL')
285
                ->getQuery()
286
                ->getScalarResult()
287
        );
288
    }
289
290
    public function findAllActiveDirectoryUsers(): array {
291
        return $this->em
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->em->create...getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
292
            ->createQueryBuilder()
293
            ->select('u')
294
            ->from(ActiveDirectoryUser::class, 'u')
295
            ->where('u.deletedAt IS NULL')
296
            ->getQuery()
297
            ->getResult();
298
    }
299
300
    /**
301
     * @inheritDoc
302
     */
303
    public function findAllUuids(int $offset = 0, ?int $limit = null): array {
304
        $qb = $this->em
305
            ->createQueryBuilder()
306
            ->select('u.uuid')
307
            ->from(User::class, 'u')
308
            ->orderBy('u.username', 'asc')
309
            ->setFirstResult($offset);
310
311
        if($limit !== null) {
312
            $qb->setMaxResults($limit);
313
        }
314
315
        return array_map(fn(array $item) => $item['uuid'], $qb->getQuery()->getScalarResult());
316
    }
317
318
    public function findOneByExternalId(string $externalId): ?User {
319
        return $this->em
320
            ->getRepository(User::class)
321
            ->findOneBy([
322
                'externalId' => $externalId
323
            ]);
324
    }
325
326
    public function findOneByUuid(string $uuid): ?User {
327
        return $this->em
328
            ->getRepository(User::class)
329
            ->findOneBy([
330
                'uuid' => $uuid
331
            ]);
332
    }
333
334
    public function findOneById(int $id): ?User {
335
        return $this->em
336
            ->getRepository(User::class)
337
            ->findOneBy([
338
                'id' => $id
339
            ]);
340
    }
341
342
    /**
343
     * @inheritDoc
344
     */
345
    public function findNextNonProvisionedUsers(int $limit): array {
346
        return $this->em
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->em->create...getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
347
            ->createQueryBuilder()
348
            ->select('u')
349
            ->from(User::class, 'u')
350
            ->orderBy('u.createdAt', 'asc')
351
            ->where('u.isProvisioned = false')
352
            ->setMaxResults($limit)
353
            ->getQuery()
354
            ->getResult();
355
    }
356
357
    /**
358
     * @inheritDoc
359
     */
360
    public function countUsers(?UserType $userType = null): int {
361
        $qb = $this->em->createQueryBuilder();
362
363
        $qb
364
            ->select('COUNT(u.id)')
365
            ->from(User::class, 'u')
366
            ->where($qb->expr()->isNull('u.deletedAt'));
367
368
        if($userType !== null) {
369
            $qb->andWhere('u.type = :type')
370
                ->setParameter('type', $userType);
371
        }
372
373
        return $qb->getQuery()
374
            ->getSingleScalarResult();
375
    }
376
377
    /**
378
     * @inheritDoc
379
     */
380
    public function findAllExternalIdsByExternalIdList(array $externalIds): array {
381
        if(count($externalIds) === 0) {
382
            return [];
383
        }
384
385
        $qb = $this->em->createQueryBuilder();
386
        $qb
387
            ->select('u.externalId')
388
            ->from(User::class, 'u')
389
            ->where($qb->expr()->in('u.externalId', ':ids'))
390
            ->setParameter('ids', $externalIds);
391
392
        $result = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
393
394
        return array_map(fn($row) => $row['externalId'], $result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type integer; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

394
        return array_map(fn($row) => $row['externalId'], /** @scrutinizer ignore-type */ $result);
Loading history...
395
    }
396
397
    /**
398
     * @inheritDoc
399
     */
400
    public function removeDeletedUsers(DateTime $threshold): int {
401
        $qb = $this->em->createQueryBuilder();
402
403
        $qb->delete(User::class, 'u')
404
            ->where($qb->expr()->isNotNull('u.deletedAt'))
405
            ->andWhere('u.deletedAt < :threshold')
406
            ->setParameter('threshold', $threshold);
407
408
        return $qb->getQuery()->execute();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->execute() could return the type array<mixed,mixed> which is incompatible with the type-hinted return integer. Consider adding an additional type-check to rule them out.
Loading history...
409
    }
410
411
    public function findParentUsersWithoutStudents(): array {
412
        $qbInner = $this->em->createQueryBuilder()
413
            ->select('uInner.id')
414
            ->from(User::class, 'uInner')
415
            ->leftJoin('uInner.type', 'tInner')
416
            ->leftJoin('uInner.linkedStudents', 'sInner')
417
            ->where("tInner.alias = 'parent'")
418
            ->andWhere('sInner.id IS NULL');
419
420
        $qb = $this->createDefaultQueryBuilder();
421
422
        $qb->where(
423
            $qb->expr()->in('u.id', $qbInner->getDQL())
424
        );
425
426
        return $qb->getQuery()->getResult();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
427
    }
428
429
    /**
430
     * @inheritDoc
431
     */
432
    public function findAllStudentsWithoutParents(): array {
433
        $qbInner = $this->em->createQueryBuilder()
434
            ->select('uInner.id')
435
            ->from(User::class, 'uInner')
436
            ->leftJoin('uInner.parents', 'pInner')
437
            ->leftJoin('uInner.type', 'tInner')
438
            ->where('pInner.id IS NULL')
439
            ->andWhere("tInner.alias = 'student'");
440
441
        $qb = $this->createDefaultQueryBuilder();
442
443
        $qb->where(
444
            $qb->expr()->in('u.id', $qbInner->getDQL())
445
        );
446
447
        return $qb->getQuery()->getResult();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $qb->getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
448
    }
449
450
    public function findGrades(): array {
451
        $result = $this->em->createQueryBuilder()
452
            ->select('DISTINCT u.grade')
453
            ->from(User::class, 'u')
454
            ->orderBy('u.grade', 'asc')
455
            ->where('u.grade IS NOT NULL')
456
            ->getQuery()
457
            ->getResult(Query::HYDRATE_ARRAY);
458
459
        return array_map(fn($row) => $row['grade'], $result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type integer; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

459
        return array_map(fn($row) => $row['grade'], /** @scrutinizer ignore-type */ $result);
Loading history...
460
    }
461
462
    /**
463
     * @inheritDoc
464
     */
465
    public function findStudentsByGrade(string $grade): array {
466
        return $this->createDefaultQueryBuilder()
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->createDefa...getQuery()->getResult() could return the type integer which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
467
            ->andWhere('u.grade = :grade')
468
            ->setParameter('grade', $grade)
469
            ->getQuery()
470
            ->getResult();
471
    }
472
473
    /**
474
     * @throws Exception
475
     */
476
    public function convertToActiveDirectory(User $user, ActiveDirectoryUser $activeDirectoryUser): ActiveDirectoryUser {
477
        $dbal = $this->em->getConnection();
478
        $dbal->update('user', [
479
            'user_principal_name' => $activeDirectoryUser->getUserPrincipalName(),
480
            'object_guid' => $activeDirectoryUser->getObjectGuid(),
481
            'ou' => $activeDirectoryUser->getOu(),
482
            'groups' => json_encode($activeDirectoryUser->getGroups(), JSON_THROW_ON_ERROR),
483
            'class' => 'ad'
484
        ], [
485
            'id' => $user->getId()
486
        ]);
487
488
        $this->em->detach($user);
489
        $adUser = $this->findOneById($user->getId());
0 ignored issues
show
Bug introduced by
It seems like $user->getId() can also be of type null; however, parameter $id of App\Repository\UserRepository::findOneById() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

489
        $adUser = $this->findOneById(/** @scrutinizer ignore-type */ $user->getId());
Loading history...
490
491
        if(!$adUser instanceof ActiveDirectoryUser) {
492
            throw new Exception('Failed to convert user.');
493
        }
494
495
        return $adUser;
496
    }
497
498
    public function convertToUser(ActiveDirectoryUser $user): User {
499
        $dbal = $this->em->getConnection();
500
        $dbal->update('user', [
501
            'user_principal_name' => null,
502
            'object_guid' => null,
503
            'ou' => null,
504
            'groups' => null,
505
            'class' => 'user'
506
        ], [
507
            'id' => $user->getId()
508
        ]);
509
510
        $this->em->detach($user);
511
        return $this->findOneById($user->getId());
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->findOneById($user->getId()) could return the type null which is incompatible with the type-hinted return App\Entity\User. Consider adding an additional type-check to rule them out.
Loading history...
Bug introduced by
It seems like $user->getId() can also be of type null; however, parameter $id of App\Repository\UserRepository::findOneById() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

511
        return $this->findOneById(/** @scrutinizer ignore-type */ $user->getId());
Loading history...
512
    }
513
514
515
}