Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
GetTripMembersHandler
100.00% covered (success)
100.00%
35 / 35
100.00% covered (success)
100.00%
3 / 3
10
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
8
 memberEntry
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace App\Trip\Application\Query;
6
7use App\Trip\Application\Port\MemberDirectory;
8use App\Trip\Application\Port\MemberSummary;
9use App\Trip\Application\TripAccess;
10use App\Trip\Domain\Exceptions\TripNotFound;
11use App\Trip\Domain\TripInvitationRepository;
12use App\Trip\Domain\TripMemberRepository;
13use App\Trip\Domain\ValueObject\MemberId;
14use App\Trip\Domain\ValueObject\TripId;
15use App\Trip\Domain\ValueObject\TripRole;
16
17final readonly class GetTripMembersHandler
18{
19    public function __construct(
20        private TripAccess $access,
21        private TripMemberRepository $members,
22        private TripInvitationRepository $invitations,
23        private MemberDirectory $directory,
24    ) {
25    }
26
27    public function handle(GetTripMembersQuery $query): TripMembersView
28    {
29        $tripId = new TripId($query->tripId);
30        [$trip, $role] = $this->access->readableWithRole($tripId, $query->userId);
31        if ($trip === null || $role === null) {
32            throw TripNotFound::withId($tripId);
33        }
34
35        $ownerId = new MemberId($trip->ownerId()->value());
36        $memberRows = $this->members->ofTrip($tripId);
37        // Pending invitations are the owner's business only.
38        $pending = $role === TripRole::OWNER ? $this->invitations->pendingOfTrip($tripId) : [];
39
40        // One directory round for every id we need to display.
41        $ids = [$ownerId];
42        foreach ($memberRows as $member) {
43            $ids[] = $member->userId();
44        }
45        foreach ($pending as $invitation) {
46            $ids[] = $invitation->inviteeId();
47        }
48        $summaries = $this->directory->summariesOf($ids);
49
50        $members = [$this->memberEntry($ownerId, TripRole::OWNER, $summaries)];
51        foreach ($memberRows as $member) {
52            $members[] = $this->memberEntry($member->userId(), $member->role(), $summaries);
53        }
54
55        $pendingInvitations = [];
56        foreach ($pending as $invitation) {
57            $summary = $summaries[$invitation->inviteeId()->value()];
58            $pendingInvitations[] = [
59                'id' => $invitation->id()->value(),
60                'email' => $summary->email,
61                'name' => $summary->name,
62                'role' => $invitation->role()->value,
63            ];
64        }
65
66        return new TripMembersView($members, $pendingInvitations);
67    }
68
69    /**
70     * @param array<string, MemberSummary> $summaries
71     *
72     * @return array{userId: string, email: string, name: string, role: string, avatarUrl: string|null}
73     */
74    private function memberEntry(MemberId $id, TripRole $role, array $summaries): array
75    {
76        // FK CASCADE guarantees every member/owner id resolves to a live user.
77        $summary = $summaries[$id->value()];
78
79        return [
80            'userId' => $id->value(),
81            'email' => $summary->email,
82            'name' => $summary->name,
83            'role' => $role->value,
84            'avatarUrl' => $summary->avatarUrl,
85        ];
86    }
87}