0

We need to get concrete error message when Magento\Catalog\Model\ProductRepository throws CouldNotSaveException during saveProduct()

private function saveProduct($product): void
{
    try {
        $this->removeProductFromLocalCacheBySku($product->getSku());
        $this->removeProductFromLocalCacheById($product->getId());
        $this->resourceModel->save($product);
    } catch (ConnectionException $exception) {
...
    } catch (\Exception $e) {
        throw new CouldNotSaveException(
            __('The product was unable to be saved. Please try again.'),
            $e
        );
    }
}

An easy way would be just adding the message in that code

__('The product was unable to be saved. Please try again.' . $e->getMessage())

But we can't edit that file, as this is a project hosted in Adobe Cloud. So, how can we get that message instead of application default message?

2
  • 1
    I'd recommend syncing down a copy of your production database, etc. locally to try to replicate the issue. At that point, you can either modify the local code for testing, or try to trigger a breakpoint at that location with Xdebug. Jul 14, 2022 at 21:28
  • Sure. But I would like to find a way to get the error in production logs Jul 16, 2022 at 8:56

2 Answers 2

3
+100

1. Create Plugin for ProductRepositoryInterface

In example also creates additional logger to write exception to separate file

app/code/Acme/StackExchange/etc/di.xml

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <!-- Create Virtual Logger -->
    <virtualType name="Acme\StackExchange\Model\VirtualLoggerHandler" type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/acme_stackexchange.log</argument>
        </arguments>
    </virtualType>
    <virtualType name="Acme\StackExchange\Model\VirtualLogger" type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="handlers" xsi:type="array">
                <item name="system" xsi:type="object">Acme\StackExchange\Model\VirtualLoggerHandler</item>
            </argument>
        </arguments>
    </virtualType>
    <!-- Register Plugin -->
    <type name="Magento\Catalog\Api\ProductRepositoryInterface">
        <plugin name="Acme_StackExchange_Plugin_Catalog_Model_ProductRepositoryPlugin"
                type="Acme\StackExchange\Plugin\Catalog\Model\ProductRepositoryPlugin" sortOrder="10"/>
    </type>
    <!-- Pass Virtual Logger to Plugin -->
    <type name="Acme\StackExchange\Plugin\Catalog\Model\ProductRepositoryPlugin">
        <arguments>
            <argument name="logger" xsi:type="object">Acme\StackExchange\Model\VirtualLogger</argument>
        </arguments>
    </type>
</config>

2. Create a Plugin

app/code/Acme/StackExchange/Plugin/Catalog/Model/ProductRepositoryPlugin.php

<?php
declare(strict_types=1);

namespace Acme\StackExchange\Plugin\Catalog\Model;

use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\CouldNotSaveException;
use Psr\Log\LoggerInterface;

class ProductRepositoryPlugin
{
    protected LoggerInterface $logger;

    public function __construct(
        LoggerInterface $logger
    ) {
        $this->logger = $logger;
    }

    public function aroundSave(
        ProductRepositoryInterface $subject,
        callable $proceed,
        ProductInterface $product,
        $saveOptions = false
    ): ProductInterface {
        try {
            return $proceed($product, $saveOptions);
        } catch (CouldNotSaveException $e) {
            if ($e->getPrevious()) {
                $this->logger->critical($e->getPrevious());
            }
            throw $e;
        }
    }
}
4
  • We can not use a plugin, or interceptor because saveProduct() is the private method (Non-public methods).
    – Msquare
    Jul 18, 2022 at 6:26
  • @Msquare I suggest to attentively read provided solution again or just test it ) Jul 18, 2022 at 7:15
  • @Raul Sanchez In my example instead of log message you can throw new Exception, and I recommend to use CouldNotSaveException instance for this. $e->getPrevious() is original one exception, that you can use for generate your custom exception message and receive original error. Jul 18, 2022 at 8:24
  • Nice! Thank you Jul 20, 2022 at 5:51
1

Patch is the way to go, we use https://github.com/cweagans/composer-patches.

composer require cweagans/composer-patches

General flow is this:

  1. Create a copy of original file, edit the file, create a patch file using diff command. Patches are relative to package folder:

    cd vendor/magento/module-catalog
    cp Model/ProductRepository.php Model/ProductRepository.orig.php
    # Edit Model/ProductRepository.php with your changes...
    mkdir ../../../patches
    diff -u Model/ProductRepository.orig.php Model/ProductRepository.php > ../../../patches/Magento_Catalog-save-product-original-exception.patch
    

    In my example it looks like this:

    --- Model/ProductRepository.orig.php    2022-07-18 10:35:56.200056524 +0300
    +++ Model/ProductRepository.php 2022-07-18 10:37:34.022779326 +0300
    @@ -909,7 +909,7 @@
                 throw $e;
             } catch (\Exception $e) {
                 throw new CouldNotSaveException(
    -                __('The product was unable to be saved. Please try again.'),
    +                __('The product was unable to be saved. Please try again. ' . $e->getMessage()),
                     $e
                 );
             }
    
    
  2. Place your patch in project folder or to public URL. patches/ local folder in this example.

  3. Register patch in composer.json

    {
        "extra": {
            "magento-force": "override",
            "composer-exit-on-patch-failure": true,
            "patches": {
                "magento/module-catalog": {
                    "Original exception message in product save": "patches/Magento_Catalog-save-product-original-exception.patch"
                }
            },
        }
    }
    
  4. Do composer install

composer install - applying patch

Our collection of patches - https://github.com/ConvertGroupsAS/magento2-patches

4
  • This is good solution for handle patches and make sense to use in some cases, but Magento Cloud has own patch system and you can just place this patch into m2-hotfixes directory. Can you explain me, why everybody try to suggest the option with overwrite original core class instead of use disigned plugin system? Any private and protected methods are call from some public and available to cover public with plugin. Jul 18, 2022 at 8:12
  • @VictorTihonchuk I already upvoted your solution with accessing original exception in plugin in this case ;) With everything could be fixed without patching, alas often it's not the case for Magento :) Plugin here has benefit of not relying on specific line numbers that will change with future Magento versions. As for m2-hotfixes, some patches are reusable across projects where not all of them are on cloud, so sometimes it's natural to have non-cloud patching solution. Jul 18, 2022 at 9:18
  • 1
    The major reason of my comment under your answer was point that Magento Cloud has own patch system devdocs.magento.com/cloud/project/project-patch.html. I'm totally agree with logic to share patches between project. I have some number of general fixes for 2.4.4 (core bugs) and some for add PHP 8.1 support for 3rd party extensions. Jul 18, 2022 at 9:24
  • Thanks. I will give this a try, but I have accepted Victor answer, as it fits more in this case Jul 20, 2022 at 5:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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