AbstractResource.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Catalog\Model\ResourceModel;
  7. use Magento\Eav\Model\Entity\Attribute\AbstractAttribute;
  8. use Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend;
  9. use Magento\Eav\Model\Entity\Attribute\Frontend\AbstractFrontend;
  10. use Magento\Eav\Model\Entity\Attribute\Source\AbstractSource;
  11. use Magento\Eav\Model\Entity\Attribute\UniqueValidationInterface;
  12. /**
  13. * Catalog entity abstract model
  14. *
  15. * @api
  16. *
  17. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  18. * @since 100.0.2
  19. */
  20. abstract class AbstractResource extends \Magento\Eav\Model\Entity\AbstractEntity
  21. {
  22. /**
  23. * Store manager
  24. *
  25. * @var \Magento\Store\Model\StoreManagerInterface
  26. */
  27. protected $_storeManager;
  28. /**
  29. * Model factory
  30. *
  31. * @var \Magento\Catalog\Model\Factory
  32. */
  33. protected $_modelFactory;
  34. /**
  35. * @param \Magento\Eav\Model\Entity\Context $context
  36. * @param \Magento\Store\Model\StoreManagerInterface $storeManager
  37. * @param \Magento\Catalog\Model\Factory $modelFactory
  38. * @param array $data
  39. * @param UniqueValidationInterface|null $uniqueValidator
  40. */
  41. public function __construct(
  42. \Magento\Eav\Model\Entity\Context $context,
  43. \Magento\Store\Model\StoreManagerInterface $storeManager,
  44. \Magento\Catalog\Model\Factory $modelFactory,
  45. $data = [],
  46. UniqueValidationInterface $uniqueValidator = null
  47. ) {
  48. $this->_storeManager = $storeManager;
  49. $this->_modelFactory = $modelFactory;
  50. parent::__construct($context, $data, $uniqueValidator);
  51. }
  52. /**
  53. * Re-declare attribute model
  54. *
  55. * @return string
  56. */
  57. protected function _getDefaultAttributeModel()
  58. {
  59. return \Magento\Catalog\Model\ResourceModel\Eav\Attribute::class;
  60. }
  61. /**
  62. * Returns default Store ID
  63. *
  64. * @return int
  65. */
  66. public function getDefaultStoreId()
  67. {
  68. return \Magento\Store\Model\Store::DEFAULT_STORE_ID;
  69. }
  70. /**
  71. * Check whether the attribute is Applicable to the object
  72. *
  73. * @param \Magento\Framework\DataObject $object
  74. * @param \Magento\Catalog\Model\ResourceModel\Eav\Attribute $attribute
  75. * @return boolean
  76. */
  77. protected function _isApplicableAttribute($object, $attribute)
  78. {
  79. $applyTo = $attribute->getApplyTo() ?: [];
  80. return (count($applyTo) == 0 || in_array($object->getTypeId(), $applyTo))
  81. && $attribute->isInSet($object->getAttributeSetId());
  82. }
  83. /**
  84. * Check whether attribute instance (attribute, backend, frontend or source) has method and applicable
  85. *
  86. * @param AbstractAttribute|AbstractBackend|AbstractFrontend|AbstractSource $instance
  87. * @param string $method
  88. * @param array $args array of arguments
  89. * @return boolean
  90. */
  91. protected function _isCallableAttributeInstance($instance, $method, $args)
  92. {
  93. if ($instance instanceof \Magento\Eav\Model\Entity\Attribute\Backend\AbstractBackend
  94. && ($method == 'beforeSave' || $method == 'afterSave')
  95. ) {
  96. $attributeCode = $instance->getAttribute()->getAttributeCode();
  97. if (isset($args[0])
  98. && $args[0] instanceof \Magento\Framework\DataObject
  99. && $args[0]->getData($attributeCode) === false
  100. ) {
  101. return false;
  102. }
  103. }
  104. return parent::_isCallableAttributeInstance($instance, $method, $args);
  105. }
  106. /**
  107. * Retrieve select object for loading entity attributes values
  108. *
  109. * Join attribute store value
  110. *
  111. * @param \Magento\Framework\DataObject $object
  112. * @param string $table
  113. * @return \Magento\Framework\DB\Select
  114. */
  115. protected function _getLoadAttributesSelect($object, $table)
  116. {
  117. /**
  118. * This condition is applicable for all cases when we was work in not single
  119. * store mode, customize some value per specific store view and than back
  120. * to single store mode. We should load correct values
  121. */
  122. if ($this->_storeManager->hasSingleStore()) {
  123. $storeId = (int) $this->_storeManager->getStore(true)->getId();
  124. } else {
  125. $storeId = (int) $object->getStoreId();
  126. }
  127. $setId = $object->getAttributeSetId();
  128. $storeIds = [$this->getDefaultStoreId()];
  129. if ($storeId != $this->getDefaultStoreId()) {
  130. $storeIds[] = $storeId;
  131. }
  132. $select = $this->getConnection()
  133. ->select()
  134. ->from(['attr_table' => $table], [])
  135. ->where("attr_table.{$this->getLinkField()} = ?", $object->getData($this->getLinkField()))
  136. ->where('attr_table.store_id IN (?)', $storeIds);
  137. if ($setId) {
  138. $select->join(
  139. ['set_table' => $this->getTable('eav_entity_attribute')],
  140. $this->getConnection()->quoteInto(
  141. 'attr_table.attribute_id = set_table.attribute_id' . ' AND set_table.attribute_set_id = ?',
  142. $setId
  143. ),
  144. []
  145. );
  146. }
  147. return $select;
  148. }
  149. /**
  150. * Prepare select object for loading entity attributes values
  151. *
  152. * @param array $selects
  153. * @return \Magento\Framework\DB\Select
  154. */
  155. protected function _prepareLoadSelect(array $selects)
  156. {
  157. $select = parent::_prepareLoadSelect($selects);
  158. $select->order('store_id');
  159. return $select;
  160. }
  161. /**
  162. * Insert or Update attribute data
  163. *
  164. * @param \Magento\Catalog\Model\AbstractModel $object
  165. * @param AbstractAttribute $attribute
  166. * @param mixed $value
  167. * @return $this
  168. */
  169. protected function _saveAttributeValue($object, $attribute, $value)
  170. {
  171. $connection = $this->getConnection();
  172. $hasSingleStore = $this->_storeManager->hasSingleStore();
  173. $storeId = $hasSingleStore
  174. ? $this->getDefaultStoreId()
  175. : (int) $this->_storeManager->getStore($object->getStoreId())->getId();
  176. $table = $attribute->getBackend()->getTable();
  177. /**
  178. * If we work in single store mode all values should be saved just
  179. * for default store id
  180. * In this case we clear all not default values
  181. */
  182. $entityIdField = $this->getLinkField();
  183. $conditions = [
  184. 'attribute_id = ?' => $attribute->getAttributeId(),
  185. "{$entityIdField} = ?" => $object->getData($entityIdField),
  186. 'store_id <> ?' => $storeId
  187. ];
  188. if ($hasSingleStore
  189. && !$object->isObjectNew()
  190. && $this->isAttributePresentForNonDefaultStore($attribute, $conditions)
  191. ) {
  192. $connection->delete(
  193. $table,
  194. $conditions
  195. );
  196. }
  197. $data = new \Magento\Framework\DataObject(
  198. [
  199. 'attribute_id' => $attribute->getAttributeId(),
  200. 'store_id' => $storeId,
  201. $entityIdField => $object->getData($entityIdField),
  202. 'value' => $this->_prepareValueForSave($value, $attribute),
  203. ]
  204. );
  205. $bind = $this->_prepareDataForTable($data, $table);
  206. if ($attribute->isScopeStore()) {
  207. /**
  208. * Update attribute value for store
  209. */
  210. $this->_attributeValuesToSave[$table][] = $bind;
  211. } elseif ($attribute->isScopeWebsite() && $storeId != $this->getDefaultStoreId()) {
  212. /**
  213. * Update attribute value for website
  214. */
  215. $storeIds = $this->_storeManager->getStore($storeId)->getWebsite()->getStoreIds(true);
  216. foreach ($storeIds as $storeId) {
  217. $bind['store_id'] = (int) $storeId;
  218. $this->_attributeValuesToSave[$table][] = $bind;
  219. }
  220. } else {
  221. /**
  222. * Update global attribute value
  223. */
  224. $bind['store_id'] = $this->getDefaultStoreId();
  225. $this->_attributeValuesToSave[$table][] = $bind;
  226. }
  227. return $this;
  228. }
  229. /**
  230. * Check if attribute present for non default Store View.
  231. *
  232. * Prevent "delete" query locking in a case when nothing to delete
  233. *
  234. * @param AbstractAttribute $attribute
  235. * @param array $conditions
  236. *
  237. * @return boolean
  238. */
  239. private function isAttributePresentForNonDefaultStore($attribute, $conditions)
  240. {
  241. $connection = $this->getConnection();
  242. $select = $connection->select()->from($attribute->getBackend()->getTable());
  243. foreach ($conditions as $condition => $conditionValue) {
  244. $select->where($condition, $conditionValue);
  245. }
  246. $select->limit(1);
  247. return !empty($connection->fetchRow($select));
  248. }
  249. /**
  250. * Insert entity attribute value
  251. *
  252. * @param \Magento\Framework\DataObject $object
  253. * @param AbstractAttribute $attribute
  254. * @param mixed $value
  255. * @return $this
  256. */
  257. protected function _insertAttribute($object, $attribute, $value)
  258. {
  259. /**
  260. * save required attributes in global scope every time if store id different from default
  261. */
  262. $storeId = (int) $this->_storeManager->getStore($object->getStoreId())->getId();
  263. if ($this->getDefaultStoreId() != $storeId) {
  264. if ($attribute->getIsRequired() || $attribute->getIsRequiredInAdminStore()) {
  265. $table = $attribute->getBackend()->getTable();
  266. $select = $this->getConnection()->select()
  267. ->from($table)
  268. ->where('attribute_id = ?', $attribute->getAttributeId())
  269. ->where('store_id = ?', $this->getDefaultStoreId())
  270. ->where($this->getLinkField() . ' = ?', $object->getData($this->getLinkField()));
  271. $row = $this->getConnection()->fetchOne($select);
  272. if (!$row) {
  273. $data = new \Magento\Framework\DataObject(
  274. [
  275. 'attribute_id' => $attribute->getAttributeId(),
  276. 'store_id' => $this->getDefaultStoreId(),
  277. $this->getLinkField() => $object->getData($this->getLinkField()),
  278. 'value' => $this->_prepareValueForSave($value, $attribute),
  279. ]
  280. );
  281. $bind = $this->_prepareDataForTable($data, $table);
  282. $this->getConnection()->insertOnDuplicate($table, $bind, ['value']);
  283. }
  284. }
  285. }
  286. return $this->_saveAttributeValue($object, $attribute, $value);
  287. }
  288. /**
  289. * Update entity attribute value
  290. *
  291. * @param \Magento\Framework\DataObject $object
  292. * @param AbstractAttribute $attribute
  293. * @param mixed $valueId
  294. * @param mixed $value
  295. * @return $this
  296. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  297. */
  298. protected function _updateAttribute($object, $attribute, $valueId, $value)
  299. {
  300. return $this->_saveAttributeValue($object, $attribute, $value);
  301. }
  302. /**
  303. * Update attribute value for specific store
  304. *
  305. * @param \Magento\Catalog\Model\AbstractModel $object
  306. * @param object $attribute
  307. * @param mixed $value
  308. * @param int $storeId
  309. * @return $this
  310. */
  311. protected function _updateAttributeForStore($object, $attribute, $value, $storeId)
  312. {
  313. $connection = $this->getConnection();
  314. $table = $attribute->getBackend()->getTable();
  315. $entityIdField = $this->getLinkField();
  316. $select = $connection->select()
  317. ->from($table, 'value_id')
  318. ->where("$entityIdField = :entity_field_id")
  319. ->where('store_id = :store_id')
  320. ->where('attribute_id = :attribute_id');
  321. $bind = [
  322. 'entity_field_id' => $object->getId(),
  323. 'store_id' => $storeId,
  324. 'attribute_id' => $attribute->getId(),
  325. ];
  326. $valueId = $connection->fetchOne($select, $bind);
  327. /**
  328. * When value for store exist
  329. */
  330. if ($valueId) {
  331. $bind = ['value' => $this->_prepareValueForSave($value, $attribute)];
  332. $where = ['value_id = ?' => (int) $valueId];
  333. $connection->update($table, $bind, $where);
  334. } else {
  335. $bind = [
  336. $entityIdField => (int) $object->getId(),
  337. 'attribute_id' => (int) $attribute->getId(),
  338. 'value' => $this->_prepareValueForSave($value, $attribute),
  339. 'store_id' => (int) $storeId,
  340. ];
  341. $connection->insert($table, $bind);
  342. }
  343. return $this;
  344. }
  345. /**
  346. * Delete entity attribute values
  347. *
  348. * @param \Magento\Framework\DataObject $object
  349. * @param string $table
  350. * @param array $info
  351. * @return $this
  352. */
  353. protected function _deleteAttributes($object, $table, $info)
  354. {
  355. $connection = $this->getConnection();
  356. $entityIdField = $this->getLinkField();
  357. $globalValues = [];
  358. $websiteAttributes = [];
  359. $storeAttributes = [];
  360. /**
  361. * Separate attributes by scope
  362. */
  363. foreach ($info as $itemData) {
  364. $attribute = $this->getAttribute($itemData['attribute_id']);
  365. if ($attribute->isScopeStore()) {
  366. $storeAttributes[] = (int) $itemData['attribute_id'];
  367. } elseif ($attribute->isScopeWebsite()) {
  368. $websiteAttributes[] = (int) $itemData['attribute_id'];
  369. } elseif ($itemData['value_id'] !== null) {
  370. $globalValues[] = (int) $itemData['value_id'];
  371. }
  372. }
  373. /**
  374. * Delete global scope attributes
  375. */
  376. if (!empty($globalValues)) {
  377. $connection->delete($table, ['value_id IN (?)' => $globalValues]);
  378. }
  379. $condition = [
  380. $entityIdField . ' = ?' => $object->getId(),
  381. ];
  382. /**
  383. * Delete website scope attributes
  384. */
  385. if (!empty($websiteAttributes)) {
  386. $storeIds = $object->getWebsiteStoreIds();
  387. if (!empty($storeIds)) {
  388. $delCondition = $condition;
  389. $delCondition['attribute_id IN(?)'] = $websiteAttributes;
  390. $delCondition['store_id IN(?)'] = $storeIds;
  391. $connection->delete($table, $delCondition);
  392. }
  393. }
  394. /**
  395. * Delete store scope attributes
  396. */
  397. if (!empty($storeAttributes)) {
  398. $delCondition = $condition;
  399. $delCondition['attribute_id IN(?)'] = $storeAttributes;
  400. $delCondition['store_id = ?'] = (int) $object->getStoreId();
  401. $connection->delete($table, $delCondition);
  402. }
  403. return $this;
  404. }
  405. /**
  406. * Retrieve Object instance with original data
  407. *
  408. * @param \Magento\Framework\DataObject $object
  409. * @return \Magento\Framework\DataObject
  410. */
  411. protected function _getOrigObject($object)
  412. {
  413. //TODO:
  414. $className = get_class($object);
  415. $origObject = $this->_modelFactory->create($className);
  416. $origObject->setData([]);
  417. $origObject->setStoreId($object->getStoreId());
  418. $this->load($origObject, $object->getData($this->getEntityIdField()));
  419. return $origObject;
  420. }
  421. /**
  422. * Return if attribute exists in original data array.
  423. * Checks also attribute's store scope:
  424. * We should insert on duplicate key update values if we unchecked 'STORE VIEW' checkbox in store view.
  425. *
  426. * @param AbstractAttribute $attribute
  427. * @param mixed $value New value of the attribute.
  428. * @param array &$origData
  429. * @return bool
  430. */
  431. protected function _canUpdateAttribute(AbstractAttribute $attribute, $value, array &$origData)
  432. {
  433. $result = parent::_canUpdateAttribute($attribute, $value, $origData);
  434. if ($result
  435. && ($attribute->isScopeStore() || $attribute->isScopeWebsite())
  436. && !$this->_isAttributeValueEmpty($attribute, $value)
  437. && $value == $origData[$attribute->getAttributeCode()]
  438. && isset($origData['store_id'])
  439. && $origData['store_id'] != $this->getDefaultStoreId()
  440. ) {
  441. return false;
  442. }
  443. return $result;
  444. }
  445. /**
  446. * Retrieve attribute's raw value from DB.
  447. *
  448. * @param int $entityId
  449. * @param int|string|array $attribute atrribute's ids or codes
  450. * @param int|\Magento\Store\Model\Store $store
  451. * @return bool|string|array
  452. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  453. * @SuppressWarnings(PHPMD.NPathComplexity)
  454. * @SuppressWarnings(PHPMD.ExcessiveMethodLength)
  455. */
  456. public function getAttributeRawValue($entityId, $attribute, $store)
  457. {
  458. if (!$entityId || empty($attribute)) {
  459. return false;
  460. }
  461. if (!is_array($attribute)) {
  462. $attribute = [$attribute];
  463. }
  464. $attributesData = [];
  465. $staticAttributes = [];
  466. $typedAttributes = [];
  467. $staticTable = null;
  468. $connection = $this->getConnection();
  469. foreach ($attribute as $item) {
  470. /* @var $attribute \Magento\Catalog\Model\Entity\Attribute */
  471. $item = $this->getAttribute($item);
  472. if (!$item) {
  473. continue;
  474. }
  475. $attributeCode = $item->getAttributeCode();
  476. $attrTable = $item->getBackend()->getTable();
  477. $isStatic = $item->getBackend()->isStatic();
  478. if ($isStatic) {
  479. $staticAttributes[] = $attributeCode;
  480. $staticTable = $attrTable;
  481. } else {
  482. /**
  483. * That structure needed to avoid farther sql joins for getting attribute's code by id
  484. */
  485. $typedAttributes[$attrTable][$item->getId()] = $attributeCode;
  486. }
  487. }
  488. /**
  489. * Collecting static attributes
  490. */
  491. if ($staticAttributes) {
  492. $select = $connection->select()->from(
  493. $staticTable,
  494. $staticAttributes
  495. )->join(
  496. ['e' => $this->getTable($this->getEntityTable())],
  497. 'e.' . $this->getLinkField() . ' = ' . $staticTable . '.' . $this->getLinkField()
  498. )->where(
  499. 'e.entity_id = :entity_id'
  500. );
  501. $attributesData = $connection->fetchRow($select, ['entity_id' => $entityId]);
  502. }
  503. /**
  504. * Collecting typed attributes, performing separate SQL query for each attribute type table
  505. */
  506. if ($store instanceof \Magento\Store\Model\Store) {
  507. $store = $store->getId();
  508. }
  509. $store = (int) $store;
  510. if ($typedAttributes) {
  511. foreach ($typedAttributes as $table => $_attributes) {
  512. $select = $connection->select()
  513. ->from(['default_value' => $table], ['attribute_id'])
  514. ->join(
  515. ['e' => $this->getTable($this->getEntityTable())],
  516. 'e.' . $this->getLinkField() . ' = ' . 'default_value.' . $this->getLinkField(),
  517. ''
  518. )->where('default_value.attribute_id IN (?)', array_keys($_attributes))
  519. ->where("e.entity_id = :entity_id")
  520. ->where('default_value.store_id = ?', 0);
  521. $bind = ['entity_id' => $entityId];
  522. if ($store != $this->getDefaultStoreId()) {
  523. $valueExpr = $connection->getCheckSql(
  524. 'store_value.value IS NULL',
  525. 'default_value.value',
  526. 'store_value.value'
  527. );
  528. $joinCondition = [
  529. $connection->quoteInto('store_value.attribute_id IN (?)', array_keys($_attributes)),
  530. "store_value.{$this->getLinkField()} = e.{$this->getLinkField()}",
  531. 'store_value.store_id = :store_id',
  532. ];
  533. $select->joinLeft(
  534. ['store_value' => $table],
  535. implode(' AND ', $joinCondition),
  536. ['attr_value' => $valueExpr]
  537. );
  538. $bind['store_id'] = $store;
  539. } else {
  540. $select->columns(['attr_value' => 'value'], 'default_value');
  541. }
  542. $result = $connection->fetchPairs($select, $bind);
  543. foreach ($result as $attrId => $value) {
  544. $attrCode = $typedAttributes[$table][$attrId];
  545. $attributesData[$attrCode] = $value;
  546. }
  547. }
  548. }
  549. if (is_array($attributesData) && sizeof($attributesData) == 1) {
  550. $attributesData = array_shift($attributesData);
  551. }
  552. return $attributesData === false ? false : $attributesData;
  553. }
  554. }