From 7cbcc42972a568bbf9b01019e1515f9e3a8399b9 Mon Sep 17 00:00:00 2001 From: Martin Eiber Date: Wed, 11 Dec 2024 15:22:09 +0100 Subject: [PATCH] [Data Object Editor] Get Select Options Endpoint (#631) * get select Options for Dynamic select * Apply php-cs-fixer changes * Fix style. * Apply php-cs-fixer changes * Fix style. * Fix Exception Handling. * Apply php-cs-fixer changes --------- Co-authored-by: martineiber --- config/data_objects.yaml | 17 ++ .../Request/SelectOptionRequestBody.php | 52 +++++ .../Controller/SelectOptionsController.php | 95 ++++++++ .../PreResponse/DynamicSelectOptionEvent.php | 39 ++++ .../Hydrator/SelectOptionHydrator.php | 33 +++ .../SelectOptionHydratorInterface.php | 27 +++ src/DataObject/Legacy/ApplyChangesHelper.php | 204 ++++++++++++++++++ .../Legacy/ApplyChangesHelperInterface.php | 37 ++++ .../SelectOptionsParameter.php | 61 ++++++ src/DataObject/Schema/SelectOption.php | 53 +++++ .../Service/SelectOptionsService.php | 164 ++++++++++++++ .../Service/SelectOptionsServiceInterface.php | 35 +++ translations/studio_api_docs.en.yaml | 6 +- 13 files changed, 822 insertions(+), 1 deletion(-) create mode 100644 src/DataObject/Attribute/Request/SelectOptionRequestBody.php create mode 100644 src/DataObject/Controller/SelectOptionsController.php create mode 100644 src/DataObject/Event/PreResponse/DynamicSelectOptionEvent.php create mode 100644 src/DataObject/Hydrator/SelectOptionHydrator.php create mode 100644 src/DataObject/Hydrator/SelectOptionHydratorInterface.php create mode 100644 src/DataObject/Legacy/ApplyChangesHelper.php create mode 100644 src/DataObject/Legacy/ApplyChangesHelperInterface.php create mode 100644 src/DataObject/MappedParameter/SelectOptionsParameter.php create mode 100644 src/DataObject/Schema/SelectOption.php create mode 100644 src/DataObject/Service/SelectOptionsService.php create mode 100644 src/DataObject/Service/SelectOptionsServiceInterface.php 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