vendor/shopware/platform/src/Core/System/SalesChannel/Context/SalesChannelContextFactory.php line 123

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\Cart\Tax\TaxDetector;
  7. use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity;
  8. use Shopware\Core\Checkout\Customer\CustomerEntity;
  9. use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
  10. use Shopware\Core\Checkout\Payment\PaymentMethodEntity;
  11. use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
  12. use Shopware\Core\Defaults;
  13. use Shopware\Core\Framework\Api\Context\AdminSalesChannelApiSource;
  14. use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
  15. use Shopware\Core\Framework\Context;
  16. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  21. use Shopware\Core\Framework\Feature;
  22. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  23. use Shopware\Core\Framework\Routing\Exception\LanguageNotFoundException;
  24. use Shopware\Core\Framework\Uuid\Uuid;
  25. use Shopware\Core\System\Currency\Aggregate\CurrencyCountryRounding\CurrencyCountryRoundingEntity;
  26. use Shopware\Core\System\Currency\CurrencyEntity;
  27. use Shopware\Core\System\SalesChannel\Event\SalesChannelContextPermissionsChangedEvent;
  28. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  29. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  30. use Shopware\Core\System\Tax\Aggregate\TaxRule\TaxRuleCollection;
  31. use Shopware\Core\System\Tax\Aggregate\TaxRule\TaxRuleEntity;
  32. use Shopware\Core\System\Tax\TaxCollection;
  33. use Shopware\Core\System\Tax\TaxEntity;
  34. use Shopware\Core\System\Tax\TaxRuleType\TaxRuleTypeFilterInterface;
  35. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  36. class SalesChannelContextFactory extends AbstractSalesChannelContextFactory
  37. {
  38.     private EntityRepositoryInterface $salesChannelRepository;
  39.     private EntityRepositoryInterface $currencyRepository;
  40.     private EntityRepositoryInterface $customerRepository;
  41.     private EntityRepositoryInterface $customerGroupRepository;
  42.     private EntityRepositoryInterface $countryRepository;
  43.     private EntityRepositoryInterface $taxRepository;
  44.     private EntityRepositoryInterface $addressRepository;
  45.     private EntityRepositoryInterface $paymentMethodRepository;
  46.     private EntityRepositoryInterface $shippingMethodRepository;
  47.     private Connection $connection;
  48.     private EntityRepositoryInterface $countryStateRepository;
  49.     private TaxDetector $taxDetector;
  50.     /**
  51.      * @var iterable|TaxRuleTypeFilterInterface[]
  52.      */
  53.     private $taxRuleTypeFilter;
  54.     private EventDispatcherInterface $eventDispatcher;
  55.     private EntityRepositoryInterface $currencyCountryRepository;
  56.     public function __construct(
  57.         EntityRepositoryInterface $salesChannelRepository,
  58.         EntityRepositoryInterface $currencyRepository,
  59.         EntityRepositoryInterface $customerRepository,
  60.         EntityRepositoryInterface $customerGroupRepository,
  61.         EntityRepositoryInterface $countryRepository,
  62.         EntityRepositoryInterface $taxRepository,
  63.         EntityRepositoryInterface $addressRepository,
  64.         EntityRepositoryInterface $paymentMethodRepository,
  65.         EntityRepositoryInterface $shippingMethodRepository,
  66.         Connection $connection,
  67.         EntityRepositoryInterface $countryStateRepository,
  68.         TaxDetector $taxDetector,
  69.         iterable $taxRuleTypeFilter,
  70.         EventDispatcherInterface $eventDispatcher,
  71.         EntityRepositoryInterface $currencyCountryRepository
  72.     ) {
  73.         $this->salesChannelRepository $salesChannelRepository;
  74.         $this->currencyRepository $currencyRepository;
  75.         $this->customerRepository $customerRepository;
  76.         $this->customerGroupRepository $customerGroupRepository;
  77.         $this->countryRepository $countryRepository;
  78.         $this->taxRepository $taxRepository;
  79.         $this->addressRepository $addressRepository;
  80.         $this->paymentMethodRepository $paymentMethodRepository;
  81.         $this->shippingMethodRepository $shippingMethodRepository;
  82.         $this->connection $connection;
  83.         $this->countryStateRepository $countryStateRepository;
  84.         $this->taxDetector $taxDetector;
  85.         $this->taxRuleTypeFilter $taxRuleTypeFilter;
  86.         $this->eventDispatcher $eventDispatcher;
  87.         $this->currencyCountryRepository $currencyCountryRepository;
  88.     }
  89.     public function getDecorated(): AbstractSalesChannelContextFactory
  90.     {
  91.         throw new DecorationPatternException(self::class);
  92.     }
  93.     public function create(string $tokenstring $salesChannelId, array $options = []): SalesChannelContext
  94.     {
  95.         $context $this->getContext($salesChannelId$options);
  96.         $criteria = new Criteria([$salesChannelId]);
  97.         $criteria->setTitle('context-factory::sales-channel');
  98.         $criteria->addAssociation('currency');
  99.         $criteria->addAssociation('domains');
  100.         /** @var SalesChannelEntity|null $salesChannel */
  101.         $salesChannel $this->salesChannelRepository->search($criteria$context)
  102.             ->get($salesChannelId);
  103.         if (!$salesChannel) {
  104.             throw new \RuntimeException(sprintf('Sales channel with id %s not found or not valid!'$salesChannelId));
  105.         }
  106.         if (!Feature::isActive('FEATURE_NEXT_17276')) {
  107.             /*
  108.              * @deprecated tag:v6.5.0 - Overriding the languageId of the SalesChannel is deprecated and will be removed in v6.5.0
  109.              * use `$salesChannelContext->getLanguageId()` instead
  110.              */
  111.             if (\array_key_exists(SalesChannelContextService::LANGUAGE_ID$options)) {
  112.                 $salesChannel->setLanguageId($options[SalesChannelContextService::LANGUAGE_ID]);
  113.             }
  114.         }
  115.         //load active currency, fallback to shop currency
  116.         $currency $salesChannel->getCurrency();
  117.         if (\array_key_exists(SalesChannelContextService::CURRENCY_ID$options)) {
  118.             $currencyId $options[SalesChannelContextService::CURRENCY_ID];
  119.             $criteria = new Criteria([$currencyId]);
  120.             $criteria->setTitle('context-factory::currency');
  121.             $currency $this->currencyRepository->search($criteria$context)->get($currencyId);
  122.         }
  123.         // customer
  124.         $customer null;
  125.         if (\array_key_exists(SalesChannelContextService::CUSTOMER_ID$options) && $options[SalesChannelContextService::CUSTOMER_ID] !== null) {
  126.             //load logged in customer and set active addresses
  127.             $customer $this->loadCustomer($options$context);
  128.         }
  129.         $shippingLocation null;
  130.         if ($customer) {
  131.             /** @var CustomerAddressEntity $activeShippingAddress */
  132.             $activeShippingAddress $customer->getActiveShippingAddress();
  133.             $shippingLocation ShippingLocation::createFromAddress($activeShippingAddress);
  134.         }
  135.         if ($shippingLocation === null) {
  136.             //load not logged in customer with default shop configuration or with provided checkout scopes
  137.             $shippingLocation $this->loadShippingLocation($options$context$salesChannel);
  138.         }
  139.         $groupId $salesChannel->getCustomerGroupId();
  140.         /** @deprecated tag:v6.5.0 - Fallback customer group is deprecated and will be removed */
  141.         $groupIds = [$salesChannel->getCustomerGroupId(), Defaults::FALLBACK_CUSTOMER_GROUP];
  142.         if ($customer) {
  143.             $groupIds[] = $customer->getGroupId();
  144.             $groupId $customer->getGroupId();
  145.         }
  146.         $groupIds array_keys(array_flip($groupIds));
  147.         $criteria = new Criteria($groupIds);
  148.         $criteria->setTitle('context-factory::customer-group');
  149.         $customerGroups $this->customerGroupRepository->search($criteria$context);
  150.         /** @deprecated tag:v6.5.0 - Fallback customer group is deprecated and will be removed */
  151.         $fallbackGroup $customerGroups->has(Defaults::FALLBACK_CUSTOMER_GROUP) ? $customerGroups->get(Defaults::FALLBACK_CUSTOMER_GROUP) : $customerGroups->get($salesChannel->getCustomerGroupId());
  152.         $customerGroup $customerGroups->get($groupId);
  153.         //loads tax rules based on active customer and delivery address
  154.         $taxRules $this->getTaxRules($context$customer$shippingLocation);
  155.         //detect active payment method, first check if checkout defined other payment method, otherwise validate if customer logged in, at least use shop default
  156.         $payment $this->getPaymentMethod($options$context$salesChannel$customer);
  157.         //detect active delivery method, at first checkout scope, at least shop default method
  158.         $shippingMethod $this->getShippingMethod($options$context$salesChannel);
  159.         [$itemRounding$totalRounding] = $this->getCashRounding($currency$shippingLocation$context);
  160.         $context = new Context(
  161.             $context->getSource(),
  162.             [],
  163.             $currency->getId(),
  164.             $context->getLanguageIdChain(),
  165.             $context->getVersionId(),
  166.             $currency->getFactor(),
  167.             true,
  168.             CartPrice::TAX_STATE_GROSS,
  169.             $itemRounding
  170.         );
  171.         $salesChannelContext = new SalesChannelContext(
  172.             $context,
  173.             $token,
  174.             $options[SalesChannelContextService::DOMAIN_ID] ?? null,
  175.             $salesChannel,
  176.             $currency,
  177.             $customerGroup,
  178.             $fallbackGroup,
  179.             $taxRules,
  180.             $payment,
  181.             $shippingMethod,
  182.             $shippingLocation,
  183.             $customer,
  184.             $itemRounding,
  185.             $totalRounding,
  186.             []
  187.         );
  188.         if (\array_key_exists(SalesChannelContextService::PERMISSIONS$options)) {
  189.             $salesChannelContext->setPermissions($options[SalesChannelContextService::PERMISSIONS]);
  190.             $event = new SalesChannelContextPermissionsChangedEvent($salesChannelContext$options[SalesChannelContextService::PERMISSIONS]);
  191.             $this->eventDispatcher->dispatch($event);
  192.             $salesChannelContext->lockPermissions();
  193.         }
  194.         $salesChannelContext->setTaxState($this->taxDetector->getTaxState($salesChannelContext));
  195.         return $salesChannelContext;
  196.     }
  197.     private function getTaxRules(Context $context, ?CustomerEntity $customerShippingLocation $shippingLocation): TaxCollection
  198.     {
  199.         $criteria = new Criteria();
  200.         $criteria->setTitle('context-factory::taxes');
  201.         $criteria->addAssociation('rules.type');
  202.         $taxes $this->taxRepository->search($criteria$context)->getEntities();
  203.         /** @var TaxEntity $tax */
  204.         foreach ($taxes as $tax) {
  205.             $taxRules $tax->getRules();
  206.             if ($taxRules === null) {
  207.                 continue;
  208.             }
  209.             $taxRules $taxRules->filter(function (TaxRuleEntity $taxRule) use ($customer$shippingLocation) {
  210.                 foreach ($this->taxRuleTypeFilter as $ruleTypeFilter) {
  211.                     if ($ruleTypeFilter->match($taxRule$customer$shippingLocation)) {
  212.                         return true;
  213.                     }
  214.                 }
  215.                 return false;
  216.             });
  217.             $taxRules->sortByTypePosition();
  218.             $taxRule $taxRules->first();
  219.             $matchingRules = new TaxRuleCollection();
  220.             if ($taxRule) {
  221.                 $matchingRules->add($taxRule);
  222.             }
  223.             $tax->setRules($matchingRules);
  224.         }
  225.         return new TaxCollection($taxes);
  226.     }
  227.     private function getPaymentMethod(array $optionsContext $contextSalesChannelEntity $salesChannel, ?CustomerEntity $customer): PaymentMethodEntity
  228.     {
  229.         $id $salesChannel->getPaymentMethodId();
  230.         if (\array_key_exists(SalesChannelContextService::PAYMENT_METHOD_ID$options)) {
  231.             $id $options[SalesChannelContextService::PAYMENT_METHOD_ID];
  232.         } elseif ($customer && $customer->getLastPaymentMethodId()) {
  233.             $id $customer->getLastPaymentMethodId();
  234.         } elseif ($customer && $customer->getDefaultPaymentMethodId()) {
  235.             $id $customer->getDefaultPaymentMethodId();
  236.         }
  237.         $criteria = (new Criteria([$id]))->addAssociation('media');
  238.         $criteria->setTitle('context-factory::payment-method');
  239.         $paymentMethod $this->paymentMethodRepository
  240.             ->search($criteria$context)
  241.             ->get($id);
  242.         if (!$paymentMethod) {
  243.             throw new UnknownPaymentMethodException($id);
  244.         }
  245.         return $paymentMethod;
  246.     }
  247.     private function getShippingMethod(array $optionsContext $contextSalesChannelEntity $salesChannel): ShippingMethodEntity
  248.     {
  249.         $id $salesChannel->getShippingMethodId();
  250.         if (\array_key_exists(SalesChannelContextService::SHIPPING_METHOD_ID$options)) {
  251.             $id $options[SalesChannelContextService::SHIPPING_METHOD_ID];
  252.         }
  253.         $criteria = (new Criteria(array_unique(array_filter([$id$salesChannel->getShippingMethodId()]))))
  254.             ->addAssociation('media');
  255.         $criteria->setTitle('context-factory::shipping-method');
  256.         $shippingMethods $this->shippingMethodRepository
  257.             ->search($criteria$context);
  258.         return $shippingMethods->get($id) ?? $shippingMethods->get($salesChannel->getShippingMethodId());
  259.     }
  260.     private function getContext(string $salesChannelId, array $session): Context
  261.     {
  262.         $sql '
  263.         # context-factory::base-context
  264.         SELECT
  265.           sales_channel.id as sales_channel_id,
  266.           sales_channel.language_id as sales_channel_default_language_id,
  267.           sales_channel.currency_id as sales_channel_currency_id,
  268.           currency.factor as sales_channel_currency_factor,
  269.           GROUP_CONCAT(LOWER(HEX(sales_channel_language.language_id))) as sales_channel_language_ids
  270.         FROM sales_channel
  271.             INNER JOIN currency
  272.                 ON sales_channel.currency_id = currency.id
  273.             LEFT JOIN sales_channel_language
  274.                 ON sales_channel_language.sales_channel_id = sales_channel.id
  275.         WHERE sales_channel.id = :id
  276.         GROUP BY sales_channel.id, sales_channel.language_id, sales_channel.currency_id, currency.factor';
  277.         $data $this->connection->fetchAssoc($sql, [
  278.             'id' => Uuid::fromHexToBytes($salesChannelId),
  279.         ]);
  280.         if ($data === false) {
  281.             throw new \RuntimeException(sprintf('No context data found for SalesChannel "%s"'$salesChannelId));
  282.         }
  283.         if (isset($session[SalesChannelContextService::ORIGINAL_CONTEXT])) {
  284.             $origin = new AdminSalesChannelApiSource($salesChannelId$session[SalesChannelContextService::ORIGINAL_CONTEXT]);
  285.         } else {
  286.             $origin = new SalesChannelApiSource($salesChannelId);
  287.         }
  288.         //explode all available languages for the provided sales channel
  289.         $languageIds $data['sales_channel_language_ids'] ? explode(','$data['sales_channel_language_ids']) : [];
  290.         $languageIds array_keys(array_flip($languageIds));
  291.         //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`)
  292.         $defaultLanguageId Uuid::fromBytesToHex($data['sales_channel_default_language_id']);
  293.         $languageChain $this->buildLanguageChain($session$defaultLanguageId$languageIds);
  294.         $versionId Defaults::LIVE_VERSION;
  295.         if (isset($session[SalesChannelContextService::VERSION_ID])) {
  296.             $versionId $session[SalesChannelContextService::VERSION_ID];
  297.         }
  298.         return new Context(
  299.             $origin,
  300.             [],
  301.             Uuid::fromBytesToHex($data['sales_channel_currency_id']),
  302.             $languageChain,
  303.             $versionId,
  304.             (float) $data['sales_channel_currency_factor'],
  305.             true
  306.         );
  307.     }
  308.     private function getParentLanguageId(string $languageId): ?string
  309.     {
  310.         if (!Uuid::isValid($languageId)) {
  311.             throw new LanguageNotFoundException($languageId);
  312.         }
  313.         $data $this->connection->createQueryBuilder()
  314.             ->select(['LOWER(HEX(language.parent_id))'])
  315.             ->from('language')
  316.             ->where('language.id = :id')
  317.             ->setParameter('id'Uuid::fromHexToBytes($languageId))
  318.             ->execute()
  319.             ->fetchColumn();
  320.         if ($data === false) {
  321.             throw new LanguageNotFoundException($languageId);
  322.         }
  323.         return $data;
  324.     }
  325.     private function loadCustomer(array $optionsContext $context): ?CustomerEntity
  326.     {
  327.         $customerId $options[SalesChannelContextService::CUSTOMER_ID];
  328.         $criteria = new Criteria([$customerId]);
  329.         $criteria->setTitle('context-factory::customer');
  330.         $criteria->addAssociation('salutation');
  331.         $criteria->addAssociation('defaultPaymentMethod');
  332.         $criteria->addAssociation('defaultBillingAddress.salutation');
  333.         $criteria->addAssociation('defaultBillingAddress.country');
  334.         $criteria->addAssociation('defaultBillingAddress.countryState');
  335.         $criteria->addAssociation('defaultShippingAddress.salutation');
  336.         $criteria->addAssociation('defaultShippingAddress.country');
  337.         $criteria->addAssociation('defaultShippingAddress.countryState');
  338.         /** @var SalesChannelApiSource $source */
  339.         $source $context->getSource();
  340.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
  341.             new EqualsFilter('customer.boundSalesChannelId'null),
  342.             new EqualsFilter('customer.boundSalesChannelId'$source->getSalesChannelId()),
  343.         ]));
  344.         $customer $this->customerRepository->search($criteria$context)->get($customerId);
  345.         if (!$customer) {
  346.             return null;
  347.         }
  348.         $billingAddressId $options[SalesChannelContextService::BILLING_ADDRESS_ID] ?? $customer->getDefaultBillingAddressId();
  349.         $shippingAddressId $options[SalesChannelContextService::SHIPPING_ADDRESS_ID] ?? $customer->getDefaultShippingAddressId();
  350.         $addressIds[] = $billingAddressId;
  351.         $addressIds[] = $shippingAddressId;
  352.         $criteria = new Criteria($addressIds);
  353.         $criteria->setTitle('context-factory::addresses');
  354.         $criteria->addAssociation('salutation');
  355.         $criteria->addAssociation('country');
  356.         $criteria->addAssociation('countryState');
  357.         $addresses $this->addressRepository->search($criteria$context);
  358.         $customer->setActiveBillingAddress($addresses->get($billingAddressId));
  359.         $customer->setActiveShippingAddress($addresses->get($shippingAddressId));
  360.         return $customer;
  361.     }
  362.     private function loadShippingLocation(
  363.         array $options,
  364.         Context $context,
  365.         SalesChannelEntity $salesChannel
  366.     ): ShippingLocation {
  367.         //allows to preview cart calculation for a specify state for not logged in customers
  368.         if (\array_key_exists(SalesChannelContextService::COUNTRY_STATE_ID$options) && $options[SalesChannelContextService::COUNTRY_STATE_ID]) {
  369.             $criteria = new Criteria([$options[SalesChannelContextService::COUNTRY_STATE_ID]]);
  370.             $criteria->addAssociation('country');
  371.             $criteria->setTitle('context-factory::country');
  372.             $state $this->countryStateRepository->search($criteria$context)
  373.                 ->get($options[SalesChannelContextService::COUNTRY_STATE_ID]);
  374.             return new ShippingLocation($state->getCountry(), $statenull);
  375.         }
  376.         $countryId $salesChannel->getCountryId();
  377.         if (\array_key_exists(SalesChannelContextService::COUNTRY_ID$options) && $options[SalesChannelContextService::COUNTRY_ID]) {
  378.             $countryId $options[SalesChannelContextService::COUNTRY_ID];
  379.         }
  380.         $criteria = new Criteria([$countryId]);
  381.         $criteria->setTitle('context-factory::country');
  382.         $country $this->countryRepository->search($criteria$context)
  383.             ->get($countryId);
  384.         return ShippingLocation::createFromCountry($country);
  385.     }
  386.     private function buildLanguageChain(array $sessionOptionsstring $defaultLanguageId, array $availableLanguageIds): array
  387.     {
  388.         $current $sessionOptions[SalesChannelContextService::LANGUAGE_ID] ?? $defaultLanguageId;
  389.         //check provided language is part of the available languages
  390.         if (!\in_array($current$availableLanguageIdstrue)) {
  391.             throw new \RuntimeException(
  392.                 sprintf('Provided language %s is not in list of available languages: %s'$currentimplode(', '$availableLanguageIds))
  393.             );
  394.         }
  395.         if ($current === Defaults::LANGUAGE_SYSTEM) {
  396.             return [Defaults::LANGUAGE_SYSTEM];
  397.         }
  398.         //provided language can be a child language
  399.         return [$current$this->getParentLanguageId($current), Defaults::LANGUAGE_SYSTEM];
  400.     }
  401.     /**
  402.      * @return CashRoundingConfig[]
  403.      */
  404.     private function getCashRounding(CurrencyEntity $currencyShippingLocation $shippingLocationContext $context): array
  405.     {
  406.         $criteria = new Criteria();
  407.         $criteria->setLimit(1);
  408.         $criteria->addFilter(new EqualsFilter('currencyId'$currency->getId()));
  409.         $criteria->addFilter(new EqualsFilter('countryId'$shippingLocation->getCountry()->getId()));
  410.         /** @var CurrencyCountryRoundingEntity|null $countryConfig */
  411.         $countryConfig $this->currencyCountryRepository
  412.             ->search($criteria$context)
  413.             ->first();
  414.         if ($countryConfig) {
  415.             return [$countryConfig->getItemRounding(), $countryConfig->getTotalRounding()];
  416.         }
  417.         return [$currency->getItemRounding(), $currency->getTotalRounding()];
  418.     }
  419. }