?php /** * Copyright 2014 Adobe * All Rights Reserved. */ namespace Magento\Catalog\Model; use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductExtension; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ImageContentValidatorInterface; use Magento\Framework\Api\ImageProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\DB\Adapter\ConnectionException; use Magento\Framework\DB\Adapter\DeadlockException; use Magento\Framework\DB\Adapter\LockWaitException; use Magento\Framework\EntityManager\Operation\Read\ReadExtensions; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\EavAttributeInterface; /** * @inheritdoc * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage */ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface, ResetAfterRequestInterface { /** * @var \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface */ protected $optionRepository; /** * @var ProductFactory */ protected $productFactory; /** * @var Product[] */ protected $instances = []; /** * @var Product[] */ protected $instancesById = []; /** * @var \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory */ protected $searchResultsFactory; /** * @var \Magento\Framework\Api\SearchCriteriaBuilder */ protected $searchCriteriaBuilder; /** * @var \Magento\Framework\Api\FilterBuilder */ protected $filterBuilder; /** * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ protected $collectionFactory; /** * @var \Magento\Catalog\Model\ResourceModel\Product */ protected $resourceModel; /** * @var Product\Initialization\Helper\ProductLinks */ protected $linkInitializer; /** * @var Product\LinkTypeProvider */ protected $linkTypeProvider; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $storeManager; /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface */ protected $attributeRepository; /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface */ protected $metadataService; /** * @var \Magento\Framework\Api\ExtensibleDataObjectConverter */ protected $extensibleDataObjectConverter; /** * @var \Magento\Framework\Filesystem */ protected $fileSystem; /** * @deprecated 103.0.2 * * @var ImageContentInterfaceFactory */ protected $contentFactory; /** * @deprecated 103.0.2 * * @var ImageProcessorInterface */ protected $imageProcessor; /** * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface */ protected $extensionAttributesJoinProcessor; /** * @deprecated 103.0.2 * * @var \Magento\Catalog\Model\Product\Gallery\Processor */ protected $mediaGalleryProcessor; /** * @var MediaGalleryProcessor */ private $mediaProcessor; /** * @var CollectionProcessorInterface */ private $collectionProcessor; /** * @var int */ private $cacheLimit = 0; /** * @var \Magento\Framework\Serialize\Serializer\Json */ private $serializer; /** * @var ReadExtensions */ private $readExtensions; /** * @var CategoryLinkManagementInterface */ private $linkManagement; /** * @var ScopeOverriddenValue */ private $scopeOverriddenValue; /** * ProductRepository constructor. * @param ProductFactory $productFactory * @param \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory $searchResultsFactory * @param ResourceModel\Product\CollectionFactory $collectionFactory * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository * @param ResourceModel\Product $resourceModel * @param Product\Initialization\Helper\ProductLinks $linkInitializer * @param Product\LinkTypeProvider $linkTypeProvider * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\Api\FilterBuilder $filterBuilder * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $metadataServiceInterface * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param Product\Option\Converter $optionConverter * @param \Magento\Framework\Filesystem $fileSystem * @param ImageContentValidatorInterface $contentValidator * @param ImageContentInterfaceFactory $contentFactory * @param MimeTypeExtensionMap $mimeTypeExtensionMap * @param ImageProcessorInterface $imageProcessor * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface $collectionProcessor [optional] * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param int $cacheLimit [optional] * @param ReadExtensions $readExtensions * @param CategoryLinkManagementInterface $linkManagement * @param ScopeOverriddenValue|null $scopeOverriddenValue * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ProductFactory $productFactory, \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory $searchResultsFactory, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory, \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, \Magento\Catalog\Model\ResourceModel\Product $resourceModel, \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $linkInitializer, \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Catalog\Api\ProductAttributeRepositoryInterface $metadataServiceInterface, \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, \Magento\Catalog\Model\Product\Option\Converter $optionConverter, \Magento\Framework\Filesystem $fileSystem, ImageContentValidatorInterface $contentValidator, ImageContentInterfaceFactory $contentFactory, MimeTypeExtensionMap $mimeTypeExtensionMap, ImageProcessorInterface $imageProcessor, \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, ?CollectionProcessorInterface $collectionProcessor = null, ?\Magento\Framework\Serialize\Serializer\Json $serializer = null, $cacheLimit = 1000, ?ReadExtensions $readExtensions = null, ?CategoryLinkManagementInterface $linkManagement = null, ?ScopeOverriddenValue $scopeOverriddenValue = null ) { $this->productFactory = $productFactory; $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->resourceModel = $resourceModel; $this->linkInitializer = $linkInitializer; $this->linkTypeProvider = $linkTypeProvider; $this->storeManager = $storeManager; $this->attributeRepository = $attributeRepository; $this->filterBuilder = $filterBuilder; $this->metadataService = $metadataServiceInterface; $this->extensibleDataObjectConverter = $extensibleDataObjectConverter; $this->fileSystem = $fileSystem; $this->contentFactory = $contentFactory; $this->imageProcessor = $imageProcessor; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); $this->cacheLimit = (int)$cacheLimit; $this->readExtensions = $readExtensions ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(ReadExtensions::class); $this->linkManagement = $linkManagement ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(CategoryLinkManagementInterface::class); $this->scopeOverriddenValue = $scopeOverriddenValue ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(ScopeOverriddenValue::class); } /** * @inheritdoc */ public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId === null ? $storeId : (int) $storeId]); $cachedProduct = $this->getProductFromLocalCache($sku, $cacheKey); if ($cachedProduct === null || $forceReload) { $product = $this->productFactory->create(); $productId = $this->resourceModel->getIdBySku($sku); if (!$productId) { throw new NoSuchEntityException( __("The product that was requested doesn't exist. Verify the product and try again.") ); } if ($editMode) { $product->setData('_edit_mode', true); } if ($storeId !== null) { $product->setData('store_id', (int) $storeId); } $product->load($productId); $this->cacheProduct($cacheKey, $product); $cachedProduct = $product; } return $cachedProduct; } /** * @inheritdoc */ public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId === null ? $storeId : (int) $storeId]); if (!isset($this->instancesById[$productId][$cacheKey]) || $forceReload) { $product = $this->productFactory->create(); if ($editMode) { $product->setData('_edit_mode', true); } if ($storeId !== null) { $product->setData('store_id', $storeId); } $product->load($productId); if (!$product->getId()) { throw new NoSuchEntityException( __("The product that was requested doesn't exist. Verify the product and try again.") ); } $this->cacheProduct($cacheKey, $product); } return $this->instancesById[$productId][$cacheKey]; } /** * Get key for cache * * @param array $data * @return string */ protected function getCacheKey($data) { $serializeData = []; foreach ($data as $key => $value) { if (is_object($value)) { $serializeData[$key] = $value->getId(); } else { $serializeData[$key] = $value; } } $serializeData = $this->serializer->serialize($serializeData); return sha1($serializeData); } /** * Add product to internal cache and truncate cache if it has more than cacheLimit elements. * * @param string $cacheKey * @param ProductInterface $product * @return void */ private function cacheProduct($cacheKey, ProductInterface $product) { $this->instancesById[$product->getId()][$cacheKey] = $product; $this->saveProductInLocalCache($product, $cacheKey); if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) { $offset = round($this->cacheLimit / -2); $this->instancesById = array_slice($this->instancesById, $offset, null, true); $this->instances = array_slice($this->instances, $offset, null, true); } } /** * Merge data from DB and updates from request * * @param array $productData * @param bool $createNew * @return ProductInterface|Product * @throws NoSuchEntityException */ protected function initializeProductData(array $productData, $createNew) { unset($productData['media_gallery']); if ($createNew) { $product = $this->productFactory->create(); $this->assignProductToWebsites($product); } elseif (!empty($productData['id'])) { $this->removeProductFromLocalCacheById($productData['id']); $product = $this->getById($productData['id']); } else { $this->removeProductFromLocalCacheBySku($productData['sku']); $product = $this->get($productData['sku']); } foreach ($productData as $key => $value) { $product->setData($key, $value); } return $product; } /** * Assign product to websites. * * @param \Magento\Catalog\Model\Product $product * @return void */ private function assignProductToWebsites(\Magento\Catalog\Model\Product $product) { if ($this->storeManager->getStore(true)->getCode() == \Magento\Store\Model\Store::ADMIN_CODE) { $websiteIds = array_keys($this->storeManager->getWebsites()); } else { $websiteIds = [$this->storeManager->getStore()->getWebsiteId()]; } $product->setWebsiteIds($websiteIds); } /** * Process new gallery media entry. * * @deprecated 103.0.2 * @see MediaGalleryProcessor::processNewMediaGalleryEntry() * * @param ProductInterface $product * @param array $newEntry * @return $this * @throws InputException * @throws StateException * @throws \Magento\Framework\Exception\LocalizedException */ protected function processNewMediaGalleryEntry( ProductInterface $product, array $newEntry ) { $this->getMediaGalleryProcessor()->processNewMediaGalleryEntry($product, $newEntry); return $this; } /** * Process product links, creating new links, updating and deleting existing links * * @param ProductInterface $product * @param \Magento\Catalog\Api\Data\ProductLinkInterface[] $newLinks * @return $this * @throws NoSuchEntityException */ private function processLinks(ProductInterface $product, $newLinks) { if ($newLinks === null) { // If product links were not specified, don't do anything return $this; } // Clear all existing product links and then set the ones we want $linkTypes = $this->linkTypeProvider->getLinkTypes(); foreach (array_keys($linkTypes) as $typeName) { $this->linkInitializer->initializeLinks($product, [$typeName => []]); } // Set each linktype info if (!empty($newLinks)) { $productLinks = []; foreach ($newLinks as $link) { $productLinks[$link->getLinkType()][] = $link; } foreach ($productLinks as $type => $linksByType) { $assignedSkuList = []; /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $link */ foreach ($linksByType as $link) { $assignedSkuList[] = $link->getLinkedProductSku(); } $linkedProductIds = $this->resourceModel->getProductsIdsBySkus($assignedSkuList); $linksToInitialize = []; foreach ($linksByType as $link) { $linkDataArray = $this->extensibleDataObjectConverter ->toNestedArray($link, [], \Magento\Catalog\Api\Data\ProductLinkInterface::class); $linkedSku = $link->getLinkedProductSku(); if (!isset($linkedProductIds[$linkedSku])) { throw new NoSuchEntityException( __('The Product with the "%1" SKU doesn\'t exist.', $linkedSku) ); } $linkDataArray['product_id'] = $linkedProductIds[$linkedSku]; $linksToInitialize[$linkedProductIds[$linkedSku]] = $linkDataArray; } $this->linkInitializer->initializeLinks($product, [$type => $linksToInitialize]); } } $product->setProductLinks($newLinks); return $this; } /** * Process Media gallery data before save product. * * Compare Media Gallery Entries Data with existing Media Gallery * * If Media entry has not value_id set it as new * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag * * Merge Existing and new media gallery * * @param ProductInterface $product contains only existing media gallery items * @param array $mediaGalleryEntries array which contains all media gallery items * @return $this * @throws InputException * @throws StateException * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function processMediaGallery(ProductInterface $product, $mediaGalleryEntries) { $this->getMediaGalleryProcessor()->processMediaGallery($product, $mediaGalleryEntries); return $this; } /** * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function save(ProductInterface $product, $saveOptions = false) { try { $assignToCategories = false; $tierPrices = $product->getData('tier_price'); $productDataToChange = $product->getData(); if (!$product->getSku()) { throw new CouldNotSaveException( __("The \"%1\" attribute value is empty. Set the attribute and try again.", "sku") ); } try { $existingProduct = $product->getId() ? $this->getById($product->getId()) : $this->get($product->getSku()); $product->setData( $this->resourceModel->getLinkField(), $existingProduct->getData($this->resourceModel->getLinkField()) ); if (!$product->hasData(Product::STATUS)) { $product->setStatus($existingProduct->getStatus()); } /** @var ProductExtension $extensionAttributes */ $extensionAttributes = $product->getExtensionAttributes(); if (empty($extensionAttributes->__toArray())) { $product->setExtensionAttributes($existingProduct->getExtensionAttributes()); $assignToCategories = true; } } catch (NoSuchEntityException $e) { $existingProduct = null; } $productDataArray = $this->extensibleDataObjectConverter ->toNestedArray($product, [], ProductInterface::class); $productDataArray = array_replace($productDataArray, $product->getData()); $ignoreLinksFlag = $product->getData('ignore_links_flag'); $productLinks = null; if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) { $productLinks = $product->getProductLinks(); } if (!isset($productDataArray['store_id'])) { $productDataArray['store_id'] = (int) $this->storeManager->getStore()->getId(); } $product = $this->initializeProductData($productDataArray, empty($existingProduct)); $this->processLinks($product, $productLinks); if (isset($productDataArray['media_gallery'])) { $this->processMediaGallery($product, $productDataArray['media_gallery']['images']); } if (!$product->getOptionsReadonly()) { $product->setCanSaveCustomOptions(true); } $validationResult = $this->resourceModel->validate($product); if (true !== $validationResult) { throw new \Magento\Framework\Exception\CouldNotSaveException( __('Invalid product data: %1', implode(',', $validationResult)) ); } if ($tierPrices !== null) { $product->setData('tier_price', $tierPrices); } try { $stores = $product->getStoreIds(); $websites = $product->getWebsiteIds(); } catch (NoSuchEntityException $exception) { $stores = null; $websites = null; } if (!empty($existingProduct) && is_array($stores) && is_array($websites)) { $hasDataChanged = false; $productAttributes = $product->getAttributes(); if ($productAttributes !== null && $product->getStoreId() !== Store::DEFAULT_STORE_ID && (count($stores) > 1 || count($websites) === 1) ) { $imageRoles = ['image', 'small_image', 'thumbnail']; foreach ($productAttributes as $attribute) { $attributeCode = $attribute->getAttributeCode(); $value = $product->getData($attributeCode); if (!in_array($attributeCode, $imageRoles) && $existingProduct->getData($attributeCode) === $value && $existingProduct->getOrigData($attributeCode) === $value && $attribute->getScope() !== EavAttributeInterface::SCOPE_GLOBAL_TEXT && !is_array($value) && !$attribute->isStatic() && $value !== null && !$this->scopeOverriddenValue->containsValue( ProductInterface::class, $product, $attributeCode, $product->getStoreId() ) ) { $product->setData( $attributeCode, $attributeCode === ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY ? false : null ); $hasDataChanged = true; } elseif (in_array($attributeCode, $imageRoles) && $existingProduct->getData($attributeCode) === $value && !array_key_exists($attributeCode, $productDataToChange) && $attribute->getScope() !== EavAttributeInterface::SCOPE_GLOBAL_TEXT && $value !== null && !$this->scopeOverriddenValue->containsValue( ProductInterface::class, $product, $attributeCode, $product->getStoreId() ) ) { $product->setData($attributeCode, null); $hasDataChanged = true; } } if ($hasDataChanged) { $product->setData('_edit_mode', true); } } } $this->saveProduct($product); if ($assignToCategories === true && $product->getCategoryIds()) { $this->linkManagement->assignProductToCategories( $product->getSku(), $product->getCategoryIds() ); } $this->removeProductFromLocalCacheBySku($product->getSku()); $this->removeProductFromLocalCacheById($product->getId()); } catch (\Exception $e) { $writer = new \Zend_Log_Writer_Stream(BP . '/var/log/product_save_error.log'); $logger = new \Zend_Log(); $logger->addWriter($writer); $logger->info('SKU: ' . $product->getSku() . ' Error: ' . $e->getMessage()); $logger->info($e->getTraceAsString()); throw $e; } return $this->get($product->getSku(), false, $product->getStoreId()); } /** * @inheritdoc */ public function delete(ProductInterface $product) { $sku = $product->getSku(); $productId = $product->getId(); try { $this->removeProductFromLocalCacheBySku($product->getSku()); $this->removeProductFromLocalCacheById($product->getId()); $this->resourceModel->delete($product); } catch (ValidatorException $e) { throw new CouldNotSaveException(__($e->getMessage()), $e); } catch (\Exception $e) { throw new \Magento\Framework\Exception\StateException( __('The "%1" product couldn\'t be removed.', $sku), $e ); } $this->removeProductFromLocalCacheBySku($sku); $this->removeProductFromLocalCacheById($productId); return true; } /** * @inheritdoc */ public function deleteById($sku) { $product = $this->get($sku); return $this->delete($product); } /** * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); $this->extensionAttributesJoinProcessor->process($collection); $collection->addAttributeToSelect('*'); $collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner'); $collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); $this->joinPositionField($collection, $searchCriteria); $this->collectionProcessor->process($searchCriteria, $collection); $collection->load(); $collection->addCategoryIds(); $this->addExtensionAttributes($collection); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); $searchResult->setTotalCount($collection->getSize()); foreach ($collection->getItems() as $product) { $this->cacheProduct( $this->getCacheKey( [ false, $product->getStoreId() ] ), $product ); } return $searchResult; } /** * Add extension attributes to loaded items. * * @param Collection $collection * @return Collection */ private function addExtensionAttributes(Collection $collection) : Collection { foreach ($collection->getItems() as $item) { $this->readExtensions->execute($item); } return $collection; } /** * Helper function that adds a FilterGroup to the collection. * * @deprecated 102.0.0 * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup * @param Collection $collection * @return void */ protected function addFilterGroupToCollection( \Magento\Framework\Api\Search\FilterGroup $filterGroup, Collection $collection ) { $fields = []; $categoryFilter = []; foreach ($filterGroup->getFilters() as $filter) { $conditionType = $filter->getConditionType() ?: 'eq'; if ($filter->getField() == 'category_id') { $categoryFilter[$conditionType][] = $filter->getValue(); continue; } $fields[] = ['attribute' => $filter->getField(), $conditionType => $filter->getValue()]; } if ($categoryFilter) { $collection->addCategoriesFilter($categoryFilter); } if ($fields) { $collection->addFieldToFilter($fields); } } /** * Clean internal product cache * * @return void */ public function cleanCache() { $this->instances = null; $this->instancesById = null; } /** * Retrieve media gallery processor. * * @return MediaGalleryProcessor */ private function getMediaGalleryProcessor() { if (null === $this->mediaProcessor) { $this->mediaProcessor = \Magento\Framework\App\ObjectManager::getInstance() ->get(MediaGalleryProcessor::class); } return $this->mediaProcessor; } /** * Retrieve collection processor * * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( // @phpstan-ignore-next-line - this is a virtual type defined in di.xml \Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor::class ); } return $this->collectionProcessor; } /** * Gets product from the local cache by SKU. * * @param string $sku * @param string $cacheKey * @return Product|null */ private function getProductFromLocalCache(string $sku, string $cacheKey) { $preparedSku = $this->prepareSku($sku); return $this->instances[$preparedSku][$cacheKey] ?? null; } /** * Removes product in the local cache by sku. * * @param string $sku * @return void */ private function removeProductFromLocalCacheBySku(string $sku): void { $preparedSku = $this->prepareSku($sku); unset($this->instances[$preparedSku]); } /** * Removes product in the local cache by id. * * @param string|null $id * @return void */ private function removeProductFromLocalCacheById(?string $id): void { unset($this->instancesById[$id]); } /** * Saves product in the local cache by sku. * * @param Product $product * @param string $cacheKey * @return void */ private function saveProductInLocalCache(Product $product, string $cacheKey): void { $preparedSku = $this->prepareSku($product->getSku()); $this->instances[$preparedSku][$cacheKey] = $product; } /** * Converts SKU to lower case and trims. * * @param string $sku * @return string */ private function prepareSku(string $sku): string { return mb_strtolower(trim($sku)); } /** * Save product resource model. * * @param ProductInterface|Product $product * @throws TemporaryCouldNotSaveException * @throws InputException * @throws CouldNotSaveException * @throws LocalizedException */ private function saveProduct($product): void { try { $this->removeProductFromLocalCacheBySku($product->getSku()); $this->removeProductFromLocalCacheById($product->getId()); $this->resourceModel->save($product); } catch (ConnectionException $exception) { throw new TemporaryCouldNotSaveException( __('Database connection error'), $exception, $exception->getCode() ); } catch (DeadlockException $exception) { throw new TemporaryCouldNotSaveException( __('Database deadlock found when trying to get lock'), $exception, $exception->getCode() ); } catch (LockWaitException $exception) { throw new TemporaryCouldNotSaveException( __('Database lock wait timeout exceeded'), $exception, $exception->getCode() ); } catch (AttributeException $exception) { throw InputException::invalidFieldValue( $exception->getAttributeCode(), $product->getData($exception->getAttributeCode()), $exception ); } catch (ValidatorException $e) { throw new CouldNotSaveException(__($e->getMessage())); } catch (LocalizedException $e) { throw $e; } catch (\Exception $e) { throw new CouldNotSaveException( __('The product was unable to be saved. Please try again.'), $e ); } } /** * Join category position field to make sorting by position possible. * * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria * @return void */ private function joinPositionField( Collection $collection, SearchCriteriaInterface $searchCriteria ): void { $categoryIds = [[]]; foreach ($searchCriteria->getFilterGroups() as $filterGroup) { foreach ($filterGroup->getFilters() as $filter) { if ($filter->getField() === 'category_id') { $filterValue = $filter->getValue(); $categoryIds[] = is_array($filterValue) ? $filterValue : explode(',', $filterValue); } } } $categoryIds = array_unique(array_merge(...$categoryIds)); if (count($categoryIds) === 1) { $collection->joinField( 'position', 'catalog_category_product', 'position', 'product_id=entity_id', ['category_id' => current($categoryIds)], 'left' ); } } /** * @inheritDoc */ public function _resetState(): void { $this->instances = []; $this->instancesById = []; } } ?php /** * Copyright 2014 Adobe * All Rights Reserved. */ namespace Magento\Catalog\Model; use Magento\Catalog\Api\CategoryLinkManagementInterface; use Magento\Catalog\Api\Data\ProductAttributeInterface; use Magento\Catalog\Api\Data\ProductExtension; use Magento\Catalog\Api\Data\ProductInterface; use Magento\Catalog\Model\Attribute\ScopeOverriddenValue; use Magento\Catalog\Model\Product\Gallery\MimeTypeExtensionMap; use Magento\Catalog\Model\ProductRepository\MediaGalleryProcessor; use Magento\Catalog\Model\ResourceModel\Product\Collection; use Magento\Eav\Model\Entity\Attribute\Exception as AttributeException; use Magento\Framework\Api\Data\ImageContentInterfaceFactory; use Magento\Framework\Api\ImageContentValidatorInterface; use Magento\Framework\Api\ImageProcessorInterface; use Magento\Framework\Api\SearchCriteria\CollectionProcessorInterface; use Magento\Framework\Api\SearchCriteriaInterface; use Magento\Framework\DB\Adapter\ConnectionException; use Magento\Framework\DB\Adapter\DeadlockException; use Magento\Framework\DB\Adapter\LockWaitException; use Magento\Framework\EntityManager\Operation\Read\ReadExtensions; use Magento\Framework\Exception\CouldNotSaveException; use Magento\Framework\Exception\InputException; use Magento\Framework\Exception\LocalizedException; use Magento\Framework\Exception\NoSuchEntityException; use Magento\Framework\Exception\StateException; use Magento\Framework\Exception\TemporaryState\CouldNotSaveException as TemporaryCouldNotSaveException; use Magento\Framework\Exception\ValidatorException; use Magento\Framework\ObjectManager\ResetAfterRequestInterface; use Magento\Store\Model\Store; use Magento\Catalog\Api\Data\EavAttributeInterface; /** * @inheritdoc * * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * @SuppressWarnings(PHPMD.CouplingBetweenObjects) * @SuppressWarnings(PHPMD.TooManyFields) * phpcs:disable Magento2.Commenting.ClassPropertyPHPDocFormatting * phpcs:disable Magento2.Annotation.MethodAnnotationStructure.InvalidDeprecatedTagUsage */ class ProductRepository implements \Magento\Catalog\Api\ProductRepositoryInterface, ResetAfterRequestInterface { /** * @var \Magento\Catalog\Api\ProductCustomOptionRepositoryInterface */ protected $optionRepository; /** * @var ProductFactory */ protected $productFactory; /** * @var Product[] */ protected $instances = []; /** * @var Product[] */ protected $instancesById = []; /** * @var \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory */ protected $searchResultsFactory; /** * @var \Magento\Framework\Api\SearchCriteriaBuilder */ protected $searchCriteriaBuilder; /** * @var \Magento\Framework\Api\FilterBuilder */ protected $filterBuilder; /** * @var \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory */ protected $collectionFactory; /** * @var \Magento\Catalog\Model\ResourceModel\Product */ protected $resourceModel; /** * @var Product\Initialization\Helper\ProductLinks */ protected $linkInitializer; /** * @var Product\LinkTypeProvider */ protected $linkTypeProvider; /** * @var \Magento\Store\Model\StoreManagerInterface */ protected $storeManager; /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface */ protected $attributeRepository; /** * @var \Magento\Catalog\Api\ProductAttributeRepositoryInterface */ protected $metadataService; /** * @var \Magento\Framework\Api\ExtensibleDataObjectConverter */ protected $extensibleDataObjectConverter; /** * @var \Magento\Framework\Filesystem */ protected $fileSystem; /** * @deprecated 103.0.2 * * @var ImageContentInterfaceFactory */ protected $contentFactory; /** * @deprecated 103.0.2 * * @var ImageProcessorInterface */ protected $imageProcessor; /** * @var \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface */ protected $extensionAttributesJoinProcessor; /** * @deprecated 103.0.2 * * @var \Magento\Catalog\Model\Product\Gallery\Processor */ protected $mediaGalleryProcessor; /** * @var MediaGalleryProcessor */ private $mediaProcessor; /** * @var CollectionProcessorInterface */ private $collectionProcessor; /** * @var int */ private $cacheLimit = 0; /** * @var \Magento\Framework\Serialize\Serializer\Json */ private $serializer; /** * @var ReadExtensions */ private $readExtensions; /** * @var CategoryLinkManagementInterface */ private $linkManagement; /** * @var ScopeOverriddenValue */ private $scopeOverriddenValue; /** * ProductRepository constructor. * @param ProductFactory $productFactory * @param \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory $searchResultsFactory * @param ResourceModel\Product\CollectionFactory $collectionFactory * @param \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository * @param ResourceModel\Product $resourceModel * @param Product\Initialization\Helper\ProductLinks $linkInitializer * @param Product\LinkTypeProvider $linkTypeProvider * @param \Magento\Store\Model\StoreManagerInterface $storeManager * @param \Magento\Framework\Api\FilterBuilder $filterBuilder * @param \Magento\Catalog\Api\ProductAttributeRepositoryInterface $metadataServiceInterface * @param \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter * @param Product\Option\Converter $optionConverter * @param \Magento\Framework\Filesystem $fileSystem * @param ImageContentValidatorInterface $contentValidator * @param ImageContentInterfaceFactory $contentFactory * @param MimeTypeExtensionMap $mimeTypeExtensionMap * @param ImageProcessorInterface $imageProcessor * @param \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor * @param CollectionProcessorInterface $collectionProcessor [optional] * @param \Magento\Framework\Serialize\Serializer\Json|null $serializer * @param int $cacheLimit [optional] * @param ReadExtensions $readExtensions * @param CategoryLinkManagementInterface $linkManagement * @param ScopeOverriddenValue|null $scopeOverriddenValue * @SuppressWarnings(PHPMD.ExcessiveParameterList) * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ public function __construct( ProductFactory $productFactory, \Magento\Catalog\Api\Data\ProductSearchResultsInterfaceFactory $searchResultsFactory, \Magento\Catalog\Model\ResourceModel\Product\CollectionFactory $collectionFactory, \Magento\Framework\Api\SearchCriteriaBuilder $searchCriteriaBuilder, \Magento\Catalog\Api\ProductAttributeRepositoryInterface $attributeRepository, \Magento\Catalog\Model\ResourceModel\Product $resourceModel, \Magento\Catalog\Model\Product\Initialization\Helper\ProductLinks $linkInitializer, \Magento\Catalog\Model\Product\LinkTypeProvider $linkTypeProvider, \Magento\Store\Model\StoreManagerInterface $storeManager, \Magento\Framework\Api\FilterBuilder $filterBuilder, \Magento\Catalog\Api\ProductAttributeRepositoryInterface $metadataServiceInterface, \Magento\Framework\Api\ExtensibleDataObjectConverter $extensibleDataObjectConverter, \Magento\Catalog\Model\Product\Option\Converter $optionConverter, \Magento\Framework\Filesystem $fileSystem, ImageContentValidatorInterface $contentValidator, ImageContentInterfaceFactory $contentFactory, MimeTypeExtensionMap $mimeTypeExtensionMap, ImageProcessorInterface $imageProcessor, \Magento\Framework\Api\ExtensionAttribute\JoinProcessorInterface $extensionAttributesJoinProcessor, ?CollectionProcessorInterface $collectionProcessor = null, ?\Magento\Framework\Serialize\Serializer\Json $serializer = null, $cacheLimit = 1000, ?ReadExtensions $readExtensions = null, ?CategoryLinkManagementInterface $linkManagement = null, ?ScopeOverriddenValue $scopeOverriddenValue = null ) { $this->productFactory = $productFactory; $this->collectionFactory = $collectionFactory; $this->searchResultsFactory = $searchResultsFactory; $this->searchCriteriaBuilder = $searchCriteriaBuilder; $this->resourceModel = $resourceModel; $this->linkInitializer = $linkInitializer; $this->linkTypeProvider = $linkTypeProvider; $this->storeManager = $storeManager; $this->attributeRepository = $attributeRepository; $this->filterBuilder = $filterBuilder; $this->metadataService = $metadataServiceInterface; $this->extensibleDataObjectConverter = $extensibleDataObjectConverter; $this->fileSystem = $fileSystem; $this->contentFactory = $contentFactory; $this->imageProcessor = $imageProcessor; $this->extensionAttributesJoinProcessor = $extensionAttributesJoinProcessor; $this->collectionProcessor = $collectionProcessor ?: $this->getCollectionProcessor(); $this->serializer = $serializer ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(\Magento\Framework\Serialize\Serializer\Json::class); $this->cacheLimit = (int)$cacheLimit; $this->readExtensions = $readExtensions ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(ReadExtensions::class); $this->linkManagement = $linkManagement ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(CategoryLinkManagementInterface::class); $this->scopeOverriddenValue = $scopeOverriddenValue ?: \Magento\Framework\App\ObjectManager::getInstance() ->get(ScopeOverriddenValue::class); } /** * @inheritdoc */ public function get($sku, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId === null ? $storeId : (int) $storeId]); $cachedProduct = $this->getProductFromLocalCache($sku, $cacheKey); if ($cachedProduct === null || $forceReload) { $product = $this->productFactory->create(); $productId = $this->resourceModel->getIdBySku($sku); if (!$productId) { throw new NoSuchEntityException( __("The product that was requested doesn't exist. Verify the product and try again.") ); } if ($editMode) { $product->setData('_edit_mode', true); } if ($storeId !== null) { $product->setData('store_id', (int) $storeId); } $product->load($productId); $this->cacheProduct($cacheKey, $product); $cachedProduct = $product; } return $cachedProduct; } /** * @inheritdoc */ public function getById($productId, $editMode = false, $storeId = null, $forceReload = false) { $cacheKey = $this->getCacheKey([$editMode, $storeId === null ? $storeId : (int) $storeId]); if (!isset($this->instancesById[$productId][$cacheKey]) || $forceReload) { $product = $this->productFactory->create(); if ($editMode) { $product->setData('_edit_mode', true); } if ($storeId !== null) { $product->setData('store_id', $storeId); } $product->load($productId); if (!$product->getId()) { throw new NoSuchEntityException( __("The product that was requested doesn't exist. Verify the product and try again.") ); } $this->cacheProduct($cacheKey, $product); } return $this->instancesById[$productId][$cacheKey]; } /** * Get key for cache * * @param array $data * @return string */ protected function getCacheKey($data) { $serializeData = []; foreach ($data as $key => $value) { if (is_object($value)) { $serializeData[$key] = $value->getId(); } else { $serializeData[$key] = $value; } } $serializeData = $this->serializer->serialize($serializeData); return sha1($serializeData); } /** * Add product to internal cache and truncate cache if it has more than cacheLimit elements. * * @param string $cacheKey * @param ProductInterface $product * @return void */ private function cacheProduct($cacheKey, ProductInterface $product) { $this->instancesById[$product->getId()][$cacheKey] = $product; $this->saveProductInLocalCache($product, $cacheKey); if ($this->cacheLimit && count($this->instances) > $this->cacheLimit) { $offset = round($this->cacheLimit / -2); $this->instancesById = array_slice($this->instancesById, $offset, null, true); $this->instances = array_slice($this->instances, $offset, null, true); } } /** * Merge data from DB and updates from request * * @param array $productData * @param bool $createNew * @return ProductInterface|Product * @throws NoSuchEntityException */ protected function initializeProductData(array $productData, $createNew) { unset($productData['media_gallery']); if ($createNew) { $product = $this->productFactory->create(); $this->assignProductToWebsites($product); } elseif (!empty($productData['id'])) { $this->removeProductFromLocalCacheById($productData['id']); $product = $this->getById($productData['id']); } else { $this->removeProductFromLocalCacheBySku($productData['sku']); $product = $this->get($productData['sku']); } foreach ($productData as $key => $value) { $product->setData($key, $value); } return $product; } /** * Assign product to websites. * * @param \Magento\Catalog\Model\Product $product * @return void */ private function assignProductToWebsites(\Magento\Catalog\Model\Product $product) { if ($this->storeManager->getStore(true)->getCode() == \Magento\Store\Model\Store::ADMIN_CODE) { $websiteIds = array_keys($this->storeManager->getWebsites()); } else { $websiteIds = [$this->storeManager->getStore()->getWebsiteId()]; } $product->setWebsiteIds($websiteIds); } /** * Process new gallery media entry. * * @deprecated 103.0.2 * @see MediaGalleryProcessor::processNewMediaGalleryEntry() * * @param ProductInterface $product * @param array $newEntry * @return $this * @throws InputException * @throws StateException * @throws \Magento\Framework\Exception\LocalizedException */ protected function processNewMediaGalleryEntry( ProductInterface $product, array $newEntry ) { $this->getMediaGalleryProcessor()->processNewMediaGalleryEntry($product, $newEntry); return $this; } /** * Process product links, creating new links, updating and deleting existing links * * @param ProductInterface $product * @param \Magento\Catalog\Api\Data\ProductLinkInterface[] $newLinks * @return $this * @throws NoSuchEntityException */ private function processLinks(ProductInterface $product, $newLinks) { if ($newLinks === null) { // If product links were not specified, don't do anything return $this; } // Clear all existing product links and then set the ones we want $linkTypes = $this->linkTypeProvider->getLinkTypes(); foreach (array_keys($linkTypes) as $typeName) { $this->linkInitializer->initializeLinks($product, [$typeName => []]); } // Set each linktype info if (!empty($newLinks)) { $productLinks = []; foreach ($newLinks as $link) { $productLinks[$link->getLinkType()][] = $link; } foreach ($productLinks as $type => $linksByType) { $assignedSkuList = []; /** @var \Magento\Catalog\Api\Data\ProductLinkInterface $link */ foreach ($linksByType as $link) { $assignedSkuList[] = $link->getLinkedProductSku(); } $linkedProductIds = $this->resourceModel->getProductsIdsBySkus($assignedSkuList); $linksToInitialize = []; foreach ($linksByType as $link) { $linkDataArray = $this->extensibleDataObjectConverter ->toNestedArray($link, [], \Magento\Catalog\Api\Data\ProductLinkInterface::class); $linkedSku = $link->getLinkedProductSku(); if (!isset($linkedProductIds[$linkedSku])) { throw new NoSuchEntityException( __('The Product with the "%1" SKU doesn\'t exist.', $linkedSku) ); } $linkDataArray['product_id'] = $linkedProductIds[$linkedSku]; $linksToInitialize[$linkedProductIds[$linkedSku]] = $linkDataArray; } $this->linkInitializer->initializeLinks($product, [$type => $linksToInitialize]); } } $product->setProductLinks($newLinks); return $this; } /** * Process Media gallery data before save product. * * Compare Media Gallery Entries Data with existing Media Gallery * * If Media entry has not value_id set it as new * * If Existing entry 'value_id' absent in Media Gallery set 'removed' flag * * Merge Existing and new media gallery * * @param ProductInterface $product contains only existing media gallery items * @param array $mediaGalleryEntries array which contains all media gallery items * @return $this * @throws InputException * @throws StateException * @throws LocalizedException * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ protected function processMediaGallery(ProductInterface $product, $mediaGalleryEntries) { $this->getMediaGalleryProcessor()->processMediaGallery($product, $mediaGalleryEntries); return $this; } /** * @inheritdoc * @SuppressWarnings(PHPMD.CyclomaticComplexity) * @SuppressWarnings(PHPMD.NPathComplexity) * @SuppressWarnings(PHPMD.ExcessiveMethodLength) */ public function save(ProductInterface $product, $saveOptions = false) { try { $assignToCategories = false; $tierPrices = $product->getData('tier_price'); $productDataToChange = $product->getData(); if (!$product->getSku()) { throw new CouldNotSaveException( __("The \"%1\" attribute value is empty. Set the attribute and try again.", "sku") ); } try { $existingProduct = $product->getId() ? $this->getById($product->getId()) : $this->get($product->getSku()); $product->setData( $this->resourceModel->getLinkField(), $existingProduct->getData($this->resourceModel->getLinkField()) ); if (!$product->hasData(Product::STATUS)) { $product->setStatus($existingProduct->getStatus()); } /** @var ProductExtension $extensionAttributes */ $extensionAttributes = $product->getExtensionAttributes(); if (empty($extensionAttributes->__toArray())) { $product->setExtensionAttributes($existingProduct->getExtensionAttributes()); $assignToCategories = true; } } catch (NoSuchEntityException $e) { $existingProduct = null; } $productDataArray = $this->extensibleDataObjectConverter ->toNestedArray($product, [], ProductInterface::class); $productDataArray = array_replace($productDataArray, $product->getData()); $ignoreLinksFlag = $product->getData('ignore_links_flag'); $productLinks = null; if (!$ignoreLinksFlag && $ignoreLinksFlag !== null) { $productLinks = $product->getProductLinks(); } if (!isset($productDataArray['store_id'])) { $productDataArray['store_id'] = (int) $this->storeManager->getStore()->getId(); } $product = $this->initializeProductData($productDataArray, empty($existingProduct)); $this->processLinks($product, $productLinks); if (isset($productDataArray['media_gallery'])) { $this->processMediaGallery($product, $productDataArray['media_gallery']['images']); } if (!$product->getOptionsReadonly()) { $product->setCanSaveCustomOptions(true); } $validationResult = $this->resourceModel->validate($product); if (true !== $validationResult) { throw new \Magento\Framework\Exception\CouldNotSaveException( __('Invalid product data: %1', implode(',', $validationResult)) ); } if ($tierPrices !== null) { $product->setData('tier_price', $tierPrices); } try { $stores = $product->getStoreIds(); $websites = $product->getWebsiteIds(); } catch (NoSuchEntityException $exception) { $stores = null; $websites = null; } if (!empty($existingProduct) && is_array($stores) && is_array($websites)) { $hasDataChanged = false; $productAttributes = $product->getAttributes(); if ($productAttributes !== null && $product->getStoreId() !== Store::DEFAULT_STORE_ID && (count($stores) > 1 || count($websites) === 1) ) { $imageRoles = ['image', 'small_image', 'thumbnail']; foreach ($productAttributes as $attribute) { $attributeCode = $attribute->getAttributeCode(); $value = $product->getData($attributeCode); if (!in_array($attributeCode, $imageRoles) && $existingProduct->getData($attributeCode) === $value && $existingProduct->getOrigData($attributeCode) === $value && $attribute->getScope() !== EavAttributeInterface::SCOPE_GLOBAL_TEXT && !is_array($value) && !$attribute->isStatic() && $value !== null && !$this->scopeOverriddenValue->containsValue( ProductInterface::class, $product, $attributeCode, $product->getStoreId() ) ) { $product->setData( $attributeCode, $attributeCode === ProductAttributeInterface::CODE_SEO_FIELD_URL_KEY ? false : null ); $hasDataChanged = true; } elseif (in_array($attributeCode, $imageRoles) && $existingProduct->getData($attributeCode) === $value && !array_key_exists($attributeCode, $productDataToChange) && $attribute->getScope() !== EavAttributeInterface::SCOPE_GLOBAL_TEXT && $value !== null && !$this->scopeOverriddenValue->containsValue( ProductInterface::class, $product, $attributeCode, $product->getStoreId() ) ) { $product->setData($attributeCode, null); $hasDataChanged = true; } } if ($hasDataChanged) { $product->setData('_edit_mode', true); } } } $this->saveProduct($product); if ($assignToCategories === true && $product->getCategoryIds()) { $this->linkManagement->assignProductToCategories( $product->getSku(), $product->getCategoryIds() ); } $this->removeProductFromLocalCacheBySku($product->getSku()); $this->removeProductFromLocalCacheById($product->getId()); } catch (\Exception $e) { $writer = new \Zend_Log_Writer_Stream(BP . '/var/log/product_save_error.log'); $logger = new \Zend_Log(); $logger->addWriter($writer); $logger->info('SKU: ' . $product->getSku() . ' Error: ' . $e->getMessage()); $logger->info($e->getTraceAsString()); throw $e; } return $this->get($product->getSku(), false, $product->getStoreId()); } /** * @inheritdoc */ public function delete(ProductInterface $product) { $sku = $product->getSku(); $productId = $product->getId(); try { $this->removeProductFromLocalCacheBySku($product->getSku()); $this->removeProductFromLocalCacheById($product->getId()); $this->resourceModel->delete($product); } catch (ValidatorException $e) { throw new CouldNotSaveException(__($e->getMessage()), $e); } catch (\Exception $e) { throw new \Magento\Framework\Exception\StateException( __('The "%1" product couldn\'t be removed.', $sku), $e ); } $this->removeProductFromLocalCacheBySku($sku); $this->removeProductFromLocalCacheById($productId); return true; } /** * @inheritdoc */ public function deleteById($sku) { $product = $this->get($sku); return $this->delete($product); } /** * @inheritdoc */ public function getList(SearchCriteriaInterface $searchCriteria) { /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */ $collection = $this->collectionFactory->create(); $this->extensionAttributesJoinProcessor->process($collection); $collection->addAttributeToSelect('*'); $collection->joinAttribute('status', 'catalog_product/status', 'entity_id', null, 'inner'); $collection->joinAttribute('visibility', 'catalog_product/visibility', 'entity_id', null, 'inner'); $this->joinPositionField($collection, $searchCriteria); $this->collectionProcessor->process($searchCriteria, $collection); $collection->load(); $collection->addCategoryIds(); $this->addExtensionAttributes($collection); $searchResult = $this->searchResultsFactory->create(); $searchResult->setSearchCriteria($searchCriteria); $searchResult->setItems($collection->getItems()); $searchResult->setTotalCount($collection->getSize()); foreach ($collection->getItems() as $product) { $this->cacheProduct( $this->getCacheKey( [ false, $product->getStoreId() ] ), $product ); } return $searchResult; } /** * Add extension attributes to loaded items. * * @param Collection $collection * @return Collection */ private function addExtensionAttributes(Collection $collection) : Collection { foreach ($collection->getItems() as $item) { $this->readExtensions->execute($item); } return $collection; } /** * Helper function that adds a FilterGroup to the collection. * * @deprecated 102.0.0 * @param \Magento\Framework\Api\Search\FilterGroup $filterGroup * @param Collection $collection * @return void */ protected function addFilterGroupToCollection( \Magento\Framework\Api\Search\FilterGroup $filterGroup, Collection $collection ) { $fields = []; $categoryFilter = []; foreach ($filterGroup->getFilters() as $filter) { $conditionType = $filter->getConditionType() ?: 'eq'; if ($filter->getField() == 'category_id') { $categoryFilter[$conditionType][] = $filter->getValue(); continue; } $fields[] = ['attribute' => $filter->getField(), $conditionType => $filter->getValue()]; } if ($categoryFilter) { $collection->addCategoriesFilter($categoryFilter); } if ($fields) { $collection->addFieldToFilter($fields); } } /** * Clean internal product cache * * @return void */ public function cleanCache() { $this->instances = null; $this->instancesById = null; } /** * Retrieve media gallery processor. * * @return MediaGalleryProcessor */ private function getMediaGalleryProcessor() { if (null === $this->mediaProcessor) { $this->mediaProcessor = \Magento\Framework\App\ObjectManager::getInstance() ->get(MediaGalleryProcessor::class); } return $this->mediaProcessor; } /** * Retrieve collection processor * * phpcs:disable Magento2.Annotation.MethodAnnotationStructure * @deprecated 102.0.0 * @return CollectionProcessorInterface */ private function getCollectionProcessor() { if (!$this->collectionProcessor) { $this->collectionProcessor = \Magento\Framework\App\ObjectManager::getInstance()->get( // @phpstan-ignore-next-line - this is a virtual type defined in di.xml \Magento\Catalog\Model\Api\SearchCriteria\ProductCollectionProcessor::class ); } return $this->collectionProcessor; } /** * Gets product from the local cache by SKU. * * @param string $sku * @param string $cacheKey * @return Product|null */ private function getProductFromLocalCache(string $sku, string $cacheKey) { $preparedSku = $this->prepareSku($sku); return $this->instances[$preparedSku][$cacheKey] ?? null; } /** * Removes product in the local cache by sku. * * @param string $sku * @return void */ private function removeProductFromLocalCacheBySku(string $sku): void { $preparedSku = $this->prepareSku($sku); unset($this->instances[$preparedSku]); } /** * Removes product in the local cache by id. * * @param string|null $id * @return void */ private function removeProductFromLocalCacheById(?string $id): void { unset($this->instancesById[$id]); } /** * Saves product in the local cache by sku. * * @param Product $product * @param string $cacheKey * @return void */ private function saveProductInLocalCache(Product $product, string $cacheKey): void { $preparedSku = $this->prepareSku($product->getSku()); $this->instances[$preparedSku][$cacheKey] = $product; } /** * Converts SKU to lower case and trims. * * @param string $sku * @return string */ private function prepareSku(string $sku): string { return mb_strtolower(trim($sku)); } /** * Save product resource model. * * @param ProductInterface|Product $product * @throws TemporaryCouldNotSaveException * @throws InputException * @throws CouldNotSaveException * @throws LocalizedException */ private function saveProduct($product): void { try { $this->removeProductFromLocalCacheBySku($product->getSku()); $this->removeProductFromLocalCacheById($product->getId()); $this->resourceModel->save($product); } catch (ConnectionException $exception) { throw new TemporaryCouldNotSaveException( __('Database connection error'), $exception, $exception->getCode() ); } catch (DeadlockException $exception) { throw new TemporaryCouldNotSaveException( __('Database deadlock found when trying to get lock'), $exception, $exception->getCode() ); } catch (LockWaitException $exception) { throw new TemporaryCouldNotSaveException( __('Database lock wait timeout exceeded'), $exception, $exception->getCode() ); } catch (AttributeException $exception) { throw InputException::invalidFieldValue( $exception->getAttributeCode(), $product->getData($exception->getAttributeCode()), $exception ); } catch (ValidatorException $e) { throw new CouldNotSaveException(__($e->getMessage())); } catch (LocalizedException $e) { throw $e; } catch (\Exception $e) { throw new CouldNotSaveException( __('The product was unable to be saved. Please try again.'), $e ); } } /** * Join category position field to make sorting by position possible. * * @param Collection $collection * @param SearchCriteriaInterface $searchCriteria * @return void */ private function joinPositionField( Collection $collection, SearchCriteriaInterface $searchCriteria ): void { $categoryIds = [[]]; foreach ($searchCriteria->getFilterGroups() as $filterGroup) { foreach ($filterGroup->getFilters() as $filter) { if ($filter->getField() === 'category_id') { $filterValue = $filter->getValue(); $categoryIds[] = is_array($filterValue) ? $filterValue : explode(',', $filterValue); } } } $categoryIds = array_unique(array_merge(...$categoryIds)); if (count($categoryIds) === 1) { $collection->joinField( 'position', 'catalog_category_product', 'position', 'product_id=entity_id', ['category_id' => current($categoryIds)], 'left' ); } } /** * @inheritDoc */ public function _resetState(): void { $this->instances = []; $this->instancesById = []; } } 1 exception(s): Exception #0 (ReflectionException): Class "Magento\Catalog\Model\ProductRepository\Interceptor" does not exist Exception #0 (ReflectionException): Class "Magento\Catalog\Model\ProductRepository\Interceptor" does not exist
#1 Magento\Framework\Code\Reader\ClassReader->getConstructor() called at [vendor/magento/framework/ObjectManager/Definition/Runtime.php:50]
#2 Magento\Framework\ObjectManager\Definition\Runtime->getParameters() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:48]
#3 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->create() called at [vendor/magento/framework/ObjectManager/ObjectManager.php:73]
#4 Magento\Framework\ObjectManager\ObjectManager->get() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:170]
#5 Magento\Framework\ObjectManager\Factory\AbstractFactory->resolveArgument() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:276]
#6 Magento\Framework\ObjectManager\Factory\AbstractFactory->getResolvedArgument() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:239]
#7 Magento\Framework\ObjectManager\Factory\AbstractFactory->resolveArgumentsInRuntime() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:34]
#8 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->_resolveArguments() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:59]
#9 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->create() called at [vendor/magento/framework/ObjectManager/ObjectManager.php:73]
#10 Magento\Framework\ObjectManager\ObjectManager->get() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:170]
#11 Magento\Framework\ObjectManager\Factory\AbstractFactory->resolveArgument() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:276]
#12 Magento\Framework\ObjectManager\Factory\AbstractFactory->getResolvedArgument() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:239]
#13 Magento\Framework\ObjectManager\Factory\AbstractFactory->resolveArgumentsInRuntime() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:34]
#14 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->_resolveArguments() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:59]
#15 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->create() called at [vendor/magento/framework/ObjectManager/ObjectManager.php:73]
#16 Magento\Framework\ObjectManager\ObjectManager->get() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:170]
#17 Magento\Framework\ObjectManager\Factory\AbstractFactory->resolveArgument() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:276]
#18 Magento\Framework\ObjectManager\Factory\AbstractFactory->getResolvedArgument() called at [vendor/magento/framework/ObjectManager/Factory/AbstractFactory.php:239]
#19 Magento\Framework\ObjectManager\Factory\AbstractFactory->resolveArgumentsInRuntime() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:34]
#20 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->_resolveArguments() called at [vendor/magento/framework/ObjectManager/Factory/Dynamic/Developer.php:59]
#21 Magento\Framework\ObjectManager\Factory\Dynamic\Developer->create() called at [vendor/magento/framework/ObjectManager/ObjectManager.php:73]
#22 Magento\Framework\ObjectManager\ObjectManager->get() called at [vendor/magento/framework/Interception/PluginList/PluginList.php:173]
#23 Magento\Framework\Interception\PluginList\PluginList->getPlugin() called at [vendor/magento/framework/Interception/Interceptor.php:119]
#24 Magento\Framework\App\Action\Forward\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/framework/Interception/Interceptor.php:153]
#25 Magento\Framework\App\Action\Forward\Interceptor->___callPlugins() called at [generated/code/Magento/Framework/App/Action/Forward/Interceptor.php:32]
#26 Magento\Framework\App\Action\Forward\Interceptor->execute() called at [vendor/magento/framework/App/Action/Forward.php:26]
#27 Magento\Framework\App\Action\Forward->dispatch() called at [vendor/magento/framework/Interception/Interceptor.php:58]
#28 Magento\Framework\App\Action\Forward\Interceptor->___callParent() called at [vendor/magento/framework/Interception/Interceptor.php:138]
#29 Magento\Framework\App\Action\Forward\Interceptor->Magento\Framework\Interception\{closure}() called at [app/code/Anowave/Ec/Plugin/App/Action/Context.php:148]
#30 Anowave\Ec\Plugin\App\Action\Context->aroundDispatch() called at [vendor/magento/framework/Interception/Interceptor.php:135]
#31 Magento\Framework\App\Action\Forward\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/amasty/shopby-seo/Plugin/Framework/App/Action/Forward.php:59]
#32 Amasty\ShopbySeo\Plugin\Framework\App\Action\Forward->aroundDispatch() called at [vendor/magento/framework/Interception/Interceptor.php:135]
#33 Magento\Framework\App\Action\Forward\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/framework/Interception/Interceptor.php:153]
#34 Magento\Framework\App\Action\Forward\Interceptor->___callPlugins() called at [generated/code/Magento/Framework/App/Action/Forward/Interceptor.php:23]
#35 Magento\Framework\App\Action\Forward\Interceptor->dispatch() called at [vendor/magento/framework/App/FrontController.php:245]
#36 Magento\Framework\App\FrontController->getActionResponse() called at [vendor/magento/framework/App/FrontController.php:212]
#37 Magento\Framework\App\FrontController->processRequest() called at [vendor/magento/framework/App/FrontController.php:146]
#38 Magento\Framework\App\FrontController->dispatch() called at [vendor/magento/framework/Interception/Interceptor.php:58]
#39 Magento\Framework\App\FrontController\Interceptor->___callParent() called at [vendor/magento/framework/Interception/Interceptor.php:138]
#40 Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/module-store/App/FrontController/Plugin/RequestPreprocessor.php:99]
#41 Magento\Store\App\FrontController\Plugin\RequestPreprocessor->aroundDispatch() called at [vendor/magento/framework/Interception/Interceptor.php:135]
#42 Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/module-page-cache/Model/App/FrontController/BuiltinPlugin.php:72]
#43 Magento\PageCache\Model\App\FrontController\BuiltinPlugin->aroundDispatch() called at [vendor/magento/framework/Interception/Interceptor.php:135]
#44 Magento\Framework\App\FrontController\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/framework/Interception/Interceptor.php:153]
#45 Magento\Framework\App\FrontController\Interceptor->___callPlugins() called at [generated/code/Magento/Framework/App/FrontController/Interceptor.php:23]
#46 Magento\Framework\App\FrontController\Interceptor->dispatch() called at [vendor/magento/framework/App/Http.php:116]
#47 Magento\Framework\App\Http->launch() called at [vendor/magento/framework/Interception/Interceptor.php:58]
#48 Magento\Framework\App\Http\Interceptor->___callParent() called at [vendor/magento/framework/Interception/Interceptor.php:138]
#49 Magento\Framework\App\Http\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/module-application-performance-monitor/Plugin/ApplicationPerformanceMonitor.php:38]
#50 Magento\ApplicationPerformanceMonitor\Plugin\ApplicationPerformanceMonitor->aroundLaunch() called at [vendor/magento/framework/Interception/Interceptor.php:135]
#51 Magento\Framework\App\Http\Interceptor->Magento\Framework\Interception\{closure}() called at [vendor/magento/framework/Interception/Interceptor.php:153]
#52 Magento\Framework\App\Http\Interceptor->___callPlugins() called at [generated/code/Magento/Framework/App/Http/Interceptor.php:23]
#53 Magento\Framework\App\Http\Interceptor->launch() called at [vendor/magento/framework/App/Bootstrap.php:264]
#54 Magento\Framework\App\Bootstrap->run() called at [pub/index.php:30]