<?php
/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);
namespace Ibexa\Bundle\Cart\EventSubscriber;
use DateTimeImmutable;
use Ibexa\Cart\Persistence\Legacy\Cart\Gateway\StorageSchema as CartStorageSchema;
use Ibexa\Cart\Persistence\Legacy\Cart\Handler\HandlerInterface as CartHandler;
use Ibexa\Cart\Persistence\Legacy\CartEntry\Gateway\StorageSchema as CartEntryStorageSchema;
use Ibexa\Cart\Persistence\Legacy\CartEntry\Handler\HandlerInterface as CartEntryHandler;
use Ibexa\Cart\Persistence\Values\Cart;
use Ibexa\Cart\Persistence\Values\CartCreateStruct;
use Ibexa\Cart\Persistence\Values\CartEntry;
use Ibexa\Cart\Persistence\Values\CartEntryCreateStruct;
use Ibexa\Cart\Persistence\Values\CartEntryUpdateStruct;
use Ibexa\Cart\Persistence\Values\CartMetadataUpdateStruct;
use Ibexa\Contracts\Core\Persistence\TransactionHandler;
use Ibexa\Contracts\Core\Repository\Values\User\User as ApiUser;
use Ibexa\Contracts\Core\SiteAccess\ConfigResolverInterface;
use Ibexa\Core\Base\Exceptions\BadStateException;
use Ibexa\Core\MVC\Symfony\Security\User;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Uid\Uuid;
final class SecurityLoginSubscriber implements EventSubscriberInterface
{
private ConfigResolverInterface $configResolver;
private CartHandler $cartSessionHandler;
private CartHandler $cartDatabaseHandler;
private CartEntryHandler $cartEntrySessionHandler;
private CartEntryHandler $cartEntryDatabaseHandler;
private TransactionHandler $transactionHandler;
public function __construct(
ConfigResolverInterface $configResolver,
CartHandler $cartSessionHandler,
CartHandler $cartDatabaseHandler,
CartEntryHandler $cartEntrySessionHandler,
CartEntryHandler $cartEntryDatabaseHandler,
TransactionHandler $transactionHandler
) {
$this->configResolver = $configResolver;
$this->cartSessionHandler = $cartSessionHandler;
$this->cartDatabaseHandler = $cartDatabaseHandler;
$this->cartEntrySessionHandler = $cartEntrySessionHandler;
$this->cartEntryDatabaseHandler = $cartEntryDatabaseHandler;
$this->transactionHandler = $transactionHandler;
}
public static function getSubscribedEvents(): array
{
return [
SecurityEvents::INTERACTIVE_LOGIN => ['onInteractiveLogin', 100],
];
}
public function onInteractiveLogin(InteractiveLoginEvent $event): void
{
$token = $event->getAuthenticationToken();
$user = $token->getUser();
if (!$user instanceof User) {
return;
}
$apiUser = $user->getAPIUser();
if ($apiUser->getUserId() === $this->getAnonymousId()) {
throw new BadStateException('$getUserId', 'Logged in as Anonymous');
}
// skip checking the cart session if the session is not running to prevent creating a new session
if (!$event->getRequest()->getSession()->isStarted()) {
return;
}
$sessionCart = $this->cartSessionHandler->findOneBy([]);
$databaseCart = $this->cartDatabaseHandler->findOneBy([
CartStorageSchema::COLUMN_OWNER_ID => $apiUser->getUserId(),
]);
if (null === $sessionCart) {
// nothing to merge
return;
}
$entries = $this->cartEntrySessionHandler->findBy([
CartEntryStorageSchema::COLUMN_CART_ID => $sessionCart->getId(),
]);
if (null === $databaseCart) {
$this->handleNewCart($sessionCart, $apiUser, $entries);
} else {
$this->handleExistingCart($databaseCart, $sessionCart, $entries);
}
$this->removeSessionCart($sessionCart, $entries);
}
private function getAnonymousId(): int
{
return (int) $this->configResolver->getParameter('anonymous_user_id');
}
/**
* @param array<\Ibexa\Cart\Persistence\Values\CartEntry> $entries
*/
private function getEntryUsingProductCode(array $entries, string $productCode): ?CartEntry
{
foreach ($entries as $entry) {
if ($entry->getProductCode() === $productCode) {
return $entry;
}
}
return null;
}
/**
* @param array<\Ibexa\Cart\Persistence\Values\CartEntry> $entries
*/
private function removeSessionCart(Cart $sessionCart, array $entries): void
{
foreach ($entries as $entry) {
$this->cartEntrySessionHandler->delete($entry->getId());
}
$this->cartSessionHandler->delete($sessionCart->getId());
}
private function updateEntry(CartEntry $existingDatabaseEntry, CartEntry $entry): void
{
$cartEntryUpdateStruct = new CartEntryUpdateStruct(
$existingDatabaseEntry->getId(),
$existingDatabaseEntry->getIdentifier(),
$existingDatabaseEntry->getCartId(),
$existingDatabaseEntry->getNames(),
$existingDatabaseEntry->getQuantity() + $entry->getQuantity(),
$existingDatabaseEntry->getPrice(),
$existingDatabaseEntry->getCurrencyId(),
$existingDatabaseEntry->getProductCode(),
$existingDatabaseEntry->getContext()
);
$this->cartEntryDatabaseHandler->update($cartEntryUpdateStruct);
}
private function createEntry(Cart $databaseCart, CartEntry $entry): void
{
$cartEntryCreateStruct = new CartEntryCreateStruct(
Uuid::v4()->toRfc4122(),
$databaseCart->getId(),
$entry->getNames(),
$entry->getQuantity(),
$entry->getPrice(),
$entry->getCurrencyId(),
$entry->getProductCode(),
new DateTimeImmutable(),
$entry->getContext()
);
$this->cartEntryDatabaseHandler->create($cartEntryCreateStruct);
}
private function createCart(Cart $sessionCart, ApiUser $user): Cart
{
$cartCreateStruct = new CartCreateStruct(
$sessionCart->getIdentifier(),
$sessionCart->getName(),
$user->getUserId(),
$sessionCart->getCurrencyId(),
new DateTimeImmutable(),
new DateTimeImmutable(),
$sessionCart->getContext(),
);
return $this->cartDatabaseHandler->create($cartCreateStruct);
}
/**
* @param array<\Ibexa\Cart\Persistence\Values\CartEntry> $entries
*/
private function handleNewCart(Cart $sessionCart, ApiUser $user, array $entries): void
{
$this->transactionHandler->beginTransaction();
try {
$databaseCart = $this->createCart($sessionCart, $user);
foreach ($entries as $entry) {
$this->createEntry($databaseCart, $entry);
}
$this->transactionHandler->commit();
} catch (\Exception $e) {
$this->transactionHandler->rollback();
throw $e;
}
}
/**
* @param array<\Ibexa\Cart\Persistence\Values\CartEntry> $entries
*/
private function handleExistingCart(
Cart $databaseCart,
Cart $sessionCart,
array $entries
): void {
$databaseCartEntries = $this->cartEntryDatabaseHandler->findBy([
CartEntryStorageSchema::COLUMN_CART_ID => $databaseCart->getId(),
]);
$this->transactionHandler->beginTransaction();
try {
$this->updateCart($databaseCart, $sessionCart);
foreach ($entries as $entry) {
$existingDatabaseEntry = $this->getEntryUsingProductCode(
$databaseCartEntries,
$entry->getProductCode()
);
if (null !== $existingDatabaseEntry) {
$this->updateEntry($existingDatabaseEntry, $entry);
} else {
$this->createEntry($databaseCart, $entry);
}
}
$this->transactionHandler->commit();
} catch (\Exception $e) {
$this->transactionHandler->rollback();
throw $e;
}
}
private function updateCart(
Cart $databaseCart,
Cart $sessionCart
): void {
$existingContext = $databaseCart->getContext();
$sessionContext = $sessionCart->getContext();
$context = $sessionContext;
if ($existingContext !== null) {
$context = array_merge($sessionContext ?? [], $existingContext);
}
$this->cartDatabaseHandler->update(
new CartMetadataUpdateStruct(
$databaseCart->getId(),
$sessionCart->getIdentifier(),
$sessionCart->getName(),
$sessionCart->getCurrencyId(),
$databaseCart->getOwnerId(),
$databaseCart->getCreated(),
new DateTimeImmutable(),
$context
)
);
}
}