magento2
magento2 copied to clipboard
Could not save child: "Unknown entity type: Magento\Bundle\Model\Selection\Interceptor requested"
Preconditions and environment
- Magento version 2.4.6, 2.4.7, 2.4.8
Steps to reproduce
-
Create a module
MyCompany/MyModule. -
Add a plugin in the
app/code/MyCompany/MyModule/etc/adminhtml/di.xmlfile:<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> -
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; } } -
Execute
bin/magento setup:upgrade. -
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”.
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.
- For more details, review the Magento Contributor Assistant documentation.
- Add a comment to assign the issue:
@magento I am working on this - To learn more about issue processing workflow, refer to the Code Contributions.
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.
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: XXXXXlabel to the ticket, indicating the functional areas it may be related to. - [ ] 4. Verify that the issue is reproducible on
2.4-developbranchDetails
- If the issue is reproducible on2.4-developbranch, please, add the labelReproduced 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: Confirmedonce verification is complete. - [ ] 6. Make sure that automatic system confirms that report has been added to the backlog.
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
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.
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
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());
}
}
}
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
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());
}
}
}