407 lines
13 KiB
PHP
407 lines
13 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Mummert\SurveyBundle\Repository;
|
|
|
|
use Contao\CoreBundle\Framework\ContaoFramework;
|
|
use Contao\StringUtil;
|
|
use Doctrine\DBAL\ArrayParameterType;
|
|
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');
|
|
$qb->andWhere('isActive = 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.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<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),
|
|
'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<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', 'templatefilter', 'templatesummary', '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;
|
|
}
|
|
} |