magento2 icon indicating copy to clipboard operation
magento2 copied to clipboard

Could not save child: "Unknown entity type: Magento\Bundle\Model\Selection\Interceptor requested"

Open faradey opened this issue 5 months ago • 1 comments

Preconditions and environment

  • Magento version 2.4.6, 2.4.7, 2.4.8

Steps to reproduce

  1. Create a module MyCompany/MyModule.

  2. Add a plugin in the app/code/MyCompany/MyModule/etc/adminhtml/di.xml file:

    <type name="Magento\Framework\Model\AbstractModel">
        <plugin name="MyCompany\MyModule\Plugin\MagentoFramework\Model\AbstractModel"
                type="MyCompany\MyModule\Plugin\MagentoFramework\Model\AbstractModel"
                disabled="false" sortOrder="1000"/>
    </type>
    
  3. Create the class MyCompany/MyModule\Plugin\MagentoFramework\Model\AbstractModel:

    <?php
    
    namespace MyCompany\MyModule\Plugin\MagentoFramework\Model;
    
    class AbstractModel
    {
        /**
         * @param \Magento\Framework\Model\AbstractModel $subject
         * @param mixed $result
         * @return \Magento\Framework\Model\AbstractModel
         */
        public function afterBeforeSave(
            \Magento\Framework\Model\AbstractModel $subject,
                                                   $result
        ) {
           $a=1;
    
            return $result;
        }
    
        /**
         * @param \Magento\Framework\Model\AbstractModel $subject
         * @return void
         */
        public function beforeAfterSave(
            \Magento\Framework\Model\AbstractModel $subject
        ) {
            $a=1;
        }
    
        public function beforeBeforeDelete(
            \Magento\Framework\Model\AbstractModel $subject
        ) {
            $a=1;
        }
    }
    
  4. Execute bin/magento setup:upgrade.

  5. Create/update a bundle product.

Expected result

The product is saved without errors.

Actual result

When saving the product, we get the error: Could not save child: "Unknown entity type: Magento\Bundle\Model\Selection\Interceptor requested"

Additional information

No response

Release note

No response

Triage and priority

  • [ ] Severity: S0 - Affects critical data or functionality and leaves users without workaround.
  • [ ] Severity: S1 - Affects critical data or functionality and forces users to employ a workaround.
  • [ ] Severity: S2 - Affects non-critical data or functionality and forces users to employ a workaround.
  • [ ] Severity: S3 - Affects non-critical data or functionality and does not force users to employ a workaround.
  • [ ] Severity: S4 - Affects aesthetics, professional look and feel, “quality” or “usability”.

faradey avatar Jun 20 '25 14:06 faradey

Hi @faradey. Thank you for your report. To speed up processing of this issue, make sure that the issue is reproducible on the vanilla Magento instance following Steps to reproduce.


Join Magento Community Engineering Slack and ask your questions in #github channel. :warning: According to the Magento Contribution requirements, all issues must go through the Community Contributions Triage process. Community Contributions Triage is a public meeting. :clock10: You can find the schedule on the Magento Community Calendar page. :telephone_receiver: The triage of issues happens in the queue order. If you want to speed up the delivery of your contribution, join the Community Contributions Triage session to discuss the appropriate ticket.

m2-assistant[bot] avatar Jun 20 '25 14:06 m2-assistant[bot]

Hi @engcom-Hotel. Thank you for working on this issue. In order to make sure that issue has enough information and ready for development, please read and check the following instruction: :point_down:

  • [ ] 1. Verify that issue has all the required information. (Preconditions, Steps to reproduce, Expected result, Actual result).
  • [ ] 2. Verify that issue has a meaningful description and provides enough information to reproduce the issue.
  • [ ] 3. Add Area: XXXXX label to the ticket, indicating the functional areas it may be related to.
  • [ ] 4. Verify that the issue is reproducible on 2.4-develop branch
    Details- If the issue is reproducible on 2.4-develop branch, please, add the label Reproduced on 2.4.x.
    - If the issue is not reproducible, add your comment that issue is not reproducible and close the issue and stop verification process here!
  • [ ] 5. Add label Issue: Confirmed once verification is complete.
  • [ ] 6. Make sure that automatic system confirms that report has been added to the backlog.

