Initial survey bundle import
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
final class SurveyAnswerRepository
|
||||
{
|
||||
public function __construct(private readonly Connection $connection)
|
||||
{
|
||||
}
|
||||
|
||||
public function saveAnswer(int $submissionId, int $questionId, string $questionType, string $value): void
|
||||
{
|
||||
$existingId = $this->connection->fetchOne(
|
||||
'SELECT id FROM tl_survey_answer WHERE submission = ? AND question = ? LIMIT 1',
|
||||
[$submissionId, $questionId],
|
||||
);
|
||||
|
||||
if (false !== $existingId) {
|
||||
$this->connection->update('tl_survey_answer', [
|
||||
'questionType' => $questionType,
|
||||
'value' => $value,
|
||||
], [
|
||||
'id' => (int) $existingId,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connection->insert('tl_survey_answer', [
|
||||
'pid' => $submissionId,
|
||||
'submission' => $submissionId,
|
||||
'question' => $questionId,
|
||||
'questionType' => $questionType,
|
||||
'value' => $value,
|
||||
]);
|
||||
}
|
||||
|
||||
public function hasAnswersForSurvey(int $surveyId): bool
|
||||
{
|
||||
return false !== $this->connection->fetchOne(
|
||||
<<<'SQL'
|
||||
SELECT ans.id
|
||||
FROM tl_survey_answer ans
|
||||
INNER JOIN tl_survey_submission sub ON sub.id = ans.submission
|
||||
WHERE sub.survey = ?
|
||||
LIMIT 1
|
||||
SQL,
|
||||
[$surveyId],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{questionId:int,value:string}>
|
||||
*/
|
||||
public function findFinishedAnswersBySurvey(int $surveyId): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
a.question AS questionId,
|
||||
a.value
|
||||
FROM tl_survey_answer a
|
||||
INNER JOIN tl_survey_submission sub ON sub.id = a.submission
|
||||
WHERE sub.survey = :survey AND sub.isFinished = 1
|
||||
ORDER BY a.question ASC, a.id ASC
|
||||
SQL,
|
||||
['survey' => $surveyId],
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row): array => [
|
||||
'questionId' => (int) $row['questionId'],
|
||||
'value' => (string) $row['value'],
|
||||
],
|
||||
$rows,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function findAnswersBySubmission(int $submissionId): array
|
||||
{
|
||||
return $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
a.id,
|
||||
a.question AS questionId,
|
||||
a.value,
|
||||
a.questionType,
|
||||
q.question
|
||||
FROM tl_survey_answer a
|
||||
INNER JOIN tl_survey_content q ON q.id = a.question
|
||||
WHERE a.submission = :submission
|
||||
ORDER BY q.sorting ASC
|
||||
SQL,
|
||||
['submission' => $submissionId],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\SurveyBundle\Model\SurveyConditionModel;
|
||||
|
||||
final class SurveyConditionRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly ContaoFramework $framework,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findById(int $id): ?SurveyConditionModel
|
||||
{
|
||||
$adapter = $this->framework->getAdapter(SurveyConditionModel::class);
|
||||
$model = $adapter->findByPk($id);
|
||||
|
||||
return $model instanceof SurveyConditionModel ? $model : null;
|
||||
}
|
||||
|
||||
public function findMatchingCondition(int $surveyId, int $sourceQuestionId, string $answerValue): ?SurveyConditionModel
|
||||
{
|
||||
$id = $this->connection->fetchOne(
|
||||
'SELECT id FROM tl_survey_condition WHERE pid = ? AND sourceQuestion = ? AND LOWER(TRIM(answerValue)) = LOWER(TRIM(?)) ORDER BY id ASC LIMIT 1',
|
||||
[$surveyId, $sourceQuestionId, $answerValue],
|
||||
);
|
||||
|
||||
return false === $id ? null : $this->findById((int) $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function findOverviewBySurvey(int $surveyId): array
|
||||
{
|
||||
return $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
c.id,
|
||||
c.answerValue,
|
||||
c.sourceQuestion,
|
||||
c.targetQuestion,
|
||||
source.question AS sourceQuestionLabel,
|
||||
target.question AS targetQuestionLabel
|
||||
FROM tl_survey_condition c
|
||||
LEFT JOIN tl_survey_content source ON source.id = c.sourceQuestion
|
||||
LEFT JOIN tl_survey_content target ON target.id = c.targetQuestion
|
||||
WHERE c.pid = :survey
|
||||
ORDER BY source.sorting ASC, c.id ASC
|
||||
SQL,
|
||||
['survey' => $surveyId],
|
||||
);
|
||||
}
|
||||
|
||||
public function create(int $surveyId, array $data): SurveyConditionModel
|
||||
{
|
||||
$this->connection->insert('tl_survey_condition', [
|
||||
'pid' => $surveyId,
|
||||
'sourceQuestion' => (int) ($data['sourceQuestion'] ?? 0),
|
||||
'answerValue' => mb_strtolower(trim((string) ($data['answerValue'] ?? ''))),
|
||||
'targetQuestion' => (int) ($data['targetQuestion'] ?? 0),
|
||||
]);
|
||||
|
||||
$id = (int) $this->connection->lastInsertId();
|
||||
|
||||
return $this->findById($id) ?? throw new \RuntimeException('Bedingung konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
public function update(SurveyConditionModel $condition, array $data): void
|
||||
{
|
||||
$this->connection->update('tl_survey_condition', [
|
||||
'sourceQuestion' => (int) ($data['sourceQuestion'] ?? $condition->sourceQuestion),
|
||||
'answerValue' => mb_strtolower(trim((string) ($data['answerValue'] ?? $condition->answerValue))),
|
||||
'targetQuestion' => (int) ($data['targetQuestion'] ?? $condition->targetQuestion),
|
||||
], [
|
||||
'id' => (int) $condition->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(int $surveyId, int $conditionId): void
|
||||
{
|
||||
$this->connection->delete('tl_survey_condition', [
|
||||
'id' => $conditionId,
|
||||
'pid' => $surveyId,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
final class SurveyEditorRepository
|
||||
{
|
||||
public function __construct(private readonly Connection $connection)
|
||||
{
|
||||
}
|
||||
|
||||
public function isEditor(int $surveyId, int $memberId): bool
|
||||
{
|
||||
return false !== $this->connection->fetchOne(
|
||||
'SELECT id FROM tl_survey_editor WHERE COALESCE(NULLIF(survey, 0), pid) = ? AND member = ? LIMIT 1',
|
||||
[$surveyId, $memberId],
|
||||
);
|
||||
}
|
||||
|
||||
public function assignEditor(int $surveyId, int $memberId): void
|
||||
{
|
||||
if ($this->isEditor($surveyId, $memberId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->connection->insert('tl_survey_editor', [
|
||||
'pid' => $surveyId,
|
||||
'survey' => $surveyId,
|
||||
'member' => $memberId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function removeEditor(int $surveyId, int $memberId): void
|
||||
{
|
||||
$this->connection->delete('tl_survey_editor', [
|
||||
'survey' => $surveyId,
|
||||
'member' => $memberId,
|
||||
]);
|
||||
|
||||
$this->connection->delete('tl_survey_editor', [
|
||||
'pid' => $surveyId,
|
||||
'member' => $memberId,
|
||||
'survey' => 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public function findSurveyIdsByMember(int $memberId): array
|
||||
{
|
||||
$surveyIds = $this->connection->fetchFirstColumn(
|
||||
'SELECT DISTINCT COALESCE(NULLIF(survey, 0), pid) FROM tl_survey_editor WHERE member = ? ORDER BY COALESCE(NULLIF(survey, 0), pid)',
|
||||
[$memberId],
|
||||
);
|
||||
|
||||
return array_values(array_map('intval', $surveyIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
public function findMemberIdsBySurvey(int $surveyId): array
|
||||
{
|
||||
$memberIds = $this->connection->fetchFirstColumn(
|
||||
'SELECT DISTINCT member FROM tl_survey_editor WHERE COALESCE(NULLIF(survey, 0), pid) = ? AND member > 0 ORDER BY member',
|
||||
[$surveyId],
|
||||
);
|
||||
|
||||
return array_values(array_map('intval', $memberIds));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function findSurveyChoices(): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT id, title FROM tl_survey ORDER BY title ASC, id ASC'
|
||||
);
|
||||
$choices = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$title = trim((string) ($row['title'] ?? ''));
|
||||
$choices[(int) $row['id']] = '' !== $title ? $title : 'Ohne Titel';
|
||||
}
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $memberIds
|
||||
*/
|
||||
public function syncMembersForSurvey(int $surveyId, array $memberIds): void
|
||||
{
|
||||
$memberIds = $this->normalizeIds($memberIds);
|
||||
$existingMemberIds = $this->findMemberIdsBySurvey($surveyId);
|
||||
|
||||
foreach (array_diff($existingMemberIds, $memberIds) as $memberId) {
|
||||
$this->removeEditor($surveyId, (int) $memberId);
|
||||
}
|
||||
|
||||
foreach (array_diff($memberIds, $existingMemberIds) as $memberId) {
|
||||
$this->assignEditor($surveyId, (int) $memberId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $surveyIds
|
||||
*/
|
||||
public function syncSurveysForMember(int $memberId, array $surveyIds): void
|
||||
{
|
||||
$surveyIds = $this->normalizeIds($surveyIds);
|
||||
$existingSurveyIds = $this->findSurveyIdsByMember($memberId);
|
||||
|
||||
foreach (array_diff($existingSurveyIds, $surveyIds) as $surveyId) {
|
||||
$this->removeEditor((int) $surveyId, $memberId);
|
||||
}
|
||||
|
||||
foreach (array_diff($surveyIds, $existingSurveyIds) as $surveyId) {
|
||||
$this->assignEditor((int) $surveyId, $memberId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function findEditorsBySurvey(int $surveyId): array
|
||||
{
|
||||
return $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
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) = :survey
|
||||
ORDER BY m.lastname ASC, m.firstname ASC
|
||||
SQL,
|
||||
['survey' => $surveyId],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function findMemberChoices(): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative('SELECT id, firstname, lastname, email FROM tl_member WHERE disable != 1 ORDER BY lastname ASC, firstname ASC');
|
||||
$choices = [];
|
||||
|
||||
foreach ($rows as $row) {
|
||||
$name = trim(sprintf('%s %s', (string) $row['firstname'], (string) $row['lastname']));
|
||||
$choices[(int) $row['id']] = '' !== $name ? sprintf('%s <%s>', $name, (string) $row['email']) : (string) $row['email'];
|
||||
}
|
||||
|
||||
return $choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<int> $ids
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
private function normalizeIds(array $ids): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids), static fn (int $id): bool => $id > 0)));
|
||||
sort($ids);
|
||||
|
||||
return $ids;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\SurveyBundle\Model\SurveyContentModel;
|
||||
|
||||
final class SurveyQuestionRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly ContaoFramework $framework,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findById(int $id): ?SurveyContentModel
|
||||
{
|
||||
$adapter = $this->framework->getAdapter(SurveyContentModel::class);
|
||||
$model = $adapter->findByPk($id);
|
||||
|
||||
return $model instanceof SurveyContentModel ? $model : null;
|
||||
}
|
||||
|
||||
public function findByIdForSurvey(int $surveyId, int $questionId, bool $publishedOnly = false): ?SurveyContentModel
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT id FROM tl_survey_content WHERE id = ? AND pid = ?'.($publishedOnly ? ' AND published = 1' : '').' LIMIT 1',
|
||||
[$questionId, $surveyId],
|
||||
);
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findById((int) $row['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<SurveyContentModel>
|
||||
*/
|
||||
public function findAllBySurvey(int $surveyId, bool $publishedOnly = false): array
|
||||
{
|
||||
$sql = 'SELECT id FROM tl_survey_content WHERE pid = ?';
|
||||
|
||||
if ($publishedOnly) {
|
||||
$sql .= ' AND published = 1';
|
||||
}
|
||||
|
||||
$sql .= ' ORDER BY sorting ASC';
|
||||
|
||||
$ids = $this->connection->fetchFirstColumn($sql, [$surveyId]);
|
||||
|
||||
return $this->hydrate($ids);
|
||||
}
|
||||
|
||||
public function create(int $surveyId, array $data): SurveyContentModel
|
||||
{
|
||||
$sorting = (int) $this->connection->fetchOne('SELECT COALESCE(MAX(sorting), 0) FROM tl_survey_content WHERE pid = ?', [$surveyId]) + 128;
|
||||
|
||||
$this->connection->insert('tl_survey_content', [
|
||||
'pid' => $surveyId,
|
||||
'sorting' => $sorting,
|
||||
'type' => (string) ($data['type'] ?? 'yes_no_maybe'),
|
||||
'question' => (string) ($data['question'] ?? ''),
|
||||
'description' => (string) ($data['description'] ?? ''),
|
||||
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
||||
'published' => !empty($data['published']) ? '1' : '',
|
||||
'allowMaybe' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') && !empty($data['allowMaybe']) ? '1' : '',
|
||||
'jumpOnYes' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') ? max(0, (int) ($data['jumpOnYes'] ?? 0)) : 0,
|
||||
'jumpOnNo' => 'yes_no_maybe' === (string) ($data['type'] ?? 'yes_no_maybe') ? max(0, (int) ($data['jumpOnNo'] ?? 0)) : 0,
|
||||
'allowMultiple' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') && !empty($data['allowMultiple']) ? '1' : '',
|
||||
'answerOption1' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption1'] ?? '')) : '',
|
||||
'answerOption2' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption2'] ?? '')) : '',
|
||||
'answerOption3' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption3'] ?? '')) : '',
|
||||
'answerOption4' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption4'] ?? '')) : '',
|
||||
'answerOption5' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption5'] ?? '')) : '',
|
||||
'answerOption6' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption6'] ?? '')) : '',
|
||||
'answerOption7' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption7'] ?? '')) : '',
|
||||
'answerOption8' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption8'] ?? '')) : '',
|
||||
'answerOption9' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption9'] ?? '')) : '',
|
||||
'answerOption10' => 'choice' === (string) ($data['type'] ?? 'yes_no_maybe') ? trim((string) ($data['answerOption10'] ?? '')) : '',
|
||||
'rangeMin' => (int) ($data['rangeMin'] ?? 0),
|
||||
'rangeMax' => (int) ($data['rangeMax'] ?? 10),
|
||||
'rangeStep' => max(1, (int) ($data['rangeStep'] ?? 1)),
|
||||
]);
|
||||
|
||||
$id = (int) $this->connection->lastInsertId();
|
||||
|
||||
return $this->findById($id) ?? throw new \RuntimeException('Frage konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
public function update(SurveyContentModel $question, array $data): void
|
||||
{
|
||||
$this->connection->update('tl_survey_content', [
|
||||
'type' => (string) ($data['type'] ?? $question->type),
|
||||
'question' => (string) ($data['question'] ?? $question->question),
|
||||
'description' => (string) ($data['description'] ?? $question->description),
|
||||
'mandatory' => !empty($data['mandatory']) ? '1' : '',
|
||||
'published' => !empty($data['published']) ? '1' : '',
|
||||
'allowMaybe' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) && !empty($data['allowMaybe']) ? '1' : '',
|
||||
'jumpOnYes' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) ? max(0, (int) ($data['jumpOnYes'] ?? $question->jumpOnYes)) : 0,
|
||||
'jumpOnNo' => 'yes_no_maybe' === (string) ($data['type'] ?? $question->type) ? max(0, (int) ($data['jumpOnNo'] ?? $question->jumpOnNo)) : 0,
|
||||
'allowMultiple' => 'choice' === (string) ($data['type'] ?? $question->type) && !empty($data['allowMultiple']) ? '1' : '',
|
||||
'answerOption1' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption1'] ?? $question->answerOption1)) : '',
|
||||
'answerOption2' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption2'] ?? $question->answerOption2)) : '',
|
||||
'answerOption3' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption3'] ?? $question->answerOption3)) : '',
|
||||
'answerOption4' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption4'] ?? $question->answerOption4)) : '',
|
||||
'answerOption5' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption5'] ?? $question->answerOption5)) : '',
|
||||
'answerOption6' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption6'] ?? $question->answerOption6)) : '',
|
||||
'answerOption7' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption7'] ?? $question->answerOption7)) : '',
|
||||
'answerOption8' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption8'] ?? $question->answerOption8)) : '',
|
||||
'answerOption9' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption9'] ?? $question->answerOption9)) : '',
|
||||
'answerOption10' => 'choice' === (string) ($data['type'] ?? $question->type) ? trim((string) ($data['answerOption10'] ?? $question->answerOption10)) : '',
|
||||
'rangeMin' => (int) ($data['rangeMin'] ?? $question->rangeMin),
|
||||
'rangeMax' => (int) ($data['rangeMax'] ?? $question->rangeMax),
|
||||
'rangeStep' => max(1, (int) ($data['rangeStep'] ?? $question->rangeStep)),
|
||||
], [
|
||||
'id' => (int) $question->id,
|
||||
]);
|
||||
}
|
||||
|
||||
public function delete(int $surveyId, int $questionId): void
|
||||
{
|
||||
$this->connection->delete('tl_survey_content', [
|
||||
'id' => $questionId,
|
||||
'pid' => $surveyId,
|
||||
]);
|
||||
|
||||
$this->connection->delete('tl_survey_condition', [
|
||||
'sourceQuestion' => $questionId,
|
||||
]);
|
||||
|
||||
$this->connection->delete('tl_survey_condition', [
|
||||
'targetQuestion' => $questionId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function countPublishedBySurvey(int $surveyId): int
|
||||
{
|
||||
return (int) $this->connection->fetchOne(
|
||||
'SELECT COUNT(*) FROM tl_survey_content WHERE pid = ? AND published = 1',
|
||||
[$surveyId],
|
||||
);
|
||||
}
|
||||
|
||||
private function hydrate(array $ids): array
|
||||
{
|
||||
$models = [];
|
||||
|
||||
foreach ($ids as $id) {
|
||||
$model = $this->findById((int) $id);
|
||||
|
||||
if ($model instanceof SurveyContentModel) {
|
||||
$models[] = $model;
|
||||
}
|
||||
}
|
||||
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Contao\StringUtil;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\SurveyBundle\Model\SurveyModel;
|
||||
|
||||
final class SurveyRepository
|
||||
{
|
||||
private ?bool $hasListMetadataColumns = null;
|
||||
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly ContaoFramework $framework,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findById(int $id): ?SurveyModel
|
||||
{
|
||||
$adapter = $this->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');
|
||||
}
|
||||
|
||||
$id = $qb->executeQuery()->fetchOne();
|
||||
|
||||
if (false === $id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->findById((int) $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function findEditableByMember(int $memberId): array
|
||||
{
|
||||
return $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
s.id,
|
||||
s.title,
|
||||
s.alias,
|
||||
s.description,
|
||||
s.published,
|
||||
s.isLocked,
|
||||
s.updatedAt,
|
||||
COUNT(DISTINCT q.id) AS questionCount,
|
||||
COUNT(DISTINCT a.id) AS answerCount
|
||||
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
|
||||
LEFT JOIN tl_survey_answer a ON a.submission = sub.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' => !empty($data['published']) ? '1' : '',
|
||||
'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' => !empty($data['published']) ? '1' : '',
|
||||
'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<int> $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),
|
||||
'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,
|
||||
]);
|
||||
}
|
||||
|
||||
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<int> $categoryIds
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
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<array{id:int,name:string}>
|
||||
*/
|
||||
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', 'assignedmembersfilter', 'assignedmemberssummary'],
|
||||
$columnNames,
|
||||
);
|
||||
}
|
||||
|
||||
private function isValidPublicAlias(string $alias): bool
|
||||
{
|
||||
return 1 === preg_match('/^[a-z0-9]{20}$/', $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<int>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mummert\SurveyBundle\Repository;
|
||||
|
||||
use Contao\CoreBundle\Framework\ContaoFramework;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mummert\SurveyBundle\Model\SurveySubmissionModel;
|
||||
|
||||
final class SurveySubmissionRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Connection $connection,
|
||||
private readonly ContaoFramework $framework,
|
||||
) {
|
||||
}
|
||||
|
||||
public function findById(int $id): ?SurveySubmissionModel
|
||||
{
|
||||
$adapter = $this->framework->getAdapter(SurveySubmissionModel::class);
|
||||
$model = $adapter->findByPk($id);
|
||||
|
||||
return $model instanceof SurveySubmissionModel ? $model : null;
|
||||
}
|
||||
|
||||
public function findOpenBySurveyAndToken(int $surveyId, string $token): ?SurveySubmissionModel
|
||||
{
|
||||
$id = $this->connection->fetchOne(
|
||||
'SELECT id FROM tl_survey_submission WHERE survey = ? AND token = ? AND isFinished != 1 ORDER BY id DESC LIMIT 1',
|
||||
[$surveyId, $token],
|
||||
);
|
||||
|
||||
return false === $id ? null : $this->findById((int) $id);
|
||||
}
|
||||
|
||||
public function findBySurveyAndToken(int $surveyId, string $token): ?SurveySubmissionModel
|
||||
{
|
||||
$id = $this->connection->fetchOne(
|
||||
'SELECT id FROM tl_survey_submission WHERE survey = ? AND token = ? ORDER BY id DESC LIMIT 1',
|
||||
[$surveyId, $token],
|
||||
);
|
||||
|
||||
return false === $id ? null : $this->findById((int) $id);
|
||||
}
|
||||
|
||||
public function hasFinishedBySurveyAndToken(int $surveyId, string $token): bool
|
||||
{
|
||||
return false !== $this->connection->fetchOne(
|
||||
'SELECT id FROM tl_survey_submission WHERE survey = ? AND token = ? AND isFinished = 1 ORDER BY id DESC LIMIT 1',
|
||||
[$surveyId, $token],
|
||||
);
|
||||
}
|
||||
|
||||
public function create(int $surveyId, string $token, ?int $currentQuestionId): SurveySubmissionModel
|
||||
{
|
||||
$now = time();
|
||||
|
||||
$this->connection->insert('tl_survey_submission', [
|
||||
'pid' => $surveyId,
|
||||
'survey' => $surveyId,
|
||||
'token' => $token,
|
||||
'startedAt' => $now,
|
||||
'completedAt' => 0,
|
||||
'currentQuestion' => $currentQuestionId ?? 0,
|
||||
'isFinished' => '',
|
||||
]);
|
||||
|
||||
$id = (int) $this->connection->lastInsertId();
|
||||
|
||||
return $this->findById($id) ?? throw new \RuntimeException('Der Umfrage-Durchlauf konnte nicht angelegt werden.');
|
||||
}
|
||||
|
||||
public function updateCurrentQuestion(int $submissionId, ?int $questionId): void
|
||||
{
|
||||
$this->connection->update('tl_survey_submission', [
|
||||
'currentQuestion' => $questionId ?? 0,
|
||||
], [
|
||||
'id' => $submissionId,
|
||||
]);
|
||||
}
|
||||
|
||||
public function markFinished(int $submissionId): void
|
||||
{
|
||||
$this->connection->update('tl_survey_submission', [
|
||||
'isFinished' => '1',
|
||||
'completedAt' => time(),
|
||||
'currentQuestion' => 0,
|
||||
], [
|
||||
'id' => $submissionId,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
public function findOverviewBySurvey(int $surveyId): array
|
||||
{
|
||||
return $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
sub.id,
|
||||
sub.token,
|
||||
sub.startedAt,
|
||||
sub.completedAt,
|
||||
sub.isFinished,
|
||||
COUNT(ans.id) AS answerCount
|
||||
FROM tl_survey_submission sub
|
||||
LEFT JOIN tl_survey_answer ans ON ans.submission = sub.id
|
||||
WHERE sub.survey = :survey
|
||||
GROUP BY sub.id
|
||||
ORDER BY sub.startedAt DESC
|
||||
SQL,
|
||||
['survey' => $surveyId],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user