Passed
Push — master ( 6aff41...cf050b )
by Marcel
10:24
created

UserRepository::getPaginatedUsers()   C

Complexity

Conditions 11
Paths 128

Size

Total Lines 88
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 11
eloc 59
c 1
b 0
f 0
nc 128
nop 8
dl 0
loc 88
ccs 0
cts 46
cp 0
crap 132
rs 6.5612

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
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
}