0

I am updating product category from admin.. getting below error.

"Could not save product \"41859\" with position 0 to category 8",

Referred this link as mentioned it is fixed in 2.2.4,

How to solve the error : "Could not save product "20072" with position 0 to category 7" in magento 2

I am also using using magento version 2.2.4. but still same error is there.

Category is not updating even if we update from admin side.

Is any fix available for this? Please anybody help me. Thanks

  • You can check this link Cannot assign products to categories Thanks, pritam – Pritam Info 24 Nov 26 '18 at 7:04
  • Seems it has magento 2 bug. – Amit Bera Nov 26 '18 at 11:04
  • @AmitBera, So any fix available for this? – jafar pinjar Nov 26 '18 at 11:08
  • I do not have any fixing right now. I will check and get back to you – Amit Bera Nov 26 '18 at 11:24
  • @AmitBera, Sure, Please check it and update me, this issue is blocked all other testing of our site. Thanks – jafar pinjar Nov 26 '18 at 11:29
1

I have faced same issue and fixed to update file Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver.php and \Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler.php and working fine.

I have override with custom module and updated fixed code.

File path: magento/app/code/Vendor/CategorySave/registration.php

<?php

\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Vendor_CategorySave',
    __DIR__
);

File path: magento/app/code/Vendor/CategorySave/etc/module.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Vendor_CategorySave" setup_version="1.0.0">
    </module>
</config>

File path: magento/app/code/Vendor/CategorySave/etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <preference for="Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver" type="Vendor\CategorySave\Observer\CategoryProcessUrlRewriteSavingObserver" />

    <preference for="Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler" type="Vendor\CategorySave\Observer\UrlRewriteHandler" />
</config>

File path: magento/app/code/Vendor/CategorySave/Observer/CategoryProcessUrlRewriteSavingObserver.php

<?php
namespace Vendor\CategorySave\Observer;
use Magento\Catalog\Model\Category;
use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator;
use Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool;
use Magento\CatalogUrlRewrite\Model\Map\DataCategoryUrlRewriteDatabaseMap;
use Magento\CatalogUrlRewrite\Model\Map\DataProductUrlRewriteDatabaseMap;
use Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer;
use Magento\Framework\Event\ObserverInterface;
use Magento\Store\Model\ResourceModel\Group\CollectionFactory;
use Magento\Store\Model\ResourceModel\Group\Collection as StoreGroupCollection;
use Magento\Framework\App\ObjectManager;
/**
 * Generates Category Url Rewrites after save and Products Url Rewrites assigned to the category that's being saved
 */
