diff --git a/src/CoreBundle/DataProvider/AbstractAttrTypeDataProvider.php b/src/CoreBundle/DataProvider/AbstractAttrTypeDataProvider.php new file mode 100644 index 000000000..127180f23 --- /dev/null +++ b/src/CoreBundle/DataProvider/AbstractAttrTypeDataProvider.php @@ -0,0 +1,284 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +namespace MetaModels\CoreBundle\DataProvider; + +use ContaoCommunityAlliance\DcGeneral\Data\ConfigInterface; +use ContaoCommunityAlliance\DcGeneral\Data\DefaultDataProvider; +use ContaoCommunityAlliance\DcGeneral\Data\DefaultFilterOptionCollection; +use ContaoCommunityAlliance\DcGeneral\Data\FilterOptionCollectionInterface; + +/** + * Abstract base class for data providers that handle virtual panel properties mapped via attr_id. + * + * Virtual properties (no real database column): + * - attr_type (filter) → tl_metamodel_attribute.type via attr_id + * - attr_name (search) → tl_metamodel_attribute.name via attr_id + * - attr_colname (search) → tl_metamodel_attribute.colname via attr_id + * + * Each is rewritten as an "attr_id IN (...)" subquery before the SQL is executed. + * + * Concrete subclasses only need to implement {@see getMetaModelIdFromParentId()} to resolve + * the MetaModel ID from a parent record ID, since the parent table differs per use-case. + */ +abstract class AbstractAttrTypeDataProvider extends DefaultDataProvider +{ + /** + * Virtual properties searchable via LIKE → column in tl_metamodel_attribute. + * + * @psalm-suppress MissingClassConstType + * @var array + */ + private const VIRTUAL_SEARCH_MAP = [ + 'attr_type' => 'type', + 'attr_name' => 'name', + 'attr_colname' => 'colname', + ]; + + /** + * Look up the MetaModel ID for a given parent record ID. + * + * Implementations query the appropriate parent table (e.g. tl_metamodel_dca or + * tl_metamodel_rendersettings) to find the pid of that record, which is the MetaModel ID. + */ + abstract protected function getMetaModelIdFromParentId(int $parentId): ?int; + + /** + * {@inheritDoc} + */ + #[\Override] + public function fetchAll(ConfigInterface $config) + { + $filter = $config->getFilter(); + if (null !== $filter && [] !== $filter) { + $config->setFilter($this->rewriteVirtualConditions($filter)); + } + + return parent::fetchAll($config); + } + + /** + * {@inheritDoc} + */ + #[\Override] + public function getFilterOptions(ConfigInterface $config) + { + $fields = $config->getFields(); + if (null !== $fields && 1 === \count($fields) && 'attr_type' === $fields[0]) { + return $this->buildAttrTypeOptions($config); + } + + return parent::getFilterOptions($config); + } + + // ------------------------------------------------------------------------- + // Filter rewriting + // ------------------------------------------------------------------------- + + /** + * Walk the entire filter tree and replace every virtual condition with a + * real "attr_id IN (...)" condition. + * + * @param array $filter + * + * @return list> + */ + private function rewriteVirtualConditions(array $filter): array + { + $result = []; + + foreach ($filter as $condition) { + if (!\is_array($condition)) { + continue; + } + + $operation = (string) ($condition['operation'] ?? ''); + $property = (string) ($condition['property'] ?? ''); + + if ('=' === $operation && 'attr_type' === $property) { + $result[] = $this->buildAttrTypeEqualsCondition((string) $condition['value'], $filter); + continue; + } + + if ('LIKE' === $operation && isset(self::VIRTUAL_SEARCH_MAP[$property])) { + $result[] = $this->buildVirtualLikeCondition($property, (string) $condition['value']); + continue; + } + + if (\in_array($operation, ['AND', 'OR'], true) && \is_array($condition['children'] ?? null)) { + $condition['children'] = $this->rewriteVirtualConditions($condition['children']); + } + + $result[] = $condition; + } + + return $result; + } + + /** + * Build the rewritten condition for an attr_type equality match. + * + * @param array $filter + * + * @return array + */ + private function buildAttrTypeEqualsCondition(string $type, array $filter): array + { + $parentId = $this->extractPropertyValue($filter, 'pid'); + $attrIds = $this->getAttributeIdsByType($type, $parentId); + + return [] === $attrIds + ? ['operation' => '=', 'property' => 'id', 'value' => -1] + : ['operation' => 'IN', 'property' => 'attr_id', 'values' => $attrIds]; + } + + /** + * Build the rewritten condition for a virtual property LIKE match. + * + * @return array + */ + private function buildVirtualLikeCondition(string $property, string $value): array + { + $column = self::VIRTUAL_SEARCH_MAP[$property]; + $attrIds = $this->getAttributeIdsByLike($column, $value); + + return [] === $attrIds + ? ['operation' => '=', 'property' => 'id', 'value' => -1] + : ['operation' => 'IN', 'property' => 'attr_id', 'values' => $attrIds]; + } + + // ------------------------------------------------------------------------- + // Filter options for the attr_type dropdown + // ------------------------------------------------------------------------- + + /** + * Build the filter option collection for the virtual 'attr_type' property. + */ + private function buildAttrTypeOptions(ConfigInterface $config): FilterOptionCollectionInterface + { + $collection = new DefaultFilterOptionCollection(); + $parentId = $this->extractPropertyValue($config->getFilter() ?? [], 'pid'); + if (null === $parentId) { + return $collection; + } + + $metaModelId = $this->getMetaModelIdFromParentId((int) $parentId); + if (null === $metaModelId) { + return $collection; + } + + $types = $this->connection + ->createQueryBuilder() + ->select('DISTINCT type') + ->from('tl_metamodel_attribute') + ->where('pid = :pid') + ->setParameter('pid', $metaModelId) + ->orderBy('type') + ->executeQuery() + ->fetchFirstColumn(); + + foreach ($types as $type) { + $collection->add($type, $type); + } + + return $collection; + } + + // ------------------------------------------------------------------------- + // Database helpers + // ------------------------------------------------------------------------- + + /** + * Recursively scan a filter array for the first '= X' value on the given property. + * + * @param array $filter + */ + private function extractPropertyValue(array $filter, string $property): mixed + { + foreach ($filter as $condition) { + if (!\is_array($condition)) { + continue; + } + + $operation = (string) ($condition['operation'] ?? ''); + + if ('=' === $operation && $property === ($condition['property'] ?? '')) { + return $condition['value']; + } + + if (\in_array($operation, ['AND', 'OR'], true) && \is_array($condition['children'] ?? null)) { + $value = $this->extractPropertyValue($condition['children'], $property); + if (null !== $value) { + return $value; + } + } + } + + return null; + } + + /** + * Return attribute IDs matching a given type within the MetaModel of a parent record. + * + * @return list + */ + private function getAttributeIdsByType(string $type, mixed $parentId): array + { + if (null === $parentId) { + return []; + } + + $metaModelId = $this->getMetaModelIdFromParentId((int) $parentId); + if (null === $metaModelId) { + return []; + } + + return $this->connection + ->createQueryBuilder() + ->select('id') + ->from('tl_metamodel_attribute') + ->where('type = :type AND pid = :pid') + ->setParameter('type', $type) + ->setParameter('pid', $metaModelId) + ->executeQuery() + ->fetchFirstColumn(); + } + + /** + * Return attribute IDs whose $column matches a LIKE pattern (DC-General wildcard syntax: * → %). + * + * The pid filter is intentionally omitted: the outer pid = parentId condition already restricts + * the result set to the correct MetaModel, so a cross-MetaModel match has no effect. + * + * @return list + */ + private function getAttributeIdsByLike(string $column, string $wildcardValue): array + { + $sqlPattern = \str_replace(['*', '?'], ['%', '_'], $wildcardValue); + + return $this->connection + ->createQueryBuilder() + ->select('id') + ->from('tl_metamodel_attribute') + ->where($column . ' LIKE :pattern') + ->setParameter('pattern', $sqlPattern) + ->executeQuery() + ->fetchFirstColumn(); + } +} diff --git a/src/CoreBundle/DataProvider/DcaSettingAttrTypeDataProvider.php b/src/CoreBundle/DataProvider/DcaSettingAttrTypeDataProvider.php new file mode 100644 index 000000000..a090c5436 --- /dev/null +++ b/src/CoreBundle/DataProvider/DcaSettingAttrTypeDataProvider.php @@ -0,0 +1,46 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +namespace MetaModels\CoreBundle\DataProvider; + +/** + * Data provider for tl_metamodel_dcasetting that handles virtual panel properties. + * + * Resolves the MetaModel ID via tl_metamodel_dca.pid. + */ +final class DcaSettingAttrTypeDataProvider extends AbstractAttrTypeDataProvider +{ + /** + * {@inheritDoc} + */ + #[\Override] + protected function getMetaModelIdFromParentId(int $parentId): ?int + { + $result = $this->connection + ->createQueryBuilder() + ->select('pid') + ->from('tl_metamodel_dca') + ->where('id = :id') + ->setParameter('id', $parentId) + ->executeQuery() + ->fetchOne(); + + return false === $result ? null : (int) $result; + } +} diff --git a/src/CoreBundle/DataProvider/RenderSettingAttrTypeDataProvider.php b/src/CoreBundle/DataProvider/RenderSettingAttrTypeDataProvider.php new file mode 100644 index 000000000..77ac1c7c0 --- /dev/null +++ b/src/CoreBundle/DataProvider/RenderSettingAttrTypeDataProvider.php @@ -0,0 +1,46 @@ + + * @copyright 2012-2026 The MetaModels team. + * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later + * @filesource + */ + +namespace MetaModels\CoreBundle\DataProvider; + +/** + * Data provider for tl_metamodel_rendersetting that handles virtual panel properties. + * + * Resolves the MetaModel ID via tl_metamodel_rendersettings.pid. + */ +final class RenderSettingAttrTypeDataProvider extends AbstractAttrTypeDataProvider +{ + /** + * {@inheritDoc} + */ + #[\Override] + protected function getMetaModelIdFromParentId(int $parentId): ?int + { + $result = $this->connection + ->createQueryBuilder() + ->select('pid') + ->from('tl_metamodel_rendersettings') + ->where('id = :id') + ->setParameter('id', $parentId) + ->executeQuery() + ->fetchOne(); + + return false === $result ? null : (int) $result; + } +} diff --git a/src/CoreBundle/Resources/contao/dca/tl_metamodel_dcasetting.php b/src/CoreBundle/Resources/contao/dca/tl_metamodel_dcasetting.php index c44095bf5..7d4e1b2ef 100644 --- a/src/CoreBundle/Resources/contao/dca/tl_metamodel_dcasetting.php +++ b/src/CoreBundle/Resources/contao/dca/tl_metamodel_dcasetting.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2025 The MetaModels team. + * (c) 2012-2026 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -21,12 +21,13 @@ * @author Cliff Parnitzky * @author Sven Baumann * @author Ingolf Steinhardt - * @copyright 2012-2025 The MetaModels team. + * @copyright 2012-2026 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ use ContaoCommunityAlliance\DcGeneral\DC\General; +use MetaModels\CoreBundle\DataProvider\DcaSettingAttrTypeDataProvider; $GLOBALS['TL_DCA']['tl_metamodel_dcasetting'] = [ 'config' => [ @@ -43,7 +44,8 @@ 'dca_config' => [ 'data_provider' => [ 'root' => [ - 'source' => 'tl_metamodel_dcasetting' + 'source' => 'tl_metamodel_dcasetting', + 'class' => DcaSettingAttrTypeDataProvider::class, ], 'parent' => [ 'source' => 'tl_metamodel_dca' @@ -135,7 +137,7 @@ 'sorting' => [ 'mode' => 4, 'fields' => ['sorting'], - 'panelLayout' => 'limit', + 'panelLayout' => 'filter;search;limit', 'headerFields' => ['name'], ], 'global_operations' => [ @@ -522,6 +524,39 @@ 'tl_class' => 'w50 cbx m12', ], 'sql' => "char(1) NOT NULL default ''" - ] + ], + 'attr_type' => [ + 'label' => 'attr_type.label', + 'description' => 'attr_type.description', + 'exclude' => true, + 'inputType' => 'select', + 'eval' => [ + 'includeBlankOption' => true, + 'tl_class' => 'w50', + 'chosen' => true, + ], + 'filter' => true, + 'search' => true, + ], + 'attr_name' => [ + 'label' => 'attr_name.label', + 'description' => 'attr_name.description', + 'exclude' => true, + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + ], + 'search' => true, + ], + 'attr_colname' => [ + 'label' => 'attr_colname.label', + 'description' => 'attr_colname.description', + 'exclude' => true, + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + ], + 'search' => true, + ], ] ]; diff --git a/src/CoreBundle/Resources/contao/dca/tl_metamodel_rendersetting.php b/src/CoreBundle/Resources/contao/dca/tl_metamodel_rendersetting.php index 6447f60d6..9318176f1 100644 --- a/src/CoreBundle/Resources/contao/dca/tl_metamodel_rendersetting.php +++ b/src/CoreBundle/Resources/contao/dca/tl_metamodel_rendersetting.php @@ -3,7 +3,7 @@ /** * This file is part of MetaModels/core. * - * (c) 2012-2024 The MetaModels team. + * (c) 2012-2026 The MetaModels team. * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. @@ -20,12 +20,13 @@ * @author Sven Baumann * @author Ingolf Steinhardt * @author David Molineus - * @copyright 2012-2024 The MetaModels team. + * @copyright 2012-2026 The MetaModels team. * @license https://github.com/MetaModels/core/blob/master/LICENSE LGPL-3.0-or-later * @filesource */ use ContaoCommunityAlliance\DcGeneral\DC\General; +use MetaModels\CoreBundle\DataProvider\RenderSettingAttrTypeDataProvider; $GLOBALS['TL_DCA']['tl_metamodel_rendersetting'] = [ 'config' => [ @@ -43,7 +44,8 @@ 'dca_config' => [ 'data_provider' => [ 'default' => [ - 'source' => 'tl_metamodel_rendersetting' + 'source' => 'tl_metamodel_rendersetting', + 'class' => RenderSettingAttrTypeDataProvider::class ], 'parent' => [ 'source' => 'tl_metamodel_rendersettings' @@ -118,7 +120,7 @@ 'sorting' => [ 'mode' => 4, 'fields' => ['sorting'], - 'panelLayout' => 'limit', + 'panelLayout' => 'filter;search;limit', 'headerFields' => ['name'], ], 'global_operations' => [ @@ -246,6 +248,39 @@ 'description' => 'enabled.description', 'default' => 1, 'sql' => "char(1) NOT NULL default ''" - ] + ], + 'attr_type' => [ + 'label' => 'attr_type.label', + 'description' => 'attr_type.description', + 'exclude' => true, + 'inputType' => 'select', + 'eval' => [ + 'includeBlankOption' => true, + 'tl_class' => 'w50', + 'chosen' => true, + ], + 'filter' => true, + 'search' => true, + ], + 'attr_name' => [ + 'label' => 'attr_name.label', + 'description' => 'attr_name.description', + 'exclude' => true, + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + ], + 'search' => true, + ], + 'attr_colname' => [ + 'label' => 'attr_colname.label', + 'description' => 'attr_colname.description', + 'exclude' => true, + 'inputType' => 'text', + 'eval' => [ + 'tl_class' => 'w50', + ], + 'search' => true, + ], ] ]; diff --git a/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.de.xlf b/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.de.xlf index d78ed3dbc..71c3b9862 100644 --- a/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.de.xlf +++ b/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.de.xlf @@ -117,6 +117,30 @@ Attribute this setting relates to. Attribut, auf das sich diese Einstellung bezieht. + + Attribute type + Attribut-Typ + + + Attribute type + Filtert die Liste nach dem Typ des referenzierten Attributs. + + + Name + Name + + + Search by the name of the referenced attribute. + Suche nach dem Namen des referenzierten Attributs. + + + Column name + Spaltenname + + + Search by the column name of the referenced attribute. + Suche nach dem Spaltennamen des referenzierten Attributs. + Custom template to use for generating Angepasstes Template für die Ausgabe @@ -549,4 +573,4 @@ - \ No newline at end of file + diff --git a/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.en.xlf b/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.en.xlf index f19c35cbc..5dee9627d 100644 --- a/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.en.xlf +++ b/src/CoreBundle/Resources/translations/tl_metamodel_dcasetting.en.xlf @@ -6,6 +6,24 @@ All input screen settings + + Attribute type + + + Filters the list by the type of the referenced attribute. + + + Name + + + Search by the name of the referenced attribute. + + + Column name + + + Search by the column name of the referenced attribute. + Id diff --git a/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.de.xlf b/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.de.xlf index ea3033abe..584577a65 100644 --- a/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.de.xlf +++ b/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.de.xlf @@ -97,6 +97,30 @@ Attribute this setting relates to. Attribut, auf das sich diese Einstellung bezieht. + + Attribute type + Attribut-Typ + + + Filters the list by the type of the referenced attribute. + Filtert die Liste nach dem Typ des referenzierten Attributs. + + + Name + Name + + + Search by the name of the referenced attribute. + Suche nach dem Namen des referenzierten Attributs. + + + Column name + Spaltenname + + + Search by the column name of the referenced attribute. + Suche nach dem Spaltennamen des referenzierten Attributs. + Template to use for generating Template für die Ausgabe diff --git a/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.en.xlf b/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.en.xlf index 427c67e78..092c7fe98 100644 --- a/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.en.xlf +++ b/src/CoreBundle/Resources/translations/tl_metamodel_rendersetting.en.xlf @@ -75,6 +75,24 @@ Attribute this setting relates to. + + Attribute type + + + Filters the list by the type of the referenced attribute. + + + Name + + + Search by the name of the referenced attribute. + + + Column name + + + Search by the column name of the referenced attribute. + Template to use for generating