diff --git a/Classes/Hook/CreateMaps2RecordHook.php b/Classes/Hook/CreateMaps2RecordHook.php index cf71849b1..d914a5c2a 100644 --- a/Classes/Hook/CreateMaps2RecordHook.php +++ b/Classes/Hook/CreateMaps2RecordHook.php @@ -20,6 +20,7 @@ use JWeiland\Maps2\Helper\StoragePidHelper; use JWeiland\Maps2\Service\GeoCodeService; use JWeiland\Maps2\Service\MapService; +use JWeiland\Maps2\Tca\ForeignColumn; use JWeiland\Maps2\Tca\Maps2Registry; use Psr\EventDispatcher\EventDispatcherInterface; use TYPO3\CMS\Backend\Utility\BackendUtility; @@ -483,13 +484,14 @@ public function synchronizeColumnsFromForeignRecordWithPoiCollection( $tableNeedsUpdate = false; foreach ($columnOptions['synchronizeColumns'] as $synchronizeColumns) { - if (!$this->isValidSynchronizeConfiguration($synchronizeColumns, $foreignTableName)) { + $foreignColumn = $this->getForeignColumnFromSynchronizeConfiguration($synchronizeColumns, $foreignTableName); + if (!$foreignColumn instanceof ForeignColumn) { return false; } $queryBuilder = $queryBuilder->set( $synchronizeColumns['poiCollectionColumnName'], - $foreignLocationRecord[$synchronizeColumns['foreignColumnName']], + $foreignColumn->resolveValue($foreignLocationRecord), ); $tableNeedsUpdate = true; } @@ -506,15 +508,17 @@ public function synchronizeColumnsFromForeignRecordWithPoiCollection( } /** - * This method checks the synchronization options itself and if columns are configured in TCA + * Builds the ForeignColumn value object for a single `synchronizeColumns` entry and validates + * it against the TCA of the foreign table. `foreignColumnName` may either be a plain column name + * or a coalesce/concat configuration array resolving several columns of the foreign table, see + * ForeignColumn::createFromConfiguration(). */ - protected function isValidSynchronizeConfiguration(array $synchronizeColumns, string $foreignTableName): bool + protected function getForeignColumnFromSynchronizeConfiguration(array $synchronizeColumns, string $foreignTableName): ?ForeignColumn { // Check options itself if ( !array_key_exists('foreignColumnName', $synchronizeColumns) || !array_key_exists('poiCollectionColumnName', $synchronizeColumns) - || !is_string($synchronizeColumns['foreignColumnName']) || !is_string($synchronizeColumns['poiCollectionColumnName']) ) { $this->messageHelper->addFlashMessage( @@ -523,26 +527,38 @@ protected function isValidSynchronizeConfiguration(array $synchronizeColumns, st ContextualFeedbackSeverity::ERROR, ); - return false; + return null; } - // Check, if configured foreign columnName is valid in TCA - $foreignColumnName = $synchronizeColumns['foreignColumnName']; - if ( - !array_key_exists($foreignTableName, $GLOBALS['TCA']) - || !array_key_exists($foreignColumnName, $GLOBALS['TCA'][$foreignTableName]['columns']) - || !is_array($GLOBALS['TCA'][$foreignTableName]['columns'][$foreignColumnName]['config']) - ) { + $foreignColumn = ForeignColumn::createFromConfiguration($synchronizeColumns['foreignColumnName']); + if (!$foreignColumn instanceof ForeignColumn) { $this->messageHelper->addFlashMessage( - 'Error while trying to synchronize columns of your record with maps2 record. It seems that "' . $foreignTableName . '" is not registered as table or "' . $foreignColumnName . '" is not a valid column in ' . $foreignTableName, - 'Missing table/column in TCA', + 'Please check your Maps registration. The key foreignColumnName has to be a column name or a valid coalesce/concat configuration.', + 'Missing registration keys', ContextualFeedbackSeverity::ERROR, ); - return false; + return null; } - return true; + // Check, if every configured foreign columnName is valid in TCA + foreach ($foreignColumn->getColumnNames() as $foreignColumnName) { + if ( + !array_key_exists($foreignTableName, $GLOBALS['TCA']) + || !array_key_exists($foreignColumnName, $GLOBALS['TCA'][$foreignTableName]['columns']) + || !is_array($GLOBALS['TCA'][$foreignTableName]['columns'][$foreignColumnName]['config']) + ) { + $this->messageHelper->addFlashMessage( + 'Error while trying to synchronize columns of your record with maps2 record. It seems that "' . $foreignTableName . '" is not registered as table or "' . $foreignColumnName . '" is not a valid column in ' . $foreignTableName, + 'Missing table/column in TCA', + ContextualFeedbackSeverity::ERROR, + ); + + return null; + } + } + + return $foreignColumn; } /** diff --git a/Classes/Tca/ForeignColumn.php b/Classes/Tca/ForeignColumn.php new file mode 100644 index 000000000..e1d453ee9 --- /dev/null +++ b/Classes/Tca/ForeignColumn.php @@ -0,0 +1,154 @@ + self::createFromConfiguration($column), + $columns, + ))); + } + + public function resolveValue(array $record): string + { + return match ($this->resolveType) { + ForeignColumnResolveTypeEnum::SINGLE => trim((string)($record[$this->columnName] ?? '')), + ForeignColumnResolveTypeEnum::COALESCE => $this->resolveCoalesce($record), + ForeignColumnResolveTypeEnum::CONCAT => $this->resolveConcat($record), + }; + } + + /** + * Flat list of every column name involved in this (possibly nested) configuration. + * Used to validate against TCA of the foreign table. + * + * @return string[] + */ + public function getColumnNames(): array + { + if ($this->resolveType === ForeignColumnResolveTypeEnum::SINGLE) { + return [$this->columnName]; + } + + $columnNames = []; + foreach ($this->children as $child) { + $columnNames[] = $child->getColumnNames(); + } + + return array_values(array_unique(array_merge([], ...$columnNames))); + } + + private function resolveCoalesce(array $record): string + { + foreach ($this->children as $child) { + $value = $child->resolveValue($record); + if ($value !== '') { + return $value; + } + } + + return ''; + } + + private function resolveConcat(array $record): string + { + $values = array_filter( + array_map( + static fn(self $child): string => $child->resolveValue($record), + $this->children, + ), + static fn(string $value): bool => $value !== '', + ); + + return implode($this->glue, $values); + } +} diff --git a/Classes/Tca/ForeignColumnResolveTypeEnum.php b/Classes/Tca/ForeignColumnResolveTypeEnum.php new file mode 100644 index 000000000..f6274f51b --- /dev/null +++ b/Classes/Tca/ForeignColumnResolveTypeEnum.php @@ -0,0 +1,19 @@ + [ + [ + 'foreignColumnName' => [ + 'type' => 'coalesce', + 'columns' => [ + 'company', + 'name', + ], + ], + 'poiCollectionColumnName' => 'title', + ], + ], + +.. _developer-maps2registry-synchronize-columns-concat: + +Concat: Combine Multiple Columns +--------------------------------- + +Use `type` `concat` if you want maps2 to join several columns into a single +value, separated by `glue` (defaults to a single space): + +.. code-block:: php + :caption: EXT:my_ext/Configuration/TCA/Overrides/tt_address.php + + 'synchronizeColumns' => [ + [ + 'foreignColumnName' => [ + 'type' => 'concat', + 'columns' => [ + 'first_name', + 'last_name', + ], + 'glue' => ' ', + ], + 'poiCollectionColumnName' => 'title', + ], + ], + +Empty columns are skipped automatically, so a record with only a `last_name` +will not end up with a leading `glue` in the synchronized value. See +:ref:`developer-maps2registry-synchronize-columns-resolution-rules` below +for the exact rules governing empty and blank columns. + +.. _developer-maps2registry-synchronize-columns-nesting: + +Nesting Coalesce and Concat +---------------------------- + +Every entry of `columns` may again be a plain column name or a nested +`coalesce`/`concat` configuration. This allows you to combine both +strategies, for example: prefer the `company` column, otherwise fall back to +the concatenated `first_name` and `last_name` columns: + +.. code-block:: php + :caption: EXT:my_ext/Configuration/TCA/Overrides/tt_address.php + + 'synchronizeColumns' => [ + [ + 'foreignColumnName' => [ + 'type' => 'coalesce', + 'columns' => [ + 'company', + [ + 'type' => 'concat', + 'columns' => [ + 'first_name', + 'last_name', + ], + 'glue' => ' ', + ], + ], + ], + 'poiCollectionColumnName' => 'title', + ], + ], + +Nesting works symmetrically and at any depth: a `concat` may just as well +contain a nested `coalesce` (e.g. `concat(first_name, coalesce(company, +last_name))`), and that combination may again be nested inside another +`coalesce` or `concat`. How an empty nested `concat` is treated by a +surrounding `coalesce` is described in +:ref:`developer-maps2registry-synchronize-columns-resolution-rules` below. + +.. _developer-maps2registry-synchronize-columns-shorthand: + +Shorthand: Plain List as Coalesce +----------------------------------- + +A plain, non-associative list of column names and/or nested configurations +(i.e. without `type`/`columns`) is automatically treated as a `coalesce`: +maps2 tries every entry in order and uses the first one that resolves to a +non-empty value. This is handy to fall back from one combined strategy to +another one, for example: prefer `company` or `name`, otherwise fall back to +the concatenated `first_name` and `last_name` columns: + +.. code-block:: php + :caption: EXT:my_ext/Configuration/TCA/Overrides/tt_address.php + + 'synchronizeColumns' => [ + [ + 'foreignColumnName' => [ + [ + 'type' => 'coalesce', + 'columns' => ['company', 'name'], + ], + [ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + ], + ], + 'poiCollectionColumnName' => 'title', + ], + ], + +`type` also accepts a :php:`\JWeiland\Maps2\Tca\ForeignColumnResolveTypeEnum` +case directly instead of its string value, if you prefer a typed constant +over a magic string. + +.. _developer-maps2registry-synchronize-columns-resolution-rules: + +Value Resolution Rules +----------------------- + +The following rules apply consistently to every example above, no matter +how deeply `coalesce` and `concat` are nested: + +#. Every column value is trimmed before it is evaluated. A column that + only contains whitespace (e.g. `" "` or a tab) is treated exactly + like an empty column. + +#. `concat` filters out empty columns *before* joining them with `glue`, + it does not join first and clean up afterwards. An empty column can + therefore never produce a stray `glue` in the result, neither at the + start, the end, nor in the middle. If *every* column of a `concat` is + empty, the whole `concat` resolves to an empty string regardless of + `glue`: a `, ` glue with all columns empty results in `''`, not in + `", , ,"`. + +#. `coalesce` only checks whether a candidate resolved to a non-empty + string. A nested `concat` that resolves to an empty string (per rule 2) + is therefore treated like any other empty candidate: `coalesce` simply + moves on to the next entry of `columns`. diff --git a/Documentation/guides.xml b/Documentation/guides.xml index 9b426ab5e..9eb2cc5ce 100644 --- a/Documentation/guides.xml +++ b/Documentation/guides.xml @@ -9,5 +9,5 @@ project-issues="https://github.com/jweiland-net/maps2/issues" edit-on-github-branch="main" edit-on-github="jweiland/maps2" typo3-core-preferred="stable"/> - + diff --git a/Tests/Functional/Hook/CreateMaps2RecordHookTest.php b/Tests/Functional/Hook/CreateMaps2RecordHookTest.php index 8eba6cea8..cf1016db6 100644 --- a/Tests/Functional/Hook/CreateMaps2RecordHookTest.php +++ b/Tests/Functional/Hook/CreateMaps2RecordHookTest.php @@ -28,6 +28,7 @@ use TYPO3\CMS\Core\Cache\CacheManager; use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend; +use TYPO3\CMS\Core\Database\Connection; use TYPO3\CMS\Core\DataHandling\DataHandler; use TYPO3\CMS\Core\EventDispatcher\EventDispatcher; use TYPO3\CMS\Core\Utility\GeneralUtility; @@ -369,4 +370,62 @@ public function processDatamapCreatesNewPoiCollection(): void $this->subject->processDatamap_afterAllOperations($dataHandler); } + + #[Test] + public function synchronizeColumnsFromForeignRecordWithPoiCollectionResolvesComposedForeignColumn(): void + { + $this->importCSVDataSet(__DIR__ . '/../Fixtures/tx_maps2_domain_model_poicollection.csv'); + + $foreignLocationRecord = [ + 'uid' => 1, + 'location' => '', + 'street' => 'Echterdinger Straße 57', + 'city' => 'Filderstadt', + 'tx_maps2_uid' => 1, + ]; + + $columnOptions = [ + 'synchronizeColumns' => [ + [ + // Prefer the location title. If empty, fall back to the concatenated street and city. + 'foreignColumnName' => [ + 'type' => 'coalesce', + 'columns' => [ + 'location', + [ + 'type' => 'concat', + 'columns' => ['street', 'city'], + 'glue' => ', ', + ], + ], + ], + 'poiCollectionColumnName' => 'title', + ], + ], + ]; + + self::assertTrue( + $this->subject->synchronizeColumnsFromForeignRecordWithPoiCollection( + $foreignLocationRecord, + 'tx_events2_domain_model_location', + 'tx_maps2_uid', + $columnOptions, + ), + ); + + $poiCollectionQueryBuilder = $this->getConnectionPool()->getQueryBuilderForTable('tx_maps2_domain_model_poicollection'); + $poiCollectionTitle = $poiCollectionQueryBuilder + ->select('title') + ->from('tx_maps2_domain_model_poicollection') + ->where( + $poiCollectionQueryBuilder->expr()->eq( + 'uid', + $poiCollectionQueryBuilder->createNamedParameter(1, Connection::PARAM_INT), + ), + ) + ->executeQuery() + ->fetchOne(); + + self::assertSame('Echterdinger Straße 57, Filderstadt', $poiCollectionTitle); + } } diff --git a/Tests/Unit/Tca/ForeignColumnTest.php b/Tests/Unit/Tca/ForeignColumnTest.php new file mode 100644 index 000000000..1e41b0abb --- /dev/null +++ b/Tests/Unit/Tca/ForeignColumnTest.php @@ -0,0 +1,502 @@ +getColumnNames()); + } + + #[Test] + public function createFromConfigurationWithEmptyStringReturnsNull(): void + { + self::assertNull(ForeignColumn::createFromConfiguration(' ')); + } + + #[Test] + public function createFromConfigurationWithInvalidTypeReturnsNull(): void + { + self::assertNull(ForeignColumn::createFromConfiguration([ + 'type' => 'foo', + 'columns' => ['title'], + ])); + } + + #[Test] + public function createFromConfigurationWithSingleTypeReturnsNull(): void + { + self::assertNull(ForeignColumn::createFromConfiguration([ + 'type' => 'single', + 'columns' => ['title'], + ])); + } + + #[Test] + public function createFromConfigurationWithMissingColumnsReturnsNull(): void + { + self::assertNull(ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + ])); + } + + #[Test] + public function createFromConfigurationWithEmptyColumnsReturnsNull(): void + { + self::assertNull(ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => ['', ' '], + ])); + } + + #[Test] + public function createFromConfigurationWithNeitherStringNorArrayReturnsNull(): void + { + self::assertNull(ForeignColumn::createFromConfiguration(123)); + } + + #[Test] + public function resolveValueWithSingleColumnReturnsTrimmedValue(): void + { + $subject = ForeignColumn::createFromConfiguration('title'); + + self::assertSame( + 'jweiland.net', + $subject->resolveValue(['title' => ' jweiland.net ']), + ); + } + + #[Test] + public function resolveValueWithSingleColumnAndMissingKeyReturnsEmptyString(): void + { + $subject = ForeignColumn::createFromConfiguration('title'); + + self::assertSame('', $subject->resolveValue([])); + } + + #[Test] + public function resolveValueWithCoalesceReturnsFirstNonEmptyColumn(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => ['company', 'name'], + ]); + + self::assertSame( + 'ACME Inc.', + $subject->resolveValue([ + 'company' => 'ACME Inc.', + 'name' => 'Stefan', + ]), + ); + } + + #[Test] + public function resolveValueWithCoalesceSkipsEmptyAndBlankColumns(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => ['company', 'name'], + ]); + + self::assertSame( + 'Stefan', + $subject->resolveValue([ + 'company' => ' ', + 'name' => 'Stefan', + ]), + ); + } + + #[Test] + public function resolveValueWithCoalesceAndAllColumnsEmptyReturnsEmptyString(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => ['company', 'name'], + ]); + + self::assertSame('', $subject->resolveValue(['company' => '', 'name' => ''])); + } + + #[Test] + public function resolveValueWithConcatJoinsColumnsWithDefaultGlue(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + ]); + + self::assertSame( + 'Stefan Froemken', + $subject->resolveValue([ + 'first_name' => 'Stefan', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function resolveValueWithConcatUsesConfiguredGlue(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => ['zip', 'city'], + 'glue' => ', ', + ]); + + self::assertSame( + '70794, Filderstadt', + $subject->resolveValue([ + 'zip' => '70794', + 'city' => 'Filderstadt', + ]), + ); + } + + #[Test] + public function resolveValueWithCommaGlueAndAllColumnsEmptyResolvesToEmptyStringWithoutStrayGlue(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => ['street', 'house_number', 'zip', 'city'], + 'glue' => ', ', + ]); + + // Empty candidates are filtered out before imploding, so no leftover + // ", , ," can appear, regardless of how many columns are empty. + self::assertSame( + '', + $subject->resolveValue(['street' => '', 'house_number' => '', 'zip' => '', 'city' => '']), + ); + } + + #[Test] + public function resolveValueWithCommaGlueAndEmptyColumnsInTheMiddleSkipsThem(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => ['street', 'house_number', 'zip', 'city'], + 'glue' => ', ', + ]); + + self::assertSame( + 'Echterdinger Str., Filderstadt', + $subject->resolveValue([ + 'street' => 'Echterdinger Str.', + 'house_number' => '', + 'zip' => '', + 'city' => 'Filderstadt', + ]), + ); + } + + #[Test] + public function resolveValueWithConcatSkipsEmptyColumns(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + ]); + + self::assertSame( + 'Froemken', + $subject->resolveValue([ + 'first_name' => '', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function resolveValueWithNestedCoalesceAndConcatCombinesBothStrategies(): void + { + // company -> name -> concat(first_name, last_name) + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => [ + 'company', + 'name', + [ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + 'glue' => ' ', + ], + ], + ]); + + self::assertSame( + 'Stefan Froemken', + $subject->resolveValue([ + 'company' => '', + 'name' => '', + 'first_name' => 'Stefan', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function resolveValueWithConcatOfWhitespaceOnlyColumnsResolvesToEmptyStringWithoutGlue(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + 'glue' => ' ', + ]); + + // Blank (whitespace-only) columns must be trimmed to '' before the + // emptiness check, so no glue leaks into the result. + self::assertSame( + '', + $subject->resolveValue([ + 'first_name' => ' ', + 'last_name' => "\t\n", + ]), + ); + } + + #[Test] + public function resolveValueWithCoalesceSkipsConcatOfWhitespaceOnlyColumns(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => [ + [ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + 'glue' => ' ', + ], + 'company', + ], + ]); + + self::assertSame( + 'ACME Inc.', + $subject->resolveValue([ + 'first_name' => ' ', + 'last_name' => ' ', + 'company' => 'ACME Inc.', + ]), + ); + } + + #[Test] + public function resolveValueWithConcatContainingNestedCoalesceCombinesBothStrategies(): void + { + // concat(first_name, coalesce(company, last_name)) + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'concat', + 'columns' => [ + 'first_name', + [ + 'type' => 'coalesce', + 'columns' => ['company', 'last_name'], + ], + ], + 'glue' => ' ', + ]); + + self::assertSame( + 'Stefan ACME Inc.', + $subject->resolveValue([ + 'first_name' => 'Stefan', + 'company' => 'ACME Inc.', + 'last_name' => 'Froemken', + ]), + ); + + self::assertSame( + 'Stefan Froemken', + $subject->resolveValue([ + 'first_name' => 'Stefan', + 'company' => '', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function resolveValueWithDeeplyMixedNestingFallsBackWhenInnerConcatResolvesEmpty(): void + { + // coalesce( concat( coalesce(a, b), c ), d ) + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => [ + [ + 'type' => 'concat', + 'columns' => [ + [ + 'type' => 'coalesce', + 'columns' => ['a', 'b'], + ], + 'c', + ], + 'glue' => '-', + ], + 'd', + ], + ]); + + self::assertSame( + ['a', 'b', 'c', 'd'], + $subject->getColumnNames(), + ); + + self::assertSame( + 'A-C', + $subject->resolveValue(['a' => 'A', 'b' => '', 'c' => 'C', 'd' => 'D']), + ); + + // Inner concat resolves to an empty string when a, b and c are all empty, + // so the outer coalesce must fall back to the sibling column "d". + self::assertSame( + 'D', + $subject->resolveValue(['a' => '', 'b' => '', 'c' => '', 'd' => 'D']), + ); + } + + #[Test] + public function resolveValueWithNestedConfigurationPrefersOuterColumnsBeforeFallback(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => [ + 'company', + [ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + ], + ], + ]); + + self::assertSame( + 'ACME Inc.', + $subject->resolveValue([ + 'company' => 'ACME Inc.', + 'first_name' => 'Stefan', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function createFromConfigurationAcceptsResolveTypeEnumInstanceAsType(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => ForeignColumnResolveTypeEnum::CONCAT, + 'columns' => ['first_name', 'last_name'], + ]); + + self::assertSame( + 'Stefan Froemken', + $subject->resolveValue(['first_name' => 'Stefan', 'last_name' => 'Froemken']), + ); + } + + #[Test] + public function createFromConfigurationWithPlainListTreatsItAsImplicitCoalesce(): void + { + $subject = ForeignColumn::createFromConfiguration(['company', 'name']); + + self::assertSame( + 'jweiland.net', + $subject->resolveValue(['company' => '', 'name' => 'jweiland.net']), + ); + } + + #[Test] + public function createFromConfigurationWithPlainListOfNestedConfigurationsFallsBackToNextEntry(): void + { + // Mirrors the real-world case: prefer company or name, otherwise fall back + // to the concatenated first and last name. + $subject = ForeignColumn::createFromConfiguration([ + [ + 'type' => ForeignColumnResolveTypeEnum::COALESCE, + 'columns' => ['company', 'name'], + ], + [ + 'type' => ForeignColumnResolveTypeEnum::CONCAT, + 'columns' => ['first_name', 'last_name'], + ], + ]); + + self::assertSame( + 'Stefan Froemken', + $subject->resolveValue([ + 'company' => '', + 'name' => '', + 'first_name' => 'Stefan', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function createFromConfigurationWithPlainListPrefersFirstNonEmptyGroup(): void + { + $subject = ForeignColumn::createFromConfiguration([ + [ + 'type' => ForeignColumnResolveTypeEnum::COALESCE, + 'columns' => ['company', 'name'], + ], + [ + 'type' => ForeignColumnResolveTypeEnum::CONCAT, + 'columns' => ['first_name', 'last_name'], + ], + ]); + + self::assertSame( + 'ACME Inc.', + $subject->resolveValue([ + 'company' => 'ACME Inc.', + 'name' => '', + 'first_name' => 'Stefan', + 'last_name' => 'Froemken', + ]), + ); + } + + #[Test] + public function getColumnNamesReturnsFlatUniqueListForNestedConfiguration(): void + { + $subject = ForeignColumn::createFromConfiguration([ + 'type' => 'coalesce', + 'columns' => [ + 'company', + 'name', + [ + 'type' => 'concat', + 'columns' => ['first_name', 'last_name'], + ], + ], + ]); + + self::assertSame( + ['company', 'name', 'first_name', 'last_name'], + $subject->getColumnNames(), + ); + } +} diff --git a/ext_emconf.php b/ext_emconf.php index 00a2b2768..598a8c7c5 100644 --- a/ext_emconf.php +++ b/ext_emconf.php @@ -10,7 +10,7 @@ $EM_CONF[$_EXTKEY] = [ 'title' => 'Maps2', 'description' => 'Create maps with Marker, Area, Routes or Radius based on Google Maps or OpenStreetMap', - 'version' => '12.1.2', + 'version' => '12.2.0', 'category' => 'plugin', 'state' => 'stable', 'author' => 'Stefan Froemken',