From bbce2b02670a83a8e6ebfc1078c3dc274c3b54d1 Mon Sep 17 00:00:00 2001 From: lukmzig Date: Fri, 27 Feb 2026 11:11:20 +0100 Subject: [PATCH 1/4] add endpoints for select options --- composer.json | 2 +- config/class.yaml | 13 +- doc/05_Additional_Custom_Attributes.md | 2 + .../SelectOptions/CreateController.php | 80 +++++++++ .../SelectOptions/DeleteController.php | 82 +++++++++ .../SelectOptions/GetController.php | 79 ++++++++ .../SelectOptions/TreeController.php | 6 +- .../SelectOptions/UpdateController.php | 91 ++++++++++ .../SelectOptions/UsagesController.php | 83 +++++++++ src/Class/Event/SelectOption/DetailEvent.php | 33 ++++ .../Event/SelectOption/UsageItemEvent.php | 33 ++++ .../Hydrator/SelectOption/DetailHydrator.php | 56 ++++++ .../SelectOption/DetailHydratorInterface.php | 25 +++ .../CreateSelectOptionParameters.php | 30 ++++ .../UpdateSelectOptionParameters.php | 54 ++++++ .../Repository/SelectOptionRepository.php | 132 ++++++++++++++ .../SelectOptionRepositoryInterface.php | 64 +++++++ .../SelectOption/CreateSelectOption.php | 40 +++++ .../Schema/SelectOption/SelectOptionData.php | 54 ++++++ .../SelectOption/SelectOptionDetail.php | 102 +++++++++++ .../SelectOption/SelectOptionUsageItem.php | 51 ++++++ .../SelectOption/UpdateSelectOption.php | 48 +++++ .../SelectOptions/SelectOptionService.php | 168 ++++++++++++++++++ .../SelectOptionServiceInterface.php | 68 +++++++ .../TreeService.php} | 30 ++-- .../TreeServiceInterface.php} | 4 +- translations/studio_api_docs.en.yaml | 25 +++ 27 files changed, 1436 insertions(+), 19 deletions(-) create mode 100644 src/Class/Controller/SelectOptions/CreateController.php create mode 100644 src/Class/Controller/SelectOptions/DeleteController.php create mode 100644 src/Class/Controller/SelectOptions/GetController.php create mode 100644 src/Class/Controller/SelectOptions/UpdateController.php create mode 100644 src/Class/Controller/SelectOptions/UsagesController.php create mode 100644 src/Class/Event/SelectOption/DetailEvent.php create mode 100644 src/Class/Event/SelectOption/UsageItemEvent.php create mode 100644 src/Class/Hydrator/SelectOption/DetailHydrator.php create mode 100644 src/Class/Hydrator/SelectOption/DetailHydratorInterface.php create mode 100644 src/Class/MappedParameter/CreateSelectOptionParameters.php create mode 100644 src/Class/MappedParameter/UpdateSelectOptionParameters.php create mode 100644 src/Class/Repository/SelectOptionRepository.php create mode 100644 src/Class/Repository/SelectOptionRepositoryInterface.php create mode 100644 src/Class/Schema/SelectOption/CreateSelectOption.php create mode 100644 src/Class/Schema/SelectOption/SelectOptionData.php create mode 100644 src/Class/Schema/SelectOption/SelectOptionDetail.php create mode 100644 src/Class/Schema/SelectOption/SelectOptionUsageItem.php create mode 100644 src/Class/Schema/SelectOption/UpdateSelectOption.php create mode 100644 src/Class/Service/SelectOptions/SelectOptionService.php create mode 100644 src/Class/Service/SelectOptions/SelectOptionServiceInterface.php rename src/Class/Service/{SelectOptionService.php => SelectOptions/TreeService.php} (80%) rename src/Class/Service/{SelectOptionServiceInterface.php => SelectOptions/TreeServiceInterface.php} (87%) diff --git a/composer.json b/composer.json index 740eda650..dd67e4b8e 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ "php": "~8.3.0 || ~8.4.0", "league/csv": "^9.22", "nesbot/carbon": "^3.8.4", - "pimcore/static-resolver-bundle": "^3.4.0", + "pimcore/static-resolver-bundle": "^3.5.0", "pimcore/generic-data-index-bundle": "^2.4.0", "pimcore/pimcore": "^12.3", "zircote/swagger-php": "^4.8 || ^5.0", diff --git a/config/class.yaml b/config/class.yaml index aa4cc4307..54f3b97dc 100644 --- a/config/class.yaml +++ b/config/class.yaml @@ -41,8 +41,11 @@ services: Pimcore\Bundle\StudioBackendBundle\Class\Service\IdentifierServiceInterface: class: Pimcore\Bundle\StudioBackendBundle\Class\Service\IdentifierService - Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptionServiceInterface: - class: Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptionService + Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptions\TreeServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptions\TreeService + + Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptions\SelectOptionServiceInterface: + class: Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptions\SelectOptionService Pimcore\Bundle\StudioBackendBundle\Class\Service\ClassDefinitionTreeServiceInterface: class: Pimcore\Bundle\StudioBackendBundle\Class\Service\ClassDefinitionTreeService @@ -72,6 +75,9 @@ services: Pimcore\Bundle\StudioBackendBundle\Class\Repository\CustomLayoutRepositoryInterface: class: Pimcore\Bundle\StudioBackendBundle\Class\Repository\CustomLayoutRepository + Pimcore\Bundle\StudioBackendBundle\Class\Repository\SelectOptionRepositoryInterface: + class: Pimcore\Bundle\StudioBackendBundle\Class\Repository\SelectOptionRepository + # # Hydrators # @@ -146,3 +152,6 @@ services: Pimcore\Bundle\StudioBackendBundle\Class\Hydrator\SelectOption\TreeFolderHydratorInterface: class: Pimcore\Bundle\StudioBackendBundle\Class\Hydrator\SelectOption\TreeFolderHydrator + + Pimcore\Bundle\StudioBackendBundle\Class\Hydrator\SelectOption\DetailHydratorInterface: + class: Pimcore\Bundle\StudioBackendBundle\Class\Hydrator\SelectOption\DetailHydrator diff --git a/doc/05_Additional_Custom_Attributes.md b/doc/05_Additional_Custom_Attributes.md index 1ac47954f..cf4ce3892 100644 --- a/doc/05_Additional_Custom_Attributes.md +++ b/doc/05_Additional_Custom_Attributes.md @@ -160,7 +160,9 @@ final class AssetEvent extends AbstractPreResponseEvent - `pre_response.recycle_bin.item` - `pre_response.role_tree_node` - `pre_response.schedule` +- `pre_response.select_option.detail` - `pre_response.select_option.tree` +- `pre_response.select_option.usage_item` - `pre_response.settings.active_bundle` - `pre_response.settings.available_country` - `pre_response.simple_search.preview` diff --git a/src/Class/Controller/SelectOptions/CreateController.php b/src/Class/Controller/SelectOptions/CreateController.php new file mode 100644 index 000000000..b96bfc7e6 --- /dev/null +++ b/src/Class/Controller/SelectOptions/CreateController.php @@ -0,0 +1,80 @@ +value)] + #[Post( + path: self::PREFIX . self::ROUTE, + operationId: 'class_select_option_create', + description: 'class_select_option_create_description', + summary: 'class_select_option_create_summary', + tags: [Tags::ClassDefinition->value], + )] + #[ReferenceRequestBody(CreateSelectOption::class)] + #[SuccessResponse( + description: 'class_select_option_create_success_response', + content: new JsonContent(ref: SelectOptionDetail::class) + )] + #[DefaultResponses([ + HttpResponseCodes::CONFLICT, + HttpResponseCodes::INTERNAL_SERVER_ERROR, + HttpResponseCodes::UNAUTHORIZED, + ])] + public function createSelectOption( + #[MapRequestPayload] CreateSelectOptionParameters $parameters, + ): JsonResponse { + return $this->jsonResponse( + $this->selectOptionService->createSelectOption($parameters) + ); + } +} diff --git a/src/Class/Controller/SelectOptions/DeleteController.php b/src/Class/Controller/SelectOptions/DeleteController.php new file mode 100644 index 000000000..e1a0f3459 --- /dev/null +++ b/src/Class/Controller/SelectOptions/DeleteController.php @@ -0,0 +1,82 @@ +value)] + #[Delete( + path: self::PREFIX . self::ROUTE, + operationId: 'class_select_option_delete', + description: 'class_select_option_delete_description', + summary: 'class_select_option_delete_summary', + tags: [Tags::ClassDefinition->value], + )] + #[StringParameter( + name: 'id', + example: 'EventStatus', + description: 'Select option configuration ID', + required: true + )] + #[SuccessResponse( + description: 'class_select_option_delete_success_response' + )] + #[DefaultResponses([ + HttpResponseCodes::CONFLICT, + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::INTERNAL_SERVER_ERROR, + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::UNAUTHORIZED, + ])] + public function deleteSelectOption(string $id): Response + { + $this->selectOptionService->deleteSelectOption($id); + + return new Response(); + } +} diff --git a/src/Class/Controller/SelectOptions/GetController.php b/src/Class/Controller/SelectOptions/GetController.php new file mode 100644 index 000000000..71d0c86dc --- /dev/null +++ b/src/Class/Controller/SelectOptions/GetController.php @@ -0,0 +1,79 @@ +value)] + #[Get( + path: self::PREFIX . self::ROUTE, + operationId: 'class_select_option_get', + description: 'class_select_option_get_description', + summary: 'class_select_option_get_summary', + tags: [Tags::ClassDefinition->value], + )] + #[StringParameter( + name: 'id', + example: 'EventStatus', + description: 'Select option configuration ID', + required: true + )] + #[SuccessResponse( + description: 'class_select_option_get_success_response', + content: new JsonContent(ref: SelectOptionDetail::class) + )] + #[DefaultResponses([ + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::UNAUTHORIZED, + ])] + public function getSelectOption(string $id): JsonResponse + { + return $this->jsonResponse( + $this->selectOptionService->getSelectOption($id) + ); + } +} diff --git a/src/Class/Controller/SelectOptions/TreeController.php b/src/Class/Controller/SelectOptions/TreeController.php index a2bdee163..d703d8c4a 100644 --- a/src/Class/Controller/SelectOptions/TreeController.php +++ b/src/Class/Controller/SelectOptions/TreeController.php @@ -16,7 +16,7 @@ use OpenApi\Attributes\Get; use Pimcore\Bundle\StudioBackendBundle\Class\Attribute\Response\Property\AnyOfSelectOptionNodes; use Pimcore\Bundle\StudioBackendBundle\Class\MappedParameter\TreeParameter; -use Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptionServiceInterface; +use Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptions\TreeServiceInterface; use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Parameter\Query\BoolParameter; use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attribute\Response\Content\CollectionJson; @@ -44,12 +44,12 @@ final class TreeController extends AbstractApiController public function __construct( SerializerInterface $serializer, - private readonly SelectOptionServiceInterface $optionService, + private readonly TreeServiceInterface $optionService, ) { parent::__construct($serializer); } - #[Route(self::ROUTE, name: 'pimcore_studio_api_class_select_option_tree', methods: ['GET'])] + #[Route(self::ROUTE, name: 'pimcore_studio_api_class_select_option_tree', methods: ['GET'], priority: 10)] #[IsGranted(UserPermissions::SELECT_OPTIONS->value)] #[Get( path: self::PREFIX . self::ROUTE, diff --git a/src/Class/Controller/SelectOptions/UpdateController.php b/src/Class/Controller/SelectOptions/UpdateController.php new file mode 100644 index 000000000..82c558b7a --- /dev/null +++ b/src/Class/Controller/SelectOptions/UpdateController.php @@ -0,0 +1,91 @@ +value)] + #[Put( + path: self::PREFIX . self::ROUTE, + operationId: 'class_select_option_update', + description: 'class_select_option_update_description', + summary: 'class_select_option_update_summary', + tags: [Tags::ClassDefinition->value], + )] + #[StringParameter( + name: 'id', + example: 'EventStatus', + description: 'Select option configuration ID', + required: true + )] + #[ReferenceRequestBody(UpdateSelectOption::class)] + #[SuccessResponse( + description: 'class_select_option_update_success_response', + content: new JsonContent(ref: SelectOptionDetail::class) + )] + #[DefaultResponses([ + HttpResponseCodes::FORBIDDEN, + HttpResponseCodes::INTERNAL_SERVER_ERROR, + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::UNAUTHORIZED, + ])] + public function updateSelectOption( + string $id, + #[MapRequestPayload] UpdateSelectOptionParameters $parameters, + ): JsonResponse { + return $this->jsonResponse( + $this->selectOptionService->updateSelectOption($id, $parameters) + ); + } +} diff --git a/src/Class/Controller/SelectOptions/UsagesController.php b/src/Class/Controller/SelectOptions/UsagesController.php new file mode 100644 index 000000000..99dcf144d --- /dev/null +++ b/src/Class/Controller/SelectOptions/UsagesController.php @@ -0,0 +1,83 @@ +value)] + #[Get( + path: self::PREFIX . self::ROUTE, + operationId: 'class_select_option_get_usages', + description: 'class_select_option_get_usages_description', + summary: 'class_select_option_get_usages_summary', + tags: [Tags::ClassDefinition->value], + )] + #[StringParameter( + name: 'id', + example: 'EventStatus', + description: 'Select option configuration ID', + required: true + )] + #[SuccessResponse( + description: 'class_select_option_get_usages_success_response', + content: new ItemsJson(SelectOptionUsageItem::class) + )] + #[DefaultResponses([ + HttpResponseCodes::NOT_FOUND, + HttpResponseCodes::UNAUTHORIZED, + ])] + public function getSelectOptionUsages(string $id): JsonResponse + { + return $this->jsonResponse( + ['items' => $this->selectOptionService->getSelectOptionUsages($id)] + ); + } +} diff --git a/src/Class/Event/SelectOption/DetailEvent.php b/src/Class/Event/SelectOption/DetailEvent.php new file mode 100644 index 000000000..f815f9e44 --- /dev/null +++ b/src/Class/Event/SelectOption/DetailEvent.php @@ -0,0 +1,33 @@ +selectOptionDetail; + } +} diff --git a/src/Class/Event/SelectOption/UsageItemEvent.php b/src/Class/Event/SelectOption/UsageItemEvent.php new file mode 100644 index 000000000..fd66eb22d --- /dev/null +++ b/src/Class/Event/SelectOption/UsageItemEvent.php @@ -0,0 +1,33 @@ +usageItem; + } +} diff --git a/src/Class/Hydrator/SelectOption/DetailHydrator.php b/src/Class/Hydrator/SelectOption/DetailHydrator.php new file mode 100644 index 000000000..dd69e6acf --- /dev/null +++ b/src/Class/Hydrator/SelectOption/DetailHydrator.php @@ -0,0 +1,56 @@ +getId(), + $config->getGroup(), + $config->getAdminOnly(), + $config->getUseTraits(), + $config->getImplementsInterfaces(), + $this->hydrateSelectOptions($config->getSelectOptions()), + $config->getEnumName(true), + $isWriteable, + ); + } + + /** + * @param SelectOption[] $selectOptions + * + * @return SelectOptionData[] + */ + private function hydrateSelectOptions(array $selectOptions): array + { + return array_map( + static fn (SelectOption $option): SelectOptionData => new SelectOptionData( + $option->getValue(), + $option->getLabel(), + $option->getName(), + ), + $selectOptions, + ); + } +} diff --git a/src/Class/Hydrator/SelectOption/DetailHydratorInterface.php b/src/Class/Hydrator/SelectOption/DetailHydratorInterface.php new file mode 100644 index 000000000..4c84afb2e --- /dev/null +++ b/src/Class/Hydrator/SelectOption/DetailHydratorInterface.php @@ -0,0 +1,25 @@ +id; + } +} diff --git a/src/Class/MappedParameter/UpdateSelectOptionParameters.php b/src/Class/MappedParameter/UpdateSelectOptionParameters.php new file mode 100644 index 000000000..7e62da27d --- /dev/null +++ b/src/Class/MappedParameter/UpdateSelectOptionParameters.php @@ -0,0 +1,54 @@ +group; + } + + public function isAdminOnly(): bool + { + return $this->adminOnly; + } + + public function getUseTraits(): string + { + return $this->useTraits; + } + + public function getImplementsInterfaces(): string + { + return $this->implementsInterfaces; + } + + public function getSelectOptions(): ?array + { + return $this->selectOptions; + } +} diff --git a/src/Class/Repository/SelectOptionRepository.php b/src/Class/Repository/SelectOptionRepository.php new file mode 100644 index 000000000..406423e74 --- /dev/null +++ b/src/Class/Repository/SelectOptionRepository.php @@ -0,0 +1,132 @@ +load(); + } + + public function getById(string $id): Config + { + $config = $this->configResolver->getById($id); + + if ($config === null) { + throw new NotFoundException(type: 'Select Option', id: $id); + } + + return $config; + } + + /** + * {@inheritdoc} + */ + public function create(string $id): Config + { + $listing = new Listing(); + + if ($listing->hasConfig($id)) { + throw new ElementExistsException( + sprintf( + 'Select options with the same ID already exists (lower/upper cases may be different): %s', + $id + ) + ); + } + + try { + $config = $this->configResolver->createFromData([ + Config::PROPERTY_ID => $id, + ]); + } catch (InvalidArgumentException | RuntimeException $e) { + throw new ApiInvalidArgumentException(message: $e->getMessage(), previous: $e); + } + + try { + $config->save(); + } catch (Exception $e) { + throw new ElementSavingFailedException(null, $e->getMessage(), $e); + } + + return $config; + } + + /** + * {@inheritdoc} + */ + public function save(Config $config): void + { + if (!$this->isWriteable($config)) { + throw new NotWriteableException(self::NOT_WRITEABLE_EXCEPTION_MESSAGE); + } + + try { + $config->save(); + } catch (Exception $e) { + throw new ElementSavingFailedException(null, $e->getMessage(), $e); + } + } + + /** + * {@inheritdoc} + */ + public function delete(Config $config): void + { + if (!$this->isWriteable($config)) { + throw new NotWriteableException(self::NOT_WRITEABLE_EXCEPTION_MESSAGE); + } + + try { + $config->delete(); + } catch (RuntimeException $e) { + throw new ConflictException($e->getMessage(), $e); + } + } + + public function isWriteable(Config $config): bool + { + return $config->isWriteable(); + } + + public function getFieldsUsedIn(Config $config): array + { + return $config->getFieldsUsedIn(); + } +} diff --git a/src/Class/Repository/SelectOptionRepositoryInterface.php b/src/Class/Repository/SelectOptionRepositoryInterface.php new file mode 100644 index 000000000..d0fa8e1c8 --- /dev/null +++ b/src/Class/Repository/SelectOptionRepositoryInterface.php @@ -0,0 +1,64 @@ + + */ + public function getFieldsUsedIn(Config $config): array; +} diff --git a/src/Class/Schema/SelectOption/CreateSelectOption.php b/src/Class/Schema/SelectOption/CreateSelectOption.php new file mode 100644 index 000000000..a7f966845 --- /dev/null +++ b/src/Class/Schema/SelectOption/CreateSelectOption.php @@ -0,0 +1,40 @@ +id; + } +} diff --git a/src/Class/Schema/SelectOption/SelectOptionData.php b/src/Class/Schema/SelectOption/SelectOptionData.php new file mode 100644 index 000000000..739d66e0a --- /dev/null +++ b/src/Class/Schema/SelectOption/SelectOptionData.php @@ -0,0 +1,54 @@ +value; + } + + public function getLabel(): string + { + return $this->label; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/src/Class/Schema/SelectOption/SelectOptionDetail.php b/src/Class/Schema/SelectOption/SelectOptionDetail.php new file mode 100644 index 000000000..f64a3cc66 --- /dev/null +++ b/src/Class/Schema/SelectOption/SelectOptionDetail.php @@ -0,0 +1,102 @@ +id; + } + + public function getGroup(): ?string + { + return $this->group; + } + + public function isAdminOnly(): bool + { + return $this->adminOnly; + } + + public function getUseTraits(): string + { + return $this->useTraits; + } + + public function getImplementsInterfaces(): string + { + return $this->implementsInterfaces; + } + + /** + * @return SelectOptionData[] + */ + public function getSelectOptions(): array + { + return $this->selectOptions; + } + + public function getEnumName(): string + { + return $this->enumName; + } + + public function getIsWriteable(): bool + { + return $this->isWriteable; + } +} diff --git a/src/Class/Schema/SelectOption/SelectOptionUsageItem.php b/src/Class/Schema/SelectOption/SelectOptionUsageItem.php new file mode 100644 index 000000000..dfcdc193f --- /dev/null +++ b/src/Class/Schema/SelectOption/SelectOptionUsageItem.php @@ -0,0 +1,51 @@ +class; + } + + public function getField(): string + { + return $this->field; + } +} diff --git a/src/Class/Schema/SelectOption/UpdateSelectOption.php b/src/Class/Schema/SelectOption/UpdateSelectOption.php new file mode 100644 index 000000000..77ff1da64 --- /dev/null +++ b/src/Class/Schema/SelectOption/UpdateSelectOption.php @@ -0,0 +1,48 @@ +selectOptionRepository->getById($id); + + return $this->hydrateDetail($config); + } + + /** + * {@inheritdoc} + */ + public function createSelectOption(CreateSelectOptionParameters $parameters): SelectOptionDetail + { + $config = $this->selectOptionRepository->create($parameters->getId()); + + return $this->hydrateDetail($config); + } + + /** + * {@inheritdoc} + */ + public function updateSelectOption(string $id, UpdateSelectOptionParameters $parameters): SelectOptionDetail + { + $config = $this->selectOptionRepository->getById($id); + $this->checkAdminAccess($config); + + $this->validateSelectOptions($parameters->getSelectOptions()); + + try { + $config = $this->configResolver->createFromData([ + Config::PROPERTY_ID => $id, + Config::PROPERTY_GROUP => $parameters->getGroup(), + Config::PROPERTY_ADMIN_ONLY => $parameters->isAdminOnly(), + Config::PROPERTY_USE_TRAITS => $parameters->getUseTraits(), + Config::PROPERTY_IMPLEMENTS_INTERFACES => $parameters->getImplementsInterfaces(), + Config::PROPERTY_SELECT_OPTIONS => $parameters->getSelectOptions(), + ]); + } catch (InvalidArgumentException | RuntimeException $e) { + throw new ApiInvalidArgumentException(message: $e->getMessage(), previous: $e); + } + + $this->selectOptionRepository->save($config); + + return $this->hydrateDetail($config); + } + + /** + * {@inheritdoc} + */ + public function deleteSelectOption(string $id): void + { + $config = $this->selectOptionRepository->getById($id); + $this->checkAdminAccess($config); + $this->selectOptionRepository->delete($config); + } + + /** + * {@inheritdoc} + */ + public function getSelectOptionUsages(string $id): array + { + $config = $this->selectOptionRepository->getById($id); + $fieldsUsedIn = $this->selectOptionRepository->getFieldsUsedIn($config); + $usages = []; + + foreach ($fieldsUsedIn as $className => $fields) { + foreach ($fields as $field) { + $usageItem = new SelectOptionUsageItem($className, $field); + $this->eventDispatcher->dispatch( + new UsageItemEvent($usageItem), + UsageItemEvent::EVENT_NAME + ); + $usages[] = $usageItem; + } + } + + return $usages; + } + + private function hydrateDetail(Config $config): SelectOptionDetail + { + $isWriteable = $this->selectOptionRepository->isWriteable($config); + $detail = $this->detailHydrator->hydrate($config, $isWriteable); + $this->eventDispatcher->dispatch(new DetailEvent($detail), DetailEvent::EVENT_NAME); + + return $detail; + } + + /** + * @throws ApiInvalidArgumentException + */ + private function validateSelectOptions(?array $selectOptions): void + { + if ($selectOptions === null) { + return; + } + + foreach ($selectOptions as $index => $option) { + if (!is_array($option) || !isset($option['value']) || $option['value'] === '') { + throw new ApiInvalidArgumentException( + message: sprintf( + 'Select option at index %d must have a non-empty "value" field', + $index, + ), + ); + } + } + } + + /** + * @throws ForbiddenException + */ + private function checkAdminAccess(Config $config): void + { + if ($config->getAdminOnly() && !$this->securityService->getCurrentUser()->isAdmin()) { + throw new ForbiddenException('Restricted to admin users'); + } + } +} diff --git a/src/Class/Service/SelectOptions/SelectOptionServiceInterface.php b/src/Class/Service/SelectOptions/SelectOptionServiceInterface.php new file mode 100644 index 000000000..b81779c22 --- /dev/null +++ b/src/Class/Service/SelectOptions/SelectOptionServiceInterface.php @@ -0,0 +1,68 @@ +selectOptionRepository->listSelectOptions(); if ($grouped === false) { return $this->getUngroupedTree($selectOptionConfigs); @@ -56,11 +57,13 @@ public function getTree(bool $grouped = false): array return $this->getGroupedNodes($groups); } - private function getUngroupedTree(Listing $configs): array + /** + * @param Config[] $configs + */ + private function getUngroupedTree(array $configs): array { $hydrated = []; - /** @var Config $config */ foreach ($configs as $config) { $hydrated[] = $this->hydrateConfig($config); } @@ -68,11 +71,13 @@ private function getUngroupedTree(Listing $configs): array return $hydrated; } - private function getGroups(Listing $configs): array + /** + * @param Config[] $configs + */ + private function getGroups(array $configs): array { $groups = []; - /** @var Config $config */ foreach ($configs as $config) { [$groupName, $type] = $this->resolveGroupInfo($config); @@ -103,8 +108,11 @@ private function resolveGroupInfo(Config $config): array private function sortGroups(array $groups): array { - $types = array_column($groups, 'type'); - array_multisort($types, SORT_DESC, array_keys($groups), SORT_ASC, $groups); + uksort($groups, static function (string $a, string $b) use ($groups): int { + $typeComparison = $groups[$b]['type'] <=> $groups[$a]['type']; + + return $typeComparison !== 0 ? $typeComparison : $a <=> $b; + }); return $groups; } diff --git a/src/Class/Service/SelectOptionServiceInterface.php b/src/Class/Service/SelectOptions/TreeServiceInterface.php similarity index 87% rename from src/Class/Service/SelectOptionServiceInterface.php rename to src/Class/Service/SelectOptions/TreeServiceInterface.php index f468877e7..9a77fff9a 100644 --- a/src/Class/Service/SelectOptionServiceInterface.php +++ b/src/Class/Service/SelectOptions/TreeServiceInterface.php @@ -11,7 +11,7 @@ * @license Pimcore Open Core License (POCL) */ -namespace Pimcore\Bundle\StudioBackendBundle\Class\Service; +namespace Pimcore\Bundle\StudioBackendBundle\Class\Service\SelectOptions; use Pimcore\Bundle\StudioBackendBundle\Class\Schema\SelectOption\SelectOptionTree; use Pimcore\Bundle\StudioBackendBundle\Class\Schema\SelectOption\SelectOptionTreeFolder; @@ -19,7 +19,7 @@ /** * @internal */ -interface SelectOptionServiceInterface +interface TreeServiceInterface { /** * @return SelectOptionTree[]|SelectOptionTreeFolder[] diff --git a/translations/studio_api_docs.en.yaml b/translations/studio_api_docs.en.yaml index 15ef31ab3..701b3f224 100644 --- a/translations/studio_api_docs.en.yaml +++ b/translations/studio_api_docs.en.yaml @@ -1584,6 +1584,31 @@ class_select_option_get_tree_description: | Get select options tree data. Results can be grouped based on the {withGroup} parameter. class_select_option_get_tree_summary: Get select options tree data class_select_option_get_tree_success_response: Select options data for the tree view +class_select_option_get_description: | + Get a single select option configuration by its ID. + Returns all configuration details including select option entries, group, traits, interfaces, and the fully qualified enum name. +class_select_option_get_summary: Get select option configuration by ID +class_select_option_get_success_response: Select option configuration detail +class_select_option_create_description: | + Create a new select option configuration. + The ID must be unique (case-insensitive). Returns the newly created configuration detail. +class_select_option_create_summary: Create a new select option configuration +class_select_option_create_success_response: Newly created select option configuration detail +class_select_option_update_description: | + Update an existing select option configuration. + Replaces the configuration data with the provided values. Admin-only configurations require admin access. +class_select_option_update_summary: Update a select option configuration +class_select_option_update_success_response: Updated select option configuration detail +class_select_option_delete_description: | + Delete a select option configuration by its ID. + Fails with a conflict error if the configuration is currently in use by class definitions. + Admin-only configurations require admin access. +class_select_option_delete_summary: Delete a select option configuration +class_select_option_delete_success_response: Successfully deleted the select option configuration +class_select_option_get_usages_description: | + Get the list of class definitions and fields that use a specific select option configuration. +class_select_option_get_usages_summary: Get usages of a select option configuration +class_select_option_get_usages_success_response: List of classes and fields using the select option configuration class_custom_layout_get: Get custom layout by given id class_custom_layout_get_description: Get custom layout by given id class_custom_layout_get_summary: Get custom layout by given id From aa678469762d8dc93c84184a584c0017399b2f72 Mon Sep 17 00:00:00 2001 From: lukmzig <30526586+lukmzig@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:18:24 +0000 Subject: [PATCH 2/4] Apply php-cs-fixer changes --- src/Class/Service/SelectOptions/SelectOptionService.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Class/Service/SelectOptions/SelectOptionService.php b/src/Class/Service/SelectOptions/SelectOptionService.php index 99405ab8d..8df9dca67 100644 --- a/src/Class/Service/SelectOptions/SelectOptionService.php +++ b/src/Class/Service/SelectOptions/SelectOptionService.php @@ -29,6 +29,7 @@ use Pimcore\Model\DataObject\SelectOptions\Config; use RuntimeException; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use function is_array; use function sprintf; /** From 1f3cc4bad10ed0fd479701b0c4bf911a1b98ff8b Mon Sep 17 00:00:00 2001 From: lukmzig Date: Fri, 27 Feb 2026 11:27:21 +0100 Subject: [PATCH 3/4] fix sonar --- .../SelectOption/SelectOptionDetail.php | 19 +++++++++-- .../SelectOption/SelectOptionUsageItem.php | 2 +- .../SelectOption/UpdateSelectOption.php | 34 ++++++++++++++++++- 3 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/Class/Schema/SelectOption/SelectOptionDetail.php b/src/Class/Schema/SelectOption/SelectOptionDetail.php index f64a3cc66..8f3533423 100644 --- a/src/Class/Schema/SelectOption/SelectOptionDetail.php +++ b/src/Class/Schema/SelectOption/SelectOptionDetail.php @@ -25,7 +25,16 @@ #[Schema( schema: 'SelectOptionDetail', title: 'Select Option Detail', - required: ['id', 'group', 'adminOnly', 'useTraits', 'implementsInterfaces', 'selectOptions', 'enumName', 'isWriteable'], + required: [ + 'id', + 'group', + 'adminOnly', + 'useTraits', + 'implementsInterfaces', + 'selectOptions', + 'enumName', + 'isWriteable' + ], type: 'object' )] final class SelectOptionDetail implements AdditionalAttributesInterface @@ -37,7 +46,7 @@ public function __construct( private readonly string $id, #[Property(description: 'Group name', type: 'string', example: 'system', nullable: true)] private readonly ?string $group, - #[Property(description: 'Whether this configuration is restricted to admin users', type: 'boolean', example: false)] + #[Property(description: 'Whether this configuration is restricted to admin', type: 'boolean', example: false)] private readonly bool $adminOnly, #[Property(description: 'PHP traits to use', type: 'string', example: '')] private readonly string $useTraits, @@ -50,7 +59,11 @@ public function __construct( )] /** @var SelectOptionData[] */ private readonly array $selectOptions, - #[Property(description: 'Fully qualified enum name', type: 'string', example: 'Pimcore\\Model\\DataObject\\SelectOptions\\EventStatus')] + #[Property( + description: 'Fully qualified enum name', + type: 'string', + example: 'Pimcore\\Model\\DataObject\\SelectOptions\\EventStatus' + )] private readonly string $enumName, #[Property(description: 'Whether the configuration is writeable', type: 'boolean', example: true)] private readonly bool $isWriteable, diff --git a/src/Class/Schema/SelectOption/SelectOptionUsageItem.php b/src/Class/Schema/SelectOption/SelectOptionUsageItem.php index dfcdc193f..0d31cdb4e 100644 --- a/src/Class/Schema/SelectOption/SelectOptionUsageItem.php +++ b/src/Class/Schema/SelectOption/SelectOptionUsageItem.php @@ -32,7 +32,7 @@ final class SelectOptionUsageItem implements AdditionalAttributesInterface use AdditionalAttributesTrait; public function __construct( - #[Property(description: 'Name of the class or definition using the select options', type: 'string', example: 'Class Product')] + #[Property(description: 'Name of the class using the select options', type: 'string', example: 'Car')] private readonly string $class, #[Property(description: 'Name of the field using the select options', type: 'string', example: 'status')] private readonly string $field, diff --git a/src/Class/Schema/SelectOption/UpdateSelectOption.php b/src/Class/Schema/SelectOption/UpdateSelectOption.php index 77ff1da64..0f062114f 100644 --- a/src/Class/Schema/SelectOption/UpdateSelectOption.php +++ b/src/Class/Schema/SelectOption/UpdateSelectOption.php @@ -23,6 +23,13 @@ #[Schema( schema: 'UpdateSelectOption', title: 'Schema used to update select option configurations', + required: [ + 'group', + 'adminOnly', + 'useTraits', + 'implementsInterfaces', + 'selectOptions' + ], type: 'object' )] final readonly class UpdateSelectOption @@ -30,7 +37,7 @@ public function __construct( #[Property(description: 'Group name', type: 'string', example: 'system', nullable: true)] private ?string $group = null, - #[Property(description: 'Whether this configuration is restricted to admin users', type: 'boolean', example: false)] + #[Property(description: 'Whether this configuration is restricted to admin', type: 'boolean', example: false)] private bool $adminOnly = false, #[Property(description: 'PHP traits to use', type: 'string', example: '')] private string $useTraits = '', @@ -45,4 +52,29 @@ public function __construct( private ?array $selectOptions = null, ) { } + + public function getGroup(): ?string + { + return $this->group; + } + + public function isAdminOnly(): bool + { + return $this->adminOnly; + } + + public function getUseTraits(): string + { + return $this->useTraits; + } + + public function getImplementsInterfaces(): string + { + return $this->implementsInterfaces; + } + + public function getSelectOptions(): ?array + { + return $this->selectOptions; + } } From f948bf4e617296e0c8a8f2ee2a36a7692e750a7f Mon Sep 17 00:00:00 2001 From: lukmzig <30526586+lukmzig@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:28:04 +0000 Subject: [PATCH 4/4] Apply php-cs-fixer changes --- src/Class/Schema/SelectOption/SelectOptionDetail.php | 2 +- src/Class/Schema/SelectOption/UpdateSelectOption.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Class/Schema/SelectOption/SelectOptionDetail.php b/src/Class/Schema/SelectOption/SelectOptionDetail.php index 8f3533423..f9679f47e 100644 --- a/src/Class/Schema/SelectOption/SelectOptionDetail.php +++ b/src/Class/Schema/SelectOption/SelectOptionDetail.php @@ -33,7 +33,7 @@ 'implementsInterfaces', 'selectOptions', 'enumName', - 'isWriteable' + 'isWriteable', ], type: 'object' )] diff --git a/src/Class/Schema/SelectOption/UpdateSelectOption.php b/src/Class/Schema/SelectOption/UpdateSelectOption.php index 0f062114f..f1544485f 100644 --- a/src/Class/Schema/SelectOption/UpdateSelectOption.php +++ b/src/Class/Schema/SelectOption/UpdateSelectOption.php @@ -28,7 +28,7 @@ 'adminOnly', 'useTraits', 'implementsInterfaces', - 'selectOptions' + 'selectOptions', ], type: 'object' )]