m2-assistant[bot] avatar Jun 24 '25 10:06 m2-assistant[bot]

Hello @faradey,

Thanks for the report and collaboration!

We have tried to reproduce the issue in the latest development branch i.e. 2.4-develop and the issue has been reproducible for us. But it seems to us an expected behaviour.

Official Documentation Links

Adobe Commerce DevDocs - Plugin Limitations: https://developer.adobe.com/commerce/php/development/components/plugins/#limitations

This page specifically states:

Plugins cannot be used on:

Final classes Final methods Non-public methods __construct Virtual types Objects that are instantiated before Magento\Framework\Interception is bootstrapped Plugins should not be used on:

Abstract classes Methods called from classes that are not a part of the interception system

Why Plugins Should Not Be Created for Abstract Classes

Plugins Attach to Specific Implementations:

Plugins in Magento 2 are designed to modify or extend the behavior of concrete class methods. Since abstract classes do not provide concrete method implementations (only declarations), plugins cannot operate on abstract methods.

Abstract methods must be implemented in a subclass, and it’s the subclass's implementation that gets executed, not the abstract class’s declaration.

Abstract Classes and Code Behavior:

An abstract class acts as a blueprint for concrete classes. Plugins cannot be reliably attached to them because there's no guarantee the abstract class's methods will be called in the same way by all implementations.

Let us know in case we missed anything.

Thanks

engcom-Hotel avatar Jun 24 '25 10:06 engcom-Hotel

Hi,

thank you for your explanation. I understand your recommendations. However, the issue isn't even with the AbstractModel class itself, but rather with how the class that saves the bundle product is specifically written. Other product types save correctly, and plugins work fine for them, but not for bundle products. This is something that needs attention.

By the way, I couldn't find any mention of abstract classes in the link you provided earlier.

And I don't see anything wrong with wanting to perform certain actions before or after saving any entity. I could have used a preference for the AbstractModel class, but a preference wouldn't work specifically for the adminhtml scope, as I require.

Ultimately, the problem exists, and it's clearly due to the bundle product saving process violating Magento's flow.

faradey avatar Jun 24 '25 13:06 faradey

Hello @faradey,

Thank you for your response!

As I can see in your main description you have tried to create a plugin on Magento\Framework\Model\AbstractModel. Based on the Magento 2 coding guidelines and plugin creation rules, we should NOT create a plugin on Magento\Framework\Model\AbstractModel.

Can you please explain the need to create plugin on that?

Thanks

engcom-Hotel avatar Jul 23 '25 13:07 engcom-Hotel

Thank you for your response.

The purpose of my plugin is to log all database interactions that originate only from the admin panel. This then allows me to determine whether or not an action can be rolled back.

Additionally, a super administrator will be able to see which actions were performed by which admin. Some of these actions might be reversible, while others may not be.

I considered how to implement this for the admin panel only and concluded that the only way is through a plugin on AbstractModel. If you know of a better approach, I would be happy to use it.

<?php
/**
 * Copyright © Vendor. All rights reserved.
 */

namespace Vendor\Module\Plugin\MagentoFramework\Model;

use Error;
use Exception;
use Vendor\Module\Helper\DataRegistry;
use Vendor\Module\Helper\ActionLog;
use Magento\Framework\App\Area;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\State;
use Psr\Log\LoggerInterface;

class AbstractModel
{
    public ScopeConfigInterface $config;
    public LoggerInterface $logger;
    public DataRegistry $registry;
    public ActionLog $actionLog;
    public RequestInterface $request;
    public State $state;

