diff --git a/src/Enum/SearchIndex/FieldCategory.php b/src/Enum/SearchIndex/FieldCategory.php index 914f43f9..0d72dc74 100644 --- a/src/Enum/SearchIndex/FieldCategory.php +++ b/src/Enum/SearchIndex/FieldCategory.php @@ -24,4 +24,5 @@ enum FieldCategory: string case SYSTEM_FIELDS = 'system_fields'; case STANDARD_FIELDS = 'standard_fields'; case CUSTOM_FIELDS = 'custom_fields'; + case INHERITED_FIELDS = '_inherited_fields'; } diff --git a/src/Enum/SearchIndex/FieldCategory/StandardField.php b/src/Enum/SearchIndex/FieldCategory/StandardField.php new file mode 100644 index 00000000..8468b7bd --- /dev/null +++ b/src/Enum/SearchIndex/FieldCategory/StandardField.php @@ -0,0 +1,29 @@ +inheritedFields; + } + + /** + * @param InheritedData[] $inheritedFields + */ + public function setInheritedFields(array $inheritedFields): DataObjectSearchResultItem + { + $this->inheritedFields = $inheritedFields; + + return $this; + } + public function withLazyLoadingHandler( ?DataObjectLazyLoadingHandlerInterface $lazyLoadingHandler ): DataObjectSearchResultItem { diff --git a/src/Model/Search/DataObject/SearchResult/SearchResultItem/InheritedData.php b/src/Model/Search/DataObject/SearchResult/SearchResultItem/InheritedData.php new file mode 100644 index 00000000..769454e5 --- /dev/null +++ b/src/Model/Search/DataObject/SearchResult/SearchResultItem/InheritedData.php @@ -0,0 +1,37 @@ +key; + } + + public function getOriginId(): int + { + return $this->originId; + } +} diff --git a/src/SearchIndexAdapter/DataObject/AdapterInterface.php b/src/SearchIndexAdapter/DataObject/AdapterInterface.php index 309a56a9..f4e06580 100644 --- a/src/SearchIndexAdapter/DataObject/AdapterInterface.php +++ b/src/SearchIndexAdapter/DataObject/AdapterInterface.php @@ -16,7 +16,9 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DataObject; +use Exception; use Pimcore\Model\DataObject\ClassDefinition\Data; +use Pimcore\Model\DataObject\Concrete; /** * @internal @@ -35,4 +37,16 @@ public function getIndexAttributeName(): string; * Used to normalize the data for the search index */ public function normalize(mixed $value): mixed; + + /** + * @throws Exception + */ + public function getInheritedData( + Concrete $dataObject, + int $objectId, + mixed $value, + string $key, + ?string $language = null, + callable $callback = null + ): array; } diff --git a/src/SearchIndexAdapter/DataObject/FieldDefinitionServiceInterface.php b/src/SearchIndexAdapter/DataObject/FieldDefinitionServiceInterface.php index 3357f01f..f9977dea 100644 --- a/src/SearchIndexAdapter/DataObject/FieldDefinitionServiceInterface.php +++ b/src/SearchIndexAdapter/DataObject/FieldDefinitionServiceInterface.php @@ -16,7 +16,10 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DataObject; +use Exception; use Pimcore\Model\DataObject\ClassDefinition; +use Pimcore\Model\DataObject\ClassDefinition\Data; +use Pimcore\Model\DataObject\Concrete; /** * @internal @@ -26,4 +29,14 @@ interface FieldDefinitionServiceInterface public function getFieldDefinitionAdapter(ClassDefinition\Data $fieldDefinition): ?AdapterInterface; public function normalizeValue(?ClassDefinition\Data $fieldDefinition, mixed $value): mixed; + + /** + * @throws Exception + */ + public function getInheritedFieldData( + ?Data $fieldDefinition, + Concrete $dataObject, + string $key, + mixed $value + ): array; } diff --git a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/AbstractAdapter.php b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/AbstractAdapter.php index 1d41325e..496ffd33 100644 --- a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/AbstractAdapter.php +++ b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/AbstractAdapter.php @@ -16,10 +16,12 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\DataObject\FieldDefinitionAdapter; +use Exception; use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DataObject\AdapterInterface; use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DataObject\FieldDefinitionServiceInterface; use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\SearchIndexConfigServiceInterface; use Pimcore\Model\DataObject\ClassDefinition\Data; +use Pimcore\Model\DataObject\Concrete; use Pimcore\Normalizer\NormalizerInterface; abstract class AbstractAdapter implements AdapterInterface @@ -64,4 +66,37 @@ public function normalize(mixed $value): mixed return $value; } + + /** + * @throws Exception + */ + public function getInheritedData( + Concrete $dataObject, + int $objectId, + mixed $value, + string $key, + ?string $language = null, + callable $callback = null + ): array { + if (!$this->fieldDefinition->isEmpty($value)) { + return []; + } + + $path = $key; + if ($language !== null) { + $path .= '.' . $language; + } + + $parent = $dataObject->getNextParentForInheritance(); + if ($parent === null) { + return $objectId === $dataObject->getId() ? [] : [$path => ['originId' => $dataObject->getId()]]; + } + + $parentValue = $callback ? $callback($parent, $key, $language) : $parent->get($key, $language); + if (!$this->fieldDefinition->isEmpty($parentValue)) { + return [$path => ['originId' => $parent->getId()]]; + } + + return $this->getInheritedData($parent, $objectId, $value, $key, $language, $callback); + } } diff --git a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ClassificationStoreAdapter.php b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ClassificationStoreAdapter.php index 7105b457..49b9ad7b 100644 --- a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ClassificationStoreAdapter.php +++ b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ClassificationStoreAdapter.php @@ -19,6 +19,7 @@ use Exception; use InvalidArgumentException; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\OpenSearch\AttributeType; +use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\LanguageServiceInterface; use Pimcore\Bundle\GenericDataIndexBundle\Traits\LoggerAwareTrait; use Pimcore\Bundle\StaticResolverBundle\Models\DataObject\ClassificationStore\ServiceResolverInterface; use Pimcore\Model\DataObject\ClassDefinition\Data; @@ -27,6 +28,7 @@ use Pimcore\Model\DataObject\Classificationstore\GroupConfig\Listing as GroupListing; use Pimcore\Model\DataObject\Classificationstore\KeyGroupRelation; use Pimcore\Model\DataObject\Classificationstore\KeyGroupRelation\Listing as KeyGroupRelationListing; +use Pimcore\Model\DataObject\Concrete; use Symfony\Contracts\Service\Attribute\Required; /** @@ -38,20 +40,28 @@ final class ClassificationStoreAdapter extends AbstractAdapter private ServiceResolverInterface $classificationService; + private LanguageServiceInterface $languageService; + + private const DEFAULT_LANGUAGE = 'default'; + #[Required] public function setClassificationService(ServiceResolverInterface $serviceResolver): void { $this->classificationService = $serviceResolver; } + #[Required] + public function setLanguageService(LanguageServiceInterface $languageService): void + { + $this->languageService = $languageService; + } + + /** + * @throws InvalidArgumentException + */ public function getIndexMapping(): array { - $classificationStore = $this->getFieldDefinition(); - if (!$classificationStore instanceof Classificationstore) { - throw new InvalidArgumentException( - 'Field definition must be an instance of ' . Classificationstore::class - ); - } + $classificationStore = $this->getClassificationStoreDefinition(); $mapping = []; $groups = $this->getClassificationStoreGroups($classificationStore->getStoreId()); @@ -66,6 +76,152 @@ public function getIndexMapping(): array ]; } + /** + * @throws Exception + */ + public function getInheritedData( + Concrete $dataObject, + int $objectId, + mixed $value, + string $key, + ?string $language = null, + callable $callback = null + ): array { + $classificationStore = $this->getClassificationStoreDefinition(); + $languages = $this->getValidLanguages($classificationStore); + $result = []; + foreach ($this->getMappingForInheritance($dataObject, $classificationStore) as $groupId => $group) { + foreach ($group['keys'] as $keyId => $groupKey) { + foreach ($languages as $lang) { + $originId = $this->getKeyValueFromElement( + $groupKey['definition'], + $dataObject, + $key, + $groupId, + $keyId, + $lang + ); + + if ($originId !== null && $originId !== $objectId) { + $result[$this->getInheritancePath($key, $group['name'], $groupKey['name'], $lang)] = [ + 'originId' => $originId, + ]; + } + } + } + } + + return $result; + } + + /** + * @throws InvalidArgumentException + */ + private function getClassificationStoreDefinition(): Classificationstore + { + $classificationStore = $this->getFieldDefinition(); + if (!$classificationStore instanceof Classificationstore) { + throw new InvalidArgumentException( + 'Field definition must be an instance of ' . Classificationstore::class + ); + } + + return $classificationStore; + } + + /** + * @throws Exception + */ + private function getKeyValueFromElement( + Data $definition, + Concrete $dataObject, + string $storeKey, + int $groupId, + int $groupKeyId, + string $language + ): ?int { + $data = $dataObject->get($storeKey)->getLocalizedKeyValue($groupId, $groupKeyId, $language, true, true); + + if (!$definition->isEmpty($data)) { + return $dataObject->getId(); + } + + $parent = $dataObject->getNextParentForInheritance(); + if ($parent === null) { + return $dataObject->getId(); + } + + return $this->getKeyValueFromElement($definition, $parent, $storeKey, $groupId, $groupKeyId, $language); + } + + private function getValidLanguages(Classificationstore $classificationStore): array + { + $languages = [self::DEFAULT_LANGUAGE]; + if ($classificationStore->isLocalized()) { + $languages = array_merge($languages, $this->languageService->getValidLanguages()); + } + + return $languages; + } + + private function getElementActiveGroups(Concrete $dataObject, Classificationstore $classificationStore): array + { + $activeGroups = []; + foreach ($classificationStore->recursiveGetActiveGroupsIds($dataObject) as $groupId => $active) { + if ($active) { + $activeGroups[] = $groupId; + } + } + + return $activeGroups; + } + + private function getInheritancePath(string $key, string $groupName, string $groupKeyName, string $lang): string + { + $path = $key . '.' . $groupName . '.' . $groupKeyName; + if ($lang !== self::DEFAULT_LANGUAGE) { + $path .= '.' . $lang; + } + + return $path; + } + + private function getMappingForInheritance( + Concrete $dataObject, + Classificationstore $classificationStore + ): array { + $mapping = []; + $groups = $this->getClassificationStoreGroups($classificationStore->getStoreId()); + $activeGroups = $this->getElementActiveGroups($dataObject, $classificationStore); + + if (empty($activeGroups)) { + return $mapping; + } + + foreach ($groups as $group) { + if (!in_array($group->getId(), $activeGroups, true)) { + continue; + } + + $mapping[$group->getId()] = [ + 'name' => $group->getName(), + ]; + $keys = $this->getClassificationStoreKeysFromGroup($group); + foreach ($keys as $groupKey) { + $definition = $this->getFieldDefinitionForKey($groupKey); + if ($definition === null) { + continue; + } + $mapping[$groupKey->getGroupId()]['keys'][$groupKey->getKeyId()] = [ + 'name' => $groupKey->getName(), + 'definition' => $definition, + ]; + } + } + + return $mapping; + } + /** * @param KeyGroupRelation[] $groupConfigs */ @@ -73,28 +229,40 @@ private function getMappingForGroupConfig(array $groupConfigs): array { $groupMapping = []; foreach ($groupConfigs as $key) { - try { - $definition = $this->classificationService->getFieldDefinitionFromKeyConfig($key); - } catch (Exception) { - $this->logger->warning( - 'Could not get field definition for type ' . $key->getType() . ' in group ' . $key->getGroupId() - ); - + $definition = $this->getFieldDefinitionForKey($key); + if ($definition === null) { continue; } - if ($definition instanceof Data) { - $adapter = $this->getFieldDefinitionService()->getFieldDefinitionAdapter($definition); + $adapter = $this->getFieldDefinitionService()->getFieldDefinitionAdapter($definition); - if ($adapter) { - $groupMapping['default']['properties'][$key->getName()] = $adapter->getIndexMapping(); - } + if ($adapter) { + $groupMapping['default']['properties'][$key->getName()] = $adapter->getIndexMapping(); } } return $groupMapping; } + private function getFieldDefinitionForKey(KeyGroupRelation $key): ?Data + { + try { + $definition = $this->classificationService->getFieldDefinitionFromKeyConfig($key); + } catch (Exception) { + $this->logger->warning( + 'Could not get field definition for type ' . $key->getType() . ' in group ' . $key->getGroupId() + ); + + return null; + } + + if ($definition instanceof Data) { + return $definition; + } + + return null; + } + /** * @return GroupConfig[] */ diff --git a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/LocalizedFieldsAdapter.php b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/LocalizedFieldsAdapter.php index c7af14c2..3cc77daf 100644 --- a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/LocalizedFieldsAdapter.php +++ b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/LocalizedFieldsAdapter.php @@ -19,6 +19,7 @@ use Exception; use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\LanguageServiceInterface; use Pimcore\Model\DataObject\ClassDefinition\Data; +use Pimcore\Model\DataObject\Concrete; use Pimcore\Model\DataObject\Localizedfield; use Symfony\Contracts\Service\Attribute\Required; @@ -70,23 +71,13 @@ public function getIndexMapping(): array */ public function normalize(mixed $value): ?array { - if (!$value instanceof Localizedfield) { + $indexData = $this->getIndexData($value); + if (empty($indexData)) { return null; } - $value->loadLazyData(); - - /** @var Data\Localizedfields $fieldDefinition */ - $fieldDefinition = $this->getFieldDefinition(); - - $attributes = []; - $indexData = $fieldDefinition->normalize($value); - $languages = array_keys($indexData); - if (!empty($indexData)) { - $attributes = array_keys(reset($indexData)); - } - + $attributes = array_keys(reset($indexData)); $result = []; foreach ($attributes as $attribute) { foreach ($languages as $language) { @@ -100,6 +91,131 @@ public function normalize(mixed $value): ?array return $result; } + public function getInheritedData( + Concrete $dataObject, + int $objectId, + mixed $value, + string $key, + ?string $language = null, + callable $callback = null + ): array { + $indexData = $this->getIndexData($value); + if (empty($indexData)) { + return []; + } + $languages = array_keys($indexData); + $attributes = array_keys(reset($indexData)); + $result = []; + foreach ($attributes as $attribute) { + foreach ($languages as $indexDataLanguage) { + $data = $this->getInheritedDataForAdapter( + $dataObject, + $value, + $key, + $indexDataLanguage, + $attribute + ); + + foreach ($data as $itemKey => $item) { + $result[$itemKey] = $item; + } + } + } + + return $result; + } + + /** + * @throws Exception + */ + public function getInheritedDataForBrick( + Concrete $dataObject, + Localizedfield $value, + string $key, + string $type + ): array { + $indexData = $this->getIndexData($value); + if (empty($indexData)) { + return []; + } + $languages = array_keys($indexData); + $attributes = array_keys(reset($indexData)); + $result = []; + $brickGetter = 'get' . ucfirst($type); + foreach ($attributes as $attribute) { + foreach ($languages as $indexDataLanguage) { + $fieldGetter = 'get' . ucfirst($attribute); + + $data = $this->getInheritedDataForAdapter( + $dataObject, + $value, + $key, + $indexDataLanguage, + $attribute, + ['containerType' => 'objectbrick', 'containerKey' => $type], + static fn ( + Concrete $parent, string $key, ?string $language + ) => $parent->get($key)->$brickGetter()?->$fieldGetter($language), + ); + + foreach ($data as $item) { + $result[$attribute . '.' . $indexDataLanguage] = + $item; + } + } + } + + return $result; + + } + + private function getIndexData(mixed $value): ?array + { + if (!$value instanceof Localizedfield) { + return []; + } + + $value->loadLazyData(); + + /** @var Data\Localizedfields $fieldDefinition */ + $fieldDefinition = $this->getFieldDefinition(); + + return $fieldDefinition->normalize($value); + } + + /** + * @throws Exception + */ + private function getInheritedDataForAdapter( + concrete $dataObject, + Localizedfield $value, + string $key, + string $language, + string $attribute, + array $context = [], + ?callable $callback = null + ): array { + $adapter = $this->fieldDefinitionService->getFieldDefinitionAdapter( + $value->getFieldDefinition($attribute, $context), + ); + if (!$adapter) { + return []; + } + $path = $attribute; + if ($context !== [] && $context['containerType'] === 'objectbrick') { + $path = $key; + } + + return $adapter->getInheritedData( + $dataObject, + $dataObject->getId(), + $value->getLocalizedValue($attribute, $language), + $path, + $language, + $callback, + ); + } + #[Required] public function setLanguageService(LanguageServiceInterface $languageService): void { diff --git a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ObjectBrickAdapter.php b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ObjectBrickAdapter.php index 20cc7011..fa77a117 100644 --- a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ObjectBrickAdapter.php +++ b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionAdapter/ObjectBrickAdapter.php @@ -16,9 +16,13 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\DataObject\FieldDefinitionAdapter; +use Exception; use InvalidArgumentException; +use Pimcore\Model\DataObject\ClassDefinition\Data; use Pimcore\Model\DataObject\ClassDefinition\Data\Objectbricks; +use Pimcore\Model\DataObject\Concrete; use Pimcore\Model\DataObject\Objectbrick; +use Pimcore\Model\DataObject\Objectbrick\Data\AbstractData; /** * @internal @@ -44,6 +48,84 @@ public function getIndexMapping(): array ]; } + public function getInheritedData( + Concrete $dataObject, + int $objectId, + mixed $value, + string $key, + ?string $language = null, + callable $callback = null + ): array { + if (!$value instanceof Objectbrick) { + return []; + } + $result = []; + foreach ($value->getAllowedBrickTypes() as $type) { + $brickGetter = 'get' . $type; + $brick = $value->$brickGetter(); + if (!$brick) { + continue; + } + + $fieldDefinitions = Objectbrick\Definition::getByKey($type)?->getFieldDefinitions(); + foreach ($fieldDefinitions as $fieldDefinition) { + $data = $this->getInheritedDataForFieldDefinition( + $dataObject, + $fieldDefinition, + $brick, + $brickGetter, + $key, + $type + ); + + foreach ($data as $itemKey => $item) { + $path = $key . '.' . $type . '.' . ($itemKey !== $key ? $itemKey : $fieldDefinition->getName()); + $result[$path] = $item; + } + } + } + + return $result; + } + + /** + * @throws Exception + */ + private function getInheritedDataForFieldDefinition( + Concrete $dataObject, + Data $fieldDefinition, + AbstractData $brick, + string $brickGetter, + string $key, + string $type + ): array { + $adapter = $this->getFieldDefinitionService()->getFieldDefinitionAdapter($fieldDefinition); + if (!$adapter) { + return []; + } + + $fieldGetter = 'get' . ucfirst($fieldDefinition->getName()); + if ($adapter instanceof LocalizedFieldsAdapter) { + return $adapter->getInheritedDataForBrick( + $dataObject, + $brick->$fieldGetter(), + $key, + $type + ); + } + + return $adapter->getInheritedData( + $dataObject, + $dataObject->getId(), + $brick->$fieldGetter(), + $key, + null, + static fn ( + Concrete $parent, string $key, ?string $language + ) => $parent->get($key)->$brickGetter()?->$fieldGetter($language) + ); + } + private function getMappingForObjectBrick(string $objectBrickType): array { $fieldDefinitions = Objectbrick\Definition::getByKey($objectBrickType)?->getFieldDefinitions(); diff --git a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionService.php b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionService.php index ed87231f..82827d71 100644 --- a/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionService.php +++ b/src/SearchIndexAdapter/OpenSearch/DataObject/FieldDefinitionService.php @@ -16,9 +16,11 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\OpenSearch\DataObject; +use Exception; use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DataObject\AdapterInterface; use Pimcore\Bundle\GenericDataIndexBundle\SearchIndexAdapter\DataObject\FieldDefinitionServiceInterface; -use Pimcore\Model\DataObject\ClassDefinition; +use Pimcore\Model\DataObject\ClassDefinition\Data; +use Pimcore\Model\DataObject\Concrete; use Psr\Container\ContainerExceptionInterface; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -32,7 +34,7 @@ public function __construct( ) { } - public function getFieldDefinitionAdapter(ClassDefinition\Data $fieldDefinition): ?AdapterInterface + public function getFieldDefinitionAdapter(Data $fieldDefinition): ?AdapterInterface { $adapter = null; @@ -48,7 +50,7 @@ public function getFieldDefinitionAdapter(ClassDefinition\Data $fieldDefinition) return $adapter; } - public function normalizeValue(?ClassDefinition\Data $fieldDefinition, mixed $value): mixed + public function normalizeValue(?Data $fieldDefinition, mixed $value): mixed { if ($fieldDefinition === null) { return $value; @@ -60,4 +62,25 @@ public function normalizeValue(?ClassDefinition\Data $fieldDefinition, mixed $va return null; } + + /** + * @throws Exception + */ + public function getInheritedFieldData( + ?Data $fieldDefinition, + Concrete $dataObject, + string $key, + mixed $value, + ): array { + if ($fieldDefinition === null) { + return []; + } + + $adapter = $this->getFieldDefinitionAdapter($fieldDefinition); + if ($adapter === null) { + return []; + } + + return $adapter->getInheritedData($dataObject, $dataObject->getId(), $value, $key); + } } diff --git a/src/Service/Serializer/Denormalizer/Search/DataObjectSearchResultDenormalizer.php b/src/Service/Serializer/Denormalizer/Search/DataObjectSearchResultDenormalizer.php index ba16fa9e..48102f79 100644 --- a/src/Service/Serializer/Denormalizer/Search/DataObjectSearchResultDenormalizer.php +++ b/src/Service/Serializer/Denormalizer/Search/DataObjectSearchResultDenormalizer.php @@ -16,9 +16,11 @@ namespace Pimcore\Bundle\GenericDataIndexBundle\Service\Serializer\Denormalizer\Search; +use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory\SystemField; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\SerializerContext; use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\DataObject\SearchResult\DataObjectSearchResultItem; +use Pimcore\Bundle\GenericDataIndexBundle\Model\Search\DataObject\SearchResult\SearchResultItem\InheritedData; use Pimcore\Bundle\GenericDataIndexBundle\Service\Serializer\DataObjectTypeSerializationHandlerService; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; @@ -73,6 +75,14 @@ public function denormalize( return $searchResultItem; } + if (isset($data[FieldCategory::STANDARD_FIELDS->value][FieldCategory::INHERITED_FIELDS->value])) { + $searchResultItem->setInheritedFields( + $this->hydrateInheritedData( + $data[FieldCategory::STANDARD_FIELDS->value][FieldCategory::INHERITED_FIELDS->value] + ) + ); + } + return $searchResultItem ->setHasWorkflowWithPermissions(SystemField::HAS_WORKFLOW_WITH_PERMISSIONS->getData($data)) ->setHasChildren(SystemField::HAS_CHILDREN->getData($data)) @@ -84,4 +94,18 @@ public function supportsDenormalization(mixed $data, string $type, string $forma { return is_array($data) && is_subclass_of($type, DataObjectSearchResultItem::class); } + + private function hydrateInheritedData(array $inheritedData): array + { + $result = []; + + foreach ($inheritedData as $key => $inheritedEntry) { + $result[] = new InheritedData( + $key, + $inheritedEntry['originId'] + ); + } + + return $result; + } } diff --git a/src/Service/Serializer/Normalizer/DataObjectNormalizer.php b/src/Service/Serializer/Normalizer/DataObjectNormalizer.php index 00cbca2c..582927e7 100644 --- a/src/Service/Serializer/Normalizer/DataObjectNormalizer.php +++ b/src/Service/Serializer/Normalizer/DataObjectNormalizer.php @@ -19,6 +19,7 @@ use Exception; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\ElementType; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory; +use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory\StandardField; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory\SystemField; use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\SerializerContext; use Pimcore\Bundle\GenericDataIndexBundle\Exception\DataObjectNormalizerException; @@ -145,28 +146,32 @@ private function normalizeSystemFields(AbstractObject $dataObject, bool $skipLaz private function normalizeStandardFields(Concrete $dataObject): array { try { + $class = $dataObject->getClass(); + $fieldDefinitions = $class->getFieldDefinitions(); + $result[FieldCategory::INHERITED_FIELDS->value] = []; $inheritedValuesBackup = AbstractObject::doGetInheritedValues(); $fallbackLanguagesBackup = Localizedfield::doGetFallbackValues(); - AbstractObject::setGetInheritedValues(true); Localizedfield::setGetFallbackValues(true); + if ($class->getAllowInherit()) { + AbstractObject::setGetInheritedValues(false); + $result = $this->getInheritedFields($fieldDefinitions, $dataObject, $result); + AbstractObject::setGetInheritedValues(true); + } - $result = []; - - foreach ($dataObject->getClass()->getFieldDefinitions() as $key => $fieldDefinition) { - - $value = $dataObject->get($key); - - $value = $this->fieldDefinitionService->normalizeValue($fieldDefinition, $value); - - $result[$key] = $value; + foreach ($fieldDefinitions as $key => $fieldDefinition) { + $normalizedValue = $this->fieldDefinitionService->normalizeValue( + $fieldDefinition, + $dataObject->get($key) + ); + $result[$key] = $normalizedValue; } AbstractObject::setGetInheritedValues($inheritedValuesBackup); Localizedfield::setGetFallbackValues($fallbackLanguagesBackup); - if (isset($result['localizedfields'])) { - $result = array_merge($result['localizedfields'], $result); - unset($result['localizedfields']); + if (isset($result[StandardField::LOCALIZED_FIELDS->value])) { + $result = array_merge($result[StandardField::LOCALIZED_FIELDS->value], $result); + unset($result[StandardField::LOCALIZED_FIELDS->value]); } return $result; @@ -174,4 +179,29 @@ private function normalizeStandardFields(Concrete $dataObject): array throw new DataObjectNormalizerException($e->getMessage()); } } + + /** + * @throws Exception + */ + private function getInheritedFields(array $fields, Concrete $dataObject, array $result): array + { + foreach ($fields as $key => $fieldDefinition) { + if (!$fieldDefinition->supportsInheritance()) { + continue; + } + + $inheritedData = $this->fieldDefinitionService->getInheritedFieldData( + $fieldDefinition, + $dataObject, + $key, + $dataObject->get($key) + ); + + foreach ($inheritedData as $itemKey => $item) { + $result[FieldCategory::INHERITED_FIELDS->value][$itemKey] = $item; + } + } + + return $result; + } } diff --git a/tests/Functional/SearchIndex/DataObjectBasicTest.php b/tests/Functional/SearchIndex/DataObjectBasicTest.php index edecc361..977b0c42 100644 --- a/tests/Functional/SearchIndex/DataObjectBasicTest.php +++ b/tests/Functional/SearchIndex/DataObjectBasicTest.php @@ -17,6 +17,7 @@ use Exception; use OpenSearch\Common\Exceptions\Missing404Exception; +use Pimcore\Bundle\GenericDataIndexBundle\Enum\SearchIndex\FieldCategory; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\DataObject\DataObjectSearchServiceInterface; use Pimcore\Bundle\GenericDataIndexBundle\Service\Search\SearchService\SearchProviderInterface; use Pimcore\Bundle\GenericDataIndexBundle\Service\SearchIndex\IndexService\ElementTypeAdapter\DataObjectTypeAdapter; @@ -60,11 +61,16 @@ protected function _after() public function testIndexingWithInheritanceSynchronous() { $object = $this->createObjectWithInheritance(); + $child = $this->createChildObject($object); $indexName = $this->dataObjectTypeAdapter->getAliasIndexName($object->getClass()); // check indexed - $response = $this->tester->checkIndexEntry($object->getId(), $indexName); - $this->assertEquals($object->getId(), $response['_source']['system_fields']['id']); + $response = $this->tester->checkIndexEntry($child->getId(), $indexName); + $this->assertEquals($child->getId(), $response['_source']['system_fields']['id']); + $this->assertEquals( + $object->getId(), + $this->getInheritedFieldsResponse($response)['input']['originId'] + ); $object->setKey('my-test-object'); $object->save(); @@ -72,10 +78,23 @@ public function testIndexingWithInheritanceSynchronous() $response = $this->tester->checkIndexEntry($object->getId(), $indexName); $this->assertEquals('my-test-object', $response['_source']['system_fields']['key']); + $child->setInput('Updated input'); + $child->save(); + + $response = $this->tester->checkIndexEntry($child->getId(), $indexName); + $this->assertEquals( + 'Updated input', $response['_source'][FieldCategory::STANDARD_FIELDS->value]['input'] + ); + $this->assertArrayNotHasKey( + 'input', + $this->getInheritedFieldsResponse($response) + ); + + //Should also delete child element $object->delete(); $this->expectException(Missing404Exception::class); - $this->tester->checkIndexEntry($object->getId(), $indexName); + $this->tester->checkIndexEntry($child->getId(), $indexName); } public function testIndexingWithInheritanceAsynchronous() @@ -83,21 +102,53 @@ public function testIndexingWithInheritanceAsynchronous() $this->tester->disableSynchronousProcessing(); // create object $object = $this->createObjectWithInheritance(); + $child = $this->createChildObject($object); $indexName = $this->dataObjectTypeAdapter->getAliasIndexName($object->getClass()); // check indexed - $this->expectException(Missing404Exception::class); - $this->tester->checkIndexEntry($object->getId(), $indexName); + $this->assertNotEmpty( + Db::get()->fetchOne( + 'select count(elementId) from generic_data_index_queue where elementId = ? and elementType="dataObject"', + [$object->getId()] + ) + ); + $this->tester->runCommand('messenger:consume', ['--limit'=>2], ['pimcore_generic_data_index_queue']); + $response = $this->tester->checkIndexEntry($child->getId(), $indexName); + $this->assertEquals($child->getKey(), $response['_source']['system_fields']['key']); + $this->assertEquals( + $object->getId(), + $this->getInheritedFieldsResponse($response)['input']['originId'] + ); + } + public function testIndexingWithInheritanceAsynchronousNoInheritance() + { + $this->tester->disableSynchronousProcessing(); + // create object + $object = $this->createObjectWithInheritance(); + $child = $this->createChildObject($object); + $indexName = $this->dataObjectTypeAdapter->getAliasIndexName($object->getClass()); + + // check indexed $this->assertNotEmpty( Db::get()->fetchOne( - 'select count(elementId) from generic_data_index_queue where elementId = ? and elementType="object"', + 'select count(elementId) from generic_data_index_queue where elementId = ? and elementType="dataObject"', [$object->getId()] ) ); + + $child->setInput('Updated input'); + $child->save(); $this->tester->runCommand('messenger:consume', ['--limit'=>2], ['pimcore_generic_data_index_queue']); - $response = $this->tester->checkIndexEntry($object->getId(), $indexName); - $this->assertEquals($object->getKey(), $response['_source']['system_fields']['key']); + + $response = $this->tester->checkIndexEntry($child->getId(), $indexName); + $this->assertEquals( + 'Updated input', $response['_source'][FieldCategory::STANDARD_FIELDS->value]['input'] + ); + $this->assertArrayNotHasKey( + 'input', + $this->getInheritedFieldsResponse($response) + ); } public function testIndexingWithoutInheritanceSynchronous() @@ -131,12 +182,9 @@ public function testIndexingWithoutInheritanceAsynchronous() $indexName = $this->dataObjectTypeAdapter->getAliasIndexName($object->getClass()); // check indexed - $this->expectException(Missing404Exception::class); - $this->tester->checkIndexEntry($object->getId(), $indexName); - $this->assertNotEmpty( Db::get()->fetchOne( - 'select count(elementId) from generic_data_index_queue where elementId = ? and elementType="object"', + 'select count(elementId) from generic_data_index_queue where elementId = ? and elementType="dataObject"', [$object->getId()] ) ); @@ -310,4 +358,18 @@ private function disableInheritance(): void $class->setAllowInherit(false); $class->save(); } + + private function createChildObject(Concrete $parent): Concrete + { + $child = $this->createObjectWithInheritance(); + $child->setParentId($parent->getId()); + $child->save(); + + return $child; + } + + private function getInheritedFieldsResponse(array $data): array + { + return $data['_source'][FieldCategory::STANDARD_FIELDS->value][FieldCategory::INHERITED_FIELDS->value]; + } }