• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

daycry / auth / 25552752016

08 May 2026 11:20AM UTC coverage: 71.592% (+13.0%) from 58.608%
25552752016

push

github

web-flow
Merge pull request #48 from daycry/development

Add Laravel-parity authentication features: Gates, Password Confirmation, Basic Auth

198 of 252 new or added lines in 18 files covered. (78.57%)

1 existing line in 1 file now uncovered.

4453 of 6220 relevant lines covered (71.59%)

62.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.0
/src/Traits/Authorizable.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Traits;
15

16
use CodeIgniter\Exceptions\LogicException;
17
use CodeIgniter\I18n\Time;
18
use Config\Database;
19
use Daycry\Auth\Entities\Group;
20
use Daycry\Auth\Exceptions\AuthorizationException;
21
use Daycry\Auth\Models\GroupModel;
22
use Daycry\Auth\Models\GroupUserModel;
23
use Daycry\Auth\Models\PermissionGroupModel;
24
use Daycry\Auth\Models\PermissionModel;
25
use Daycry\Auth\Models\PermissionUserModel;
26
use Daycry\Auth\Services\AuditLogger;
27

28
trait Authorizable
29
{
30
    protected ?array $groupCache       = null;
31
    protected ?array $permissionsCache = null;
32
    protected ?array $groups           = null;
33
    protected ?array $permissions      = null;
34

35
    /**
36
     * @var array<string, list<string>>|null Group name => list of permission names
37
     */
38
    protected ?array $groupPermissionsCache = null;
39

40
    private function groupModel(): GroupModel
49✔
41
    {
42
        /** @var GroupModel */
43
        return model(GroupModel::class);
49✔
44
    }
45

46
    private function groupUserModel(): GroupUserModel
49✔
47
    {
48
        /** @var GroupUserModel */
49
        return model(GroupUserModel::class);
49✔
50
    }
51

52
    private function permissionModel(): PermissionModel
30✔
53
    {
54
        /** @var PermissionModel */
55
        return model(PermissionModel::class);
30✔
56
    }
57

58
    private function permissionUserModel(): PermissionUserModel
30✔
59
    {
60
        /** @var PermissionUserModel */
61
        return model(PermissionUserModel::class);
30✔
62
    }
63

64
    private function permissionGroupModel(): PermissionGroupModel
24✔
65
    {
66
        /** @var PermissionGroupModel */
67
        return model(PermissionGroupModel::class);
24✔
68
    }
69

70
    /**
71
     * Adds one or more groups to the current User.
72
     *
73
     * @return $this
74
     */
75
    public function addGroup(string ...$groups): self
28✔
76
    {
77
        $this->populateGroups();
28✔
78

79
        $groupCount = count($this->groupCache);
28✔
80
        $added      = [];
28✔
81

82
        foreach ($groups as $group) {
28✔
83
            $group = strtolower($group);
28✔
84

85
            // don't allow dupes
86
            if (in_array($group, $this->groupCache, true)) {
28✔
87
                continue;
2✔
88
            }
89

90
            $groupsNames = ($this->groups) ? array_values($this->groups) : [];
27✔
91

92
            // make sure it's a valid group
93
            if (! in_array($group, $groupsNames, true)) {
27✔
94
                throw AuthorizationException::forUnknownGroup($group);
2✔
95
            }
96

97
            $this->groupCache[] = $group;
26✔
98
            $added[]            = $group;
26✔
99
        }
100

101
        // Only save the results if there's anything new.
102
        if (count($this->groupCache) > $groupCount) {
26✔
103
            $this->saveGroups();
25✔
104

105
            (new AuditLogger())->record(
25✔
106
                AuditLogger::EVENT_GROUP_ASSIGNED,
25✔
107
                (int) $this->id,
25✔
108
                ['groups' => $added],
25✔
109
            );
25✔
110
        }
111

112
        return $this;
26✔
113
    }
114

115
    /**
116
     * Removes one or more groups from the user.
117
     *
118
     * @return $this
119
     */
120
    public function removeGroup(string ...$groups): self
5✔
121
    {
122
        $this->populateGroups();
5✔
123

124
        foreach ($groups as &$group) {
5✔
125
            $group = strtolower($group);
5✔
126
        }
127
        unset($group);
5✔
128

129
        $removed = array_values(array_intersect($this->groupCache, $groups));
5✔
130

131
        // Remove from local cache
132
        $this->groupCache = array_diff($this->groupCache, $groups);
5✔
133

134
        // Update the database.
135
        $this->saveGroups();
5✔
136

137
        if ($removed !== []) {
5✔
138
            (new AuditLogger())->record(
4✔
139
                AuditLogger::EVENT_GROUP_REVOKED,
4✔
140
                (int) $this->id,
4✔
141
                ['groups' => $removed],
4✔
142
            );
4✔
143
        }
144

145
        return $this;
5✔
146
    }
147

148
    /**
149
     * Given an array of groups, will update the database
150
     * so only those groups are valid for this user, removing
151
     * all groups not in this list.
152
     *
153
     * @return $this
154
     *
155
     * @throws AuthorizationException
156
     */
157
    public function syncGroups(string ...$groups): self
2✔
158
    {
159
        $this->populateGroups();
2✔
160

161
        foreach ($groups as $group) {
2✔
162
            if (! in_array($group, array_values($this->groups), true)) {
2✔
163
                throw AuthorizationException::forUnknownGroup($group);
1✔
164
            }
165
        }
166

167
        $this->groupCache = $groups;
1✔
168
        $this->saveGroups();
1✔
169

170
        return $this;
1✔
171
    }
172

173
    /**
174
     * Returns all groups this user is a part of.
175
     */
176
    public function getGroups(): ?array
7✔
177
    {
178
        $this->populateGroups();
7✔
179

180
        return array_values($this->groupCache);
7✔
181
    }
182

183
    /**
184
     * Returns all permissions this user has
185
     * assigned directly to them.
186
     */
187
    public function getPermissions(): ?array
12✔
188
    {
189
        $this->populatePermissions();
12✔
190

191
        return array_values($this->permissionsCache);
12✔
192
    }
193

194
    /**
195
     * Adds one or more permissions to the current user.
196
     *
197
     * @return $this
198
     *
199
     * @throws AuthorizationException
200
     */
201
    public function addPermission(string ...$permissions): self
17✔
202
    {
203
        $this->populatePermissions();
17✔
204

205
        $permissionCount = count($this->permissionsCache);
17✔
206
        $added           = [];
17✔
207

208
        foreach ($permissions as $permission) {
17✔
209
            $permission = strtolower($permission);
17✔
210

211
            // don't allow dupes
212
            if (in_array($permission, $this->permissionsCache, true)) {
17✔
213
                continue;
3✔
214
            }
215

216
            $permissionsNames = ($this->permissions) ? array_values($this->permissions) : [];
15✔
217

218
            // make sure it's a valid group
219
            if (! in_array($permission, $permissionsNames, true)) {
15✔
220
                throw AuthorizationException::forUnknownPermission($permission);
2✔
221
            }
222

223
            $this->permissionsCache[] = $permission;
13✔
224
            $added[]                  = $permission;
13✔
225
        }
226

227
        // Only save the results if there's anything new.
228
        if (count($this->permissionsCache) > $permissionCount) {
15✔
229
            $this->savePermissions();
13✔
230

231
            (new AuditLogger())->record(
13✔
232
                AuditLogger::EVENT_PERMISSION_GRANTED,
13✔
233
                (int) $this->id,
13✔
234
                ['permissions' => $added],
13✔
235
            );
13✔
236
        }
237

238
        return $this;
15✔
239
    }
240

241
    /**
242
     * Removes one or more permissions from the current user.
243
     *
244
     * @return $this
245
     */
246
    public function removePermission(string ...$permissions): self
5✔
247
    {
248
        $this->populatePermissions();
5✔
249

250
        foreach ($permissions as &$permission) {
5✔
251
            $permission = strtolower($permission);
5✔
252
        }
253
        unset($permission);
5✔
254

255
        $removed = array_values(array_intersect($this->permissionsCache, $permissions));
5✔
256

257
        // Remove from local cache
258
        $this->permissionsCache = array_diff($this->permissionsCache, $permissions);
5✔
259

260
        // Update the database.
261
        $this->savePermissions();
5✔
262

263
        if ($removed !== []) {
5✔
264
            (new AuditLogger())->record(
4✔
265
                AuditLogger::EVENT_PERMISSION_REVOKED,
4✔
266
                (int) $this->id,
4✔
267
                ['permissions' => $removed],
4✔
268
            );
4✔
269
        }
270

271
        return $this;
5✔
272
    }
273

274
    /**
275
     * Given an array of permissions, will update the database
276
     * so only those permissions are valid for this user, removing
277
     * all permissions not in this list.
278
     *
279
     * @return $this
280
     *
281
     * @throws AuthorizationException
282
     */
283
    public function syncPermissions(string ...$permissions): self
2✔
284
    {
285
        $this->populatePermissions();
2✔
286

287
        foreach ($permissions as $permission) {
2✔
288
            if (! in_array($permission, array_values($this->permissions), true)) {
2✔
289
                throw AuthorizationException::forUnknownPermission($permission);
1✔
290
            }
291
        }
292

293
        $this->permissionsCache = $permissions;
1✔
294
        $this->savePermissions();
1✔
295

296
        return $this;
1✔
297
    }
298

299
    /**
300
     * Checks to see if the user has the permission set
301
     * directly on themselves. This disregards any groups
302
     * they are part of.
303
     */
304
    public function hasPermission(string $permission): bool
5✔
305
    {
306
        $this->populatePermissions();
5✔
307

308
        $permission = strtolower($permission);
5✔
309

310
        return in_array($permission, $this->permissionsCache, true);
5✔
311
    }
312

313
    /**
314
     * Checks user permissions and their group permissions
315
     * to see if the user has a specific permission or group
316
     * of permissions.
317
     *
318
     * @param string $permissions string(s) consisting of a scope and action, like `users.create`
319
     */
320
    public function can(string ...$permissions): bool
16✔
321
    {
322
        // Get user's permissions and store in cache
323
        $this->populatePermissions();
16✔
324

325
        // Check the groups the user belongs to
326
        $this->populateGroups();
16✔
327

328
        foreach ($permissions as $permission) {
16✔
329
            // Permission must contain a scope and action
330
            if (! str_contains($permission, '.')) {
16✔
331
                throw new LogicException(
2✔
332
                    'A permission must be a string consisting of a scope and action, like `users.create`.'
2✔
333
                    . ' Invalid permission: ' . $permission,
2✔
334
                );
2✔
335
            }
336

337
            $permission = strtolower($permission);
14✔
338

339
            if (in_array('*', $this->permissionsCache, true)) {
14✔
340
                return true;
×
341
            }
342

343
            // Check user's permissions
344
            if (in_array($permission, $this->permissionsCache, true)) {
14✔
345
                return true;
7✔
346
            }
347

348
            if (count($this->groupCache) === 0) {
12✔
349
                return false;
8✔
350
            }
351

352
            foreach ($this->groupCache as $groupName) {
5✔
353
                $groupPermNames = $this->groupPermissionsCache[$groupName] ?? [];
5✔
354

355
                if (in_array('*', $groupPermNames, true)) {
5✔
356
                    return true;
×
357
                }
358

359
                // Check exact match
360
                if (in_array($permission, $groupPermNames, true)) {
5✔
361
                    return true;
3✔
362
                }
363

364
                // Check wildcard match (e.g. 'users.*' covers 'users.edit')
365
                $check = substr($permission, 0, strpos($permission, '.')) . '.*';
4✔
366
                if (in_array($check, $groupPermNames, true)) {
4✔
367
                    return true;
4✔
368
                }
369
            }
370
        }
371

372
        return false;
2✔
373
    }
374

375
    /**
376
     * Checks an ability registered with the Gate (closure or policy).
377
     *
378
     * Sister method to {@see can()} — `can()` handles RBAC permission
379
     * strings ("users.create"), `canDo()` handles abilities backed by
380
     * a closure or a Policy class and may receive resource arguments:
381
     *
382
     *     $user->canDo('post.update', $post)
383
     *
384
     * @param mixed ...$arguments Forwarded to the resolved rule.
385
     */
NEW
386
    public function canDo(string $ability, ...$arguments): bool
×
387
    {
NEW
388
        return service('gate')->forUser($this)->allows($ability, ...$arguments);
×
389
    }
390

391
    /**
392
     * Negation of {@see canDo()}.
393
     *
394
     * @param mixed ...$arguments
395
     */
NEW
396
    public function cantDo(string $ability, ...$arguments): bool
×
397
    {
NEW
398
        return ! $this->canDo($ability, ...$arguments);
×
399
    }
400

401
    /**
402
     * Checks to see if the user is a member of one
403
     * of the groups passed in.
404
     */
405
    public function inGroup(string ...$groups): bool
15✔
406
    {
407
        $this->populateGroups();
15✔
408

409
        foreach ($groups as $group) {
15✔
410
            if (in_array(strtolower($group), $this->groupCache, true)) {
15✔
411
                return true;
10✔
412
            }
413
        }
414

415
        return false;
10✔
416
    }
417

418
    /**
419
     * User for populate all groups
420
     */
421
    private function getAllUserGroups(): array
49✔
422
    {
423
        $userGroupModel = $this->groupUserModel();
49✔
424
        $userGroups     = $userGroupModel->getForUser($this);
49✔
425

426
        $ids = [];
49✔
427

428
        foreach ($userGroups as $userGroup) {
49✔
429
            $ids[] = $userGroup->group_id;
24✔
430
        }
431

432
        $groupModel = $this->groupModel();
49✔
433

434
        if ($ids !== []) {
49✔
435
            return $groupModel->getByIds($ids);
24✔
436
        }
437

438
        return [];
42✔
439
    }
440

441
    /**
442
     * User for populate all permissions
443
     */
444
    private function getAllUserPermissions(): array
30✔
445
    {
446
        $userPermissionsModel = $this->permissionUserModel();
30✔
447
        $userPermissions      = $userPermissionsModel->getForUser($this);
30✔
448

449
        $ids = [];
30✔
450

451
        foreach ($userPermissions as $userPermission) {
30✔
452
            $ids[] = $userPermission->permission_id;
15✔
453
        }
454

455
        $permissionModel = $this->permissionModel();
30✔
456

457
        if ($ids !== []) {
30✔
458
            return $permissionModel->getByIds($ids);
15✔
459
        }
460

461
        return [];
21✔
462
    }
463

464
    /**
465
     * User for populate all permissions
466
     */
467
    private function getGroupPermissions(Group $group): array
×
468
    {
469
        $groupPermissionsModel = $this->permissionGroupModel();
×
470
        $groupPermissions      = $groupPermissionsModel->getForGroup($group);
×
471

472
        $ids = [];
×
473

474
        foreach ($groupPermissions as $groupPermission) {
×
475
            $ids[] = $groupPermission->permission_id;
×
476
        }
477

478
        $permissionModel = $this->permissionModel();
×
479

480
        if ($ids !== []) {
×
481
            return $permissionModel->getByIds($ids);
×
482
        }
483

484
        return [];
×
485
    }
486

487
    /**
488
     * Loads all permissions for all of the user's groups in two queries
489
     * (one for permission_group rows, one for permission names) and
490
     * populates $groupPermissionsCache. This eliminates the N+1 problem
491
     * where can() previously called getGroupPermissions() per group.
492
     */
493
    private function eagerLoadGroupPermissions(): void
24✔
494
    {
495
        // Resolve group IDs from group names
496
        $groupIds = [];
24✔
497

498
        foreach ($this->groupCache as $groupName) {
24✔
499
            $id = array_search($groupName, $this->groups, true);
24✔
500

501
            if ($id !== false) {
24✔
502
                $groupIds[] = $id;
24✔
503
            }
504
        }
505

506
        if ($groupIds === []) {
24✔
507
            return;
×
508
        }
509

510
        // Single query: get all permission_group rows for all user groups (respecting until_at)
511
        $groupPermModel = $this->permissionGroupModel();
24✔
512
        $now            = Time::now()->format('Y-m-d H:i:s');
24✔
513

514
        $allGroupPerms = $groupPermModel
24✔
515
            ->whereIn('group_id', $groupIds)
24✔
516
            ->groupStart()
24✔
517
            ->where('until_at')
24✔
518
            ->orWhere('until_at >', $now)
24✔
519
            ->groupEnd()
24✔
520
            ->findAll();
24✔
521

522
        // Build a map of group_id => [permission_id, ...] and collect all permission IDs
523
        $permIds      = [];
24✔
524
        $groupPermMap = [];
24✔
525

526
        foreach ($allGroupPerms as $gp) {
24✔
527
            $pid                  = (int) $gp->permission_id;
5✔
528
            $gid                  = (int) $gp->group_id;
5✔
529
            $permIds[]            = $pid;
5✔
530
            $groupPermMap[$gid][] = $pid;
5✔
531
        }
532

533
        // Single query: get all permission names by IDs
534
        $permNames = [];
24✔
535

536
        if ($permIds !== []) {
24✔
537
            $permModel = $this->permissionModel();
5✔
538
            $perms     = $permModel->getByIds(array_unique($permIds));
5✔
539

540
            foreach ($perms as $p) {
5✔
541
                $permNames[(int) $p->id] = $p->name;
5✔
542
            }
543
        }
544

545
        // Build the cache: groupName => [permissionName, ...]
546
        foreach ($this->groupCache as $groupName) {
24✔
547
            $gId                                     = array_search($groupName, $this->groups, true);
24✔
548
            $this->groupPermissionsCache[$groupName] = [];
24✔
549

550
            if ($gId !== false && isset($groupPermMap[(int) $gId])) {
24✔
551
                foreach ($groupPermMap[(int) $gId] as $pid) {
5✔
552
                    if (isset($permNames[$pid])) {
5✔
553
                        $this->groupPermissionsCache[$groupName][] = $permNames[$pid];
5✔
554
                    }
555
                }
556
            }
557
        }
558
    }
559

560
    /**
561
     * Used internally to populate the User groups
562
     * so we hit the database as little as possible.
563
     * Reads from persistent cache when permissionCacheEnabled is true.
564
     */
565
    private function populateGroups(): void
49✔
566
    {
567
        if (is_array($this->groupCache) && is_array($this->groups) && is_array($this->groupPermissionsCache)) {
49✔
568
            return;
15✔
569
        }
570

571
        // Try persistent cache first
572
        if (service('settings')->get('AuthSecurity.permissionCacheEnabled')) {
49✔
573
            /** @var array{groups: array<int, string>, groupCache: list<string>, groupPermissionsCache: array<string, list<string>>}|null $cached */
574
            $cached = cache($this->getPermissionCacheKey('groups'));
5✔
575

576
            if ($cached !== null) {
5✔
577
                $this->groups                = $cached['groups'];
×
578
                $this->groupCache            = $cached['groupCache'];
×
579
                $this->groupPermissionsCache = $cached['groupPermissionsCache'];
×
580

581
                return;
×
582
            }
583
        }
584

585
        $groupModel = $this->groupModel();
49✔
586
        $rows       = $groupModel->findAll();
49✔
587

588
        foreach ($rows as $row) {
49✔
589
            $this->groups[$row->id] = $row->name;
49✔
590
        }
591

592
        $this->groupCache = array_column($this->getAllUserGroups(), 'name');
49✔
593

594
        // Eager-load all group permissions in a single pass (eliminates N+1 queries in can())
595
        $this->groupPermissionsCache = [];
49✔
596

597
        if ($this->groupCache !== []) {
49✔
598
            $this->eagerLoadGroupPermissions();
24✔
599
        }
600

601
        // Store in persistent cache
602
        if (service('settings')->get('AuthSecurity.permissionCacheEnabled')) {
49✔
603
            $ttl = (int) (service('settings')->get('AuthSecurity.permissionCacheTTL') ?? 300);
5✔
604
            cache()->save($this->getPermissionCacheKey('groups'), [
5✔
605
                'groups'                => $this->groups,
5✔
606
                'groupCache'            => $this->groupCache,
5✔
607
                'groupPermissionsCache' => $this->groupPermissionsCache,
5✔
608
            ], $ttl);
5✔
609
        }
610
    }
611

612
    /**
613
     * Used internally to populate the User permissions
614
     * so we hit the database as little as possible.
615
     * Reads from persistent cache when permissionCacheEnabled is true.
616
     */
617
    private function populatePermissions(): void
30✔
618
    {
619
        if (is_array($this->permissionsCache) && is_array($this->permissions)) {
30✔
620
            return;
20✔
621
        }
622

623
        // Try persistent cache first
624
        if (service('settings')->get('AuthSecurity.permissionCacheEnabled')) {
30✔
625
            /** @var array{permissions: array<int, string>, permissionsCache: list<string>}|null $cached */
626
            $cached = cache($this->getPermissionCacheKey('permissions'));
4✔
627

628
            if ($cached !== null) {
4✔
629
                $this->permissions      = $cached['permissions'];
×
630
                $this->permissionsCache = $cached['permissionsCache'];
×
631

632
                return;
×
633
            }
634
        }
635

636
        $permissionModel = $this->permissionModel();
30✔
637
        $rows            = $permissionModel->findAll();
30✔
638

639
        foreach ($rows as $row) {
30✔
640
            $this->permissions[$row->id] = $row->name;
26✔
641
        }
642

643
        $this->permissionsCache = array_column($this->getAllUserPermissions(), 'name');
30✔
644

645
        // Store in persistent cache
646
        if (service('settings')->get('AuthSecurity.permissionCacheEnabled')) {
30✔
647
            $ttl = (int) (service('settings')->get('AuthSecurity.permissionCacheTTL') ?? 300);
4✔
648
            cache()->save($this->getPermissionCacheKey('permissions'), [
4✔
649
                'permissions'      => $this->permissions,
4✔
650
                'permissionsCache' => $this->permissionsCache,
4✔
651
            ], $ttl);
4✔
652
        }
653
    }
654

655
    /**
656
     * Returns the cache key for this user's permission/group cache entry.
657
     *
658
     * @param string $type 'groups' or 'permissions'
659
     */
660
    private function getPermissionCacheKey(string $type): string
8✔
661
    {
662
        return 'auth_' . $type . '_' . $this->id;
8✔
663
    }
664

665
    /**
666
     * Deletes the persistent cache entries for this user's groups and permissions.
667
     * Call this when group/permission assignments change.
668
     */
669
    public function clearPermissionCache(): void
5✔
670
    {
671
        cache()->delete($this->getPermissionCacheKey('groups'));
5✔
672
        cache()->delete($this->getPermissionCacheKey('permissions'));
5✔
673

674
        $this->groupPermissionsCache = null;
5✔
675
    }
676

677
    /**
678
     * Inserts or Updates the current groups.
679
     */
680
    private function saveGroups(): void
29✔
681
    {
682
        $model = $this->groupUserModel();
29✔
683

684
        $names = $this->groupCache;
29✔
685

686
        $cache = [];
29✔
687

688
        foreach ($names as $name) {
29✔
689
            $cache[] = array_search($name, $this->groups, true);
27✔
690
        }
691

692
        $existing = array_column($this->getAllUserGroups(), 'id');
29✔
693

694
        $this->saveGroupsOrPermissions('group_id', $model, $cache, $existing);
29✔
695

696
        // Invalidate persistent cache after DB write
697
        if (service('settings')->get('AuthSecurity.permissionCacheEnabled')) {
29✔
698
            cache()->delete($this->getPermissionCacheKey('groups'));
4✔
699
        }
700

701
        // Force re-population of group permissions on next access
702
        $this->groupPermissionsCache = null;
29✔
703
    }
704

705
    /**
706
     * Inserts or Updates either the current permissions.
707
     */
708
    private function savePermissions(): void
17✔
709
    {
710
        $model = $this->permissionUserModel();
17✔
711

712
        $names = $this->permissionsCache;
17✔
713

714
        $cache = [];
17✔
715

716
        foreach ($names as $name) {
17✔
717
            $cache[] = array_search($name, $this->permissions, true);
14✔
718
        }
719

720
        $existing = array_column($this->getAllUserPermissions(), 'id');
17✔
721

722
        $this->saveGroupsOrPermissions('permission_id', $model, $cache, $existing);
17✔
723

724
        // Invalidate persistent cache after DB write
725
        if (service('settings')->get('AuthSecurity.permissionCacheEnabled')) {
17✔
726
            cache()->delete($this->getPermissionCacheKey('permissions'));
4✔
727
        }
728
    }
729

730
    /**
731
     * @param         GroupUserModel|PermissionUserModel $model
732
     * @phpstan-param 'group_id'|'permission_id'         $type
733
     */
734
    private function saveGroupsOrPermissions(string $type, $model, array $cache, array $existing): void
45✔
735
    {
736
        $db  = Database::connect();
45✔
737
        $new = array_diff($cache, $existing);
45✔
738

739
        $db->transStart();
45✔
740

741
        // Delete any not in the cache
742
        if ($cache !== []) {
45✔
743
            $model->deleteNotIn($this->id, $cache);
40✔
744
        }
745
        // Nothing in the cache? Then make sure
746
        // we delete all from this user
747
        else {
748
            $model->deleteAll($this->id);
9✔
749
        }
750

751
        // Insert new ones
752
        if ($new !== []) {
45✔
753
            $inserts = [];
38✔
754

755
            foreach ($new as $item) {
38✔
756
                $inserts[] = [
38✔
757
                    'user_id'    => $this->id,
38✔
758
                    $type        => $item,
38✔
759
                    'created_at' => Time::now()->format('Y-m-d H:i:s'),
38✔
760
                ];
38✔
761
            }
762

763
            $model->insertBatch($inserts);
38✔
764
        }
765

766
        $db->transComplete();
45✔
767
    }
768
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc