Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Eav.php
Go to the documentation of this file.
1 <?php
7 
15 use Magento\Catalog\Model\ResourceModel\Eav\AttributeFactory as EavAttributeFactory;
20 use Magento\Eav\Model\ResourceModel\Entity\Attribute\Group\CollectionFactory as GroupCollectionFactory;
29 use Magento\Ui\Component\Form\Element\Wysiwyg as WysiwygElement;
32 use Magento\Ui\DataProvider\Mapper\FormElement as FormElementMapper;
33 use Magento\Ui\DataProvider\Mapper\MetaProperties as MetaPropertiesMapper;
36 use Magento\Eav\Model\ResourceModel\Entity\Attribute\CollectionFactory as AttributeCollectionFactory;
37 
48 class Eav extends AbstractModifier
49 {
51 
56  protected $locator;
57 
62  protected $eavConfig;
63 
69 
74  protected $request;
75 
81 
86  protected $storeManager;
87 
92  protected $formElementMapper;
93 
99 
105 
111 
117 
122  protected $sortOrderBuilder;
123 
129 
134  protected $translitFilter;
135 
140  protected $arrayManager;
141 
145  private $scopeOverriddenValue;
146 
150  private $attributesToDisable;
151 
157 
162  protected $dataPersistor;
163 
167  private $attributes = [];
168 
172  private $attributeGroups = [];
173 
177  private $canDisplayUseDefault = [];
178 
182  private $bannedInputTypes = ['media_image'];
183 
187  private $prevSetAttributes;
188 
192  private $localeCurrency;
193 
198  private $attributesCache = [];
199 
203  private $attributeCollectionFactory;
204 
208  private $wysiwygConfigProcessor;
209 
213  private $scopeConfig;
214 
241  public function __construct(
246  GroupCollectionFactory $groupCollectionFactory,
248  FormElementMapper $formElementMapper,
249  MetaPropertiesMapper $metaPropertiesMapper,
254  EavAttributeFactory $eavAttributeFactory,
257  ScopeOverriddenValue $scopeOverriddenValue,
259  $attributesToDisable = [],
261  CompositeConfigProcessor $wysiwygConfigProcessor = null,
262  ScopeConfigInterface $scopeConfig = null,
263  AttributeCollectionFactory $attributeCollectionFactory = null
264  ) {
265  $this->locator = $locator;
266  $this->catalogEavValidationRules = $catalogEavValidationRules;
267  $this->eavConfig = $eavConfig;
268  $this->request = $request;
269  $this->groupCollectionFactory = $groupCollectionFactory;
270  $this->storeManager = $storeManager;
271  $this->formElementMapper = $formElementMapper;
272  $this->metaPropertiesMapper = $metaPropertiesMapper;
273  $this->attributeGroupRepository = $attributeGroupRepository;
274  $this->searchCriteriaBuilder = $searchCriteriaBuilder;
275  $this->attributeRepository = $attributeRepository;
276  $this->sortOrderBuilder = $sortOrderBuilder;
277  $this->eavAttributeFactory = $eavAttributeFactory;
278  $this->translitFilter = $translitFilter;
279  $this->arrayManager = $arrayManager;
280  $this->scopeOverriddenValue = $scopeOverriddenValue;
281  $this->dataPersistor = $dataPersistor;
282  $this->attributesToDisable = $attributesToDisable;
283  $this->attributesToEliminate = $attributesToEliminate;
284  $this->wysiwygConfigProcessor = $wysiwygConfigProcessor ?: \Magento\Framework\App\ObjectManager::getInstance()
285  ->get(CompositeConfigProcessor::class);
286  $this->scopeConfig = $scopeConfig ?: \Magento\Framework\App\ObjectManager::getInstance()
287  ->get(ScopeConfigInterface::class);
288  $this->attributeCollectionFactory = $attributeCollectionFactory
289  ?: \Magento\Framework\App\ObjectManager::getInstance()->get(AttributeCollectionFactory::class);
290  }
291 
296  public function modifyMeta(array $meta)
297  {
298  $sortOrder = 0;
299 
300  foreach ($this->getGroups() as $groupCode => $group) {
301  $attributes = !empty($this->getAttributes()[$groupCode]) ? $this->getAttributes()[$groupCode] : [];
302 
303  if ($attributes) {
304  $meta[$groupCode]['children'] = $this->getAttributesMeta($attributes, $groupCode);
305  $meta[$groupCode]['arguments']['data']['config']['componentType'] = Fieldset::NAME;
306  $meta[$groupCode]['arguments']['data']['config']['label'] = __($group->getAttributeGroupName());
307  $meta[$groupCode]['arguments']['data']['config']['collapsible'] = true;
308  $meta[$groupCode]['arguments']['data']['config']['dataScope'] = self::DATA_SCOPE_PRODUCT;
309  $meta[$groupCode]['arguments']['data']['config']['sortOrder'] =
310  $sortOrder * self::SORT_ORDER_MULTIPLIER;
311  }
312 
313  $sortOrder++;
314  }
315 
316  return $meta;
317  }
318 
327  private function getAttributesMeta(array $attributes, $groupCode)
328  {
329  $meta = [];
330 
331  foreach ($attributes as $sortOrder => $attribute) {
332  if (in_array($attribute->getFrontendInput(), $this->bannedInputTypes)) {
333  continue;
334  }
335 
336  if (in_array($attribute->getAttributeCode(), $this->attributesToEliminate)) {
337  continue;
338  }
339 
340  if (!($attributeContainer = $this->setupAttributeContainerMeta($attribute))) {
341  continue;
342  }
343 
344  $attributeContainer = $this->addContainerChildren($attributeContainer, $attribute, $groupCode, $sortOrder);
345 
346  $meta[static::CONTAINER_PREFIX . $attribute->getAttributeCode()] = $attributeContainer;
347  }
348 
349  return $meta;
350  }
351 
363  public function addContainerChildren(
364  array $attributeContainer,
366  $groupCode,
367  $sortOrder
368  ) {
369  foreach ($this->getContainerChildren($attribute, $groupCode, $sortOrder) as $childCode => $child) {
370  $attributeContainer['children'][$childCode] = $child;
371  }
372 
373  $attributeContainer = $this->arrayManager->merge(
374  ltrim(static::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER),
375  $attributeContainer,
376  [
377  'sortOrder' => $sortOrder * self::SORT_ORDER_MULTIPLIER
378  ]
379  );
380 
381  return $attributeContainer;
382  }
383 
394  public function getContainerChildren(ProductAttributeInterface $attribute, $groupCode, $sortOrder)
395  {
396  if (!($child = $this->setupAttributeMeta($attribute, $groupCode, $sortOrder))) {
397  return [];
398  }
399 
400  return [$attribute->getAttributeCode() => $child];
401  }
402 
407  public function modifyData(array $data)
408  {
409  if (!$this->locator->getProduct()->getId() && $this->dataPersistor->get('catalog_product')) {
410  return $this->resolvePersistentData($data);
411  }
412 
413  $productId = $this->locator->getProduct()->getId();
414 
416  foreach (array_keys($this->getGroups()) as $groupCode) {
418  $attributes = !empty($this->getAttributes()[$groupCode]) ? $this->getAttributes()[$groupCode] : [];
419 
420  foreach ($attributes as $attribute) {
421  if (null !== ($attributeValue = $this->setupAttributeData($attribute))) {
422  if ($attribute->getFrontendInput() === 'price' && is_scalar($attributeValue)) {
423  $attributeValue = $this->formatPrice($attributeValue);
424  }
425  $data[$productId][self::DATA_SOURCE_DEFAULT][$attribute->getAttributeCode()] = $attributeValue;
426  }
427  }
428  }
429 
430  return $data;
431  }
432 
439  private function resolvePersistentData(array $data)
440  {
441  $persistentData = (array)$this->dataPersistor->get('catalog_product');
442  $this->dataPersistor->clear('catalog_product');
443  $productId = $this->locator->getProduct()->getId();
444 
445  if (empty($data[$productId][self::DATA_SOURCE_DEFAULT])) {
447  }
448 
449  $data[$productId] = array_replace_recursive(
450  $data[$productId][self::DATA_SOURCE_DEFAULT],
451  $persistentData
452  );
453 
454  return $data;
455  }
456 
462  private function getProductType()
463  {
464  return (string)$this->request->getParam('type', $this->locator->getProduct()->getTypeId());
465  }
466 
472  private function getPreviousSetId()
473  {
474  return (int)$this->request->getParam('prev_set_id', 0);
475  }
476 
482  private function getGroups()
483  {
484  if (!$this->attributeGroups) {
485  $searchCriteria = $this->prepareGroupSearchCriteria()->create();
486  $attributeGroupSearchResult = $this->attributeGroupRepository->getList($searchCriteria);
487  foreach ($attributeGroupSearchResult->getItems() as $group) {
488  $this->attributeGroups[$this->calculateGroupCode($group)] = $group;
489  }
490  }
491 
492  return $this->attributeGroups;
493  }
494 
500  private function prepareGroupSearchCriteria()
501  {
502  return $this->searchCriteriaBuilder->addFilter(
504  $this->getAttributeSetId()
505  );
506  }
507 
513  private function getAttributeSetId()
514  {
515  return $this->locator->getProduct()->getAttributeSetId();
516  }
517 
523  private function getAttributes()
524  {
525  if (!$this->attributes) {
526  $this->attributes = $this->loadAttributesForGroups($this->getGroups());
527  }
528 
529  return $this->attributes;
530  }
531 
538  private function loadAttributesForGroups(array $groups)
539  {
540  $attributes = [];
541  $groupIds = [];
542 
543  foreach ($groups as $group) {
544  $groupIds[$group->getAttributeGroupId()] = $this->calculateGroupCode($group);
545  $attributes[$this->calculateGroupCode($group)] = [];
546  }
547 
548  $collection = $this->attributeCollectionFactory->create();
549  $collection->setAttributeGroupFilter(array_keys($groupIds));
550 
551  $mapAttributeToGroup = [];
552 
553  foreach ($collection->getItems() as $attribute) {
554  $mapAttributeToGroup[$attribute->getAttributeId()] = $attribute->getAttributeGroupId();
555  }
556 
557  $sortOrder = $this->sortOrderBuilder
558  ->setField('sort_order')
559  ->setAscendingDirection()
560  ->create();
561 
562  $searchCriteria = $this->searchCriteriaBuilder
563  ->addFilter(AttributeGroupInterface::GROUP_ID, array_keys($groupIds), 'in')
565  ->addSortOrder($sortOrder)
566  ->create();
567 
568  $groupAttributes = $this->attributeRepository->getList($searchCriteria)->getItems();
569 
570  $productType = $this->getProductType();
571 
572  foreach ($groupAttributes as $attribute) {
573  $applyTo = $attribute->getApplyTo();
574  $isRelated = !$applyTo || in_array($productType, $applyTo);
575  if ($isRelated) {
576  $attributeGroupId = $mapAttributeToGroup[$attribute->getAttributeId()];
577  $attributeGroupCode = $groupIds[$attributeGroupId];
578  $attributes[$attributeGroupCode][] = $attribute;
579  }
580  }
581 
582  return $attributes;
583  }
584 
591  private function getPreviousSetAttributes()
592  {
593  if ($this->prevSetAttributes === null) {
594  $searchCriteria = $this->searchCriteriaBuilder
595  ->addFilter('attribute_set_id', $this->getPreviousSetId())
596  ->create();
597  $attributes = $this->attributeRepository->getList($searchCriteria)->getItems();
598  $this->prevSetAttributes = [];
599  foreach ($attributes as $attribute) {
600  $this->prevSetAttributes[] = $attribute->getAttributeCode();
601  }
602  }
603 
604  return $this->prevSetAttributes;
605  }
606 
612  private function isProductExists()
613  {
614  return (bool) $this->locator->getProduct()->getId();
615  }
616 
630  public function setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder)
631  {
632  $configPath = ltrim(static::META_CONFIG_PATH, ArrayManager::DEFAULT_PATH_DELIMITER);
633  $attributeCode = $attribute->getAttributeCode();
634  $meta = $this->arrayManager->set($configPath, [], [
635  'dataType' => $attribute->getFrontendInput(),
636  'formElement' => $this->getFormElementsMapValue($attribute->getFrontendInput()),
637  'visible' => $attribute->getIsVisible(),
638  'required' => $attribute->getIsRequired(),
639  'notice' => $attribute->getNote() === null ? null : __($attribute->getNote()),
640  'default' => (!$this->isProductExists()) ? $this->getAttributeDefaultValue($attribute) : null,
641  'label' => __($attribute->getDefaultFrontendLabel()),
642  'code' => $attributeCode,
643  'source' => $groupCode,
644  'scopeLabel' => $this->getScopeLabel($attribute),
645  'globalScope' => $this->isScopeGlobal($attribute),
646  'sortOrder' => $sortOrder * self::SORT_ORDER_MULTIPLIER,
647  ]);
648 
649  // TODO: Refactor to $attribute->getOptions() when MAGETWO-48289 is done
650  $attributeModel = $this->getAttributeModel($attribute);
651  if ($attributeModel->usesSource()) {
652  $options = $attributeModel->getSource()->getAllOptions();
653  $meta = $this->arrayManager->merge($configPath, $meta, [
654  'options' => $this->convertOptionsValueToString($options),
655  ]);
656  }
657 
658  if ($this->canDisplayUseDefault($attribute)) {
659  $meta = $this->arrayManager->merge($configPath, $meta, [
660  'service' => [
661  'template' => 'ui/form/element/helper/service',
662  ]
663  ]);
664  }
665 
666  if (!$this->arrayManager->exists($configPath . '/componentType', $meta)) {
667  $meta = $this->arrayManager->merge($configPath, $meta, [
668  'componentType' => Field::NAME,
669  ]);
670  }
671 
672  $product = $this->locator->getProduct();
673  if (in_array($attributeCode, $this->attributesToDisable)
674  || $product->isLockedAttribute($attributeCode)) {
675  $meta = $this->arrayManager->merge($configPath, $meta, [
676  'disabled' => true,
677  ]);
678  }
679 
680  // TODO: getAttributeModel() should not be used when MAGETWO-48284 is complete
681  $childData = $this->arrayManager->get($configPath, $meta, []);
682  if (($rules = $this->catalogEavValidationRules->build($this->getAttributeModel($attribute), $childData))) {
683  $meta = $this->arrayManager->merge($configPath, $meta, [
684  'validation' => $rules,
685  ]);
686  }
687 
688  $meta = $this->addUseDefaultValueCheckbox($attribute, $meta);
689 
690  switch ($attribute->getFrontendInput()) {
691  case 'boolean':
692  $meta = $this->customizeCheckbox($attribute, $meta);
693  break;
694  case 'textarea':
695  $meta = $this->customizeWysiwyg($attribute, $meta);
696  break;
697  case 'price':
698  $meta = $this->customizePriceAttribute($attribute, $meta);
699  break;
700  case 'gallery':
701  // Gallery attribute is being handled by "Images And Videos" section
702  $meta = [];
703  break;
704  }
705 
706  return $meta;
707  }
708 
715  private function getAttributeDefaultValue(ProductAttributeInterface $attribute)
716  {
717  if ($attribute->getAttributeCode() === 'page_layout') {
718  $defaultValue = $this->scopeConfig->getValue(
719  'web/default_layouts/default_product_layout',
721  $this->storeManager->getStore()
722  );
723  $attribute->setDefaultValue($defaultValue);
724  }
725  return $attribute->getDefaultValue();
726  }
727 
734  private function convertOptionsValueToString(array $options) : array
735  {
736  array_walk($options, function (&$value) {
737  if (isset($value['value']) && is_scalar($value['value'])) {
738  $value['value'] = (string)$value['value'];
739  }
740  });
741 
742  return $options;
743  }
744 
752  private function addUseDefaultValueCheckbox(ProductAttributeInterface $attribute, array $meta)
753  {
754  $canDisplayService = $this->canDisplayUseDefault($attribute);
755  if ($canDisplayService) {
756  $meta['arguments']['data']['config']['service'] = [
757  'template' => 'ui/form/element/helper/service',
758  ];
759 
760  $meta['arguments']['data']['config']['disabled'] = !$this->scopeOverriddenValue->containsValue(
761  \Magento\Catalog\Api\Data\ProductInterface::class,
762  $this->locator->getProduct(),
763  $attribute->getAttributeCode(),
764  $this->locator->getStore()->getId()
765  );
766  }
767  return $meta;
768  }
769 
779  {
780  $containerMeta = $this->arrayManager->set(
781  'arguments/data/config',
782  [],
783  [
784  'formElement' => 'container',
785  'componentType' => 'container',
786  'breakLine' => false,
787  'label' => $attribute->getDefaultFrontendLabel(),
788  'required' => $attribute->getIsRequired(),
789  ]
790  );
791 
792  if ($attribute->getIsWysiwygEnabled()) {
793  $containerMeta = $this->arrayManager->merge(
794  'arguments/data/config',
795  $containerMeta,
796  [
797  'component' => 'Magento_Ui/js/form/components/group'
798  ]
799  );
800  }
801 
802  return $containerMeta;
803  }
804 
814  {
815  $product = $this->locator->getProduct();
816  $productId = $product->getId();
817  $prevSetId = $this->getPreviousSetId();
818  $notUsed = !$prevSetId
819  || ($prevSetId && !in_array($attribute->getAttributeCode(), $this->getPreviousSetAttributes()));
820 
821  if ($productId && $notUsed) {
822  return $this->getValue($attribute);
823  }
824 
825  return null;
826  }
827 
835  private function customizeCheckbox(ProductAttributeInterface $attribute, array $meta)
836  {
837  if ($attribute->getFrontendInput() === 'boolean') {
838  $meta['arguments']['data']['config']['prefer'] = 'toggle';
839  $meta['arguments']['data']['config']['valueMap'] = [
840  'true' => '1',
841  'false' => '0',
842  ];
843  }
844 
845  return $meta;
846  }
847 
855  private function customizePriceAttribute(ProductAttributeInterface $attribute, array $meta)
856  {
857  if ($attribute->getFrontendInput() === 'price') {
858  $meta['arguments']['data']['config']['addbefore'] = $this->locator->getStore()
859  ->getBaseCurrency()
860  ->getCurrencySymbol();
861  }
862 
863  return $meta;
864  }
865 
873  private function customizeWysiwyg(ProductAttributeInterface $attribute, array $meta)
874  {
875  if (!$attribute->getIsWysiwygEnabled()) {
876  return $meta;
877  }
878 
879  $meta['arguments']['data']['config']['formElement'] = WysiwygElement::NAME;
880  $meta['arguments']['data']['config']['wysiwyg'] = true;
881  $meta['arguments']['data']['config']['wysiwygConfigData'] = $this->wysiwygConfigProcessor->process($attribute);
882 
883  return $meta;
884  }
885 
892  private function getFormElementsMapValue($value)
893  {
894  $valueMap = $this->formElementMapper->getMappings();
895 
896  return $valueMap[$value] ?? $value;
897  }
898 
905  private function getValue(ProductAttributeInterface $attribute)
906  {
908  $product = $this->locator->getProduct();
909 
910  return $product->getData($attribute->getAttributeCode());
911  }
912 
919  private function getScopeLabel(ProductAttributeInterface $attribute)
920  {
921  if ($this->storeManager->isSingleStoreMode()
922  || $attribute->getFrontendInput() === AttributeInterface::FRONTEND_INPUT
923  ) {
924  return '';
925  }
926 
927  switch ($attribute->getScope()) {
929  return __('[GLOBAL]');
931  return __('[WEBSITE]');
933  return __('[STORE VIEW]');
934  }
935 
936  return '';
937  }
938 
945  private function canDisplayUseDefault(ProductAttributeInterface $attribute)
946  {
947  $attributeCode = $attribute->getAttributeCode();
949  $product = $this->locator->getProduct();
950  if ($product->isLockedAttribute($attributeCode)) {
951  return false;
952  }
953 
954  if (isset($this->canDisplayUseDefault[$attributeCode])) {
955  return $this->canDisplayUseDefault[$attributeCode];
956  }
957 
958  return $this->canDisplayUseDefault[$attributeCode] = (
960  && $product
961  && $product->getId()
962  && $product->getStoreId()
963  );
964  }
965 
972  private function isScopeGlobal($attribute)
973  {
975  }
976 
985  private function getAttributeModel($attribute)
986  {
987  $attributeId = $attribute->getAttributeId();
988 
989  if (!array_key_exists($attributeId, $this->attributesCache)) {
990  $this->attributesCache[$attributeId] = $this->eavAttributeFactory->create()->load($attributeId);
991  }
992 
993  return $this->attributesCache[$attributeId];
994  }
995 
1006  private function calculateGroupCode(AttributeGroupInterface $group)
1007  {
1008  $attributeGroupCode = $group->getAttributeGroupCode();
1009 
1010  if ($attributeGroupCode === 'images') {
1011  $attributeGroupCode = 'image-management';
1012  }
1013 
1014  return $attributeGroupCode;
1015  }
1016 
1024  private function getLocaleCurrency()
1025  {
1026  if ($this->localeCurrency === null) {
1027  $this->localeCurrency = \Magento\Framework\App\ObjectManager::getInstance()->get(CurrencyInterface::class);
1028  }
1029  return $this->localeCurrency;
1030  }
1031 
1039  protected function formatPrice($value)
1040  {
1041  if (!is_numeric($value)) {
1042  return null;
1043  }
1044 
1045  $store = $this->storeManager->getStore();
1046  $currency = $this->getLocaleCurrency()->getCurrency($store->getBaseCurrencyCode());
1047  $value = $currency->toCurrency($value, ['display' => \Magento\Framework\Currency::NO_SYMBOL]);
1048 
1049  return $value;
1050  }
1051 }
setupAttributeMeta(ProductAttributeInterface $attribute, $groupCode, $sortOrder)
Definition: Eav.php:630
$attributeGroupId
addContainerChildren(array $attributeContainer, ProductAttributeInterface $attribute, $groupCode, $sortOrder)
Definition: Eav.php:363
$group
Definition: sections.phtml:16
__()
Definition: __.php:13
$searchCriteria
getContainerChildren(ProductAttributeInterface $attribute, $groupCode, $sortOrder)
Definition: Eav.php:394
$value
Definition: gender.phtml:16
$attributeCode
Definition: extend.phtml:12
setupAttributeContainerMeta(ProductAttributeInterface $attribute)
Definition: Eav.php:778
$attributes
Definition: matrix.phtml:13
__construct(LocatorInterface $locator, CatalogEavValidationRules $catalogEavValidationRules, Config $eavConfig, RequestInterface $request, GroupCollectionFactory $groupCollectionFactory, StoreManagerInterface $storeManager, FormElementMapper $formElementMapper, MetaPropertiesMapper $metaPropertiesMapper, ProductAttributeGroupRepositoryInterface $attributeGroupRepository, ProductAttributeRepositoryInterface $attributeRepository, SearchCriteriaBuilder $searchCriteriaBuilder, SortOrderBuilder $sortOrderBuilder, EavAttributeFactory $eavAttributeFactory, Translit $translitFilter, ArrayManager $arrayManager, ScopeOverriddenValue $scopeOverriddenValue, DataPersistorInterface $dataPersistor, $attributesToDisable=[], $attributesToEliminate=[], CompositeConfigProcessor $wysiwygConfigProcessor=null, ScopeConfigInterface $scopeConfig=null, AttributeCollectionFactory $attributeCollectionFactory=null)
Definition: Eav.php:241
setupAttributeData(ProductAttributeInterface $attribute)
Definition: Eav.php:813