class CategoryProcessUrlRewriteSavingObserver extends \Magento\CatalogUrlRewrite\Observer\CategoryProcessUrlRewriteSavingObserver
{
    /**
     * @var \Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator
     */
    private $categoryUrlRewriteGenerator;
    /**
     * @var \Magento\CatalogUrlRewrite\Model\UrlRewriteBunchReplacer
     */
    private $urlRewriteBunchReplacer;
    /**
     * @var \Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler
     */
    private $urlRewriteHandler;
    /**
     * @var \Magento\CatalogUrlRewrite\Model\Map\DatabaseMapPool
     */
    private $databaseMapPool;
    /**
     * @var string[]
     */
    private $dataUrlRewriteClassNames;
    /**
     * @var CollectionFactory
     */
    private $storeGroupFactory;
    /**
     * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator
     * @param UrlRewriteHandler $urlRewriteHandler
     * @param UrlRewriteBunchReplacer $urlRewriteBunchReplacer
     * @param DatabaseMapPool $databaseMapPool
     * @param string[] $dataUrlRewriteClassNames
     * @param CollectionFactory|null $storeGroupFactory
     */
    public function __construct(
        CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator,
        UrlRewriteHandler $urlRewriteHandler,
        UrlRewriteBunchReplacer $urlRewriteBunchReplacer,
        DatabaseMapPool $databaseMapPool,
        $dataUrlRewriteClassNames = [
            DataCategoryUrlRewriteDatabaseMap::class,
            DataProductUrlRewriteDatabaseMap::class
        ],
        CollectionFactory $storeGroupFactory = null
    ) {
        $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
        $this->urlRewriteHandler = $urlRewriteHandler;
        $this->urlRewriteBunchReplacer = $urlRewriteBunchReplacer;
        $this->databaseMapPool = $databaseMapPool;
        $this->dataUrlRewriteClassNames = $dataUrlRewriteClassNames;
        $this->storeGroupFactory = $storeGroupFactory
            ?: ObjectManager::getInstance()->get(CollectionFactory::class);
    }
    /**
     * Generate urls for UrlRewrite and save it in storage
     *
     * @param \Magento\Framework\Event\Observer $observer
     * @return void
     * @throws \Magento\Framework\Exception\AlreadyExistsException
     */
    public function execute(\Magento\Framework\Event\Observer $observer)
    {
        /** @var Category $category */
        $category = $observer->getEvent()->getData('category');
        if ($category->getParentId() == Category::TREE_ROOT_ID) {
            return;
        }
        if (!$category->hasData('store_id')) {
            $this->setCategoryStoreId($category);
        }
        $mapsGenerated = false;
        if ($this->isCategoryHasChanged($category)) {
            if ($category->dataHasChangedFor('url_key')) {
                $categoryUrlRewriteResult = $this->categoryUrlRewriteGenerator->generate($category);
                $this->urlRewriteBunchReplacer->doBunchReplace($categoryUrlRewriteResult);
            }
            if ($this->isChangedOnlyProduct($category)) {
                $productUrlRewriteResult =
                    $this->urlRewriteHandler->updateProductUrlRewritesForChangedProduct($category);
                $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult);
            } else {
                $productUrlRewriteResult = $this->urlRewriteHandler->generateProductUrlRewrites($category);
                $this->urlRewriteBunchReplacer->doBunchReplace($productUrlRewriteResult);
            }
            $mapsGenerated = true;
        }
        //frees memory for maps that are self-initialized in multiple classes that were called by the generators
        if ($mapsGenerated) {
            $this->resetUrlRewritesDataMaps($category);
        }
    }
    /**
     * Check is category changed changed.
     *
     * @param Category $category
     * @return bool
     */
    private function isCategoryHasChanged(Category $category): bool
    {
        if ($category->dataHasChangedFor('url_key')
            || $category->dataHasChangedFor('is_anchor')
            || !empty($category->getChangedProductIds())) {
            return true;
        }
        return false;
    }
    /**
     * Check is only product changed.
     *
     * @param Category $category
     * @return bool
     */
    private function isChangedOnlyProduct(Category $category): bool
    {
        if (!empty($category->getChangedProductIds())
            && !$category->dataHasChangedFor('is_anchor')
            && !$category->dataHasChangedFor('url_key')) {
            return true;
        }
        return false;
    }
    /**
     * In case store_id is not set for category then we can assume that it was passed through product import.
     * Store group must have only one root category, so receiving category's path and checking if one of it parts
     * is the root category for store group, we can set default_store_id value from it to category.
     * it prevents urls duplication for different stores
     * ("Default Category/category/sub" and "Default Category2/category/sub")
     *
     * @param Category $category
     * @return void
     */
    private function setCategoryStoreId($category)
    {
        /** @var StoreGroupCollection $storeGroupCollection */
        $storeGroupCollection = $this->storeGroupFactory->create();
        foreach ($storeGroupCollection as $storeGroup) {
            /** @var \Magento\Store\Model\Group $storeGroup */
            if (in_array($storeGroup->getRootCategoryId(), explode('/', $category->getPath()))) {
                $category->setStoreId($storeGroup->getDefaultStoreId());
            }
        }
    }
    /**
     * Resets used data maps to free up memory and temporary tables
     *
     * @param Category $category
     * @return void
     */
    private function resetUrlRewritesDataMaps($category)
    {
        foreach ($this->dataUrlRewriteClassNames as $className) {
            $this->databaseMapPool->resetMap($className, $category->getEntityId());
        }
    }
}

File path: magento/app/code/Vendor/CategorySave/Observer/UrlRewriteHandler.php

