> */ public function findByMemberId(int $memberId): array { return $this->connection->createQueryBuilder() ->select('DISTINCT e.*') ->from('tl_calendar_events', 'e') ->innerJoin('e', 'tl_calendar_events_organization', 'ceo', 'ceo.event_id = e.id') ->innerJoin('ceo', 'tl_member_organization', 'mo', 'mo.organization_id = ceo.organization_id') ->where('mo.member_id = :memberId') ->setParameter('memberId', $memberId, ParameterType::INTEGER) ->orderBy('e.startDate', 'DESC') ->executeQuery() ->fetchAllAssociative(); } public function memberHasEvent(int $memberId, int $eventId): bool { $exists = $this->connection->createQueryBuilder() ->select('1') ->from('tl_calendar_events', 'e') ->innerJoin('e', 'tl_calendar_events_organization', 'ceo', 'ceo.event_id = e.id') ->innerJoin('ceo', 'tl_member_organization', 'mo', 'mo.organization_id = ceo.organization_id') ->where('e.id = :eventId') ->andWhere('mo.member_id = :memberId') ->setParameter('eventId', $eventId, ParameterType::INTEGER) ->setParameter('memberId', $memberId, ParameterType::INTEGER) ->setMaxResults(1) ->executeQuery() ->fetchOne(); return false !== $exists; } public function togglePublished(int $eventId): void { $published = $this->connection->createQueryBuilder() ->select('published') ->from('tl_calendar_events') ->where('id = :id') ->setParameter('id', $eventId, ParameterType::INTEGER) ->setMaxResults(1) ->executeQuery() ->fetchOne(); $isPublished = '1' === (string) $published; $this->connection->update( 'tl_calendar_events', [ 'published' => $isPublished ? 0 : 1, 'tstamp' => time(), ], ['id' => $eventId], ['id' => ParameterType::INTEGER], ); } public function duplicate(int $eventId): ?int { $row = $this->findById($eventId); if (null === $row) { return null; } unset($row['id']); if (array_key_exists('title', $row)) { $normalizedTitle = html_entity_decode((string) $row['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8'); $row['title'] = trim($normalizedTitle.' (Kopie)'); } if (array_key_exists('alias', $row)) { $row['alias'] = ''; } if (array_key_exists('published', $row)) { $row['published'] = 0; } $now = time(); if (array_key_exists('dateAdded', $row)) { $row['dateAdded'] = $now; } if (array_key_exists('tstamp', $row)) { $row['tstamp'] = $now; } $this->connection->insert('tl_calendar_events', $row); $newEventId = (int) $this->connection->lastInsertId(); $organizationIds = $this->connection->createQueryBuilder() ->select('organization_id') ->from('tl_calendar_events_organization') ->where('event_id = :eventId') ->setParameter('eventId', $eventId, ParameterType::INTEGER) ->executeQuery() ->fetchFirstColumn(); foreach ($organizationIds as $organizationId) { $this->connection->insert( 'tl_calendar_events_organization', [ 'event_id' => $newEventId, 'organization_id' => (int) $organizationId, ], [ 'event_id' => ParameterType::INTEGER, 'organization_id' => ParameterType::INTEGER, ], ); } $tagIds = $this->getTagIdsForEvent($eventId); if ([] !== $tagIds) { $this->assignTagsToEvent($newEventId, $tagIds); } return $newEventId; } public function createDraftForMember(int $memberId, int $authorId, int $archiveId): ?int { $resolvedArchiveId = $this->resolveArchiveId($archiveId); if ($resolvedArchiveId <= 0) { return null; } $organizationId = $this->connection->createQueryBuilder() ->select('organization_id') ->from('tl_member_organization') ->where('member_id = :memberId') ->setParameter('memberId', $memberId, ParameterType::INTEGER) ->orderBy('organization_id', 'ASC') ->setMaxResults(1) ->executeQuery() ->fetchOne(); if (false === $organizationId) { return null; } $now = time(); $this->connection->insert( 'tl_calendar_events', [ 'pid' => $resolvedArchiveId, 'author' => max(0, $authorId), 'title' => '', 'alias' => '', 'startDate' => 0, 'endDate' => null, 'published' => 0, 'tstamp' => $now, ], [ 'pid' => ParameterType::INTEGER, 'author' => ParameterType::INTEGER, ], ); $eventId = (int) $this->connection->lastInsertId(); if ($eventId <= 0) { return null; } $this->connection->insert( 'tl_calendar_events_organization', [ 'tstamp' => $now, 'event_id' => $eventId, 'organization_id' => (int) $organizationId, ], [ 'event_id' => ParameterType::INTEGER, 'organization_id' => ParameterType::INTEGER, ], ); return $eventId; } /** @return array{authorId:int,archiveId:int} */ public function findCreationDefaultsByListPage(int $listPageId): array { if ($listPageId <= 0) { return ['authorId' => 0, 'archiveId' => 0]; } $row = $this->connection->createQueryBuilder() ->select('frontendAuthorId', 'frontendArchiveId') ->from('tl_module') ->where('type = :type') ->andWhere('listPage = :listPage') ->setParameter('type', 'event_edit') ->setParameter('listPage', $listPageId, ParameterType::INTEGER) ->orderBy('id', 'ASC') ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); if (false === $row || null === $row) { return ['authorId' => 0, 'archiveId' => 0]; } return [ 'authorId' => (int) ($row['frontendAuthorId'] ?? 0), 'archiveId' => (int) ($row['frontendArchiveId'] ?? 0), ]; } private function resolveArchiveId(int $archiveId): int { if ($archiveId > 0) { $exists = $this->connection->createQueryBuilder() ->select('id') ->from('tl_calendar') ->where('id = :id') ->setParameter('id', $archiveId, ParameterType::INTEGER) ->setMaxResults(1) ->executeQuery() ->fetchOne(); if (false !== $exists) { return $archiveId; } } $fallback = $this->connection->createQueryBuilder() ->select('id') ->from('tl_calendar') ->orderBy('id', 'ASC') ->setMaxResults(1) ->executeQuery() ->fetchOne(); return false !== $fallback ? (int) $fallback : 0; } public function delete(int $eventId): void { $this->connection->delete( 'tl_tags_rel', ['ptable' => 'tl_calendar_events', 'field' => 'tags', 'pid' => $eventId], ['pid' => ParameterType::INTEGER], ); $this->connection->delete( 'tl_calendar_events_organization', ['event_id' => $eventId], ['event_id' => ParameterType::INTEGER], ); $this->connection->delete( 'tl_calendar_events', ['id' => $eventId], ['id' => ParameterType::INTEGER], ); } /** @return array|null */ public function findById(int $eventId): ?array { $row = $this->connection->createQueryBuilder() ->select('*') ->from('tl_calendar_events') ->where('id = :id') ->setParameter('id', $eventId, ParameterType::INTEGER) ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); return false === $row ? null : $row; } /** @return array{id:int,title:string}|null */ public function findPrimaryOrganizationForEvent(int $eventId): ?array { $row = $this->connection->createQueryBuilder() ->select('o.id', 'o.title') ->from('tl_calendar_events_organization', 'ceo') ->innerJoin('ceo', 'tl_organization', 'o', 'o.id = ceo.organization_id') ->where('ceo.event_id = :eventId') ->setParameter('eventId', $eventId, ParameterType::INTEGER) ->orderBy('ceo.organization_id', 'ASC') ->setMaxResults(1) ->executeQuery() ->fetchAssociative(); if (false === $row || null === $row) { return null; } return [ 'id' => (int) $row['id'], 'title' => (string) ($row['title'] ?? ''), ]; } /** @return array */ public function getOrganizationIdsForEvent(int $eventId): array { $rows = $this->connection->createQueryBuilder() ->select('organization_id') ->from('tl_calendar_events_organization') ->where('event_id = :eventId') ->setParameter('eventId', $eventId, ParameterType::INTEGER) ->orderBy('organization_id', 'ASC') ->executeQuery() ->fetchFirstColumn(); return array_values(array_unique(array_map('intval', $rows))); } /** * @return array */ public function getTagChoicesForEventType(array $allowedTagIds = []): array { $rows = $this->connection->createQueryBuilder() ->select('DISTINCT t.id', 't.tag') ->from('tl_tags', 't') ->innerJoin('t', 'tl_tags_rel', 'r', 'r.tag_id = t.id') ->where('r.ptable = :ptable') ->andWhere('r.field = :field') ->setParameter('ptable', 'tl_calendar_events') ->setParameter('field', 'tags') ->orderBy('t.tag', 'ASC') ->executeQuery() ->fetchAllAssociative(); if ([] === $rows) { $rows = $this->connection->createQueryBuilder() ->select('t.id', 't.tag') ->from('tl_tags', 't') ->orderBy('t.tag', 'ASC') ->executeQuery() ->fetchAllAssociative(); } $choices = []; foreach ($rows as $row) { $choices[(string) ($row['tag'] ?? '')] = (int) $row['id']; } $allowedTagIds = array_values(array_unique(array_map('intval', $allowedTagIds))); if ([] !== $allowedTagIds) { $choices = array_filter( $choices, static fn (int $id): bool => in_array($id, $allowedTagIds, true), ); } return $choices; } /** @return array */ public function getTagIdsForEvent(int $eventId): array { if ($eventId <= 0) { return []; } $ids = $this->connection->createQueryBuilder() ->select('tag_id') ->from('tl_tags_rel') ->where('ptable = :ptable') ->andWhere('field = :field') ->andWhere('pid = :pid') ->setParameter('ptable', 'tl_calendar_events') ->setParameter('field', 'tags') ->setParameter('pid', $eventId, ParameterType::INTEGER) ->orderBy('tag_id', 'ASC') ->executeQuery() ->fetchFirstColumn(); return array_values(array_unique(array_map('intval', $ids))); } /** * @return array */ public function getLocationChoices(): array { $rows = $this->connection->createQueryBuilder() ->select('id', 'title') ->from('tl_location') ->where('published = :published') ->setParameter('published', '1') ->orderBy('title', 'ASC') ->executeQuery() ->fetchAllAssociative(); $choices = []; foreach ($rows as $row) { $choices[(string) $row['title']] = (int) $row['id']; } return $choices; } /** * @return array */ public function getOrganizationChoicesForMember(int $memberId): array { $rows = $this->connection->createQueryBuilder() ->select('o.id', 'o.title') ->from('tl_member_organization', 'mo') ->innerJoin('mo', 'tl_organization', 'o', 'o.id = mo.organization_id') ->where('mo.member_id = :memberId') ->setParameter('memberId', $memberId, ParameterType::INTEGER) ->orderBy('o.title', 'ASC') ->executeQuery() ->fetchAllAssociative(); $choices = []; $seenLabels = []; foreach ($rows as $row) { $id = (int) $row['id']; $label = trim((string) ($row['title'] ?? '')); if ('' === $label) { $label = 'Organisation '.$id; } if (isset($seenLabels[$label])) { $label .= ' (#'.$id.')'; } $seenLabels[$label] = true; $choices[$label] = $id; } return $choices; } /** @return array */ public function getOrganizationIdsForMember(int $memberId): array { $ids = $this->connection->createQueryBuilder() ->select('organization_id') ->from('tl_member_organization') ->where('member_id = :memberId') ->setParameter('memberId', $memberId, ParameterType::INTEGER) ->orderBy('organization_id', 'ASC') ->executeQuery() ->fetchFirstColumn(); return array_values(array_unique(array_map('intval', $ids))); } /** @param array $organizationIds */ public function assignEventToOrganizations(int $eventId, array $organizationIds): void { $this->connection->delete( 'tl_calendar_events_organization', ['event_id' => $eventId], ['event_id' => ParameterType::INTEGER], ); $organizationIds = array_values(array_unique(array_map('intval', $organizationIds))); foreach ($organizationIds as $organizationId) { if ($organizationId <= 0) { continue; } $this->connection->insert( 'tl_calendar_events_organization', [ 'tstamp' => time(), 'event_id' => $eventId, 'organization_id' => $organizationId, ], [ 'event_id' => ParameterType::INTEGER, 'organization_id' => ParameterType::INTEGER, ], ); } } /** * @param array $data */ public function update(int $eventId, array $data): void { $eventData = $this->buildEventData($data); $this->connection->update( 'tl_calendar_events', $eventData, ['id' => $eventId], [ 'id' => ParameterType::INTEGER, ], ); } /** * @param array $data * @param array $organizationIds */ public function createForMember(int $memberId, int $authorId, int $archiveId, array $data, array $organizationIds): ?int { if ($memberId <= 0) { return null; } $resolvedArchiveId = $this->resolveArchiveId($archiveId); if ($resolvedArchiveId <= 0) { return null; } $eventData = $this->buildEventData($data); $eventData['pid'] = $resolvedArchiveId; $eventData['author'] = max(0, $authorId); $this->connection->insert( 'tl_calendar_events', $eventData, [ 'pid' => ParameterType::INTEGER, 'author' => ParameterType::INTEGER, ], ); $eventId = (int) $this->connection->lastInsertId(); if ($eventId <= 0) { return null; } $memberOrganizationIds = $this->getOrganizationIdsForMember($memberId); $allowedOrganizationIds = array_values(array_intersect(array_map('intval', $organizationIds), array_map('intval', $memberOrganizationIds))); if ([] === $allowedOrganizationIds) { $this->connection->delete('tl_calendar_events', ['id' => $eventId], ['id' => ParameterType::INTEGER]); return null; } $this->assignEventToOrganizations($eventId, $allowedOrganizationIds); return $eventId; } /** @param array $tagIds */ public function assignTagsToEvent(int $eventId, array $tagIds): void { $this->connection->delete( 'tl_tags_rel', ['ptable' => 'tl_calendar_events', 'field' => 'tags', 'pid' => $eventId], ['pid' => ParameterType::INTEGER], ); $tagIds = array_values(array_unique(array_map('intval', $tagIds))); if ([] === $tagIds) { return; } $allowedTagIds = $this->getAllowedTagIdsForEvents(); $tagIds = array_values(array_intersect($tagIds, $allowedTagIds)); foreach ($tagIds as $tagId) { $this->connection->insert( 'tl_tags_rel', [ 'tag_id' => $tagId, 'pid' => $eventId, 'ptable' => 'tl_calendar_events', 'field' => 'tags', ], [ 'tag_id' => ParameterType::INTEGER, 'pid' => ParameterType::INTEGER, ], ); } } /** * @param array $data * * @return array */ private function buildEventData(array $data): array { $startDate = !empty($data['startDate']) ? strtotime((string) $data['startDate']) : false; $endDate = !empty($data['endDate']) ? strtotime((string) $data['endDate']) : false; $url = trim((string) ($data['url'] ?? '')); $addTime = !empty($data['addTime']); $alias = $this->buildAlias($startDate, (string) ($data['title'] ?? '')); $startDateTimestamp = false !== $startDate ? (int) $startDate : 0; $endDateTimestamp = false !== $endDate ? (int) $endDate : 0; $timeBaseTimestamp = $endDateTimestamp > 0 ? $endDateTimestamp : $startDateTimestamp; $startTimeTimestamp = $startDateTimestamp; $endTimeTimestamp = $timeBaseTimestamp > 0 ? $timeBaseTimestamp + 86399 : 0; if ($addTime) { $startTimeCandidate = $this->combineDateAndTime($startDateTimestamp, (string) ($data['startTime'] ?? '')); $startTimeTimestamp = null !== $startTimeCandidate ? $startTimeCandidate : $startDateTimestamp; $endTimeCandidate = $this->combineDateAndTime($timeBaseTimestamp, (string) ($data['endTime'] ?? '')); $endTimeTimestamp = null !== $endTimeCandidate ? $endTimeCandidate : $startTimeTimestamp; } return [ 'title' => $data['title'] ?? '', 'alias' => $alias, 'startDate' => false !== $startDate ? $startDate : 0, 'endDate' => false !== $endDate ? $endDate : null, 'location_id' => (int) ($data['location_id'] ?? 0), 'addTime' => $addTime ? 1 : 0, 'startTime' => $startTimeTimestamp, 'endTime' => $endTimeTimestamp, 'teaser' => $data['teaser'] ?? null, 'description' => $data['description'] ?? null, 'url' => $url, 'source' => '' !== $url ? 'external' : 'default', 'photographer' => $data['photographer'] ?? '', 'addImage' => !empty($data['addImage']) ? 1 : 0, 'termsAccepted' => !empty($data['termsAccepted']) ? '1' : '', 'isSoldOut' => !empty($data['isSoldOut']) ? '1' : '', 'isCanceled' => !empty($data['isCanceled']) ? '1' : '', 'published' => !empty($data['published']) ? 1 : 0, 'tstamp' => time(), ]; } private function buildAlias(false|int $startDate, string $title): string { if (false === $startDate || $startDate <= 0) { return ''; } $normalizedTitle = trim($title); if ('' === $normalizedTitle) { return ''; } $normalizedTitle = strtolower($normalizedTitle); $transliterated = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $normalizedTitle); if (false !== $transliterated) { $normalizedTitle = strtolower($transliterated); } $normalizedTitle = preg_replace('/[^a-z0-9]+/', '', $normalizedTitle) ?? ''; $normalizedTitle = substr($normalizedTitle, 0, 20); if ('' === $normalizedTitle) { return ''; } return date('Y-m-d', $startDate).'_'.$normalizedTitle; } private function combineDateAndTime(int $dateTimestamp, string $time): ?int { if ($dateTimestamp <= 0) { return null; } $normalizedTime = trim($time); if (!preg_match('/^([01]\d|2[0-3]):([0-5]\d)$/', $normalizedTime, $matches)) { return null; } $hours = (int) $matches[1]; $minutes = (int) $matches[2]; return $dateTimestamp + ($hours * 3600) + ($minutes * 60); } public function updateImageFields(int $eventId, bool $addImage, ?string $singleSrcUuid): void { $types = [ 'id' => ParameterType::INTEGER, ]; if (null !== $singleSrcUuid) { $types['singleSRC'] = ParameterType::BINARY; } $this->connection->update( 'tl_calendar_events', [ 'addImage' => $addImage ? 1 : 0, 'singleSRC' => $singleSrcUuid, 'tstamp' => time(), ], ['id' => $eventId], $types, ); } /** @return array */ private function getAllowedTagIdsForEvents(): array { $ids = $this->connection->createQueryBuilder() ->select('DISTINCT r.tag_id') ->from('tl_tags_rel', 'r') ->where('r.ptable = :ptable') ->andWhere('r.field = :field') ->setParameter('ptable', 'tl_calendar_events') ->setParameter('field', 'tags') ->executeQuery() ->fetchFirstColumn(); $ids = array_values(array_unique(array_map('intval', $ids))); if ([] !== $ids) { return $ids; } return array_values(array_unique(array_map('intval', $this->connection->createQueryBuilder() ->select('id') ->from('tl_tags') ->executeQuery() ->fetchFirstColumn()))); } }