diff --git a/vendor/magento/module-customer/Model/ResourceModel/Address.php b/vendor/magento/module-customer/Model/ResourceModel/Address.php index 8e44638e7aee8..9f27ba8ca231b 100644 --- a/vendor/magento/module-customer/Model/ResourceModel/Address.php +++ b/vendor/magento/module-customer/Model/ResourceModel/Address.php @@ -5,17 +5,18 @@ * Copyright © Magento, Inc. All rights reserved. * See COPYING.txt for license details. */ + namespace Magento\Customer\Model\ResourceModel; -use Magento\Customer\Controller\Adminhtml\Group\Delete; use Magento\Customer\Model\CustomerRegistry; use Magento\Customer\Model\ResourceModel\Address\DeleteRelation; +use Magento\Eav\Model\ResourceModel\OrphanedMultiselectCleaner; use Magento\Framework\App\ObjectManager; +use Magento\Framework\DataObject; /** - * Class Address + * Customer address entity resource model * - * @package Magento\Customer\Model\ResourceModel * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity @@ -30,6 +31,21 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity */ protected $customerRepository; + /** + * @var OrphanedMultiselectCleaner + */ + private $orphanedMultiselectCleaner; + + /** + * @var DeleteRelation + */ + private $deleteRelation; + + /** + * @var CustomerRegistry + */ + private $customerRegistry; + /** * @param \Magento\Eav\Model\Entity\Context $context * @param \Magento\Framework\Model\ResourceModel\Db\VersionControl\Snapshot $entitySnapshot @@ -37,6 +53,9 @@ class Address extends \Magento\Eav\Model\Entity\VersionControl\AbstractEntity * @param \Magento\Framework\Validator\Factory $validatorFactory * @param \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository * @param array $data + * @param OrphanedMultiselectCleaner|null $orphanedMultiselectCleaner + * @param DeleteRelation|null $deleteRelation + * @param CustomerRegistry|null $customerRegistry */ public function __construct( \Magento\Eav\Model\Entity\Context $context, @@ -44,10 +63,19 @@ public function __construct( \Magento\Framework\Model\ResourceModel\Db\VersionControl\RelationComposite $entityRelationComposite, \Magento\Framework\Validator\Factory $validatorFactory, \Magento\Customer\Api\CustomerRepositoryInterface $customerRepository, - $data = [] + $data = [], + ?OrphanedMultiselectCleaner $orphanedMultiselectCleaner = null, + ?DeleteRelation $deleteRelation = null, + ?CustomerRegistry $customerRegistry = null ) { $this->customerRepository = $customerRepository; $this->_validatorFactory = $validatorFactory; + $this->orphanedMultiselectCleaner = $orphanedMultiselectCleaner + ?? ObjectManager::getInstance()->get(OrphanedMultiselectCleaner::class); + $this->deleteRelation = $deleteRelation + ?? ObjectManager::getInstance()->get(DeleteRelation::class); + $this->customerRegistry = $customerRegistry + ?? ObjectManager::getInstance()->get(CustomerRegistry::class); parent::__construct($context, $entitySnapshot, $entityRelationComposite, $data); } @@ -78,11 +106,15 @@ public function getEntityType() /** * Check customer address before saving * - * @param \Magento\Framework\DataObject $address + * @param DataObject $address * @return $this */ - protected function _beforeSave(\Magento\Framework\DataObject $address) + protected function _beforeSave(DataObject $address) { + if ($address->getId()) { + $this->cleanOrphanedMultiselectValues($address); + } + parent::_beforeSave($address); $this->_validate($address); @@ -93,7 +125,7 @@ protected function _beforeSave(\Magento\Framework\DataObject $address) /** * Validate customer address entity * - * @param \Magento\Framework\DataObject $address + * @param DataObject $address * @return void * @throws \Magento\Framework\Validator\Exception When validation failed */ @@ -124,25 +156,14 @@ public function delete($object) } /** - * Get instance of DeleteRelation class - * - * @deprecated 101.0.0 - * @return DeleteRelation - */ - private function getDeleteRelation() - { - return ObjectManager::getInstance()->get(DeleteRelation::class); - } - - /** - * Get instance of CustomerRegistry class + * Clean up orphaned multiselect attribute values before validation * - * @deprecated 101.0.0 - * @return CustomerRegistry + * @param DataObject $address + * @return void */ - private function getCustomerRegistry() + private function cleanOrphanedMultiselectValues(DataObject $address): void { - return ObjectManager::getInstance()->get(CustomerRegistry::class); + $this->orphanedMultiselectCleaner->cleanEntity($this, $address); } /** @@ -151,11 +172,10 @@ private function getCustomerRegistry() * @param \Magento\Customer\Model\Address $address * @return $this */ - protected function _afterDelete(\Magento\Framework\DataObject $address) + protected function _afterDelete(DataObject $address) { - $customer = $this->getCustomerRegistry()->retrieve($address->getCustomerId()); - - $this->getDeleteRelation()->deleteRelation($address, $customer); + $customer = $this->customerRegistry->retrieve($address->getCustomerId()); + $this->deleteRelation->deleteRelation($address, $customer); return parent::_afterDelete($address); } } diff --git a/vendor/magento/module-customer/Model/ResourceModel/Customer.php b/vendor/magento/module-customer/Model/ResourceModel/Customer.php index 0ed677c60c7ab..720d53a372c5f 100644 --- a/vendor/magento/module-customer/Model/ResourceModel/Customer.php +++ b/vendor/magento/module-customer/Model/ResourceModel/Customer.php @@ -12,6 +12,7 @@ use Magento\Customer\Model\Customer\NotificationStorage; use Magento\Eav\Model\Entity\Context; use Magento\Eav\Model\Entity\VersionControl\AbstractEntity; +use Magento\Eav\Model\ResourceModel\OrphanedMultiselectCleaner; use Magento\Framework\App\Config\ScopeConfigInterface; use Magento\Framework\App\ObjectManager; use Magento\Framework\DataObject; @@ -73,6 +74,11 @@ class Customer extends AbstractEntity */ private $encryptor; + /** + * @var OrphanedMultiselectCleaner + */ + private $orphanedMultiselectCleaner; + /** * Customer constructor. * @@ -86,6 +92,7 @@ class Customer extends AbstractEntity * @param array $data * @param AccountConfirmation|null $accountConfirmation * @param EncryptorInterface|null $encryptor + * @param OrphanedMultiselectCleaner|null $orphanedMultiselectCleaner * @SuppressWarnings(PHPMD.ExcessiveParameterList) */ public function __construct( @@ -98,7 +105,8 @@ public function __construct( StoreManagerInterface $storeManager, $data = [], AccountConfirmation $accountConfirmation = null, - EncryptorInterface $encryptor = null + ?EncryptorInterface $encryptor = null, + ?OrphanedMultiselectCleaner $orphanedMultiselectCleaner = null ) { parent::__construct($context, $entitySnapshot, $entityRelationComposite, $data); @@ -107,6 +115,8 @@ public function __construct( $this->dateTime = $dateTime; $this->accountConfirmation = $accountConfirmation ?: ObjectManager::getInstance() ->get(AccountConfirmation::class); + $this->orphanedMultiselectCleaner = $orphanedMultiselectCleaner + ?? ObjectManager::getInstance()->get(OrphanedMultiselectCleaner::class); $this->setType('customer'); $this->setConnection('customer_read'); $this->storeManager = $storeManager; @@ -151,6 +161,10 @@ protected function _beforeSave(DataObject $customer) } $customer->getGroupId(); + if ($customer->getId()) { + $this->cleanOrphanedMultiselectValues($customer); + } + parent::_beforeSave($customer); if (!$customer->getEmail()) { @@ -519,6 +533,17 @@ public function updateSessionCutOff(int $customerId, int $timestamp): void ); } + /** + * Clean up orphaned multiselect attribute values before validation + * + * @param DataObject $customer + * @return void + */ + private function cleanOrphanedMultiselectValues(DataObject $customer): void + { + $this->orphanedMultiselectCleaner->cleanEntity($this, $customer); + } + /** * @inheritDoc */ diff --git a/vendor/magento/module-eav/Model/Attribute/Data/Multiselect.php b/vendor/magento/module-eav/Model/Attribute/Data/Multiselect.php index b926ed4d58656..0950581e93169 100644 --- a/vendor/magento/module-eav/Model/Attribute/Data/Multiselect.php +++ b/vendor/magento/module-eav/Model/Attribute/Data/Multiselect.php @@ -99,7 +99,12 @@ public function validateValue($value) } if (!empty($value) && $attribute->getSourceModel()) { - $values = is_array($value) ? $value : explode(',', (string) $value); + if (is_array($value)) { + $values = $value; + } else { + $values = preg_split('/[,\n\r]+/', (string) $value); + } + $values = array_map('trim', $values); $errors = array_merge( $errors, $this->validateBySource($values) diff --git a/vendor/magento/module-eav/Model/ResourceModel/OrphanedMultiselectCleaner.php b/vendor/magento/module-eav/Model/ResourceModel/OrphanedMultiselectCleaner.php new file mode 100644 index 0000000000000..1f1c5163f0fa7 --- /dev/null +++ b/vendor/magento/module-eav/Model/ResourceModel/OrphanedMultiselectCleaner.php @@ -0,0 +1,110 @@ +loadAllAttributes($entity); + $entityData = $entity->getData(); + + foreach ($entityData as $attributeCode => $value) { + $this->cleanAttributeValue($resource, $entity, $attributeCode, $value); + } + } + + /** + * Clean a single multiselect attribute value + * + * @param AbstractEntity $resource + * @param DataObject $entity + * @param string $attributeCode + * @param mixed $value + * @return void + */ + private function cleanAttributeValue( + AbstractEntity $resource, + DataObject $entity, + string $attributeCode, + mixed $value + ): void { + if (empty($value) && $value !== '0') { + return; + } + + if ($entity->dataHasChangedFor($attributeCode)) { + return; + } + + try { + $attribute = $resource->getAttribute($attributeCode); + if (!$this->isMultiselectAttribute($attribute)) { + return; + } + + $values = is_array($value) ? $value : explode(',', (string) $value); + $validValues = $this->filterValidOptionValues($attribute, $values); + + if (count($validValues) !== count($values)) { + $entity->unsetData($attributeCode); + $entity->setData($attributeCode, implode(',', $validValues) ?: null); + } + } catch (\Exception $e) { + return; + } + } + + /** + * Check if attribute is a multiselect with source model + * + * @param mixed $attribute + * @return bool + */ + private function isMultiselectAttribute($attribute): bool + { + return $attribute + && $attribute->getFrontendInput() === 'multiselect' + && $attribute->usesSource(); + } + + /** + * Filter valid option values from array + * + * @param AbstractAttribute $attribute + * @param array $values + * @return array + * @throws LocalizedException + */ + private function filterValidOptionValues($attribute, array $values): array + { + $validValues = []; + foreach ($values as $optionId) { + $optionId = trim((string) $optionId); + if ($optionId !== '' && $attribute->getSource()->getOptionText($optionId) !== false) { + $validValues[] = $optionId; + } + } + return $validValues; + } +}