vendor/shopware/core/Framework/DataAbstractionLayer/Dbal/EntityDefinitionQueryHelper.php line 506

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\Framework\DataAbstractionLayer\Dbal;
  3. use Doctrine\DBAL\Connection;
  4. use Ramsey\Uuid\Guid\Fields;
  5. use Shopware\Core\Defaults;
  6. use Shopware\Core\Framework\Context;
  7. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\Exception\UnmappedFieldException;
  8. use Shopware\Core\Framework\DataAbstractionLayer\Dbal\FieldResolver\FieldResolverContext;
  9. use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
  10. use Shopware\Core\Framework\DataAbstractionLayer\Field\AssociationField;
  11. use Shopware\Core\Framework\DataAbstractionLayer\Field\Field;
  12. use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField;
  17. use Shopware\Core\Framework\DataAbstractionLayer\Field\ReferenceVersionField;
  18. use Shopware\Core\Framework\DataAbstractionLayer\Field\StorageAware;
  19. use Shopware\Core\Framework\DataAbstractionLayer\Field\TranslatedField;
  20. use Shopware\Core\Framework\DataAbstractionLayer\Field\VersionField;
  21. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  22. use Shopware\Core\Framework\DataAbstractionLayer\Search\CriteriaPartInterface;
  23. use Shopware\Core\Framework\Feature;
  24. use Shopware\Core\Framework\Uuid\Uuid;
  25. /**
  26.  * This class acts only as helper/common class for all dbal operations for entity definitions.
  27.  * It knows how an association should be joined, how a parent-child inheritance should act, how translation chains work, ...
  28.  */
  29. class EntityDefinitionQueryHelper
  30. {
  31.     public const HAS_TO_MANY_JOIN 'has_to_many_join';
  32.     public static function escape(string $string): string
  33.     {
  34.         if (mb_strpos($string'`') !== false) {
  35.             throw new \InvalidArgumentException('Backtick not allowed in identifier');
  36.         }
  37.         return '`' $string '`';
  38.     }
  39.     public static function getFieldsOfAccessor(EntityDefinition $definitionstring $accessorbool $resolveTranslated true): array
  40.     {
  41.         $parts explode('.'$accessor);
  42.         if ($definition->getEntityName() === $parts[0]) {
  43.             array_shift($parts);
  44.         }
  45.         $accessorFields = [];
  46.         $source $definition;
  47.         foreach ($parts as $part) {
  48.             $fields $source->getFields();
  49.             if ($part === 'extensions') {
  50.                 continue;
  51.             }
  52.             $field $fields->get($part);
  53.             if ($field instanceof TranslatedField && $resolveTranslated) {
  54.                 $source $source->getTranslationDefinition();
  55.                 $fields $source->getFields();
  56.                 $accessorFields[] = $fields->get($part);
  57.                 continue;
  58.             }
  59.             if ($field instanceof TranslatedField && !$resolveTranslated) {
  60.                 $accessorFields[] = $field;
  61.                 continue;
  62.             }
  63.             $accessorFields[] = $field;
  64.             if (!$field instanceof AssociationField) {
  65.                 break;
  66.             }
  67.             $source $field->getReferenceDefinition();
  68.             if ($field instanceof ManyToManyAssociationField) {
  69.                 $source $field->getToManyReferenceDefinition();
  70.             }
  71.         }
  72.         return array_filter($accessorFields);
  73.     }
  74.     /**
  75.      * Returns the field instance of the provided fieldName.
  76.      *
  77.      * @example
  78.      *
  79.      * fieldName => 'product.name'
  80.      * Returns the (new TranslatedField('name')) declaration
  81.      *
  82.      * Allows additionally nested referencing
  83.      *
  84.      * fieldName => 'category.products.name'
  85.      * Returns as well the above field definition
  86.      */
  87.     public function getField(string $fieldNameEntityDefinition $definitionstring $rootbool $resolveTranslated true): ?Field
  88.     {
  89.         $original $fieldName;
  90.         $prefix $root '.';
  91.         if (mb_strpos($fieldName$prefix) === 0) {
  92.             $fieldName mb_substr($fieldNamemb_strlen($prefix));
  93.         } else {
  94.             $original $prefix $original;
  95.         }
  96.         $fields $definition->getFields();
  97.         $isAssociation mb_strpos($fieldName'.') !== false;
  98.         if (!$isAssociation && $fields->has($fieldName)) {
  99.             return $fields->get($fieldName);
  100.         }
  101.         $associationKey explode('.'$fieldName);
  102.         $associationKey array_shift($associationKey);
  103.         $field $fields->get($associationKey);
  104.         if ($field instanceof TranslatedField && $resolveTranslated) {
  105.             return self::getTranslatedField($definition$field);
  106.         }
  107.         if ($field instanceof TranslatedField) {
  108.             return $field;
  109.         }
  110.         if (!$field instanceof AssociationField) {
  111.             return $field;
  112.         }
  113.         $referenceDefinition $field->getReferenceDefinition();
  114.         if ($field instanceof ManyToManyAssociationField) {
  115.             $referenceDefinition $field->getToManyReferenceDefinition();
  116.         }
  117.         return $this->getField(
  118.             $original,
  119.             $referenceDefinition,
  120.             $root '.' $field->getPropertyName()
  121.         );
  122.     }
  123.     /**
  124.      * Builds the sql field accessor for the provided field.
  125.      *
  126.      * @example
  127.      *
  128.      * fieldName => product.taxId
  129.      * root      => product
  130.      * returns   => `product`.`tax_id`
  131.      *
  132.      * This function is also used for complex field accessors like JsonArray Field, JsonObject fields.
  133.      * It considers the translation and parent-child inheritance.
  134.      *
  135.      * fieldName => product.name
  136.      * root      => product
  137.      * return    => COALESCE(`product.translation`.`name`,`product.parent.translation`.`name`)
  138.      *
  139.      * @throws UnmappedFieldException
  140.      */
  141.     public function getFieldAccessor(string $fieldNameEntityDefinition $definitionstring $rootContext $context): string
  142.     {
  143.         $fieldName str_replace('extensions.'''$fieldName);
  144.         $original $fieldName;
  145.         $prefix $root '.';
  146.         if (mb_strpos($fieldName$prefix) === 0) {
  147.             $fieldName mb_substr($fieldNamemb_strlen($prefix));
  148.         } else {
  149.             $original $prefix $original;
  150.         }
  151.         $fields $definition->getFields();
  152.         if ($fields->has($fieldName)) {
  153.             $field $fields->get($fieldName);
  154.             return $this->buildInheritedAccessor($field$root$definition$context$fieldName);
  155.         }
  156.         $parts explode('.'$fieldName);
  157.         $associationKey array_shift($parts);
  158.         if ($associationKey === 'extensions') {
  159.             $associationKey array_shift($parts);
  160.         }
  161.         if (!$fields->has($associationKey)) {
  162.             throw new UnmappedFieldException($original$definition);
  163.         }
  164.         $field $fields->get($associationKey);
  165.         //case for json object fields, other fields has now same option to act with more point notations but hasn't to be an association field. E.g. price.gross
  166.         if (!$field instanceof AssociationField && ($field instanceof StorageAware || $field instanceof TranslatedField)) {
  167.             return $this->buildInheritedAccessor($field$root$definition$context$fieldName);
  168.         }
  169.         if (!$field instanceof AssociationField) {
  170.             throw new \RuntimeException(sprintf('Expected field "%s" to be instance of %s'$associationKeyAssociationField::class));
  171.         }
  172.         $referenceDefinition $field->getReferenceDefinition();
  173.         if ($field instanceof ManyToManyAssociationField) {
  174.             $referenceDefinition $field->getToManyReferenceDefinition();
  175.         }
  176.         return $this->getFieldAccessor(
  177.             $original,
  178.             $referenceDefinition,
  179.             $root '.' $field->getPropertyName(),
  180.             $context
  181.         );
  182.     }
  183.     public static function getAssociationPath(string $accessorEntityDefinition $definition): ?string
  184.     {
  185.         $fields self::getFieldsOfAccessor($definition$accessortrue);
  186.         $path = [];
  187.         foreach ($fields as $field) {
  188.             if (!$field instanceof AssociationField) {
  189.                 break;
  190.             }
  191.             $path[] = $field->getPropertyName();
  192.         }
  193.         if (empty($path)) {
  194.             return null;
  195.         }
  196.         return implode('.'$path);
  197.     }
  198.     /**
  199.      * Creates the basic root query for the provided entity definition and application context.
  200.      * It considers the current context version.
  201.      */
  202.     public function getBaseQuery(QueryBuilder $queryEntityDefinition $definitionContext $context): QueryBuilder
  203.     {
  204.         $table $definition->getEntityName();
  205.         $query->from(self::escape($table));
  206.         $useVersionFallback // only applies for versioned entities
  207.             $definition->isVersionAware()
  208.             // only add live fallback if the current version isn't the live version
  209.             && $context->getVersionId() !== Defaults::LIVE_VERSION
  210.             // sub entities have no live fallback
  211.             && $definition->getParentDefinition() === null;
  212.         if ($useVersionFallback) {
  213.             $this->joinVersion($query$definition$definition->getEntityName(), $context);
  214.         } elseif ($definition->isVersionAware()) {
  215.             $versionIdField array_filter(
  216.                 $definition->getPrimaryKeys()->getElements(),
  217.                 function ($f) {
  218.                     return $f instanceof VersionField || $f instanceof ReferenceVersionField;
  219.                 }
  220.             );
  221.             if (!$versionIdField) {
  222.                 throw new \RuntimeException('Missing `VersionField` in `' $definition->getClass() . '`');
  223.             }
  224.             /** @var FkField|null $versionIdField */
  225.             $versionIdField array_shift($versionIdField);
  226.             $query->andWhere(self::escape($table) . '.' self::escape($versionIdField->getStorageName()) . ' = :version');
  227.             $query->setParameter('version'Uuid::fromHexToBytes($context->getVersionId()));
  228.         }
  229.         return $query;
  230.     }
  231.     /**
  232.      * Used for dynamic sql joins. In case that the given fieldName is unknown or event nested with multiple association
  233.      * roots, the function can resolve each association part of the field name, even if one part of the fieldName contains a translation or event inherited data field.
  234.      */
  235.     public function resolveAccessor(
  236.         string $accessor,
  237.         EntityDefinition $definition,
  238.         string $root,
  239.         QueryBuilder $query,
  240.         Context $context,
  241.         ?CriteriaPartInterface $criteriaPart null
  242.     ): void {
  243.         $accessor str_replace('extensions.'''$accessor);
  244.         $parts explode('.'$accessor);
  245.         if ($parts[0] === $root) {
  246.             unset($parts[0]);
  247.         }
  248.         $alias $root;
  249.         $path = [$root];
  250.         $rootDefinition $definition;
  251.         foreach ($parts as $part) {
  252.             $field $definition->getFields()->get($part);
  253.             if ($field === null) {
  254.                 return;
  255.             }
  256.             $resolver $field->getResolver();
  257.             if ($resolver === null) {
  258.                 continue;
  259.             }
  260.             if ($field instanceof AssociationField) {
  261.                 $path[] = $field->getPropertyName();
  262.             }
  263.             $currentPath implode('.'$path);
  264.             $resolverContext = new FieldResolverContext($currentPath$alias$field$definition$rootDefinition$query$context$criteriaPart);
  265.             $alias $this->callResolver($resolverContext);
  266.             if (!$field instanceof AssociationField) {
  267.                 return;
  268.             }
  269.             $definition $field->getReferenceDefinition();
  270.             if ($field instanceof ManyToManyAssociationField) {
  271.                 $definition $field->getToManyReferenceDefinition();
  272.             }
  273.             if ($definition->isInheritanceAware() && $context->considerInheritance() && $parent $definition->getField('parent')) {
  274.                 $resolverContext = new FieldResolverContext($currentPath$alias$parent$definition$rootDefinition$query$context$criteriaPart);
  275.                 $this->callResolver($resolverContext);
  276.             }
  277.         }
  278.     }
  279.     public function resolveField(Field $fieldEntityDefinition $definitionstring $rootQueryBuilder $queryContext $context): void
  280.     {
  281.         $resolver $field->getResolver();
  282.         if ($resolver === null) {
  283.             return;
  284.         }
  285.         $resolver->join(new FieldResolverContext($root$root$field$definition$definition$query$contextnull));
  286.     }
  287.     /**
  288.      * Adds the full translation select part to the provided sql query.
  289.      * Considers the parent-child inheritance and provided context language inheritance.
  290.      * The raw parameter allows to skip the parent-child inheritance.
  291.      */
  292.     public function addTranslationSelect(string $rootEntityDefinition $definitionQueryBuilder $queryContext $context, array $partial = []): void
  293.     {
  294.         $translationDefinition $definition->getTranslationDefinition();
  295.         if (!$translationDefinition) {
  296.             return;
  297.         }
  298.         $fields $translationDefinition->getFields();
  299.         if (!empty($partial)) {
  300.             $fields $translationDefinition->getFields()->filter(function (Field $field) use ($partial) {
  301.                 return $field->is(PrimaryKey::class)
  302.                     || isset($partial[$field->getPropertyName()])
  303.                     || $field instanceof FkField;
  304.             });
  305.         }
  306.         $inherited $context->considerInheritance() && $definition->isInheritanceAware();
  307.         $chain EntityDefinitionQueryHelper::buildTranslationChain($root$context$inherited);
  308.         /** @var TranslatedField $field */
  309.         foreach ($fields as $field) {
  310.             if (!$field instanceof StorageAware) {
  311.                 continue;
  312.             }
  313.             $selects = [];
  314.             foreach ($chain as $select) {
  315.                 $vars = [
  316.                     '#root#' => $select,
  317.                     '#field#' => $field->getPropertyName(),
  318.                 ];
  319.                 $query->addSelect(str_replace(
  320.                     array_keys($vars),
  321.                     array_values($vars),
  322.                     EntityDefinitionQueryHelper::escape('#root#.#field#')
  323.                 ));
  324.                 $selects[] = str_replace(
  325.                     array_keys($vars),
  326.                     array_values($vars),
  327.                     self::escape('#root#.#field#')
  328.                 );
  329.             }
  330.             //check if current field is a translated field of the origin definition
  331.             $origin $definition->getFields()->get($field->getPropertyName());
  332.             if (!$origin instanceof TranslatedField) {
  333.                 continue;
  334.             }
  335.             $selects[] = self::escape($root '.translation.' $field->getPropertyName());
  336.             //add selection for resolved parent-child and language inheritance
  337.             $query->addSelect(
  338.                 sprintf('COALESCE(%s)'implode(','$selects)) . ' as '
  339.                 self::escape($root '.' $field->getPropertyName())
  340.             );
  341.         }
  342.     }
  343.     public function joinVersion(QueryBuilder $queryEntityDefinition $definitionstring $rootContext $context): void
  344.     {
  345.         $table $definition->getEntityName();
  346.         $versionRoot $root '_version';
  347.         $query->andWhere(
  348.             str_replace(
  349.                 ['#root#''#table#''#version#'],
  350.                 [self::escape($root), self::escape($table), self::escape($versionRoot)],
  351.                 '#root#.version_id = COALESCE(
  352.                     (SELECT DISTINCT version_id FROM #table# AS #version# WHERE #version#.`id` = #root#.`id` AND `version_id` = :version),
  353.                     :liveVersion
  354.                 )'
  355.             )
  356.         );
  357.         $query->setParameter('liveVersion'Uuid::fromHexToBytes(Defaults::LIVE_VERSION));
  358.         $query->setParameter('version'Uuid::fromHexToBytes($context->getVersionId()));
  359.     }
  360.     public static function getTranslatedField(EntityDefinition $definitionTranslatedField $translatedField): Field
  361.     {
  362.         $translationDefinition $definition->getTranslationDefinition();
  363.         if ($translationDefinition === null) {
  364.             throw new \RuntimeException(sprintf('Entity %s has no translation definition'$definition->getEntityName()));
  365.         }
  366.         $field $translationDefinition->getFields()->get($translatedField->getPropertyName());
  367.         if ($field === null || !$field instanceof StorageAware || !$field instanceof Field) {
  368.             throw new \RuntimeException(
  369.                 sprintf(
  370.                     'Missing translated storage aware property %s in %s',
  371.                     $translatedField->getPropertyName(),
  372.                     $translationDefinition->getClass()
  373.                 )
  374.             );
  375.         }
  376.         return $field;
  377.     }
  378.     public static function buildTranslationChain(string $rootContext $contextbool $includeParent): array
  379.     {
  380.         $count \count($context->getLanguageIdChain()) - 1;
  381.         for ($i $count$i >= 1; --$i) {
  382.             $chain[] = $root '.translation.fallback_' $i;
  383.             if ($includeParent) {
  384.                 $chain[] = $root '.parent.translation.fallback_' $i;
  385.             }
  386.         }
  387.         $chain[] = $root '.translation';
  388.         if ($includeParent) {
  389.             $chain[] = $root '.parent.translation';
  390.         }
  391.         return $chain;
  392.     }
  393.     public function addIdCondition(Criteria $criteriaEntityDefinition $definitionQueryBuilder $query): void
  394.     {
  395.         $primaryKeys $criteria->getIds();
  396.         $primaryKeys array_values($primaryKeys);
  397.         if (empty($primaryKeys)) {
  398.             return;
  399.         }
  400.         if (!\is_array($primaryKeys[0]) || \count($primaryKeys[0]) === 1) {
  401.             $primaryKeyField $definition->getPrimaryKeys()->first();
  402.             /** @feature-deprecated (flag:FEATURE_NEXT_14872) remove FeatureCheck
  403.              * if ($primaryKeyField instanceof IdField || $primaryKeyField instanceof FkField) {
  404.              */
  405.             if ($primaryKeyField instanceof IdField || (Feature::isActive('FEATURE_NEXT_14872') && $primaryKeyField instanceof FkField)) {
  406.                 $primaryKeys array_map(function ($id) {
  407.                     if (\is_array($id)) {
  408.                         /** @var string $shiftedId */
  409.                         $shiftedId array_shift($id);
  410.                         return Uuid::fromHexToBytes($shiftedId);
  411.                     }
  412.                     return Uuid::fromHexToBytes($id);
  413.                 }, $primaryKeys);
  414.             }
  415.             if (!$primaryKeyField instanceof StorageAware) {
  416.                 throw new \RuntimeException('Primary key fields has to be an instance of StorageAware');
  417.             }
  418.             $query->andWhere(sprintf(
  419.                 '%s.%s IN (:ids)',
  420.                 EntityDefinitionQueryHelper::escape($definition->getEntityName()),
  421.                 EntityDefinitionQueryHelper::escape($primaryKeyField->getStorageName())
  422.             ));
  423.             $query->setParameter('ids'$primaryKeysConnection::PARAM_STR_ARRAY);
  424.             return;
  425.         }
  426.         $this->addIdConditionWithOr($criteria$definition$query);
  427.     }
  428.     private function callResolver(FieldResolverContext $context): string
  429.     {
  430.         $resolver $context->getField()->getResolver();
  431.         if (!$resolver) {
  432.             return $context->getAlias();
  433.         }
  434.         return $resolver->join($context);
  435.     }
  436.     private function addIdConditionWithOr(Criteria $criteriaEntityDefinition $definitionQueryBuilder $query): void
  437.     {
  438.         $wheres = [];
  439.         foreach ($criteria->getIds() as $primaryKey) {
  440.             if (!\is_array($primaryKey)) {
  441.                 $primaryKey = ['id' => $primaryKey];
  442.             }
  443.             $where = [];
  444.             foreach ($primaryKey as $propertyName => $value) {
  445.                 $field $definition->getFields()->get($propertyName);
  446.                 /*
  447.                  * @deprecated tag:v6.5.0 - with 6.5.0 the only passing the propertyName will be supported
  448.                  */
  449.                 if (!$field) {
  450.                     $field $definition->getFields()->getByStorageName($propertyName);
  451.                 }
  452.                 if (!$field) {
  453.                     throw new UnmappedFieldException($propertyName$definition);
  454.                 }
  455.                 if (!$field instanceof StorageAware) {
  456.                     throw new \RuntimeException('Only storage aware fields are supported in read condition');
  457.                 }
  458.                 if ($field instanceof IdField || $field instanceof FkField) {
  459.                     $value Uuid::fromHexToBytes($value);
  460.                 }
  461.                 $key 'pk' Uuid::randomHex();
  462.                 $accessor EntityDefinitionQueryHelper::escape($definition->getEntityName()) . '.' EntityDefinitionQueryHelper::escape($field->getStorageName());
  463.                 /*
  464.                  * @deprecated tag:v6.5.0 - check for duplication in accessors will be removed,
  465.                  * when we only support propertyNames to be used in search and when IdSearchResult only returns the propertyNames
  466.                  */
  467.                 if (!\array_key_exists($accessor$where)) {
  468.                     $where[$accessor] = $accessor ' = :' $key;
  469.                     $query->setParameter($key$value);
  470.                 }
  471.             }
  472.             $wheres[] = '(' implode(' AND '$where) . ')';
  473.         }
  474.         $wheres implode(' OR '$wheres);
  475.         $query->andWhere($wheres);
  476.     }
  477.     private function getTranslationFieldAccessor(Field $fieldstring $accessor, array $chainContext $context): string
  478.     {
  479.         if (!$field instanceof StorageAware) {
  480.             throw new \RuntimeException('Only storage aware fields are supported as translated field');
  481.         }
  482.         $selects = [];
  483.         foreach ($chain as $part) {
  484.             $select $this->buildFieldSelector($part$field$context$accessor);
  485.             $selects[] = str_replace(
  486.                 '`.' self::escape($field->getStorageName()),
  487.                 '.' $field->getPropertyName() . '`',
  488.                 $select
  489.             );
  490.         }
  491.         /*
  492.          * Simplified Example:
  493.          * COALESCE(
  494.              JSON_UNQUOTE(JSON_EXTRACT(`tbl.translation.fallback_2`.`translated_attributes`, '$.path')) AS datetime(3), # child language
  495.              JSON_UNQUOTE(JSON_EXTRACT(`tbl.translation.fallback_1`.`translated_attributes`, '$.path')) AS datetime(3), # root language
  496.              JSON_UNQUOTE(JSON_EXTRACT(`tbl.translation`.`translated_attributes`, '$.path')) AS datetime(3) # system language
  497.            );
  498.          */
  499.         return sprintf('COALESCE(%s)'implode(','$selects));
  500.     }
  501.     private function buildInheritedAccessor(
  502.         Field $field,
  503.         string $root,
  504.         EntityDefinition $definition,
  505.         Context $context,
  506.         string $original
  507.     ): string {
  508.         if ($field instanceof TranslatedField) {
  509.             $inheritedChain self::buildTranslationChain($root$context$definition->isInheritanceAware() && $context->considerInheritance());
  510.             $translatedField self::getTranslatedField($definition$field);
  511.             return $this->getTranslationFieldAccessor($translatedField$original$inheritedChain$context);
  512.         }
  513.         $select $this->buildFieldSelector($root$field$context$original);
  514.         if (!$field->is(Inherited::class) || !$context->considerInheritance()) {
  515.             return $select;
  516.         }
  517.         $parentSelect $this->buildFieldSelector($root '.parent'$field$context$original);
  518.         return sprintf('IFNULL(%s, %s)'$select$parentSelect);
  519.     }
  520.     private function buildFieldSelector(string $rootField $fieldContext $contextstring $accessor): string
  521.     {
  522.         return $field->getAccessorBuilder()->buildAccessor($root$field$context$accessor);
  523.     }
  524. }