db->tableExists($this->table)) { return null; } $roles = $this->rolesForUser($userId); $flags = self::flagsForRoles($roles); $now = utc_now(); $data = [ 'user_id' => $userId, 'primary_category' => self::primaryCategory($flags), 'is_admin' => $flags['is_admin'] ? 1 : 0, 'is_teacher' => $flags['is_teacher'] ? 1 : 0, 'is_parent' => $flags['is_parent'] ? 1 : 0, 'role_names' => implode(', ', array_values(array_unique(array_map( static fn (array $role): string => (string) ($role['name'] ?? ''), $roles )))), 'updated_at' => $now, ]; $existing = $this->where('user_id', $userId)->first(); if ($existing) { $this->update((int) $existing['id'], $data); return $this->find((int) $existing['id']); } $data['created_at'] = $now; $id = $this->insert($data); return $id ? $this->find((int) $id) : null; } public function syncAll(): void { if (! $this->db->tableExists('users') || ! $this->db->tableExists($this->table)) { return; } $rows = $this->db->table('users')->select('id')->get()->getResultArray(); foreach ($rows as $row) { $this->syncUser((int) ($row['id'] ?? 0)); } } public function getForUser(int $userId): ?array { if ($userId <= 0 || ! $this->db->tableExists($this->table)) { return null; } $profile = $this->where('user_id', $userId)->first(); return $profile ?: $this->syncUser($userId); } public function getUsersByCategory(string $category): array { $field = match (self::normalizeRoleToken($category)) { self::CATEGORY_ADMIN => 'is_admin', self::CATEGORY_TEACHER => 'is_teacher', self::CATEGORY_PARENT => 'is_parent', default => null, }; if ($field === null || ! $this->db->tableExists($this->table)) { return []; } return $this->select('users.*, user_access_profiles.primary_category, user_access_profiles.role_names') ->join('users', 'users.id = user_access_profiles.user_id', 'inner') ->where($field, 1) ->orderBy('users.lastname', 'ASC') ->orderBy('users.firstname', 'ASC') ->findAll(); } public static function flagsForRoles(array $roles): array { $tokens = []; foreach ($roles as $role) { if (is_array($role)) { $tokens[] = self::normalizeRoleToken((string) ($role['slug'] ?? '')); $tokens[] = self::normalizeRoleToken((string) ($role['name'] ?? '')); continue; } $tokens[] = self::normalizeRoleToken((string) $role); } $tokens = array_values(array_unique(array_filter($tokens))); $isParent = in_array('parent', $tokens, true); $isTeacher = count(array_intersect($tokens, array_map([self::class, 'normalizeRoleToken'], self::TEACHER_ROLE_TOKENS))) > 0; $isAdmin = false; foreach ($tokens as $token) { if (! in_array($token, array_map([self::class, 'normalizeRoleToken'], self::NON_ADMIN_ROLE_TOKENS), true)) { $isAdmin = true; break; } } return [ 'is_admin' => $isAdmin, 'is_teacher' => $isTeacher, 'is_parent' => $isParent, ]; } public static function primaryCategory(array $flags): string { if (! empty($flags['is_admin'])) { return self::CATEGORY_ADMIN; } if (! empty($flags['is_teacher'])) { return self::CATEGORY_TEACHER; } if (! empty($flags['is_parent'])) { return self::CATEGORY_PARENT; } return self::CATEGORY_GUEST; } private function rolesForUser(int $userId): array { if (! $this->db->tableExists('user_roles') || ! $this->db->tableExists('roles')) { return []; } $builder = $this->db->table('user_roles ur') ->select('r.name, r.slug') ->join('roles r', 'r.id = ur.role_id', 'inner') ->where('ur.user_id', $userId) ->where('COALESCE(r.is_active, 1) = 1', null, false); if ($this->db->fieldExists('deleted_at', 'user_roles')) { $builder->where('ur.deleted_at', null); } return $builder->get()->getResultArray(); } private static function normalizeRoleToken(string $value): string { $value = strtolower(trim($value)); return str_replace([' ', '-'], '_', $value); } }