EavAttribute.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Swatches\Model\Plugin;
  7. use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
  8. use Magento\Framework\App\ObjectManager;
  9. use Magento\Framework\Exception\InputException;
  10. use Magento\Framework\Serialize\Serializer\Json;
  11. use Magento\Swatches\Model\ResourceModel\Swatch as SwatchResource;
  12. use Magento\Swatches\Model\Swatch;
  13. /**
  14. * Plugin model for Catalog Resource Attribute
  15. */
  16. class EavAttribute
  17. {
  18. const DEFAULT_STORE_ID = 0;
  19. /**
  20. * @var SwatchResource
  21. */
  22. private $swatchResource;
  23. /**
  24. * Base option title used for string operations to detect is option already exists or new
  25. */
  26. const BASE_OPTION_TITLE = 'option';
  27. /**
  28. * @var \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory
  29. */
  30. protected $swatchCollectionFactory;
  31. /**
  32. * @var \Magento\Swatches\Model\SwatchFactory
  33. */
  34. protected $swatchFactory;
  35. /**
  36. * @var \Magento\Swatches\Helper\Data
  37. */
  38. protected $swatchHelper;
  39. /**
  40. * Array which contains links for new created attributes for swatches
  41. *
  42. * @var array
  43. */
  44. protected $dependencyArray = [];
  45. /**
  46. * Swatch existing status
  47. *
  48. * @var bool
  49. */
  50. protected $isSwatchExists;
  51. /**
  52. * Serializer from arrays to string.
  53. *
  54. * @var Json
  55. */
  56. private $serializer;
  57. /**
  58. * @param \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory $collectionFactory
  59. * @param \Magento\Swatches\Model\SwatchFactory $swatchFactory
  60. * @param \Magento\Swatches\Helper\Data $swatchHelper
  61. * @param Json|null $serializer
  62. * @param SwatchResource|null $swatchResource
  63. */
  64. public function __construct(
  65. \Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory $collectionFactory,
  66. \Magento\Swatches\Model\SwatchFactory $swatchFactory,
  67. \Magento\Swatches\Helper\Data $swatchHelper,
  68. Json $serializer = null,
  69. SwatchResource $swatchResource = null
  70. ) {
  71. $this->swatchCollectionFactory = $collectionFactory;
  72. $this->swatchFactory = $swatchFactory;
  73. $this->swatchHelper = $swatchHelper;
  74. $this->serializer = $serializer ?: ObjectManager::getInstance()->create(Json::class);
  75. $this->swatchResource = $swatchResource ?: ObjectManager::getInstance()->create(SwatchResource::class);
  76. }
  77. /**
  78. * Set base data to Attribute
  79. *
  80. * @param Attribute $attribute
  81. * @return void
  82. */
  83. public function beforeBeforeSave(Attribute $attribute)
  84. {
  85. if ($this->swatchHelper->isSwatchAttribute($attribute)) {
  86. $this->setProperOptionsArray($attribute);
  87. $this->validateOptions($attribute);
  88. $this->swatchHelper->assembleAdditionalDataEavAttribute($attribute);
  89. }
  90. $this->convertSwatchToDropdown($attribute);
  91. }
  92. /**
  93. * Swatch save operations
  94. *
  95. * @param Attribute $attribute
  96. * @throws \Magento\Framework\Exception\LocalizedException
  97. * @return void
  98. */
  99. public function afterAfterSave(Attribute $attribute)
  100. {
  101. if ($this->swatchHelper->isSwatchAttribute($attribute)) {
  102. $this->processSwatchOptions($attribute);
  103. $this->saveDefaultSwatchOptionValue($attribute);
  104. $this->saveSwatchParams($attribute);
  105. }
  106. }
  107. /**
  108. * Substitute suitable options and swatches arrays
  109. *
  110. * @param Attribute $attribute
  111. * @return void
  112. */
  113. protected function setProperOptionsArray(Attribute $attribute)
  114. {
  115. $canReplace = false;
  116. if ($this->swatchHelper->isVisualSwatch($attribute)) {
  117. $canReplace = true;
  118. $defaultValue = $attribute->getData('defaultvisual');
  119. $optionsArray = $attribute->getData('optionvisual');
  120. $swatchesArray = $attribute->getData('swatchvisual');
  121. } elseif ($this->swatchHelper->isTextSwatch($attribute)) {
  122. $canReplace = true;
  123. $defaultValue = $attribute->getData('defaulttext');
  124. $optionsArray = $attribute->getData('optiontext');
  125. $swatchesArray = $attribute->getData('swatchtext');
  126. }
  127. if ($canReplace == true) {
  128. if (!empty($optionsArray)) {
  129. $attribute->setData('option', $optionsArray);
  130. }
  131. if (!empty($defaultValue)) {
  132. $attribute->setData('default', $defaultValue);
  133. } else {
  134. $attribute->setData('default', [0 => $attribute->getDefaultValue()]);
  135. }
  136. if (!empty($swatchesArray)) {
  137. $attribute->setData('swatch', $swatchesArray);
  138. }
  139. }
  140. }
  141. /**
  142. * Prepare attribute for conversion from any swatch type to dropdown
  143. *
  144. * @param Attribute $attribute
  145. * @throws \Magento\Framework\Exception\LocalizedException
  146. * @return void
  147. */
  148. protected function convertSwatchToDropdown(Attribute $attribute)
  149. {
  150. if ($attribute->getData(Swatch::SWATCH_INPUT_TYPE_KEY) == Swatch::SWATCH_INPUT_TYPE_DROPDOWN) {
  151. $additionalData = $attribute->getData('additional_data');
  152. if (!empty($additionalData)) {
  153. $additionalData = $this->serializer->unserialize($additionalData);
  154. if (is_array($additionalData) && isset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY])) {
  155. $option = $attribute->getOption() ?: [];
  156. $this->cleanEavAttributeOptionSwatchValues($option);
  157. unset($additionalData[Swatch::SWATCH_INPUT_TYPE_KEY]);
  158. $attribute->setData('additional_data', $this->serializer->serialize($additionalData));
  159. }
  160. }
  161. }
  162. }
  163. /**
  164. * Creates array which link new option ids
  165. *
  166. * @param Attribute $attribute
  167. * @return Attribute
  168. * @throws \Magento\Framework\Exception\LocalizedException
  169. */
  170. protected function processSwatchOptions(Attribute $attribute)
  171. {
  172. $optionsArray = $attribute->getData('option');
  173. if (!empty($optionsArray) && is_array($optionsArray)) {
  174. $optionsArray = $this->prepareOptionIds($optionsArray);
  175. $attributeSavedOptions = $attribute->getSource()->getAllOptions();
  176. $this->prepareOptionLinks($optionsArray, $attributeSavedOptions);
  177. }
  178. return $attribute;
  179. }
  180. /**
  181. * Get options array without deleted items
  182. *
  183. * @param array $optionsArray
  184. * @return array
  185. */
  186. protected function prepareOptionIds(array $optionsArray)
  187. {
  188. if (isset($optionsArray['value']) && is_array($optionsArray['value'])) {
  189. foreach (array_keys($optionsArray['value']) as $optionId) {
  190. if (isset($optionsArray['delete']) && isset($optionsArray['delete'][$optionId])
  191. && $optionsArray['delete'][$optionId] == 1
  192. ) {
  193. unset($optionsArray['value'][$optionId]);
  194. }
  195. }
  196. }
  197. return $optionsArray;
  198. }
  199. /**
  200. * Create links for non existed swatch options
  201. *
  202. * @param array $optionsArray
  203. * @param array $attributeSavedOptions
  204. * @return void
  205. */
  206. protected function prepareOptionLinks(array $optionsArray, array $attributeSavedOptions)
  207. {
  208. $dependencyArray = [];
  209. if (is_array($optionsArray['value'])) {
  210. $optionCounter = 1;
  211. foreach (array_keys($optionsArray['value']) as $baseOptionId) {
  212. $dependencyArray[$baseOptionId] = $attributeSavedOptions[$optionCounter]['value'];
  213. $optionCounter++;
  214. }
  215. }
  216. $this->dependencyArray = $dependencyArray;
  217. }
  218. /**
  219. * Save all Swatches data
  220. *
  221. * @param Attribute $attribute
  222. * @return void
  223. */
  224. protected function saveSwatchParams(Attribute $attribute)
  225. {
  226. if ($this->swatchHelper->isVisualSwatch($attribute)) {
  227. $this->processVisualSwatch($attribute);
  228. $attributeOptions = $attribute->getOptiontext() ?: [];
  229. $this->cleanTextSwatchValuesAfterSwitch($attributeOptions);
  230. } elseif ($this->swatchHelper->isTextSwatch($attribute)) {
  231. $this->processTextualSwatch($attribute);
  232. }
  233. }
  234. /**
  235. * Save Visual Swatch data
  236. *
  237. * @param Attribute $attribute
  238. * @return void
  239. */
  240. protected function processVisualSwatch(Attribute $attribute)
  241. {
  242. $swatchArray = $attribute->getData('swatch/value');
  243. if (isset($swatchArray) && is_array($swatchArray)) {
  244. foreach ($swatchArray as $optionId => $value) {
  245. $optionId = $this->getAttributeOptionId($optionId);
  246. $isOptionForDelete = $this->isOptionForDelete($attribute, $optionId);
  247. if ($optionId === null || $isOptionForDelete) {
  248. //option was deleted by button with basket
  249. continue;
  250. }
  251. $swatch = $this->loadSwatchIfExists($optionId, self::DEFAULT_STORE_ID);
  252. $swatchType = $this->determineSwatchType($value);
  253. $this->saveSwatchData($swatch, $optionId, self::DEFAULT_STORE_ID, $swatchType, $value);
  254. $this->isSwatchExists = null;
  255. }
  256. }
  257. }
  258. /**
  259. * Clean swatch option values after switching to the dropdown type.
  260. *
  261. * @param array $attributeOptions
  262. * @param int|null $swatchType
  263. * @throws \Magento\Framework\Exception\LocalizedException
  264. */
  265. private function cleanEavAttributeOptionSwatchValues(array $attributeOptions, int $swatchType = null)
  266. {
  267. if (count($attributeOptions) && isset($attributeOptions['value'])) {
  268. $optionsIDs = array_keys($attributeOptions['value']);
  269. $this->swatchResource->clearSwatchOptionByOptionIdAndType($optionsIDs, $swatchType);
  270. }
  271. }
  272. /**
  273. * Cleaning the text type of swatch option values after switching.
  274. *
  275. * @param array $attributeOptions
  276. * @throws \Magento\Framework\Exception\LocalizedException
  277. */
  278. private function cleanTextSwatchValuesAfterSwitch(array $attributeOptions)
  279. {
  280. $this->cleanEavAttributeOptionSwatchValues($attributeOptions, Swatch::SWATCH_TYPE_TEXTUAL);
  281. }
  282. /**
  283. * @param string $value
  284. * @return int
  285. */
  286. private function determineSwatchType($value)
  287. {
  288. $swatchType = Swatch::SWATCH_TYPE_EMPTY;
  289. if (!empty($value) && $value[0] == '#') {
  290. $swatchType = Swatch::SWATCH_TYPE_VISUAL_COLOR;
  291. } elseif (!empty($value) && $value[0] == '/') {
  292. $swatchType = Swatch::SWATCH_TYPE_VISUAL_IMAGE;
  293. }
  294. return $swatchType;
  295. }
  296. /**
  297. * Save Textual Swatch data
  298. *
  299. * @param Attribute $attribute
  300. * @return void
  301. */
  302. protected function processTextualSwatch(Attribute $attribute)
  303. {
  304. $swatchArray = $attribute->getData('swatch/value');
  305. if (isset($swatchArray) && is_array($swatchArray)) {
  306. foreach ($swatchArray as $optionId => $storeValues) {
  307. $optionId = $this->getAttributeOptionId($optionId);
  308. $isOptionForDelete = $this->isOptionForDelete($attribute, $optionId);
  309. if ($optionId === null || $isOptionForDelete) {
  310. //option was deleted by button with basket
  311. continue;
  312. }
  313. $defaultSwatchValue = reset($storeValues);
  314. foreach ($storeValues as $storeId => $value) {
  315. if (!$value) {
  316. $value = $defaultSwatchValue;
  317. }
  318. $swatch = $this->loadSwatchIfExists($optionId, $storeId);
  319. $swatch->isDeleted($isOptionForDelete);
  320. $this->saveSwatchData(
  321. $swatch,
  322. $optionId,
  323. $storeId,
  324. \Magento\Swatches\Model\Swatch::SWATCH_TYPE_TEXTUAL,
  325. $value
  326. );
  327. $this->isSwatchExists = null;
  328. }
  329. }
  330. }
  331. }
  332. /**
  333. * Get option id. If it not exist get it from dependency link array
  334. *
  335. * @param integer $optionId
  336. * @return int
  337. */
  338. protected function getAttributeOptionId($optionId)
  339. {
  340. if (substr($optionId, 0, 6) == self::BASE_OPTION_TITLE) {
  341. $optionId = isset($this->dependencyArray[$optionId]) ? $this->dependencyArray[$optionId] : null;
  342. }
  343. return $optionId;
  344. }
  345. /**
  346. * Check if is option for delete
  347. *
  348. * @param Attribute $attribute
  349. * @param integer $optionId
  350. * @return bool
  351. */
  352. protected function isOptionForDelete(Attribute $attribute, $optionId)
  353. {
  354. $isOptionForDelete = $attribute->getData('option/delete/' . $optionId);
  355. return isset($isOptionForDelete) && $isOptionForDelete;
  356. }
  357. /**
  358. * Load swatch if it exists in database
  359. *
  360. * @param int $optionId
  361. * @param int $storeId
  362. * @return Swatch
  363. */
  364. protected function loadSwatchIfExists($optionId, $storeId)
  365. {
  366. $collection = $this->swatchCollectionFactory->create();
  367. $collection->addFieldToFilter('option_id', $optionId);
  368. $collection->addFieldToFilter('store_id', $storeId);
  369. $collection->setPageSize(1);
  370. $loadedSwatch = $collection->getFirstItem();
  371. if ($loadedSwatch->getId()) {
  372. $this->isSwatchExists = true;
  373. }
  374. return $loadedSwatch;
  375. }
  376. /**
  377. * Save operation
  378. *
  379. * @param Swatch $swatch
  380. * @param integer $optionId
  381. * @param integer $storeId
  382. * @param integer $type
  383. * @param string $value
  384. * @return void
  385. */
  386. protected function saveSwatchData($swatch, $optionId, $storeId, $type, $value)
  387. {
  388. if ($this->isSwatchExists) {
  389. $swatch->setData('type', $type);
  390. $swatch->setData('value', $value);
  391. } else {
  392. $swatch->setData('option_id', $optionId);
  393. $swatch->setData('store_id', $storeId);
  394. $swatch->setData('type', $type);
  395. $swatch->setData('value', $value);
  396. }
  397. $swatch->save();
  398. }
  399. /**
  400. * Save default swatch value using Swatch model instead of Eav model
  401. *
  402. * @param Attribute $attribute
  403. * @return void
  404. */
  405. protected function saveDefaultSwatchOptionValue(Attribute $attribute)
  406. {
  407. if (!$this->swatchHelper->isSwatchAttribute($attribute)) {
  408. return;
  409. }
  410. $defaultValue = $attribute->getData('default/0');
  411. if (!empty($defaultValue)) {
  412. /** @var \Magento\Swatches\Model\Swatch $swatch */
  413. $swatch = $this->swatchFactory->create();
  414. // created and removed on frontend option not exists in dependency array
  415. if (substr($defaultValue, 0, 6) == self::BASE_OPTION_TITLE &&
  416. isset($this->dependencyArray[$defaultValue])
  417. ) {
  418. $defaultValue = $this->dependencyArray[$defaultValue];
  419. }
  420. $swatch->getResource()->saveDefaultSwatchOption($attribute->getId(), $defaultValue);
  421. }
  422. }
  423. /**
  424. * Validate that attribute options exist
  425. *
  426. * @param Attribute $attribute
  427. * @return bool
  428. * @throws InputException
  429. */
  430. protected function validateOptions(Attribute $attribute)
  431. {
  432. $options = null;
  433. if ($this->swatchHelper->isVisualSwatch($attribute)) {
  434. $options = $attribute->getData('optionvisual');
  435. } elseif ($this->swatchHelper->isTextSwatch($attribute)) {
  436. $options = $attribute->getData('optiontext');
  437. }
  438. if ($options && !$this->isOptionsValid($options, $attribute)) {
  439. throw new InputException(__('Admin is a required field in each row'));
  440. }
  441. return true;
  442. }
  443. /**
  444. * Check if attribute options are valid
  445. *
  446. * @param array $options
  447. * @param Attribute $attribute
  448. * @return bool
  449. */
  450. protected function isOptionsValid(array $options, Attribute $attribute)
  451. {
  452. if (!isset($options['value'])) {
  453. return false;
  454. }
  455. foreach ($options['value'] as $optionId => $option) {
  456. // do not validate options marked as deleted
  457. if ($this->isOptionForDelete($attribute, $optionId)) {
  458. continue;
  459. }
  460. if (!isset($option[0]) || $option[0] === '') {
  461. return false;
  462. }
  463. }
  464. return true;
  465. }
  466. /**
  467. * @param Attribute $attribute
  468. * @param bool $result
  469. * @return bool
  470. */
  471. public function afterUsesSource(Attribute $attribute, $result)
  472. {
  473. if ($this->swatchHelper->isSwatchAttribute($attribute)) {
  474. return true;
  475. }
  476. return $result;
  477. }
  478. }