AbstractProduct.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Rule\Model\Condition\Product;
  7. use Magento\Catalog\Model\ProductCategoryList;
  8. use Magento\Framework\App\ObjectManager;
  9. /**
  10. * Abstract Rule product condition data model
  11. *
  12. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  13. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  14. * @api
  15. * @since 100.0.2
  16. */
  17. abstract class AbstractProduct extends \Magento\Rule\Model\Condition\AbstractCondition
  18. {
  19. /**
  20. * All attribute values as array in form:
  21. * array(
  22. * [entity_id_1] => array(
  23. * [store_id_1] => store_value_1,
  24. * [store_id_2] => store_value_2,
  25. * ...
  26. * [store_id_n] => store_value_n
  27. * ),
  28. * ...
  29. * )
  30. *
  31. * Will be set only for not global scope attribute
  32. *
  33. * @var array
  34. */
  35. protected $_entityAttributeValues = null;
  36. /**
  37. * Attribute data key that indicates whether it should be used for rules
  38. *
  39. * @var string
  40. */
  41. protected $_isUsedForRuleProperty = 'is_used_for_promo_rules';
  42. /**
  43. * Adminhtml data
  44. *
  45. * @var \Magento\Backend\Helper\Data
  46. */
  47. protected $_backendData;
  48. /**
  49. * @var \Magento\Eav\Model\Config
  50. */
  51. protected $_config;
  52. /**
  53. * @var \Magento\Catalog\Model\ProductFactory
  54. */
  55. protected $_productFactory;
  56. /**
  57. * @var \Magento\Catalog\Api\ProductRepositoryInterface
  58. */
  59. protected $productRepository;
  60. /**
  61. * @var \Magento\Catalog\Model\ResourceModel\Product
  62. */
  63. protected $_productResource;
  64. /**
  65. * @var \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection
  66. */
  67. protected $_attrSetCollection;
  68. /**
  69. * @var \Magento\Framework\Locale\FormatInterface
  70. */
  71. protected $_localeFormat;
  72. /**
  73. * @var ProductCategoryList
  74. */
  75. private $productCategoryList;
  76. /**
  77. * @param \Magento\Rule\Model\Condition\Context $context
  78. * @param \Magento\Backend\Helper\Data $backendData
  79. * @param \Magento\Eav\Model\Config $config
  80. * @param \Magento\Catalog\Model\ProductFactory $productFactory
  81. * @param \Magento\Catalog\Api\ProductRepositoryInterface $productRepository
  82. * @param \Magento\Catalog\Model\ResourceModel\Product $productResource
  83. * @param \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection
  84. * @param \Magento\Framework\Locale\FormatInterface $localeFormat
  85. * @param ProductCategoryList|null $categoryList
  86. * @param array $data
  87. *
  88. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  89. */
  90. public function __construct(
  91. \Magento\Rule\Model\Condition\Context $context,
  92. \Magento\Backend\Helper\Data $backendData,
  93. \Magento\Eav\Model\Config $config,
  94. \Magento\Catalog\Model\ProductFactory $productFactory,
  95. \Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
  96. \Magento\Catalog\Model\ResourceModel\Product $productResource,
  97. \Magento\Eav\Model\ResourceModel\Entity\Attribute\Set\Collection $attrSetCollection,
  98. \Magento\Framework\Locale\FormatInterface $localeFormat,
  99. array $data = [],
  100. ProductCategoryList $categoryList = null
  101. ) {
  102. $this->_backendData = $backendData;
  103. $this->_config = $config;
  104. $this->_productFactory = $productFactory;
  105. $this->productRepository = $productRepository;
  106. $this->_productResource = $productResource;
  107. $this->_attrSetCollection = $attrSetCollection;
  108. $this->_localeFormat = $localeFormat;
  109. $this->productCategoryList = $categoryList ?: ObjectManager::getInstance()->get(ProductCategoryList::class);
  110. parent::__construct($context, $data);
  111. }
  112. /**
  113. * Customize default operator input by type mapper for some types
  114. *
  115. * @return array
  116. */
  117. public function getDefaultOperatorInputByType()
  118. {
  119. if (null === $this->_defaultOperatorInputByType) {
  120. parent::getDefaultOperatorInputByType();
  121. /*
  122. * '{}' and '!{}' are left for back-compatibility and equal to '==' and '!='
  123. */
  124. $this->_defaultOperatorInputByType['category'] = ['==', '!=', '{}', '!{}', '()', '!()'];
  125. $this->_arrayInputTypes[] = 'category';
  126. }
  127. return $this->_defaultOperatorInputByType;
  128. }
  129. /**
  130. * Retrieve attribute object
  131. *
  132. * @return \Magento\Catalog\Model\ResourceModel\Eav\Attribute
  133. */
  134. public function getAttributeObject()
  135. {
  136. try {
  137. $obj = $this->_config->getAttribute(\Magento\Catalog\Model\Product::ENTITY, $this->getAttribute());
  138. } catch (\Exception $e) {
  139. $obj = new \Magento\Framework\DataObject();
  140. $obj->setEntity($this->_productFactory->create())->setFrontendInput('text');
  141. }
  142. return $obj;
  143. }
  144. /**
  145. * Add special attributes
  146. *
  147. * @param array &$attributes
  148. * @return void
  149. */
  150. protected function _addSpecialAttributes(array &$attributes)
  151. {
  152. $attributes['attribute_set_id'] = __('Attribute Set');
  153. $attributes['category_ids'] = __('Category');
  154. }
  155. /**
  156. * Load attribute options
  157. *
  158. * @return $this
  159. */
  160. public function loadAttributeOptions()
  161. {
  162. $productAttributes = $this->_productResource->loadAllAttributes()->getAttributesByCode();
  163. $attributes = [];
  164. foreach ($productAttributes as $attribute) {
  165. /* @var $attribute \Magento\Catalog\Model\ResourceModel\Eav\Attribute */
  166. if (!$attribute->isAllowedForRuleCondition() || !$attribute->getDataUsingMethod(
  167. $this->_isUsedForRuleProperty
  168. )
  169. ) {
  170. continue;
  171. }
  172. $attributes[$attribute->getAttributeCode()] = $attribute->getFrontendLabel();
  173. }
  174. $this->_addSpecialAttributes($attributes);
  175. asort($attributes);
  176. $this->setAttributeOption($attributes);
  177. return $this;
  178. }
  179. /**
  180. * Prepares values options to be used as select options or hashed array
  181. * Result is stored in following keys:
  182. * 'value_select_options' - normal select array: array(array('value' => $value, 'label' => $label), ...)
  183. * 'value_option' - hashed array: array($value => $label, ...),
  184. *
  185. * @return $this
  186. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  187. */
  188. protected function _prepareValueOptions()
  189. {
  190. // Check that both keys exist. Maybe somehow only one was set not in this routine, but externally.
  191. $selectReady = $this->getData('value_select_options');
  192. $hashedReady = $this->getData('value_option');
  193. if ($selectReady && $hashedReady) {
  194. return $this;
  195. }
  196. // Get array of select options. It will be used as source for hashed options
  197. $selectOptions = null;
  198. if ($this->getAttribute() === 'attribute_set_id') {
  199. $entityTypeId = $this->_config->getEntityType(\Magento\Catalog\Model\Product::ENTITY)->getId();
  200. $selectOptions = $this->_attrSetCollection
  201. ->setEntityTypeFilter($entityTypeId)
  202. ->load()
  203. ->toOptionArray();
  204. } elseif ($this->getAttribute() === 'type_id') {
  205. foreach ($selectReady as $value => $label) {
  206. if (is_array($label) && isset($label['value'])) {
  207. $selectOptions[] = $label;
  208. } else {
  209. $selectOptions[] = ['value' => $value, 'label' => $label];
  210. }
  211. }
  212. $selectReady = null;
  213. } elseif (is_object($this->getAttributeObject())) {
  214. $attributeObject = $this->getAttributeObject();
  215. if ($attributeObject->usesSource()) {
  216. if ($attributeObject->getFrontendInput() == 'multiselect') {
  217. $addEmptyOption = false;
  218. } else {
  219. $addEmptyOption = true;
  220. }
  221. $selectOptions = $attributeObject->getSource()->getAllOptions($addEmptyOption);
  222. }
  223. }
  224. $this->_setSelectOptions($selectOptions, $selectReady, $hashedReady);
  225. return $this;
  226. }
  227. /**
  228. * Set new values only if we really got them
  229. *
  230. * @param array $selectOptions
  231. * @param array $selectReady
  232. * @param array $hashedReady
  233. * @return $this
  234. */
  235. protected function _setSelectOptions($selectOptions, $selectReady, $hashedReady)
  236. {
  237. if ($selectOptions !== null) {
  238. // Overwrite only not already existing values
  239. if (!$selectReady) {
  240. $this->setData('value_select_options', $selectOptions);
  241. }
  242. if (!$hashedReady) {
  243. $hashedOptions = [];
  244. foreach ($selectOptions as $option) {
  245. if (is_array($option['value'])) {
  246. continue; // We cannot use array as index
  247. }
  248. $hashedOptions[$option['value']] = $option['label'];
  249. }
  250. $this->setData('value_option', $hashedOptions);
  251. }
  252. }
  253. return $this;
  254. }
  255. /**
  256. * Retrieve value by option
  257. *
  258. * @param string|null $option
  259. * @return string
  260. */
  261. public function getValueOption($option = null)
  262. {
  263. $this->_prepareValueOptions();
  264. return $this->getData('value_option' . ($option !== null ? '/' . $option : ''));
  265. }
  266. /**
  267. * Retrieve select option values
  268. *
  269. * @return array
  270. */
  271. public function getValueSelectOptions()
  272. {
  273. $this->_prepareValueOptions();
  274. return $this->getData('value_select_options');
  275. }
  276. /**
  277. * Retrieve after element HTML
  278. *
  279. * @return string
  280. */
  281. public function getValueAfterElementHtml()
  282. {
  283. $html = '';
  284. switch ($this->getAttribute()) {
  285. case 'sku':
  286. case 'category_ids':
  287. $image = $this->_assetRepo->getUrl('images/rule_chooser_trigger.gif');
  288. break;
  289. }
  290. if (!empty($image)) {
  291. $html = '<a href="javascript:void(0)" class="rule-chooser-trigger"><img src="' .
  292. $image .
  293. '" alt="" class="v-middle rule-chooser-trigger" title="' .
  294. __(
  295. 'Open Chooser'
  296. ) . '" /></a>';
  297. }
  298. return $html;
  299. }
  300. /**
  301. * Retrieve attribute element
  302. *
  303. * @return \Magento\Framework\Data\Form\Element\AbstractElement
  304. */
  305. public function getAttributeElement()
  306. {
  307. $element = parent::getAttributeElement();
  308. $element->setShowAsText(true);
  309. return $element;
  310. }
  311. /**
  312. * Collect validated attributes
  313. *
  314. * @param \Magento\Catalog\Model\ResourceModel\Product\Collection $productCollection
  315. * @return $this
  316. */
  317. public function collectValidatedAttributes($productCollection)
  318. {
  319. $attribute = $this->getAttribute();
  320. if ('category_ids' != $attribute) {
  321. $productCollection->addAttributeToSelect($attribute, 'left');
  322. if ($this->getAttributeObject()->isScopeGlobal()) {
  323. $attributes = $this->getRule()->getCollectedAttributes();
  324. $attributes[$attribute] = true;
  325. $this->getRule()->setCollectedAttributes($attributes);
  326. } else {
  327. $this->_entityAttributeValues = $productCollection->getAllAttributeValues($attribute);
  328. }
  329. }
  330. return $this;
  331. }
  332. /**
  333. * Retrieve input type
  334. *
  335. * @return string
  336. */
  337. public function getInputType()
  338. {
  339. if ($this->getAttribute() === 'attribute_set_id') {
  340. return 'select';
  341. }
  342. if (!is_object($this->getAttributeObject())) {
  343. return 'string';
  344. }
  345. if ($this->getAttributeObject()->getAttributeCode() == 'category_ids') {
  346. return 'category';
  347. }
  348. switch ($this->getAttributeObject()->getFrontendInput()) {
  349. case 'select':
  350. return 'select';
  351. case 'multiselect':
  352. return 'multiselect';
  353. case 'date':
  354. return 'date';
  355. case 'boolean':
  356. return 'boolean';
  357. default:
  358. return 'string';
  359. }
  360. }
  361. /**
  362. * Retrieve value element type
  363. *
  364. * @return string
  365. */
  366. public function getValueElementType()
  367. {
  368. if ($this->getAttribute() === 'attribute_set_id') {
  369. return 'select';
  370. }
  371. if (!is_object($this->getAttributeObject())) {
  372. return 'text';
  373. }
  374. switch ($this->getAttributeObject()->getFrontendInput()) {
  375. case 'select':
  376. case 'boolean':
  377. return 'select';
  378. case 'multiselect':
  379. return 'multiselect';
  380. case 'date':
  381. return 'date';
  382. default:
  383. return 'text';
  384. }
  385. }
  386. /**
  387. * Retrieve value element chooser URL
  388. *
  389. * @return string
  390. */
  391. public function getValueElementChooserUrl()
  392. {
  393. $url = false;
  394. switch ($this->getAttribute()) {
  395. case 'sku':
  396. case 'category_ids':
  397. $url = 'catalog_rule/promo_widget/chooser/attribute/' . $this->getAttribute();
  398. if ($this->getJsFormObject()) {
  399. $url .= '/form/' . $this->getJsFormObject();
  400. }
  401. break;
  402. default:
  403. break;
  404. }
  405. return $url !== false ? $this->_backendData->getUrl($url) : '';
  406. }
  407. /**
  408. * Retrieve Explicit Apply
  409. *
  410. * @return bool
  411. * @SuppressWarnings(PHPMD.BooleanGetMethodName)
  412. */
  413. public function getExplicitApply()
  414. {
  415. switch ($this->getAttribute()) {
  416. case 'sku':
  417. case 'category_ids':
  418. return true;
  419. default:
  420. break;
  421. }
  422. if (is_object($this->getAttributeObject())) {
  423. switch ($this->getAttributeObject()->getFrontendInput()) {
  424. case 'date':
  425. return true;
  426. default:
  427. break;
  428. }
  429. }
  430. return false;
  431. }
  432. /**
  433. * Load array
  434. *
  435. * @param array $arr
  436. * @return $this
  437. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  438. */
  439. public function loadArray($arr)
  440. {
  441. $this->setAttribute(isset($arr['attribute']) ? $arr['attribute'] : false);
  442. $attribute = $this->getAttributeObject();
  443. $isContainsOperator = !empty($arr['operator']) && in_array($arr['operator'], ['{}', '!{}']);
  444. if ($attribute && $attribute->getBackendType() == 'decimal' && !$isContainsOperator) {
  445. if (isset($arr['value'])) {
  446. if (!empty($arr['operator']) && in_array(
  447. $arr['operator'],
  448. ['!()', '()']
  449. ) && false !== strpos(
  450. $arr['value'],
  451. ','
  452. )
  453. ) {
  454. $tmp = [];
  455. foreach (explode(',', $arr['value']) as $value) {
  456. $tmp[] = $this->_localeFormat->getNumber($value);
  457. }
  458. $arr['value'] = implode(',', $tmp);
  459. } else {
  460. $arr['value'] = $this->_localeFormat->getNumber($arr['value']);
  461. }
  462. } else {
  463. $arr['value'] = false;
  464. }
  465. $arr['is_value_parsed'] = isset(
  466. $arr['is_value_parsed']
  467. ) ? $this->_localeFormat->getNumber(
  468. $arr['is_value_parsed']
  469. ) : false;
  470. }
  471. return parent::loadArray($arr);
  472. }
  473. /**
  474. * Validate product attribute value for condition
  475. *
  476. * @param \Magento\Framework\Model\AbstractModel $model
  477. * @return bool
  478. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  479. * @SuppressWarnings(PHPMD.NPathComplexity)
  480. */
  481. public function validate(\Magento\Framework\Model\AbstractModel $model)
  482. {
  483. $attrCode = $this->getAttribute();
  484. if ('category_ids' == $attrCode) {
  485. $productId = (int)$model->getEntityId();
  486. return $this->validateAttribute($this->productCategoryList->getCategoryIds($productId));
  487. } elseif (!isset($this->_entityAttributeValues[$model->getId()])) {
  488. if (!$model->getResource()) {
  489. return false;
  490. }
  491. $attr = $model->getResource()->getAttribute($attrCode);
  492. if ($attr && $attr->getBackendType() == 'datetime' && !is_int($this->getValue())) {
  493. $this->setValue(strtotime($this->getValue()));
  494. $value = strtotime($model->getData($attrCode));
  495. return $this->validateAttribute($value);
  496. }
  497. if ($attr && $attr->getFrontendInput() == 'multiselect') {
  498. $value = $model->getData($attrCode);
  499. $value = strlen($value) ? explode(',', $value) : [];
  500. return $this->validateAttribute($value);
  501. }
  502. return parent::validate($model);
  503. } else {
  504. $result = false;
  505. // any valid value will set it to TRUE
  506. // remember old attribute state
  507. $oldAttrValue = $model->hasData($attrCode) ? $model->getData($attrCode) : null;
  508. foreach ($this->_entityAttributeValues[$model->getId()] as $value) {
  509. $attr = $model->getResource()->getAttribute($attrCode);
  510. if ($attr && $attr->getBackendType() == 'datetime') {
  511. $value = strtotime($value);
  512. } elseif ($attr && $attr->getFrontendInput() == 'multiselect') {
  513. $value = strlen($value) ? explode(',', $value) : [];
  514. }
  515. $model->setData($attrCode, $value);
  516. $result |= parent::validate($model);
  517. if ($result) {
  518. break;
  519. }
  520. }
  521. if ($oldAttrValue === null) {
  522. $model->unsetData($attrCode);
  523. } else {
  524. $model->setData($attrCode, $oldAttrValue);
  525. }
  526. return (bool)$result;
  527. }
  528. }
  529. /**
  530. * Get argument value to bind
  531. *
  532. * @return array|float|int|mixed|string|\Zend_Db_Expr
  533. */
  534. public function getBindArgumentValue()
  535. {
  536. if ($this->getAttribute() == 'category_ids') {
  537. return new \Zend_Db_Expr(
  538. $this->_productResource->getConnection()
  539. ->select()
  540. ->from(
  541. $this->_productResource->getTable('catalog_category_product'),
  542. ['product_id']
  543. )->where(
  544. 'category_id IN (?)',
  545. $this->getValueParsed()
  546. )->__toString()
  547. );
  548. }
  549. return parent::getBindArgumentValue();
  550. }
  551. /**
  552. * Get mapped sql field
  553. *
  554. * @return string
  555. */
  556. public function getMappedSqlField()
  557. {
  558. if ($this->getAttribute() == 'sku') {
  559. $mappedSqlField = 'e.sku';
  560. } elseif (!$this->isAttributeSetOrCategory()) {
  561. $mappedSqlField = $this->getEavAttributeTableAlias() . '.value';
  562. } elseif ($this->getAttribute() == 'category_ids') {
  563. $mappedSqlField = 'e.entity_id';
  564. } else {
  565. $mappedSqlField = parent::getMappedSqlField();
  566. }
  567. return $mappedSqlField;
  568. }
  569. /**
  570. * Validate product by entity ID
  571. *
  572. * @param int $productId
  573. * @return bool
  574. */
  575. public function validateByEntityId($productId)
  576. {
  577. if ('category_ids' == $this->getAttribute()) {
  578. $result = $this->validateAttribute($this->_getAvailableInCategories($productId));
  579. } elseif ('attribute_set_id' == $this->getAttribute()) {
  580. $result = $this->validateAttribute($this->_getAttributeSetId($productId));
  581. } else {
  582. $product = $this->productRepository->getById($productId);
  583. $result = $this->validate($product);
  584. unset($product);
  585. }
  586. return $result;
  587. }
  588. /**
  589. * Retrieve category ids where product is available
  590. *
  591. * @param int $productId
  592. * @return array
  593. */
  594. protected function _getAvailableInCategories($productId)
  595. {
  596. return $this->_productResource->getConnection()
  597. ->fetchCol(
  598. $this->_productResource->getConnection()
  599. ->select()
  600. ->distinct()
  601. ->from(
  602. $this->_productResource->getTable('catalog_category_product'),
  603. ['category_id']
  604. )->where(
  605. 'product_id = ?',
  606. $productId
  607. )
  608. );
  609. }
  610. /**
  611. * Get attribute set id for product
  612. *
  613. * @param int $productId
  614. * @return string
  615. */
  616. protected function _getAttributeSetId($productId)
  617. {
  618. return $this->_productResource->getConnection()
  619. ->fetchOne(
  620. $this->_productResource->getConnection()
  621. ->select()
  622. ->distinct()
  623. ->from(
  624. $this->_productResource->getTable('catalog_product_entity'),
  625. ['attribute_set_id']
  626. )->where(
  627. 'entity_id = ?',
  628. $productId
  629. )
  630. );
  631. }
  632. /**
  633. * Correct '==' and '!=' operators
  634. * Categories can't be equal because product is included categories selected by administrator and in their parents
  635. *
  636. * @return string
  637. */
  638. public function getOperatorForValidate()
  639. {
  640. $operator = $this->getOperator();
  641. if ($this->getInputType() == 'category') {
  642. if ($operator == '==') {
  643. $operator = '{}';
  644. } elseif ($operator == '!=') {
  645. $operator = '!{}';
  646. }
  647. }
  648. return $operator;
  649. }
  650. /**
  651. * Check is attribute set or category
  652. *
  653. * @return bool
  654. */
  655. protected function isAttributeSetOrCategory()
  656. {
  657. return in_array($this->getAttribute(), ['attribute_set_id', 'category_ids']);
  658. }
  659. /**
  660. * Get eav attribute alias
  661. *
  662. * @return string
  663. */
  664. protected function getEavAttributeTableAlias()
  665. {
  666. $attribute = $this->getAttributeObject();
  667. return 'at_' . $attribute->getAttributeCode();
  668. }
  669. }