framework->getAdapter(SurveyModel::class); $model = $adapter->findByPk($id); return $model instanceof SurveyModel ? $model : null; } public function findByPublicAlias(string $alias, bool $publishedOnly = true): ?SurveyModel { $qb = $this->connection->createQueryBuilder(); $qb ->select('id') ->from('tl_survey') ->where('alias = :alias') ->setParameter('alias', $alias) ->setMaxResults(1) ; if ($publishedOnly) { $qb->andWhere('published = 1'); $qb->andWhere('isActive = 1'); } $id = $qb->executeQuery()->fetchOne(); if (false === $id) { return null; } return $this->findById((int) $id); } /** * @return list> */ public function findEditableByMember(int $memberId): array { return $this->connection->fetchAllAssociative( <<<'SQL' SELECT s.id, s.title, s.alias, s.description, s.published, s.isActive, s.isLocked, s.updatedAt, COUNT(DISTINCT q.id) AS questionCount, COUNT(DISTINCT CASE WHEN sub.isFinished = 1 THEN sub.id END) AS participationCount FROM tl_survey s INNER JOIN tl_survey_editor e ON COALESCE(NULLIF(e.survey, 0), e.pid) = s.id AND e.member = :member LEFT JOIN tl_survey_content q ON q.pid = s.id LEFT JOIN tl_survey_submission sub ON sub.survey = s.id GROUP BY s.id ORDER BY s.updatedAt DESC, s.title ASC SQL, ['member' => $memberId], ); } public function create(int $memberId, array $data): SurveyModel { $now = time(); $title = trim((string) ($data['title'] ?? 'Neue Umfrage')); $this->connection->insert('tl_survey', [ 'tstamp' => $now, 'title' => $title, 'alias' => $this->ensurePublicAlias(null), 'category' => serialize($this->normalizeCategoryIds($data['category'] ?? [])), 'description' => (string) ($data['description'] ?? ''), 'published' => '', 'isActive' => '', 'isLocked' => '', 'createdBy' => $memberId, 'updatedBy' => $memberId, 'createdAt' => $now, 'updatedAt' => $now, ]); $id = (int) $this->connection->lastInsertId(); $this->refreshListMetadata($id); return $this->findById($id) ?? throw new \RuntimeException('Umfrage konnte nicht angelegt werden.'); } public function update(SurveyModel $survey, int $memberId, array $data): void { $title = trim((string) ($data['title'] ?? $survey->title)); $this->connection->update('tl_survey', [ 'tstamp' => time(), 'title' => $title, 'alias' => $this->ensurePublicAlias((string) $survey->alias, (int) $survey->id), 'category' => serialize($this->normalizeCategoryIds($data['category'] ?? StringUtil::deserialize($survey->category, true))), 'description' => (string) ($data['description'] ?? $survey->description), 'published' => array_key_exists('published', $data) ? (!empty($data['published']) ? '1' : '') : (string) $survey->published, 'isActive' => array_key_exists('isActive', $data) ? (!empty($data['isActive']) ? '1' : '') : (string) ($survey->isActive ?? ''), 'updatedBy' => $memberId, 'updatedAt' => time(), ], [ 'id' => (int) $survey->id, ]); $this->refreshListMetadata((int) $survey->id); } public function ensurePublicAlias(?string $alias, int $excludeId = 0): string { $candidate = strtolower(trim((string) $alias)); if ($this->isValidPublicAlias($candidate) && !$this->aliasExists($candidate, $excludeId)) { return $candidate; } do { $candidate = $this->generateRandomPublicAlias(); } while ($this->aliasExists($candidate, $excludeId)); return $candidate; } /** * @param list $categoryIds */ public function updateCategories(int $surveyId, array $categoryIds): void { $this->connection->update('tl_survey', [ 'category' => serialize($this->normalizeCategoryIds($categoryIds)), 'tstamp' => time(), 'updatedAt' => time(), ], [ 'id' => $surveyId, ]); $this->refreshListMetadata($surveyId); } public function refreshListMetadata(int $surveyId): void { if (!$this->hasListMetadataColumns()) { return; } $survey = $this->findById($surveyId); if (!$survey instanceof SurveyModel) { return; } $categoryIds = $this->normalizeCategoryIds($survey->category); $categoryTitles = $this->fetchCategoryTitles($categoryIds); $assignedMembers = $this->fetchAssignedMembers($surveyId); $memberIds = []; $memberNames = []; foreach ($assignedMembers as $assignedMember) { $memberIds[] = (int) $assignedMember['id']; $memberNames[] = (string) $assignedMember['name']; } $this->connection->update('tl_survey', [ 'categoryFilter' => implode(',', $categoryIds), 'categorySummary' => implode(', ', $categoryTitles), 'templateFilter' => '1' === (string) $survey->isTemplate ? '1' : '0', 'templateSummary' => '1' === (string) $survey->isTemplate ? 'Ja' : 'Nein', 'assignedMembersFilter' => implode(',', $memberIds), 'assignedMembersSummary' => implode(', ', $memberNames), ], [ 'id' => $surveyId, ]); } public function touch(int $surveyId): void { $now = time(); $this->connection->update('tl_survey', [ 'tstamp' => $now, 'updatedAt' => $now, ], [ 'id' => $surveyId, ]); } public function lock(int $surveyId): void { $this->connection->update('tl_survey', [ 'isLocked' => '1', 'tstamp' => time(), 'updatedAt' => time(), ], [ 'id' => $surveyId, ]); } public function publish(int $surveyId, int $memberId): void { $now = time(); $this->connection->update('tl_survey', [ 'published' => '1', 'isActive' => '1', 'isLocked' => '1', 'tstamp' => $now, 'updatedBy' => $memberId, 'updatedAt' => $now, ], [ 'id' => $surveyId, ]); } public function setActive(int $surveyId, int $memberId, bool $isActive): void { $now = time(); $this->connection->update('tl_survey', [ 'isActive' => $isActive ? '1' : '', 'tstamp' => $now, 'updatedBy' => $memberId, 'updatedAt' => $now, ], [ 'id' => $surveyId, ]); } public function deleteCascade(int $surveyId): void { $this->connection->transactional(function () use ($surveyId): void { $submissionIds = array_values(array_map('intval', $this->connection->fetchFirstColumn( 'SELECT id FROM tl_survey_submission WHERE survey = ?', [$surveyId], ))); if ([] !== $submissionIds) { $this->connection->executeStatement( 'DELETE FROM tl_survey_answer WHERE submission IN (?)', [$submissionIds], [ArrayParameterType::INTEGER], ); } $this->connection->delete('tl_survey_submission', ['survey' => $surveyId]); $this->connection->delete('tl_survey_condition', ['pid' => $surveyId]); $this->connection->delete('tl_survey_content', ['pid' => $surveyId]); $this->connection->delete('tl_survey_editor', ['survey' => $surveyId]); $this->connection->delete('tl_survey_editor', ['pid' => $surveyId]); $this->connection->delete('tl_survey', ['id' => $surveyId]); }); } private function generateRandomPublicAlias(): string { $characters = 'abcdefghijklmnopqrstuvwxyz0123456789'; $alias = ''; for ($index = 0; $index < 20; ++$index) { $alias .= $characters[random_int(0, strlen($characters) - 1)]; } return $alias; } private function aliasExists(string $alias, int $excludeId = 0): bool { $qb = $this->connection->createQueryBuilder(); $qb ->select('id') ->from('tl_survey') ->where('alias = :alias') ->setParameter('alias', $alias) ->setMaxResults(1) ; if ($excludeId > 0) { $qb ->andWhere('id != :excludeId') ->setParameter('excludeId', $excludeId) ; } return false !== $qb->executeQuery()->fetchOne(); } /** * @param list $categoryIds * * @return list */ private function fetchCategoryTitles(array $categoryIds): array { if ([] === $categoryIds) { return []; } $placeholders = implode(',', array_fill(0, count($categoryIds), '?')); $rows = $this->connection->fetchAllAssociative( sprintf('SELECT id, title FROM tl_survey_category WHERE id IN (%s)', $placeholders), $categoryIds, ); $titlesById = []; foreach ($rows as $row) { $titlesById[(int) $row['id']] = (string) $row['title']; } $titles = []; foreach ($categoryIds as $categoryId) { if (isset($titlesById[$categoryId])) { $titles[] = $titlesById[$categoryId]; } } return $titles; } /** * @return list */ private function fetchAssignedMembers(int $surveyId): array { $rows = $this->connection->fetchAllAssociative( <<<'SQL' SELECT DISTINCT m.id, m.firstname, m.lastname, m.email FROM tl_survey_editor e INNER JOIN tl_member m ON m.id = e.member WHERE COALESCE(NULLIF(e.survey, 0), e.pid) = ? ORDER BY m.lastname ASC, m.firstname ASC, m.email ASC SQL, [$surveyId], ); $members = []; foreach ($rows as $row) { $name = trim(sprintf('%s %s', (string) $row['firstname'], (string) $row['lastname'])); $members[] = [ 'id' => (int) $row['id'], 'name' => '' !== $name ? $name : (string) $row['email'], ]; } return $members; } private function hasListMetadataColumns(): bool { if (null !== $this->hasListMetadataColumns) { return $this->hasListMetadataColumns; } $columnNames = array_map( static fn (string $columnName): string => strtolower($columnName), array_keys($this->connection->createSchemaManager()->listTableColumns('tl_survey')), ); return $this->hasListMetadataColumns = !array_diff( ['categoryfilter', 'categorysummary', 'templatefilter', 'templatesummary', 'assignedmembersfilter', 'assignedmemberssummary'], $columnNames, ); } private function isValidPublicAlias(string $alias): bool { return 1 === preg_match('/^[a-z0-9]{20}$/', $alias); } /** * @return list */ private function normalizeCategoryIds(mixed $value): array { $categoryIds = is_array($value) ? $value : StringUtil::deserialize($value, true); $categoryIds = array_values(array_unique(array_filter(array_map('intval', $categoryIds), static fn (int $id): bool => $id > 0))); sort($categoryIds); return $categoryIds; } }