    /**
     * @param ScopeConfigInterface $config
     * @param LoggerInterface $logger
     * @param DataRegistry $registry
     * @param ActionLog $actionLog
     * @param RequestInterface $request
     * @param State $state
     */
    public function __construct(
        ScopeConfigInterface $config,
        LoggerInterface      $logger,
        DataRegistry         $registry,
        ActionLog            $actionLog,
        RequestInterface     $request,
        State                $state
    ) {
        $this->config = $config;
        $this->logger = $logger;
        $this->registry = $registry;
        $this->actionLog = $actionLog;
        $this->request = $request;
        $this->state = $state;
    }

    /**
     * @param \Magento\Framework\Model\AbstractModel $subject
     * @param $result
     * @return \Magento\Framework\Model\AbstractModel
     */
    public function afterBeforeSave(
        \Magento\Framework\Model\AbstractModel $subject,
        $result
    ) {
        try {
            if ((int)$this->config->getValue(ActionLog::XML_PATH_ENABLE) == 1
                && (int)$this->config->getValue(ActionLog::XML_PATH_ENABLE_REVERT) == 1
                && $this->state->getAreaCode() == Area::AREA_ADMINHTML
                && !empty($this->request->getParams())
                && !$this->actionLog->isUnusedModel($subject)
            ) {
                $registryData = $this->registry->getValue(ActionLog::REGISTRY_KEY_REVERT) ?? [];
                if (!isset($registryData[get_class($subject)])) {
                    $registryData[get_class($subject)] = [];
                }
                $registryData[get_class($subject)][] = [
                    'id' => $subject->getId(),
                    'action_type' => $subject->getId() == null ? 'create' : 'update',
                    'before' => $this->actionLog->getOrigData($subject),
                    'status' => false,
                ];
                $this->registry->unsValue(ActionLog::REGISTRY_KEY_REVERT);
                $this->registry->setValue(ActionLog::REGISTRY_KEY_REVERT, $registryData);
            }
        } catch (Exception|Error $e) {
            $this->logger->critical($e->getMessage());
        }

        return $result;
    }

    /**
     * @param \Magento\Framework\Model\AbstractModel $subject
     * @return void
     */
    public function beforeAfterSave(
        \Magento\Framework\Model\AbstractModel $subject
    ) {
        try {
            if ((int)$this->config->getValue(ActionLog::XML_PATH_ENABLE) == 1
                && (int)$this->config->getValue(ActionLog::XML_PATH_ENABLE_REVERT) == 1
                && $this->state->getAreaCode() == Area::AREA_ADMINHTML
                && !empty($this->request->getParams())
                && !$this->actionLog->isUnusedModel($subject)
                && $subject->getId()
            ) {
                $registryData = $this->registry->getValue(ActionLog::REGISTRY_KEY_REVERT) ?? [];
                if (!empty($registryData[get_class($subject)])) {
                    foreach ($registryData[get_class($subject)] as &$registryDatum) {
                        if ((!empty($registryDatum['id']) && $registryDatum['id'] == $subject->getId())
                            || ($subject->getId() && empty($registryDatum['id']) && $registryDatum['action_type'] == 'create')
                        ) {
                            $registryDatum['id'] = $subject->getId();
                            $registryDatum['status'] = true;
                            $registryDatum['scope'] = $this->actionLog->getScope();
                            $registryDatum['scope_id'] = $this->actionLog->getStoreId($subject);
                        }
                    }
                    $this->registry->unsValue(ActionLog::REGISTRY_KEY_REVERT);
                    $this->registry->setValue(ActionLog::REGISTRY_KEY_REVERT, $registryData);
                }
            }
        } catch (Exception|Error $e) {
            $this->logger->critical($e->getMessage());
        }
    }

