, groups: list} */ public function assigned(File|Folder $subject): array { [$clientIds, $groupIds] = $this->assignedIds($subject); return [ 'clients' => $this->clients($clientIds), 'groups' => $this->groups($groupIds), ]; } /** * assigned(), narrowed to the recipients this viewer may be told * about. The display half of the pair — see the warning above. * * @return array{clients: list, groups: list} */ public function assignedFor(File|Folder $subject, ?User $viewer): array { return $this->identity->filterShares($viewer, $this->assigned($subject)); } /** * The assigned lists plus everything still available to share with, * narrowed to what this viewer is allowed to reach. * * @return array{assigned_clients: list, assigned_groups: list, available_clients: list, available_groups: list} */ public function forSubject(File|Folder $subject, User $viewer): array { [$clientIds, $groupIds] = $this->assignedIds($subject); // Resolved once each: these hit the database, and the call sites this // replaces evaluated them twice apiece — once to decide whether to // filter, once to supply the ids. $assignableClientIds = $this->scope->assignableClientIds($viewer); $assignableGroupIds = $this->scope->assignableGroupIds($viewer); $availableClients = User::query() ->where('type', UserType::Client) ->whereNotIn('id', $clientIds) ->when($assignableClientIds !== null, fn (Builder $query) => $query->whereIn('id', $assignableClientIds)) ->orderBy('name') ->get(); $availableGroups = Group::query() ->whereNotIn('id', $groupIds) ->when($assignableGroupIds !== null, fn (Builder $query) => $query->whereIn('id', $assignableGroupIds)) ->orderBy('name') ->get(); // assignedFor, not assigned: an edit page listing a recipient this // viewer may not identify would both name them and offer a control // for a share the viewer cannot otherwise reach. available_* below // was already narrowed this way; assigned_* was not, which is the // asymmetry that made the whole panel a roster listing. $assigned = $this->assignedFor($subject, $viewer); return [ 'assigned_clients' => $assigned['clients'], 'assigned_groups' => $assigned['groups'], 'available_clients' => $this->pairs($availableClients), 'available_groups' => $this->pairs($availableGroups), ]; } /** * @return array{0: Collection, 1: Collection} */ private function assignedIds(File|Folder $subject): array { // A file revision owns no assignment rows — it inherits the // recipients of its version chain's root (File::sharingOwnerId). // Reading its own rows would show every revision as shared with // nobody, which is the opposite of what is true. $assigned = $subject instanceof File ? FileAssignment::query()->where('file_id', $subject->sharingOwnerId())->get() : $subject->assignments()->get(); // getMorphClass() rather than ::class, so this keeps agreeing with // what was written to assignable_type even if a morph map is added. return [ $assigned->where('assignable_type', (new User)->getMorphClass())->pluck('assignable_id'), $assigned->where('assignable_type', (new Group)->getMorphClass())->pluck('assignable_id'), ]; } /** * @param Collection $ids * @return list */ private function clients(Collection $ids): array { return $this->pairs(User::query()->whereIn('id', $ids)->orderBy('name')->get()); } /** * @param Collection $ids * @return list */ private function groups(Collection $ids): array { return $this->pairs(Group::query()->whereIn('id', $ids)->orderBy('name')->get()); } /** * Built by iteration rather than map()->values(): Collection's value * template is invariant, so passing a Collection where * Collection is declared does not type-check, and the * result of map() is not provably a list either. * * @param iterable $models * @return list */ private function pairs(iterable $models): array { $pairs = []; foreach ($models as $model) { $pairs[] = ['id' => (int) $model->getKey(), 'name' => $model->name]; } return $pairs; } }