vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/EntityReader.php line 1154

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
  3. use Doctrine\DBAL\Connection;
  4. use Psr\Log\LoggerInterface;
  5. use Shopware\Core\Framework\Context;
  6. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Exception\ParentAssociationCanNotBeFetched;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Entity;
  8. use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\ChildrenAssociationField;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\CascadeDelete;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Extension;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Runtime;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\JsonField;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyIdField;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToManyAssociationField;
  23. use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
  24. use Shopware\Core\Framework\DataAbstractionLayer\Field\ParentAssociationField;
  25. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  26. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
  27. use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
  28. use Shopware\Core\Framework\DataAbstractionLayer\Read\EntityReaderInterface;
  29. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  30. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsAnyFilter;
  31. use Shopware\Core\Framework\DataAbstractionLayer\Search\Parser\SqlQueryParser;
  32. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  33. use Shopware\Core\Framework\Log\Package;
  34. use Shopware\Core\Framework\Struct\ArrayEntity;
  35. use Shopware\Core\Framework\Struct\ArrayStruct;
  36. use Shopware\Core\Framework\Uuid\Uuid;
  37. use function array_filter;
  38. /**
  39. * @deprecated tag:v6.5.0 - reason:becomes-internal - Will be internal
  40. */
  41. #[Package('core')]
  42. class EntityReader implements EntityReaderInterface
  43. {
  44. public const INTERNAL_MAPPING_STORAGE = 'internal_mapping_storage';
  45. public const FOREIGN_KEYS = 'foreignKeys';
  46. public const MANY_TO_MANY_LIMIT_QUERY = 'many_to_many_limit_query';
  47. private Connection $connection;
  48. private EntityHydrator $hydrator;
  49. private EntityDefinitionQueryHelper $queryHelper;
  50. private SqlQueryParser $parser;
  51. private CriteriaQueryBuilder $criteriaQueryBuilder;
  52. private LoggerInterface $logger;
  53. public function __construct(
  54. Connection $connection,
  55. EntityHydrator $hydrator,
  56. EntityDefinitionQueryHelper $queryHelper,
  57. SqlQueryParser $parser,
  58. CriteriaQueryBuilder $criteriaQueryBuilder,
  59. LoggerInterface $logger
  60. ) {
  61. $this->connection = $connection;
  62. $this->hydrator = $hydrator;
  63. $this->queryHelper = $queryHelper;
  64. $this->parser = $parser;
  65. $this->criteriaQueryBuilder = $criteriaQueryBuilder;
  66. $this->logger = $logger;
  67. }
  68. /**
  69. * @return EntityCollection<Entity>
  70. */
  71. public function read(EntityDefinition $definition, Criteria $criteria, Context $context): EntityCollection
  72. {
  73. $criteria->resetSorting();
  74. $criteria->resetQueries();
  75. /** @var EntityCollection<Entity> $collectionClass */
  76. $collectionClass = $definition->getCollectionClass();
  77. $fields = $this->buildCriteriaFields($criteria, $definition);
  78. return $this->_read(
  79. $criteria,
  80. $definition,
  81. $context,
  82. new $collectionClass(),
  83. $definition->getFields()->getBasicFields(),
  84. true,
  85. $fields
  86. );
  87. }
  88. protected function getParser(): SqlQueryParser
  89. {
  90. return $this->parser;
  91. }
  92. /**
  93. * @param EntityCollection<Entity> $collection
  94. *
  95. * @return EntityCollection<Entity>
  96. */
  97. private function _read(
  98. Criteria $criteria,
  99. EntityDefinition $definition,
  100. Context $context,
  101. EntityCollection $collection,
  102. FieldCollection $fields,
  103. bool $performEmptySearch = false,
  104. array $partial = []
  105. ): EntityCollection {
  106. $hasFilters = !empty($criteria->getFilters()) || !empty($criteria->getPostFilters());
  107. $hasIds = !empty($criteria->getIds());
  108. if (!$performEmptySearch && !$hasFilters && !$hasIds) {
  109. return $collection;
  110. }
  111. if ($partial !== []) {
  112. $fields = $definition->getFields()->filter(function (Field $field) use (&$partial) {
  113. if ($field->getFlag(PrimaryKey::class) || $field instanceof ManyToManyIdField) {
  114. $partial[$field->getPropertyName()] = [];
  115. return true;
  116. }
  117. return isset($partial[$field->getPropertyName()]);
  118. });
  119. }
  120. // always add the criteria fields to the collection, otherwise we have conflicts between criteria.fields and criteria.association logic
  121. $fields = $this->addAssociationFieldsToCriteria($criteria, $definition, $fields);
  122. if ($definition->isInheritanceAware() && $criteria->hasAssociation('parent')) {
  123. throw new ParentAssociationCanNotBeFetched();
  124. }
  125. $rows = $this->fetch($criteria, $definition, $context, $fields, $partial);
  126. $collection = $this->hydrator->hydrate($collection, $definition->getEntityClass(), $definition, $rows, $definition->getEntityName(), $context, $partial);
  127. $collection = $this->fetchAssociations($criteria, $definition, $context, $collection, $fields, $partial);
  128. $hasIds = !empty($criteria->getIds());
  129. if ($hasIds && empty($criteria->getSorting())) {
  130. $collection->sortByIdArray($criteria->getIds());
  131. }
  132. return $collection;
  133. }
  134. private function joinBasic(
  135. EntityDefinition $definition,
  136. Context $context,
  137. string $root,
  138. QueryBuilder $query,
  139. FieldCollection $fields,
  140. ?Criteria $criteria = null,
  141. array $partial = []
  142. ): void {
  143. $isPartial = $partial !== [];
  144. $filtered = $fields->filter(static function (Field $field) use ($isPartial, $partial) {
  145. if ($field->is(Runtime::class)) {
  146. return false;
  147. }
  148. if (!$isPartial || $field->getFlag(PrimaryKey::class)) {
  149. return true;
  150. }
  151. return isset($partial[$field->getPropertyName()]);
  152. });
  153. $parentAssociation = null;
  154. if ($definition->isInheritanceAware() && $context->considerInheritance()) {
  155. $parentAssociation = $definition->getFields()->get('parent');
  156. if ($parentAssociation !== null) {
  157. $this->queryHelper->resolveField($parentAssociation, $definition, $root, $query, $context);
  158. }
  159. }
  160. $addTranslation = false;
  161. /** @var Field $field */
  162. foreach ($filtered as $field) {
  163. //translated fields are handled after loop all together
  164. if ($field instanceof TranslatedField) {
  165. $this->queryHelper->resolveField($field, $definition, $root, $query, $context);
  166. $addTranslation = true;
  167. continue;
  168. }
  169. //self references can not be resolved if set to autoload, otherwise we get an endless loop
  170. if (!$field instanceof ParentAssociationField && $field instanceof AssociationField && $field->getAutoload() && $field->getReferenceDefinition() === $definition) {
  171. continue;
  172. }
  173. //many to one associations can be directly fetched in same query
  174. if ($field instanceof ManyToOneAssociationField || $field instanceof OneToOneAssociationField) {
  175. $reference = $field->getReferenceDefinition();
  176. $basics = $reference->getFields()->getBasicFields();
  177. $this->queryHelper->resolveField($field, $definition, $root, $query, $context);
  178. $alias = $root . '.' . $field->getPropertyName();
  179. $joinCriteria = null;
  180. if ($criteria && $criteria->hasAssociation($field->getPropertyName())) {
  181. $joinCriteria = $criteria->getAssociation($field->getPropertyName());
  182. $basics = $this->addAssociationFieldsToCriteria($joinCriteria, $reference, $basics);
  183. }
  184. $this->joinBasic($reference, $context, $alias, $query, $basics, $joinCriteria, $partial[$field->getPropertyName()] ?? []);
  185. continue;
  186. }
  187. //add sub select for many to many field
  188. if ($field instanceof ManyToManyAssociationField) {
  189. if ($this->isAssociationRestricted($criteria, $field->getPropertyName())) {
  190. continue;
  191. }
  192. //requested a paginated, filtered or sorted list
  193. $this->addManyToManySelect($definition, $root, $field, $query, $context);
  194. continue;
  195. }
  196. //other associations like OneToManyAssociationField fetched lazy by additional query
  197. if ($field instanceof AssociationField) {
  198. continue;
  199. }
  200. if ($parentAssociation !== null
  201. && $field instanceof StorageAware
  202. && $field->is(Inherited::class)
  203. && $context->considerInheritance()
  204. ) {
  205. $parentAlias = $root . '.' . $parentAssociation->getPropertyName();
  206. //contains the field accessor for the child value (eg. `product.name`.`name`)
  207. $childAccessor = EntityDefinitionQueryHelper::escape($root) . '.'
  208. . EntityDefinitionQueryHelper::escape($field->getStorageName());
  209. //contains the field accessor for the parent value (eg. `product.parent`.`name`)
  210. $parentAccessor = EntityDefinitionQueryHelper::escape($parentAlias) . '.'
  211. . EntityDefinitionQueryHelper::escape($field->getStorageName());
  212. //contains the alias for the resolved field (eg. `product.name`)
  213. $fieldAlias = EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName());
  214. if ($field instanceof JsonField) {
  215. // merged in hydrator
  216. $parentFieldAlias = EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName() . '.inherited');
  217. $query->addSelect(sprintf('%s as %s', $parentAccessor, $parentFieldAlias));
  218. }
  219. //add selection for resolved parent-child inheritance field
  220. $query->addSelect(sprintf('COALESCE(%s, %s) as %s', $childAccessor, $parentAccessor, $fieldAlias));
  221. continue;
  222. }
  223. //all other StorageAware fields are stored inside the main entity
  224. if ($field instanceof StorageAware) {
  225. $query->addSelect(
  226. EntityDefinitionQueryHelper::escape($root) . '.'
  227. . EntityDefinitionQueryHelper::escape($field->getStorageName()) . ' as '
  228. . EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName())
  229. );
  230. }
  231. }
  232. if ($addTranslation) {
  233. $this->queryHelper->addTranslationSelect($root, $definition, $query, $context, $partial);
  234. }
  235. }
  236. private function fetch(Criteria $criteria, EntityDefinition $definition, Context $context, FieldCollection $fields, array $partial = []): array
  237. {
  238. $table = $definition->getEntityName();
  239. $query = $this->criteriaQueryBuilder->build(
  240. new QueryBuilder($this->connection),
  241. $definition,
  242. $criteria,
  243. $context
  244. );
  245. $this->joinBasic($definition, $context, $table, $query, $fields, $criteria, $partial);
  246. if (!empty($criteria->getIds())) {
  247. $this->queryHelper->addIdCondition($criteria, $definition, $query);
  248. }
  249. if ($criteria->getTitle()) {
  250. $query->setTitle($criteria->getTitle() . '::read');
  251. }
  252. return $query->executeQuery()->fetchAllAssociative();
  253. }
  254. /**
  255. * @param EntityCollection<Entity> $collection
  256. */
  257. private function loadManyToMany(
  258. Criteria $criteria,
  259. ManyToManyAssociationField $association,
  260. Context $context,
  261. EntityCollection $collection,
  262. array $partial
  263. ): void {
  264. $associationCriteria = $criteria->getAssociation($association->getPropertyName());
  265. if (!$associationCriteria->getTitle() && $criteria->getTitle()) {
  266. $associationCriteria->setTitle(
  267. $criteria->getTitle() . '::association::' . $association->getPropertyName()
  268. );
  269. }
  270. //check if the requested criteria is restricted (limit, offset, sorting, filtering)
  271. if ($this->isAssociationRestricted($criteria, $association->getPropertyName())) {
  272. //if restricted load paginated list of many to many
  273. $this->loadManyToManyWithCriteria($associationCriteria, $association, $context, $collection, $partial);
  274. return;
  275. }
  276. //otherwise the association is loaded in the root query of the entity as sub select which contains all ids
  277. //the ids are extracted in the entity hydrator (see: \Shopware\Core\Framework\DataAbstractionLayer\Dbal\EntityHydrator::extractManyToManyIds)
  278. $this->loadManyToManyOverExtension($associationCriteria, $association, $context, $collection, $partial);
  279. }
  280. private function addManyToManySelect(
  281. EntityDefinition $definition,
  282. string $root,
  283. ManyToManyAssociationField $field,
  284. QueryBuilder $query,
  285. Context $context
  286. ): void {
  287. $mapping = $field->getMappingDefinition();
  288. $versionCondition = '';
  289. if ($mapping->isVersionAware() && $definition->isVersionAware() && $field->is(CascadeDelete::class)) {
  290. $versionField = $definition->getEntityName() . '_version_id';
  291. $versionCondition = ' AND #alias#.' . $versionField . ' = #root#.version_id';
  292. }
  293. $source = EntityDefinitionQueryHelper::escape($root) . '.' . EntityDefinitionQueryHelper::escape($field->getLocalField());
  294. if ($field->is(Inherited::class) && $context->considerInheritance()) {
  295. $source = EntityDefinitionQueryHelper::escape($root) . '.' . EntityDefinitionQueryHelper::escape($field->getPropertyName());
  296. }
  297. $parameters = [
  298. '#alias#' => EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName() . '.mapping'),
  299. '#mapping_reference_column#' => EntityDefinitionQueryHelper::escape($field->getMappingReferenceColumn()),
  300. '#mapping_table#' => EntityDefinitionQueryHelper::escape($mapping->getEntityName()),
  301. '#mapping_local_column#' => EntityDefinitionQueryHelper::escape($field->getMappingLocalColumn()),
  302. '#root#' => EntityDefinitionQueryHelper::escape($root),
  303. '#source#' => $source,
  304. '#property#' => EntityDefinitionQueryHelper::escape($root . '.' . $field->getPropertyName() . '.id_mapping'),
  305. ];
  306. $query->addSelect(
  307. str_replace(
  308. array_keys($parameters),
  309. array_values($parameters),
  310. '(SELECT GROUP_CONCAT(HEX(#alias#.#mapping_reference_column#) SEPARATOR \'||\')
  311. FROM #mapping_table# #alias#
  312. WHERE #alias#.#mapping_local_column# = #source#'
  313. . $versionCondition
  314. . ' ) as #property#'
  315. )
  316. );
  317. }
  318. /**
  319. * @param EntityCollection<Entity> $collection
  320. */
  321. private function collectManyToManyIds(EntityCollection $collection, AssociationField $association): array
  322. {
  323. $ids = [];
  324. $property = $association->getPropertyName();
  325. /** @var Entity $struct */
  326. foreach ($collection as $struct) {
  327. /** @var ArrayStruct<string, mixed> $ext */
  328. $ext = $struct->getExtension(self::INTERNAL_MAPPING_STORAGE);
  329. /** @var array<string> $tmp */
  330. $tmp = $ext->get($property);
  331. foreach ($tmp as $id) {
  332. $ids[] = $id;
  333. }
  334. }
  335. return $ids;
  336. }
  337. /**
  338. * @param EntityCollection<Entity> $collection
  339. */
  340. private function loadOneToMany(
  341. Criteria $criteria,
  342. EntityDefinition $definition,
  343. OneToManyAssociationField $association,
  344. Context $context,
  345. EntityCollection $collection,
  346. array $partial
  347. ): void {
  348. $fieldCriteria = new Criteria();
  349. if ($criteria->hasAssociation($association->getPropertyName())) {
  350. $fieldCriteria = $criteria->getAssociation($association->getPropertyName());
  351. }
  352. if (!$fieldCriteria->getTitle() && $criteria->getTitle()) {
  353. $fieldCriteria->setTitle(
  354. $criteria->getTitle() . '::association::' . $association->getPropertyName()
  355. );
  356. }
  357. //association should not be paginated > load data over foreign key condition
  358. if ($fieldCriteria->getLimit() === null) {
  359. $this->loadOneToManyWithoutPagination($definition, $association, $context, $collection, $fieldCriteria, $partial);
  360. return;
  361. }
  362. //load association paginated > use internal counter loops
  363. $this->loadOneToManyWithPagination($definition, $association, $context, $collection, $fieldCriteria, $partial);
  364. }
  365. /**
  366. * @param EntityCollection<Entity> $collection
  367. */
  368. private function loadOneToManyWithoutPagination(
  369. EntityDefinition $definition,
  370. OneToManyAssociationField $association,
  371. Context $context,
  372. EntityCollection $collection,
  373. Criteria $fieldCriteria,
  374. array $partial
  375. ): void {
  376. $ref = $association->getReferenceDefinition()->getFields()->getByStorageName(
  377. $association->getReferenceField()
  378. );
  379. \assert($ref instanceof Field);
  380. $propertyName = $ref->getPropertyName();
  381. if ($association instanceof ChildrenAssociationField) {
  382. $propertyName = 'parentId';
  383. }
  384. //build orm property accessor to add field sortings and conditions `customer_address.customerId`
  385. $propertyAccessor = $association->getReferenceDefinition()->getEntityName() . '.' . $propertyName;
  386. $ids = array_values($collection->getIds());
  387. $isInheritanceAware = $definition->isInheritanceAware() && $context->considerInheritance();
  388. if ($isInheritanceAware) {
  389. $parentIds = array_values(array_filter($collection->map(function (Entity $entity) {
  390. return $entity->get('parentId');
  391. })));
  392. $ids = array_unique(array_merge($ids, $parentIds));
  393. }
  394. $fieldCriteria->addFilter(new EqualsAnyFilter($propertyAccessor, $ids));
  395. $referenceClass = $association->getReferenceDefinition();
  396. /** @var EntityCollection<Entity> $collectionClass */
  397. $collectionClass = $referenceClass->getCollectionClass();
  398. if ($partial !== []) {
  399. // Make sure our collection index will be loaded
  400. $partial[$propertyName] = [];
  401. $collectionClass = EntityCollection::class;
  402. }
  403. $data = $this->_read(
  404. $fieldCriteria,
  405. $referenceClass,
  406. $context,
  407. new $collectionClass(),
  408. $referenceClass->getFields()->getBasicFields(),
  409. false,
  410. $partial
  411. );
  412. $grouped = [];
  413. foreach ($data as $entity) {
  414. $fk = $entity->get($propertyName);
  415. $grouped[$fk][] = $entity;
  416. }
  417. //assign loaded data to root entities
  418. foreach ($collection as $entity) {
  419. $structData = new $collectionClass();
  420. if (isset($grouped[$entity->getUniqueIdentifier()])) {
  421. $structData->fill($grouped[$entity->getUniqueIdentifier()]);
  422. }
  423. //assign data of child immediately
  424. if ($association->is(Extension::class)) {
  425. $entity->addExtension($association->getPropertyName(), $structData);
  426. } else {
  427. //otherwise the data will be assigned directly as properties
  428. $entity->assign([$association->getPropertyName() => $structData]);
  429. }
  430. if (!$association->is(Inherited::class) || $structData->count() > 0 || !$context->considerInheritance()) {
  431. continue;
  432. }
  433. //if association can be inherited by the parent and the struct data is empty, filter again for the parent id
  434. $structData = new $collectionClass();
  435. if (isset($grouped[$entity->get('parentId')])) {
  436. $structData->fill($grouped[$entity->get('parentId')]);
  437. }
  438. if ($association->is(Extension::class)) {
  439. $entity->addExtension($association->getPropertyName(), $structData);
  440. continue;
  441. }
  442. $entity->assign([$association->getPropertyName() => $structData]);
  443. }
  444. }
  445. /**
  446. * @param EntityCollection<Entity> $collection
  447. */
  448. private function loadOneToManyWithPagination(
  449. EntityDefinition $definition,
  450. OneToManyAssociationField $association,
  451. Context $context,
  452. EntityCollection $collection,
  453. Criteria $fieldCriteria,
  454. array $partial
  455. ): void {
  456. $isPartial = $partial !== [];
  457. $propertyAccessor = $this->buildOneToManyPropertyAccessor($definition, $association);
  458. // inject sorting for foreign key, otherwise the internal counter wouldn't work `order by customer_address.customer_id, other_sortings`
  459. $sorting = array_merge(
  460. [new FieldSorting($propertyAccessor, FieldSorting::ASCENDING)],
  461. $fieldCriteria->getSorting()
  462. );
  463. $fieldCriteria->resetSorting();
  464. $fieldCriteria->addSorting(...$sorting);
  465. $ids = array_values($collection->getIds());
  466. if ($isPartial) {
  467. // Make sure our collection index will be loaded
  468. $partial[$association->getPropertyName()] = [];
  469. }
  470. $isInheritanceAware = $definition->isInheritanceAware() && $context->considerInheritance();
  471. if ($isInheritanceAware) {
  472. $parentIds = array_values(array_filter($collection->map(function (Entity $entity) {
  473. return $entity->get('parentId');
  474. })));
  475. $ids = array_unique(array_merge($ids, $parentIds));
  476. }
  477. $fieldCriteria->addFilter(new EqualsAnyFilter($propertyAccessor, $ids));
  478. $mapping = $this->fetchPaginatedOneToManyMapping($definition, $association, $context, $collection, $fieldCriteria);
  479. $ids = [];
  480. foreach ($mapping as $associationIds) {
  481. foreach ($associationIds as $associationId) {
  482. $ids[] = $associationId;
  483. }
  484. }
  485. $fieldCriteria->setIds(array_filter($ids));
  486. $fieldCriteria->resetSorting();
  487. $fieldCriteria->resetFilters();
  488. $fieldCriteria->resetPostFilters();
  489. $referenceClass = $association->getReferenceDefinition();
  490. /** @var EntityCollection<Entity> $collectionClass */
  491. $collectionClass = $referenceClass->getCollectionClass();
  492. $data = $this->_read(
  493. $fieldCriteria,
  494. $referenceClass,
  495. $context,
  496. new $collectionClass(),
  497. $referenceClass->getFields()->getBasicFields(),
  498. false,
  499. $partial
  500. );
  501. //assign loaded reference collections to root entities
  502. /** @var Entity $entity */
  503. foreach ($collection as $entity) {
  504. //extract mapping ids for the current entity
  505. $mappingIds = $mapping[$entity->getUniqueIdentifier()];
  506. $structData = $data->getList($mappingIds);
  507. //assign data of child immediately
  508. if ($association->is(Extension::class)) {
  509. $entity->addExtension($association->getPropertyName(), $structData);
  510. } else {
  511. $entity->assign([$association->getPropertyName() => $structData]);
  512. }
  513. if (!$association->is(Inherited::class) || $structData->count() > 0 || !$context->considerInheritance()) {
  514. continue;
  515. }
  516. $parentId = $entity->get('parentId');
  517. if ($parentId === null) {
  518. continue;
  519. }
  520. //extract mapping ids for the current entity
  521. $mappingIds = $mapping[$parentId];
  522. $structData = $data->getList($mappingIds);
  523. //assign data of child immediately
  524. if ($association->is(Extension::class)) {
  525. $entity->addExtension($association->getPropertyName(), $structData);
  526. } else {
  527. $entity->assign([$association->getPropertyName() => $structData]);
  528. }
  529. }
  530. }
  531. /**
  532. * @param EntityCollection<Entity> $collection
  533. */
  534. private function loadManyToManyOverExtension(
  535. Criteria $criteria,
  536. ManyToManyAssociationField $association,
  537. Context $context,
  538. EntityCollection $collection,
  539. array $partial
  540. ): void {
  541. //collect all ids of many to many association which already stored inside the struct instances
  542. $ids = $this->collectManyToManyIds($collection, $association);
  543. $criteria->setIds($ids);
  544. $referenceClass = $association->getToManyReferenceDefinition();
  545. /** @var EntityCollection<Entity> $collectionClass */
  546. $collectionClass = $referenceClass->getCollectionClass();
  547. $data = $this->_read(
  548. $criteria,
  549. $referenceClass,
  550. $context,
  551. new $collectionClass(),
  552. $referenceClass->getFields()->getBasicFields(),
  553. false,
  554. $partial
  555. );
  556. /** @var Entity $struct */
  557. foreach ($collection as $struct) {
  558. /** @var ArrayEntity $extension */
  559. $extension = $struct->getExtension(self::INTERNAL_MAPPING_STORAGE);
  560. //use assign function to avoid setter name building
  561. $structData = $data->getList(
  562. $extension->get($association->getPropertyName())
  563. );
  564. //if the association is added as extension (for plugins), we have to add the data as extension
  565. if ($association->is(Extension::class)) {
  566. $struct->addExtension($association->getPropertyName(), $structData);
  567. } else {
  568. $struct->assign([$association->getPropertyName() => $structData]);
  569. }
  570. }
  571. }
  572. /**
  573. * @param EntityCollection<Entity> $collection
  574. */
  575. private function loadManyToManyWithCriteria(
  576. Criteria $fieldCriteria,
  577. ManyToManyAssociationField $association,
  578. Context $context,
  579. EntityCollection $collection,
  580. array $partial
  581. ): void {
  582. $fields = $association->getToManyReferenceDefinition()->getFields();
  583. $reference = null;
  584. foreach ($fields as $field) {
  585. if (!$field instanceof ManyToManyAssociationField) {
  586. continue;
  587. }
  588. if ($field->getReferenceDefinition() !== $association->getReferenceDefinition()) {
  589. continue;
  590. }
  591. $reference = $field;
  592. break;
  593. }
  594. if (!$reference) {
  595. throw new \RuntimeException(
  596. sprintf(
  597. 'No inverse many to many association found, for association %s',
  598. $association->getPropertyName()
  599. )
  600. );
  601. }
  602. //build inverse accessor `product.categories.id`
  603. $accessor = $association->getToManyReferenceDefinition()->getEntityName() . '.' . $reference->getPropertyName() . '.id';
  604. $fieldCriteria->addFilter(new EqualsAnyFilter($accessor, $collection->getIds()));
  605. $root = EntityDefinitionQueryHelper::escape(
  606. $association->getToManyReferenceDefinition()->getEntityName() . '.' . $reference->getPropertyName() . '.mapping'
  607. );
  608. $query = new QueryBuilder($this->connection);
  609. // to many selects results in a `group by` clause. In this case the order by parts will be executed with MIN/MAX aggregation
  610. // but at this point the order by will be moved to an sub select where we don't have a group state, the `state` prevents this behavior
  611. $query->addState(self::MANY_TO_MANY_LIMIT_QUERY);
  612. $query = $this->criteriaQueryBuilder->build(
  613. $query,
  614. $association->getToManyReferenceDefinition(),
  615. $fieldCriteria,
  616. $context
  617. );
  618. $localColumn = EntityDefinitionQueryHelper::escape($association->getMappingLocalColumn());
  619. $referenceColumn = EntityDefinitionQueryHelper::escape($association->getMappingReferenceColumn());
  620. $orderBy = '';
  621. $parts = $query->getQueryPart('orderBy');
  622. if (!empty($parts)) {
  623. $orderBy = ' ORDER BY ' . implode(', ', $parts);
  624. $query->resetQueryPart('orderBy');
  625. }
  626. // order by is handled in group_concat
  627. $fieldCriteria->resetSorting();
  628. $query->select([
  629. 'LOWER(HEX(' . $root . '.' . $localColumn . ')) as `key`',
  630. 'GROUP_CONCAT(LOWER(HEX(' . $root . '.' . $referenceColumn . ')) ' . $orderBy . ') as `value`',
  631. ]);
  632. $query->addGroupBy($root . '.' . $localColumn);
  633. if ($fieldCriteria->getLimit() !== null) {
  634. $limitQuery = $this->buildManyToManyLimitQuery($association);
  635. $params = [
  636. '#source_column#' => EntityDefinitionQueryHelper::escape($association->getMappingLocalColumn()),
  637. '#reference_column#' => EntityDefinitionQueryHelper::escape($association->getMappingReferenceColumn()),
  638. '#table#' => $root,
  639. ];
  640. $query->innerJoin(
  641. $root,
  642. '(' . $limitQuery . ')',
  643. 'counter_table',
  644. str_replace(
  645. array_keys($params),
  646. array_values($params),
  647. 'counter_table.#source_column# = #table#.#source_column# AND
  648. counter_table.#reference_column# = #table#.#reference_column# AND
  649. counter_table.id_count <= :limit'
  650. )
  651. );
  652. $query->setParameter('limit', $fieldCriteria->getLimit());
  653. $this->connection->executeQuery('SET @n = 0; SET @c = null;');
  654. }
  655. $mapping = $query->executeQuery()->fetchAllKeyValue();
  656. $ids = [];
  657. foreach ($mapping as &$row) {
  658. $row = array_filter(explode(',', $row));
  659. foreach ($row as $id) {
  660. $ids[] = $id;
  661. }
  662. }
  663. unset($row);
  664. $fieldCriteria->setIds($ids);
  665. $referenceClass = $association->getToManyReferenceDefinition();
  666. /** @var EntityCollection<Entity> $collectionClass */
  667. $collectionClass = $referenceClass->getCollectionClass();
  668. $data = $this->_read(
  669. $fieldCriteria,
  670. $referenceClass,
  671. $context,
  672. new $collectionClass(),
  673. $referenceClass->getFields()->getBasicFields(),
  674. false,
  675. $partial
  676. );
  677. /** @var Entity $struct */
  678. foreach ($collection as $struct) {
  679. $structData = new $collectionClass();
  680. $id = $struct->getUniqueIdentifier();
  681. $parentId = $struct->has('parentId') ? $struct->get('parentId') : '';
  682. if (\array_key_exists($struct->getUniqueIdentifier(), $mapping)) {
  683. //filter mapping list of whole data array
  684. $structData = $data->getList($mapping[$id]);
  685. //sort list by ids if the criteria contained a sorting
  686. $structData->sortByIdArray($mapping[$id]);
  687. } elseif (\array_key_exists($parentId, $mapping) && $association->is(Inherited::class) && $context->considerInheritance()) {
  688. //filter mapping for the inherited parent association
  689. $structData = $data->getList($mapping[$parentId]);
  690. //sort list by ids if the criteria contained a sorting
  691. $structData->sortByIdArray($mapping[$parentId]);
  692. }
  693. //if the association is added as extension (for plugins), we have to add the data as extension
  694. if ($association->is(Extension::class)) {
  695. $struct->addExtension($association->getPropertyName(), $structData);
  696. } else {
  697. $struct->assign([$association->getPropertyName() => $structData]);
  698. }
  699. }
  700. }
  701. /**
  702. * @param EntityCollection<Entity> $collection
  703. */
  704. private function fetchPaginatedOneToManyMapping(
  705. EntityDefinition $definition,
  706. OneToManyAssociationField $association,
  707. Context $context,
  708. EntityCollection $collection,
  709. Criteria $fieldCriteria
  710. ): array {
  711. $sortings = $fieldCriteria->getSorting();
  712. // Remove first entry
  713. array_shift($sortings);
  714. //build query based on provided association criteria (sortings, search, filter)
  715. $query = $this->criteriaQueryBuilder->build(
  716. new QueryBuilder($this->connection),
  717. $association->getReferenceDefinition(),
  718. $fieldCriteria,
  719. $context
  720. );
  721. $foreignKey = $association->getReferenceField();
  722. if (!$association->getReferenceDefinition()->getField('id')) {
  723. throw new \RuntimeException(
  724. sprintf(
  725. 'Paginated to many association must have an id field. No id field found for association %s.%s',
  726. $definition->getEntityName(),
  727. $association->getPropertyName()
  728. )
  729. );
  730. }
  731. //build sql accessor for foreign key field in reference table `customer_address.customer_id`
  732. $sqlAccessor = EntityDefinitionQueryHelper::escape($association->getReferenceDefinition()->getEntityName()) . '.'
  733. . EntityDefinitionQueryHelper::escape($foreignKey);
  734. $query->select(
  735. [
  736. //build select with an internal counter loop, the counter loop will be reset if the foreign key changed (this is the reason for the sorting inject above)
  737. '@n:=IF(@c=' . $sqlAccessor . ', @n+1, IF(@c:=' . $sqlAccessor . ',1,1)) as id_count',
  738. //add select for foreign key for join condition
  739. $sqlAccessor,
  740. //add primary key select to group concat them
  741. EntityDefinitionQueryHelper::escape($association->getReferenceDefinition()->getEntityName()) . '.id',
  742. ]
  743. );
  744. foreach ($query->getQueryPart('orderBy') as $i => $sorting) {
  745. // The first order is the primary key
  746. if ($i === 0) {
  747. continue;
  748. }
  749. --$i;
  750. // Strip the ASC/DESC at the end of the sort
  751. $query->addSelect(\sprintf('%s as sort_%s', substr($sorting, 0, -4), $i));
  752. }
  753. $root = EntityDefinitionQueryHelper::escape($definition->getEntityName());
  754. //create a wrapper query which select the root primary key and the grouped reference ids
  755. $wrapper = $this->connection->createQueryBuilder();
  756. $wrapper->select(
  757. [
  758. 'LOWER(HEX(' . $root . '.id)) as id',
  759. 'LOWER(HEX(child.id)) as child_id',
  760. ]
  761. );
  762. foreach ($sortings as $i => $sorting) {
  763. $wrapper->addOrderBy(sprintf('sort_%s', $i), $sorting->getDirection());
  764. }
  765. $wrapper->from($root, $root);
  766. //wrap query into a sub select to restrict the association count from the outer query
  767. $wrapper->leftJoin(
  768. $root,
  769. '(' . $query->getSQL() . ')',
  770. 'child',
  771. 'child.' . $foreignKey . ' = ' . $root . '.id AND id_count >= :offset AND id_count <= :limit'
  772. );
  773. //filter result to loaded root entities
  774. $wrapper->andWhere($root . '.id IN (:rootIds)');
  775. $bytes = $collection->map(
  776. function (Entity $entity) {
  777. return Uuid::fromHexToBytes($entity->getUniqueIdentifier());
  778. }
  779. );
  780. if ($definition->isInheritanceAware() && $context->considerInheritance()) {
  781. /** @var Entity $entity */
  782. foreach ($collection->getElements() as $entity) {
  783. if ($entity->get('parentId')) {
  784. $bytes[$entity->get('parentId')] = Uuid::fromHexToBytes($entity->get('parentId'));
  785. }
  786. }
  787. }
  788. $wrapper->setParameter('rootIds', $bytes, Connection::PARAM_STR_ARRAY);
  789. $limit = $fieldCriteria->getOffset() + $fieldCriteria->getLimit();
  790. $offset = $fieldCriteria->getOffset() + 1;
  791. $wrapper->setParameter('limit', $limit);
  792. $wrapper->setParameter('offset', $offset);
  793. foreach ($query->getParameters() as $key => $value) {
  794. $type = $query->getParameterType($key);
  795. $wrapper->setParameter($key, $value, $type);
  796. }
  797. //initials the cursor and loop counter, pdo do not allow to execute SET and SELECT in one statement
  798. $this->connection->executeQuery('SET @n = 0; SET @c = null;');
  799. $rows = $wrapper->executeQuery()->fetchAllAssociative();
  800. $grouped = [];
  801. foreach ($rows as $row) {
  802. $id = $row['id'];
  803. if (!isset($grouped[$id])) {
  804. $grouped[$id] = [];
  805. }
  806. if (empty($row['child_id'])) {
  807. continue;
  808. }
  809. $grouped[$id][] = $row['child_id'];
  810. }
  811. return $grouped;
  812. }
  813. private function buildManyToManyLimitQuery(ManyToManyAssociationField $association): QueryBuilder
  814. {
  815. $table = EntityDefinitionQueryHelper::escape($association->getMappingDefinition()->getEntityName());
  816. $sourceColumn = EntityDefinitionQueryHelper::escape($association->getMappingLocalColumn());
  817. $referenceColumn = EntityDefinitionQueryHelper::escape($association->getMappingReferenceColumn());
  818. $params = [
  819. '#table#' => $table,
  820. '#source_column#' => $sourceColumn,
  821. ];
  822. $query = new QueryBuilder($this->connection);
  823. $query->select([
  824. str_replace(
  825. array_keys($params),
  826. array_values($params),
  827. '@n:=IF(@c=#table#.#source_column#, @n+1, IF(@c:=#table#.#source_column#,1,1)) as id_count'
  828. ),
  829. $table . '.' . $referenceColumn,
  830. $table . '.' . $sourceColumn,
  831. ]);
  832. $query->from($table, $table);
  833. $query->orderBy($table . '.' . $sourceColumn);
  834. return $query;
  835. }
  836. private function buildOneToManyPropertyAccessor(EntityDefinition $definition, OneToManyAssociationField $association): string
  837. {
  838. $reference = $association->getReferenceDefinition();
  839. if ($association instanceof ChildrenAssociationField) {
  840. return $reference->getEntityName() . '.parentId';
  841. }
  842. $ref = $reference->getFields()->getByStorageName(
  843. $association->getReferenceField()
  844. );
  845. if (!$ref) {
  846. throw new \RuntimeException(
  847. sprintf(
  848. 'Reference field %s not found in definition %s for definition %s',
  849. $association->getReferenceField(),
  850. $reference->getEntityName(),
  851. $definition->getEntityName()
  852. )
  853. );
  854. }
  855. return $reference->getEntityName() . '.' . $ref->getPropertyName();
  856. }
  857. private function isAssociationRestricted(?Criteria $criteria, string $accessor): bool
  858. {
  859. if ($criteria === null) {
  860. return false;
  861. }
  862. if (!$criteria->hasAssociation($accessor)) {
  863. return false;
  864. }
  865. $fieldCriteria = $criteria->getAssociation($accessor);
  866. return $fieldCriteria->getOffset() !== null
  867. || $fieldCriteria->getLimit() !== null
  868. || !empty($fieldCriteria->getSorting())
  869. || !empty($fieldCriteria->getFilters())
  870. || !empty($fieldCriteria->getPostFilters())
  871. ;
  872. }
  873. private function addAssociationFieldsToCriteria(
  874. Criteria $criteria,
  875. EntityDefinition $definition,
  876. FieldCollection $fields
  877. ): FieldCollection {
  878. foreach ($criteria->getAssociations() as $fieldName => $_fieldCriteria) {
  879. $field = $definition->getFields()->get($fieldName);
  880. if (!$field) {
  881. $this->logger->warning(
  882. sprintf('Criteria association "%s" could not be resolved. Double check your Criteria!', $fieldName)
  883. );
  884. continue;
  885. }
  886. $fields->add($field);
  887. }
  888. return $fields;
  889. }
  890. /**
  891. * @param EntityCollection<Entity> $collection
  892. */
  893. private function loadToOne(
  894. AssociationField $association,
  895. Context $context,
  896. EntityCollection $collection,
  897. Criteria $criteria,
  898. array $partial
  899. ): void {
  900. if (!$association instanceof OneToOneAssociationField && !$association instanceof ManyToOneAssociationField) {
  901. return;
  902. }
  903. if (!$criteria->hasAssociation($association->getPropertyName())) {
  904. return;
  905. }
  906. $associationCriteria = $criteria->getAssociation($association->getPropertyName());
  907. if (!$associationCriteria->getAssociations()) {
  908. return;
  909. }
  910. if (!$associationCriteria->getTitle() && $criteria->getTitle()) {
  911. $associationCriteria->setTitle(
  912. $criteria->getTitle() . '::association::' . $association->getPropertyName()
  913. );
  914. }
  915. $related = array_filter($collection->map(function (Entity $entity) use ($association) {
  916. if ($association->is(Extension::class)) {
  917. return $entity->getExtension($association->getPropertyName());
  918. }
  919. return $entity->get($association->getPropertyName());
  920. }));
  921. $referenceDefinition = $association->getReferenceDefinition();
  922. $collectionClass = $referenceDefinition->getCollectionClass();
  923. if ($partial !== []) {
  924. $collectionClass = EntityCollection::class;
  925. }
  926. $fields = $referenceDefinition->getFields()->getBasicFields();
  927. $fields = $this->addAssociationFieldsToCriteria($associationCriteria, $referenceDefinition, $fields);
  928. // This line removes duplicate entries, so after fetchAssociations the association must be reassigned
  929. $relatedCollection = new $collectionClass();
  930. if (!$relatedCollection instanceof EntityCollection) {
  931. throw new \RuntimeException(sprintf('Collection class %s has to be an instance of EntityCollection', $collectionClass));
  932. }
  933. $relatedCollection->fill($related);
  934. $this->fetchAssociations($associationCriteria, $referenceDefinition, $context, $relatedCollection, $fields, $partial);
  935. /** @var Entity $entity */
  936. foreach ($collection as $entity) {
  937. if ($association->is(Extension::class)) {
  938. $item = $entity->getExtension($association->getPropertyName());
  939. } else {
  940. $item = $entity->get($association->getPropertyName());
  941. }
  942. /** @var Entity|null $item */
  943. if ($item === null) {
  944. continue;
  945. }
  946. if ($association->is(Extension::class)) {
  947. $entity->addExtension($association->getPropertyName(), $relatedCollection->get($item->getUniqueIdentifier()));
  948. continue;
  949. }
  950. $entity->assign([
  951. $association->getPropertyName() => $relatedCollection->get($item->getUniqueIdentifier()),
  952. ]);
  953. }
  954. }
  955. /**
  956. * @param EntityCollection<Entity> $collection
  957. *
  958. * @return EntityCollection<Entity>
  959. */
  960. private function fetchAssociations(
  961. Criteria $criteria,
  962. EntityDefinition $definition,
  963. Context $context,
  964. EntityCollection $collection,
  965. FieldCollection $fields,
  966. array $partial
  967. ): EntityCollection {
  968. if ($collection->count() <= 0) {
  969. return $collection;
  970. }
  971. foreach ($fields as $association) {
  972. if (!$association instanceof AssociationField) {
  973. continue;
  974. }
  975. if ($association instanceof OneToOneAssociationField || $association instanceof ManyToOneAssociationField) {
  976. $this->loadToOne($association, $context, $collection, $criteria, $partial[$association->getPropertyName()] ?? []);
  977. continue;
  978. }
  979. if ($association instanceof OneToManyAssociationField) {
  980. $this->loadOneToMany($criteria, $definition, $association, $context, $collection, $partial[$association->getPropertyName()] ?? []);
  981. continue;
  982. }
  983. if ($association instanceof ManyToManyAssociationField) {
  984. $this->loadManyToMany($criteria, $association, $context, $collection, $partial[$association->getPropertyName()] ?? []);
  985. }
  986. }
  987. foreach ($collection as $struct) {
  988. $struct->removeExtension(self::INTERNAL_MAPPING_STORAGE);
  989. }
  990. return $collection;
  991. }
  992. private function addAssociationsToCriteriaFields(Criteria $criteria, array &$fields): void
  993. {
  994. if ($fields === []) {
  995. return;
  996. }
  997. foreach ($criteria->getAssociations() as $fieldName => $fieldCriteria) {
  998. if (!isset($fields[$fieldName])) {
  999. $fields[$fieldName] = [];
  1000. }
  1001. $this->addAssociationsToCriteriaFields($fieldCriteria, $fields[$fieldName]);
  1002. }
  1003. }
  1004. private function buildCriteriaFields(Criteria $criteria, EntityDefinition $definition): array
  1005. {
  1006. if (empty($criteria->getFields())) {
  1007. return [];
  1008. }
  1009. $fields = [];
  1010. $this->addAssociationsToCriteriaFields($criteria, $fields);
  1011. foreach ($criteria->getFields() as $field) {
  1012. $association = EntityDefinitionQueryHelper::getFieldsOfAccessor($definition, $field, true);
  1013. if ($association !== [] && $association[0] instanceof AssociationField) {
  1014. $criteria->addAssociation($field);
  1015. }
  1016. $pointer = &$fields;
  1017. foreach (explode('.', $field) as $part) {
  1018. if (!isset($pointer[$part])) {
  1019. $pointer[$part] = [];
  1020. }
  1021. $pointer = &$pointer[$part];
  1022. }
  1023. }
  1024. return $fields;
  1025. }
  1026. }