    /**
     * @param \Magento\Framework\Model\AbstractModel $subject
     */
    public function beforeBeforeDelete(
        \Magento\Framework\Model\AbstractModel $subject
    ) {
        try {
            if ((int)$this->config->getValue(ActionLog::XML_PATH_ENABLE) == 1
                && (int)$this->config->getValue(ActionLog::XML_PATH_ENABLE_REVERT) == 1
                && $this->state->getAreaCode() == Area::AREA_ADMINHTML
                && !empty($this->request->getParams())
                && !$this->actionLog->isUnusedModel($subject)
            ) {
                $registryData = $this->registry->getValue(ActionLog::REGISTRY_KEY_REVERT) ?? [];
                if (!isset($registryData[get_class($subject)])) {
                    $registryData[get_class($subject)] = [];
                }
                $registryData[get_class($subject)][] = [
                    'id' => $subject->getId(),
                    'status' => true,
                    'action_type' => 'delete',
                    'scope' => $this->actionLog->getScope(),
                    'scope_id' => $this->actionLog->getStoreId($subject),
                    'before' => $this->actionLog->getOrigData($subject),
                ];
                $this->registry->unsValue(ActionLog::REGISTRY_KEY_REVERT);
                $this->registry->setValue(ActionLog::REGISTRY_KEY_REVERT, $registryData);
            }
        } catch (Exception|Error $e) {
            $this->logger->critical($e->getMessage());
        }
    }
}

faradey avatar Jul 31 '25 11:07 faradey

Thanks @faradey for your inputs on this.

But as per the Magento coding standard doesn’t allow us to create a plugin on Magento\Framework\Model\AbstractModel, so we should move to other strategies for the above mentioned scenario.

I suggest you to post the scenarion on Magento stackexchange for better inputs, as this platform is used to post the bugs/issues for Core Magento.

Hence we are closing this issue.

Thanks

engcom-Hotel avatar Aug 01 '25 09:08 engcom-Hotel

Hello, I believe the issue still persists.

I am providing the refactored code below. It continues to throw the following error: Could not save child: "Unknown entity type: Magento\Bundle\Model\Selection\Interceptor requested"

As you can see, this case no longer involves an abstract class.

Would it be possible for you to please take another look at the classes related to Bundle products? It appears there may be an underlying issue within them that conflicts with standard Magento development practices.

<type name="Magento\Framework\DataObject">
    <plugin name="VendorName\ModuleName\Plugin\MagentoFramework\Model\AbstractModel"
            type="VendorName\ModuleName\Plugin\MagentoFramework\Model\AbstractModel"
            disabled="false" sortOrder="1000"/>
</type>
<?php
/**
 * Copyright © VendorName. All rights reserved.
 */

namespace VendorName\ModuleName\Plugin\MagentoFramework\Model;

use Error;
use Exception;
use VendorName\ModuleName\Helper\DataRegistry;
use VendorName\ModuleName\Helper\ActionLog;
use Magento\Framework\App\Area;
use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\RequestInterface;
use Magento\Framework\App\State;
use Magento\Framework\DataObject;
use Psr\Log\LoggerInterface;

class AbstractModel
{
    public ScopeConfigInterface $config;
    public LoggerInterface $logger;
    public DataRegistry $registry;
    public ActionLog $actionLog;
    public RequestInterface $request;
    public State $state;

    public function __construct(
        ScopeConfigInterface $config,
        LoggerInterface      $logger,
        DataRegistry         $registry,
        ActionLog            $actionLog,
        RequestInterface     $request,
        State                $state
    ) {
        $this->config = $config;
        $this->logger = $logger;
        $this->registry = $registry;
        $this->actionLog = $actionLog;
        $this->request = $request;
        $this->state = $state;
    }

