<?php declare(strict_types=1);
namespace NetzpEvents6\Subscriber;
use NetzpEvents6\Components\EventsHelper;
use NetzpEvents6\Components\SlotStruct;
use NetzpEvents6\Components\TicketDocumentGenerator;
use NetzpEvents6\Core\SearchResult;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemAddedEvent;
use Shopware\Core\Checkout\Cart\Event\BeforeLineItemQuantityChangedEvent;
use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
use Shopware\Core\Checkout\Cart\Order\CartConvertedEvent;
use Shopware\Core\Content\Product\Events\ProductListingCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductSearchCriteriaEvent;
use Shopware\Core\Content\Product\Events\ProductSuggestCriteriaEvent;
use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\ContainsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Struct\ArrayStruct;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Shopware\Storefront\Controller\CartLineItemController;
use Shopware\Storefront\Page\Account\Order\AccountOrderPageLoadedEvent;
use Shopware\Storefront\Page\Product\ProductPageCriteriaEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
use Shopware\Core\Checkout\Order\Event\OrderStateMachineStateChangeEvent;
use Shopware\Core\Framework\Struct;
use Symfony\Contracts\EventDispatcher\Event;
use NetzpEvents6\Core\Content\Participant\ParticipantEntity;
class FrontendSubscriber implements EventSubscriberInterface
{
const SEARCH_TYPE_EVENTS = 11;
private $container;
private $config;
private $requestStack;
private $helper;
private $withoutParticipants;
private $salesChannelId;
public function __construct(
ContainerInterface $container,
SystemConfigService $config,
RequestStack $requestStack,
EventsHelper $helper
) {
$this->container = $container;
$this->config = $config;
$this->requestStack = $requestStack;
$this->helper = $helper;
$context = $this->requestStack->getCurrentRequest()->attributes->get('sw-context');
$this->salesChannelId = null;
if ($context && $context->getSource() && $context->getSource() instanceof SalesChannelApiSource) {
$this->salesChannelId = $context->getSource()->getSalesChannelId();
}
$pluginConfig = $this->config->get('NetzpEvents6.config', $this->salesChannelId);
$this->withoutParticipants = (bool)$pluginConfig['anonymous'];
}
public static function getSubscribedEvents(): array
{
return [
ProductPageCriteriaEvent::class => 'onProductCriteriaLoaded',
ProductListingCriteriaEvent::class => 'onProductListingCriteria',
ProductSearchCriteriaEvent::class => 'onSearchCriteria',
ProductSuggestCriteriaEvent::class => 'onSuggestCriteria',
BeforeLineItemAddedEvent::class => 'onLineItemAdded',
BeforeLineItemQuantityChangedEvent::class => 'onLineItemUpdated',
CartConvertedEvent::class => 'onCartConverted',
CheckoutOrderPlacedEvent::class => 'onOrderPlaced',
AccountOrderPageLoadedEvent::class => 'onOrderLoaded',
ControllerArgumentsEvent::class => 'onControllerEvent',
'state_enter.order.state.cancelled' => 'orderStateCanceled',
'netzp.search.register' => 'registerSearchProvider'
];
}
public function onOrderLoaded(AccountOrderPageLoadedEvent $event)
{
$repo = $this->container->get('document.repository');
$repoDocumentType = $this->container->get('document_type.repository');
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('technicalName', TicketDocumentGenerator::EVENT_TICKET));
$documentType = $repoDocumentType->search($criteria, $event->getContext())->first();
if( ! $documentType) {
return;
}
foreach ($event->getPage()->getOrders() as $order)
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('orderId', $order->getId()));
$criteria->addFilter(new EqualsFilter('documentTypeId', $documentType->getId()));
$document = $repo->search($criteria, $event->getContext())->first();
if($document) {
$order->addExtension('ticket',
new Struct\ArrayStruct(['id' => $document->getId(), 'deeplinkCode' => $document->getDeeplinkCode()])
);
}
}
}
public function onProductCriteriaLoaded(ProductPageCriteriaEvent $event): void
{
$this->addCriteria($event, true);
}
public function onProductListingCriteria(ProductListingCriteriaEvent $event): void
{
$this->addCriteria($event);
}
public function onSearchCriteria(ProductSearchCriteriaEvent $event): void
{
$this->addCriteria($event);
}
public function onSuggestCriteria(ProductSuggestCriteriaEvent $event): void
{
$this->addCriteria($event);
}
private function addCriteria($event, bool $addFilter = false)
{
$criteria = $event->getCriteria();
$criteria->addAssociation('event');
$criteria->addAssociation('event.slots');
$criteria->addAssociation('eventparent');
$criteria->addAssociation('eventparent.slots');
if($addFilter) {
$bookingPeriod = 'start';
$pluginConfig = $this->config->get('NetzpEvents6.config', $this->salesChannelId);
if(array_key_exists('bookingperiod', $pluginConfig)) {
$bookingPeriod = $pluginConfig['bookingperiod'];
}
$now = $this->helper->getCurrentDateTimeFormatted($this->requestStack);
$maxStartDate = $now;
$slots = $criteria->getAssociation('event')->getAssociation('slots');
if($bookingPeriod == 'end') {
$slots->addFilter(new RangeFilter('until', ['gte' => $now]));
}
else {
$maxStartDate = $this->helper->getBookingStartDate($bookingPeriod)->format('Y-m-d H:i');
$slots->addFilter(new RangeFilter('from', ['gt' => $maxStartDate]));
}
$slots->addFilter(new EqualsFilter('event.archived', false));
$slots->addFilter(new EqualsFilter('active', 1));
$slots->addSorting(new FieldSorting('from', 'ASC'));
$slotsParent = $criteria->getAssociation('eventparent')->getAssociation('slots');
if($bookingPeriod == 'end') {
$slotsParent->addFilter(new RangeFilter('until', ['gte' => $now]));
}
else {
$slotsParent->addFilter(new RangeFilter('from', ['gt' => $maxStartDate]));
}
$slotsParent->addFilter(new EqualsFilter('event.archived', false));
$slotsParent->addFilter(new EqualsFilter('active', 1));
$slotsParent->addSorting(new FieldSorting('from', 'ASC'));
}
}
public function onLineItemAdded(BeforeLineItemAddedEvent $event)
{
$lineItem = $event->getLineItem();
$request = $this->requestStack->getCurrentRequest()->request;
if ($request->has('netzpEventId'))
{
$eventId = $request->get('netzpEventId');
$repo = $this->container->get('s_plugin_netzp_events_slots.repository');
$criteria = new Criteria([$eventId]);
$criteria->addAssociation('event');
$netzpEventSlot = $repo->search($criteria, $event->getContext())->getEntities()->first();
if($netzpEventSlot)
{
$dateFrom = $netzpEventSlot->getFrom() ?
$netzpEventSlot->getFrom()->format(\DateTimeInterface::ATOM) :
'';
$dateUntil = $netzpEventSlot->getUntil() ?
$netzpEventSlot->getUntil()->format(\DateTimeInterface::ATOM) :
'';
$slot = new SlotStruct();
$slot->setId($netzpEventSlot->getId());
$slot->setTitle($netzpEventSlot->getEvent()->getTranslated()['title']);
$slot->setLocation($netzpEventSlot->getTranslated()['location']);
$slot->setAddress($netzpEventSlot->getTranslated()['address']);
$slot->setFrom($dateFrom);
$slot->setUntil($dateUntil);
$slot->setTimezone($netzpEventSlot->getEvent()->getTimezone());
$slot->setAvailable($netzpEventSlot->getTicketsAvailable() - $netzpEventSlot->getTicketsBooked());
$additionalFields = $this->mapAdditionalFields($netzpEventSlot->getEvent()->getAdditionalFields());
$additionalFieldLabels = $this->mapAdditionalFieldsLabels($netzpEventSlot->getEvent()->getAdditionalFields());
$tmpAdditionalFields = [];
for($i = 1; $i <= $lineItem->getQuantity(); $i++) {
$tmpAdditionalFields[$i] = $additionalFields;
}
$slot->setAdditionalFields($tmpAdditionalFields);
$slot->setAdditionalFieldsLabels($additionalFieldLabels);
if($this->withoutParticipants)
{
$participants = [];
$participantsIds = [];
for($i = 1; $i <= $lineItem->getQuantity(); $i++) {
$participants[$i] = '(' . $i . ')';
$participantsIds[$i] = Uuid::randomHex();
}
$slot->setParticipants($participants);
$slot->setParticipantsIds($participantsIds);
}
$lineItem->setPayloadValue('netzp_event', $slot->getVars());
}
}
}
public function onLineItemUpdated(BeforeLineItemQuantityChangedEvent $event)
{
$lineItem = $event->getLineItem();
$request = $this->requestStack->getCurrentRequest()->request;
if ($lineItem->hasPayloadValue('netzp_event'))
{
$payload = $lineItem->getPayloadValue('netzp_event');
if($this->withoutParticipants)
{
$participants = [];
$participantsIds = [];
for($i = 1; $i <= $lineItem->getQuantity(); $i++)
{
$participants[$i] = '(' . $i . ')';
$participantsIds[$i] = Uuid::randomHex();
}
$payload['participants'] = $participants;
$payload['participantsIds'] = $participantsIds;
}
else if($request->has('netzpEventParticipant'))
{
$participants = $request->get('netzpEventParticipant');
$participantsIds = [];
$n = 1;
foreach($participants as $participant)
{
$participantsIds[$n] = Uuid::randomHex();
$n++;
}
$payload['participants'] = $participants;
$payload['participantsIds'] = $participantsIds;
}
if($request->has('netzpEventAdditionalField'))
{
$additionalFields = $request->get('netzpEventAdditionalField');
$payload['additionalFields'] = $additionalFields;
}
if ($lineItem->getQuantity() > count($payload['additionalFields'])) {
$additionalFields = $payload['additionalFields'];
for($i = count($payload['additionalFields']) + 1; $i <= $lineItem->getQuantity(); $i++) {
$firstEntry = array_map(function($f) {
return '';
}, $additionalFields[1]);
$additionalFields[$i] = $firstEntry; // copy from first participant entry
}
$payload['additionalFields'] = $additionalFields;
}
$lineItem->setPayloadValue('netzp_event', $payload);
}
}
public function onControllerEvent(ControllerArgumentsEvent $event): void
{
$controller = $event->getController();
$route = $event->getRequest()->attributes->get('_route') ?? '';
if (is_array($controller) && count($controller) > 0) {
$controller = $controller[0];
}
if ( ! $controller) {
return;
}
if($controller instanceof CartLineItemController && $route === 'frontend.checkout.line-item.add')
{
$args = $event->getArguments();
// prevent article stacking only for events
if(count($args) >= 1 && $event->getRequest()->request->get('netzpEventId', '') != '') {
$keys = $args[1]->get('lineItems')->keys();
if($keys && is_array($keys) && count($keys) > 0) {
$lineItem = $args[1]->get('lineItems')->get($keys[0]);
if ($lineItem) {
$lineItem->set('id', Uuid::randomHex());
$event->setArguments($args);
}
}
}
}
}
public function onCartConverted(CartConvertedEvent $event)
{
$cart = $event->getCart();
$lineItems = $cart->getLineItems();
foreach($lineItems as $lineItem) {
if($lineItem->hasPayloadValue('netzp_event')) {
$this->helper->processEvent($lineItem, $event->getContext());
}
}
}
public function onOrderPlaced(CheckoutOrderPlacedEvent $event)
{
$this->helper->processOrder($event);
}
public function orderStateCanceled(OrderStateMachineStateChangeEvent $event)
{
$repoSlots = $this->container->get('s_plugin_netzp_events_slots.repository');
$repoParticipants = $this->container->get('s_plugin_netzp_events_participants.repository');
$context = $event->getContext();
$lineItems = $event->getOrder()->getLineItems();
foreach($lineItems as $lineItem)
{
$payload = $lineItem->getPayload();
if(array_key_exists('netzp_event', $payload)) {
$ticket = $payload['netzp_event'];
$criteria = new Criteria([$ticket['id']]);
$criteria->addAssociations(['event', 'participants', 'event.product']);
$slot = $repoSlots->search($criteria, $context)->getEntities()->first();
$numberOfParticipants = count($ticket['participants']);
$newTicketsBooked = $slot->getTicketsBooked() - $numberOfParticipants;
$repoSlots->update([
[
'id' => $slot->getId(),
'ticketsBooked' => $newTicketsBooked
]
], $context);
$this->helper->invalidateProductCache($slot->getEvent()->getProductid());
$this->helper->invalidateEventListing();
foreach($ticket['participants'] as $participantNo => $participant) {
$participantId = $ticket['participantsIds'][$participantNo];
$repoParticipants->update([
[
'id' => $participantId,
'status' => ParticipantEntity::STATUS_CANCELED
]
], $context);
}
}
}
}
public function registerSearchProvider(Event $event)
{
$pluginConfig = $this->config->get('NetzpEvents6.config', $this->salesChannelId);
if((bool)$pluginConfig['searchEvents'] === false) {
return;
}
$event->setData([
'key' => 'events',
'label' => 'netzp.events.searchLabel',
'function' => [$this, 'doSearch']
]);
}
public function doSearch(string $query, SalesChannelContext $salesChannelContext, bool $isSuggest = false): array
{
$results = [];
$events = $this->getEvents($query, $salesChannelContext, $isSuggest);
if($events == null) {
return [];
}
foreach ($events->getEntities() as $event) {
if( ! $event->getProduct()) continue;
$tmpResult = new SearchResult();
$tmpResult->setType(static::SEARCH_TYPE_EVENTS);
$tmpResult->setId($event->getId());
$tmpResult->setTitle($event->getTranslated()['title'] ?? '');
$tmpResult->setDescription($event->getTranslated()['teaser'] ?? '');
$tmpResult->setMedia($event->getImage());
$tmpResult->setTotal($events->getTotal());
$tmpResult->addExtension('productId', new ArrayStruct(['value' => $event->getProduct()->getId()]));
$tmpResult->addExtension('slots', new ArrayStruct(['value' => $event->getSlots()->count()]));
$results[] = $tmpResult;
}
return $results;
}
private function getEvents($query, SalesChannelContext $salesChannelContext, bool $isSuggest = false)
{
$query = trim($query);
$words = explode(' ', $query);
if (count($words) > 0) {
$now = $this->helper->getCurrentDateTimeFormatted($this->requestStack);
$repo = $this->container->get('s_plugin_netzp_events.repository');
$criteria = new Criteria();
$criteria->addAssociation('product');
$criteria->addAssociation('image');
$criteria->addAssociation('slots');
$slots = $criteria->getAssociation('slots');
$slots->addSorting(new FieldSorting('from', 'ASC'));
$slots->addFilter(new RangeFilter('from', ['gt' => $now]));
$slots->addFilter(new EqualsFilter('active', true));
$criteria->addSorting(new FieldSorting('title', FieldSorting::ASCENDING));
$criteria->addFilter(new EqualsFilter('bookable', true));
$criteria->addFilter(new EqualsFilter('archived', false));
$filter = [];
foreach ($words as $word) {
$filter[] = new ContainsFilter('title', $word);
$filter[] = new ContainsFilter('teaser', $word);
$filter[] = new ContainsFilter('description', $word);
}
$criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, $filter));
return $repo->search($criteria, $salesChannelContext->getContext());
}
return null;
}
private function mapAdditionalFields(?string $additionalFields): array
{
if($additionalFields === null || $additionalFields == '') {
return [];
}
$fields = array_map(function($field) {
return $this->sanitizeAdditionalField($field);
}, explode(';', $additionalFields));
$fields = array_flip($fields);
$fields = array_map(function($f) {
return '';
}, $fields);
return $fields;
}
private function mapAdditionalFieldsLabels(?string $additionalFields): array
{
if($additionalFields === null || $additionalFields == '') {
return [];
}
$fields = explode(';', $additionalFields);
$tmp = [];
foreach($fields as $field) {
$f = $this->sanitizeAdditionalField($field);
$tmp[$f] = $field;
}
return $tmp;
}
private function sanitizeAdditionalField(?string $f)
{
$f = trim($f);
$f = strtolower($f);
$f = str_replace(' ', '', $f);
$f = str_replace('/', '', $f);
$f = str_replace('_', '', $f);
$f = str_replace(',', '', $f);
$f = str_replace(':', '', $f);
$f = str_replace(';', '', $f);
$f = str_replace('#', '', $f);
$f = str_replace('@', '', $f);
$f = str_replace('$', '', $f);
$f = str_replace('€', '', $f);
$f = str_replace('+', '', $f);
$f = str_replace('-', '', $f);
$f = str_replace('\'', '', $f);
$f = str_replace('\"', '', $f);
return $f;
}
}