AbstractCollection.php 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. namespace Magento\Eav\Model\Entity\Collection;
  7. use Magento\Framework\App\ResourceConnection\SourceProviderInterface;
  8. use Magento\Framework\Data\Collection\AbstractDb;
  9. use Magento\Framework\DB\Select;
  10. use Magento\Framework\Exception\LocalizedException;
  11. /**
  12. * Entity/Attribute/Model - collection abstract
  13. *
  14. * @api
  15. * @SuppressWarnings(PHPMD.TooManyFields)
  16. * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
  17. * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
  18. * @since 100.0.2
  19. */
  20. abstract class AbstractCollection extends AbstractDb implements SourceProviderInterface
  21. {
  22. /**
  23. * Attribute table alias prefix
  24. */
  25. const ATTRIBUTE_TABLE_ALIAS_PREFIX = 'at_';
  26. /**
  27. * Array of items with item id key
  28. *
  29. * @var array
  30. */
  31. protected $_itemsById = [];
  32. /**
  33. * Entity static fields
  34. *
  35. * @var array
  36. */
  37. protected $_staticFields = [];
  38. /**
  39. * Entity object to define collection's attributes
  40. *
  41. * @var \Magento\Eav\Model\Entity\AbstractEntity
  42. */
  43. protected $_entity;
  44. /**
  45. * Entity types to be fetched for objects in collection
  46. *
  47. * @var array
  48. */
  49. protected $_selectEntityTypes = [];
  50. /**
  51. * Attributes to be fetched for objects in collection
  52. *
  53. * @var array
  54. */
  55. protected $_selectAttributes = [];
  56. /**
  57. * Attributes to be filtered order sorted by
  58. *
  59. * @var array
  60. */
  61. protected $_filterAttributes = [];
  62. /**
  63. * Joined entities
  64. *
  65. * @var array
  66. */
  67. protected $_joinEntities = [];
  68. /**
  69. * Joined attributes
  70. *
  71. * @var array
  72. */
  73. protected $_joinAttributes = [];
  74. /**
  75. * Joined fields data
  76. *
  77. * @var array
  78. */
  79. protected $_joinFields = [];
  80. /**
  81. * Cast map for attribute order
  82. *
  83. * @var string[]
  84. */
  85. protected $_castToIntMap = ['validate-digits'];
  86. /**
  87. * Core event manager proxy
  88. *
  89. * @var \Magento\Framework\Event\ManagerInterface
  90. */
  91. protected $_eventManager = null;
  92. /**
  93. * @var \Magento\Eav\Model\Config
  94. */
  95. protected $_eavConfig;
  96. /**
  97. * @var \Magento\Framework\App\ResourceConnection
  98. */
  99. protected $_resource;
  100. /**
  101. * @var \Magento\Eav\Model\EntityFactory
  102. */
  103. protected $_eavEntityFactory;
  104. /**
  105. * @var \Magento\Eav\Model\ResourceModel\Helper
  106. */
  107. protected $_resourceHelper;
  108. /**
  109. * @var \Magento\Framework\Validator\UniversalFactory
  110. */
  111. protected $_universalFactory;
  112. /**
  113. * @param \Magento\Framework\Data\Collection\EntityFactory $entityFactory
  114. * @param \Psr\Log\LoggerInterface $logger
  115. * @param \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy
  116. * @param \Magento\Framework\Event\ManagerInterface $eventManager
  117. * @param \Magento\Eav\Model\Config $eavConfig
  118. * @param \Magento\Framework\App\ResourceConnection $resource
  119. * @param \Magento\Eav\Model\EntityFactory $eavEntityFactory
  120. * @param \Magento\Eav\Model\ResourceModel\Helper $resourceHelper
  121. * @param \Magento\Framework\Validator\UniversalFactory $universalFactory
  122. * @param mixed $connection
  123. * @codeCoverageIgnore
  124. * @SuppressWarnings(PHPMD.ExcessiveParameterList)
  125. */
  126. public function __construct(
  127. \Magento\Framework\Data\Collection\EntityFactory $entityFactory,
  128. \Psr\Log\LoggerInterface $logger,
  129. \Magento\Framework\Data\Collection\Db\FetchStrategyInterface $fetchStrategy,
  130. \Magento\Framework\Event\ManagerInterface $eventManager,
  131. \Magento\Eav\Model\Config $eavConfig,
  132. \Magento\Framework\App\ResourceConnection $resource,
  133. \Magento\Eav\Model\EntityFactory $eavEntityFactory,
  134. \Magento\Eav\Model\ResourceModel\Helper $resourceHelper,
  135. \Magento\Framework\Validator\UniversalFactory $universalFactory,
  136. \Magento\Framework\DB\Adapter\AdapterInterface $connection = null
  137. ) {
  138. $this->_eventManager = $eventManager;
  139. $this->_eavConfig = $eavConfig;
  140. $this->_resource = $resource;
  141. $this->_eavEntityFactory = $eavEntityFactory;
  142. $this->_resourceHelper = $resourceHelper;
  143. $this->_universalFactory = $universalFactory;
  144. parent::__construct($entityFactory, $logger, $fetchStrategy, $connection);
  145. $this->_construct();
  146. $this->setConnection($this->getEntity()->getConnection());
  147. $this->_prepareStaticFields();
  148. $this->_initSelect();
  149. }
  150. /**
  151. * Initialize collection
  152. *
  153. * @return void
  154. */
  155. protected function _construct()
  156. {
  157. }
  158. /**
  159. * Retrieve table name
  160. *
  161. * @param string $table
  162. * @return string
  163. * @codeCoverageIgnore
  164. */
  165. public function getTable($table)
  166. {
  167. return $this->getResource()->getTable($table);
  168. }
  169. /**
  170. * Prepare static entity fields
  171. *
  172. * @return $this
  173. */
  174. protected function _prepareStaticFields()
  175. {
  176. foreach ($this->getEntity()->getDefaultAttributes() as $field) {
  177. $this->_staticFields[$field] = $field;
  178. }
  179. return $this;
  180. }
  181. /**
  182. * Init select
  183. *
  184. * @return $this
  185. */
  186. protected function _initSelect()
  187. {
  188. $this->getSelect()->from(['e' => $this->getEntity()->getEntityTable()]);
  189. $entity = $this->getEntity();
  190. if ($entity->getTypeId() && $entity->getEntityTable() == \Magento\Eav\Model\Entity::DEFAULT_ENTITY_TABLE) {
  191. $this->addAttributeToFilter('entity_type_id', $this->getEntity()->getTypeId());
  192. }
  193. return $this;
  194. }
  195. /**
  196. * Standard resource collection initialization
  197. *
  198. * @param string $model
  199. * @param string $entityModel
  200. * @return $this
  201. */
  202. protected function _init($model, $entityModel)
  203. {
  204. $this->setItemObjectClass($model);
  205. $entity = $this->_universalFactory->create($entityModel);
  206. $this->setEntity($entity);
  207. return $this;
  208. }
  209. /**
  210. * Set entity to use for attributes
  211. *
  212. * @param \Magento\Eav\Model\Entity\AbstractEntity $entity
  213. * @return $this
  214. * @throws LocalizedException
  215. */
  216. public function setEntity($entity)
  217. {
  218. if ($entity instanceof \Magento\Eav\Model\Entity\AbstractEntity) {
  219. $this->_entity = $entity;
  220. } elseif (is_string($entity) || $entity instanceof \Magento\Framework\App\Config\Element) {
  221. $this->_entity = $this->_eavEntityFactory->create()->setType($entity);
  222. } else {
  223. throw new LocalizedException(
  224. __('The "%1" entity supplied is invalid. Verify the entity and try again.', print_r($entity, 1))
  225. );
  226. }
  227. return $this;
  228. }
  229. /**
  230. * Get collection's entity object
  231. *
  232. * @return \Magento\Eav\Model\Entity\AbstractEntity
  233. * @throws LocalizedException
  234. */
  235. public function getEntity()
  236. {
  237. if (empty($this->_entity)) {
  238. throw new LocalizedException(__('Entity is not initialized'));
  239. }
  240. return $this->_entity;
  241. }
  242. /**
  243. * Get resource instance
  244. *
  245. * @return \Magento\Framework\Model\ResourceModel\Db\AbstractDb
  246. * @codeCoverageIgnore
  247. */
  248. public function getResource()
  249. {
  250. return $this->getEntity();
  251. }
  252. /**
  253. * Set template object for the collection
  254. *
  255. * @param \Magento\Framework\DataObject $object
  256. * @return $this
  257. */
  258. public function setObject($object = null)
  259. {
  260. if (is_object($object)) {
  261. $this->setItemObjectClass(get_class($object));
  262. } else {
  263. $this->setItemObjectClass($object);
  264. }
  265. return $this;
  266. }
  267. /**
  268. * Add an object to the collection
  269. *
  270. * @param \Magento\Framework\DataObject $object
  271. * @return $this
  272. * @throws LocalizedException
  273. */
  274. public function addItem(\Magento\Framework\DataObject $object)
  275. {
  276. if (!$object instanceof $this->_itemObjectClass) {
  277. throw new LocalizedException(
  278. __("The object wasn't added because it's invalid. To continue, enter a valid object and try again.")
  279. );
  280. }
  281. return parent::addItem($object);
  282. }
  283. /**
  284. * Retrieve entity attribute
  285. *
  286. * @param string $attributeCode
  287. * @return \Magento\Eav\Model\Entity\Attribute\AbstractAttribute
  288. */
  289. public function getAttribute($attributeCode)
  290. {
  291. if (isset($this->_joinAttributes[$attributeCode])) {
  292. return $this->_joinAttributes[$attributeCode]['attribute'];
  293. }
  294. return $this->getEntity()->getAttribute($attributeCode);
  295. }
  296. /**
  297. * Add attribute filter to collection
  298. *
  299. * If $attribute is an array will add OR condition with following format:
  300. * array(
  301. * array('attribute'=>'firstname', 'like'=>'test%'),
  302. * array('attribute'=>'lastname', 'like'=>'test%'),
  303. * )
  304. *
  305. * @param \Magento\Eav\Model\Entity\Attribute\AttributeInterface|integer|string|array $attribute
  306. * @param null|string|array $condition
  307. * @param string $joinType
  308. * @return $this
  309. * @throws \Magento\Framework\Exception\LocalizedException
  310. *
  311. * @see self::_getConditionSql for $condition
  312. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  313. */
  314. public function addAttributeToFilter($attribute, $condition = null, $joinType = 'inner')
  315. {
  316. if ($attribute === null) {
  317. $this->getSelect();
  318. return $this;
  319. }
  320. if (is_numeric($attribute)) {
  321. $attributeModel = $this->getEntity()->getAttribute($attribute);
  322. if (!$attributeModel) {
  323. throw new \Magento\Framework\Exception\LocalizedException(
  324. __('Invalid attribute identifier for filter (%1)', get_class($attribute))
  325. );
  326. }
  327. $attribute = $attributeModel->getAttributeCode();
  328. } elseif ($attribute instanceof \Magento\Eav\Model\Entity\Attribute\AttributeInterface) {
  329. $attribute = $attribute->getAttributeCode();
  330. }
  331. if (is_array($attribute)) {
  332. $sqlArr = [];
  333. foreach ($attribute as $condition) {
  334. $sqlArr[] = $this->_getAttributeConditionSql($condition['attribute'], $condition, $joinType);
  335. }
  336. $conditionSql = '(' . implode(') OR (', $sqlArr) . ')';
  337. } elseif (is_string($attribute)) {
  338. if ($condition === null) {
  339. $condition = '';
  340. }
  341. $conditionSql = $this->_getAttributeConditionSql($attribute, $condition, $joinType);
  342. }
  343. if (!empty($conditionSql)) {
  344. $this->getSelect()->where($conditionSql, null, \Magento\Framework\DB\Select::TYPE_CONDITION);
  345. $this->invalidateSize();
  346. } else {
  347. throw new \Magento\Framework\Exception\LocalizedException(
  348. __('Invalid attribute identifier for filter (%1)', get_class($attribute))
  349. );
  350. }
  351. return $this;
  352. }
  353. /**
  354. * Wrapper for compatibility with \Magento\Framework\Data\Collection\AbstractDb
  355. *
  356. * @param mixed $attribute
  357. * @param mixed $condition
  358. * @return $this|AbstractDb
  359. * @codeCoverageIgnore
  360. */
  361. public function addFieldToFilter($attribute, $condition = null)
  362. {
  363. return $this->addAttributeToFilter($attribute, $condition);
  364. }
  365. /**
  366. * Add attribute to sort order
  367. *
  368. * @param string $attribute
  369. * @param string $dir
  370. * @return $this
  371. */
  372. public function addAttributeToSort($attribute, $dir = self::SORT_ORDER_ASC)
  373. {
  374. if (isset($this->_joinFields[$attribute])) {
  375. $this->getSelect()->order($this->_getAttributeFieldName($attribute) . ' ' . $dir);
  376. return $this;
  377. }
  378. if (isset($this->_staticFields[$attribute])) {
  379. $this->getSelect()->order("e.{$attribute} {$dir}");
  380. return $this;
  381. }
  382. if (isset($this->_joinAttributes[$attribute])) {
  383. $attrInstance = $this->_joinAttributes[$attribute]['attribute'];
  384. $entityField = $this->_getAttributeTableAlias($attribute) . '.' . $attrInstance->getAttributeCode();
  385. } else {
  386. $attrInstance = $this->getEntity()->getAttribute($attribute);
  387. $entityField = 'e.' . $attribute;
  388. }
  389. if ($attrInstance) {
  390. if ($attrInstance->getBackend()->isStatic()) {
  391. $orderExpr = $entityField;
  392. } else {
  393. $this->_addAttributeJoin($attribute, 'left');
  394. if (isset($this->_joinAttributes[$attribute]) || isset($this->_joinFields[$attribute])) {
  395. $orderExpr = $attribute;
  396. } else {
  397. $orderExpr = $this->_getAttributeTableAlias($attribute) . '.value';
  398. }
  399. }
  400. if (in_array($attrInstance->getFrontendClass(), $this->_castToIntMap)) {
  401. $orderExpr = new \Zend_Db_Expr("CAST({$this->_prepareOrderExpression($orderExpr)} AS SIGNED)");
  402. }
  403. $orderExpr .= ' ' . $dir;
  404. $this->getSelect()->order($orderExpr);
  405. }
  406. return $this;
  407. }
  408. /**
  409. * Retrieve attribute expression by specified column
  410. *
  411. * @param string $field
  412. * @return string|Zend_Db_Expr
  413. */
  414. protected function _prepareOrderExpression($field)
  415. {
  416. foreach ($this->getSelect()->getPart(\Magento\Framework\DB\Select::COLUMNS) as $columnEntry) {
  417. if ($columnEntry[2] != $field) {
  418. continue;
  419. }
  420. if ($columnEntry[1] instanceof \Zend_Db_Expr) {
  421. return $columnEntry[1];
  422. }
  423. }
  424. return $field;
  425. }
  426. /**
  427. * Add attribute to entities in collection
  428. *
  429. * If $attribute == '*' select all attributes
  430. *
  431. * @param array|string|integer|\Magento\Framework\App\Config\Element $attribute
  432. * @param bool|string $joinType flag for joining attribute
  433. * @return $this
  434. * @throws LocalizedException
  435. */
  436. public function addAttributeToSelect($attribute, $joinType = false)
  437. {
  438. if (is_array($attribute)) {
  439. foreach ($attribute as $a) {
  440. $this->addAttributeToSelect($a, $joinType);
  441. }
  442. return $this;
  443. }
  444. if ($joinType !== false && !$this->getEntity()->getAttribute($attribute)->isStatic()) {
  445. $this->_addAttributeJoin($attribute, $joinType);
  446. } elseif ('*' === $attribute) {
  447. $entity = clone $this->getEntity();
  448. $attributes = $entity->loadAllAttributes()->getAttributesByCode();
  449. foreach ($attributes as $attrCode => $attr) {
  450. $this->_selectAttributes[$attrCode] = $attr->getId();
  451. }
  452. } else {
  453. if (isset($this->_joinAttributes[$attribute])) {
  454. $attrInstance = $this->_joinAttributes[$attribute]['attribute'];
  455. } else {
  456. $attrInstance = $this->_eavConfig->getAttribute($this->getEntity()->getType(), $attribute);
  457. }
  458. if (empty($attrInstance)) {
  459. throw new LocalizedException(
  460. __(
  461. 'The "%1" attribute requested is invalid. Verify the attribute and try again.',
  462. (string)$attribute
  463. )
  464. );
  465. }
  466. $this->_selectAttributes[$attrInstance->getAttributeCode()] = $attrInstance->getId();
  467. }
  468. return $this;
  469. }
  470. /**
  471. * Add entity type to select statement
  472. *
  473. * @param string $entityType
  474. * @param string $prefix
  475. * @return $this
  476. * @codeCoverageIgnore
  477. */
  478. public function addEntityTypeToSelect($entityType, $prefix)
  479. {
  480. $this->_selectEntityTypes[$entityType] = ['prefix' => $prefix];
  481. return $this;
  482. }
  483. /**
  484. * Add field to static
  485. *
  486. * @param string $field
  487. * @return $this
  488. */
  489. public function addStaticField($field)
  490. {
  491. if (!isset($this->_staticFields[$field])) {
  492. $this->_staticFields[$field] = $field;
  493. }
  494. return $this;
  495. }
  496. /**
  497. * Add attribute expression (SUM, COUNT, etc)
  498. *
  499. * Example: ('sub_total', 'SUM({{attribute}})', 'revenue')
  500. * Example: ('sub_total', 'SUM({{revenue}})', 'revenue')
  501. *
  502. * For some functions like SUM use groupByAttribute.
  503. *
  504. * @param string $alias
  505. * @param string $expression
  506. * @param string $attribute
  507. * @return $this
  508. * @throws LocalizedException
  509. */
  510. public function addExpressionAttributeToSelect($alias, $expression, $attribute)
  511. {
  512. // validate alias
  513. if (isset($this->_joinFields[$alias])) {
  514. throw new LocalizedException(__('Joint field or attribute expression with this alias is already declared'));
  515. }
  516. if (!is_array($attribute)) {
  517. $attribute = [$attribute];
  518. }
  519. $fullExpression = $expression;
  520. // Replacing multiple attributes
  521. foreach ($attribute as $attributeItem) {
  522. if (isset($this->_staticFields[$attributeItem])) {
  523. $attrField = sprintf('e.%s', $attributeItem);
  524. } else {
  525. $attributeInstance = $this->getAttribute($attributeItem);
  526. if ($attributeInstance->getBackend()->isStatic()) {
  527. $attrField = 'e.' . $attributeItem;
  528. } else {
  529. $this->_addAttributeJoin($attributeItem, 'left');
  530. $attrField = $this->_getAttributeFieldName($attributeItem);
  531. }
  532. }
  533. $fullExpression = str_replace('{{attribute}}', $attrField, $fullExpression);
  534. $fullExpression = str_replace('{{' . $attributeItem . '}}', $attrField, $fullExpression);
  535. }
  536. $this->getSelect()->columns([$alias => $fullExpression]);
  537. $this->_joinFields[$alias] = ['table' => false, 'field' => $fullExpression];
  538. return $this;
  539. }
  540. /**
  541. * Groups results by specified attribute
  542. *
  543. * @param string|array $attribute
  544. * @return $this
  545. */
  546. public function groupByAttribute($attribute)
  547. {
  548. if (is_array($attribute)) {
  549. foreach ($attribute as $attributeItem) {
  550. $this->groupByAttribute($attributeItem);
  551. }
  552. } else {
  553. if (isset($this->_joinFields[$attribute])) {
  554. $this->getSelect()->group($this->_getAttributeFieldName($attribute));
  555. return $this;
  556. }
  557. if (isset($this->_staticFields[$attribute])) {
  558. $this->getSelect()->group(sprintf('e.%s', $attribute));
  559. return $this;
  560. }
  561. if (isset($this->_joinAttributes[$attribute])) {
  562. $attrInstance = $this->_joinAttributes[$attribute]['attribute'];
  563. $entityField = $this->_getAttributeTableAlias($attribute) . '.' . $attrInstance->getAttributeCode();
  564. } else {
  565. $attrInstance = $this->getEntity()->getAttribute($attribute);
  566. $entityField = 'e.' . $attribute;
  567. }
  568. if ($attrInstance->getBackend()->isStatic()) {
  569. $this->getSelect()->group($entityField);
  570. } else {
  571. $this->_addAttributeJoin($attribute);
  572. $this->getSelect()->group($this->_getAttributeTableAlias($attribute) . '.value');
  573. }
  574. }
  575. return $this;
  576. }
  577. /**
  578. * Add attribute from joined entity to select
  579. *
  580. * Examples:
  581. * ('billing_firstname', 'customer_address/firstname', 'default_billing')
  582. * ('billing_lastname', 'customer_address/lastname', 'default_billing')
  583. * ('shipping_lastname', 'customer_address/lastname', 'default_billing')
  584. * ('shipping_postalcode', 'customer_address/postalcode', 'default_shipping')
  585. * ('shipping_city', $cityAttribute, 'default_shipping')
  586. *
  587. * Developer is encouraged to use existing instances of attributes and entities
  588. * After first use of string entity name it will be cached in the collection
  589. *
  590. * @todo connect between joined attributes of same entity
  591. * @param string $alias alias for the joined attribute
  592. * @param string|\Magento\Eav\Model\Entity\Attribute\AbstractAttribute $attribute
  593. * @param string $bind attribute of the main entity to link with joined $filter
  594. * @param string $filter primary key for the joined entity (entity_id default)
  595. * @param string $joinType inner|left
  596. * @param int|null $storeId
  597. * @return $this
  598. * @throws LocalizedException
  599. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  600. * @SuppressWarnings(PHPMD.NPathComplexity)
  601. */
  602. public function joinAttribute($alias, $attribute, $bind, $filter = null, $joinType = 'inner', $storeId = null)
  603. {
  604. // validate alias
  605. if (isset($this->_joinAttributes[$alias])) {
  606. throw new LocalizedException(__('Invalid alias, already exists in joint attributes'));
  607. }
  608. $bindAttribute = null;
  609. // validate bind attribute
  610. if (is_string($bind)) {
  611. $bindAttribute = $this->getAttribute($bind);
  612. }
  613. if (!$bindAttribute || !$bindAttribute->isStatic() && !$bindAttribute->getId()) {
  614. throw new LocalizedException(__('The foreign key is invalid. Verify the foreign key and try again.'));
  615. }
  616. // try to explode combined entity/attribute if supplied
  617. if (is_string($attribute)) {
  618. $attrArr = explode('/', $attribute);
  619. if (isset($attrArr[1])) {
  620. $entity = $attrArr[0];
  621. $attribute = $attrArr[1];
  622. }
  623. }
  624. // validate entity
  625. if (empty($entity) && $attribute instanceof \Magento\Eav\Model\Entity\Attribute\AbstractAttribute) {
  626. $entity = $attribute->getEntity();
  627. } elseif (is_string($entity)) {
  628. // retrieve cached entity if possible
  629. if (isset($this->_joinEntities[$entity])) {
  630. $entity = $this->_joinEntities[$entity];
  631. } else {
  632. $entity = $this->_eavEntityFactory->create()->setType($attrArr[0]);
  633. }
  634. }
  635. if (!$entity || !$entity->getTypeId()) {
  636. throw new LocalizedException(__('The entity type is invalid. Verify the entity type and try again.'));
  637. }
  638. // cache entity
  639. if (!isset($this->_joinEntities[$entity->getType()])) {
  640. $this->_joinEntities[$entity->getType()] = $entity;
  641. }
  642. // validate attribute
  643. if (is_string($attribute)) {
  644. $attribute = $entity->getAttribute($attribute);
  645. }
  646. if (!$attribute) {
  647. throw new LocalizedException(__('The attribute type is invalid. Verify the attribute type and try again.'));
  648. }
  649. if (empty($filter)) {
  650. $filter = $entity->getLinkField();
  651. }
  652. // add joined attribute
  653. $this->_joinAttributes[$alias] = [
  654. 'bind' => $bind,
  655. 'bindAttribute' => $bindAttribute,
  656. 'attribute' => $attribute,
  657. 'filter' => $filter,
  658. 'store_id' => $storeId,
  659. ];
  660. $this->_addAttributeJoin($alias, $joinType);
  661. return $this;
  662. }
  663. /**
  664. * Join regular table field and use an attribute as fk
  665. *
  666. * Examples:
  667. * ('country_name', 'directory_country_name', 'name', 'country_id=shipping_country',
  668. * "{{table}}.language_code='en'", 'left')
  669. *
  670. * @param string $alias 'country_name'
  671. * @param string $table 'directory_country_name'
  672. * @param string $field 'name'
  673. * @param string $bind 'PK(country_id)=FK(shipping_country_id)'
  674. * @param string|array $cond "{{table}}.language_code='en'" OR array('language_code'=>'en')
  675. * @param string $joinType 'left'
  676. * @return $this
  677. * @throws LocalizedException
  678. */
  679. public function joinField($alias, $table, $field, $bind, $cond = null, $joinType = 'inner')
  680. {
  681. // validate alias
  682. if (isset($this->_joinFields[$alias])) {
  683. throw new LocalizedException(__('A joined field with this alias is already declared.'));
  684. }
  685. $table = $this->_resource->getTableName($table);
  686. $tableAlias = $this->_getAttributeTableAlias($alias);
  687. // validate bind
  688. list($pKey, $fKey) = explode('=', $bind);
  689. $pKey = $this->getSelect()->getConnection()->quoteColumnAs(trim($pKey), null);
  690. $bindCond = $tableAlias . '.' . trim($pKey) . '=' . $this->_getAttributeFieldName(trim($fKey));
  691. // process join type
  692. switch ($joinType) {
  693. case 'left':
  694. $joinMethod = 'joinLeft';
  695. break;
  696. default:
  697. $joinMethod = 'join';
  698. break;
  699. }
  700. $condArr = [$bindCond];
  701. // add where condition if needed
  702. if ($cond !== null) {
  703. if (is_array($cond)) {
  704. foreach ($cond as $key => $value) {
  705. $condArr[] = $this->_getConditionSql($tableAlias . '.' . $key, $value);
  706. }
  707. } else {
  708. $condArr[] = str_replace('{{table}}', $tableAlias, $cond);
  709. }
  710. }
  711. $cond = '(' . implode(') AND (', $condArr) . ')';
  712. // join table
  713. $this->getSelect()->{$joinMethod}(
  714. [$tableAlias => $table],
  715. $cond,
  716. $field ? [$alias => $field] : []
  717. );
  718. // save joined attribute
  719. $this->_joinFields[$alias] = ['table' => $tableAlias, 'field' => $field];
  720. return $this;
  721. }
  722. /**
  723. * Join a table
  724. *
  725. * @param string|array $table
  726. * @param string $bind
  727. * @param string|array $fields
  728. * @param null|array $cond
  729. * @param string $joinType
  730. * @return $this
  731. * @throws LocalizedException
  732. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  733. */
  734. public function joinTable($table, $bind, $fields = null, $cond = null, $joinType = 'inner')
  735. {
  736. $tableAlias = null;
  737. if (is_array($table)) {
  738. list($tableAlias, $tableName) = [key($table), current($table)];
  739. } else {
  740. $tableName = $table;
  741. }
  742. $tableName = $this->_resource->getTableName($tableName);
  743. if (empty($tableAlias)) {
  744. $tableAlias = $tableName;
  745. }
  746. // validate fields and aliases
  747. if (!$fields) {
  748. throw new LocalizedException(__('Invalid joint fields'));
  749. }
  750. foreach ($fields as $alias => $field) {
  751. if (isset($this->_joinFields[$alias])) {
  752. throw new LocalizedException(__('A joint field with a "%1" alias is already declared.', $alias));
  753. }
  754. $this->_joinFields[$alias] = ['table' => $tableAlias, 'field' => $field];
  755. }
  756. // validate bind
  757. list($pKey, $fKey) = explode('=', $bind);
  758. $bindCond = $tableAlias . '.' . $pKey . '=' . $this->_getAttributeFieldName($fKey);
  759. // process join type
  760. switch ($joinType) {
  761. case 'left':
  762. $joinMethod = 'joinLeft';
  763. break;
  764. default:
  765. $joinMethod = 'join';
  766. }
  767. $condArr = [$bindCond];
  768. // add where condition if needed
  769. if ($cond !== null) {
  770. if (is_array($cond)) {
  771. foreach ($cond as $key => $value) {
  772. $condArr[] = $this->_getConditionSql($tableAlias . '.' . $key, $value);
  773. }
  774. } else {
  775. $condArr[] = str_replace('{{table}}', $tableAlias, $cond);
  776. }
  777. }
  778. $cond = '(' . implode(') AND (', $condArr) . ')';
  779. // join table
  780. $this->getSelect()->{$joinMethod}([$tableAlias => $tableName], $cond, $fields);
  781. return $this;
  782. }
  783. /**
  784. * Remove an attribute from selection list
  785. *
  786. * @param string $attribute
  787. * @return $this
  788. */
  789. public function removeAttributeToSelect($attribute = null)
  790. {
  791. if ($attribute === null) {
  792. $this->_selectAttributes = [];
  793. } else {
  794. unset($this->_selectAttributes[$attribute]);
  795. }
  796. return $this;
  797. }
  798. /**
  799. * Set collection page start and records to show
  800. *
  801. * @param integer $pageNum
  802. * @param integer $pageSize
  803. * @return $this
  804. * @codeCoverageIgnore
  805. */
  806. public function setPage($pageNum, $pageSize)
  807. {
  808. $this->setCurPage($pageNum)->setPageSize($pageSize);
  809. return $this;
  810. }
  811. /**
  812. * Load collection data into object items
  813. *
  814. * @param bool $printQuery
  815. * @param bool $logQuery
  816. * @return $this
  817. */
  818. public function load($printQuery = false, $logQuery = false)
  819. {
  820. if ($this->isLoaded()) {
  821. return $this;
  822. }
  823. \Magento\Framework\Profiler::start('EAV:load_collection');
  824. \Magento\Framework\Profiler::start('before_load');
  825. $this->_eventManager->dispatch('eav_collection_abstract_load_before', ['collection' => $this]);
  826. $this->_beforeLoad();
  827. \Magento\Framework\Profiler::stop('before_load');
  828. $this->_renderFilters();
  829. $this->_renderOrders();
  830. \Magento\Framework\Profiler::start('load_entities');
  831. $this->_loadEntities($printQuery, $logQuery);
  832. \Magento\Framework\Profiler::stop('load_entities');
  833. \Magento\Framework\Profiler::start('load_attributes');
  834. $this->_loadAttributes($printQuery, $logQuery);
  835. \Magento\Framework\Profiler::stop('load_attributes');
  836. \Magento\Framework\Profiler::start('set_orig_data');
  837. foreach ($this->_items as $item) {
  838. $item->setOrigData();
  839. $this->beforeAddLoadedItem($item);
  840. $item->setDataChanges(false);
  841. }
  842. \Magento\Framework\Profiler::stop('set_orig_data');
  843. $this->_setIsLoaded();
  844. \Magento\Framework\Profiler::start('after_load');
  845. $this->_afterLoad();
  846. \Magento\Framework\Profiler::stop('after_load');
  847. \Magento\Framework\Profiler::stop('EAV:load_collection');
  848. return $this;
  849. }
  850. /**
  851. * Clone and reset collection
  852. *
  853. * @param int|null $limit
  854. * @param int|null $offset
  855. * @return Select
  856. */
  857. protected function _getAllIdsSelect($limit = null, $offset = null)
  858. {
  859. $idsSelect = clone $this->getSelect();
  860. $idsSelect->reset(\Magento\Framework\DB\Select::ORDER);
  861. $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT);
  862. $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET);
  863. $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS);
  864. $idsSelect->columns('e.' . $this->getEntity()->getIdFieldName());
  865. $idsSelect->limit($limit, $offset);
  866. return $idsSelect;
  867. }
  868. /**
  869. * Retrieve all ids for collection
  870. *
  871. * @param null|int|string $limit
  872. * @param null|int|string $offset
  873. * @return array
  874. */
  875. public function getAllIds($limit = null, $offset = null)
  876. {
  877. return $this->getConnection()->fetchCol($this->_getAllIdsSelect($limit, $offset), $this->_bindParams);
  878. }
  879. /**
  880. * Retrieve all ids sql
  881. *
  882. * @return Select
  883. */
  884. public function getAllIdsSql()
  885. {
  886. $idsSelect = clone $this->getSelect();
  887. $idsSelect->reset(\Magento\Framework\DB\Select::ORDER);
  888. $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_COUNT);
  889. $idsSelect->reset(\Magento\Framework\DB\Select::LIMIT_OFFSET);
  890. $idsSelect->reset(\Magento\Framework\DB\Select::COLUMNS);
  891. $idsSelect->reset(\Magento\Framework\DB\Select::GROUP);
  892. $idsSelect->columns('e.' . $this->getEntity()->getIdFieldName());
  893. return $idsSelect;
  894. }
  895. /**
  896. * Save all the entities in the collection
  897. *
  898. * @todo make batch save directly from collection
  899. *
  900. * @return $this
  901. */
  902. public function save()
  903. {
  904. foreach ($this->getItems() as $item) {
  905. $item->save();
  906. }
  907. return $this;
  908. }
  909. /**
  910. * Delete all the entities in the collection
  911. *
  912. * @todo make batch delete directly from collection
  913. *
  914. * @return $this
  915. */
  916. public function delete()
  917. {
  918. foreach ($this->getItems() as $key => $item) {
  919. $this->getEntity()->delete($item);
  920. unset($this->_items[$key]);
  921. }
  922. return $this;
  923. }
  924. /**
  925. * Import 2D array into collection as objects
  926. *
  927. * If the imported items already exist, update the data for existing objects
  928. *
  929. * @param array $arr
  930. * @return $this
  931. */
  932. public function importFromArray($arr)
  933. {
  934. $entityIdField = $this->getEntity()->getLinkField();
  935. foreach ($arr as $row) {
  936. $entityId = $row[$entityIdField];
  937. if (!isset($this->_items[$entityId])) {
  938. $this->_items[$entityId] = $this->getNewEmptyItem();
  939. $this->_items[$entityId]->setData($row);
  940. } else {
  941. $this->_items[$entityId]->addData($row);
  942. }
  943. }
  944. return $this;
  945. }
  946. /**
  947. * Get collection data as a 2D array
  948. *
  949. * @return array
  950. */
  951. public function exportToArray()
  952. {
  953. $result = [];
  954. $entityIdField = $this->getEntity()->getLinkField();
  955. foreach ($this->getItems() as $item) {
  956. $result[$item->getData($entityIdField)] = $item->getData();
  957. }
  958. return $result;
  959. }
  960. /**
  961. * Retrieve row id field name
  962. *
  963. * @return string
  964. * @codeCoverageIgnore
  965. */
  966. public function getRowIdFieldName()
  967. {
  968. return $this->getIdFieldName();
  969. }
  970. /**
  971. * Id field name getter
  972. *
  973. * @return string
  974. */
  975. public function getIdFieldName()
  976. {
  977. if ($this->_idFieldName === null) {
  978. $this->_setIdFieldName($this->getEntity()->getIdFieldName());
  979. }
  980. return $this->_idFieldName;
  981. }
  982. /**
  983. * Set row id field name
  984. *
  985. * @param string $fieldName
  986. * @return $this
  987. */
  988. public function setRowIdFieldName($fieldName)
  989. {
  990. return $this->_setIdFieldName($fieldName);
  991. }
  992. /**
  993. * Load entities records into items
  994. *
  995. * @param bool $printQuery
  996. * @param bool $logQuery
  997. * @return $this
  998. * @throws \Exception
  999. */
  1000. public function _loadEntities($printQuery = false, $logQuery = false)
  1001. {
  1002. $this->getEntity();
  1003. if ($this->_pageSize) {
  1004. $this->getSelect()->limitPage($this->getCurPage(), $this->_pageSize);
  1005. }
  1006. $this->printLogQuery($printQuery, $logQuery);
  1007. try {
  1008. /**
  1009. * Prepare select query
  1010. * @var string $query
  1011. */
  1012. $query = $this->getSelect();
  1013. $rows = $this->_fetchAll($query);
  1014. } catch (\Exception $e) {
  1015. $this->printLogQuery(false, true, $query);
  1016. throw $e;
  1017. }
  1018. foreach ($rows as $value) {
  1019. $object = $this->getNewEmptyItem()->setData($value);
  1020. $this->addItem($object);
  1021. if (isset($this->_itemsById[$object->getId()])) {
  1022. $this->_itemsById[$object->getId()][] = $object;
  1023. } else {
  1024. $this->_itemsById[$object->getId()] = [$object];
  1025. }
  1026. }
  1027. return $this;
  1028. }
  1029. /**
  1030. * Load attributes into loaded entities
  1031. *
  1032. * @param bool $printQuery
  1033. * @param bool $logQuery
  1034. * @return $this
  1035. * @throws LocalizedException
  1036. * @throws \Exception
  1037. * @SuppressWarnings(PHPMD.CyclomaticComplexity)
  1038. * @SuppressWarnings(PHPMD.NPathComplexity)
  1039. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  1040. */
  1041. public function _loadAttributes($printQuery = false, $logQuery = false)
  1042. {
  1043. if (empty($this->_items) || empty($this->_itemsById) || empty($this->_selectAttributes)) {
  1044. return $this;
  1045. }
  1046. $entity = $this->getEntity();
  1047. $tableAttributes = [];
  1048. $attributeTypes = [];
  1049. foreach ($this->_selectAttributes as $attributeCode => $attributeId) {
  1050. if (!$attributeId) {
  1051. continue;
  1052. }
  1053. $attribute = $this->_eavConfig->getAttribute($entity->getType(), $attributeCode);
  1054. if ($attribute && !$attribute->isStatic()) {
  1055. $tableAttributes[$attribute->getBackendTable()][] = $attributeId;
  1056. if (!isset($attributeTypes[$attribute->getBackendTable()])) {
  1057. $attributeTypes[$attribute->getBackendTable()] = $attribute->getBackendType();
  1058. }
  1059. }
  1060. }
  1061. $selects = [];
  1062. foreach ($tableAttributes as $table => $attributes) {
  1063. $select = $this->_getLoadAttributesSelect($table, $attributes);
  1064. $selects[$attributeTypes[$table]][] = $this->_addLoadAttributesSelectValues(
  1065. $select,
  1066. $table,
  1067. $attributeTypes[$table]
  1068. );
  1069. }
  1070. $selectGroups = $this->_resourceHelper->getLoadAttributesSelectGroups($selects);
  1071. foreach ($selectGroups as $selects) {
  1072. if (!empty($selects)) {
  1073. try {
  1074. if (is_array($selects)) {
  1075. $select = implode(' UNION ALL ', $selects);
  1076. } else {
  1077. $select = $selects;
  1078. }
  1079. $values = $this->getConnection()->fetchAll($select);
  1080. } catch (\Exception $e) {
  1081. $this->printLogQuery(true, true, $select);
  1082. throw $e;
  1083. }
  1084. foreach ($values as $value) {
  1085. $this->_setItemAttributeValue($value);
  1086. }
  1087. }
  1088. }
  1089. return $this;
  1090. }
  1091. /**
  1092. * Retrieve attributes load select
  1093. *
  1094. * @param string $table
  1095. * @param string[] $attributeIds
  1096. * @return Select
  1097. */
  1098. protected function _getLoadAttributesSelect($table, $attributeIds = [])
  1099. {
  1100. if (empty($attributeIds)) {
  1101. $attributeIds = $this->_selectAttributes;
  1102. }
  1103. $entity = $this->getEntity();
  1104. $linkField = $entity->getLinkField();
  1105. $select = $this->getConnection()->select()
  1106. ->from(
  1107. ['e' => $this->getEntity()->getEntityTable()],
  1108. ['entity_id']
  1109. )
  1110. ->join(
  1111. ['t_d' => $table],
  1112. "e.{$linkField} = t_d.{$linkField}",
  1113. ['t_d.attribute_id']
  1114. )->where(
  1115. " e.entity_id IN (?)",
  1116. array_keys($this->_itemsById)
  1117. )->where(
  1118. 't_d.attribute_id IN (?)',
  1119. $attributeIds
  1120. );
  1121. if ($entity->getEntityTable() == \Magento\Eav\Model\Entity::DEFAULT_ENTITY_TABLE && $entity->getTypeId()) {
  1122. $select->where(
  1123. 't_d.entity_type_id =?',
  1124. $entity->getTypeId()
  1125. );
  1126. }
  1127. return $select;
  1128. }
  1129. /**
  1130. * Add select values
  1131. *
  1132. * @param \Magento\Framework\DB\Select $select
  1133. * @param string $table
  1134. * @param string $type
  1135. * @return Select
  1136. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  1137. * @codeCoverageIgnore
  1138. */
  1139. protected function _addLoadAttributesSelectValues($select, $table, $type)
  1140. {
  1141. $select->columns(['value' => 't_d.value']);
  1142. return $select;
  1143. }
  1144. /**
  1145. * Initialize entity object property value
  1146. *
  1147. * Parameter $valueInfo is _getLoadAttributesSelect fetch result row
  1148. *
  1149. * @param array $valueInfo
  1150. * @return $this
  1151. * @throws LocalizedException
  1152. */
  1153. protected function _setItemAttributeValue($valueInfo)
  1154. {
  1155. $entityIdField = $this->getEntity()->getEntityIdField();
  1156. $entityId = $valueInfo[$entityIdField];
  1157. if (!isset($this->_itemsById[$entityId])) {
  1158. throw new LocalizedException(
  1159. __('A header row is missing for an attribute. Verify the header row and try again.')
  1160. );
  1161. }
  1162. $attributeCode = array_search($valueInfo['attribute_id'], $this->_selectAttributes);
  1163. if (!$attributeCode) {
  1164. $attribute = $this->_eavConfig->getAttribute(
  1165. $this->getEntity()->getType(),
  1166. $valueInfo['attribute_id']
  1167. );
  1168. $attributeCode = $attribute->getAttributeCode();
  1169. }
  1170. foreach ($this->_itemsById[$entityId] as $object) {
  1171. $object->setData($attributeCode, $valueInfo['value']);
  1172. }
  1173. return $this;
  1174. }
  1175. /**
  1176. * Get alias for attribute value table
  1177. *
  1178. * @param string $attributeCode
  1179. * @return string
  1180. */
  1181. protected function _getAttributeTableAlias($attributeCode)
  1182. {
  1183. return $this->getConnection()->getTableName(self::ATTRIBUTE_TABLE_ALIAS_PREFIX . $attributeCode);
  1184. }
  1185. /**
  1186. * Retrieve attribute field name by attribute code
  1187. *
  1188. * @param string $attributeCode
  1189. * @return string
  1190. * @throws LocalizedException
  1191. */
  1192. protected function _getAttributeFieldName($attributeCode)
  1193. {
  1194. $attributeCode = trim($attributeCode);
  1195. if (isset($this->_joinAttributes[$attributeCode]['condition_alias'])) {
  1196. return $this->_joinAttributes[$attributeCode]['condition_alias'];
  1197. }
  1198. if (isset($this->_staticFields[$attributeCode])) {
  1199. return sprintf('e.%s', $attributeCode);
  1200. }
  1201. if (isset($this->_joinFields[$attributeCode])) {
  1202. $attr = $this->_joinFields[$attributeCode];
  1203. return $attr['table'] ? $attr['table'] . '.' . $attr['field'] : $attr['field'];
  1204. }
  1205. $attribute = $this->getAttribute($attributeCode);
  1206. if (!$attribute) {
  1207. throw new LocalizedException(
  1208. __('The "%1" attribute name is invalid. Reset the name and try again.', $attributeCode)
  1209. );
  1210. }
  1211. if ($attribute->isStatic()) {
  1212. if (isset($this->_joinAttributes[$attributeCode])) {
  1213. $fieldName = $this->_getAttributeTableAlias($attributeCode) . '.' . $attributeCode;
  1214. } else {
  1215. $fieldName = 'e.' . $attributeCode;
  1216. }
  1217. } else {
  1218. $fieldName = $this->_getAttributeTableAlias($attributeCode) . '.value';
  1219. }
  1220. return $fieldName;
  1221. }
  1222. /**
  1223. * Add attribute value table to the join if it wasn't added previously
  1224. *
  1225. * @param string $attributeCode
  1226. * @param string $joinType inner|left
  1227. * @return $this
  1228. * @throws LocalizedException
  1229. * @SuppressWarnings(PHPMD.NPathComplexity)
  1230. */
  1231. protected function _addAttributeJoin($attributeCode, $joinType = 'inner')
  1232. {
  1233. if (!empty($this->_filterAttributes[$attributeCode])) {
  1234. return $this;
  1235. }
  1236. $connection = $this->getConnection();
  1237. $attrTable = $this->_getAttributeTableAlias($attributeCode);
  1238. if (isset($this->_joinAttributes[$attributeCode])) {
  1239. $attribute = $this->_joinAttributes[$attributeCode]['attribute'];
  1240. $fkName = $this->_joinAttributes[$attributeCode]['bind'];
  1241. $fkAttribute = $this->_joinAttributes[$attributeCode]['bindAttribute'];
  1242. $fkTable = $this->_getAttributeTableAlias($fkName);
  1243. if ($fkAttribute->getBackend()->isStatic()) {
  1244. if (isset($this->_joinAttributes[$fkName])) {
  1245. $fKey = $fkTable . '.' . $fkAttribute->getAttributeCode();
  1246. } else {
  1247. $fKey = 'e.' . $fkAttribute->getAttributeCode();
  1248. }
  1249. } else {
  1250. $this->_addAttributeJoin($fkAttribute->getAttributeCode(), $joinType);
  1251. $fKey = $fkTable . '.value';
  1252. }
  1253. $pKey = $attrTable . '.' . $this->_joinAttributes[$attributeCode]['filter'];
  1254. } else {
  1255. $entity = $this->getEntity();
  1256. $fKey = 'e.' . $this->getEntityPkName($entity);
  1257. $pKey = $attrTable . '.' . $this->getEntityPkName($entity);
  1258. $attribute = $entity->getAttribute($attributeCode);
  1259. }
  1260. if (!$attribute) {
  1261. throw new LocalizedException(
  1262. __('The "%1" attribute name is invalid. Reset the name and try again.', $attributeCode)
  1263. );
  1264. }
  1265. if ($attribute->getBackend()->isStatic()) {
  1266. $attrFieldName = $attrTable . '.' . $attribute->getAttributeCode();
  1267. } else {
  1268. $attrFieldName = $attrTable . '.value';
  1269. }
  1270. $fKey = $connection->quoteColumnAs($fKey, null);
  1271. $pKey = $connection->quoteColumnAs($pKey, null);
  1272. $condArr = ["{$pKey} = {$fKey}"];
  1273. if (!$attribute->getBackend()->isStatic()) {
  1274. $condArr[] = $this->getConnection()->quoteInto(
  1275. $connection->quoteColumnAs("{$attrTable}.attribute_id", null) . ' = ?',
  1276. $attribute->getId()
  1277. );
  1278. }
  1279. /**
  1280. * process join type
  1281. */
  1282. $joinMethod = $joinType == 'left' ? 'joinLeft' : 'join';
  1283. $this->_joinAttributeToSelect($joinMethod, $attribute, $attrTable, $condArr, $attributeCode, $attrFieldName);
  1284. $this->removeAttributeToSelect($attributeCode);
  1285. $this->_filterAttributes[$attributeCode] = $attribute->getId();
  1286. /**
  1287. * Fix double join for using same as filter
  1288. */
  1289. $this->_joinFields[$attributeCode] = ['table' => '', 'field' => $attrFieldName];
  1290. return $this;
  1291. }
  1292. /**
  1293. * Retrieve Entity Primary Key
  1294. *
  1295. * @param \Magento\Eav\Model\Entity\AbstractEntity $entity
  1296. * @return string
  1297. * @since 100.1.0
  1298. */
  1299. protected function getEntityPkName(\Magento\Eav\Model\Entity\AbstractEntity $entity)
  1300. {
  1301. return $entity->getEntityIdField();
  1302. }
  1303. /**
  1304. * Adding join statement to collection select instance
  1305. *
  1306. * @param string $method
  1307. * @param object $attribute
  1308. * @param string $tableAlias
  1309. * @param array $condition
  1310. * @param string $fieldCode
  1311. * @param string $fieldAlias
  1312. * @return $this
  1313. */
  1314. protected function _joinAttributeToSelect($method, $attribute, $tableAlias, $condition, $fieldCode, $fieldAlias)
  1315. {
  1316. $this->getSelect()->{$method}(
  1317. [$tableAlias => $attribute->getBackend()->getTable()],
  1318. '(' . implode(') AND (', $condition) . ')',
  1319. [$fieldCode => $fieldAlias]
  1320. );
  1321. return $this;
  1322. }
  1323. /**
  1324. * Get condition sql for the attribute
  1325. *
  1326. * @param string $attribute
  1327. * @param mixed $condition
  1328. * @param string $joinType
  1329. * @return string
  1330. *
  1331. * @see self::_getConditionSql
  1332. */
  1333. protected function _getAttributeConditionSql($attribute, $condition, $joinType = 'inner')
  1334. {
  1335. if (isset($this->_joinFields[$attribute])) {
  1336. return $this->_getConditionSql($this->_getAttributeFieldName($attribute), $condition);
  1337. }
  1338. if (isset($this->_staticFields[$attribute])) {
  1339. return $this->_getConditionSql($this->getConnection()->quoteIdentifier('e.' . $attribute), $condition);
  1340. }
  1341. // process linked attribute
  1342. if (isset($this->_joinAttributes[$attribute])) {
  1343. $entity = $this->getAttribute($attribute)->getEntity();
  1344. } else {
  1345. $entity = $this->getEntity();
  1346. }
  1347. if ($entity->isAttributeStatic($attribute)) {
  1348. $conditionSql = $this->_getConditionSql(
  1349. $this->getConnection()->quoteIdentifier('e.' . $attribute),
  1350. $condition
  1351. );
  1352. } else {
  1353. if (isset($condition['null'])) {
  1354. $joinType = 'left';
  1355. }
  1356. $this->_addAttributeJoin($attribute, $joinType);
  1357. if (isset($this->_joinAttributes[$attribute]['condition_alias'])) {
  1358. $field = $this->_joinAttributes[$attribute]['condition_alias'];
  1359. } else {
  1360. $field = $this->_getAttributeTableAlias($attribute) . '.value';
  1361. }
  1362. $conditionSql = $this->_getConditionSql($field, $condition);
  1363. }
  1364. return $conditionSql;
  1365. }
  1366. /**
  1367. * Set sorting order
  1368. *
  1369. * Parameter $attribute can also be an array of attributes
  1370. *
  1371. * @param string|array $attribute
  1372. * @param string $dir
  1373. * @return $this
  1374. */
  1375. public function setOrder($attribute, $dir = self::SORT_ORDER_ASC)
  1376. {
  1377. if (is_array($attribute)) {
  1378. foreach ($attribute as $attr) {
  1379. parent::setOrder($attr, $dir);
  1380. }
  1381. return $this;
  1382. }
  1383. return parent::setOrder($attribute, $dir);
  1384. }
  1385. /**
  1386. * Retrieve array of attributes
  1387. *
  1388. * @param array $arrAttributes
  1389. * @return array
  1390. */
  1391. public function toArray($arrAttributes = [])
  1392. {
  1393. $arr = [];
  1394. foreach ($this->getItems() as $key => $item) {
  1395. $arr[$key] = $item->toArray($arrAttributes);
  1396. }
  1397. return $arr;
  1398. }
  1399. /**
  1400. * Treat "order by" items as attributes to sort
  1401. *
  1402. * @return $this
  1403. */
  1404. protected function _renderOrders()
  1405. {
  1406. if (!$this->_isOrdersRendered) {
  1407. foreach ($this->_orders as $attribute => $direction) {
  1408. $this->addAttributeToSort($attribute, $direction);
  1409. }
  1410. $this->_isOrdersRendered = true;
  1411. }
  1412. return $this;
  1413. }
  1414. /**
  1415. * After load method
  1416. *
  1417. * @return $this
  1418. * @codeCoverageIgnore
  1419. */
  1420. protected function _afterLoad()
  1421. {
  1422. return $this;
  1423. }
  1424. /**
  1425. * Reset collection
  1426. *
  1427. * @return $this
  1428. */
  1429. protected function _reset()
  1430. {
  1431. parent::_reset();
  1432. $this->_selectEntityTypes = [];
  1433. $this->_selectAttributes = [];
  1434. $this->_filterAttributes = [];
  1435. $this->_joinEntities = [];
  1436. $this->_joinAttributes = [];
  1437. $this->_joinFields = [];
  1438. return $this;
  1439. }
  1440. /**
  1441. * Check whether attribute with code is already added to collection
  1442. *
  1443. * @param string $attributeCode
  1444. * @return bool
  1445. * @since 102.0.0
  1446. */
  1447. public function isAttributeAdded($attributeCode) : bool
  1448. {
  1449. return isset($this->_selectAttributes[$attributeCode]);
  1450. }
  1451. /**
  1452. * Returns already loaded element ids
  1453. *
  1454. * @return array
  1455. * @codeCoverageIgnore
  1456. */
  1457. public function getLoadedIds()
  1458. {
  1459. return array_keys($this->_items);
  1460. }
  1461. /**
  1462. * Clear collection
  1463. *
  1464. * @return $this
  1465. */
  1466. public function clear()
  1467. {
  1468. $this->_itemsById = [];
  1469. return parent::clear();
  1470. }
  1471. /**
  1472. * Remove all items from collection
  1473. *
  1474. * @return $this
  1475. */
  1476. public function removeAllItems()
  1477. {
  1478. $this->_itemsById = [];
  1479. return parent::removeAllItems();
  1480. }
  1481. /**
  1482. * Remove item from collection by item key
  1483. *
  1484. * @param mixed $key
  1485. * @return $this
  1486. */
  1487. public function removeItemByKey($key)
  1488. {
  1489. if (isset($this->_items[$key])) {
  1490. unset($this->_itemsById[$this->_items[$key]->getId()]);
  1491. }
  1492. return parent::removeItemByKey($key);
  1493. }
  1494. /**
  1495. * Returns main table.
  1496. * Returns main table name - extracted from "module/table" style and
  1497. * validated by db adapter
  1498. *
  1499. * @return string
  1500. */
  1501. public function getMainTable()
  1502. {
  1503. return $this->getSelect()->getPart(Select::FROM)['e']['tableName'];
  1504. }
  1505. /**
  1506. * Wrapper for compatibility with \Magento\Framework\Data\Collection\AbstractDb
  1507. *
  1508. * @param string $field
  1509. * @param string $alias
  1510. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  1511. * @return $this|\Magento\Framework\Data\Collection\AbstractDb
  1512. * @SuppressWarnings(PHPMD.UnusedFormalParameter)
  1513. * @codeCoverageIgnore
  1514. */
  1515. public function addFieldToSelect($field, $alias = null)
  1516. {
  1517. return $this->addAttributeToSelect($field);
  1518. }
  1519. /**
  1520. * Wrapper for compatibility with \Magento\Framework\Data\Collection\AbstractDb
  1521. *
  1522. * @param string $field
  1523. * @return $this|\Magento\Framework\Data\Collection\AbstractDb
  1524. * @codeCoverageIgnore
  1525. */
  1526. public function removeFieldFromSelect($field)
  1527. {
  1528. return $this->removeAttributeToSelect($field);
  1529. }
  1530. /**
  1531. * Wrapper for compatibility with \Magento\Framework\Data\Collection\AbstractDb
  1532. *
  1533. * @return $this|\Magento\Framework\Data\Collection\AbstractDb
  1534. * @codeCoverageIgnore
  1535. */
  1536. public function removeAllFieldsFromSelect()
  1537. {
  1538. return $this->removeAttributeToSelect();
  1539. }
  1540. /**
  1541. * Invalidates "Total Records Count".
  1542. * Invalidates saved "Total Records Count" attribute with last counting,
  1543. * so a next calling of method getSize() will query new total records count.
  1544. *
  1545. * @return void
  1546. */
  1547. private function invalidateSize(): void
  1548. {
  1549. $this->_totalRecords = null;
  1550. }
  1551. }