From c56c18189e04e8a95aab0c8345e395abbceb7160 Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Wed, 22 Apr 2026 20:18:31 +0200 Subject: [PATCH 1/4] [Index] filter class dropdown to classes implementing IndexableInterface --- .../Controller/IndexController.php | 23 +++----- .../IndexBundle/Form/Type/IndexType.php | 3 +- .../Type/IndexablePimcoreClassChoiceType.php | 54 +++++++++++++++++++ .../IndexBundle/Resources/config/services.yml | 4 ++ .../Resources/config/services/form.yml | 6 +++ .../Service/IndexableClassesProvider.php | 49 +++++++++++++++++ .../IndexableClassesProviderInterface.php | 26 +++++++++ 7 files changed, 146 insertions(+), 19 deletions(-) create mode 100644 src/CoreShop/Bundle/IndexBundle/Form/Type/IndexablePimcoreClassChoiceType.php create mode 100644 src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php create mode 100644 src/CoreShop/Component/Index/Service/IndexableClassesProviderInterface.php diff --git a/src/CoreShop/Bundle/IndexBundle/Controller/IndexController.php b/src/CoreShop/Bundle/IndexBundle/Controller/IndexController.php index 8921812336..8b1b80284b 100644 --- a/src/CoreShop/Bundle/IndexBundle/Controller/IndexController.php +++ b/src/CoreShop/Bundle/IndexBundle/Controller/IndexController.php @@ -22,7 +22,7 @@ use CoreShop\Bundle\StudioFormBundle\Form\Schema\RuleFormSchemaCollector; use CoreShop\Component\Index\Interpreter\LocalizedInterpreterInterface; use CoreShop\Component\Index\Interpreter\RelationInterpreterInterface; -use CoreShop\Component\Index\Model\IndexableInterface; +use CoreShop\Component\Index\Service\IndexableClassesProviderInterface; use CoreShop\Component\Registry\ServiceRegistryInterface; use Pimcore\Model\DataObject; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -49,6 +49,7 @@ public function getTypesAction(): Response public function getConfigAction( RuleFormSchemaCollector $schemaCollector, + IndexableClassesProviderInterface $indexableClassesProvider, #[Autowire(service: 'coreshop.form_registry.index.getter')] FormTypeRegistryInterface $getterFormTypeRegistry, #[Autowire(service: 'coreshop.form_registry.index.interpreter')] @@ -118,22 +119,10 @@ public function getConfigAction( } } - $classes = new DataObject\ClassDefinition\Listing(); - $classes = $classes->load(); - $availableClasses = []; - - foreach ($classes as $class) { - if ($class instanceof DataObject\ClassDefinition) { - $pimcoreClass = 'Pimcore\Model\DataObject\\' . ucfirst($class->getName()); - $implements = class_implements($pimcoreClass) ?: []; - - if (in_array(IndexableInterface::class, $implements, true)) { - $availableClasses[] = [ - 'name' => $class->getName(), - ]; - } - } - } + $availableClasses = array_map( + static fn (string $name) => ['name' => $name], + $indexableClassesProvider->getIndexableClassNames(), + ); $workersResult = []; diff --git a/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexType.php b/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexType.php index 5240faaa64..fe3ce7822a 100644 --- a/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexType.php +++ b/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexType.php @@ -19,7 +19,6 @@ use CoreShop\Bundle\ResourceBundle\Form\Registry\FormTypeRegistryInterface; use CoreShop\Bundle\ResourceBundle\Form\Type\AbstractResourceType; -use CoreShop\Bundle\ResourceBundle\Form\Type\PimcoreClassChoiceType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -42,7 +41,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void $builder ->add('name', TextType::class) ->add('worker', IndexWorkerChoiceType::class) - ->add('class', PimcoreClassChoiceType::class) + ->add('class', IndexablePimcoreClassChoiceType::class) ->add('columns', IndexColumnCollectionType::class) ->add('indexLastVersion', CheckboxType::class) ; diff --git a/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexablePimcoreClassChoiceType.php b/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexablePimcoreClassChoiceType.php new file mode 100644 index 0000000000..f0e9351506 --- /dev/null +++ b/src/CoreShop/Bundle/IndexBundle/Form/Type/IndexablePimcoreClassChoiceType.php @@ -0,0 +1,54 @@ +indexableClassesProvider->getIndexableClassNames() as $name) { + $choices[$name] = $name; + } + + $resolver->setDefaults([ + 'choices' => $choices, + ]); + } + + public function getParent(): string + { + return ChoiceType::class; + } + + public function getBlockPrefix(): string + { + return 'coreshop_index_indexable_pimcore_class_choice'; + } +} diff --git a/src/CoreShop/Bundle/IndexBundle/Resources/config/services.yml b/src/CoreShop/Bundle/IndexBundle/Resources/config/services.yml index 705ab93654..dd9c6a794b 100755 --- a/src/CoreShop/Bundle/IndexBundle/Resources/config/services.yml +++ b/src/CoreShop/Bundle/IndexBundle/Resources/config/services.yml @@ -339,6 +339,10 @@ services: - '@coreshop.repository.index' - '@coreshop.registry.index.worker' + # Indexable Classes Provider + CoreShop\Component\Index\Service\IndexableClassesProviderInterface: '@CoreShop\Bundle\IndexBundle\Service\IndexableClassesProvider' + CoreShop\Bundle\IndexBundle\Service\IndexableClassesProvider: ~ + CoreShop\Component\Index\Extension\DecimalIndexColumnTypeConfigExtension: tags: - { name: coreshop.index.extension } diff --git a/src/CoreShop/Bundle/IndexBundle/Resources/config/services/form.yml b/src/CoreShop/Bundle/IndexBundle/Resources/config/services/form.yml index a0ad5a672c..786fe22647 100644 --- a/src/CoreShop/Bundle/IndexBundle/Resources/config/services/form.yml +++ b/src/CoreShop/Bundle/IndexBundle/Resources/config/services/form.yml @@ -38,6 +38,12 @@ services: - { name: form.type } - { name: coreshop.studio_form } + CoreShop\Bundle\IndexBundle\Form\Type\IndexablePimcoreClassChoiceType: + arguments: + - '@CoreShop\Component\Index\Service\IndexableClassesProviderInterface' + tags: + - { name: form.type } + CoreShop\Bundle\IndexBundle\Form\Schema\IndexSchemaEnricher: tags: - { name: coreshop_studio_form.enricher } diff --git a/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php b/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php new file mode 100644 index 0000000000..2984661466 --- /dev/null +++ b/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php @@ -0,0 +1,49 @@ +load() as $class) { + if (!$class instanceof ClassDefinition) { + continue; + } + + $pimcoreClass = 'Pimcore\\Model\\DataObject\\' . ucfirst($class->getName()); + + if (!class_exists($pimcoreClass)) { + continue; + } + + if (in_array(IndexableInterface::class, class_implements($pimcoreClass) ?: [], true)) { + $result[] = $class->getName(); + } + } + + return $result; + } +} diff --git a/src/CoreShop/Component/Index/Service/IndexableClassesProviderInterface.php b/src/CoreShop/Component/Index/Service/IndexableClassesProviderInterface.php new file mode 100644 index 0000000000..8578377539 --- /dev/null +++ b/src/CoreShop/Component/Index/Service/IndexableClassesProviderInterface.php @@ -0,0 +1,26 @@ + Pimcore DataObject class names whose generated model implements IndexableInterface + */ + public function getIndexableClassNames(): array; +} From f64588082883ae70f79240f62633eb5e114c8428 Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Wed, 22 Apr 2026 20:40:14 +0200 Subject: [PATCH 2/4] [Index] drop TableIndex data_class so MysqlWorker config roundtrips as array --- .../Type/Worker/MysqlWorkerTableIndexType.php | 10 ++----- .../Bundle/IndexBundle/Worker/MysqlWorker.php | 26 ++++++++++--------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/src/CoreShop/Bundle/IndexBundle/Form/Type/Worker/MysqlWorkerTableIndexType.php b/src/CoreShop/Bundle/IndexBundle/Form/Type/Worker/MysqlWorkerTableIndexType.php index 0fbcbdda82..f9f70021aa 100644 --- a/src/CoreShop/Bundle/IndexBundle/Form/Type/Worker/MysqlWorkerTableIndexType.php +++ b/src/CoreShop/Bundle/IndexBundle/Form/Type/Worker/MysqlWorkerTableIndexType.php @@ -21,8 +21,8 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\CollectionType; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\OptionsResolver\OptionsResolver; final class MysqlWorkerTableIndexType extends AbstractType { @@ -37,6 +37,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => false, ]) ->add('columns', CollectionType::class, [ + 'entry_type' => TextType::class, 'allow_delete' => true, 'allow_add' => true, 'required' => false, @@ -44,13 +45,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ; } - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefault('data_class', TableIndex::class); - - parent::configureOptions($resolver); - } - public function getBlockPrefix(): string { return 'coreshop_index_worker_mysql'; diff --git a/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php b/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php index b884d5e4ec..1643e2cb04 100644 --- a/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php +++ b/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php @@ -207,14 +207,15 @@ protected function createTableSchema(IndexInterface $index, Schema $tableSchema) } if (array_key_exists('indexes', $index->getConfiguration())) { - /** - * @var TableIndex $tableIndex - */ foreach ($index->getConfiguration()['indexes'] as $tableIndex) { - if ($tableIndex->getType() === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { - $table->addUniqueIndex($tableIndex->getColumns()); + if (!is_array($tableIndex) || empty($tableIndex['columns'])) { + continue; + } + + if (($tableIndex['type'] ?? null) === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { + $table->addUniqueIndex(array_values($tableIndex['columns'])); } else { - $table->addIndex($tableIndex->getColumns()); + $table->addIndex(array_values($tableIndex['columns'])); } } } @@ -269,14 +270,15 @@ protected function createLocalizedTableSchema(IndexInterface $index, Schema $tab } if (array_key_exists('localizedIndexes', $index->getConfiguration())) { - /** - * @var TableIndex $tableIndex - */ foreach ($index->getConfiguration()['localizedIndexes'] as $tableIndex) { - if ($tableIndex->getType() === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { - $table->addUniqueIndex($tableIndex->getColumns()); + if (!is_array($tableIndex) || empty($tableIndex['columns'])) { + continue; + } + + if (($tableIndex['type'] ?? null) === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { + $table->addUniqueIndex(array_values($tableIndex['columns'])); } else { - $table->addIndex($tableIndex->getColumns()); + $table->addIndex(array_values($tableIndex['columns'])); } } } From afa2d7a740fb492f35b67096589f48d347ff48e9 Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Wed, 22 Apr 2026 20:40:20 +0200 Subject: [PATCH 3/4] [Index] force column type selection on add via edit modal --- .../indexes/components/ColumnsPanel.tsx | 31 ++++++++++++------- .../indexes/components/FieldEditModal.tsx | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/ColumnsPanel.tsx b/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/ColumnsPanel.tsx index 37400de2eb..2373526123 100644 --- a/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/ColumnsPanel.tsx +++ b/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/ColumnsPanel.tsx @@ -69,13 +69,16 @@ export const ColumnsPanel: React.FC = ({ return } - // Create new column with defaults - const newColumn: IndexColumn = { + // `field.dataType` is the Pimcore ClassDefinition fieldtype (e.g. 'input', 'numeric'), + // not a valid index column type. Open the edit modal with the draft column and let the + // user pick a valid index column type (STRING, INTEGER, …). The column is only appended + // once the user clicks Apply (see handleSaveField with editingIndex === null). + const draftColumn: IndexColumn = { name: field.name || field.objectKey || '', objectKey: field.objectKey || '', objectType: field.objectType, dataType: field.dataType, - columnType: field.dataType || 'TEXT', + columnType: undefined, getter: field.getter, getterConfig: field.configuration, interpreter: field.interpreter, @@ -83,10 +86,9 @@ export const ColumnsPanel: React.FC = ({ configuration: field.configuration } - onChange({ - ...index, - columns: [...columns, newColumn] - }) + setEditingColumn(draftColumn) + setEditingIndex(null) + setDialogVisible(true) } const handleDrop = (info: DragAndDropInfo) => { @@ -102,14 +104,21 @@ export const ColumnsPanel: React.FC = ({ } const handleSaveField = (updatedColumn: IndexColumn) => { - if (editingIndex !== null) { - const newColumns = [...columns] - newColumns[editingIndex] = updatedColumn + if (editingIndex === null) { + // New field — append onChange({ ...index, - columns: newColumns + columns: [...columns, updatedColumn] }) + return } + + const newColumns = [...columns] + newColumns[editingIndex] = updatedColumn + onChange({ + ...index, + columns: newColumns + }) } const handleCloseModal = () => { diff --git a/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/FieldEditModal.tsx b/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/FieldEditModal.tsx index 4502a84ae7..09bcb3c923 100644 --- a/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/FieldEditModal.tsx +++ b/src/CoreShop/Bundle/IndexBundle/Resources/assets/pimcore-studio/src/modules/indexes/components/FieldEditModal.tsx @@ -56,7 +56,7 @@ export const FieldEditModal: React.FC = ({ name: field.name || '', getter: field.getter || undefined, interpreter: field.interpreter || undefined, - columnType: field.columnType || 'INTEGER' + columnType: field.columnType || undefined }) setSelectedGetter(field.getter) setSelectedInterpreter(field.interpreter) From 7ab6afd131a1ff9ba4d6115ffa352d823fcb9373 Mon Sep 17 00:00:00 2001 From: Dominik Pfaffenbauer Date: Thu, 23 Apr 2026 07:54:40 +0200 Subject: [PATCH 4/4] [Index] accept TableIndex object and array in MysqlWorker config, fix Psalm null-name --- .../Service/IndexableClassesProvider.php | 10 +++- .../Bundle/IndexBundle/Worker/MysqlWorker.php | 51 ++++++++++++++++--- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php b/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php index 2984661466..8f47bf9aad 100644 --- a/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php +++ b/src/CoreShop/Bundle/IndexBundle/Service/IndexableClassesProvider.php @@ -33,14 +33,20 @@ public function getIndexableClassNames(): array continue; } - $pimcoreClass = 'Pimcore\\Model\\DataObject\\' . ucfirst($class->getName()); + $name = $class->getName(); + + if (null === $name || '' === $name) { + continue; + } + + $pimcoreClass = 'Pimcore\\Model\\DataObject\\' . ucfirst($name); if (!class_exists($pimcoreClass)) { continue; } if (in_array(IndexableInterface::class, class_implements($pimcoreClass) ?: [], true)) { - $result[] = $class->getName(); + $result[] = $name; } } diff --git a/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php b/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php index 1643e2cb04..d849b9016f 100644 --- a/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php +++ b/src/CoreShop/Bundle/IndexBundle/Worker/MysqlWorker.php @@ -208,14 +208,16 @@ protected function createTableSchema(IndexInterface $index, Schema $tableSchema) if (array_key_exists('indexes', $index->getConfiguration())) { foreach ($index->getConfiguration()['indexes'] as $tableIndex) { - if (!is_array($tableIndex) || empty($tableIndex['columns'])) { + $normalized = $this->normalizeTableIndex($tableIndex); + + if (null === $normalized) { continue; } - if (($tableIndex['type'] ?? null) === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { - $table->addUniqueIndex(array_values($tableIndex['columns'])); + if ($normalized['type'] === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { + $table->addUniqueIndex($normalized['columns']); } else { - $table->addIndex(array_values($tableIndex['columns'])); + $table->addIndex($normalized['columns']); } } } @@ -271,14 +273,16 @@ protected function createLocalizedTableSchema(IndexInterface $index, Schema $tab if (array_key_exists('localizedIndexes', $index->getConfiguration())) { foreach ($index->getConfiguration()['localizedIndexes'] as $tableIndex) { - if (!is_array($tableIndex) || empty($tableIndex['columns'])) { + $normalized = $this->normalizeTableIndex($tableIndex); + + if (null === $normalized) { continue; } - if (($tableIndex['type'] ?? null) === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { - $table->addUniqueIndex(array_values($tableIndex['columns'])); + if ($normalized['type'] === TableIndex::TABLE_INDEX_TYPE_UNIQUE) { + $table->addUniqueIndex($normalized['columns']); } else { - $table->addIndex(array_values($tableIndex['columns'])); + $table->addIndex($normalized['columns']); } } } @@ -286,6 +290,37 @@ protected function createLocalizedTableSchema(IndexInterface $index, Schema $tab return $tableSchema; } + /** + * Accept both {@see TableIndex} objects (legacy programmatic setup) and plain arrays + * (the form + JSON-stored configuration format). Returns null for empty/invalid entries. + * + * @return array{type: string|null, columns: array}|null + */ + private function normalizeTableIndex(mixed $tableIndex): ?array + { + if ($tableIndex instanceof TableIndex) { + $columns = $tableIndex->getColumns(); + + if (empty($columns)) { + return null; + } + + return [ + 'type' => $tableIndex->getType(), + 'columns' => array_values($columns), + ]; + } + + if (is_array($tableIndex) && !empty($tableIndex['columns']) && is_array($tableIndex['columns'])) { + return [ + 'type' => $tableIndex['type'] ?? null, + 'columns' => array_values($tableIndex['columns']), + ]; + } + + return null; + } + protected function createRelationalTableSchema(IndexInterface $index, Schema $tableSchema) { $table = $tableSchema->createTable($this->getRelationTablename($index->getName()));