diff --git a/config/data_objects.yaml b/config/data_objects.yaml
index c7cac5dd9..ca81e32a7 100644
--- a/config/data_objects.yaml
+++ b/config/data_objects.yaml
@@ -44,6 +44,23 @@ services:
Pimcore\Bundle\StudioBackendBundle\DataObject\Service\LayoutServiceInterface:
class: Pimcore\Bundle\StudioBackendBundle\DataObject\Service\LayoutService
+ Pimcore\Bundle\StudioBackendBundle\DataObject\Service\SelectOptionsServiceInterface:
+ class: Pimcore\Bundle\StudioBackendBundle\DataObject\Service\SelectOptionsService
+
+ #
+ # Legacy Services
+ #
+ Pimcore\Bundle\StudioBackendBundle\DataObject\Legacy\ApplyChangesHelperInterface:
+ class: Pimcore\Bundle\StudioBackendBundle\DataObject\Legacy\ApplyChangesHelper
+
+
+ #
+ # Hydrator
+ #
+ Pimcore\Bundle\StudioBackendBundle\DataObject\Hydrator\SelectOptionHydratorInterface:
+ class: Pimcore\Bundle\StudioBackendBundle\DataObject\Hydrator\SelectOptionHydrator
+
+
#
# Data Adapters
#
diff --git a/src/DataObject/Attribute/Request/SelectOptionRequestBody.php b/src/DataObject/Attribute/Request/SelectOptionRequestBody.php
new file mode 100644
index 000000000..e94c063bf
--- /dev/null
+++ b/src/DataObject/Attribute/Request/SelectOptionRequestBody.php
@@ -0,0 +1,52 @@
+value)]
+ #[Post(
+ path: self::PREFIX . '/data-objects/select-options',
+ operationId: 'data_object_get_select_options',
+ description: 'data_object_get_select_options_description',
+ summary: 'data_object_get_select_options_summary',
+ tags: [Tags::DataObjects->value]
+ )]
+ #[SelectOptionRequestBody]
+ #[SuccessResponse(
+ description: 'data_object_get_select_options_success_response',
+ content: new CollectionJson(new GenericCollection(SelectOption::class))
+ )]
+ #[DefaultResponses([
+ HttpResponseCodes::UNAUTHORIZED,
+ HttpResponseCodes::NOT_FOUND,
+ HttpResponseCodes::BAD_REQUEST,
+ ])]
+ public function getSelectOptions(#[MapRequestPayload] SelectOptionsParameter $selectOptionsParameter): JsonResponse
+ {
+ $selectOptions = $this->selectOptionsService->getSelectOptions($selectOptionsParameter);
+
+ return $this->getPaginatedCollection(
+ $this->serializer,
+ $selectOptions,
+ count($selectOptions),
+ );
+ }
+}
diff --git a/src/DataObject/Event/PreResponse/DynamicSelectOptionEvent.php b/src/DataObject/Event/PreResponse/DynamicSelectOptionEvent.php
new file mode 100644
index 000000000..f84f2c7f3
--- /dev/null
+++ b/src/DataObject/Event/PreResponse/DynamicSelectOptionEvent.php
@@ -0,0 +1,39 @@
+selectOption);
+ }
+
+ /**
+ * Use this to get additional infos out of the response object
+ */
+ public function getSelectOption(): SelectOption
+ {
+ return $this->selectOption;
+ }
+}
diff --git a/src/DataObject/Hydrator/SelectOptionHydrator.php b/src/DataObject/Hydrator/SelectOptionHydrator.php
new file mode 100644
index 000000000..d487809a2
--- /dev/null
+++ b/src/DataObject/Hydrator/SelectOptionHydrator.php
@@ -0,0 +1,33 @@
+ $value) {
+ try {
+ $fd = $object->getClass()->getFieldDefinition($key);
+ } catch (Exception) {
+ throw new NotFoundException('Class ', $object->getClassId());
+ }
+
+ if ($fd) {
+ if ($fd instanceof Localizedfields) {
+ $user = $this->securityService->getCurrentUser();
+ if (!$user->getAdmin()) {
+ $allowedLanguages = $this->dataObjectServiceResolver->getLanguagePermissions(
+ $object,
+ $user,
+ 'lEdit'
+ );
+ if (!is_null($allowedLanguages)) {
+ $allowedLanguages = array_keys($allowedLanguages);
+ $submittedLanguages = array_keys($changes[$key]);
+ foreach ($submittedLanguages as $submittedLanguage) {
+ if (!in_array($submittedLanguage, $allowedLanguages)) {
+ unset($value[$submittedLanguage]);
+ }
+ }
+ }
+ }
+ }
+
+ if ($fd instanceof ReverseObjectRelation) {
+ try {
+ $remoteClass = $this->classDefinitionResolver->getByName($fd->getOwnerClassName());
+ } catch (Exception) {
+ throw new NotFoundException('Class definition ', $fd->getOwnerClassName());
+ }
+
+ $relations = $object->getRelationData($fd->getOwnerFieldName(), false, $remoteClass->getId());
+ $toAdd = $this->detectAddedRemoteOwnerRelations($relations, $value);
+ $toDelete = $this->detectDeletedRemoteOwnerRelations($relations, $value);
+ if (count($toAdd) > 0 || count($toDelete) > 0) {
+ try {
+ $this->processRemoteOwnerRelations($object, $toDelete, $toAdd, $fd->getOwnerFieldName());
+ } catch (DuplicateFullPathException $e) {
+ throw new DatabaseException($e->getMessage());
+ }
+ }
+ } else {
+ $object->setValue($key, $fd->getDataFromEditmode($value, $object));
+ }
+ }
+ }
+ }
+
+ private function detectAddedRemoteOwnerRelations(array $relations, array $value): array
+ {
+ $originals = [];
+ $changed = [];
+ foreach ($relations as $r) {
+ $originals[] = $r['dest_id'];
+ }
+
+ foreach ($value as $row) {
+ $changed[] = $row['id'];
+ }
+
+ return array_diff($changed, $originals);
+ }
+
+ private function detectDeletedRemoteOwnerRelations(array $relations, array $value): array
+ {
+ $originals = [];
+ $changed = [];
+ foreach ($relations as $r) {
+ $originals[] = $r['dest_id'];
+ }
+
+ foreach ($value as $row) {
+ $changed[] = $row['id'];
+ }
+
+ return array_diff($originals, $changed);
+ }
+
+ /**
+ * @throws DuplicateFullPathException
+ */
+ private function processRemoteOwnerRelations(
+ Concrete $object,
+ array $toDelete,
+ array $toAdd,
+ string $ownerFieldName
+ ): void {
+ $getter = 'get' . ucfirst($ownerFieldName);
+ $setter = 'set' . ucfirst($ownerFieldName);
+
+ foreach ($toDelete as $id) {
+ $owner = $this->dataObjectResolver->getById($id);
+ //TODO: lock ?!
+ if (method_exists($owner, $getter)) {
+ $currentData = $owner->$getter();
+ if (is_array($currentData)) {
+ for ($i = 0; $i < count($currentData); $i++) {
+ if ($currentData[$i]->getId() == $object->getId()) {
+ unset($currentData[$i]);
+ $owner->$setter($currentData);
+
+ break;
+ }
+ }
+ } else {
+ if ($currentData->getId() == $object->getId()) {
+ $owner->$setter(null);
+ }
+ }
+ }
+ $owner->setUserModification($this->securityService->getCurrentUser()->getId());
+ $owner->save();
+ $this->logger->debug(
+ 'Saved object id [ ' . $owner->getId() . ' ] by remote modification through
+ [' . $object->getId() . '], Action: deleted [ ' . $object->getId() . " ]
+ from [ $ownerFieldName]"
+ );
+ }
+
+ foreach ($toAdd as $id) {
+ $owner = $this->dataObjectResolver->getById($id);
+ //TODO: lock ?!
+ if (method_exists($owner, $getter)) {
+ $currentData = $owner->$getter();
+ if (is_array($currentData)) {
+ $currentData[] = $object;
+ } else {
+ $currentData = $object;
+ }
+ $owner->$setter($currentData);
+ $owner->setUserModification($this->securityService->getCurrentUser()->getId());
+ $owner->save();
+ $this->logger->debug(
+ 'Saved object id [ ' . $owner->getId() . ' ] by remote modification
+ through [' . $object->getId() . '], Action:
+ added [ ' . $object->getId() . " ] to [ $ownerFieldName ]"
+ );
+ }
+ }
+ }
+}
diff --git a/src/DataObject/Legacy/ApplyChangesHelperInterface.php b/src/DataObject/Legacy/ApplyChangesHelperInterface.php
new file mode 100644
index 000000000..2e8cfafc5
--- /dev/null
+++ b/src/DataObject/Legacy/ApplyChangesHelperInterface.php
@@ -0,0 +1,37 @@
+fieldName;
+ }
+
+ public function getObjectId(): int
+ {
+ return $this->objectId;
+ }
+
+ public function getChangedData(): array
+ {
+ return $this->changedData;
+ }
+
+ public function hasChangedData(): bool
+ {
+ return !empty($this->changedData);
+ }
+
+ public function getContext(): array
+ {
+ return $this->context;
+ }
+}
diff --git a/src/DataObject/Schema/SelectOption.php b/src/DataObject/Schema/SelectOption.php
new file mode 100644
index 000000000..e4e28137a
--- /dev/null
+++ b/src/DataObject/Schema/SelectOption.php
@@ -0,0 +1,53 @@
+key;
+ }
+
+ public function getValue(): string|int
+ {
+ return $this->value;
+ }
+}
diff --git a/src/DataObject/Service/SelectOptionsService.php b/src/DataObject/Service/SelectOptionsService.php
new file mode 100644
index 000000000..9652b56b8
--- /dev/null
+++ b/src/DataObject/Service/SelectOptionsService.php
@@ -0,0 +1,164 @@
+getObject(
+ $selectOptionsParameter->getObjectId()
+ );
+
+ if ($selectOptionsParameter->hasChangedData()) {
+ $this->applyChangesHelper->applyChanges($object, $selectOptionsParameter->getChangedData());
+ }
+
+ $fieldDefinition = $this->getFieldDefinition($selectOptionsParameter, $object);
+
+ $provider = $this->getProvider($fieldDefinition);
+
+ try {
+ $class = $object->getClass();
+ } catch (Exception) {
+ throw new NotFoundException('class', $object->getClassId());
+ }
+
+ $options = $provider->getOptions(
+ [
+ 'object' => $object,
+ 'fieldname' => $fieldDefinition->getName(),
+ 'class' => $class,
+ 'context' => $selectOptionsParameter->getContext(),
+ ],
+ $fieldDefinition
+ );
+
+ $selectOptions = [];
+ foreach ($options as $option) {
+ $selectOption = $this->selectOptionHydrator->hydrate($option);
+ $this->dispatchDataObjectEvent($selectOption);
+ $selectOptions[] = $selectOption;
+ }
+
+ return $selectOptions;
+
+ }
+
+ private function getMode(Select|Multiselect $fieldDefinition): int
+ {
+ return $fieldDefinition instanceof Multiselect
+ ? OptionsProviderResolver::MODE_MULTISELECT
+ : OptionsProviderResolver::MODE_SELECT;
+ }
+
+ private function dispatchDataObjectEvent(SelectOption $selectOption): void
+ {
+ $this->eventDispatcher->dispatch(
+ new DynamicSelectOptionEvent($selectOption),
+ DynamicSelectOptionEvent::EVENT_NAME
+ );
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ */
+ private function getProvider(Select|Multiselect $fieldDefinition): SelectOptionsProviderInterface
+ {
+ $provider = $this->optionsProviderResolver->resolveProvider(
+ $fieldDefinition->getOptionsProviderClass(),
+ $this->getMode($fieldDefinition)
+ );
+
+ if (!$provider instanceof SelectOptionsProviderInterface) {
+ throw new InvalidArgumentException('Provider does not implement SelectOptionsProviderInterface');
+ }
+
+ return $provider;
+ }
+
+ /**
+ * @throws InvalidArgumentException
+ * @throws NotFoundException
+ */
+ private function getFieldDefinition(
+ SelectOptionsParameter $selectOptionsParameter,
+ Concrete $object
+ ): Select|Multiselect {
+ try {
+ $fieldDefinition = $object->getClass()->getFieldDefinition($selectOptionsParameter->getFieldName());
+ } catch (Exception) {
+ throw new NotFoundException('class', $object->getClassId());
+ }
+
+ if (!$fieldDefinition instanceof Select && !$fieldDefinition instanceof Multiselect) {
+ throw new InvalidArgumentException('Field is not a select or multiselect field');
+ }
+
+ return $fieldDefinition;
+ }
+
+ /**
+ * @throws NotFoundException
+ */
+ private function getObject(int $objectId): Concrete
+ {
+ $object = $this->concreteObjectResolver->getById($objectId);
+
+ if ($object === null) {
+ throw new NotFoundException('Data Object', $objectId);
+ }
+
+ return $object;
+ }
+}
diff --git a/src/DataObject/Service/SelectOptionsServiceInterface.php b/src/DataObject/Service/SelectOptionsServiceInterface.php
new file mode 100644
index 000000000..b4f9b933a
--- /dev/null
+++ b/src/DataObject/Service/SelectOptionsServiceInterface.php
@@ -0,0 +1,35 @@
+{value} from one unit to all other available units based on the given {fromUnitId}.
Units have to have {fromUnitId} defined as base unit to be considered for conversion.
unit_quantity_value_convert_all_summary: Convert quantity value from one unit to all other related units
-unit_quantity_value_convert_all_success_response: Converted quantity value
\ No newline at end of file
+unit_quantity_value_convert_all_success_response: Converted quantity value
+data_object_get_select_options_description: |
+ Get all dynamic select options for data objects for given field
+data_object_get_select_options_summary: Get all dynamic select options for data objects
+data_object_get_select_options_success_response: List of dynamic select options
\ No newline at end of file