Data.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Swatches\Helper;
  7. use Magento\Catalog\Api\Data\ProductAttributeMediaGalleryEntryInterface;
  8. use Magento\Catalog\Api\Data\ProductInterface as Product;
  9. use Magento\Catalog\Api\ProductRepositoryInterface;
  10. use Magento\Catalog\Model\Product as ModelProduct;
  11. use Magento\Catalog\Model\Product\Image\UrlBuilder;
  12. use Magento\Catalog\Model\ResourceModel\Eav\Attribute;
  13. use Magento\Catalog\Model\ResourceModel\Product\Collection as ProductCollection;
  14. use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory;
  15. use Magento\ConfigurableProduct\Model\Product\Type\Configurable;
  16. use Magento\Framework\App\ObjectManager;
  17. use Magento\Framework\Serialize\Serializer\Json;
  18. use Magento\Store\Model\StoreManagerInterface;
  19. use Magento\Swatches\Model\ResourceModel\Swatch\CollectionFactory as SwatchCollectionFactory;
  20. use Magento\Swatches\Model\Swatch;
  21. use Magento\Swatches\Model\SwatchAttributesProvider;
  22. use Magento\Swatches\Model\SwatchAttributeType;
  23. /**
  24. * Class Helper Data
  25. *
  26. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  27. */
  28. class Data
  29. {
  30. /**
  31. * When we init media gallery empty image types contain this value.
  32. */
  33. const EMPTY_IMAGE_VALUE = 'no_selection';
  34. /**
  35. * Default store ID
  36. */
  37. const DEFAULT_STORE_ID = 0;
  38. /**
  39. * @var CollectionFactory
  40. */
  41. protected $productCollectionFactory;
  42. /**
  43. * @var ProductRepositoryInterface
  44. */
  45. protected $productRepository;
  46. /**
  47. * @var StoreManagerInterface
  48. */
  49. protected $storeManager;
  50. /**
  51. * @var SwatchCollectionFactory
  52. */
  53. protected $swatchCollectionFactory;
  54. /**
  55. * Product metadata pool
  56. *
  57. * @var \Magento\Framework\EntityManager\MetadataPool
  58. */
  59. private $metadataPool;
  60. /**
  61. * @var SwatchAttributesProvider
  62. */
  63. private $swatchAttributesProvider;
  64. /**
  65. * Data key which should populated to Attribute entity from "additional_data" field
  66. *
  67. * @var array
  68. */
  69. protected $eavAttributeAdditionalDataKeys = [
  70. Swatch::SWATCH_INPUT_TYPE_KEY,
  71. 'update_product_preview_image',
  72. 'use_product_image_for_swatch'
  73. ];
  74. /**
  75. * Serializer to/from JSON.
  76. *
  77. * @var Json
  78. */
  79. private $serializer;
  80. /**
  81. * @var SwatchAttributeType
  82. */
  83. private $swatchTypeChecker;
  84. /**
  85. * @var UrlBuilder
  86. */
  87. private $imageUrlBuilder;
  88. /**
  89. * @param CollectionFactory $productCollectionFactory
  90. * @param ProductRepositoryInterface $productRepository
  91. * @param StoreManagerInterface $storeManager
  92. * @param SwatchCollectionFactory $swatchCollectionFactory
  93. * @param UrlBuilder $urlBuilder
  94. * @param Json|null $serializer
  95. * @param SwatchAttributesProvider $swatchAttributesProvider
  96. * @param SwatchAttributeType|null $swatchTypeChecker
  97. */
  98. public function __construct(
  99. CollectionFactory $productCollectionFactory,
  100. ProductRepositoryInterface $productRepository,
  101. StoreManagerInterface $storeManager,
  102. SwatchCollectionFactory $swatchCollectionFactory,
  103. UrlBuilder $urlBuilder,
  104. Json $serializer = null,
  105. SwatchAttributesProvider $swatchAttributesProvider = null,
  106. SwatchAttributeType $swatchTypeChecker = null
  107. ) {
  108. $this->productCollectionFactory = $productCollectionFactory;
  109. $this->productRepository = $productRepository;
  110. $this->storeManager = $storeManager;
  111. $this->swatchCollectionFactory = $swatchCollectionFactory;
  112. $this->serializer = $serializer ?: ObjectManager::getInstance()->create(Json::class);
  113. $this->swatchAttributesProvider = $swatchAttributesProvider
  114. ?: ObjectManager::getInstance()->get(SwatchAttributesProvider::class);
  115. $this->swatchTypeChecker = $swatchTypeChecker
  116. ?: ObjectManager::getInstance()->create(SwatchAttributeType::class);
  117. $this->imageUrlBuilder = $urlBuilder;
  118. }
  119. /**
  120. * Assemble Additional Data for Eav Attribute
  121. *
  122. * @param Attribute $attribute
  123. * @return $this
  124. */
  125. public function assembleAdditionalDataEavAttribute(Attribute $attribute)
  126. {
  127. $initialAdditionalData = [];
  128. $additionalData = (string)$attribute->getData('additional_data');
  129. if (!empty($additionalData)) {
  130. $additionalData = $this->serializer->unserialize($additionalData);
  131. if (is_array($additionalData)) {
  132. $initialAdditionalData = $additionalData;
  133. }
  134. }
  135. $dataToAdd = [];
  136. foreach ($this->eavAttributeAdditionalDataKeys as $key) {
  137. $dataValue = $attribute->getData($key);
  138. if (null !== $dataValue) {
  139. $dataToAdd[$key] = $dataValue;
  140. }
  141. }
  142. $additionalData = array_merge($initialAdditionalData, $dataToAdd);
  143. $attribute->setData('additional_data', $this->serializer->serialize($additionalData));
  144. return $this;
  145. }
  146. /**
  147. * Check is media attribute available
  148. *
  149. * @param ModelProduct $product
  150. * @param string $attributeCode
  151. * @return bool
  152. */
  153. private function isMediaAvailable(ModelProduct $product, string $attributeCode): bool
  154. {
  155. $isAvailable = false;
  156. $mediaGallery = $product->getMediaGalleryEntries();
  157. foreach ($mediaGallery as $mediaEntry) {
  158. if (in_array($attributeCode, $mediaEntry->getTypes(), true)) {
  159. $isAvailable = !$mediaEntry->isDisabled();
  160. break;
  161. }
  162. }
  163. return $isAvailable;
  164. }
  165. /**
  166. * Load first variation
  167. *
  168. * @param string $attributeCode swatch_image|image
  169. * @param ModelProduct $configurableProduct
  170. * @param array $requiredAttributes
  171. * @return bool|Product
  172. */
  173. private function loadFirstVariation($attributeCode, ModelProduct $configurableProduct, array $requiredAttributes)
  174. {
  175. if ($this->isProductHasSwatch($configurableProduct)) {
  176. $usedProducts = $configurableProduct->getTypeInstance()->getUsedProducts($configurableProduct);
  177. foreach ($usedProducts as $simpleProduct) {
  178. if (!array_diff_assoc($requiredAttributes, $simpleProduct->getData())
  179. && $this->isMediaAvailable($simpleProduct, $attributeCode)
  180. ) {
  181. return $simpleProduct;
  182. }
  183. }
  184. }
  185. return false;
  186. }
  187. /**
  188. * Load first variation with swatch image
  189. *
  190. * @param Product $configurableProduct
  191. * @param array $requiredAttributes
  192. * @return bool|Product
  193. */
  194. public function loadFirstVariationWithSwatchImage(Product $configurableProduct, array $requiredAttributes)
  195. {
  196. return $this->loadFirstVariation('swatch_image', $configurableProduct, $requiredAttributes);
  197. }
  198. /**
  199. * Load first variation with image
  200. *
  201. * @param Product $configurableProduct
  202. * @param array $requiredAttributes
  203. * @return bool|Product
  204. */
  205. public function loadFirstVariationWithImage(Product $configurableProduct, array $requiredAttributes)
  206. {
  207. return $this->loadFirstVariation('image', $configurableProduct, $requiredAttributes);
  208. }
  209. /**
  210. * Load Variation Product using fallback
  211. *
  212. * @param Product $parentProduct
  213. * @param array $attributes
  214. * @return bool|Product
  215. */
  216. public function loadVariationByFallback(Product $parentProduct, array $attributes)
  217. {
  218. if (!$this->isProductHasSwatch($parentProduct)) {
  219. return false;
  220. }
  221. $productCollection = $this->productCollectionFactory->create();
  222. $productLinkedFiled = $this->getMetadataPool()
  223. ->getMetadata(\Magento\Catalog\Api\Data\ProductInterface::class)
  224. ->getLinkField();
  225. $parentId = $parentProduct->getData($productLinkedFiled);
  226. $this->addFilterByParent($productCollection, $parentId);
  227. $configurableAttributes = $this->getAttributesFromConfigurable($parentProduct);
  228. $resultAttributesToFilter = [];
  229. foreach ($configurableAttributes as $attribute) {
  230. $attributeCode = $attribute->getData('attribute_code');
  231. if (array_key_exists($attributeCode, $attributes)) {
  232. $resultAttributesToFilter[$attributeCode] = $attributes[$attributeCode];
  233. }
  234. }
  235. $this->addFilterByAttributes($productCollection, $resultAttributesToFilter);
  236. $variationProduct = $productCollection->getFirstItem();
  237. if ($variationProduct && $variationProduct->getId()) {
  238. return $this->productRepository->getById($variationProduct->getId());
  239. }
  240. return false;
  241. }
  242. /**
  243. * Add filter by attribute
  244. *
  245. * @param ProductCollection $productCollection
  246. * @param array $attributes
  247. * @return void
  248. */
  249. private function addFilterByAttributes(ProductCollection $productCollection, array $attributes)
  250. {
  251. foreach ($attributes as $code => $option) {
  252. $productCollection->addAttributeToFilter($code, ['eq' => $option]);
  253. }
  254. }
  255. /**
  256. * Add filter by parent
  257. *
  258. * @param ProductCollection $productCollection
  259. * @param integer $parentId
  260. * @return void
  261. */
  262. private function addFilterByParent(ProductCollection $productCollection, $parentId)
  263. {
  264. $tableProductRelation = $productCollection->getTable('catalog_product_relation');
  265. $productCollection
  266. ->getSelect()
  267. ->join(
  268. ['pr' => $tableProductRelation],
  269. 'e.entity_id = pr.child_id'
  270. )
  271. ->where('pr.parent_id = ?', $parentId);
  272. }
  273. /**
  274. * Method getting full media gallery for current Product
  275. *
  276. * Array structure: [
  277. * ['image'] => 'http://url/pub/media/catalog/product/2/0/blabla.jpg',
  278. * ['mediaGallery'] => [
  279. * galleryImageId1 => simpleProductImage1.jpg,
  280. * galleryImageId2 => simpleProductImage2.jpg,
  281. * ...,
  282. * ]
  283. * ]
  284. *
  285. * @param ModelProduct $product
  286. *
  287. * @return array
  288. * @throws \Magento\Framework\Exception\LocalizedException
  289. */
  290. public function getProductMediaGallery(ModelProduct $product): array
  291. {
  292. $baseImage = null;
  293. $gallery = [];
  294. $mediaGallery = $product->getMediaGalleryEntries();
  295. /** @var ProductAttributeMediaGalleryEntryInterface $mediaEntry */
  296. foreach ($mediaGallery as $mediaEntry) {
  297. if ($mediaEntry->isDisabled()) {
  298. continue;
  299. }
  300. if (!$baseImage || $this->isMainImage($mediaEntry)) {
  301. $baseImage = $mediaEntry;
  302. }
  303. $gallery[$mediaEntry->getId()] = $this->collectImageData($mediaEntry);
  304. }
  305. if (!$baseImage) {
  306. return [];
  307. }
  308. $resultGallery = $this->collectImageData($baseImage);
  309. $resultGallery['gallery'] = $gallery;
  310. return $resultGallery;
  311. }
  312. /**
  313. * Checks if image is main image in gallery
  314. *
  315. * @param ProductAttributeMediaGalleryEntryInterface $mediaEntry
  316. * @return bool
  317. */
  318. private function isMainImage(ProductAttributeMediaGalleryEntryInterface $mediaEntry): bool
  319. {
  320. return in_array('image', $mediaEntry->getTypes(), true);
  321. }
  322. /**
  323. * Returns image data for swatches
  324. *
  325. * @param ProductAttributeMediaGalleryEntryInterface $mediaEntry
  326. * @return array
  327. */
  328. private function collectImageData(ProductAttributeMediaGalleryEntryInterface $mediaEntry): array
  329. {
  330. $image = $this->getAllSizeImages($mediaEntry->getFile());
  331. $image[ProductAttributeMediaGalleryEntryInterface::POSITION] = $mediaEntry->getPosition();
  332. $image['isMain'] =$this->isMainImage($mediaEntry);
  333. return $image;
  334. }
  335. /**
  336. * Get all size images
  337. *
  338. * @param string $imageFile
  339. * @return array
  340. */
  341. private function getAllSizeImages($imageFile)
  342. {
  343. return [
  344. 'large' => $this->imageUrlBuilder->getUrl($imageFile, 'product_swatch_image_large'),
  345. 'medium' => $this->imageUrlBuilder->getUrl($imageFile, 'product_swatch_image_medium'),
  346. 'small' => $this->imageUrlBuilder->getUrl($imageFile, 'product_swatch_image_small')
  347. ];
  348. }
  349. /**
  350. * Retrieve collection of Swatch attributes
  351. *
  352. * @param Product $product
  353. * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute[]
  354. */
  355. private function getSwatchAttributes(Product $product)
  356. {
  357. $swatchAttributes = $this->swatchAttributesProvider->provide($product);
  358. return $swatchAttributes;
  359. }
  360. /**
  361. * Retrieve collection of Eav Attributes from Configurable product
  362. *
  363. * @param Product $product
  364. * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute[]
  365. */
  366. public function getAttributesFromConfigurable(Product $product)
  367. {
  368. $result = [];
  369. $typeInstance = $product->getTypeInstance();
  370. if ($typeInstance instanceof Configurable) {
  371. $configurableAttributes = $typeInstance->getConfigurableAttributes($product);
  372. /** @var \Magento\ConfigurableProduct\Model\Product\Type\Configurable\Attribute $configurableAttribute */
  373. foreach ($configurableAttributes as $configurableAttribute) {
  374. /** @var \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute */
  375. $attribute = $configurableAttribute->getProductAttribute();
  376. $result[] = $attribute;
  377. }
  378. }
  379. return $result;
  380. }
  381. /**
  382. * Retrieve all visible Swatch attributes for current product.
  383. *
  384. * @param Product $product
  385. * @return array
  386. */
  387. public function getSwatchAttributesAsArray(Product $product)
  388. {
  389. $result = [];
  390. $swatchAttributes = $this->getSwatchAttributes($product);
  391. foreach ($swatchAttributes as $swatchAttribute) {
  392. $swatchAttribute->setStoreId($this->storeManager->getStore()->getId());
  393. $attributeData = $swatchAttribute->getData();
  394. foreach ($swatchAttribute->getSource()->getAllOptions(false) as $option) {
  395. $attributeData['options'][$option['value']] = $option['label'];
  396. }
  397. $result[$attributeData['attribute_id']] = $attributeData;
  398. }
  399. return $result;
  400. }
  401. /**
  402. * @var array
  403. */
  404. private $swatchesCache = [];
  405. /**
  406. * Get swatch options by option id's according to fallback logic
  407. *
  408. * @param array $optionIds
  409. * @return array
  410. */
  411. public function getSwatchesByOptionsId(array $optionIds)
  412. {
  413. $swatches = $this->getCachedSwatches($optionIds);
  414. if (count($swatches) !== count($optionIds)) {
  415. $swatchOptionIds = array_diff($optionIds, array_keys($swatches));
  416. /** @var \Magento\Swatches\Model\ResourceModel\Swatch\Collection $swatchCollection */
  417. $swatchCollection = $this->swatchCollectionFactory->create();
  418. $swatchCollection->addFilterByOptionsIds($swatchOptionIds);
  419. $swatches = [];
  420. $fallbackValues = [];
  421. $currentStoreId = $this->storeManager->getStore()->getId();
  422. foreach ($swatchCollection as $item) {
  423. if ($item['type'] != Swatch::SWATCH_TYPE_TEXTUAL) {
  424. $swatches[$item['option_id']] = $item->getData();
  425. } elseif ($item['store_id'] == $currentStoreId && $item['value'] != '') {
  426. $fallbackValues[$item['option_id']][$currentStoreId] = $item->getData();
  427. } elseif ($item['store_id'] == self::DEFAULT_STORE_ID) {
  428. $fallbackValues[$item['option_id']][self::DEFAULT_STORE_ID] = $item->getData();
  429. }
  430. }
  431. if (!empty($fallbackValues)) {
  432. $swatches = $this->addFallbackOptions($fallbackValues, $swatches);
  433. }
  434. $this->setCachedSwatches($swatchOptionIds, $swatches);
  435. }
  436. return array_filter($this->getCachedSwatches($optionIds));
  437. }
  438. /**
  439. * Get cached swatches
  440. *
  441. * @param array $optionIds
  442. * @return array
  443. */
  444. private function getCachedSwatches(array $optionIds)
  445. {
  446. return array_intersect_key($this->swatchesCache, array_combine($optionIds, $optionIds));
  447. }
  448. /**
  449. * Cache swatch. If no swathes found for specific option id - set null for prevent double call
  450. *
  451. * @param array $optionIds
  452. * @param array $swatches
  453. * @return void
  454. */
  455. private function setCachedSwatches(array $optionIds, array $swatches)
  456. {
  457. foreach ($optionIds as $optionId) {
  458. $this->swatchesCache[$optionId] = isset($swatches[$optionId]) ? $swatches[$optionId] : null;
  459. }
  460. }
  461. /**
  462. * Add fallback options
  463. *
  464. * @param array $fallbackValues
  465. * @param array $swatches
  466. * @return array
  467. */
  468. private function addFallbackOptions(array $fallbackValues, array $swatches)
  469. {
  470. $currentStoreId = $this->storeManager->getStore()->getId();
  471. foreach ($fallbackValues as $optionId => $optionsArray) {
  472. if (isset($optionsArray[$currentStoreId]['type'], $swatches[$optionId]['type'])
  473. && $swatches[$optionId]['type'] === $optionsArray[$currentStoreId]['type']
  474. ) {
  475. $swatches[$optionId] = $optionsArray[$currentStoreId];
  476. } elseif (isset($optionsArray[$currentStoreId])) {
  477. $swatches[$optionId] = $optionsArray[$currentStoreId];
  478. } elseif (isset($optionsArray[self::DEFAULT_STORE_ID])) {
  479. $swatches[$optionId] = $optionsArray[self::DEFAULT_STORE_ID];
  480. }
  481. }
  482. return $swatches;
  483. }
  484. /**
  485. * Check if the Product has Swatch attributes
  486. *
  487. * @param Product $product
  488. * @return bool
  489. */
  490. public function isProductHasSwatch(Product $product)
  491. {
  492. return !empty($this->getSwatchAttributes($product));
  493. }
  494. /**
  495. * Check if an attribute is Swatch
  496. *
  497. * @param Attribute $attribute
  498. * @return bool
  499. */
  500. public function isSwatchAttribute(Attribute $attribute)
  501. {
  502. return $this->swatchTypeChecker->isSwatchAttribute($attribute);
  503. }
  504. /**
  505. * Is attribute Visual Swatch
  506. *
  507. * @param Attribute $attribute
  508. * @return bool
  509. */
  510. public function isVisualSwatch(Attribute $attribute)
  511. {
  512. return $this->swatchTypeChecker->isVisualSwatch($attribute);
  513. }
  514. /**
  515. * Is attribute Textual Swatch
  516. *
  517. * @param Attribute $attribute
  518. * @return bool
  519. */
  520. public function isTextSwatch(Attribute $attribute)
  521. {
  522. return $this->swatchTypeChecker->isTextSwatch($attribute);
  523. }
  524. /**
  525. * Get product metadata pool.
  526. *
  527. * @return \Magento\Framework\EntityManager\MetadataPool
  528. * @deprecared
  529. */
  530. protected function getMetadataPool()
  531. {
  532. if (!$this->metadataPool) {
  533. $this->metadataPool = \Magento\Framework\App\ObjectManager::getInstance()
  534. ->get(\Magento\Framework\EntityManager\MetadataPool::class);
  535. }
  536. return $this->metadataPool;
  537. }
  538. }