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

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