vendor/shopware/core/System/SalesChannel/Context/BaseContextFactory.php line 80

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\SalesChannel\Context;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\Delivery\Struct\ShippingLocation;
  5. use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
  6. use Shopware\Core\Checkout\Customer\Aggregate\CustomerGroup\CustomerGroupEntity;
  7. use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
  8. use Shopware\Core\Checkout\Payment\PaymentMethodEntity;
  9. use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
  10. use Shopware\Core\Defaults;
  11. use Shopware\Core\Framework\Api\Context\AdminSalesChannelApiSource;
  12. use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
  13. use Shopware\Core\Framework\Context;
  14. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  18. use Shopware\Core\Framework\Log\Package;
  19. use Shopware\Core\Framework\Routing\Exception\LanguageNotFoundException;
  20. use Shopware\Core\Framework\Uuid\Uuid;
  21. use Shopware\Core\System\Country\Aggregate\CountryState\CountryStateEntity;
  22. use Shopware\Core\System\Country\CountryEntity;
  23. use Shopware\Core\System\Currency\Aggregate\CurrencyCountryRounding\CurrencyCountryRoundingEntity;
  24. use Shopware\Core\System\Currency\CurrencyEntity;
  25. use Shopware\Core\System\SalesChannel\BaseContext;
  26. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  27. use Shopware\Core\System\Tax\TaxCollection;
  28. use function array_unique;
  29. /**
  30.  * @internal
  31.  */
  32. #[Package('core')]
  33. class BaseContextFactory extends AbstractBaseContextFactory
  34. {
  35.     public function __construct(
  36.         private readonly EntityRepository $salesChannelRepository,
  37.         private readonly EntityRepository $currencyRepository,
  38.         private readonly EntityRepository $customerGroupRepository,
  39.         private readonly EntityRepository $countryRepository,
  40.         private readonly EntityRepository $taxRepository,
  41.         private readonly EntityRepository $paymentMethodRepository,
  42.         private readonly EntityRepository $shippingMethodRepository,
  43.         private readonly Connection $connection,
  44.         private readonly EntityRepository $countryStateRepository,
  45.         private readonly EntityRepository $currencyCountryRepository
  46.     ) {
  47.     }
  48.     public function create(string $salesChannelId, array $options = []): BaseContext
  49.     {
  50.         $context $this->getContext($salesChannelId$options);
  51.         $criteria = new Criteria([$salesChannelId]);
  52.         $criteria->setTitle('base-context-factory::sales-channel');
  53.         $criteria->addAssociation('currency');
  54.         $criteria->addAssociation('domains');
  55.         /** @var SalesChannelEntity|null $salesChannel */
  56.         $salesChannel $this->salesChannelRepository->search($criteria$context)
  57.             ->get($salesChannelId);
  58.         if (!$salesChannel) {
  59.             throw new \RuntimeException(sprintf('Sales channel with id %s not found or not valid!'$salesChannelId));
  60.         }
  61.         //load active currency, fallback to shop currency
  62.         /** @var CurrencyEntity $currency */
  63.         $currency $salesChannel->getCurrency();
  64.         if (\array_key_exists(SalesChannelContextService::CURRENCY_ID$options)) {
  65.             $currencyId $options[SalesChannelContextService::CURRENCY_ID];
  66.             \assert(\is_string($currencyId) && Uuid::isValid($currencyId));
  67.             $criteria = new Criteria([$currencyId]);
  68.             $criteria->setTitle('base-context-factory::currency');
  69.             /** @var CurrencyEntity|null $currency */
  70.             $currency $this->currencyRepository->search($criteria$context)->get($currencyId);
  71.             if (!$currency) {
  72.                 throw new \RuntimeException(sprintf('Currency with id %s not found'$currencyId));
  73.             }
  74.         }
  75.         //load not logged in customer with default shop configuration or with provided checkout scopes
  76.         $shippingLocation $this->loadShippingLocation($options$context$salesChannel);
  77.         $groupId $salesChannel->getCustomerGroupId();
  78.         $criteria = new Criteria([$salesChannel->getCustomerGroupId()]);
  79.         $criteria->setTitle('base-context-factory::customer-group');
  80.         $customerGroups $this->customerGroupRepository->search($criteria$context);
  81.         /** @var CustomerGroupEntity $customerGroup */
  82.         $customerGroup $customerGroups->get($groupId);
  83.         //loads tax rules based on active customer and delivery address
  84.         $taxRules $this->getTaxRules($context);
  85.         //detect active payment method, first check if checkout defined other payment method, otherwise validate if customer logged in, at least use shop default
  86.         $payment $this->getPaymentMethod($options$context$salesChannel);
  87.         //detect active delivery method, at first checkout scope, at least shop default method
  88.         $shippingMethod $this->getShippingMethod($options$context$salesChannel);
  89.         [$itemRounding$totalRounding] = $this->getCashRounding($currency$shippingLocation$context);
  90.         $context = new Context(
  91.             $context->getSource(),
  92.             [],
  93.             $currency->getId(),
  94.             $context->getLanguageIdChain(),
  95.             $context->getVersionId(),
  96.             $currency->getFactor(),
  97.             true,
  98.             CartPrice::TAX_STATE_GROSS,
  99.             $itemRounding
  100.         );
  101.         return new BaseContext(
  102.             $context,
  103.             $salesChannel,
  104.             $currency,
  105.             $customerGroup,
  106.             $taxRules,
  107.             $payment,
  108.             $shippingMethod,
  109.             $shippingLocation,
  110.             $itemRounding,
  111.             $totalRounding
  112.         );
  113.     }
  114.     private function getTaxRules(Context $context): TaxCollection
  115.     {
  116.         $criteria = new Criteria();
  117.         $criteria->setTitle('base-context-factory::taxes');
  118.         $criteria->addAssociation('rules.type');
  119.         /** @var TaxCollection $taxes */
  120.         $taxes $this->taxRepository->search($criteria$context)->getEntities();
  121.         return $taxes;
  122.     }
  123.     /**
  124.      * @param array<string, mixed> $options
  125.      */
  126.     private function getPaymentMethod(array $optionsContext $contextSalesChannelEntity $salesChannel): PaymentMethodEntity
  127.     {
  128.         $id $options[SalesChannelContextService::PAYMENT_METHOD_ID] ?? $salesChannel->getPaymentMethodId();
  129.         $criteria = (new Criteria([$id]))->addAssociation('media');
  130.         $criteria->setTitle('base-context-factory::payment-method');
  131.         /** @var PaymentMethodEntity|null $paymentMethod */
  132.         $paymentMethod $this->paymentMethodRepository
  133.             ->search($criteria$context)
  134.             ->get($id);
  135.         if (!$paymentMethod) {
  136.             throw new UnknownPaymentMethodException($id);
  137.         }
  138.         return $paymentMethod;
  139.     }
  140.     /**
  141.      * @param array<string, mixed> $options
  142.      */
  143.     private function getShippingMethod(array $optionsContext $contextSalesChannelEntity $salesChannel): ShippingMethodEntity
  144.     {
  145.         $id $options[SalesChannelContextService::SHIPPING_METHOD_ID] ?? $salesChannel->getShippingMethodId();
  146.         $ids array_unique(array_filter([$id$salesChannel->getShippingMethodId()]));
  147.         $criteria = new Criteria($ids);
  148.         $criteria->addAssociation('media');
  149.         $criteria->setTitle('base-context-factory::shipping-method');
  150.         $shippingMethods $this->shippingMethodRepository->search($criteria$context);
  151.         /** @var ShippingMethodEntity $shippingMethod */
  152.         $shippingMethod $shippingMethods->get($id) ?? $shippingMethods->get($salesChannel->getShippingMethodId());
  153.         return $shippingMethod;
  154.     }
  155.     /**
  156.      * @param array<string, mixed> $session
  157.      */
  158.     private function getContext(string $salesChannelId, array $session): Context
  159.     {
  160.         $sql '
  161.         # context-factory::base-context
  162.         SELECT
  163.           sales_channel.id as sales_channel_id,
  164.           sales_channel.language_id as sales_channel_default_language_id,
  165.           sales_channel.currency_id as sales_channel_currency_id,
  166.           currency.factor as sales_channel_currency_factor,
  167.           GROUP_CONCAT(LOWER(HEX(sales_channel_language.language_id))) as sales_channel_language_ids
  168.         FROM sales_channel
  169.             INNER JOIN currency
  170.                 ON sales_channel.currency_id = currency.id
  171.             LEFT JOIN sales_channel_language
  172.                 ON sales_channel_language.sales_channel_id = sales_channel.id
  173.         WHERE sales_channel.id = :id
  174.         GROUP BY sales_channel.id, sales_channel.language_id, sales_channel.currency_id, currency.factor';
  175.         $data $this->connection->fetchAssociative($sql, [
  176.             'id' => Uuid::fromHexToBytes($salesChannelId),
  177.         ]);
  178.         if ($data === false) {
  179.             throw new \RuntimeException(sprintf('No context data found for SalesChannel "%s"'$salesChannelId));
  180.         }
  181.         if (isset($session[SalesChannelContextService::ORIGINAL_CONTEXT])) {
  182.             $origin = new AdminSalesChannelApiSource($salesChannelId$session[SalesChannelContextService::ORIGINAL_CONTEXT]);
  183.         } else {
  184.             $origin = new SalesChannelApiSource($salesChannelId);
  185.         }
  186.         //explode all available languages for the provided sales channel
  187.         $languageIds $data['sales_channel_language_ids'] ? explode(',', (string) $data['sales_channel_language_ids']) : [];
  188.         $languageIds array_keys(array_flip($languageIds));
  189.         //check which language should be used in the current request (request header set, or context already contains a language - stored in `sales_channel_api_context`)
  190.         $defaultLanguageId Uuid::fromBytesToHex($data['sales_channel_default_language_id']);
  191.         $languageChain $this->buildLanguageChain($session$defaultLanguageId$languageIds);
  192.         $versionId Defaults::LIVE_VERSION;
  193.         if (isset($session[SalesChannelContextService::VERSION_ID])) {
  194.             $versionId $session[SalesChannelContextService::VERSION_ID];
  195.         }
  196.         return new Context(
  197.             $origin,
  198.             [],
  199.             Uuid::fromBytesToHex($data['sales_channel_currency_id']),
  200.             $languageChain,
  201.             $versionId,
  202.             (float) $data['sales_channel_currency_factor'],
  203.             true
  204.         );
  205.     }
  206.     private function getParentLanguageId(string $languageId): ?string
  207.     {
  208.         if (!Uuid::isValid($languageId)) {
  209.             throw new LanguageNotFoundException($languageId);
  210.         }
  211.         $data $this->connection->createQueryBuilder()
  212.             ->select(['LOWER(HEX(language.parent_id))'])
  213.             ->from('language')
  214.             ->where('language.id = :id')
  215.             ->setParameter('id'Uuid::fromHexToBytes($languageId))
  216.             ->executeQuery()
  217.             ->fetchOne();
  218.         if ($data === false) {
  219.             throw new LanguageNotFoundException($languageId);
  220.         }
  221.         return $data;
  222.     }
  223.     /**
  224.      * @param array<string, mixed> $options
  225.      */
  226.     private function loadShippingLocation(array $optionsContext $contextSalesChannelEntity $salesChannel): ShippingLocation
  227.     {
  228.         //allows previewing cart calculation for a specify state for not logged in customers
  229.         if (isset($options[SalesChannelContextService::COUNTRY_STATE_ID])) {
  230.             $countryStateId $options[SalesChannelContextService::COUNTRY_STATE_ID];
  231.             \assert(\is_string($countryStateId) && Uuid::isValid($countryStateId));
  232.             $criteria = new Criteria([$countryStateId]);
  233.             $criteria->addAssociation('country');
  234.             $criteria->setTitle('base-context-factory::country');
  235.             /** @var CountryStateEntity|null $state */
  236.             $state $this->countryStateRepository->search($criteria$context)
  237.                 ->get($countryStateId);
  238.             if (!$state) {
  239.                 throw new \RuntimeException(sprintf('Country state with id "%s" not found'$countryStateId));
  240.             }
  241.             /** @var CountryEntity $country */
  242.             $country $state->getCountry();
  243.             return new ShippingLocation($country$statenull);
  244.         }
  245.         $countryId $options[SalesChannelContextService::COUNTRY_ID] ?? $salesChannel->getCountryId();
  246.         \assert(\is_string($countryId) && Uuid::isValid($countryId));
  247.         $criteria = new Criteria([$countryId]);
  248.         $criteria->setTitle('base-context-factory::country');
  249.         /** @var CountryEntity|null $country */
  250.         $country $this->countryRepository->search($criteria$context)->get($countryId);
  251.         if (!$country) {
  252.             throw new \RuntimeException(sprintf('Country with id "%s" not found'$countryId));
  253.         }
  254.         return ShippingLocation::createFromCountry($country);
  255.     }
  256.     /**
  257.      * @param array<string, mixed> $sessionOptions
  258.      * @param array<string> $availableLanguageIds
  259.      *
  260.      * @return non-empty-array<string>
  261.      */
  262.     private function buildLanguageChain(array $sessionOptionsstring $defaultLanguageId, array $availableLanguageIds): array
  263.     {
  264.         $current $sessionOptions[SalesChannelContextService::LANGUAGE_ID] ?? $defaultLanguageId;
  265.         \assert(\is_string($current) && Uuid::isValid($current));
  266.         //check provided language is part of the available languages
  267.         if (!\in_array($current$availableLanguageIdstrue)) {
  268.             throw new \RuntimeException(
  269.                 sprintf('Provided language %s is not in list of available languages: %s'$currentimplode(', '$availableLanguageIds))
  270.             );
  271.         }
  272.         if ($current === Defaults::LANGUAGE_SYSTEM) {
  273.             return [Defaults::LANGUAGE_SYSTEM];
  274.         }
  275.         //provided language can be a child language
  276.         return array_filter([$current$this->getParentLanguageId($current), Defaults::LANGUAGE_SYSTEM]);
  277.     }
  278.     /**
  279.      * @return CashRoundingConfig[]
  280.      */
  281.     private function getCashRounding(CurrencyEntity $currencyShippingLocation $shippingLocationContext $context): array
  282.     {
  283.         $criteria = new Criteria();
  284.         $criteria->setTitle('base-context-factory::cash-rounding');
  285.         $criteria->setLimit(1);
  286.         $criteria->addFilter(new EqualsFilter('currencyId'$currency->getId()));
  287.         $criteria->addFilter(new EqualsFilter('countryId'$shippingLocation->getCountry()->getId()));
  288.         /** @var CurrencyCountryRoundingEntity|null $countryConfig */
  289.         $countryConfig $this->currencyCountryRepository->search($criteria$context)->first();
  290.         if ($countryConfig) {
  291.             return [$countryConfig->getItemRounding(), $countryConfig->getTotalRounding()];
  292.         }
  293.         return [$currency->getItemRounding(), $currency->getTotalRounding()];
  294.     }
  295. }