Attribute.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Eav\Model\Entity;
  7. use Magento\Framework\Api\AttributeValueFactory;
  8. use Magento\Framework\Exception\LocalizedException;
  9. use Magento\Framework\Stdlib\DateTime;
  10. use Magento\Framework\Stdlib\DateTime\DateTimeFormatterInterface;
  11. /**
  12. * EAV Entity attribute model
  13. *
  14. * @api
  15. * @method \Magento\Eav\Model\Entity\Attribute setOption($value)
  16. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  17. * @since 100.0.2
  18. */
  19. class Attribute extends \Magento\Eav\Model\Entity\Attribute\AbstractAttribute implements
  20. \Magento\Framework\DataObject\IdentityInterface
  21. {
  22. /**
  23. * Attribute code max length.
  24. *
  25. * The value is defined as 60 because in the flat mode attribute code will be transformed into column name.
  26. * MySQL allows only 64 symbols in column name.
  27. */
  28. const ATTRIBUTE_CODE_MAX_LENGTH = 60;
  29. /**
  30. * Attribute code min length.
  31. */
  32. const ATTRIBUTE_CODE_MIN_LENGTH = 1;
  33. /**
  34. * Cache tag
  35. */
  36. const CACHE_TAG = 'EAV_ATTRIBUTE';
  37. /**
  38. * Prefix of model events names
  39. *
  40. * @var string
  41. */
  42. protected $_eventPrefix = 'eav_entity_attribute';
  43. /**
  44. * Parameter name in event
  45. *
  46. * In observe method you can use $observer->getEvent()->getAttribute() in this case
  47. *
  48. * @var string
  49. */
  50. protected $_eventObject = 'attribute';
  51. /**
  52. * @var string
  53. */
  54. protected $_cacheTag = self::CACHE_TAG;
  55. /**
  56. * @var \Magento\Framework\Stdlib\DateTime\TimezoneInterface
  57. */
  58. protected $_localeDate;
  59. /**
  60. * @var \Magento\Catalog\Model\Product\ReservedAttributeList
  61. */
  62. protected $reservedAttributeList;
  63. /**
  64. * @var \Magento\Framework\Locale\ResolverInterface
  65. */
  66. protected $_localeResolver;
  67. /**
  68. * @var DateTimeFormatterInterface
  69. */
  70. protected $dateTimeFormatter;
  71. /**
  72. * @param \Magento\Framework\Model\Context $context
  73. * @param \Magento\Framework\Registry $registry
  74. * @param \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory
  75. * @param AttributeValueFactory $customAttributeFactory
  76. * @param \Magento\Eav\Model\Config $eavConfig
  77. * @param TypeFactory $eavTypeFactory
  78. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  79. * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper
  80. * @param \Magento\Framework\Validator\UniversalFactory $universalFactory
  81. * @param \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory $optionDataFactory
  82. * @param \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor
  83. * @param \Magento\Framework\Api\DataObjectHelper $dataObjectHelper
  84. * @param \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate
  85. * @param \Magento\Catalog\Model\Product\ReservedAttributeList $reservedAttributeList
  86. * @param \Magento\Framework\Locale\ResolverInterface $localeResolver
  87. * @param DateTimeFormatterInterface $dateTimeFormatter
  88. * @param \Magento\Framework\Model\ResourceModel\AbstractResource $resource
  89. * @param \Magento\Framework\Data\Collection\AbstractDb $resourceCollection
  90. * @param array $data
  91. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  92. * @codeCoverageIgnore
  93. */
  94. public function __construct(
  95. \Magento\Framework\Model\Context $context,
  96. \Magento\Framework\Registry $registry,
  97. \Magento\Framework\Api\ExtensionAttributesFactory $extensionFactory,
  98. AttributeValueFactory $customAttributeFactory,
  99. \Magento\Eav\Model\Config $eavConfig,
  100. \Magento\Eav\Model\Entity\TypeFactory $eavTypeFactory,
  101. \Magento\Store\Model\StoreManagerInterface $storeManager,
  102. \Magento\Eav\Model\ResourceModel\Helper $resourceHelper,
  103. \Magento\Framework\Validator\UniversalFactory $universalFactory,
  104. \Magento\Eav\Api\Data\AttributeOptionInterfaceFactory $optionDataFactory,
  105. \Magento\Framework\Reflection\DataObjectProcessor $dataObjectProcessor,
  106. \Magento\Framework\Api\DataObjectHelper $dataObjectHelper,
  107. \Magento\Framework\Stdlib\DateTime\TimezoneInterface $localeDate,
  108. \Magento\Catalog\Model\Product\ReservedAttributeList $reservedAttributeList,
  109. \Magento\Framework\Locale\ResolverInterface $localeResolver,
  110. DateTimeFormatterInterface $dateTimeFormatter,
  111. \Magento\Framework\Model\ResourceModel\AbstractResource $resource = null,
  112. \Magento\Framework\Data\Collection\AbstractDb $resourceCollection = null,
  113. array $data = []
  114. ) {
  115. parent::__construct(
  116. $context,
  117. $registry,
  118. $extensionFactory,
  119. $customAttributeFactory,
  120. $eavConfig,
  121. $eavTypeFactory,
  122. $storeManager,
  123. $resourceHelper,
  124. $universalFactory,
  125. $optionDataFactory,
  126. $dataObjectProcessor,
  127. $dataObjectHelper,
  128. $resource,
  129. $resourceCollection,
  130. $data
  131. );
  132. $this->_localeDate = $localeDate;
  133. $this->_localeResolver = $localeResolver;
  134. $this->reservedAttributeList = $reservedAttributeList;
  135. $this->dateTimeFormatter = $dateTimeFormatter;
  136. }
  137. /**
  138. * Retrieve default attribute backend model by attribute code
  139. *
  140. * @return string
  141. */
  142. protected function _getDefaultBackendModel()
  143. {
  144. switch ($this->getAttributeCode()) {
  145. case 'created_at':
  146. return \Magento\Eav\Model\Entity\Attribute\Backend\Time\Created::class;
  147. case 'updated_at':
  148. return \Magento\Eav\Model\Entity\Attribute\Backend\Time\Updated::class;
  149. case 'store_id':
  150. return \Magento\Eav\Model\Entity\Attribute\Backend\Store::class;
  151. case 'increment_id':
  152. return \Magento\Eav\Model\Entity\Attribute\Backend\Increment::class;
  153. default:
  154. break;
  155. }
  156. return parent::_getDefaultBackendModel();
  157. }
  158. /**
  159. * Retrieve default attribute source model
  160. *
  161. * @return string
  162. */
  163. protected function _getDefaultSourceModel()
  164. {
  165. if ($this->getAttributeCode() == 'store_id') {
  166. return \Magento\Eav\Model\Entity\Attribute\Source\Store::class;
  167. }
  168. return parent::_getDefaultSourceModel();
  169. }
  170. /**
  171. * Delete entity
  172. *
  173. * @return \Magento\Eav\Model\ResourceModel\Entity\Attribute
  174. * @codeCoverageIgnore
  175. */
  176. public function deleteEntity()
  177. {
  178. return $this->_getResource()->deleteEntity($this);
  179. }
  180. /**
  181. * Load entity_attribute_id into $this by $this->attribute_set_id
  182. *
  183. * @return $this
  184. */
  185. public function loadEntityAttributeIdBySet()
  186. {
  187. // load attributes collection filtered by attribute_id and attribute_set_id
  188. $filteredAttributes = $this->getResourceCollection()->setAttributeSetFilter(
  189. $this->getAttributeSetId()
  190. )->addFieldToFilter(
  191. 'entity_attribute.attribute_id',
  192. $this->getId()
  193. )->load();
  194. if (count($filteredAttributes) > 0) {
  195. // getFirstItem() can be used as we can have one or zero records in the collection
  196. $this->setEntityAttributeId($filteredAttributes->getFirstItem()->getEntityAttributeId());
  197. }
  198. return $this;
  199. }
  200. /**
  201. * Prepare data for save
  202. *
  203. * @return $this
  204. * @throws LocalizedException
  205. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  206. * @SuppressWarnings(PHPMD.NPathComplexity)
  207. */
  208. public function beforeSave()
  209. {
  210. // prevent overriding product data
  211. if (isset($this->_data['attribute_code']) && $this->reservedAttributeList->isReservedAttribute($this)) {
  212. throw new LocalizedException(
  213. __(
  214. 'The attribute code \'%1\' is reserved by system. Please try another attribute code',
  215. $this->_data['attribute_code']
  216. )
  217. );
  218. }
  219. /**
  220. * Check for maximum attribute_code length
  221. */
  222. if (isset(
  223. $this->_data['attribute_code']
  224. ) && !\Zend_Validate::is(
  225. $this->_data['attribute_code'],
  226. 'StringLength',
  227. ['max' => self::ATTRIBUTE_CODE_MAX_LENGTH]
  228. )
  229. ) {
  230. throw new LocalizedException(
  231. __(
  232. 'The attribute code needs to be %1 characters or fewer. Re-enter the code and try again.',
  233. self::ATTRIBUTE_CODE_MAX_LENGTH
  234. )
  235. );
  236. }
  237. $defaultValue = $this->getDefaultValue();
  238. $hasDefaultValue = (string)$defaultValue != '';
  239. if ($this->getBackendType() == 'decimal' && $hasDefaultValue) {
  240. $numberFormatter = new \NumberFormatter($this->_localeResolver->getLocale(), \NumberFormatter::DECIMAL);
  241. $defaultValue = $numberFormatter->parse($defaultValue);
  242. if ($defaultValue === false) {
  243. throw new LocalizedException(
  244. __('The default decimal value is invalid. Verify the value and try again.')
  245. );
  246. }
  247. $this->setDefaultValue($defaultValue);
  248. }
  249. if ($this->getBackendType() == 'datetime') {
  250. if (!$this->getBackendModel()) {
  251. $this->setBackendModel(\Magento\Eav\Model\Entity\Attribute\Backend\Datetime::class);
  252. }
  253. if (!$this->getFrontendModel()) {
  254. $this->setFrontendModel(\Magento\Eav\Model\Entity\Attribute\Frontend\Datetime::class);
  255. }
  256. // save default date value as timestamp
  257. if ($hasDefaultValue) {
  258. try {
  259. $locale = $this->_localeResolver->getLocale();
  260. $defaultValue = $this->_localeDate->date($defaultValue, $locale, false, false);
  261. $this->setDefaultValue($defaultValue->format(DateTime::DATETIME_PHP_FORMAT));
  262. } catch (\Exception $e) {
  263. throw new LocalizedException(__('The default date is invalid. Verify the date and try again.'));
  264. }
  265. }
  266. }
  267. if ($this->getFrontendInput() == 'media_image') {
  268. if (!$this->getFrontendModel()) {
  269. $this->setFrontendModel(\Magento\Catalog\Model\Product\Attribute\Frontend\Image::class);
  270. }
  271. }
  272. if ($this->getBackendType() == 'gallery') {
  273. if (!$this->getBackendModel()) {
  274. $this->setBackendModel(\Magento\Eav\Model\Entity\Attribute\Backend\DefaultBackend::class);
  275. }
  276. }
  277. return parent::beforeSave();
  278. }
  279. /**
  280. * Save additional data
  281. *
  282. * @return $this
  283. */
  284. public function afterSave()
  285. {
  286. $this->_getResource()->saveInSetIncluding($this);
  287. return parent::afterSave();
  288. }
  289. /**
  290. * @inheritdoc
  291. * @since 100.0.7
  292. */
  293. public function afterDelete()
  294. {
  295. return parent::afterDelete();
  296. }
  297. /**
  298. * Detect backend storage type using frontend input type
  299. *
  300. * @param string $type frontend_input field value
  301. * @return string backend_type field value
  302. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  303. */
  304. public function getBackendTypeByInput($type)
  305. {
  306. $field = null;
  307. switch ($type) {
  308. case 'text':
  309. case 'gallery':
  310. case 'media_image':
  311. case 'multiselect':
  312. $field = 'varchar';
  313. break;
  314. case 'image':
  315. case 'textarea':
  316. $field = 'text';
  317. break;
  318. case 'date':
  319. $field = 'datetime';
  320. break;
  321. case 'select':
  322. case 'boolean':
  323. $field = 'int';
  324. break;
  325. case 'price':
  326. case 'weight':
  327. $field = 'decimal';
  328. break;
  329. default:
  330. break;
  331. }
  332. return $field;
  333. }
  334. /**
  335. * Detect default value using frontend input type
  336. *
  337. * @param string $type frontend_input field name
  338. * @return string default_value field value
  339. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  340. */
  341. public function getDefaultValueByInput($type)
  342. {
  343. $field = '';
  344. switch ($type) {
  345. case 'select':
  346. case 'gallery':
  347. case 'media_image':
  348. break;
  349. case 'multiselect':
  350. $field = null;
  351. break;
  352. case 'text':
  353. case 'price':
  354. case 'image':
  355. case 'weight':
  356. $field = 'default_value_text';
  357. break;
  358. case 'textarea':
  359. case 'texteditor':
  360. $field = 'default_value_textarea';
  361. break;
  362. case 'date':
  363. $field = 'default_value_date';
  364. break;
  365. case 'boolean':
  366. $field = 'default_value_yesno';
  367. break;
  368. default:
  369. break;
  370. }
  371. return $field;
  372. }
  373. /**
  374. * Retrieve attribute codes by frontend type
  375. *
  376. * @param string $type
  377. * @return array
  378. * @codeCoverageIgnore
  379. */
  380. public function getAttributeCodesByFrontendType($type)
  381. {
  382. return $this->getResource()->getAttributeCodesByFrontendType($type);
  383. }
  384. /**
  385. * Return array of labels of stores
  386. *
  387. * @return string[]
  388. */
  389. public function getStoreLabels()
  390. {
  391. if (!$this->getData('store_labels')) {
  392. $storeLabel = $this->getResource()->getStoreLabelsByAttributeId($this->getId());
  393. $this->setData('store_labels', $storeLabel);
  394. }
  395. return $this->getData('store_labels');
  396. }
  397. /**
  398. * Return store label of attribute
  399. *
  400. * @param int|null $storeId
  401. * @return string
  402. */
  403. public function getStoreLabel($storeId = null)
  404. {
  405. if ($this->hasData('store_label')) {
  406. return $this->getData('store_label');
  407. }
  408. $store = $this->_storeManager->getStore($storeId);
  409. $labels = $this->getStoreLabels();
  410. if (isset($labels[$store->getId()])) {
  411. return $labels[$store->getId()];
  412. } else {
  413. return $this->getFrontendLabel();
  414. }
  415. }
  416. /**
  417. * Get attribute sort weight
  418. *
  419. * @param int $setId
  420. * @return float
  421. */
  422. public function getSortWeight($setId)
  423. {
  424. $groupSortWeight = isset($this->_data['attribute_set_info'][$setId]['group_sort'])
  425. ? (float) $this->_data['attribute_set_info'][$setId]['group_sort'] * 1000
  426. : 0.0;
  427. $sortWeight = isset($this->_data['attribute_set_info'][$setId]['sort'])
  428. ? (float) $this->_data['attribute_set_info'][$setId]['sort'] * 0.0001
  429. : 0.0;
  430. return $groupSortWeight + $sortWeight;
  431. }
  432. /**
  433. * Get identities
  434. *
  435. * @return array
  436. * @codeCoverageIgnore
  437. */
  438. public function getIdentities()
  439. {
  440. return [self::CACHE_TAG . '_' . $this->getId()];
  441. }
  442. /**
  443. * @inheritdoc
  444. * @since 100.0.7
  445. */
  446. public function __sleep()
  447. {
  448. $this->unsetData('attribute_set_info');
  449. return array_diff(
  450. parent::__sleep(),
  451. ['_localeDate', '_localeResolver', 'reservedAttributeList', 'dateTimeFormatter']
  452. );
  453. }
  454. /**
  455. * @inheritdoc
  456. * @since 100.0.7
  457. */
  458. public function __wakeup()
  459. {
  460. parent::__wakeup();
  461. $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
  462. $this->_localeDate = $objectManager->get(\Magento\Framework\Stdlib\DateTime\TimezoneInterface::class);
  463. $this->_localeResolver = $objectManager->get(\Magento\Framework\Locale\ResolverInterface::class);
  464. $this->reservedAttributeList = $objectManager->get(\Magento\Catalog\Model\Product\ReservedAttributeList::class);
  465. $this->dateTimeFormatter = $objectManager->get(DateTimeFormatterInterface::class);
  466. }
  467. }