    /**
     * @param DataObject $subject
     * @param $result
     * @return DataObject
     */
    public function afterBeforeSave(
        DataObject $subject,
        $result
    ) {
        if (!($subject instanceof \Magento\Framework\Model\AbstractModel)) {
            return $result;
        }

        try {
            if ((int)$this->config->getValue(ActionLog::XML_PATH_ENABLE) == 1
                && (int)$this->config->getValue(ActionLog::XML_PATH_ENABLE_REVERT) == 1
                && $this->state->getAreaCode() == Area::AREA_ADMINHTML
                && !empty($this->request->getParams())
                && !$this->actionLog->isUnusedModel($subject)
            ) {
                $registryData = $this->registry->getValue(ActionLog::REGISTRY_KEY_REVERT) ?? [];
                if (!isset($registryData[get_class($subject)])) {
                    $registryData[get_class($subject)] = [];
                }
                $registryData[get_class($subject)][] = [
                    'id' => $subject->getId(),
                    'action_type' => $subject->getId() == null ? 'create' : 'update',
                    'before' => $this->actionLog->getOrigData($subject),
                    'status' => false,
                ];
                $this->registry->unsValue(ActionLog::REGISTRY_KEY_REVERT);
                $this->registry->setValue(ActionLog::REGISTRY_KEY_REVERT, $registryData);
            }
        } catch (Exception|Error $e) {
            $this->logger->critical($e->getMessage());
        }

        return $result;
    }

    /**
     * @param DataObject $subject
     * @return void
     */
    public function beforeAfterSave(
        DataObject $subject
    ) {
        if (!($subject instanceof \Magento\Framework\Model\AbstractModel)) {
            return;
        }

        try {
            if ((int)$this->config->getValue(ActionLog::XML_PATH_ENABLE) == 1
                && (int)$this->config->getValue(ActionLog::XML_PATH_ENABLE_REVERT) == 1
                && $this->state->getAreaCode() == Area::AREA_ADMINHTML
                && !empty($this->request->getParams())
                && !$this->actionLog->isUnusedModel($subject)
                && $subject->getId()
            ) {
                $registryData = $this->registry->getValue(ActionLog::REGISTRY_KEY_REVERT) ?? [];
                if (!empty($registryData[get_class($subject)])) {
                    foreach ($registryData[get_class($subject)] as &$registryDatum) {
                        if ((!empty($registryDatum['id']) && $registryDatum['id'] == $subject->getId())
                            || ($subject->getId() && empty($registryDatum['id']) && $registryDatum['action_type'] == 'create')
                        ) {
                            $registryDatum['id'] = $subject->getId();
                            $registryDatum['status'] = true;
                            $registryDatum['scope'] = $this->actionLog->getScope();
                            $registryDatum['scope_id'] = $this->actionLog->getStoreId($subject);
                        }
                    }
                    $this->registry->unsValue(ActionLog::REGISTRY_KEY_REVERT);
                    $this->registry->setValue(ActionLog::REGISTRY_KEY_REVERT, $registryData);
                }
            }
        } catch (Exception|Error $e) {
            $this->logger->critical($e->getMessage());
        }
    }

    public function beforeBeforeDelete(
        DataObject $subject
    ) {
        if (!($subject instanceof \Magento\Framework\Model\AbstractModel)) {
            return;
        }

        try {
            if ((int)$this->config->getValue(ActionLog::XML_PATH_ENABLE) == 1
                && (int)$this->config->getValue(ActionLog::XML_PATH_ENABLE_REVERT) == 1
                && $this->state->getAreaCode() == Area::AREA_ADMINHTML
                && !empty($this->request->getParams())
                && !$this->actionLog->isUnusedModel($subject)
            ) {
                $registryData = $this->registry->getValue(ActionLog::REGISTRY_KEY_REVERT) ?? [];
                if (!isset($registryData[get_class($subject)])) {
                    $registryData[get_class($subject)] = [];
                }
                $registryData[get_class($subject)][] = [
                    'id' => $subject->getId(),
                    'status' => true,
                    'action_type' => 'delete',
                    'scope' => $this->actionLog->getScope(),
                    'scope_id' => $this->actionLog->getStoreId($subject),
                    'before' => $this->actionLog->getOrigData($subject),
                ];
                $this->registry->unsValue(ActionLog::REGISTRY_KEY_REVERT);
                $this->registry->setValue(ActionLog::REGISTRY_KEY_REVERT, $registryData);
            }
        } catch (Exception|Error $e) {
            $this->logger->critical($e->getMessage());
        }
    }
}

faradey avatar Aug 04 '25 13:08 faradey