486 lines
19 KiB
PHP
486 lines
19 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mummert\SurveyBundle\Controller\FrontendModule;
|
|
|
|
use Contao\Config;
|
|
use Contao\CoreBundle\Controller\FrontendModule\AbstractFrontendModuleController;
|
|
use Contao\CoreBundle\DependencyInjection\Attribute\AsFrontendModule;
|
|
use Contao\CoreBundle\Twig\FragmentTemplate;
|
|
use Contao\FrontendUser;
|
|
use Contao\ModuleModel;
|
|
use Contao\PageModel;
|
|
use Mummert\SurveyBundle\Form\Model\SurveyEditorData;
|
|
use Mummert\SurveyBundle\Form\Model\SurveyQuestionData;
|
|
use Mummert\SurveyBundle\Form\SurveyEditorType;
|
|
use Mummert\SurveyBundle\Form\SurveyQuestionEditorType;
|
|
use Mummert\SurveyBundle\Model\SurveyModel;
|
|
use Mummert\SurveyBundle\Repository\SurveyConditionRepository;
|
|
use Mummert\SurveyBundle\Repository\SurveyEditorRepository;
|
|
use Mummert\SurveyBundle\Repository\SurveyQuestionRepository;
|
|
use Mummert\SurveyBundle\Repository\SurveyRepository;
|
|
use Mummert\SurveyBundle\Service\SurveyEditorService;
|
|
use Mummert\SurveyBundle\Service\SurveySubmissionService;
|
|
use Symfony\Component\Form\FormFactoryInterface;
|
|
use Symfony\Component\Form\FormInterface;
|
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
#[AsFrontendModule(type: 'member_survey_edit', category: 'survey', template: 'member_survey_edit')]
|
|
final class MemberSurveyEditController extends AbstractFrontendModuleController
|
|
{
|
|
public function __construct(
|
|
private readonly SurveyRepository $surveyRepository,
|
|
private readonly SurveyQuestionRepository $surveyQuestionRepository,
|
|
private readonly SurveyConditionRepository $surveyConditionRepository,
|
|
private readonly SurveyEditorRepository $surveyEditorRepository,
|
|
private readonly SurveyEditorService $surveyEditorService,
|
|
private readonly SurveySubmissionService $surveySubmissionService,
|
|
private readonly FormFactoryInterface $formFactory,
|
|
) {
|
|
}
|
|
|
|
protected function getResponse(FragmentTemplate $template, ModuleModel $model, Request $request): Response
|
|
{
|
|
$memberId = $this->resolveEditorMemberId($request);
|
|
|
|
if ($memberId <= 0) {
|
|
$template->set('loginRequired', true);
|
|
$template->set('entryNotAllowed', false);
|
|
$template->set('backUrl', $this->resolveListUrl($model));
|
|
$template->set('createQuestionMode', false);
|
|
|
|
return $template->getResponse();
|
|
}
|
|
|
|
if (!$this->isEditorEntryAllowed($request)) {
|
|
$template->set('loginRequired', false);
|
|
$template->set('entryNotAllowed', true);
|
|
$template->set('createMode', false);
|
|
$template->set('survey', null);
|
|
$template->set('backUrl', $this->resolveListUrl($model));
|
|
$template->set('createQuestionMode', false);
|
|
|
|
return $template->getResponse();
|
|
}
|
|
|
|
$survey = $this->resolveSurvey($request, $memberId);
|
|
$createQuestionMode = $this->isCreateQuestionRequested($request);
|
|
|
|
if ($survey instanceof SurveyModel && $request->isMethod('POST') && $request->request->has('_survey_action')) {
|
|
return $this->handleAction($survey, $request, $model, $memberId);
|
|
}
|
|
|
|
$surveyForm = $this->formFactory->createNamed('survey_meta', SurveyEditorType::class, $survey instanceof SurveyModel ? SurveyEditorData::fromModel($survey) : new SurveyEditorData());
|
|
$surveyForm->handleRequest($request);
|
|
|
|
if ($surveyForm->isSubmitted() && $surveyForm->isValid()) {
|
|
try {
|
|
if ($survey instanceof SurveyModel) {
|
|
$this->surveyEditorService->updateSurvey($survey, $memberId, $surveyForm->getData());
|
|
$this->addFlash('success', 'Die Umfrage wurde gespeichert.');
|
|
|
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
|
}
|
|
|
|
$createdSurvey = $this->surveyEditorService->createSurvey($memberId, $surveyForm->getData());
|
|
$this->addFlash('success', 'Die Umfrage wurde angelegt.');
|
|
|
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $createdSurvey->id));
|
|
} catch (\Throwable $exception) {
|
|
$this->addFlash('error', $exception->getMessage());
|
|
}
|
|
}
|
|
|
|
if (!$survey instanceof SurveyModel) {
|
|
$template->set('loginRequired', false);
|
|
$template->set('entryNotAllowed', false);
|
|
$template->set('createMode', true);
|
|
$template->set('structureLocked', false);
|
|
$template->set('surveyForm', $surveyForm->createView());
|
|
$template->set('backUrl', $this->resolveListUrl($model));
|
|
$template->set('publicUrl', null);
|
|
$template->set('survey', null);
|
|
$template->set('questions', []);
|
|
$template->set('conditions', []);
|
|
$template->set('editors', []);
|
|
$template->set('submissions', []);
|
|
$template->set('activeQuestionId', 0);
|
|
$template->set('createQuestionMode', false);
|
|
|
|
return $template->getResponse();
|
|
}
|
|
|
|
$structureLocked = $this->surveyEditorService->isStructureLocked($survey);
|
|
|
|
$questions = $this->surveyQuestionRepository->findAllBySurvey((int) $survey->id);
|
|
$activeQuestion = $this->resolveActiveQuestion($request, (int) $survey->id);
|
|
$activeQuestionId = $activeQuestion ? (int) $activeQuestion->id : 0;
|
|
$questionFormViews = [];
|
|
$newQuestionFormView = null;
|
|
|
|
if (!$structureLocked) {
|
|
foreach ($questions as $question) {
|
|
$questionId = (int) $question->id;
|
|
$questionForm = $this->formFactory->createNamed('survey_question_'.$questionId, SurveyQuestionEditorType::class, SurveyQuestionData::fromModel($question), [
|
|
'question_choices' => $this->buildQuestionChoices((int) $survey->id, $questionId),
|
|
]);
|
|
$questionForm->handleRequest($request);
|
|
|
|
if ($questionForm->isSubmitted()) {
|
|
$activeQuestionId = $questionId;
|
|
$createQuestionMode = false;
|
|
|
|
if ($questionForm->isValid()) {
|
|
try {
|
|
$this->surveyEditorService->saveQuestion($survey, $questionForm->getData(), $questionId);
|
|
$this->addFlash('success', 'Die Frage wurde aktualisiert.');
|
|
|
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
|
} catch (\Throwable $exception) {
|
|
$this->addFlash('error', $exception->getMessage());
|
|
}
|
|
} else {
|
|
$this->addFlash('error', 'Frage konnte nicht gespeichert werden: '.$this->describeFormErrors($questionForm));
|
|
}
|
|
}
|
|
|
|
$questionFormViews[$questionId] = $questionForm->createView();
|
|
}
|
|
|
|
$newQuestionForm = $this->formFactory->createNamed('survey_question_new', SurveyQuestionEditorType::class, new SurveyQuestionData(), [
|
|
'question_choices' => $this->buildQuestionChoices((int) $survey->id),
|
|
]);
|
|
$newQuestionForm->handleRequest($request);
|
|
|
|
if ($newQuestionForm->isSubmitted()) {
|
|
$activeQuestionId = 0;
|
|
$createQuestionMode = true;
|
|
|
|
if ($newQuestionForm->isValid()) {
|
|
try {
|
|
$this->surveyEditorService->saveQuestion($survey, $newQuestionForm->getData(), 0);
|
|
$this->addFlash('success', 'Die Frage wurde angelegt.');
|
|
|
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
|
} catch (\Throwable $exception) {
|
|
$this->addFlash('error', $exception->getMessage());
|
|
}
|
|
} else {
|
|
$this->addFlash('error', 'Neue Frage konnte nicht gespeichert werden: '.$this->describeFormErrors($newQuestionForm));
|
|
}
|
|
}
|
|
|
|
$newQuestionFormView = $newQuestionForm->createView();
|
|
}
|
|
|
|
$template->set('loginRequired', false);
|
|
$template->set('entryNotAllowed', false);
|
|
$template->set('createMode', false);
|
|
$template->set('structureLocked', $structureLocked);
|
|
$template->set('createQuestionMode', $createQuestionMode);
|
|
$template->set('survey', $survey);
|
|
$template->set('surveyForm', $surveyForm->createView());
|
|
$template->set('questionForms', $questionFormViews);
|
|
$template->set('newQuestionForm', $newQuestionFormView);
|
|
$template->set('questions', $questions);
|
|
$conditionOverview = $this->surveyConditionRepository->findOverviewBySurvey((int) $survey->id);
|
|
|
|
$template->set('conditions', $this->buildJumpRuleOverview($questions, $conditionOverview));
|
|
$template->set('editors', $this->surveyEditorRepository->findEditorsBySurvey((int) $survey->id));
|
|
$template->set('submissions', $this->surveySubmissionService->getSubmissionOverview($survey));
|
|
$template->set('activeQuestionId', $activeQuestionId);
|
|
$template->set('backUrl', $this->resolveListUrl($model));
|
|
$template->set('publicUrl', $this->resolveReaderUrl($model, (string) $survey->alias));
|
|
$template->set('metadataFormActionUrl', $this->buildEditUrl($model, $request, (int) $survey->id));
|
|
|
|
return $template->getResponse();
|
|
}
|
|
|
|
private function resolveSurvey(Request $request, int $memberId): ?SurveyModel
|
|
{
|
|
$surveyId = (int) $request->query->get('survey', 0);
|
|
|
|
if ($surveyId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$survey = $this->surveyRepository->findById($surveyId);
|
|
|
|
if (!$survey instanceof SurveyModel) {
|
|
throw new \RuntimeException('Die Umfrage wurde nicht gefunden.');
|
|
}
|
|
|
|
if (!$this->surveyEditorRepository->isEditor((int) $survey->id, $memberId)) {
|
|
throw new \RuntimeException('Sie dürfen diese Umfrage nicht bearbeiten.');
|
|
}
|
|
|
|
return $survey;
|
|
}
|
|
|
|
private function resolveEditorMemberId(Request $request): int
|
|
{
|
|
$user = $this->getUser();
|
|
|
|
if ($user instanceof FrontendUser) {
|
|
return (int) $user->id;
|
|
}
|
|
|
|
return $this->isDebugAccessAllowed($request) ? $this->resolveDebugMemberId($request) : 0;
|
|
}
|
|
|
|
private function resolveDebugMemberId(Request $request): int
|
|
{
|
|
$memberId = (int) $request->query->get('debugMember', 0);
|
|
|
|
if ($memberId > 0) {
|
|
return $memberId;
|
|
}
|
|
|
|
$surveyId = (int) $request->query->get('survey', 0);
|
|
|
|
if ($surveyId <= 0) {
|
|
return 0;
|
|
}
|
|
|
|
$memberIds = $this->surveyEditorRepository->findMemberIdsBySurvey($surveyId);
|
|
|
|
return $memberIds[0] ?? 0;
|
|
}
|
|
|
|
private function isDebugAccessAllowed(Request $request): bool
|
|
{
|
|
$host = strtolower((string) ($request->server->get('HTTP_HOST') ?: $request->getHost()));
|
|
|
|
return (
|
|
'localhost' === $host
|
|
|| '127.0.0.1' === $host
|
|
|| str_ends_with($host, '.ddev.site')
|
|
) && $request->query->getBoolean('debugAccess');
|
|
}
|
|
|
|
private function handleAction(SurveyModel $survey, Request $request, ModuleModel $model, int $memberId): Response
|
|
{
|
|
$action = (string) $request->request->get('_survey_action');
|
|
$itemId = (int) $request->request->get('item_id', 0);
|
|
$token = (string) $request->request->get('_token');
|
|
|
|
if (!$this->isCsrfTokenValid($action.'-'.$itemId, $token)) {
|
|
throw new \RuntimeException('Ungültiger CSRF-Token.');
|
|
}
|
|
|
|
try {
|
|
if ('publish-survey' === $action) {
|
|
$this->surveyEditorService->publishSurvey($survey, $memberId);
|
|
$this->addFlash('success', 'Die Umfrage ist jetzt veröffentlicht.');
|
|
}
|
|
|
|
if ('toggle-active' === $action) {
|
|
$isActive = $this->surveyEditorService->toggleSurveyActive($survey, $memberId);
|
|
$this->addFlash('success', $isActive ? 'Die Umfrage ist jetzt wieder aktiv.' : 'Die Umfrage wurde vorübergehend deaktiviert.');
|
|
}
|
|
|
|
if ('delete-question' === $action) {
|
|
$this->surveyEditorService->deleteQuestion($survey, $itemId);
|
|
$this->addFlash('success', 'Die Frage wurde gelöscht.');
|
|
}
|
|
|
|
if ('delete-condition' === $action) {
|
|
$this->surveyEditorService->deleteCondition($survey, $itemId);
|
|
$this->addFlash('success', 'Die Bedingung wurde gelöscht.');
|
|
}
|
|
} catch (\Throwable $exception) {
|
|
$this->addFlash('error', $exception->getMessage());
|
|
}
|
|
|
|
return new RedirectResponse($this->buildEditUrl($model, $request, (int) $survey->id));
|
|
}
|
|
|
|
private function resolveActiveQuestion(Request $request, int $surveyId): mixed
|
|
{
|
|
$questionId = (int) $request->query->get('question', 0);
|
|
|
|
return $questionId > 0 ? $this->surveyQuestionRepository->findByIdForSurvey($surveyId, $questionId) : null;
|
|
}
|
|
|
|
private function isCreateQuestionRequested(Request $request): bool
|
|
{
|
|
return 'new' === trim((string) $request->query->get('question', ''));
|
|
}
|
|
|
|
private function isEditorEntryAllowed(Request $request): bool
|
|
{
|
|
if ((int) $request->query->get('survey', 0) > 0) {
|
|
return true;
|
|
}
|
|
|
|
return $request->query->getBoolean('create');
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
private function buildQuestionChoices(int $surveyId, int $excludeQuestionId = 0): array
|
|
{
|
|
$choices = [];
|
|
|
|
foreach ($this->surveyQuestionRepository->findAllBySurvey($surveyId) as $question) {
|
|
if ($excludeQuestionId > 0 && (int) $question->id === $excludeQuestionId) {
|
|
continue;
|
|
}
|
|
|
|
$choices[(int) $question->id] = sprintf('#%d %s', (int) $question->id, (string) $question->question);
|
|
}
|
|
|
|
return $choices;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, mixed> $questions
|
|
* @param list<array<string, mixed>> $conditionOverview
|
|
*
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function buildJumpRuleOverview(array $questions, array $conditionOverview): array
|
|
{
|
|
$questionLabels = [];
|
|
$questionSortings = [];
|
|
$rules = [];
|
|
|
|
foreach ($questions as $question) {
|
|
$questionId = (int) ($question->id ?? 0);
|
|
|
|
if ($questionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
$questionLabels[$questionId] = (string) ($question->question ?? '');
|
|
$questionSortings[$questionId] = (int) ($question->sorting ?? 0);
|
|
}
|
|
|
|
foreach ($questions as $question) {
|
|
$questionId = (int) ($question->id ?? 0);
|
|
|
|
if ($questionId <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if ('yes_no_maybe' !== (string) ($question->type ?? '')) {
|
|
continue;
|
|
}
|
|
|
|
foreach (['Ja' => (int) ($question->jumpOnYes ?? 0), 'Nein' => (int) ($question->jumpOnNo ?? 0)] as $answerValue => $targetQuestionId) {
|
|
if ($targetQuestionId <= 0 || !isset($questionLabels[$targetQuestionId])) {
|
|
continue;
|
|
}
|
|
|
|
$rules[] = [
|
|
'id' => 'question-'.$questionId.'-'.$targetQuestionId.'-'.$answerValue,
|
|
'answerValue' => $answerValue,
|
|
'sourceQuestion' => $questionId,
|
|
'targetQuestion' => $targetQuestionId,
|
|
'sourceQuestionLabel' => (string) ($question->question ?? ''),
|
|
'targetQuestionLabel' => $questionLabels[$targetQuestionId] ?? '',
|
|
'sourceSorting' => $questionSortings[$questionId] ?? 0,
|
|
];
|
|
}
|
|
}
|
|
|
|
foreach ($conditionOverview as $condition) {
|
|
$sourceQuestionId = (int) ($condition['sourceQuestion'] ?? 0);
|
|
|
|
$rules[] = [
|
|
'id' => $condition['id'] ?? 0,
|
|
'answerValue' => (string) ($condition['answerValue'] ?? ''),
|
|
'sourceQuestion' => $sourceQuestionId,
|
|
'targetQuestion' => (int) ($condition['targetQuestion'] ?? 0),
|
|
'sourceQuestionLabel' => (string) ($condition['sourceQuestionLabel'] ?? ''),
|
|
'targetQuestionLabel' => (string) ($condition['targetQuestionLabel'] ?? ''),
|
|
'sourceSorting' => $questionSortings[$sourceQuestionId] ?? 0,
|
|
];
|
|
}
|
|
|
|
usort($rules, static function (array $left, array $right): int {
|
|
$sortingComparison = ((int) ($left['sourceSorting'] ?? 0)) <=> ((int) ($right['sourceSorting'] ?? 0));
|
|
|
|
if (0 !== $sortingComparison) {
|
|
return $sortingComparison;
|
|
}
|
|
|
|
return ((int) ($left['targetQuestion'] ?? 0)) <=> ((int) ($right['targetQuestion'] ?? 0));
|
|
});
|
|
|
|
return $rules;
|
|
}
|
|
|
|
private function resolveListUrl(ModuleModel $model): string
|
|
{
|
|
$page = $this->resolvePage((int) ($model->surveyListPage ?? 0)) ?? $this->getPageModel();
|
|
|
|
return $page instanceof PageModel ? $this->generateContentUrl($page) : '/';
|
|
}
|
|
|
|
private function resolveReaderUrl(ModuleModel $model, string $publicAlias): ?string
|
|
{
|
|
$page = $this->resolvePage((int) $this->getContaoAdapter(Config::class)->get('surveyReaderPage'));
|
|
|
|
if (!$page instanceof PageModel) {
|
|
return null;
|
|
}
|
|
|
|
return $this->generateContentUrl($page, ['auto_item' => $publicAlias]);
|
|
}
|
|
|
|
private function buildEditUrl(ModuleModel $model, Request $request, int $surveyId): string
|
|
{
|
|
$parameters = ['survey' => $surveyId];
|
|
|
|
if ($request->query->getBoolean('debugAccess') && $this->isDebugAccessAllowed($request)) {
|
|
$parameters['debugAccess'] = '1';
|
|
}
|
|
|
|
$debugMemberId = (int) $request->query->get('debugMember', 0);
|
|
|
|
if ($debugMemberId > 0) {
|
|
$parameters['debugMember'] = (string) $debugMemberId;
|
|
}
|
|
|
|
$basePath = $request->getBaseUrl().$request->getPathInfo();
|
|
|
|
if ('' !== $basePath) {
|
|
$queryString = http_build_query($parameters);
|
|
$url = $basePath.('' !== $queryString ? '?'.$queryString : '');
|
|
} else {
|
|
$page = $this->getPageModel();
|
|
$url = $page instanceof PageModel ? $this->generateContentUrl($page, $parameters) : $this->resolveListUrl($model);
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
private function describeFormErrors(FormInterface $form): string
|
|
{
|
|
$messages = [];
|
|
|
|
foreach ($form->getErrors(true, true) as $error) {
|
|
$origin = $error->getOrigin();
|
|
$name = $origin instanceof FormInterface ? $origin->getName() : 'form';
|
|
$messages[] = sprintf('%s: %s', $name, $error->getMessage());
|
|
}
|
|
|
|
return [] !== $messages ? implode(' | ', array_unique($messages)) : 'Unbekannter Formularfehler.';
|
|
}
|
|
|
|
private function resolvePage(int $pageId): ?PageModel
|
|
{
|
|
if ($pageId <= 0) {
|
|
return null;
|
|
}
|
|
|
|
$page = $this->getContaoAdapter(PageModel::class)->findById($pageId);
|
|
|
|
return $page instanceof PageModel ? $page : null;
|
|
}
|
|
} |