<?php
declare(strict_types=1);
namespace Vendor\CategorySave\Observer;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Model\Category;
use Magento\Catalog\Model\ResourceModel\Product\Collection;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
use Magento\CatalogUrlRewrite\Model\Category\ChildrenCategoriesProvider;
use Magento\CatalogUrlRewrite\Model\CategoryProductUrlPathGenerator;
use Magento\CatalogUrlRewrite\Model\CategoryUrlRewriteGenerator;
use Magento\CatalogUrlRewrite\Model\ProductScopeRewriteGenerator;
use Magento\CatalogUrlRewrite\Model\ProductUrlRewriteGenerator;
use Magento\Framework\App\ObjectManager;
use Magento\Framework\Serialize\Serializer\Json;
use Magento\UrlRewrite\Model\MergeDataProvider;
use Magento\UrlRewrite\Model\MergeDataProviderFactory;
use Magento\UrlRewrite\Model\UrlPersistInterface;
use Magento\UrlRewrite\Service\V1\Data\UrlRewrite;
/**
 * Class for management url rewrites.
 *
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class UrlRewriteHandler extends \Magento\CatalogUrlRewrite\Observer\UrlRewriteHandler
{
    /**
     * @var ChildrenCategoriesProvider
     */
    protected $childrenCategoriesProvider;
    /**
     * @var CategoryUrlRewriteGenerator
     */
    protected $categoryUrlRewriteGenerator;
    /**
     * @var ProductUrlRewriteGenerator
     */
    protected $productUrlRewriteGenerator;
    /**
     * @var UrlPersistInterface
     */
    protected $urlPersist;
    /**
     * @var array
     */
    protected $isSkippedProduct;
    /**
     * @var CollectionFactory
     */
    protected $productCollectionFactory;
    /**
     * @var CategoryProductUrlPathGenerator
     */
    private $categoryBasedProductRewriteGenerator;
    /**
     * @var MergeDataProvider
     */
    private $mergeDataProviderPrototype;
    /**
     * @var Json
     */
    private $serializer;
    /**
     * @var ProductScopeRewriteGenerator
     */
    private $productScopeRewriteGenerator;
    /**
     * @param ChildrenCategoriesProvider $childrenCategoriesProvider
     * @param CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator
     * @param ProductUrlRewriteGenerator $productUrlRewriteGenerator
     * @param UrlPersistInterface $urlPersist
     * @param CollectionFactory $productCollectionFactory
     * @param CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator
     * @param MergeDataProviderFactory|null $mergeDataProviderFactory
     * @param Json|null $serializer
     * @param ProductScopeRewriteGenerator|null $productScopeRewriteGenerator
     */
    public function __construct(
        ChildrenCategoriesProvider $childrenCategoriesProvider,
        CategoryUrlRewriteGenerator $categoryUrlRewriteGenerator,
        ProductUrlRewriteGenerator $productUrlRewriteGenerator,
        UrlPersistInterface $urlPersist,
        CollectionFactory $productCollectionFactory,
        CategoryProductUrlPathGenerator $categoryBasedProductRewriteGenerator,
        MergeDataProviderFactory $mergeDataProviderFactory = null,
        Json $serializer = null,
        ProductScopeRewriteGenerator $productScopeRewriteGenerator = null
    ) {
        $this->childrenCategoriesProvider = $childrenCategoriesProvider;
        $this->categoryUrlRewriteGenerator = $categoryUrlRewriteGenerator;
        $this->productUrlRewriteGenerator = $productUrlRewriteGenerator;
        $this->urlPersist = $urlPersist;
        $this->productCollectionFactory = $productCollectionFactory;
        $this->categoryBasedProductRewriteGenerator = $categoryBasedProductRewriteGenerator;
        $objectManager = ObjectManager::getInstance();
        $mergeDataProviderFactory = $mergeDataProviderFactory ?: $objectManager->get(MergeDataProviderFactory::class);
        $this->mergeDataProviderPrototype = $mergeDataProviderFactory->create();
        $this->serializer = $serializer ?: $objectManager->get(Json::class);
        $this->productScopeRewriteGenerator = $productScopeRewriteGenerator
            ?: $objectManager->get(ProductScopeRewriteGenerator::class);
    }
    /**
     * Generates URL rewrites for products assigned to category.
     *
     * @param Category $category
     * @return array
     */
    public function generateProductUrlRewrites(Category $category): array
    {
        $mergeDataProvider = clone $this->mergeDataProviderPrototype;
        $this->isSkippedProduct[$category->getEntityId()] = [];
        $saveRewriteHistory = (bool)$category->getData('save_rewrites_history');
        $storeId = (int)$category->getStoreId();
        if ($category->getChangedProductIds()) {
            $this->generateChangedProductUrls($mergeDataProvider, $category, $storeId, $saveRewriteHistory);
        } else {
            $mergeDataProvider->merge(
                $this->getCategoryProductsUrlRewrites(
                    $category,
                    $storeId,
                    $saveRewriteHistory,
                    $category->getEntityId()
                )
            );
        }
        foreach ($this->childrenCategoriesProvider->getChildren($category, true) as $childCategory) {
            $mergeDataProvider->merge(
                $this->getCategoryProductsUrlRewrites(
                    $childCategory,
                    $storeId,
                    $saveRewriteHistory,
                    $category->getEntityId()
                )
            );
        }
        return $mergeDataProvider->getData();
    }
    /**
     * Update product url rewrites for changed product.
     *
     * @param Category $category
     * @return array
     */
    public function updateProductUrlRewritesForChangedProduct(Category $category): array
    {
        $mergeDataProvider = clone $this->mergeDataProviderPrototype;
        $this->isSkippedProduct[$category->getEntityId()] = [];
        $saveRewriteHistory = (bool)$category->getData('save_rewrites_history');
        $storeIds = $this->getCategoryStoreIds($category);
        if ($category->getChangedProductIds()) {
            foreach ($storeIds as $storeId) {
                $this->generateChangedProductUrls($mergeDataProvider, $category, (int)$storeId, $saveRewriteHistory);
            }
        }
        return $mergeDataProvider->getData();
    }
    /**
     * Delete category rewrites for children.
     *
     * @param Category $category
     * @return void
     */
    public function deleteCategoryRewritesForChildren(Category $category)
    {
        $categoryIds = $this->childrenCategoriesProvider->getChildrenIds($category, true);
        $categoryIds[] = $category->getId();
        foreach ($categoryIds as $categoryId) {
            $this->urlPersist->deleteByData(
                [
                    UrlRewrite::ENTITY_ID =>
                        $categoryId,
                    UrlRewrite::ENTITY_TYPE =>
                        CategoryUrlRewriteGenerator::ENTITY_TYPE,
                ]
            );
            $this->urlPersist->deleteByData(
                [
                    UrlRewrite::METADATA =>
                        $this->serializer->serialize(['category_id' => $categoryId]),
                    UrlRewrite::ENTITY_TYPE =>
                        ProductUrlRewriteGenerator::ENTITY_TYPE,
                ]
            );
        }
    }
    /**
     * Get category products url rewrites.
     *
     * @param Category $category
     * @param int $storeId
     * @param bool $saveRewriteHistory
     * @param int|null $rootCategoryId
     * @return array
     */
    private function getCategoryProductsUrlRewrites(
        Category $category,
        $storeId,
        $saveRewriteHistory,
        $rootCategoryId = null
    ) {
        $mergeDataProvider = clone $this->mergeDataProviderPrototype;
        /** @var Collection $productCollection */
        $productCollection = $this->productCollectionFactory->create();
        $productCollection->addCategoriesFilter(['eq' => [$category->getEntityId()]])
            ->setStoreId($storeId)
            ->addAttributeToSelect('name')
            ->addAttributeToSelect('visibility')
            ->addAttributeToSelect('url_key')
            ->addAttributeToSelect('url_path');
        foreach ($productCollection as $product) {
            if (isset($this->isSkippedProduct[$category->getEntityId()]) &&
                in_array($product->getId(), $this->isSkippedProduct[$category->getEntityId()])
            ) {
                continue;
            }
            $this->isSkippedProduct[$category->getEntityId()][] = $product->getId();
            $product->setStoreId($storeId);
            $product->setData('save_rewrites_history', $saveRewriteHistory);
            $mergeDataProvider->merge(
                $this->categoryBasedProductRewriteGenerator->generate($product, $rootCategoryId)
            );
        }
        return $mergeDataProvider->getData();
    }
    /**
     * Generates product URL rewrites.
     *
     * @param MergeDataProvider $mergeDataProvider
     * @param Category $category
     * @param int $storeId
     * @param bool $saveRewriteHistory
     * @return void
     */
    private function generateChangedProductUrls(
        MergeDataProvider $mergeDataProvider,
        Category $category,
        int $storeId,
        bool $saveRewriteHistory
    ) {
        $this->isSkippedProduct[$category->getEntityId()] = $category->getAffectedProductIds();
        $categoryStoreIds = [$storeId];
        // If category is changed in the Global scope when need to regenerate product URL rewrites for all other scopes.
        if ($this->productScopeRewriteGenerator->isGlobalScope($storeId)) {
            $categoryStoreIds = $this->getCategoryStoreIds($category);
        }
        foreach ($categoryStoreIds as $categoryStoreId) {
            /* @var Collection $collection */
            $collection = $this->productCollectionFactory->create()
                ->setStoreId($categoryStoreId)
                ->addIdFilter($category->getAffectedProductIds())
                ->addAttributeToSelect('visibility')
                ->addAttributeToSelect('name')
                ->addAttributeToSelect('url_key')
                ->addAttributeToSelect('url_path');
            $collection->setPageSize(1000);
            $pageCount = $collection->getLastPageNumber();
            $currentPage = 1;
            while ($currentPage <= $pageCount) {
                $collection->setCurPage($currentPage);
                foreach ($collection as $product) {
                    $product->setData('save_rewrites_history', $saveRewriteHistory);
                    $product->setStoreId($categoryStoreId);
                    $mergeDataProvider->merge(
                        $this->productUrlRewriteGenerator->generate($product, $category->getEntityId())
                    );
                }
                $collection->clear();
                $currentPage++;
            }
        }
    }
    /**
     * Gets category store IDs without Global Store.
     *
     * @param Category $category
     * @return array
     */
    private function getCategoryStoreIds(Category $category): array
    {
        $ids = $category->getStoreIds();
        return array_filter($ids);
    }
}

Hope it help!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.